diff --git a/.eslintrc.js b/.eslintrc.js index c86b6f8434..5a1f5eff8d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,10 +3,11 @@ module.exports = { extends: [ 'eslint:recommended', 'plugin:react/recommended', + 'plugin:react-hooks/recommended', 'plugin:jest/recommended', ], parser: 'babel-eslint', - plugins: ['react', 'react-native', 'detox'], + plugins: ['react', 'react-hooks', 'react-native', 'detox'], parserOptions: { ecmaVersion: 6, sourceType: 'module', diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6107a0b784..cab9b30998 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1,6 @@ - + + +#### Description: @@ -10,6 +12,6 @@ -#### How to test +#### How to test: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3b0205b308..5a58d8e3e3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,10 @@ We welcome participation in an open project. We want to make it as easy as possible for people to work together, so please follow these guidelines to prepare and submit a pull request. -## How to prepare +- Filtering by [good first issues](https://github.com/tripleblindmarket/covid-safe-paths/labels/good%20first%20issue) is the recommended way to begin contributing to the project +- Localization/Language updates are handled in [Lokalise](https://lokalise.com/). More info on how to contribute [here](https://github.com/tripleblindmarket/covid-safe-paths/wiki/Localization-Contributing-Guidelines) + +## Create A New Issue - You need a Github account. You can [create one](https://github.com/signup/free) for free. @@ -23,13 +26,13 @@ We welcome participation in an open project. We want to make it as easy as possi cd ~ # get to your home directory or where ever you want to go -git clone https://github.com/YOURACCOUNT/covid-safe-paths +git clone git@github.com:YOURACCOUNT/covid-safe-paths.git # change into the newly created directory cd covid-safe-paths # set upstream against COVID Safe Paths repository -git remote add upstream https://github.com/tripleblindmarket/covid-safe-paths.git +git remote add upstream git@github.com:tripleblindmarket/covid-safe-paths.git ``` @@ -37,57 +40,57 @@ git remote add upstream https://github.com/tripleblindmarket/covid-safe-paths.gi ## Make Changes -1. Create a branch based on the `develop` branch on your forked repository. Name the branch something to reflect what you are doing. For example, if you want to add a new icon, a branch name you could use: - -```bash -git checkout develop # you want to branch from the main 'develop' branch - -git pull # make sure you have the latest code when you start the branch - -git checkout -b "feature/new-icon" develop # new branch created! - -"or" - -git checkout -b "fix/new-icon" develop # new branch created! - -"or" - -git checkout -b "release/new-icon" develop # new branch created! -``` - -2. Stick to the coding style and patterns that are used already. - -3. Document code! Comments are good. More comments are better. :) - -4. Make commits as you desire. Ultimately they will be squashed, so make - -notes to yourself. It's as simple as `git commit -m "commit message goes here"`! - -5. Rebase your feature branch with upstream/develop to avoid any code conflicts: +### Create a branch + +1. Always create a new branch from the latest `upstream/develop`: + ```bash + git checkout develop # you want to branch from the latest 'develop' branch + + git pull upstream/develop # make sure you have the latest code from upstream + + git push origin develop # optional, push these changes to YOUR fork's develop branch + ``` +2. Create the branch. Name the branch something to reflect what you are doing. + ``` + git checkout -b "feature/new-icon" develop # new branch created! + + "or" + + git checkout -b "fix/new-icon" develop # new branch created! + + "or" + + git checkout -b "release/new-icon" develop # new branch created! + ``` +3. Stick to the coding style and patterns that are used already. +4. Document code! Comments are good. More comments are better. :) +5. Make commits as you desire. Ultimately they will be squashed, so make notes to yourself. It's as simple as `git commit -m "commit message goes here"`! + +### Merge upstream/develop into your branch to get the latest changes. ```bash -# 1. Rebase Base(COVID Safe Paths) repository with fork repository - develop branch - -git checkout develop # switch to base branch(local) - -git fetch upstream # fetch latest commits from "COVID Safe Paths" develop branch - -git rebase upstream/develop # rebase code against your forked develop branch(local) +# if you've already done this it will fail, that's fine: +git remote add upstream git@github.com:tripleblindmarket/covid-safe-paths.git -git push -f origin develop # push rebased code after resolving conflicts to forked develop branch(remote) +# ensure you are on your feature/fix branch +git checkout feature/my-feature -# 2. Rebase feature branch(local) with develop branch(local) +# get latest upstream branches e.g. upstream/develop +git fetch upstream -git checkout # switch back to original feature branch(local) you are working +# merge upstream/develop into your local branch, this will always create a single merge commit +git merge upstream/develop --no-ff -git rebase develop # now rebase your feature branch(local) against develop branch(local) +# you may need to resolve conflicts. If so, resolve them and commit the merge: +git commit -git push origin feature/ # after resolving all conflicts, push your new feature branch to the remote forked repository +# push your changes up to your branch again +git push -u origin # now your feature branch is ready for PR against COVID Safe Paths develop branch. ``` -6. Start a PR to submit your changes back to the original project: +### Start a PR to submit your changes back to the original project: - Visit https://github.com/your-git-userid/covid-safe-paths/branches @@ -108,25 +111,10 @@ git push origin feature/ # after resolving all conflic - Provide a meaningful title and description to your PR, as shown in the above image. - Provide Issue ID on PR description to link/close the issue upon PR merged. +- If you are changing visuals, please provide screenshots so the PR reviewer can see what you've done without running it in the app. ## Helpful resources on Git -- Git commands: - -``` -git checkout develop - -git fetch - -git reset --hard origin/develop - -git checkout - -git rebase develop - -git push -f -``` - - Documentation on how to [create a Pull Request (PR) on Github](https://help.github.com/articles/using-pull-requests/) for review and merging. **Note**: Even if you have write access, do not work directly on `master` or push directly to `develop`! All work is done against `develop` reviewed and merged via PRs, and ultimately `develop` gets merged into `master` for tagged code releases. @@ -173,6 +161,8 @@ _Advanced users may install the `hub` gem and use the [`hub pull-request` comman ## Reviewing Pull Requests +- If you are using VS Code, use the [GitHub PR extension](https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github), which will allow you to checkout and run anyone's PR with ease. + - Open the PR on Github. At the top of the PR page is a number which identifies it -123 and the name of the author's branch -branch-name. Copy down both of these. * Open git bash and ensure your working directory is clean by running `git status` diff --git a/README.md b/README.md index 9695c39f35..924892a307 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,160 @@ -# COVID Safe Paths ![Android and iOS build on MacOS](https://github.com/tripleblindmarket/covid-safe-paths/workflows/Android%20and%20iOS%20build%20on%20MacOS/badge.svg) - -**Applying the technology and philosophy of Private Kit to COVID** +
+

COVID Safe Paths

+ + + safe paths logo + + +

+ Applying the technology and philosophy of Private Kit to COVID-19 +

+ + **https://covidsafepaths.org | https://safepaths.mit.edu** +
+ +
Help us stop COVID-19. -We’re building the next generation of secure location logging to preserve privacy and #flattenthecurve +We’re building the next generation of secure location logging to preserve privacy and _#flattenthecurve_ Location logs provide time-stamped records of where users have been, allowing them to share information with health officials accurately and quickly. This helps support contact tracing efforts to slow the spread of the virus. -What’s truly special about Safe Paths, though, is its privacy protection. Data never leaves a user's device without their password entry and explicit consent. The location log generated by Safe Paths cannot be accessed from outside the user’s device, meaning data transfer occurs only if the user chooses to share it with a researcher or health official. +What’s truly special about Safe Paths, though, is its privacy protection. ----- +Data never leaves a user's device without their password entry and explicit consent. The location log generated by Safe Paths cannot be accessed from outside the user’s device, meaning data transfer occurs only if the user chooses to share it with a researcher or health official. -Safe Paths is a ‘privacy-first’ app that allows you to log your GPS trails on your own phone. The information is stored locally and never shared with anyone (not even with us or MIT) until you explicitly decide to manually export the data. The location log generated by Safe Paths cannot be accessed from outside the user’s device. Location information can be imported and exported by the user and used in other projects and applications. +## Overview + +Safe Paths is a ‘privacy-first’ app that allows you to log your GPS trails on your own phone. The information is stored locally and never shared with anyone (not even with us or MIT) until you explicitly decide to manually export the data. + +The location log generated by Safe Paths cannot be accessed from outside the user’s device. However, the user can import and export their location information and use it in other projects and applications. Safe Paths logs your device’s location once every five minutes and stores 28 days of data in under 100KB of space. -**Home page:** https://covidsafepaths.org and https://safepaths.mit.edu +### Private Kit WhitePaper + +[Apps Gone Rogue: Maintaining Personal Privacy in an Epidemic](https://drive.google.com/file/d/1nwOR4drE3YdkCkyy_HBd6giQPPhLEkRc/view?usp=sharing) + +### Downloads for COVID Safe Paths + +_coming soon!_ -**Private Kit WhitePaper:** [Apps Gone Rogue: Maintaining Personal Privacy in an Epidemic](https://drive.google.com/file/d/1nwOR4drE3YdkCkyy_HBd6giQPPhLEkRc/view?usp=sharing) +### Downloads for Private Kit (technology beta) -**Downloads for COVID Safe Paths:** _coming soon!_ +[Google Play](https://play.google.com/store/apps/details?id=edu.mit.privatekit) | [Apple Store](https://apps.apple.com/us/app/private-kit-prototype/id1501903733) -**Downloads for Private Kit (technology beta):** [Google Play](https://play.google.com/store/apps/details?id=edu.mit.privatekit) | [Apple Store](https://apps.apple.com/us/app/private-kit-prototype/id1501903733) +
# Development Overview -This is a React Native app version 61.5 +![Android and iOS build on MacOS](https://github.com/tripleblindmarket/covid-safe-paths/workflows/Android%20and%20iOS%20build%20on%20MacOS/badge.svg) + +_Safe Paths_ is a built on [React Native](https://reactnative.dev/docs/getting-started) v0.61.5 + +## Contributing + +Read the [contribution guidelines](CONTRIBUTING.md). ## Architecture -Please refer to `docs/Private Kit Diagram.png` for a basic overview on the sequencing of generalized events and services that are used by Safe Paths. +View the [architecture diagram](docs/Private_Kit_Diagram.png) for a basic overview on the sequencing of generalized events and services that are used by Safe Paths. ## Developer Setup -Run the ```dev_setup.sh``` (Linux/MacOS) or ```dev_setup.bat``` (Windows) for needed tools. It is assumed that Android Studio and/or xcode (on macOS) is set up and configured correctly to run applications in the simulator. For Android Studio, the dev_setup script can help you, but you still may need to configure at least one android virtual device (avd) from within Android Studio first before you can run the app. +First, run the appropriate setup script for your system. This will install relevant packages, walk through Android Studio configuration, etc. -## Running +**Note:** You will still need to [configure an Android Virtual Device (AVD)](https://developer.android.com/studio/run/managing-avds#createavd) after running the script. -```yarn``` must be installed for this project, as it does a better job installing dependendies across platforms. The dev_setup script above should have done that for you. +#### Linux/MacOS -### Install modules to the correct locations +``` +dev_setup.sh +``` + +#### Windows -1. ```yarn install``` (always do this after a new clone or checkout) -2. ```yarn install:pod``` (additional step for macOS if you want to do an iOS build) +``` +dev_setup.bat +``` -### Run the app in a simulator +## Running + +**Note:** In some cases, these procedures can lead to the error `Failed to load bundle - Could not connect to development server`. In these cases, kill all other react-native processes and try it again. + +#### Android (Windows, Linux, macOS) -To run in the Android simulator (Windows, Linux, macOS): ``` npx react-native run-android ``` -or (on macOS only) + +Device storage can be cleared by long-pressing on the app icon in the simulator, clicking "App info", then "Storage", and lastly, "Clear Storage". + +#### iOS (macOS only) + ``` +yarn install:pod ## only needs to be ran once npx react-native run-ios ``` -NOTE: In some cases, these procedures can lead to the error `Failed to load bundle - Could not connect to development server`. In these cases, kill all other react-native processes and try it again. +Device storage can be cleared by clicking "Hardware" on the system toolbar, and then "Erase all content and settings". -## Contributing +### Release Builds -Read the [contribution guidelines](CONTRIBUTING.md). +Generating a release build is an optional step in the development process. + +- [Android instructions](https://reactnative.dev/docs/signed-apk-android) + +### Debugging + +[react-native-debugger](https://github.com/jhen0409/react-native-debugger) is recommended. This tool will provide visibility of the JSX hierarchy, breakpoint usage, monitoring of network calls, and other common debugging tasks. + +## Testing + +Tests are ran automatically through Github actions - PRs are not able to be merged if there are tests that are failing. + +### Unit Test + +To run the unit tests: + +``` +yarn test --watch +``` + +[Snapshot testing](https://jestjs.io/docs/en/snapshot-testing) is used as a quick way to verify that the UI has not changed. To update the snapshots: + +``` +yarn update-snapshots +``` + +### e2e Test +**Note:** Right now, there is only e2e test support for iOS. + +e2e tests are written using [_detox_](https://github.com/wix/Detox). Screenshots of each test run are saved to `e2e/artifacts` for review. + +To run the e2e tests: + +``` +yarn detox-setup ## only needs to be run once +yarn build:e2e:ios ## needs to be run after any code change +yarn test:e2e:iphone{11, -se, 8} +``` +### Manual Device Testing +Mobile devices come in many different shapes and sizes - it is important to test your code on a variety of simulators to ensure it looks correct on all device types. +Before pushing up code, it is recommended to manually test your code on the following devices: +- Nexus 4 (smaller screen) +- iPhone 8 (smaller screen) +- Pixel 3 XL (larger screen) +- iPhone 11 (screen w/ notch) diff --git a/app/helpers/GoogleTakeOutAutoImport.js b/app/helpers/GoogleTakeOutAutoImport.js index 8e4d2c30b7..40fed66d75 100644 --- a/app/helpers/GoogleTakeOutAutoImport.js +++ b/app/helpers/GoogleTakeOutAutoImport.js @@ -10,6 +10,7 @@ import { mergeJSONWithLocalData } from '../helpers/GoogleData'; export class NoRecentLocationsError extends Error {} export class InvalidFileExtensionError extends Error {} +export class EmptyFilePathError extends Error {} const ZIP_EXT_CHECK_REGEX = /\.zip$/; let progress; @@ -49,6 +50,9 @@ export function getFilenamesForLatest2Months(rootPath, now) { // Imports any Takeout location data // Currently works for Google Takeout Location data export async function importTakeoutData(filePath) { + if (!filePath) { + throw new EmptyFilePathError(); + } let unifiedPath = filePath; if (Platform.OS === 'ios') { @@ -93,30 +97,19 @@ export async function importTakeoutData(filePath) { if (isExist) { console.log('[INFO] File exists:', `file://${filepath}`); - const contents = await RNFS.readFile(`file://${filepath}`).catch( - err => { - console.log( - `[INFO] Caught error on opening "file://${filepath}"`, - err, - ); - console.log( - `[INFO] Attempting to open file "file://${filepath}" again`, - err, - ); - - /** - * IMPORTANT!!! - * A temporary hack around URI generation bug in react-native-fs on android. - * An exception is thrown as `file://` is not in the file URI: - * "Error: ENOENT: No content provider: - * /data/user/0/edu.mit.privatekit/cache/Takeout-2020-04-12T15:48:54.295Z/Takeout/Location History/Semantic Location History/2020/2020_APRIL.json, - * open '/data/user/0/edu.mit.privatekit/cache/Takeout-2020-04-12T15:48:54.295Z/Takeout/Location History/Semantic Location History/2020/2020_APRIL.json' - * " - * @see https://github.com/itinance/react-native-fs/blob/master/android/src/main/java/com/rnfs/RNFSManager.java#L110 - */ - return RNFS.readFile(`file://file://${filepath}`); - }, - ); + const contents = await RNFS.readFile(`file://${filepath}`).catch(() => { + /** + * IMPORTANT!!! + * A temporary hack around URI generation bug in react-native-fs on android. + * An exception is thrown as `file://` is not in the file URI: + * "Error: ENOENT: No content provider: + * /data/user/0/edu.mit.privatekit/cache/Takeout-2020-04-12T15:48:54.295Z/Takeout/Location History/Semantic Location History/2020/2020_APRIL.json, + * open '/data/user/0/edu.mit.privatekit/cache/Takeout-2020-04-12T15:48:54.295Z/Takeout/Location History/Semantic Location History/2020/2020_APRIL.json' + * " + * @see https://github.com/itinance/react-native-fs/blob/master/android/src/main/java/com/rnfs/RNFSManager.java#L110 + */ + return RNFS.readFile(`file://file://${filepath}`); + }); newLocations = [ ...newLocations, diff --git a/app/locales/en.json b/app/locales/en.json index 8a230a2c91..223f814119 100644 --- a/app/locales/en.json +++ b/app/locales/en.json @@ -21,12 +21,18 @@ "button_text": "Import past locations", "google": { "disclaimer": "Safe Paths has no affiliation with Google and never shares your data.", - "instructions_detailed": "Visit Google Takeout and export your Location History using the following settings: \n1. Delivery method: \"Add to Drive\" \n2. Frequency: \"Export once\" \n3. File type & size: \".zip\" and \"1GB\"\n4. Google sends an email when the export is ready \n5. Return here to import locations. Import options:\n- Import from Google Drive\n- Download from browser, then import from local phone files. Make sure to be on WiFi network as files can be big.", + "instructions_detailed": "Visit Google Takeout and export your Location History using the following settings: \n1. Delivery method: \"Add to Drive\" \n2. Frequency: \"Export once\" \n3. File type & size: \".zip\" and \"1GB\"\n4. Google sends an email when the export is ready \n5. Return here to import locations from Google Drive", "instructions_first": "Adding location data from Google will give you a head start on building your recent locations.", "instructions_second": "Before you can import, you must first \"Take out\" your location data from Google.", "title": "Google Maps", - "visit_button_text": "Visit Google Takeout" + "visit_button_text": "Visit Google Takeout", + "already_imported":"Provided Takeout file has already been imported.", + "no_recent_locations": "Takeout doesn't have any recent locations.", + "invalid_file_format": "Provided file format is not supported. \nSupported formats: \".zip\".", + "file_open_error": "Could not open the file. \nPlease, make sure the file is opened from Google Drive" }, + "success":"Recent locations has been successfully imported!", + "error":"Something went wrong while importing your data.", "subtitle": "To see if you encountered someone with COVID-19 prior to downloading this app, you can import your personal location history.", "title": "Import Locations" }, diff --git a/app/views/ExposureHistory/ExposureHistory.js b/app/views/ExposureHistory/ExposureHistory.js index 8d9ed2f8fc..cec5d8c091 100644 --- a/app/views/ExposureHistory/ExposureHistory.js +++ b/app/views/ExposureHistory/ExposureHistory.js @@ -35,18 +35,18 @@ export const ExposureHistoryScreen = ({ navigation }) => { fetchData(); + const handleBackPress = () => { + navigation.goBack(); + return true; + }; + BackHandler.addEventListener('hardwareBackPress', handleBackPress); // teardown code return () => { BackHandler.removeEventListener('hardwareBackPress', handleBackPress); }; - }, []); - - const handleBackPress = () => { - navigation.goBack(); - return true; - }; + }, [navigation]); const hasExposure = history?.length && history.some(h => h.exposureMinutes > 0); diff --git a/app/views/Import.js b/app/views/Import.js index c997d47706..333c6d6842 100644 --- a/app/views/Import.js +++ b/app/views/Import.js @@ -9,13 +9,13 @@ import { View, } from 'react-native'; -import languages from './../locales/languages'; import NavigationBarWrapper from '../components/NavigationBarWrapper'; import { Typography } from '../components/Typography'; import colors from '../constants/colors'; import fontFamily from '../constants/fonts'; import { pickFile } from '../helpers/General'; import { + EmptyFilePathError, InvalidFileExtensionError, NoRecentLocationsError, importTakeoutData, @@ -33,41 +33,44 @@ const ImportScreen = props => { const { navigation: { goBack }, } = props; - const [importResults, setImportResults] = useState(makeImportResults()); - + const [importResults, _setImportResults] = useState(makeImportResults()); + const setImportResults = (...args) => + _setImportResults(makeImportResults(...args)); async function importPickFile() { try { // reset info message - setImportResults(makeImportResults()); + setImportResults(); const filePath = await pickFile(); - if (filePath) { - const newLocations = await importTakeoutData(filePath); - if (newLocations.length) { - setImportResults(makeImportResults('label.import_success')); - } else { - setImportResults(makeImportResults('label.import_already_imported')); - } + + const newLocations = await importTakeoutData(filePath); + + if (newLocations.length) { + setImportResults(t('import.success')); + } else { + setImportResults(t('import.google.already_imported')); } } catch (err) { if (err instanceof NoRecentLocationsError) { - setImportResults( - makeImportResults('label.import_no_recent_locations', true), - ); + setImportResults(t('import.google.no_recent_locations'), true); } else if (err instanceof InvalidFileExtensionError) { - setImportResults( - makeImportResults('label.import_invalid_file_format', true), - ); + setImportResults(t('import.google.invalid_file_format'), true); + } else if (err instanceof EmptyFilePathError) { + /** + * If the imported file is opened from other than Google Drive folder, + * filepath is returned as null. Leaving a message to ensure import file + * is located on Google Drive. + */ + setImportResults(t('import.google.file_open_error'), true); } else { - setImportResults(makeImportResults('label.import_error', true)); + console.log('[ERROR] Failed to import locations', err); + setImportResults(t('import.error'), true); } } } return ( - + @@ -90,7 +93,7 @@ const ImportScreen = props => { } style={styles.buttonTouchable}> - {languages.t('import.google.visit_button_text')} + {t('import.google.visit_button_text')} { onPress={importPickFile} style={styles.buttonTouchable}> - {languages.t('import.title')} + {t('import.title')} @@ -108,7 +111,7 @@ const ImportScreen = props => { ...styles.importResults, ...(importResults?.error ? styles.importResultsError : {}), }}> - {languages.t(importResults.label)} + {importResults.label} ) : null} diff --git a/app/views/Settings.js b/app/views/Settings.js index dab4ac6281..9d22d11dcf 100644 --- a/app/views/Settings.js +++ b/app/views/Settings.js @@ -34,12 +34,11 @@ export const SettingsScreen = ({ navigation }) => { navigation.goBack(); }; - const handleBackPress = () => { - backToMain(); - return true; - }; - useEffect(() => { + const handleBackPress = () => { + navigation.goBack(); + return true; + }; BackHandler.addEventListener('hardwareBackPress', handleBackPress); // TODO: this should be a service or hook @@ -53,7 +52,7 @@ export const SettingsScreen = ({ navigation }) => { return () => { BackHandler.removeEventListener('hardwareBackPress', handleBackPress); }; - }, []); + }, [navigation]); const locationToggleButtonPressed = async () => { try { diff --git a/app/views/__tests__/__snapshots__/Import.spec.js.snap b/app/views/__tests__/__snapshots__/Import.spec.js.snap index d16de84915..d6685f521c 100644 --- a/app/views/__tests__/__snapshots__/Import.spec.js.snap +++ b/app/views/__tests__/__snapshots__/Import.spec.js.snap @@ -165,9 +165,7 @@ Array [ 2. Frequency: "Export once" 3. File type & size: ".zip" and "1GB" 4. Google sends an email when the export is ready -5. Return here to import locations. Import options: -- Import from Google Drive -- Download from browser, then import from local phone files. Make sure to be on WiFi network as files can be big. +5. Return here to import locations from Google Drive