diff --git a/android/app/build.gradle b/android/app/build.gradle index f33fb0eb15b979060d382fecb1382bca96ff5288..9c525a648e76fe606b309c52018f154ace22949e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -149,6 +149,8 @@ android { } dependencies { + compile project(':nodejs-mobile-react-native') + compile project(':react-native-restart') compile project(':react-native-push-notification') compile project(':react-native-vector-icons') compile project(':react-native-fs') diff --git a/android/app/src/main/java/com/drip/MainApplication.java b/android/app/src/main/java/com/drip/MainApplication.java index da1f79f0f32e9cadcd14ebc8a84617d867c3333e..dc83fe74950ab011cd8191124c54ee2af646c020 100644 --- a/android/app/src/main/java/com/drip/MainApplication.java +++ b/android/app/src/main/java/com/drip/MainApplication.java @@ -3,6 +3,8 @@ package com.drip; import android.app.Application; import com.facebook.react.ReactApplication; +import com.janeasystems.rn_nodejs_mobile.RNNodeJsMobilePackage; +import com.avishayil.rnrestart.ReactNativeRestartPackage; import com.dieam.reactnativepushnotification.ReactNativePushNotificationPackage; import com.oblador.vectoricons.VectorIconsPackage; import com.rnfs.RNFSPackage; @@ -30,6 +32,8 @@ public class MainApplication extends Application implements ReactApplication, Sh protected List<ReactPackage> getPackages() { return Arrays.<ReactPackage>asList( new MainReactPackage(), + new RNNodeJsMobilePackage(), + new ReactNativeRestartPackage(), new ReactNativePushNotificationPackage(), new VectorIconsPackage(), new RNFSPackage(), diff --git a/android/settings.gradle b/android/settings.gradle index fe009fa9e1231ffd0c4187396c2d24a4a417a9e0..327fbe15d242706c1685f18d7b8f6264eb10f452 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,4 +1,8 @@ rootProject.name = 'drip' +include ':nodejs-mobile-react-native' +project(':nodejs-mobile-react-native').projectDir = new File(rootProject.projectDir, '../node_modules/nodejs-mobile-react-native/android') +include ':react-native-restart' +project(':react-native-restart').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-restart/android') include ':react-native-push-notification' project(':react-native-push-notification').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-push-notification/android') include ':react-native-vector-icons' diff --git a/assets/drip_small.png b/assets/drip_small.png new file mode 100644 index 0000000000000000000000000000000000000000..abdcde1a17e2008629a6df73d3f44417d5a0f120 Binary files /dev/null and b/assets/drip_small.png differ diff --git a/components/app-wrapper.js b/components/app-wrapper.js new file mode 100644 index 0000000000000000000000000000000000000000..cd50bb79e4dad3d204e1b0c283aeb8f4dd61240e --- /dev/null +++ b/components/app-wrapper.js @@ -0,0 +1,28 @@ +import React, { Component } from 'react' +import { View } from 'react-native' +import nodejs from 'nodejs-mobile-react-native' +import App from './app' +import PasswordPrompt from './password-prompt' + +export default class AppWrapper extends Component { + constructor() { + super() + this.state = {} + nodejs.start('main.js') + } + render() { + return ( + <View style={{ flex: 1 }}> + {this.state.showApp ? + <App/> + : + <PasswordPrompt + showApp={() => { + this.setState({showApp: true}) + }} + /> + } + </View> + ) + } +} \ No newline at end of file diff --git a/components/calendar.js b/components/calendar.js index 95e8bf866b17600dc2cb379ed8048a691af18e9a..4ffce88eed7b09eaa5aa5620197f1b1606cdeda8 100644 --- a/components/calendar.js +++ b/components/calendar.js @@ -1,37 +1,37 @@ import React, { Component } from 'react' import { CalendarList } from 'react-native-calendars' import {LocalDate} from 'js-joda' -import { getOrCreateCycleDay, bleedingDaysSortedByDate } from '../db' +import { getOrCreateCycleDay, getBleedingDaysSortedByDate } from '../db' import cycleModule from '../lib/cycle' import {shadesOfRed} from '../styles/index' import styles from '../styles/index' + export default class CalendarView extends Component { constructor(props) { super(props) + this.bleedingDays = getBleedingDaysSortedByDate() const predictedMenses = cycleModule().getPredictedMenses() this.state = { - bleedingDaysInCalFormat: toCalFormat(bleedingDaysSortedByDate), + bleedingDaysInCalFormat: toCalFormat(this.bleedingDays), predictedBleedingDaysInCalFormat: predictionToCalFormat(predictedMenses), todayInCalFormat: todayToCalFormat() } - this.setStateWithCalFormattedDays = (function (CalendarComponent) { - return function() { - const predictedMenses = cycleModule().getPredictedMenses() - CalendarComponent.setState({ - bleedingDaysInCalFormat: toCalFormat(bleedingDaysSortedByDate), - predictedBleedingDaysInCalFormat: predictionToCalFormat(predictedMenses), - todayInCalFormat: todayToCalFormat() - }) - } - })(this) + this.bleedingDays.addListener(this.setStateWithCalFormattedDays) + } - bleedingDaysSortedByDate.addListener(this.setStateWithCalFormattedDays) + setStateWithCalFormattedDays = () => { + const predictedMenses = cycleModule().getPredictedMenses() + this.setState({ + bleedingDaysInCalFormat: toCalFormat(this.bleedingDays), + predictedBleedingDaysInCalFormat: predictionToCalFormat(predictedMenses), + todayInCalFormat: todayToCalFormat() + }) } componentWillUnmount() { - bleedingDaysSortedByDate.removeListener(this.setStateWithCalFormattedDays) + this.bleedingDays.removeListener(this.setStateWithCalFormattedDays) } passDateToDayView = (result) => { diff --git a/components/chart/chart.js b/components/chart/chart.js index a4cc7d19014e77a7bc46c98494c4988f68402904..ddd8d007b890ebd6c9cb6cae9e01ccf0d174d241 100644 --- a/components/chart/chart.js +++ b/components/chart/chart.js @@ -5,7 +5,7 @@ import { LocalDate } from 'js-joda' import { makeYAxisLabels, normalizeToScale, makeHorizontalGrid } from './y-axis' import nfpLines from './nfp-lines' import DayColumn from './day-column' -import { getCycleDay, cycleDaysSortedByDate, getAmountOfCycleDays } from '../../db' +import { getCycleDay, getCycleDaysSortedByDate, getAmountOfCycleDays } from '../../db' import styles from './styles' import { scaleObservable } from '../../local-storage' import config from '../../config' @@ -25,6 +25,7 @@ export default class CycleChart extends Component { /> ) } + this.cycleDaysSortedByDate = getCycleDaysSortedByDate() } onLayout = ({ nativeEvent }) => { @@ -35,12 +36,12 @@ export default class CycleChart extends Component { this.setState({ columns: this.makeColumnInfo(nfpLines(height)) }) } - cycleDaysSortedByDate.addListener(this.reCalculateChartInfo) + this.cycleDaysSortedByDate.addListener(this.reCalculateChartInfo) this.removeObvListener = scaleObservable(this.reCalculateChartInfo, false) } componentWillUnmount() { - cycleDaysSortedByDate.removeListener(this.reCalculateChartInfo) + this.cycleDaysSortedByDate.removeListener(this.reCalculateChartInfo) this.removeObvListener() } @@ -71,7 +72,7 @@ export default class CycleChart extends Component { 'pain', 'note' ].filter((symptomName) => { - return cycleDaysSortedByDate.some(cycleDay => cycleDay[symptomName]) + return this.cycleDaysSortedByDate.some(cycleDay => cycleDay[symptomName]) }) const columns = xAxisDates.map(dateString => { diff --git a/components/chart/day-column.js b/components/chart/day-column.js index 9b010fda01c53c7a5040e72addfceb590d7b354d..e541ab004426a230843ad1a920a40002cb039227 100644 --- a/components/chart/day-column.js +++ b/components/chart/day-column.js @@ -9,10 +9,13 @@ import { getOrCreateCycleDay } from '../../db' import cycleModule from '../../lib/cycle' import DotAndLine from './dot-and-line' -const getCycleDayNumber = cycleModule().getCycleDayNumber const label = styles.column.label export default class DayColumn extends Component { + constructor() { + super() + this.getCycleDayNumber = cycleModule().getCycleDayNumber + } passDateToDayView(dateString) { const cycleDay = getOrCreateCycleDay(dateString) this.props.navigate('CycleDay', { cycleDay }) @@ -68,7 +71,7 @@ export default class DayColumn extends Component { ) } - const cycleDayNumber = getCycleDayNumber(dateString) + const cycleDayNumber = this.getCycleDayNumber(dateString) const shortDate = dateString.split('-').slice(1).join('-') const cycleDayLabel = ( <Text style = {label.number}> diff --git a/components/home.js b/components/home.js index 1fa051d3ad879129153aa82a9bb5c02cb64bdf11..fffe6939b16daa9a6f8594bd0a2d32f19ecc2cea 100644 --- a/components/home.js +++ b/components/home.js @@ -8,37 +8,35 @@ import { import { LocalDate, ChronoUnit } from 'js-joda' import styles from '../styles/index' import cycleModule from '../lib/cycle' -import { getOrCreateCycleDay, bleedingDaysSortedByDate, fillWithMucusDummyData, fillWithCervixDummyData, deleteAll } from '../db' +import { getOrCreateCycleDay, getBleedingDaysSortedByDate, fillWithMucusDummyData, fillWithCervixDummyData } from '../db' import {bleedingPrediction as labels} from './labels' -const getCycleDayNumber = cycleModule().getCycleDayNumber - export default class Home extends Component { constructor(props) { super(props) + this.getCycleDayNumber = cycleModule().getCycleDayNumber this.todayDateString = LocalDate.now().toString() - const cycleDayNumber = getCycleDayNumber(this.todayDateString) + const cycleDayNumber = this.getCycleDayNumber(this.todayDateString) this.state = { welcomeText: determineWelcomeText(cycleDayNumber), predictionText: determinePredictionText() } - this.setStateWithCurrentText = (function (HomeComponent) { - return function () { - const cycleDayNumber = getCycleDayNumber(HomeComponent.todayDateString) - HomeComponent.setState({ - welcomeText: determineWelcomeText(cycleDayNumber), - predictionText: determinePredictionText() - }) - } - })(this) + this.bleedingDays = getBleedingDaysSortedByDate() + this.bleedingDays.addListener(this.setStateWithCurrentText) + } - bleedingDaysSortedByDate.addListener(this.setStateWithCurrentText) + setStateWithCurrentText = () => { + const cycleDayNumber = this.getCycleDayNumber(this.todayDateString) + this.setState({ + welcomeText: determineWelcomeText(cycleDayNumber), + predictionText: determinePredictionText() + }) } componentWillUnmount() { - bleedingDaysSortedByDate.removeListener(this.setStateWithCurrentText) + this.bleedingDays.removeListener(this.setStateWithCurrentText) } passTodayToDayView() { @@ -72,12 +70,6 @@ export default class Home extends Component { title="fill with example data for cervix&temp"> </Button> </View> - <View style={styles.homeButton}> - <Button - onPress={() => deleteAll()} - title="delete everything"> - </Button> - </View> </View> </ScrollView> ) diff --git a/components/labels.js b/components/labels.js index c61a939013115aff48b197329bdef3132c492334..7fde2b803482f9b78b683c4352d2e8bda8605400 100644 --- a/components/labels.js +++ b/components/labels.js @@ -3,7 +3,12 @@ export const shared = { save: 'Save', errorTitle: 'Error', successTitle: 'Success', - warning: 'Warning' + warning: 'Warning', + incorrectPassword: 'Password incorrect', + incorrectPasswordMessage: 'That password is incorrect.', + tryAgain: 'Try again', + ok: 'OK', + unlock: 'Unlock' } export const settings = { @@ -48,6 +53,20 @@ export const settings = { noTimeSet: 'Set a time for a daily reminder to take your temperature', timeSet: time => `Daily reminder set for ${time}`, notification: 'Record your morning temperature' + }, + passwordSettings: { + title: 'App password', + explainerDisabled: "Encrypt the app's database with a password. You need to enter the password every time the app is started.", + explainerEnabled: "Password protection and database encryption is currently enabled", + setPassword: 'Set password', + changePassword: 'Change password', + deletePassword: 'Delete password', + enterCurrent: "Please enter your current password", + enterNew: "Please enter a new password", + backupReminderTitle: 'Read this before making changes to your password', + backupReminder: 'Just to be safe, please backup your data using the export function before making changes to your password.\n\nLonger passwords are better! Consider using a passphrase.\n\nPlease also make sure you do not lose your password. There is no way to recover your data if you do.\n\nMaking any changes to your password setting will keep your data as it was before and restart the app.', + deleteBackupReminderTitle: 'Read this before deleting your password', + deleteBackupReminder: 'Deleting your password means your data will no longer be encrypted.\n\nJust to be safe, please backup your data using the export function before deleting your password.\n\nMaking any changes to your password setting will keep your data as it was before and restart the app.', } } @@ -90,4 +109,16 @@ export const bleedingPrediction = { predictionStarted1DayLeft: 'Your period is likely to start today or tomorrow.', predictionStartedNoDaysLeft: 'Your period is likely to start today.', predictionInPast: (startDate, endDate) => `Based on your documented data, your period was likely to start between ${startDate} and ${endDate}.` +} + +export const passwordPrompt = { + title: 'Unlock app', + enterPassword: 'Enter password here', + deleteDatabaseExplainer: "If you've forgotten your password, unfortunately, there is nothing we can do to recover your data, because it is encrypted with the password only you know. You can, however, delete all your encrypted data and start fresh. Once all data has been erased, you can set a new password in the settings, if you like.", + forgotPassword: 'Forgot your password?', + deleteDatabaseTitle: 'Forgot your password?', + deleteData: 'Yes, delete all my data', + areYouSureTitle: 'Are you sure?', + areYouSure: 'Are you absolutely sure you want to permanently delete all your data?', + reallyDeleteData: 'Yes, I am sure' } \ No newline at end of file diff --git a/components/password-prompt.js b/components/password-prompt.js new file mode 100644 index 0000000000000000000000000000000000000000..ce9122122043f3f17c34dc7367ca8a316224c91e --- /dev/null +++ b/components/password-prompt.js @@ -0,0 +1,128 @@ +import React, { Component } from 'react' +import { View, TextInput, TouchableOpacity, Alert, Image } from 'react-native' +import nodejs from 'nodejs-mobile-react-native' +import { saveEncryptionFlag } from '../local-storage' +import { AppText } from './app-text' +import styles from '../styles' +import { passwordPrompt as labels, shared } from './labels' +import { requestHash, deleteDbAndOpenNew, openDb } from '../db' + +export default class PasswordPrompt extends Component { + constructor(props) { + super(props) + this.state = { + password: null + } + + nodejs.channel.addListener( + 'check-pw', + this.passHashToDb, + this + ) + + this.tryToOpenDb() + } + + async tryToOpenDb() { + try { + await openDb({ persistConnection: true }) + } catch (err) { + this.setState({ showPasswordPrompt: true }) + await saveEncryptionFlag(true) + return + } + + await saveEncryptionFlag(false) + this.props.showApp() + } + + passHashToDb = async hash => { + try { + await openDb({ hash, persistConnection: true }) + } catch (err) { + Alert.alert( + shared.incorrectPassword, + shared.incorrectPasswordMessage, + [{ + text: shared.tryAgain, + onPress: () => this.setState({ password: null }) + }] + ) + return + } + this.props.showApp() + } + + confirmDeletion = async () => { + Alert.alert( + labels.deleteDatabaseTitle, + labels.deleteDatabaseExplainer, + [{ + text: shared.cancel, + style: 'cancel' + }, { + text: labels.deleteData, + onPress: () => { + Alert.alert( + labels.areYouSureTitle, + labels.areYouSure, + [{ + text: shared.cancel, + style: 'cancel' + }, { + text: labels.reallyDeleteData, + onPress: async () => { + await deleteDbAndOpenNew() + await saveEncryptionFlag(false) + this.props.showApp() + } + }] + ) + } + }] + ) + } + + componentWillUnmount() { + nodejs.channel.removeListener('check-pw', this.passHashToDb) + } + + render() { + return ( + <View flex={1}> + {this.state.showPasswordPrompt && + <View style={styles.passwordPromptPage}> + <Image + source={require('../assets/drip_small.png')} + style={styles.passwordPromptImage} + /> + <TextInput + onChangeText={val => this.setState({ password: val })} + style={styles.passwordPromptField} + secureTextEntry={true} + placeholder={labels.enterPassword} + /> + <TouchableOpacity + style={styles.passwordPromptButton} + onPress={() => { + requestHash('check-pw', this.state.password) + }} + disabled={!this.state.password} + > + <AppText style={styles.passwordPromptButtonText}> + {labels.title} + </AppText> + </TouchableOpacity> + <TouchableOpacity + onPress={this.confirmDeletion} + > + <AppText style={styles.passwordPromptForgotPasswordText}> + {labels.forgotPassword} + </AppText> + </TouchableOpacity> + </View> + } + </View> + ) + } +} \ No newline at end of file diff --git a/components/settings.js b/components/settings.js deleted file mode 100644 index 125cd27176c8cd9aef2ad1fd907b5e511baaf1da..0000000000000000000000000000000000000000 --- a/components/settings.js +++ /dev/null @@ -1,283 +0,0 @@ -import React, { Component } from 'react' -import { - View, - TouchableOpacity, - ScrollView, - Alert, - Switch -} from 'react-native' -import DateTimePicker from 'react-native-modal-datetime-picker-nevo' -import Slider from '@ptomasroos/react-native-multi-slider' -import Share from 'react-native-share' -import { DocumentPicker, DocumentPickerUtil } from 'react-native-document-picker' -import rnfs from 'react-native-fs' -import styles, { secondaryColor } from '../styles/index' -import config from '../config' -import { settings as labels, shared as sharedLabels } from './labels' -import getDataAsCsvDataUri from '../lib/import-export/export-to-csv' -import importCsv from '../lib/import-export/import-from-csv' -import { - scaleObservable, - saveTempScale, - tempReminderObservable, - saveTempReminder -} from '../local-storage' -import { AppText } from './app-text' - -export default class Settings extends Component { - constructor(props) { - super(props) - this.state = {} - } - - render() { - return ( - <ScrollView> - <TempReminderPicker/> - <View style={styles.settingsSegment}> - <AppText style={styles.settingsSegmentTitle}> - {labels.tempScale.segmentTitle} - </AppText> - <AppText>{labels.tempScale.segmentExplainer}</AppText> - <TempSlider/> - </View> - <View style={styles.settingsSegment}> - <AppText style={styles.settingsSegmentTitle}> - {labels.export.button} - </AppText> - <AppText>{labels.export.segmentExplainer}</AppText> - <TouchableOpacity - onPress={openShareDialogAndExport} - style={styles.settingsButton}> - <AppText style={styles.settingsButtonText}> - {labels.export.button} - </AppText> - </TouchableOpacity> - </View> - <View style={styles.settingsSegment}> - <AppText style={styles.settingsSegmentTitle}> - {labels.import.button} - </AppText> - <AppText>{labels.import.segmentExplainer}</AppText> - <TouchableOpacity - onPress={openImportDialogAndImport} - style={styles.settingsButton}> - <AppText style={styles.settingsButtonText}> - {labels.import.button} - </AppText> - </TouchableOpacity> - </View> - </ScrollView> - ) - } -} - -class TempReminderPicker extends Component { - constructor(props) { - super(props) - this.state = Object.assign({}, tempReminderObservable.value) - } - - render() { - return ( - <TouchableOpacity - style={styles.settingsSegment} - onPress={() => this.setState({ isTimePickerVisible: true })} - > - <AppText style={styles.settingsSegmentTitle}> - {labels.tempReminder.title} - </AppText> - <View style={{ flexDirection: 'row', alignItems: 'center' }}> - <View style={{ flex: 1 }}> - {this.state.time && this.state.enabled ? - <AppText>{labels.tempReminder.timeSet(this.state.time)}</AppText> - : - <AppText>{labels.tempReminder.noTimeSet}</AppText> - } - </View> - <Switch - value={this.state.enabled} - onValueChange={switchOn => { - this.setState({ enabled: switchOn }) - if (switchOn && !this.state.time) { - this.setState({ isTimePickerVisible: true }) - } - if (!switchOn) saveTempReminder({ enabled: false }) - }} - /> - <DateTimePicker - mode="time" - isVisible={this.state.isTimePickerVisible} - onConfirm={jsDate => { - const time = padWithZeros(`${jsDate.getHours()}:${jsDate.getMinutes()}`) - this.setState({ - time, - isTimePickerVisible: false, - enabled: true - }) - saveTempReminder({ - time, - enabled: true - }) - }} - onCancel={() => { - this.setState({ isTimePickerVisible: false }) - if (!this.state.time) this.setState({enabled: false}) - }} - /> - </View> - </TouchableOpacity> - ) - } -} - -class TempSlider extends Component { - constructor(props) { - super(props) - this.state = Object.assign({}, scaleObservable.value) - } - - onValuesChange = (values) => { - this.setState({ - min: values[0], - max: values[1] - }) - } - - onValuesChangeFinish = (values) => { - this.setState({ - min: values[0], - max: values[1] - }) - try { - saveTempScale(this.state) - } catch(err) { - alertError(labels.tempScale.saveError) - } - } - - render() { - return ( - <View style={{ alignItems: 'center' }}> - <AppText>{`${labels.tempScale.min} ${this.state.min}`}</AppText> - <AppText>{`${labels.tempScale.max} ${this.state.max}`}</AppText> - <Slider - values={[this.state.min, this.state.max]} - min={config.temperatureScale.min} - max={config.temperatureScale.max} - step={0.5} - onValuesChange={this.onValuesChange} - onValuesChangeFinish={this.onValuesChangeFinish} - selectedStyle={{ - backgroundColor: 'darkgrey', - }} - unselectedStyle={{ - backgroundColor: 'silver', - }} - trackStyle={{ - height: 10, - }} - markerStyle={{ - backgroundColor: secondaryColor, - height: 20, - width: 20, - borderRadius: 100, - marginTop: 10 - }} - /> - </View> - ) - } -} - -async function openShareDialogAndExport() { - let data - try { - data = getDataAsCsvDataUri() - if (!data) { - return alertError(labels.errors.noData) - } - } catch (err) { - console.error(err) - return alertError(labels.errors.couldNotConvert) - } - - try { - await Share.open({ - title: labels.export.title, - url: data, - subject: labels.export.subject, - type: 'text/csv', - showAppsToView: true - }) - } catch (err) { - console.error(err) - return alertError(labels.export.errors.problemSharing) - } -} - -function openImportDialogAndImport() { - Alert.alert( - labels.import.title, - labels.import.message, - [{ - text: labels.import.replaceOption, - onPress: () => getFileContentAndImport({ deleteExisting: false }) - }, { - text: labels.import.deleteOption, - onPress: () => getFileContentAndImport({ deleteExisting: true }) - }, { - text: sharedLabels.cancel, style: 'cancel', onPress: () => { } - }] - ) -} - -async function getFileContentAndImport({ deleteExisting }) { - let fileInfo - try { - fileInfo = await new Promise((resolve, reject) => { - DocumentPicker.show({ - filetype: [DocumentPickerUtil.allFiles()], - }, (err, res) => { - if (err) return reject(err) - resolve(res) - }) - }) - } catch (err) { - // because cancelling also triggers an error, we do nothing here - return - } - - let fileContent - try { - fileContent = await rnfs.readFile(fileInfo.uri, 'utf8') - } catch (err) { - return importError(labels.import.errors.couldNotOpenFile) - } - - try { - await importCsv(fileContent, deleteExisting) - Alert.alert(sharedLabels.successTitle, labels.import.success.message) - } catch(err) { - importError(err.message) - } -} - -function alertError(msg) { - Alert.alert(sharedLabels.errorTitle, msg) -} - -function importError(msg) { - const postFixed = `${msg}\n\n${labels.import.errors.postFix}` - alertError(postFixed) -} - -function padWithZeros(time) { - const vals = time.split(':') - return vals.map(val => { - if (parseInt(val) < 10) { - val = `0${val}` - } - return val - }).join(':') -} \ No newline at end of file diff --git a/components/settings/alert-error.js b/components/settings/alert-error.js new file mode 100644 index 0000000000000000000000000000000000000000..ae375405ff0a3136a10622314224b19819053f5b --- /dev/null +++ b/components/settings/alert-error.js @@ -0,0 +1,6 @@ +import { Alert } from 'react-native' +import { shared as sharedLabels } from '../labels' + +export default function alertError(msg) { + Alert.alert(sharedLabels.errorTitle, msg) +} \ No newline at end of file diff --git a/components/settings/export-dialog.js b/components/settings/export-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..4e3c21f170265e35b203a6a94da81c24a452af01 --- /dev/null +++ b/components/settings/export-dialog.js @@ -0,0 +1,31 @@ +import Share from 'react-native-share' +import getDataAsCsvDataUri from '../../lib/import-export/export-to-csv' +import alertError from './alert-error' +import { settings as labels } from '../labels' + +export default async function openShareDialogAndExport() { + let data + try { + data = getDataAsCsvDataUri() + if (!data) { + return alertError(labels.errors.noData) + } + } catch (err) { + console.error(err) + return alertError(labels.errors.couldNotConvert) + } + + try { + await Share.open({ + title: labels.export.title, + url: data, + subject: labels.export.subject, + type: 'text/csv', + showAppsToView: true + }) + } catch (err) { + console.error(err) + return alertError(labels.export.errors.problemSharing) + } +} + diff --git a/components/settings/import-dialog.js b/components/settings/import-dialog.js new file mode 100644 index 0000000000000000000000000000000000000000..a07671780bbe98a854cefdea9126610fd71f567b --- /dev/null +++ b/components/settings/import-dialog.js @@ -0,0 +1,58 @@ +import { Alert } from 'react-native' +import { DocumentPicker, DocumentPickerUtil } from 'react-native-document-picker' +import rnfs from 'react-native-fs' +import importCsv from '../../lib/import-export/import-from-csv' +import { settings as labels, shared as sharedLabels } from '../labels' +import alertError from './alert-error' + +export default function openImportDialogAndImport() { + Alert.alert( + labels.import.title, + labels.import.message, + [{ + text: labels.import.replaceOption, + onPress: () => getFileContentAndImport({ deleteExisting: false }) + }, { + text: labels.import.deleteOption, + onPress: () => getFileContentAndImport({ deleteExisting: true }) + }, { + text: sharedLabels.cancel, style: 'cancel', onPress: () => { } + }] + ) +} + +async function getFileContentAndImport({ deleteExisting }) { + let fileInfo + try { + fileInfo = await new Promise((resolve, reject) => { + DocumentPicker.show({ + filetype: [DocumentPickerUtil.allFiles()], + }, (err, res) => { + if (err) return reject(err) + resolve(res) + }) + }) + } catch (err) { + // because cancelling also triggers an error, we do nothing here + return + } + + let fileContent + try { + fileContent = await rnfs.readFile(fileInfo.uri, 'utf8') + } catch (err) { + return importError(labels.import.errors.couldNotOpenFile) + } + + try { + await importCsv(fileContent, deleteExisting) + Alert.alert(sharedLabels.successTitle, labels.import.success.message) + } catch(err) { + importError(err.message) + } +} + +function importError(msg) { + const postFixed = `${msg}\n\n${labels.import.errors.postFix}` + alertError(postFixed) +} \ No newline at end of file diff --git a/components/settings/index.js b/components/settings/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e0828d8042e638cc1cf9818097f4c56e56446d35 --- /dev/null +++ b/components/settings/index.js @@ -0,0 +1,63 @@ +import React, { Component } from 'react' +import { + View, + TouchableOpacity, + ScrollView, +} from 'react-native' +import styles from '../../styles/index' +import { settings as labels } from '../labels' +import { AppText } from '../app-text' +import TempReminderPicker from './temp-reminder-picker' +import TempSlider from './temp-slider' +import openImportDialogAndImport from './import-dialog' +import openShareDialogAndExport from './export-dialog' +import PasswordSetting from './password' + +export default class Settings extends Component { + constructor(props) { + super(props) + this.state = {} + } + + render() { + return ( + <ScrollView> + <TempReminderPicker/> + <View style={styles.settingsSegment}> + <AppText style={styles.settingsSegmentTitle}> + {labels.tempScale.segmentTitle} + </AppText> + <AppText>{labels.tempScale.segmentExplainer}</AppText> + <TempSlider/> + </View> + <PasswordSetting /> + <View style={styles.settingsSegment}> + <AppText style={styles.settingsSegmentTitle}> + {labels.export.button} + </AppText> + <AppText>{labels.export.segmentExplainer}</AppText> + <TouchableOpacity + onPress={openShareDialogAndExport} + style={styles.settingsButton}> + <AppText style={styles.settingsButtonText}> + {labels.export.button} + </AppText> + </TouchableOpacity> + </View> + <View style={styles.settingsSegment}> + <AppText style={styles.settingsSegmentTitle}> + {labels.import.button} + </AppText> + <AppText>{labels.import.segmentExplainer}</AppText> + <TouchableOpacity + onPress={openImportDialogAndImport} + style={styles.settingsButton}> + <AppText style={styles.settingsButtonText}> + {labels.import.button} + </AppText> + </TouchableOpacity> + </View> + </ScrollView> + ) + } +} diff --git a/components/settings/password/check-current-password.js b/components/settings/password/check-current-password.js new file mode 100644 index 0000000000000000000000000000000000000000..b177de486f180db0b8e6fcaf3fe65bbfcd863bb1 --- /dev/null +++ b/components/settings/password/check-current-password.js @@ -0,0 +1,23 @@ +import { Alert } from 'react-native' +import { openDb } from '../../../db' +import { shared } from '../../labels' + +export default async function checkPassword({hash, onCancel, onTryAgain }) { + try { + await openDb({ hash, persistConnection: false }) + return true + } catch (err) { + Alert.alert( + shared.incorrectPassword, + shared.incorrectPasswordMessage, + [{ + text: shared.cancel, + onPress: onCancel + }, { + text: shared.tryAgain, + onPress: onTryAgain + }] + ) + return false + } +} \ No newline at end of file diff --git a/components/settings/password/create.js b/components/settings/password/create.js new file mode 100644 index 0000000000000000000000000000000000000000..97559ee229bfca8c8de7f44e18b84f131f95a84b --- /dev/null +++ b/components/settings/password/create.js @@ -0,0 +1,61 @@ +import React, { Component } from 'react' +import { + View, + TouchableOpacity, +} from 'react-native' +import nodejs from 'nodejs-mobile-react-native' +import { AppText } from '../../app-text' +import styles from '../../../styles' +import { settings as labels } from '../../labels' +import { requestHash, changeEncryptionAndRestartApp } from '../../../db' +import PasswordField from './password-field' +import showBackUpReminder from './show-backup-reminder' + +export default class CreatePassword extends Component { + constructor() { + super() + this.state = { + enteringNewPassword: false, + newPassword: null + } + nodejs.channel.addListener( + 'create-pw-hash', + changeEncryptionAndRestartApp, + this + ) + } + + componentWillUnmount() { + nodejs.channel.removeListener('create-pw-hash', changeEncryptionAndRestartApp) + } + + render () { + return ( + <View> + {this.state.enteringNewPassword && + <PasswordField + placeholder={labels.passwordSettings.enterNew} + value={this.state.newPassword} + onChangeText={val => this.setState({newPassword: val})} + /> + } + <TouchableOpacity + onPress={() => { + if (!this.state.enteringNewPassword) { + showBackUpReminder(() => { + this.setState({ enteringNewPassword: true }) + }) + } else { + requestHash('create-pw-hash', this.state.newPassword) + } + }} + disabled={this.state.enteringNewPassword && !this.state.newPassword} + style={styles.settingsButton}> + <AppText style={styles.settingsButtonText}> + {labels.passwordSettings.setPassword} + </AppText> + </TouchableOpacity> + </View> + ) + } +} \ No newline at end of file diff --git a/components/settings/password/delete.js b/components/settings/password/delete.js new file mode 100644 index 0000000000000000000000000000000000000000..58666f51d4199c5bf5d52255f19e46c1f57a064c --- /dev/null +++ b/components/settings/password/delete.js @@ -0,0 +1,80 @@ +import React, { Component } from 'react' +import { + View, + TouchableOpacity +} from 'react-native' +import nodejs from 'nodejs-mobile-react-native' +import { AppText } from '../../app-text' +import styles from '../../../styles' +import { settings as labels } from '../../labels' +import { requestHash, changeEncryptionAndRestartApp } from '../../../db' +import PasswordField from './password-field' +import showBackUpReminder from './show-backup-reminder' +import checkCurrentPassword from './check-current-password' + +export default class DeletePassword extends Component { + constructor() { + super() + this.state = { + enteringCurrentPassword: false, + currentPassword: null + } + + nodejs.channel.addListener( + 'pre-delete-pw-check', + this.removeEncryption, + this + ) + } + + componentWillUnmount() { + nodejs.channel.removeListener('pre-delete-pw-check', this.removeEncryption) + } + + removeEncryption = async hash => { + const passwordIsCorrect = await checkCurrentPassword({ + hash, + onTryAgain: () => this.setState({currentPassword: null}), + onCancel: () => this.setState({ + enteringCurrentPassword: false, + currentPassword: null + }) + }) + + if (passwordIsCorrect) await changeEncryptionAndRestartApp() + } + + render() { + return ( + <View> + {this.state.enteringCurrentPassword && + <PasswordField + onChangeText={val => this.setState({ currentPassword: val })} + value={this.state.currentPassword} + placeholder={labels.passwordSettings.enterCurrent} + /> + } + <TouchableOpacity + onPress={() => { + if (!this.state.enteringCurrentPassword) { + showBackUpReminder(() => { + this.setState({ enteringCurrentPassword: true }) + }, true) + } else { + requestHash('pre-delete-pw-check', this.state.currentPassword) + } + }} + disabled={ + this.state.enteringCurrentPassword && + !this.state.currentPassword + } + style={styles.settingsButton} + > + <AppText style={styles.settingsButtonText}> + {labels.passwordSettings.deletePassword} + </AppText> + </TouchableOpacity> + </View> + ) + } +} \ No newline at end of file diff --git a/components/settings/password/index.js b/components/settings/password/index.js new file mode 100644 index 0000000000000000000000000000000000000000..87ece3df88e91cde51da077551f52bc5d332dbea --- /dev/null +++ b/components/settings/password/index.js @@ -0,0 +1,50 @@ +import React, { Component } from 'react' +import { View } from 'react-native' +import CreatePassword from './create' +import ChangePassword from './update' +import DeletePassword from './delete' +import { AppText } from '../../app-text' +import { + hasEncryptionObservable +} from '../../../local-storage' +import styles from '../../../styles/index' +import { settings as labels } from '../../labels' + +export default class PasswordSetting extends Component { + constructor(props) { + super(props) + this.state = { + showUpdateAndDelete: hasEncryptionObservable.value, + showCreate: !hasEncryptionObservable.value + } + } + + render() { + return ( + <View style={styles.settingsSegment}> + + <AppText style={styles.settingsSegmentTitle}> + {labels.passwordSettings.title} + </AppText> + + {this.state.showUpdateAndDelete ? + <AppText>{labels.passwordSettings.explainerEnabled}</AppText> + : + <AppText>{labels.passwordSettings.explainerDisabled}</AppText> + } + + {this.state.showUpdateAndDelete && + <View> + <ChangePassword/> + <DeletePassword/> + </View> + } + + {this.state.showCreate && + <CreatePassword/> + } + + </View> + ) + } +} \ No newline at end of file diff --git a/components/settings/password/password-field.js b/components/settings/password/password-field.js new file mode 100644 index 0000000000000000000000000000000000000000..719356685edb38e4ebf8b60a059a13dcc18977cf --- /dev/null +++ b/components/settings/password/password-field.js @@ -0,0 +1,16 @@ +import React from 'react' +import { TextInput } from 'react-native' +import styles from '../../../styles' + +export default function PasswordField(props) { + return ( + <TextInput + style={styles.passwordField} + autoFocus={true} + secureTextEntry={true} + onChangeText={props.onChangeText} + value={props.value} + placeholder={props.placeholder} + /> + ) +} diff --git a/components/settings/password/show-backup-reminder.js b/components/settings/password/show-backup-reminder.js new file mode 100644 index 0000000000000000000000000000000000000000..11a7d834a29236c81dc9af2ced7f9b08a12cad62 --- /dev/null +++ b/components/settings/password/show-backup-reminder.js @@ -0,0 +1,25 @@ +import { Alert } from 'react-native' +import { settings as labels, shared } from '../../labels' + +export default function showBackUpReminder(okHandler, isDelete) { + let title, message + if (isDelete) { + title = labels.passwordSettings.deleteBackupReminderTitle + message = labels.passwordSettings.deleteBackupReminder + } else { + title = labels.passwordSettings.backupReminderTitle + message = labels.passwordSettings.backupReminder + } + + Alert.alert( + title, + message, + [{ + text: shared.cancel, + style: 'cancel' + }, { + text: shared.ok, + onPress: okHandler + }] + ) +} \ No newline at end of file diff --git a/components/settings/password/update.js b/components/settings/password/update.js new file mode 100644 index 0000000000000000000000000000000000000000..12af518639690712c75b07ba5ed71b87c2e9b28f --- /dev/null +++ b/components/settings/password/update.js @@ -0,0 +1,129 @@ + +import React, { Component } from 'react' +import { + View, + TouchableOpacity} from 'react-native' +import nodejs from 'nodejs-mobile-react-native' +import { AppText } from '../../app-text' +import styles from '../../../styles' +import { settings as labels, shared } from '../../labels' +import { requestHash, changeEncryptionAndRestartApp } from '../../../db' +import PasswordField from './password-field' +import showBackUpReminder from './show-backup-reminder' +import checkCurrentPassword from './check-current-password' + +export default class ChangePassword extends Component { + constructor() { + super() + this.state = { + enteringCurrentPassword: false, + currentPassword: null, + enteringNewPassword: false, + newPassword: null + } + + nodejs.channel.addListener( + 'pre-change-pw-check', + this.openNewPasswordField, + this + ) + + nodejs.channel.addListener( + 'change-pw', + changeEncryptionAndRestartApp, + this + ) + } + + componentWillUnmount() { + nodejs.channel.removeListener('pre-change-pw-check', this.openNewPasswordField) + nodejs.channel.removeListener('change-pw', changeEncryptionAndRestartApp) + } + + openNewPasswordField = async hash => { + const passwordCorrect = await checkCurrentPassword({ + hash, + onTryAgain: () => this.setState({ currentPassword: null }), + onCancel: () => this.setState({ + enteringCurrentPassword: false, + currentPassword: null + }) + }) + + if (passwordCorrect) { + this.setState({ + enteringCurrentPassword: false, + currentPassword: null, + enteringNewPassword: true + }) + } + } + + render() { + return ( + <View> + {!this.state.enteringCurrentPassword && + !this.state.enteringNewPassword && + <TouchableOpacity + onPress={() => showBackUpReminder(() => { + this.setState({ enteringCurrentPassword: true }) + })} + disabled={this.state.currentPassword} + style={styles.settingsButton}> + <AppText style={styles.settingsButtonText}> + {labels.passwordSettings.changePassword} + </AppText> + </TouchableOpacity> + } + + {this.state.enteringCurrentPassword && + <View> + <PasswordField + onChangeText={val => { + this.setState({ + currentPassword: val, + wrongPassword: false + }) + }} + value={this.state.currentPassword} + placeholder={labels.passwordSettings.enterCurrent} + /> + <TouchableOpacity + onPress={() => requestHash('pre-change-pw-check', this.state.currentPassword)} + disabled={!this.state.currentPassword} + style={styles.settingsButton}> + <AppText style={styles.settingsButtonText}> + {shared.unlock} + </AppText> + </TouchableOpacity> + </View> + } + + {this.state.enteringNewPassword && + <View> + <PasswordField + style={styles.passwordField} + onChangeText={val => { + this.setState({ + newPassword: val + }) + }} + value={this.state.changedPassword} + placeholder={labels.passwordSettings.enterNew} + /> + + <TouchableOpacity + onPress={() => requestHash('change-pw', this.state.newPassword)} + disabled={ !this.state.newPassword } + style={styles.settingsButton}> + <AppText style={styles.settingsButtonText}> + {labels.passwordSettings.changePassword} + </AppText> + </TouchableOpacity> + </View> + } + + </View> + ) + } +} \ No newline at end of file diff --git a/components/settings/temp-reminder-picker.js b/components/settings/temp-reminder-picker.js new file mode 100644 index 0000000000000000000000000000000000000000..8cfbf86edda1c4b3d1bcfbebda3238f5fe40f712 --- /dev/null +++ b/components/settings/temp-reminder-picker.js @@ -0,0 +1,83 @@ +import React, { Component } from 'react' +import { + View, + TouchableOpacity, + Switch +} from 'react-native' +import DateTimePicker from 'react-native-modal-datetime-picker-nevo' +import { AppText } from '../app-text' +import { + tempReminderObservable, + saveTempReminder +} from '../../local-storage' +import styles from '../../styles/index' +import { settings as labels } from '../labels' + +export default class TempReminderPicker extends Component { + constructor(props) { + super(props) + this.state = Object.assign({}, tempReminderObservable.value) + } + + render() { + return ( + <TouchableOpacity + style={styles.settingsSegment} + onPress={() => this.setState({ isTimePickerVisible: true })} + > + <AppText style={styles.settingsSegmentTitle}> + {labels.tempReminder.title} + </AppText> + <View style={{ flexDirection: 'row', alignItems: 'center' }}> + <View style={{ flex: 1 }}> + {this.state.time && this.state.enabled ? + <AppText>{labels.tempReminder.timeSet(this.state.time)}</AppText> + : + <AppText>{labels.tempReminder.noTimeSet}</AppText> + } + </View> + <Switch + value={this.state.enabled} + onValueChange={switchOn => { + this.setState({ enabled: switchOn }) + if (switchOn && !this.state.time) { + this.setState({ isTimePickerVisible: true }) + } + if (!switchOn) saveTempReminder({ enabled: false }) + }} + /> + <DateTimePicker + mode="time" + isVisible={this.state.isTimePickerVisible} + onConfirm={jsDate => { + const time = padWithZeros(`${jsDate.getHours()}:${jsDate.getMinutes()}`) + this.setState({ + time, + isTimePickerVisible: false, + enabled: true + }) + saveTempReminder({ + time, + enabled: true + }) + }} + onCancel={() => { + this.setState({ isTimePickerVisible: false }) + if (!this.state.time) this.setState({enabled: false}) + }} + /> + </View> + </TouchableOpacity> + ) + } +} + +function padWithZeros(time) { + const vals = time.split(':') + return vals.map(val => { + if (parseInt(val) < 10) { + val = `0${val}` + } + return val + }).join(':') +} \ No newline at end of file diff --git a/components/settings/temp-slider.js b/components/settings/temp-slider.js new file mode 100644 index 0000000000000000000000000000000000000000..f21fd74006ebe606417b26baebb75e25241af88d --- /dev/null +++ b/components/settings/temp-slider.js @@ -0,0 +1,71 @@ +import React, { Component } from 'react' +import { View } from 'react-native' +import Slider from '@ptomasroos/react-native-multi-slider' +import { AppText } from '../app-text' +import { + scaleObservable, + saveTempScale, +} from '../../local-storage' +import { secondaryColor } from '../../styles/index' +import { settings as labels } from '../labels' +import config from '../../config' +import alertError from './alert-error' + +export default class TempSlider extends Component { + constructor(props) { + super(props) + this.state = Object.assign({}, scaleObservable.value) + } + + onValuesChange = (values) => { + this.setState({ + min: values[0], + max: values[1] + }) + } + + onValuesChangeFinish = (values) => { + this.setState({ + min: values[0], + max: values[1] + }) + try { + saveTempScale(this.state) + } catch(err) { + alertError(labels.tempScale.saveError) + } + } + + render() { + return ( + <View style={{ alignItems: 'center' }}> + <AppText>{`${labels.tempScale.min} ${this.state.min}`}</AppText> + <AppText>{`${labels.tempScale.max} ${this.state.max}`}</AppText> + <Slider + values={[this.state.min, this.state.max]} + min={config.temperatureScale.min} + max={config.temperatureScale.max} + step={0.5} + onValuesChange={this.onValuesChange} + onValuesChangeFinish={this.onValuesChangeFinish} + selectedStyle={{ + backgroundColor: 'darkgrey', + }} + unselectedStyle={{ + backgroundColor: 'silver', + }} + trackStyle={{ + height: 10, + }} + markerStyle={{ + backgroundColor: secondaryColor, + height: 20, + width: 20, + borderRadius: 100, + marginTop: 10 + }} + /> + </View> + ) + } +} \ No newline at end of file diff --git a/db/index.js b/db/index.js index 1b79d7bc037914504961b110f8c05adba920b45b..6dc4feb537bd72e5839e76079d743aba01f0854c 100644 --- a/db/index.js +++ b/db/index.js @@ -1,5 +1,8 @@ import Realm from 'realm' import { LocalDate, ChronoUnit } from 'js-joda' +import nodejs from 'nodejs-mobile-react-native' +import fs from 'react-native-fs' +import restart from 'react-native-restart' import { cycleWithFhmMucus, longAndComplicatedCycleWithMucus, @@ -8,165 +11,30 @@ import { longAndComplicatedCycleWithCervix, cycleWithTempAndNoCervixShift } from './fixtures' +import dbSchema from './schema' -const TemperatureSchema = { - name: 'Temperature', - properties: { - value: 'double', - exclude: 'bool', - time: { - type: 'string', - optional: true - }, - note: { - type: 'string', - optional: true - } - } -} - -const BleedingSchema = { - name: 'Bleeding', - properties: { - value: 'int', - exclude: 'bool' - } -} - -const MucusSchema = { - name: 'Mucus', - properties: { - feeling: 'int', - texture: 'int', - value: 'int', - exclude: 'bool' - } -} - -const CervixSchema = { - name: 'Cervix', - properties: { - opening: 'int', - firmness: 'int', - position: {type: 'int', optional: true }, - exclude: 'bool' - } -} - -const NoteSchema = { - name: 'Note', - properties: { - value: 'string' - } -} - -const DesireSchema = { - name: 'Desire', - properties: { - value: 'int' - } -} - -const SexSchema = { - name: 'Sex', - properties: { - solo: { type: 'bool', optional: true }, - partner: { type: 'bool', optional: true }, - condom: { type: 'bool', optional: true }, - pill: { type: 'bool', optional: true }, - iud: { type: 'bool', optional: true }, - patch: { type: 'bool', optional: true }, - ring: { type: 'bool', optional: true }, - implant: { type: 'bool', optional: true }, - other: { type: 'bool', optional: true }, - note: { type: 'string', optional: true } - } +let db +const realmConfig = { + schema: dbSchema } -const PainSchema = { - name: 'Pain', - properties: { - cramps: { type: 'bool', optional: true }, - ovulationPain: { type: 'bool', optional: true }, - headache: { type: 'bool', optional: true }, - backache: { type: 'bool', optional: true }, - nausea: { type: 'bool', optional: true }, - tenderBreasts: { type: 'bool', optional: true }, - migraine: { type: 'bool', optional: true }, - other: { type: 'bool', optional: true }, - note: { type: 'string', optional: true } - } +export function getBleedingDaysSortedByDate() { + return db.objects('CycleDay').filtered('bleeding != null').sorted('date', true) } - -const CycleDaySchema = { - name: 'CycleDay', - primaryKey: 'date', - properties: { - date: 'string', - temperature: { - type: 'Temperature', - optional: true - }, - bleeding: { - type: 'Bleeding', - optional: true - }, - mucus: { - type: 'Mucus', - optional: true - }, - cervix: { - type: 'Cervix', - optional: true - }, - note: { - type: 'Note', - optional: true - }, - desire: { - type: 'Desire', - optional: true - }, - sex: { - type: 'Sex', - optional: true - }, - pain: { - type: 'Pain', - optional: true - } - } +export function getTemperatureDaysSortedByDate() { + return db.objects('CycleDay').filtered('temperature != null').sorted('date', true) } - -const realmConfig = { - schema: [ - CycleDaySchema, - TemperatureSchema, - BleedingSchema, - MucusSchema, - CervixSchema, - NoteSchema, - DesireSchema, - SexSchema, - PainSchema - ], - // we only want this in dev mode - deleteRealmIfMigrationNeeded: true +export function getCycleDaysSortedByDate() { + return db.objects('CycleDay').sorted('date', true) } -const db = new Realm(realmConfig) - -const bleedingDaysSortedByDate = db.objects('CycleDay').filtered('bleeding != null').sorted('date', true) -const temperatureDaysSortedByDate = db.objects('CycleDay').filtered('temperature != null').sorted('date', true) -const cycleDaysSortedByDate = db.objects('CycleDay').sorted('date', true) - -function saveSymptom(symptom, cycleDay, val) { +export function saveSymptom(symptom, cycleDay, val) { db.write(() => { cycleDay[symptom] = val }) } -function getOrCreateCycleDay(localDate) { +export function getOrCreateCycleDay(localDate) { let result = db.objectForPrimaryKey('CycleDay', localDate) if (!result) { db.write(() => { @@ -178,11 +46,11 @@ function getOrCreateCycleDay(localDate) { return result } -function getCycleDay(localDate) { +export function getCycleDay(localDate) { return db.objectForPrimaryKey('CycleDay', localDate) } -function fillWithMucusDummyData() { +export function fillWithMucusDummyData() { const dummyCycles = [ cycleWithFhmMucus, longAndComplicatedCycleWithMucus, @@ -207,7 +75,7 @@ function fillWithMucusDummyData() { }) } -function fillWithCervixDummyData() { +export function fillWithCervixDummyData() { const dummyCycles = [ cycleWithFhmCervix, longAndComplicatedCycleWithCervix, @@ -232,16 +100,9 @@ function fillWithCervixDummyData() { }) } - -function deleteAll() { - db.write(() => { - db.deleteAll() - }) -} - -function getPreviousTemperature(cycleDay) { +export function getPreviousTemperature(cycleDay) { cycleDay.wrappedDate = LocalDate.parse(cycleDay.date) - const winner = temperatureDaysSortedByDate.find(day => { + const winner = getTemperatureDaysSortedByDate().find(day => { const wrappedDate = LocalDate.parse(day.date) return wrappedDate.isBefore(cycleDay.wrappedDate) }) @@ -249,12 +110,7 @@ function getPreviousTemperature(cycleDay) { return winner.temperature.value } -const schema = db.schema.reduce((acc, curr) => { - acc[curr.name] = curr.properties - return acc -}, {}) - -function tryToCreateCycleDay(day, i) { +export function tryToCreateCycleDay(day, i) { try { db.create('CycleDay', day) } catch (err) { @@ -263,7 +119,8 @@ function tryToCreateCycleDay(day, i) { } } -function getAmountOfCycleDays() { +export function getAmountOfCycleDays() { + const cycleDaysSortedByDate = getCycleDaysSortedByDate() const amountOfCycleDays = cycleDaysSortedByDate.length if (!amountOfCycleDays) return 0 const earliest = cycleDaysSortedByDate[amountOfCycleDays - 1] @@ -272,14 +129,21 @@ function getAmountOfCycleDays() { return earliestAsLocalDate.until(today, ChronoUnit.DAYS) } -function tryToImportWithDelete(cycleDays) { +export function getSchema() { + return db.schema.reduce((acc, curr) => { + acc[curr.name] = curr.properties + return acc + }, {}) +} + +export function tryToImportWithDelete(cycleDays) { db.write(() => { db.delete(db.objects('CycleDay')) cycleDays.forEach(tryToCreateCycleDay) }) } -function tryToImportWithoutDelete(cycleDays) { +export function tryToImportWithoutDelete(cycleDays) { db.write(() => { cycleDays.forEach((day, i) => { const existing = getCycleDay(day.date) @@ -289,19 +153,56 @@ function tryToImportWithoutDelete(cycleDays) { }) } -export { - saveSymptom, - getOrCreateCycleDay, - bleedingDaysSortedByDate, - temperatureDaysSortedByDate, - cycleDaysSortedByDate, - fillWithMucusDummyData, - fillWithCervixDummyData, - deleteAll, - getPreviousTemperature, - getCycleDay, - getAmountOfCycleDays, - schema, - tryToImportWithDelete, - tryToImportWithoutDelete +export function requestHash(type, pw) { + nodejs.channel.post('request-SHA512', JSON.stringify({ + type: type, + message: pw + })) +} + +export async function openDb ({ hash, persistConnection }) { + if (hash) { + realmConfig.encryptionKey = hashToInt8Array(hash) + } + + const connection = await Realm.open(realmConfig) + + if (persistConnection) db = connection +} + +export async function changeEncryptionAndRestartApp(hash) { + let key + if (hash) key = hashToInt8Array(hash) + const defaultPath = db.path + const dir = db.path.split('/') + dir.pop() + dir.push('copied.realm') + const copyPath = dir.join('/') + const exists = await fs.exists(copyPath) + if (exists) await fs.unlink(copyPath) + // for some reason, realm complains if we give it a key with value undefined + if (key) { + db.writeCopyTo(copyPath, key) + } else { + db.writeCopyTo(copyPath) + } + db.close() + await fs.unlink(defaultPath) + await fs.moveFile(copyPath, defaultPath) + restart.Restart() +} + +export async function deleteDbAndOpenNew() { + const exists = await fs.exists(Realm.defaultPath) + if (exists) await fs.unlink(Realm.defaultPath) + await openDb({ persistConnection: true }) +} + +function hashToInt8Array(hash) { + const key = new Uint8Array(64) + for (let i = 0; i < key.length; i++) { + const twoDigitHex = hash.slice(i * 2, i * 2 + 2) + key[i] = parseInt(twoDigitHex, 16) + } + return key } diff --git a/db/schema.js b/db/schema.js new file mode 100644 index 0000000000000000000000000000000000000000..8e8792ac0e67adc96bb51a96e92f8ed666f888ed --- /dev/null +++ b/db/schema.js @@ -0,0 +1,140 @@ +const TemperatureSchema = { + name: 'Temperature', + properties: { + value: 'double', + exclude: 'bool', + time: { + type: 'string', + optional: true + }, + note: { + type: 'string', + optional: true + } + } +} + +const BleedingSchema = { + name: 'Bleeding', + properties: { + value: 'int', + exclude: 'bool' + } +} + +const MucusSchema = { + name: 'Mucus', + properties: { + feeling: 'int', + texture: 'int', + value: 'int', + exclude: 'bool' + } +} + +const CervixSchema = { + name: 'Cervix', + properties: { + opening: 'int', + firmness: 'int', + position: {type: 'int', optional: true }, + exclude: 'bool' + } +} + +const NoteSchema = { + name: 'Note', + properties: { + value: 'string' + } +} + +const DesireSchema = { + name: 'Desire', + properties: { + value: 'int' + } +} + +const SexSchema = { + name: 'Sex', + properties: { + solo: { type: 'bool', optional: true }, + partner: { type: 'bool', optional: true }, + condom: { type: 'bool', optional: true }, + pill: { type: 'bool', optional: true }, + iud: { type: 'bool', optional: true }, + patch: { type: 'bool', optional: true }, + ring: { type: 'bool', optional: true }, + implant: { type: 'bool', optional: true }, + other: { type: 'bool', optional: true }, + note: { type: 'string', optional: true } + } +} + +const PainSchema = { + name: 'Pain', + properties: { + cramps: { type: 'bool', optional: true }, + ovulationPain: { type: 'bool', optional: true }, + headache: { type: 'bool', optional: true }, + backache: { type: 'bool', optional: true }, + nausea: { type: 'bool', optional: true }, + tenderBreasts: { type: 'bool', optional: true }, + migraine: { type: 'bool', optional: true }, + other: { type: 'bool', optional: true }, + note: { type: 'string', optional: true } + } +} + +const CycleDaySchema = { + name: 'CycleDay', + primaryKey: 'date', + properties: { + date: 'string', + temperature: { + type: 'Temperature', + optional: true + }, + bleeding: { + type: 'Bleeding', + optional: true + }, + mucus: { + type: 'Mucus', + optional: true + }, + cervix: { + type: 'Cervix', + optional: true + }, + note: { + type: 'Note', + optional: true + }, + desire: { + type: 'Desire', + optional: true + }, + sex: { + type: 'Sex', + optional: true + }, + pain: { + type: 'Pain', + optional: true + } + } +} + +export default [ + CycleDaySchema, + TemperatureSchema, + BleedingSchema, + MucusSchema, + CervixSchema, + NoteSchema, + DesireSchema, + SexSchema, + PainSchema +] \ No newline at end of file diff --git a/index.js b/index.js index 25e2616636a3d34fa039d3c945d85e78e560efef..9ff353c3a1c179826a10e2efb5d5974b0e6f6986 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ import { AppRegistry } from 'react-native' -import App from './components/app' +import AppWrapper from './components/app-wrapper' -AppRegistry.registerComponent('home', () => App) \ No newline at end of file +AppRegistry.registerComponent('home', () => AppWrapper) \ No newline at end of file diff --git a/ios/drip.xcodeproj/project.pbxproj b/ios/drip.xcodeproj/project.pbxproj index 25f1f7601b795048c76bb96a0d0b2d95dddf0838..34660c7692480809a78b319432f97ebd74bd4583 100644 --- a/ios/drip.xcodeproj/project.pbxproj +++ b/ios/drip.xcodeproj/project.pbxproj @@ -58,6 +58,12 @@ A1410AC4C98A49B2820D9E45 /* Zocial.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B6F5078F7DEC470782757471 /* Zocial.ttf */; }; 29DF0CCC1AEA4C92BCA0BCCD /* libRNDocumentPicker.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D211D71BE5A8436A978770A9 /* libRNDocumentPicker.a */; }; 17AD822C42A44BADA96BD860 /* libRNFS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 84CCEBD3B2C44758853BC941 /* libRNFS.a */; }; + 72DA6B4241504DB096AFAD40 /* libRCTRestart.a in Frameworks */ = {isa = PBXBuildFile; fileRef = AB636AA0286D45CE9B23B2C3 /* libRCTRestart.a */; }; + E09F3B05A4F84E9883101CC7 /* libRNNodeJsMobile.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F992F2D99E614DD79FAD6565 /* libRNNodeJsMobile.a */; }; + E43EF009AC8C4698AB322190 /* NodeMobile.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C225FC4966694B9FBD32E946 /* NodeMobile.framework */; }; + 8EA186B6112C41D1B206762D /* NodeMobile.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C225FC4966694B9FBD32E946 /* NodeMobile.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + E4584E55EEC24302A3E84A23 /* nodejs-project in Resources */ = {isa = PBXBuildFile; fileRef = 6466AE2461BE4FA88B8372F0 /* nodejs-project */; }; + A16B351C3F3644CF95F104D2 /* builtin_modules in Resources */ = {isa = PBXBuildFile; fileRef = 36F1B55D0DEE47AA9AF4BBDD /* builtin_modules */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -389,6 +395,13 @@ D211D71BE5A8436A978770A9 /* libRNDocumentPicker.a */ = {isa = PBXFileReference; name = "libRNDocumentPicker.a"; path = "libRNDocumentPicker.a"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; }; 49089E09BFCF4F3DB209B6E9 /* RNFS.xcodeproj */ = {isa = PBXFileReference; name = "RNFS.xcodeproj"; path = "../node_modules/react-native-fs/RNFS.xcodeproj"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; }; 84CCEBD3B2C44758853BC941 /* libRNFS.a */ = {isa = PBXFileReference; name = "libRNFS.a"; path = "libRNFS.a"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; }; + 50DBC4BCDDF74A10AEDC99D5 /* RCTRestart.xcodeproj */ = {isa = PBXFileReference; name = "RCTRestart.xcodeproj"; path = "../node_modules/react-native-restart/ios/RCTRestart.xcodeproj"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; }; + AB636AA0286D45CE9B23B2C3 /* libRCTRestart.a */ = {isa = PBXFileReference; name = "libRCTRestart.a"; path = "libRCTRestart.a"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; }; + 65F706FAFA1444AE9937D472 /* RNNodeJsMobile.xcodeproj */ = {isa = PBXFileReference; name = "RNNodeJsMobile.xcodeproj"; path = "../node_modules/nodejs-mobile-react-native/ios/RNNodeJsMobile.xcodeproj"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; }; + F992F2D99E614DD79FAD6565 /* libRNNodeJsMobile.a */ = {isa = PBXFileReference; name = "libRNNodeJsMobile.a"; path = "libRNNodeJsMobile.a"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; }; + C225FC4966694B9FBD32E946 /* NodeMobile.framework */ = {isa = PBXFileReference; name = "NodeMobile.framework"; path = "../node_modules/nodejs-mobile-react-native/ios/NodeMobile.framework"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = wrapper.framework; explicitFileType = undefined; includeInIndex = 0; }; + 6466AE2461BE4FA88B8372F0 /* nodejs-project */ = {isa = PBXFileReference; name = "nodejs-project"; path = "../nodejs-assets/nodejs-project"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; + 36F1B55D0DEE47AA9AF4BBDD /* builtin_modules */ = {isa = PBXFileReference; name = "builtin_modules"; path = "../node_modules/nodejs-mobile-react-native/install/resources/nodejs-modules/builtin_modules"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -424,6 +437,9 @@ AED64B7892744F21B3A156BB /* libRNVectorIcons.a in Frameworks */, 29DF0CCC1AEA4C92BCA0BCCD /* libRNDocumentPicker.a in Frameworks */, 17AD822C42A44BADA96BD860 /* libRNFS.a in Frameworks */, + 72DA6B4241504DB096AFAD40 /* libRCTRestart.a in Frameworks */, + E09F3B05A4F84E9883101CC7 /* libRNNodeJsMobile.a in Frameworks */, + E43EF009AC8C4698AB322190 /* NodeMobile.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -576,6 +592,7 @@ 2D16E6891FA4F8E400B85C8A /* libReact.a */, 9AEBF0735214455AAEDF56D5 /* libc++.tbd */, CD8C8B91E0A747B3883A0D56 /* libz.tbd */, + C225FC4966694B9FBD32E946 /* NodeMobile.framework */, ); name = Frameworks; sourceTree = "<group>"; @@ -618,6 +635,8 @@ D1E5ACC4B66345868F556374 /* RNVectorIcons.xcodeproj */, 1F05FE29622E4F21AF70C2B7 /* RNDocumentPicker.xcodeproj */, 49089E09BFCF4F3DB209B6E9 /* RNFS.xcodeproj */, + 50DBC4BCDDF74A10AEDC99D5 /* RCTRestart.xcodeproj */, + 65F706FAFA1444AE9937D472 /* RNNodeJsMobile.xcodeproj */, ); name = Libraries; sourceTree = "<group>"; @@ -640,6 +659,8 @@ 83CBBA001A601CBA00E9B192 /* Products */, 2D16E6871FA4F8E400B85C8A /* Frameworks */, 006C39A0B9774387BC5ACA43 /* Resources */, + 6466AE2461BE4FA88B8372F0 /* nodejs-project */, + 36F1B55D0DEE47AA9AF4BBDD /* builtin_modules */, ); indentWidth = 2; sourceTree = "<group>"; @@ -717,6 +738,10 @@ 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, + 2B572382D4504B8FB4B9D251 /* Embed Frameworks */, + 7DDFD19623084447885928A6 /* Build NodeJS Mobile Native Modules */, + 554E2494DF2646B083F4BD1D /* Sign NodeJS Mobile Native Modules */, + 8F5D6E75B7D344BD80BC6EC0 /* Remove NodeJS Mobile Framework Simulator Strips */, ); buildRules = ( ); @@ -1148,6 +1173,8 @@ DB91E6CCC3EB4A549D947797 /* Octicons.ttf in Resources */, 3DF2498A20844F298CD84CC3 /* SimpleLineIcons.ttf in Resources */, A1410AC4C98A49B2820D9E45 /* Zocial.ttf in Resources */, + E4584E55EEC24302A3E84A23 /* nodejs-project in Resources */, + A16B351C3F3644CF95F104D2 /* builtin_modules in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1197,6 +1224,160 @@ shellPath = /bin/sh; shellScript = "export NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh"; }; + 7DDFD19623084447885928A6 /* Build NodeJS Mobile Native Modules */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + name = "Build NodeJS Mobile Native Modules"; + inputPaths = ( + ); + outputPaths = ( + ); + shellPath = /bin/sh; + shellScript = " +set -e +if [ -z \"$NODEJS_MOBILE_BUILD_NATIVE_MODULES\" ]; then +# If build native modules preference is not set, look for it in the project's +#nodejs-assets/BUILD_NATIVE_MODULES.txt file. +NODEJS_ASSETS_DIR=\"$( cd \"$PROJECT_DIR\" && cd ../nodejs-assets/ && pwd )\" +PREFERENCE_FILE_PATH=\"$NODEJS_ASSETS_DIR/BUILD_NATIVE_MODULES.txt\" + if [ -f \"$PREFERENCE_FILE_PATH\" ]; then + NODEJS_MOBILE_BUILD_NATIVE_MODULES=\"$(cat $PREFERENCE_FILE_PATH | xargs)\" + fi +fi +if [ -z \"$NODEJS_MOBILE_BUILD_NATIVE_MODULES\" ]; then +# If build native modules preference is not set, try to find .gyp files +#to turn it on. + gypfiles=($(find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -type f -name \"*.gyp\")) + if [ ${#gypfiles[@]} -gt 0 ]; then + NODEJS_MOBILE_BUILD_NATIVE_MODULES=1 + else + NODEJS_MOBILE_BUILD_NATIVE_MODULES=0 + fi +fi +if [ \"1\" != \"$NODEJS_MOBILE_BUILD_NATIVE_MODULES\" ]; then exit 0; fi +# Delete object files that may already come from within the npm package. +find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.o\" -type f -delete +find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.a\" -type f -delete +find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.node\" -type f -delete +# Delete bundle contents that may be there from previous builds. +find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -path \"*/*.node/*\" -delete +find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.node\" -type d -delete +find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -path \"*/*.framework/*\" -delete +find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.framework\" -type d -delete +# Apply patches to the modules package.json +if [ -d \"$CODESIGNING_FOLDER_PATH\"/nodejs-project/node_modules/ ]; then + PATCH_SCRIPT_DIR=\"$( cd \"$PROJECT_DIR\" && cd ../node_modules/nodejs-mobile-react-native/scripts/ && pwd )\" + NODEJS_PROJECT_MODULES_DIR=\"$( cd \"$CODESIGNING_FOLDER_PATH\" && cd nodejs-project/node_modules/ && pwd )\" + node \"$PATCH_SCRIPT_DIR\"/patch-package.js $NODEJS_PROJECT_MODULES_DIR +fi +# Get the nodejs-mobile-gyp location +if [ -d \"$PROJECT_DIR/../node_modules/nodejs-mobile-gyp/\" ]; then + NODEJS_MOBILE_GYP_DIR=\"$( cd \"$PROJECT_DIR\" && cd ../node_modules/nodejs-mobile-gyp/ && pwd )\" +else + NODEJS_MOBILE_GYP_DIR=\"$( cd \"$PROJECT_DIR\" && cd ../node_modules/nodejs-mobile-react-native/node_modules/nodejs-mobile-gyp/ && pwd )\" +fi +NODEJS_MOBILE_GYP_BIN_FILE=\"$NODEJS_MOBILE_GYP_DIR\"/bin/node-gyp.js +# Rebuild modules with right environment +NODEJS_HEADERS_DIR=\"$( cd \"$PROJECT_DIR\" && cd ../node_modules/nodejs-mobile-react-native/ios/libnode/ && pwd )\" +pushd $CODESIGNING_FOLDER_PATH/nodejs-project/ +if [ \"$PLATFORM_NAME\" == \"iphoneos\" ] +then + GYP_DEFINES=\"OS=ios\" npm_config_nodedir=\"$NODEJS_HEADERS_DIR\" npm_config_node_gyp=\"$NODEJS_MOBILE_GYP_BIN_FILE\" npm_config_platform=\"ios\" npm_config_format=\"make-ios\" npm_config_node_engine=\"chakracore\" npm_config_arch=\"arm64\" npm --verbose rebuild --build-from-source +else + GYP_DEFINES=\"OS=ios\" npm_config_nodedir=\"$NODEJS_HEADERS_DIR\" npm_config_node_gyp=\"$NODEJS_MOBILE_GYP_BIN_FILE\" npm_config_platform=\"ios\" npm_config_format=\"make-ios\" npm_config_node_engine=\"chakracore\" npm_config_arch=\"x64\" npm --verbose rebuild --build-from-source +fi +popd +"; + }; + 554E2494DF2646B083F4BD1D /* Sign NodeJS Mobile Native Modules */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + name = "Sign NodeJS Mobile Native Modules"; + inputPaths = ( + ); + outputPaths = ( + ); + shellPath = /bin/sh; + shellScript = " +set -e +if [ -z \"$NODEJS_MOBILE_BUILD_NATIVE_MODULES\" ]; then +# If build native modules preference is not set, look for it in the project's +#nodejs-assets/BUILD_NATIVE_MODULES.txt file. +NODEJS_ASSETS_DIR=\"$( cd \"$PROJECT_DIR\" && cd ../nodejs-assets/ && pwd )\" +PREFERENCE_FILE_PATH=\"$NODEJS_ASSETS_DIR/BUILD_NATIVE_MODULES.txt\" + if [ -f \"$PREFERENCE_FILE_PATH\" ]; then + NODEJS_MOBILE_BUILD_NATIVE_MODULES=\"$(cat $PREFERENCE_FILE_PATH | xargs)\" + fi +fi +if [ -z \"$NODEJS_MOBILE_BUILD_NATIVE_MODULES\" ]; then +# If build native modules preference is not set, try to find .gyp files +#to turn it on. + gypfiles=($(find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -type f -name \"*.gyp\")) + if [ ${#gypfiles[@]} -gt 0 ]; then + NODEJS_MOBILE_BUILD_NATIVE_MODULES=1 + else + NODEJS_MOBILE_BUILD_NATIVE_MODULES=0 + fi +fi +if [ \"1\" != \"$NODEJS_MOBILE_BUILD_NATIVE_MODULES\" ]; then exit 0; fi +# Delete object files +find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.o\" -type f -delete +find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.a\" -type f -delete +# Create Info.plist for each framework built and loader override. +PATCH_SCRIPT_DIR=\"$( cd \"$PROJECT_DIR\" && cd ../node_modules/nodejs-mobile-react-native/scripts/ && pwd )\" +NODEJS_PROJECT_DIR=\"$( cd \"$CODESIGNING_FOLDER_PATH\" && cd nodejs-project/ && pwd )\" +node \"$PATCH_SCRIPT_DIR\"/ios-create-plists-and-dlopen-override.js $NODEJS_PROJECT_DIR +# Embed every resulting .framework in the application and delete them afterwards. +embed_framework() +{ + FRAMEWORK_NAME=\"$(basename \"$1\")\" + cp -r \"$1\" \"$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH/\" + + /usr/bin/codesign --force --sign $EXPANDED_CODE_SIGN_IDENTITY --preserve-metadata=identifier,entitlements,flags --timestamp=none \"$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH/$FRAMEWORK_NAME\" +} +find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.framework\" -type d | while read frmwrk_path; do embed_framework \"$frmwrk_path\"; done + +#Delete gyp temporary .deps dependency folders from the project structure. +find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -path \"*/.deps/*\" -delete +find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \".deps\" -type d -delete + +#Delete frameworks from their build paths +find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -path \"*/*.framework/*\" -delete +find \"$CODESIGNING_FOLDER_PATH/nodejs-project/\" -name \"*.framework\" -type d -delete +"; + }; + 8F5D6E75B7D344BD80BC6EC0 /* Remove NodeJS Mobile Framework Simulator Strips */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + name = "Remove NodeJS Mobile Framework Simulator Strips"; + inputPaths = ( + ); + outputPaths = ( + ); + shellPath = /bin/sh; + shellScript = " +set -e +FRAMEWORK_BINARY_PATH=\"$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH/NodeMobile.framework/NodeMobile\" +FRAMEWORK_STRIPPED_PATH=\"$FRAMEWORK_BINARY_PATH-strip\" +if [ \"$PLATFORM_NAME\" != \"iphonesimulator\" ]; then + if $(lipo \"$FRAMEWORK_BINARY_PATH\" -verify_arch \"x86_64\") ; then + lipo -output \"$FRAMEWORK_STRIPPED_PATH\" -remove \"x86_64\" \"$FRAMEWORK_BINARY_PATH\" + rm \"$FRAMEWORK_BINARY_PATH\" + mv \"$FRAMEWORK_STRIPPED_PATH\" \"$FRAMEWORK_BINARY_PATH\" + echo \"Removed simulator strip from NodeMobile.framework\" + fi +fi +"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -1284,6 +1465,8 @@ "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", ); HEADER_SEARCH_PATHS = ( "$(inherited)", @@ -1292,7 +1475,15 @@ "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", "$(SRCROOT)/../node_modules/react-native-document-picker/ios/RNDocumentPicker", "$(SRCROOT)/../node_modules/react-native-fs/**", + "$(SRCROOT)/../node_modules/react-native-restart/ios/RCTRestart/**", + "$(SRCROOT)/../node_modules/nodejs-mobile-react-native/ios/**", + ); + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"../node_modules/nodejs-mobile-react-native/ios\"", + "\"../node_modules/nodejs-mobile-react-native/ios\"", ); + ENABLE_BITCODE = NO; }; name = Debug; }; @@ -1315,6 +1506,8 @@ "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", ); HEADER_SEARCH_PATHS = ( "$(inherited)", @@ -1323,7 +1516,15 @@ "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", "$(SRCROOT)/../node_modules/react-native-document-picker/ios/RNDocumentPicker", "$(SRCROOT)/../node_modules/react-native-fs/**", + "$(SRCROOT)/../node_modules/react-native-restart/ios/RCTRestart/**", + "$(SRCROOT)/../node_modules/nodejs-mobile-react-native/ios/**", + ); + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"../node_modules/nodejs-mobile-react-native/ios\"", + "\"../node_modules/nodejs-mobile-react-native/ios\"", ); + ENABLE_BITCODE = NO; }; name = Release; }; @@ -1349,7 +1550,14 @@ "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", "$(SRCROOT)/../node_modules/react-native-document-picker/ios/RNDocumentPicker", "$(SRCROOT)/../node_modules/react-native-fs/**", + "$(SRCROOT)/../node_modules/react-native-restart/ios/RCTRestart/**", + "$(SRCROOT)/../node_modules/nodejs-mobile-react-native/ios/**", ); + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"../node_modules/nodejs-mobile-react-native/ios\"", + ); + ENABLE_BITCODE = NO; }; name = Debug; }; @@ -1374,7 +1582,14 @@ "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", "$(SRCROOT)/../node_modules/react-native-document-picker/ios/RNDocumentPicker", "$(SRCROOT)/../node_modules/react-native-fs/**", + "$(SRCROOT)/../node_modules/react-native-restart/ios/RCTRestart/**", + "$(SRCROOT)/../node_modules/nodejs-mobile-react-native/ios/**", ); + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"../node_modules/nodejs-mobile-react-native/ios\"", + ); + ENABLE_BITCODE = NO; }; name = Release; }; @@ -1406,6 +1621,8 @@ "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", ); HEADER_SEARCH_PATHS = ( "$(inherited)", @@ -1414,7 +1631,15 @@ "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", "$(SRCROOT)/../node_modules/react-native-document-picker/ios/RNDocumentPicker", "$(SRCROOT)/../node_modules/react-native-fs/**", + "$(SRCROOT)/../node_modules/react-native-restart/ios/RCTRestart/**", + "$(SRCROOT)/../node_modules/nodejs-mobile-react-native/ios/**", + ); + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"../node_modules/nodejs-mobile-react-native/ios\"", + "\"../node_modules/nodejs-mobile-react-native/ios\"", ); + ENABLE_BITCODE = NO; }; name = Debug; }; @@ -1446,6 +1671,8 @@ "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", ); HEADER_SEARCH_PATHS = ( "$(inherited)", @@ -1454,7 +1681,15 @@ "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", "$(SRCROOT)/../node_modules/react-native-document-picker/ios/RNDocumentPicker", "$(SRCROOT)/../node_modules/react-native-fs/**", + "$(SRCROOT)/../node_modules/react-native-restart/ios/RCTRestart/**", + "$(SRCROOT)/../node_modules/nodejs-mobile-react-native/ios/**", + ); + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"../node_modules/nodejs-mobile-react-native/ios\"", + "\"../node_modules/nodejs-mobile-react-native/ios\"", ); + ENABLE_BITCODE = NO; }; name = Release; }; @@ -1485,6 +1720,8 @@ "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", ); HEADER_SEARCH_PATHS = ( "$(inherited)", @@ -1493,7 +1730,15 @@ "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", "$(SRCROOT)/../node_modules/react-native-document-picker/ios/RNDocumentPicker", "$(SRCROOT)/../node_modules/react-native-fs/**", + "$(SRCROOT)/../node_modules/react-native-restart/ios/RCTRestart/**", + "$(SRCROOT)/../node_modules/nodejs-mobile-react-native/ios/**", ); + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"../node_modules/nodejs-mobile-react-native/ios\"", + "\"../node_modules/nodejs-mobile-react-native/ios\"", + ); + ENABLE_BITCODE = NO; }; name = Debug; }; @@ -1524,6 +1769,8 @@ "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", ); HEADER_SEARCH_PATHS = ( "$(inherited)", @@ -1532,7 +1779,15 @@ "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", "$(SRCROOT)/../node_modules/react-native-document-picker/ios/RNDocumentPicker", "$(SRCROOT)/../node_modules/react-native-fs/**", + "$(SRCROOT)/../node_modules/react-native-restart/ios/RCTRestart/**", + "$(SRCROOT)/../node_modules/nodejs-mobile-react-native/ios/**", ); + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"../node_modules/nodejs-mobile-react-native/ios\"", + "\"../node_modules/nodejs-mobile-react-native/ios\"", + ); + ENABLE_BITCODE = NO; }; name = Release; }; @@ -1574,6 +1829,7 @@ MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + ENABLE_BITCODE = NO; }; name = Debug; }; @@ -1609,6 +1865,7 @@ MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; + ENABLE_BITCODE = NO; }; name = Release; }; @@ -1661,6 +1918,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 2B572382D4504B8FB4B9D251 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8EA186B6112C41D1B206762D /* NodeMobile.framework in Embed Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + name = "Embed Frameworks"; + dstPath = ""; + dstSubfolderSpec = 10; + }; +/* End PBXCopyFilesBuildPhase section */ }; rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; } diff --git a/lib/cycle.js b/lib/cycle.js index da91e2dcbe0eb8d51d03a57c798be87d4372ac4b..7deb02e0cedf79996deb5a90cd29626977055c7b 100644 --- a/lib/cycle.js +++ b/lib/cycle.js @@ -13,8 +13,8 @@ export default function config(opts) { if (!opts) { // we only want to require (and run) the db module // when not running the tests - bleedingDaysSortedByDate = require('../db').bleedingDaysSortedByDate - cycleDaysSortedByDate = require('../db').cycleDaysSortedByDate + bleedingDaysSortedByDate = require('../db').getBleedingDaysSortedByDate() + cycleDaysSortedByDate = require('../db').getCycleDaysSortedByDate() maxBreakInBleeding = 1 maxCycleLength = 99 minCyclesForPrediction = 3 diff --git a/lib/import-export/export-to-csv.js b/lib/import-export/export-to-csv.js index c5bca2b3ffb4c4afd5669fa3d62c93e4ac1468ca..619c5e2b4bcc6c2e1a8bb5e497e8571543348985 100644 --- a/lib/import-export/export-to-csv.js +++ b/lib/import-export/export-to-csv.js @@ -1,9 +1,10 @@ import objectPath from 'object-path' import { Base64 } from 'js-base64' -import { cycleDaysSortedByDate } from '../../db' +import { getCycleDaysSortedByDate } from '../../db' import getColumnNamesForCsv from './get-csv-column-names' export default function makeDataURI() { + const cycleDaysSortedByDate = getCycleDaysSortedByDate() if (!cycleDaysSortedByDate.length) return null const csv = transformToCsv(cycleDaysSortedByDate) diff --git a/lib/import-export/get-csv-column-names.js b/lib/import-export/get-csv-column-names.js index 1b63943ab6b5a6b05eee6460684b62b3947f3075..3e8b425417cd212d442cb05bf2fbdeaece8b383c 100644 --- a/lib/import-export/get-csv-column-names.js +++ b/lib/import-export/get-csv-column-names.js @@ -1,9 +1,10 @@ -import { schema } from '../../db' +import { getSchema } from '../../db' export default function getColumnNamesForCsv() { return getPrefixedKeys('CycleDay') function getPrefixedKeys(schemaName, prefix) { + const schema = getSchema() const model = schema[schemaName] return Object.keys(model).reduce((acc, key) => { const prefixedKey = prefix ? [prefix, key].join('.') : key diff --git a/lib/import-export/import-from-csv.js b/lib/import-export/import-from-csv.js index 77c2444430072f4441340dcef4e732ebcf2fbf9a..c34ebc9b21b9e1f0c1860abb453b6defcccd3350 100644 --- a/lib/import-export/import-from-csv.js +++ b/lib/import-export/import-from-csv.js @@ -1,6 +1,6 @@ import csvParser from 'csvtojson' import isObject from 'isobject' -import { schema, tryToImportWithDelete, tryToImportWithoutDelete } from '../../db' +import { getSchema, tryToImportWithDelete, tryToImportWithoutDelete } from '../../db' import getColumnNamesForCsv from './get-csv-column-names' export default async function importCsv(csv, deleteFirst) { @@ -23,6 +23,7 @@ export default async function importCsv(csv, deleteFirst) { return Number(val) } + const schema = getSchema() const config = { ignoreEmpty: true, colParser: getColumnNamesForCsv().reduce((acc, colName) => { @@ -76,6 +77,7 @@ function putNullForEmptySymptoms(data) { } function getDbType(modelProperties, path) { + const schema = getSchema() if (path.length === 1) return modelProperties[path[0]].type const modelName = modelProperties[path[0]].objectType return getDbType(schema[modelName], path.slice(1)) diff --git a/lib/sympto-adapter.js b/lib/sympto-adapter.js index b6ab2171789b4e8a2cb8ea5123ff623192c071cc..363538ef2554cbff05c0e5a4f806e2140592c572 100644 --- a/lib/sympto-adapter.js +++ b/lib/sympto-adapter.js @@ -2,12 +2,6 @@ import getFertilityStatus from './sympto' import cycleModule from './cycle' import { fertilityStatus } from '../components/cycle-day/labels/labels' -const { - getCycleForDay, - getCyclesBefore, - getPreviousCycle -} = cycleModule() - export function getFertilityStatusStringForDay(dateString) { const status = getCycleStatusForDay(dateString) if (!status) return fertilityStatus.unknown @@ -28,6 +22,12 @@ export function getFertilityStatusStringForDay(dateString) { } export function getCycleStatusForDay(dateString) { + const { + getCycleForDay, + getCyclesBefore, + getPreviousCycle + } = cycleModule() + const cycle = getCycleForDay(dateString) if (!cycle) return null diff --git a/local-storage/index.js b/local-storage/index.js index 83f50e52cd5eb011196677b392055cd9cb377d44..d41d4d7f23fa8f089f87c7d66fbae7fcc265f5bc 100644 --- a/local-storage/index.js +++ b/local-storage/index.js @@ -19,12 +19,29 @@ scaleObservable((scale) => { } }) +export async function saveTempScale(scale) { + await AsyncStorage.setItem('tempScale', JSON.stringify(scale)) + scaleObservable.set(scale) +} export const tempReminderObservable = Observable() setObvWithInitValue('tempReminder', tempReminderObservable, { enabled: false }) +export async function saveTempReminder(reminder) { + await AsyncStorage.setItem('tempReminder', JSON.stringify(reminder)) + tempReminderObservable.set(reminder) +} + +export const hasEncryptionObservable = Observable() +setObvWithInitValue('hasEncryption', hasEncryptionObservable, false) + +export async function saveEncryptionFlag(bool) { + await AsyncStorage.setItem('hasEncryption', JSON.stringify(bool)) + hasEncryptionObservable.set(bool) +} + async function setObvWithInitValue(key, obv, defaultValue) { const result = await AsyncStorage.getItem(key) let value @@ -34,14 +51,4 @@ async function setObvWithInitValue(key, obv, defaultValue) { value = defaultValue } obv.set(value) -} - -export async function saveTempScale(scale) { - await AsyncStorage.setItem('tempScale', JSON.stringify(scale)) - scaleObservable.set(scale) -} - -export async function saveTempReminder(reminder) { - await AsyncStorage.setItem('tempReminder', JSON.stringify(reminder)) - tempReminderObservable.set(reminder) } \ No newline at end of file diff --git a/nodejs-assets/nodejs-project/main.js b/nodejs-assets/nodejs-project/main.js new file mode 100644 index 0000000000000000000000000000000000000000..616196dcf89768d8f71c503141e904b219bafa8a --- /dev/null +++ b/nodejs-assets/nodejs-project/main.js @@ -0,0 +1,12 @@ +// too see stdout / stderr from this process, run +// adb logcat | grep "NODEJS-MOBILE" +const rnBridge = require('rn-bridge') +const crypto = require('crypto') + +rnBridge.channel.on('request-SHA512', (msg) => { + msg = JSON.parse(msg) + const hash = crypto.createHash('sha512') + hash.update(msg.message) + const result = hash.digest('hex') + rnBridge.channel.post(msg.type, result) +}) \ No newline at end of file diff --git a/nodejs-assets/nodejs-project/package.json b/nodejs-assets/nodejs-project/package.json new file mode 100644 index 0000000000000000000000000000000000000000..331050815a18ec4952d1f2c2c0baf42835f0f7a8 --- /dev/null +++ b/nodejs-assets/nodejs-project/package.json @@ -0,0 +1,4 @@ +{ + "dependencies": {}, + "main": "main.js" +} diff --git a/package-lock.json b/package-lock.json index bb711bfba3e084ee8405a575c2e300ce60c44afe..26216ecf09f86ac6edbd8417ac9505f2c1acebe1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1088,11 +1088,6 @@ } } }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", @@ -1287,9 +1282,9 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "atob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", - "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" }, "aws-sign2": { "version": "0.6.0", @@ -1992,9 +1987,9 @@ } }, "babel-preset-fbjs": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-fbjs/-/babel-preset-fbjs-2.2.0.tgz", - "integrity": "sha512-jj0KFJDioYZMtPtZf77dQuU+Ad/1BtN0UnAYlHDa8J8f4tGXr3YrPoJImD5MdueaOPeN/jUdrCgu330EfXr0XQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/babel-preset-fbjs/-/babel-preset-fbjs-2.3.0.tgz", + "integrity": "sha512-ZOpAI1/bN0Y3J1ZAK9gRsFkHy9gGgJoDRUjtUCla/129LC7uViq9nIK22YdHfey8szohYoZY3f9L2lGOv0Edqw==", "requires": { "babel-plugin-check-es2015-constants": "^6.8.0", "babel-plugin-syntax-class-properties": "^6.8.0", @@ -2205,11 +2200,6 @@ "kind-of": "^6.0.2" } }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", @@ -2252,9 +2242,9 @@ } }, "big-integer": { - "version": "1.6.34", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.34.tgz", - "integrity": "sha512-+w6B0Uo0ZvTSzDkXjoBCTNK0oe+aVL+yPi7kwGZm8hd8+Nj1AFPoxoq1Bl/mEu/G/ivOkUc1LRqVR0XeWFUzuA==" + "version": "1.6.36", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.36.tgz", + "integrity": "sha512-t70bfa7HYEA1D9idDbmuv7YbsbVkQ+Hp+8KFSul4aE5e/i1bjCNIRYJZlA8Q8p0r9T8cF/RVvwUgRA//FydEyg==" }, "bl": { "version": "1.2.2", @@ -2405,13 +2395,6 @@ "to-object-path": "^0.3.0", "union-value": "^1.0.0", "unset-value": "^1.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } } }, "caller-path": { @@ -2484,6 +2467,11 @@ "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", "dev": true }, + "chownr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", + "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=" + }, "circular-json": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", @@ -2513,11 +2501,6 @@ "requires": { "is-descriptor": "^0.1.0" } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" } } }, @@ -2620,9 +2603,9 @@ } }, "commander": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.16.0.tgz", - "integrity": "sha512-sVXqklSaotK9at437sFlFpyOcJonxe0yST/AG9DkQKUdIE6IqGIMv4SfAQSKaJbSdVEJYItASCrBiVQHq1HQew==" + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.18.0.tgz", + "integrity": "sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ==" }, "commondir": { "version": "1.0.1", @@ -2696,9 +2679,12 @@ "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "convert-source-map": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", - "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=" + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "requires": { + "safe-buffer": "~5.1.1" + } }, "copy-descriptor": { "version": "0.1.1", @@ -2964,11 +2950,6 @@ "kind-of": "^6.0.2" } }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", @@ -3736,13 +3717,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3755,18 +3734,15 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -3869,8 +3845,7 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -3880,7 +3855,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3893,20 +3867,17 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true, - "optional": true + "bundled": true }, "minipass": { "version": "2.2.4", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -3923,7 +3894,6 @@ "mkdirp": { "version": "0.5.1", "bundled": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -3996,8 +3966,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -4007,7 +3976,6 @@ "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } @@ -4113,7 +4081,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4394,13 +4361,6 @@ "get-value": "^2.0.6", "has-values": "^1.0.0", "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } } }, "has-values": { @@ -4834,13 +4794,6 @@ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "requires": { "isobject": "^3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } } }, "is-posix-bracket": { @@ -5567,6 +5520,30 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, + "minipass": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.4.tgz", + "integrity": "sha512-mlouk1OHlaUE8Odt1drMtG1bAJA4ZA6B/ehysgV0LUIrDHdKgo1KorZq3pK0b/7Z7LJIQ12MNM6aC+Tn6lUZ5w==", + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + }, + "dependencies": { + "yallist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" + } + } + }, + "minizlib": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz", + "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", + "requires": { + "minipass": "^2.2.1" + } + }, "mixin-deep": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", @@ -5674,9 +5651,9 @@ "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" }, "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz", + "integrity": "sha512-F4miItu2rGnV2ySkXOQoA8FKz/SR2Q2sWP0sbTxNxz/tuokeC8WxOhPMcwi0qIyGtVn/rrSeLbvVkznqCdwYnw==", "optional": true }, "nanomatch": { @@ -5737,6 +5714,11 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=" + }, "negotiator": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", @@ -5865,6 +5847,68 @@ } } }, + "nodejs-mobile-gyp": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/nodejs-mobile-gyp/-/nodejs-mobile-gyp-0.2.0.tgz", + "integrity": "sha512-zdeI4UhZUaDbwfOSS7ZgtKzagNb7nUiqV/Es67LKixiSIMSE1+9fwoVu9TVpCPFgizsoxRSxaxtSmZlL0yRmYA==", + "requires": { + "glob": "^7.0.3", + "graceful-fs": "^4.1.2", + "minimatch": "^3.0.2", + "mkdirp": "^0.5.0", + "nopt": "2 || 3", + "npmlog": "0 || 1 || 2 || 3 || 4", + "osenv": "0", + "request": "2", + "rimraf": "2", + "semver": "~5.3.0", + "tar": "^3.1.3", + "which": "1" + }, + "dependencies": { + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "requires": { + "abbrev": "1" + } + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" + }, + "tar": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-3.2.1.tgz", + "integrity": "sha512-ZSzds1E0IqutvMU8HxjMaU8eB7urw2fGwTq88ukDOVuUIh0656l7/P7LiVPxhO5kS4flcRJQk8USG+cghQbTUQ==", + "requires": { + "chownr": "^1.0.1", + "minipass": "^2.0.2", + "minizlib": "^1.0.3", + "mkdirp": "^0.5.0", + "yallist": "^3.0.2" + } + }, + "yallist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" + } + } + }, + "nodejs-mobile-react-native": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/nodejs-mobile-react-native/-/nodejs-mobile-react-native-0.3.0.tgz", + "integrity": "sha512-cY0tF27Mf3VbIGA8xz/c6j7WlP1b8sapYfb+SZUbjYxztYH1YFGdjymAQ/s4vtvK0rXf2h1WXMRjRw6nu4jjKA==", + "requires": { + "mkdirp": "^0.5.1", + "ncp": "^2.0.0", + "nodejs-mobile-gyp": "^0.2.0", + "xcode": "^0.9.3" + } + }, "nopt": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", @@ -5971,13 +6015,6 @@ "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", "requires": { "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } } }, "object.omit": { @@ -5995,13 +6032,6 @@ "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", "requires": { "isobject": "^3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } } }, "obv": { @@ -6192,9 +6222,9 @@ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" }, "path-parse": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", - "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, "path-type": { "version": "2.0.0", @@ -6373,9 +6403,9 @@ "integrity": "sha512-eTPo5t/4bgaMNZxyjWx6N2a6AuE0mq51KWvpc7nU/MAqixcI6v6KrGUKES0HaomdnolQBBXU/++X6/QQ9KL4tw==" }, "randomatic": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.0.0.tgz", - "integrity": "sha512-VdxFOIEY3mNO5PtSRkkle/hPJDHvQhK21oa73K4yAc9qmp6N429gAyF1gZMOTMeS0/AYzaV/2Trcef+NaIonSA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.0.tgz", + "integrity": "sha512-KnGPVE0lo2WoXxIZ7cPR8YBpiol4gsSuOwDSg410oHh80ZMp5EiypNqL2K4Z77vJn6lB5rap7IkAmcUlalcnBQ==", "requires": { "is-number": "^4.0.0", "kind-of": "^6.0.0", @@ -6427,14 +6457,14 @@ "integrity": "sha1-K7qMaUBMXkqUQ5hgC8xMlB+GBoI=" }, "react-deep-force-update": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/react-deep-force-update/-/react-deep-force-update-1.1.1.tgz", - "integrity": "sha1-vNMUeAJ7ZLMznxCJIatSC0MT3Cw=" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/react-deep-force-update/-/react-deep-force-update-1.1.2.tgz", + "integrity": "sha512-WUSQJ4P/wWcusaH+zZmbECOk7H5N2pOIl0vzheeornkIMhu+qrNdGFm0bDZLCb0hSF0jf/kH1SgkNGfBdTc4wA==" }, "react-devtools-core": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-3.2.3.tgz", - "integrity": "sha1-o34ZnZSGXiy7YWuXvo9YIGdOar0=", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-3.3.3.tgz", + "integrity": "sha512-RftDnDpmIjNz39JQA04EmUD6wxpDW1J+zKSh25g/JOdtoe9BeoreIXbeQJcdnHBPZ4x3Dpy64nrXYzHnpO684A==", "requires": { "shell-quote": "^1.6.1", "ws": "^3.3.1" @@ -6610,6 +6640,11 @@ "resolved": "https://registry.npmjs.org/react-native-push-notification/-/react-native-push-notification-3.1.1.tgz", "integrity": "sha512-4+4yQXNPqh5IVvpSBmR4Cy/UeMjTcfE8KIJgEuT7pME97WK+aGPn6W3ybhOoXC1n+ZWKfrAlsHydLE4xfBZDJg==" }, + "react-native-restart": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/react-native-restart/-/react-native-restart-0.0.7.tgz", + "integrity": "sha512-/7TdL3pHM7H0Cut6U6SK1jd6fVYvLlQq0O4inJ7j4YD0w9BtAAFqLkLnBDOA0jL4oLw/89yR8HhLOzNFGvVsfw==" + }, "react-native-share": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-1.1.0.tgz", @@ -6863,9 +6898,9 @@ "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" }, "repeat-element": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", - "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" }, "repeat-string": { "version": "1.6.1", @@ -7337,11 +7372,6 @@ } } }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", @@ -7643,11 +7673,6 @@ "kind-of": "^6.0.2" } }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", @@ -8397,11 +8422,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" } } }, diff --git a/package.json b/package.json index a58e1298fc5a4ca487210ad176d7f3c78bf434d3..035ca5137b8034346540afcb8152588334617be1 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "js-base64": "^2.4.8", "js-joda": "^1.8.2", "moment": "^2.22.2", + "nodejs-mobile-react-native": "^0.3.0", "object-path": "^0.11.4", "obv": "0.0.1", "react": "16.4.1", @@ -32,6 +33,7 @@ "react-native-fs": "^2.10.14", "react-native-modal-datetime-picker-nevo": "^4.11.0", "react-native-push-notification": "^3.1.1", + "react-native-restart": "0.0.7", "react-native-share": "^1.1.0", "react-native-vector-icons": "^5.0.0", "realm": "^2.7.1" diff --git a/styles/index.js b/styles/index.js index 629533a32db7782984d0e7479a7beb79b1b9386d..ee63b83cb349ba7716827c8c74183b85b8fdc1d7 100644 --- a/styles/index.js +++ b/styles/index.js @@ -170,7 +170,7 @@ export default StyleSheet.create({ backgroundColor: secondaryColor, padding: 10, alignItems: 'center', - margin: 10 + margin: 10, }, settingsButtonText: { color: fontOnPrimaryColor @@ -249,6 +249,42 @@ export default StyleSheet.create({ fontSize: 20, color: secondaryColor, marginTop: 1 + }, + passwordField: { + padding: 10, + marginTop: 10, + marginHorizontal: 10, + backgroundColor: 'white' + }, + passwordPromptPage: { + padding: 30, + alignItems: 'center' + }, + passwordPromptField: { + padding: 10, + marginTop: 10, + marginHorizontal: 10, + borderBottomWidth: 3, + borderBottomColor: primaryColor, + width: '100%', + fontSize: 20, + marginVertical: 20 + }, + passwordPromptButton: { + backgroundColor: secondaryColor, + padding: 10, + alignItems: 'center', + margin: 10, + width: '100%', + borderRadius: 10 + }, + passwordPromptButtonText: { + color: fontOnPrimaryColor, + fontSize: 20 + }, + passwordPromptForgotPasswordText: { + marginTop: 20, + color: 'grey' } }) @@ -268,6 +304,6 @@ export const iconStyles = { color: fontOnPrimaryColor }, menuIconInactive: { - color: 'lightgrey' + color: 'lightgrey', }, } \ No newline at end of file