From 7ca6fcd79a62a44b1942350e9e6bd402c0d0a583 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Tue, 12 Sep 2023 00:47:17 -0600 Subject: [PATCH 001/134] Updated README 1. Updated README like @JGreenlee mentioned to successfully build 2. Maintaining current version 3. Will update again after updating JDK in this intel machine --- README.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 0c02868db..06d860d0c 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,11 @@ Installing (one time only) --- Run the setup script for the platform you want to build +Make sure you switch to the "label_dashboard_profile_sept_2023" branch +``` +git checkout label_dashboard_profile_sept_2023 +``` + ``` $ bash setup/setup_android_native.sh AND/OR @@ -156,14 +161,6 @@ $ cp www/json/startupConfig.json.sample www/json/startupConfig.json $ cp ..... www/json/connectionConfig.json ``` -### Activation (after install, and in every new shell) - -``` -$ source setup/activate_native.sh -``` - -### Activation (after install, and in every new shell) - If connecting to a development server over http, make sure to turn on http support on android ``` @@ -174,10 +171,15 @@ If connecting to a development server over http, make sure to turn on http suppo ### Run in the emulator +Pick a version and execute the following: + ``` -$ npx cordova emulate ios -AND/OR -$ npx cordova emulate android +$ npm run +``` + +For instance: (build-dev-android) +``` +$ npm run build-dev-android ``` Creating logos From 6822c67466d48e5cc3059aa6003b7930e25e9095 Mon Sep 17 00:00:00 2001 From: niccolopaganini Date: Sat, 23 Sep 2023 13:13:44 -0600 Subject: [PATCH 002/134] multiple changes: 1. structure/ flow 2. added contents section 3. updated certain resources --- README.md | 301 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 185 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index 06d860d0c..a14f99a01 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,60 @@ -e-mission phone app --------------------- +# e-mission phone app -This is the phone component of the e-mission system. +__This is the phone component of the e-mission system.__ -:sparkles: This has now been upgraded to cordova android@9.0.0 and iOS@6.0.1 ([details](https://github.com/e-mission/e-mission-docs/issues/554)). It has also been upgraded to [android API 29](https://github.com/e-mission/e-mission-phone/pull/707/), [cordova-lib@10.0.0 and the most recent node and npm versions](https://github.com/e-mission/e-mission-phone/pull/708)It also now supports CI, so we should not have any build issues in the future. The limitations from the [previous upgrade](https://github.com/e-mission/e-mission-docs/issues/519) have all been resolved. This should be ready to build out of the box, after all the configuration files are changed. +:sparkles: This has now been upgraded to cordova android@12.0.0 and iOS@6.2.0. It has also been upgraded to [android API 33 and the latest iOS versions](https://github.com/e-mission/e-mission-docs/issues/934), [cordova-lib@10.0.0 and the most recent node and npm versions](). It also now supports CI, so we should not have any build issues in the future. __This should be ready to build out of the box.__ -Additional Documentation ---- +## Additional Documentation Additional documentation has been moved to its own repository [e-mission-docs](https://github.com/e-mission/e-mission-docs). Specific e-mission-phone wikis can be found here: https://github.com/e-mission/e-mission-docs/tree/master/docs/e-mission-phone **Issues:** Since this repository is part of a larger project, all issues are tracked [in the central docs repository](https://github.com/e-mission/e-mission-docs/issues). If you have a question, [as suggested by the open source guide](https://opensource.guide/how-to-contribute/#communicating-effectively), please file an issue instead of sending an email. Since issues are public, other contributors can try to answer the question and benefit from the answer. -Updating the UI only +:sparkles: Check 6. Contributing if you're interested in contributing for this project :sparkles: + +## Contents +#### 1. [Creating logos](#1.-Creating-logos) -> Information regarding app Logo +#### 2. [Updating the UI only](#2.-Updating-the-UI-only) -> For UI changes ONLY +#### 3. [Updating the e-mission-* plugins or adding new plugins](#3.-Updating-the-e-mission-\*-plugins-or-adding-new-plugins) -> Work with native code +#### 4. [End to End Testing](#4.-End-to-End-Testing) +#### 5. [Beta-testing debugging](#5.-Beta-testing-debugging) +#### 6. [Contributing](#6.-Contributing) --- + +## 1. Creating logos + +If you are building your own version of the app, you must have your own logo to +avoid app store conficts. Updating the logo is very simple using the [`ionic +cordova resources`](https://ionicframework.com/docs/v3/cli/cordova/resources/) +command. + +**Note**: You may have to install the [`cordova-res` package](https://github.com/ionic-team/cordova-res) for the command to work + +## 2. Updating the UI only [![osx-serve-install](https://github.com/e-mission/e-mission-phone/workflows/osx-serve-install/badge.svg)](https://github.com/e-mission/e-mission-phone/actions?query=workflow%3Aosx-serve-install) If you want to make only UI changes, (as opposed to modifying the existing plugins, adding new plugins, etc), you can use the **new and improved** (as of June 2018) [e-mission dev app](https://github.com/e-mission/e-mission-devapp/) and install the most recent version from [releases](https://github.com/e-mission/e-mission-devapp/releases). ### Installing (one-time) -Run the setup script +1. Run the setup script ``` -$ bash setup/setup_serve.sh +bash setup/setup_serve.sh ``` - **(optional)** Configure by changing the files in `www/json`. Defaults are in `www/json/*.sample` ``` -$ ls www/json/*.sample -$ cp www/json/startupConfig.json.sample www/json/startupConfig.json -$ cp ..... www/json/connectionConfig.json +ls www/json/*.sample +cp www/json/startupConfig.json.sample www/json/startupConfig.json +cp ..... www/json/connectionConfig.json ``` ### Activation (after install, and in every new shell) - +2. Run this to activate ``` -$ source setup/activate_serve.sh +source setup/activate_serve.sh ``` ### Running @@ -46,7 +62,7 @@ $ source setup/activate_serve.sh 1. Start the phonegap deployment server and note the URL(s) that the server is listening to. ``` - $ npm run serve + npm run serve .... [phonegap] listening on 10.0.0.14:3000 [phonegap] listening on 192.168.162.1:3000 @@ -56,10 +72,10 @@ $ source setup/activate_serve.sh .... ``` -1. Change the devapp connection URL to one of these (e.g. 192.168.162.1:3000) and press "Connect" -1. The app will now display the version of e-mission app that is in your local directory - 1. The console logs will be displayed back in the server window (prefaced by `[console]`) - 1. Breakpoints can be added by connecting through the browser +2. Change the devapp connection URL to one of these (e.g. 192.168.162.1:3000) and press "Connect" +3. The app will now display the version of e-mission app that is in your local directory + 4. The console logs will be displayed back in the server window (prefaced by `[console]`) + 5. Breakpoints can be added by connecting through the browser - Safari ([enable develop menu](https://support.apple.com/guide/safari/use-the-safari-develop-menu-sfri20948/mac)): Develop -> Simulator -> index.html - Chrome: chrome://inspect -> Remote target (emulator) @@ -67,37 +83,68 @@ $ source setup/activate_serve.sh **Note1**: You may need to scroll up, past all the warnings about `Content Security Policy has been added` to find the port that the server is listening to. -End to end testing ---- -A lot of the visualizations that we display in the phone client come from the server. In order to do end to end testing, we need to run a local server and connect to it. Instructions for: +## 3. Updating the e-mission-\* plugins or adding new plugins -1. installing a local server, -2. running it, -3. loading it with test data, and -4. running analysis on it +[![osx-build-ios](https://github.com/e-mission/e-mission-phone/actions/workflows/ios-build.yml/badge.svg)](https://github.com/e-mission/e-mission-phone/actions/workflows/ios-build.yml) +[![osx-build-android](https://github.com/e-mission/e-mission-phone/actions/workflows/android-build.yml/badge.svg)](https://github.com/e-mission/e-mission-phone/actions/workflows/android-build.yml) -are available in the [e-mission-server README](https://github.com/e-mission/e-mission-server/blob/master/README.md). +__Important__ -In order to make end to end testing easy, if the local server is started on a HTTP (versus HTTPS port), it is in development mode. By default, the phone app connects to the local server (localhost on iOS, [10.0.2.2 on android](https://stackoverflow.com/questions/5806220/how-to-connect-to-my-http-localhost-web-server-from-android-emulator-in-eclips)) with the `prompted-auth` authentication method. To connect to a different server, or to use a different authentication method, you need to create a `www/json/connectionConfig.json` file. More details on configuring authentication [can be found in the docs](https://github.com/e-mission/e-mission-docs/blob/master/docs/install/configuring_authentication.md). +Most of the recent issues encountered have been due to incompatible setup. We +have now: +- locked down the dependencies, +- created setup and teardown scripts to setup self-contained environments with + those dependencies, and +- CI enabled to validate that they continue work. -One advantage of using `skip` authentication in development mode is that any user email can be entered without a password. Developers can use one of the emails that they loaded test data for in step (3) above. So if the test data loaded was with `-u shankari@eecs.berkeley.edu`, then the login email for the phone app would also be `shankari@eecs.berkeley.edu`. +If you have setup failures, please compare the configuration in the **passing CI +builds** with your configuration. That is almost certainly the source of the error. -Updating the e-mission-\* plugins or adding new plugins ---- -[![osx-build-ios](https://github.com/e-mission/e-mission-phone/actions/workflows/ios-build.yml/badge.svg)](https://github.com/e-mission/e-mission-phone/actions/workflows/ios-build.yml) -[![osx-build-android](https://github.com/e-mission/e-mission-phone/actions/workflows/android-build.yml/badge.svg)](https://github.com/e-mission/e-mission-phone/actions/workflows/android-build.yml) +### Tested on +__MacOS__ +- Intel chip, MacOS Ventura 13.6 +- Intel chip, MacOS Ventura 13.5.2 +- Intel chip, MacOS Ventura 13.0 +- Intel chip, MacOS Monterey 12.6.7 Pre-requisites --- -- the version of xcode used by the CI +- The version of xcode used by the CI. - to install a particular version, use [xcode-select](https://www.unix.com/man-page/OSX/1/xcode-select/) - or this [supposedly easier to use repo](https://github.com/xcpretty/xcode-install) - **NOTE**: the basic xcode install on Catalina was messed up for me due to a prior installation of command line tools. [These workarounds helped](https://github.com/nodejs/node-gyp/blob/master/macOS_Catalina.md). - git -- Java 11. Tested with [OpenJDK 11 (Temurin) using AdoptOpenJDK](https://adoptopenjdk.net/releases.html). -- android SDK; install manually or use setup script below. Note that you only need to run this once **per computer**. +- Java 17. Tested with [OpenJDK 17 (Temurin) using AdoptOpenJDK](https://adoptium.net). +- if you are not on the most recent version of OSX: `homebrew` + - this allows us to install the current version of cocoapods without + running into ruby incompatibilities - e.g. + https://github.com/CocoaPods/CocoaPods/issues/11763 + +__1. Export statements__ +``` +export ANDROID_SDK_ROOT="/Users//Library/Android/sdk" +``` +``` +export ANDROID_HOME="/Users//Library/Android/sdk" +``` +aka the path where you want the SDK to be installed. + +To setup JAVA_HOME (after installing the latest JDK ), run this command: +``` +/usr/libexec/java_home +``` +Find the location of the Java installation (Default will look something like this:) +``` +/Library/Java/JavaVirtualMachines/... +``` +and then export the package as: +``` +export JAVA_HOME="" +``` + +- android SDK; install manually or use setup script below (**recommended**). Note that you only need to run this once **per computer**. ``` - $ bash setup/prereq_android_sdk_install.sh + bash setup/prereq_android_sdk_install.sh ```
Expected output @@ -120,45 +167,25 @@ Pre-requisites ```
-- if you are not on the most recent version of OSX, `homebrew` - - this allows us to install the current version of cocoapods without - running into ruby incompatibilities - e.g. - https://github.com/CocoaPods/CocoaPods/issues/11763 - -Important ---- -Most of the recent issues encountered have been due to incompatible setup. We -have now: -- locked down the dependencies, -- created setup and teardown scripts to setup self-contained environments with - those dependencies, and -- CI enabled to validate that they continue work. -If you have setup failures, please compare the configuration in the passing CI -builds with your configuration. That is almost certainly the source of the error. -Installing (one time only) ---- -Run the setup script for the platform you want to build +__2. Installing (one time only)__ -Make sure you switch to the "label_dashboard_profile_sept_2023" branch -``` -git checkout label_dashboard_profile_sept_2023 -``` +- Run the setup script for the platform you want to build ``` -$ bash setup/setup_android_native.sh +bash setup/setup_android_native.sh AND/OR -$ bash setup/setup_ios_native.sh +bash setup/setup_ios_native.sh ``` **(optional)** Configure by changing the files in `www/json`. Defaults are in `www/json/*.sample` ``` -$ ls www/json/*.sample -$ cp www/json/startupConfig.json.sample www/json/startupConfig.json -$ cp ..... www/json/connectionConfig.json +ls www/json/*.sample +cp www/json/startupConfig.json.sample www/json/startupConfig.json +cp ..... www/json/connectionConfig.json ``` If connecting to a development server over http, make sure to turn on http support on android @@ -168,44 +195,58 @@ If connecting to a development server over http, make sure to turn on http suppo ``` +__3. Run this in every new shell__ + +- __Activation__ +``` +source setup/activate_native.sh +``` +
Expected Output + +``` +Activating nvm +Using version 19.5.0 +Now using node v19.5.0 (npm v9.3.1) +npm version = 9.3.1 +Adding cocoapods to the path +Verifying /Users//Library/Android/sk or /Users//Library/Android/sdk is set +Activating sdkman, and by default, gradle +Ensuring that we use the most recent version of the command line tools +Configuring the repo for building native code +Copied config.cordovabuild.xml -> config.xml and package.cordovabuild.json -> package.json +``` -### Run in the emulator +
-Pick a version and execute the following: +- __Pick a type of build and execute the following:__ +More "versions" are available in [`package.cordovabuild.json`](https://github.com/e-mission/e-mission-phone/blob/fce117ff859abd995613bd405dbc7d27c703b09b/package.cordovabuild.json) ``` -$ npm run +npm run ``` For instance: (build-dev-android) ``` -$ npm run build-dev-android +npm run build-dev-android ``` -Creating logos ---- -If you are building your own version of the app, you must have your own logo to -avoid app store conficts. Updating the logo is very simple using the [`ionic -cordova resources`](https://ionicframework.com/docs/v3/cli/cordova/resources/) -command. -**Note**: You may have to install the [`cordova-res` package](https://github.com/ionic-team/cordova-res) for the command to work +## 4. End to End Testing +A lot of the visualizations that we display in the phone client come from the server. In order to do end to end testing, we need to run a local server and connect to it. Instructions for: -Troubleshooting ---- -- Make sure to use `npx ionic` and `npx cordova`. This is - because the setup script installs all the modules locally in a self-contained - environment using `npm install` and not `npm install -g` -- Check the CI to see whether there is a known issue -- Run the commands from the script one by one and see which fails - - compare the failed command with the CI logs -- Another workaround is to delete the local environment and recreate it - - javascript errors: `rm -rf node_modules && npm install` - - native code compile errors: `rm -rf plugins && rm -rf platforms && npx cordova prepare` +1. installing a local server, +2. running it, +3. loading it with test data, and +4. running analysis on it -Beta-testing debugging ---- +are available in the [e-mission-server README](https://github.com/e-mission/e-mission-server/blob/master/README.md). + +In order to make end to end testing easy, if the local server is started on a HTTP (versus HTTPS port), it is in development mode. By default, the phone app connects to the local server (localhost on iOS, [10.0.2.2 on android](https://stackoverflow.com/questions/5806220/how-to-connect-to-my-http-localhost-web-server-from-android-emulator-in-eclips)) with the `prompted-auth` authentication method. To connect to a different server, or to use a different authentication method, you need to create a `www/json/connectionConfig.json` file. More details on configuring authentication [can be found in the docs](https://github.com/e-mission/e-mission-docs/blob/master/docs/install/configuring_authentication.md). + +One advantage of using `skip` authentication in development mode is that any user email can be entered without a password. Developers can use one of the emails that they loaded test data for in step (3) above. So if the test data loaded was with `-u shankari@eecs.berkeley.edu`, then the login email for the phone app would also be `shankari@eecs.berkeley.edu`. + +## 5. Beta-testing debugging If users run into problems, they have the ability to email logs to the maintainer. These logs are in the form of an sqlite3 database, so they have to be opened using `sqlite3`. Alternatively, you can export it to a csv with @@ -213,40 +254,68 @@ dates using the `bin/csv_export_add_date.py` script. ``` -$ mv ~/Downloads/loggerDB /tmp/logger. -$ pwd +mv ~/Downloads/loggerDB /tmp/logger. +pwd .../e-mission-phone -$ python bin/csv_export_add_date.py /tmp/loggerDB. -$ less /tmp/loggerDB..withdate.log +python bin/csv_export_add_date.py /tmp/loggerDB. +less /tmp/loggerDB..withdate.log ``` -Contributing ---- - -Add the main repo as upstream - - $ git remote add upstream https://github.com/covid19database/phone-app.git - -Create a new branch (IMPORTANT). Please do not submit pull requests from master - - $ git checkout -b mybranch +## 6. Contributing -Make changes to the branch and commit them - $ git commit - -Push the changes to your local fork - - $ git push origin mybranch - -Generate a pull request from the UI +1. Add the main repo as upstream +``` +2. git remote add upstream +``` +3. Create a new branch (IMPORTANT). Please do not submit pull requests from master +``` +4. git checkout -b +``` +5. Make changes to the branch and commit them +``` +6. git commit +``` + 7. Push the changes to your local fork +``` +8. git push origin +``` +9. Generate a pull request from the UI -Address my review comments +__\*__Address my review comments__\*__ Once I merge the pull request, pull the changes to your fork and delete the branch ``` -$ git checkout master -$ git pull upstream master -$ git push origin master -$ git branch -d mybranch +git checkout master +``` +``` +git pull upstream master +``` +``` +git push origin master +``` +``` +git branch -d ``` + +--- +### Troubleshooting +1. Xcode command line tools +``` +Warning: No developer tools installed. +You should install the Command Line Tools. +``` +``` +xcode-select --install +``` + +2. Creating Logos +- Make sure to use `npx ionic` and `npx cordova`. This is + because the setup script installs all the modules locally in a self-contained + environment using `npm install` and not `npm install -g` +- Check the CI to see whether there is a known issue +- Run the commands from the script one by one and see which fails + - compare the failed command with the CI logs +- Another workaround is to delete the local environment and recreate it + - javascript errors: `rm -rf node_modules && npm install` + - native code compile errors: `rm -rf plugins && rm -rf platforms && npx cordova prepare` From 8ea506d1ce5ad113cb0cc86ec5f5cb425d815d7d Mon Sep 17 00:00:00 2001 From: niccolopaganini Date: Sat, 23 Sep 2023 13:18:38 -0600 Subject: [PATCH 003/134] build successful screenshot --- Build_ss.png | Bin 0 -> 83719 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Build_ss.png diff --git a/Build_ss.png b/Build_ss.png new file mode 100644 index 0000000000000000000000000000000000000000..18ab48b232d04ae28e72f5677d393045be584659 GIT binary patch literal 83719 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 literal 0 HcmV?d00001 From ed0e871645855418908da33ea3b148183caf2c93 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Sat, 23 Sep 2023 13:23:49 -0600 Subject: [PATCH 004/134] Update README.md - Added hyperlink to the title - Added output screenshot --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a14f99a01..5a25ba0e6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# e-mission phone app +# [e-mission phone app](https://github.com/e-mission/e-mission-phone/tree/master) __This is the phone component of the e-mission system.__ @@ -229,7 +229,8 @@ For instance: (build-dev-android) ``` npm run build-dev-android ``` - +Your output should look something like this: +![Build Successful Message screenshot](Build_ss.png) ## 4. End to End Testing From 54656b8d0992e9946b9cd20d86e843814d3b1f18 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Sat, 23 Sep 2023 13:29:52 -0600 Subject: [PATCH 005/134] Update README.md - Updating markdown anchors --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5a25ba0e6..ed7c03d22 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,16 @@ https://github.com/e-mission/e-mission-docs/tree/master/docs/e-mission-phone **Issues:** Since this repository is part of a larger project, all issues are tracked [in the central docs repository](https://github.com/e-mission/e-mission-docs/issues). If you have a question, [as suggested by the open source guide](https://opensource.guide/how-to-contribute/#communicating-effectively), please file an issue instead of sending an email. Since issues are public, other contributors can try to answer the question and benefit from the answer. -:sparkles: Check 6. Contributing if you're interested in contributing for this project :sparkles: +:sparkles: Check 6. [Contributing](#6-contributing) if you're interested in contributing for this project :sparkles: ## Contents -#### 1. [Creating logos](#1.-Creating-logos) -> Information regarding app Logo -#### 2. [Updating the UI only](#2.-Updating-the-UI-only) -> For UI changes ONLY -#### 3. [Updating the e-mission-* plugins or adding new plugins](#3.-Updating-the-e-mission-\*-plugins-or-adding-new-plugins) -> Work with native code -#### 4. [End to End Testing](#4.-End-to-End-Testing) -#### 5. [Beta-testing debugging](#5.-Beta-testing-debugging) -#### 6. [Contributing](#6.-Contributing) +#### 1. [Creating logos](#1-creating-logos) -> Information regarding app Logo +#### 2. [Updating the UI only](#2-updating-the-ui-only) -> For UI changes ONLY +#### 3. [Updating the e-mission-* plugins or adding new plugins](#3-updating-the-e-mission--plugins-or-adding-new-plugins) -> Work with native code +#### 4. [End to End Testing](#4-end-to-end-testing) +#### 5. [Beta-testing debugging](#5-beta-testing-debugging) +#### 6. [Contributing](#6-contributing) + --- ## 1. Creating logos From 290dcc5038c671abf733fb3288600435c3129973 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Sat, 23 Sep 2023 13:34:56 -0600 Subject: [PATCH 006/134] Update README.md Updating markdown anchors as 4,5, & 6 weren't working --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed7c03d22..e9eb61229 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ https://github.com/e-mission/e-mission-docs/tree/master/docs/e-mission-phone **Issues:** Since this repository is part of a larger project, all issues are tracked [in the central docs repository](https://github.com/e-mission/e-mission-docs/issues). If you have a question, [as suggested by the open source guide](https://opensource.guide/how-to-contribute/#communicating-effectively), please file an issue instead of sending an email. Since issues are public, other contributors can try to answer the question and benefit from the answer. -:sparkles: Check 6. [Contributing](#6-contributing) if you're interested in contributing for this project :sparkles: +:sparkles: Check [6. Contributing](#6-contributing) if you're interested in contributing for this project :sparkles: ## Contents #### 1. [Creating logos](#1-creating-logos) -> Information regarding app Logo From 809fc5d4fe89e17c9f5d40dced40c41eb46f0a8a Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Sat, 23 Sep 2023 13:46:50 -0600 Subject: [PATCH 007/134] Update README.md - touch up - fixed non-working markdown anchors --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e9eb61229..e267f8e71 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,12 @@ https://github.com/e-mission/e-mission-docs/tree/master/docs/e-mission-phone **Issues:** Since this repository is part of a larger project, all issues are tracked [in the central docs repository](https://github.com/e-mission/e-mission-docs/issues). If you have a question, [as suggested by the open source guide](https://opensource.guide/how-to-contribute/#communicating-effectively), please file an issue instead of sending an email. Since issues are public, other contributors can try to answer the question and benefit from the answer. -:sparkles: Check [6. Contributing](#6-contributing) if you're interested in contributing for this project :sparkles: +:sparkles: Check [Contributing](#6-contributing) if you're interested in contributing for this project :sparkles: ## Contents -#### 1. [Creating logos](#1-creating-logos) -> Information regarding app Logo -#### 2. [Updating the UI only](#2-updating-the-ui-only) -> For UI changes ONLY -#### 3. [Updating the e-mission-* plugins or adding new plugins](#3-updating-the-e-mission--plugins-or-adding-new-plugins) -> Work with native code +#### 1. [Creating logos](#1-creating-logos) +#### 2. [Updating the UI only](#2-updating-the-ui-only) +#### 3. [Updating the e-mission-* plugins or adding new plugins](#3-updating-the-e-mission--plugins-or-adding-new-plugins) #### 4. [End to End Testing](#4-end-to-end-testing) #### 5. [Beta-testing debugging](#5-beta-testing-debugging) #### 6. [Contributing](#6-contributing) From b64ab621b0325c95166950473807d67942dcb9b3 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Mon, 25 Sep 2023 00:29:30 -0600 Subject: [PATCH 008/134] Update README.md Removed manual numbering and updated markdown anchors --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index e267f8e71..76d56ea88 100644 --- a/README.md +++ b/README.md @@ -13,16 +13,16 @@ https://github.com/e-mission/e-mission-docs/tree/master/docs/e-mission-phone :sparkles: Check [Contributing](#6-contributing) if you're interested in contributing for this project :sparkles: ## Contents -#### 1. [Creating logos](#1-creating-logos) -#### 2. [Updating the UI only](#2-updating-the-ui-only) -#### 3. [Updating the e-mission-* plugins or adding new plugins](#3-updating-the-e-mission--plugins-or-adding-new-plugins) -#### 4. [End to End Testing](#4-end-to-end-testing) -#### 5. [Beta-testing debugging](#5-beta-testing-debugging) -#### 6. [Contributing](#6-contributing) +#### 1. [Creating logos](#creating-logos) +#### 2. [Updating the UI only](#updating-the-ui-only) +#### 3. [Updating the e-mission-* plugins or adding new plugins](#updating-the-e-mission--plugins-or-adding-new-plugins) +#### 4. [End to End Testing](#end-to-end-testing) +#### 5. [Beta-testing debugging](#beta-testing-debugging) +#### 6. [Contributing](#contributing) --- -## 1. Creating logos +## Creating logos If you are building your own version of the app, you must have your own logo to avoid app store conficts. Updating the logo is very simple using the [`ionic @@ -31,7 +31,7 @@ command. **Note**: You may have to install the [`cordova-res` package](https://github.com/ionic-team/cordova-res) for the command to work -## 2. Updating the UI only +## Updating the UI only [![osx-serve-install](https://github.com/e-mission/e-mission-phone/workflows/osx-serve-install/badge.svg)](https://github.com/e-mission/e-mission-phone/actions?query=workflow%3Aosx-serve-install) If you want to make only UI changes, (as opposed to modifying the existing plugins, adding new plugins, etc), you can use the **new and improved** (as of June 2018) [e-mission dev app](https://github.com/e-mission/e-mission-devapp/) and install the most recent version from [releases](https://github.com/e-mission/e-mission-devapp/releases). @@ -84,7 +84,7 @@ source setup/activate_serve.sh **Note1**: You may need to scroll up, past all the warnings about `Content Security Policy has been added` to find the port that the server is listening to. -## 3. Updating the e-mission-\* plugins or adding new plugins +## Updating the e-mission-\* plugins or adding new plugins [![osx-build-ios](https://github.com/e-mission/e-mission-phone/actions/workflows/ios-build.yml/badge.svg)](https://github.com/e-mission/e-mission-phone/actions/workflows/ios-build.yml) [![osx-build-android](https://github.com/e-mission/e-mission-phone/actions/workflows/android-build.yml/badge.svg)](https://github.com/e-mission/e-mission-phone/actions/workflows/android-build.yml) @@ -233,7 +233,7 @@ npm run build-dev-android Your output should look something like this: ![Build Successful Message screenshot](Build_ss.png) -## 4. End to End Testing +## End to End Testing A lot of the visualizations that we display in the phone client come from the server. In order to do end to end testing, we need to run a local server and connect to it. Instructions for: @@ -248,7 +248,7 @@ In order to make end to end testing easy, if the local server is started on a HT One advantage of using `skip` authentication in development mode is that any user email can be entered without a password. Developers can use one of the emails that they loaded test data for in step (3) above. So if the test data loaded was with `-u shankari@eecs.berkeley.edu`, then the login email for the phone app would also be `shankari@eecs.berkeley.edu`. -## 5. Beta-testing debugging +## Beta-testing debugging If users run into problems, they have the ability to email logs to the maintainer. These logs are in the form of an sqlite3 database, so they have to be opened using `sqlite3`. Alternatively, you can export it to a csv with @@ -263,7 +263,7 @@ python bin/csv_export_add_date.py /tmp/loggerDB. less /tmp/loggerDB..withdate.log ``` -## 6. Contributing +## Contributing 1. Add the main repo as upstream From 72922cff1c2fb2b4797106cf5bdcb0ead702700f Mon Sep 17 00:00:00 2001 From: niccolopaganini Date: Mon, 25 Sep 2023 00:42:26 -0600 Subject: [PATCH 009/134] 1. Changed expected output for plugins 2. Removed tested on sub-section 3. Revised formatting for activation sub-section -> for better clarity --- README.md | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 76d56ea88..0b19e492e 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ https://github.com/e-mission/e-mission-docs/tree/master/docs/e-mission-phone **Issues:** Since this repository is part of a larger project, all issues are tracked [in the central docs repository](https://github.com/e-mission/e-mission-docs/issues). If you have a question, [as suggested by the open source guide](https://opensource.guide/how-to-contribute/#communicating-effectively), please file an issue instead of sending an email. Since issues are public, other contributors can try to answer the question and benefit from the answer. -:sparkles: Check [Contributing](#6-contributing) if you're interested in contributing for this project :sparkles: +:sparkles: Check [Contributing](#contributing) if you're interested in contributing for this project :sparkles: ## Contents #### 1. [Creating logos](#creating-logos) @@ -101,13 +101,6 @@ have now: If you have setup failures, please compare the configuration in the **passing CI builds** with your configuration. That is almost certainly the source of the error. -### Tested on -__MacOS__ -- Intel chip, MacOS Ventura 13.6 -- Intel chip, MacOS Ventura 13.5.2 -- Intel chip, MacOS Ventura 13.0 -- Intel chip, MacOS Monterey 12.6.7 - Pre-requisites --- - The version of xcode used by the CI. @@ -196,9 +189,8 @@ If connecting to a development server over http, make sure to turn on http suppo ``` -__3. Run this in every new shell__ +__3. Run this in every new shell for Activation__ -- __Activation__ ``` source setup/activate_native.sh ``` @@ -218,8 +210,9 @@ Copied config.cordovabuild.xml -> config.xml and package.cordovabuild.json -> pa ``` +
-- __Pick a type of build and execute the following:__ + __4. Pick a type of build and execute the following:__ More "versions" are available in [`package.cordovabuild.json`](https://github.com/e-mission/e-mission-phone/blob/fce117ff859abd995613bd405dbc7d27c703b09b/package.cordovabuild.json) ``` @@ -230,8 +223,17 @@ For instance: (build-dev-android) ``` npm run build-dev-android ``` -Your output should look something like this: -![Build Successful Message screenshot](Build_ss.png) +
Your expected output should look something like this + +``` +BUILD SUCCESSFUL in 2m 48s +52 actionable tasks: 52 executed +Built the following apk(s): +/Users//e-mission-phone/platforms/android/app/build/outputs/apk/debug/app-debug.apk +``` + +
+ ## End to End Testing From 728a7f53afe24926ac5c909f6ead55320e62a8c1 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 28 Sep 2023 09:13:49 -0600 Subject: [PATCH 010/134] rename the angular service with ng prefix hoping to avoid naming problems when creating the new ts service by the same name --- www/js/control/{uploadService.js => nguploadService.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename www/js/control/{uploadService.js => nguploadService.js} (100%) diff --git a/www/js/control/uploadService.js b/www/js/control/nguploadService.js similarity index 100% rename from www/js/control/uploadService.js rename to www/js/control/nguploadService.js From 2049c59edf4e5987fe283a27fbcf934dafe43660 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 28 Sep 2023 09:14:57 -0600 Subject: [PATCH 011/134] start uploadService.ts and tests starting to convert the service, but running into a fair number of issues, especially because of complex ionic popups --- www/__tests__/uploadService.tests.ts | 2 + www/js/control/uploadService.ts | 180 +++++++++++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 www/__tests__/uploadService.tests.ts create mode 100644 www/js/control/uploadService.ts diff --git a/www/__tests__/uploadService.tests.ts b/www/__tests__/uploadService.tests.ts new file mode 100644 index 000000000..50a253a2e --- /dev/null +++ b/www/__tests__/uploadService.tests.ts @@ -0,0 +1,2 @@ +import {} from "../js/control/uploadService"; + diff --git a/www/js/control/uploadService.ts b/www/js/control/uploadService.ts new file mode 100644 index 000000000..e359bed88 --- /dev/null +++ b/www/js/control/uploadService.ts @@ -0,0 +1,180 @@ +import { logDebug, logInfo, logError, displayError, displayErrorMsg } from "../plugin/logger"; +import { useTranslation } from "react-i18next"; + +/** + * @returns A promise that resolves with an upload URL or rejects with an error + */ +function getUploadConfig() { + return new Promise(function (resolve, reject) { + //logInfo( "About to get email config"); + let url = []; + fetch("json/uploadConfig.json").then( function (uploadConfig) { + //logDebug("uploadConfigString = " + JSON.stringify(uploadConfig['data'])); + url.push(uploadConfig["data"].url); + resolve(url); + }).catch(function (err) { + fetch("json/uploadConfig.json.sample"). then(function (uploadConfig) { + //logDebug("default uploadConfigString = " + JSON.stringify(uploadConfig['data'])); + console.log("default uploadConfigString = " + JSON.stringify(uploadConfig['data'])) + url.push(uploadConfig["data"].url); + resolve(url); + }).catch(function (err) { + //logError("Error while reading default upload config" + err); + reject(err); + }) + }) + }) +} + +function onReadError(err) { + displayError(err, "Error while reading log"); +} + +function onUploadError(err) { + displayError(err, "Error while uploading log"); +} + +function readDBFile(parentDir, database, callbackFn) { + return new Promise(function(resolve, reject) { + window['resolveLocalFileSystemURL'](parentDir, function(fs) { + fs.filesystem.root.getFile(fs.fullPath+database, null, (fileEntry) => { + console.log(fileEntry); + fileEntry.file(function(file) { + console.log(file); + var reader = new FileReader(); + + reader.onprogress = function(report) { + console.log("Current progress is "+JSON.stringify(report)); + if (callbackFn != undefined) { + callbackFn(report.loaded * 100 / report.total); + } + } + + reader.onerror = function(error) { + console.log(this.error); + reject({"error": {"message": this.error}}); + } + + reader.onload = function() { + console.log("Successful file read with " + this.result.byteLength +" characters"); + resolve(new DataView(this.result)); + } + + reader.readAsArrayBuffer(file); + }, reject); + }, reject); + }); + }); +} + +const sendToServer = function upload(url, binArray, params) { + //attempting to replace angular.identity + var identity = function() { + return arguments[0]; + } + + var config = { + method: "POST", + body: binArray, + headers: {'Content-Type': undefined }, + transformRequest: identity, + params: params + }; + return fetch (url, config); +} + + +//only export of this file, used in ProfileSettings and passed the argument (""loggerDB"") +export function uploadFile(database) { + const { t } = useTranslation(); + getUploadConfig().then((uploadConfig) => { + var parentDir = "unknown"; + + if (window['cordova'].platformId.toLowerCase() == "android") { + parentDir = window['cordova'].file.applicationStorageDirectory+"/databases"; + } + else if (window['cordova'].platformId.toLowerCase() == "ios") { + parentDir = window['cordova'].file.dataDirectory + "../LocalDatabase"; + } else { + alert("parentDir unexpectedly = " + parentDir + "!") + } + + const newScope = {}; + newScope["data"] = {}; + newScope["fromDirText"] = t('upload-service.upload-from-dir', {parentDir: parentDir}); + newScope["toServerText"] = t('upload-service.upload-to-server', {serverURL: uploadConfig}); + + let didCancel = true; + let detailsPopup = () => console.log("I need a popup"); + + // const detailsPopup = $ionicPopup.show({ + // title: i18next.t("upload-service.upload-database", { db: database }), + // template: newScope.toServerText + // + '', + // scope: newScope, + // buttons: [ + // { + // text: 'Cancel', + // onTap: function(e) { + // didCancel = true; + // detailsPopup.close(); + // } + // }, + // { + // text: 'Upload', + // type: 'button-positive', + // onTap: function(e) { + // if (!newScope.data.reason) { + // //don't allow the user to close unless he enters wifi password + // didCancel = false; + // e.preventDefault(); + // } else { + // didCancel = false; + // return newScope.data.reason; + // } + // } + // } + // ] + // }); + + logInfo("Going to upload " + database); + const readFileAndInfo = [readDBFile(parentDir, database, detailsPopup)]; + Promise.all(readFileAndInfo).then(([binString, reason]) => { + if(!didCancel) + { + console.log("Uploading file of size "+binString['byteLength']); + const progressScope = {...newScope}; //make a child copy of the current scope + const params = { + reason: reason, + tz: Intl.DateTimeFormat().resolvedOptions().timeZone + } + uploadConfig.forEach((url) => { + alert(t("upload-service.upload-database", {db: database}) + + "\n" + + t("upload-service.upload-progress", {filesizemb: binString['byteLength'] / (1000 * 1000), serverURL: uploadConfig}) + ); + // const progressPopup = $ionicPopup.show({ + // title: t("upload-service.upload-database", + // {db: database}), + // template: t("upload-service.upload-progress", + // {filesizemb: binString['byteLength'] / (1000 * 1000), + // serverURL: uploadConfig}) + // + '
', + // scope: progressScope, + // buttons: [ + // { text: 'Cancel', type: 'button-cancel', }, + // ] + // }); + sendToServer(url, binString, params).then((response) => { + console.log(response); + //progressPopup.close(); + displayErrorMsg(t("upload-service.upload-details", + {filesizemb: binString['byteLength'] / (1000 * 1000), serverURL: uploadConfig}), + t("upload-service.upload-success")); + }).catch(onUploadError); + }); + } + }).catch(onReadError); + }).catch(onReadError); + }; \ No newline at end of file From 7de88284bd14f8d66d5d3fc45a268fbc36ee3e4b Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 28 Sep 2023 09:16:34 -0600 Subject: [PATCH 012/134] add logError fcn to Logger.ts the upload service called for a log at the error level, adding support for that in logger.ts --- www/js/plugin/logger.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/www/js/plugin/logger.ts b/www/js/plugin/logger.ts index c4e476de1..9f6d41d73 100644 --- a/www/js/plugin/logger.ts +++ b/www/js/plugin/logger.ts @@ -33,6 +33,9 @@ export const logInfo = (message: string) => export const logWarn = (message: string) => window['Logger'].log(window['Logger'].LEVEL_WARN, message); +export const logError = (message: string) => + window['Logger'].log(window['Logger'].LEVEL_ERROR, message); + export function displayError(error: Error, title?: string) { const errorMsg = error.message ? error.message + '\n' + error.stack : JSON.stringify(error); displayErrorMsg(errorMsg, title); From c0d651cc94c030bc27c3bae9f9be21d0bdc46874 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Thu, 28 Sep 2023 11:41:47 -0700 Subject: [PATCH 013/134] Remove old angular service file --- www/index.js | 1 - www/js/splash/customURL.js | 40 -------------------------------------- 2 files changed, 41 deletions(-) delete mode 100644 www/js/splash/customURL.js diff --git a/www/index.js b/www/index.js index 66a0d45df..578b3dc75 100644 --- a/www/index.js +++ b/www/index.js @@ -13,7 +13,6 @@ import './js/config/imperial.js'; import './js/config/server_conn.js'; import './js/stats/clientstats.js'; import './js/splash/referral.js'; -import './js/splash/customURL.js'; import './js/splash/startprefs.js'; import './js/splash/pushnotify.js'; import './js/splash/storedevicesettings.js'; diff --git a/www/js/splash/customURL.js b/www/js/splash/customURL.js deleted file mode 100644 index 521244bc0..000000000 --- a/www/js/splash/customURL.js +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - -import angular from 'angular'; - -angular.module('emission.splash.customURLScheme', []) - -.factory('CustomURLScheme', function($rootScope) { - var cus = {}; - - var parseURL = function(url) { - var addr = url.split('//')[1]; - var route = addr.split('?')[0]; - var params = addr.split('?')[1]; - var paramsList = params.split('&'); - var rtn = {route: route}; - for (var i = 0; i < paramsList.length; i++) { - var splitList = paramsList[i].split('='); - rtn[splitList[0]] = splitList[1]; - } - return rtn; - }; - - /* - * Register a custom URL handler. - * handler arguments are: - * - * event: - * url: the url that was passed in - * urlComponents: the URL parsed into multiple components - */ - cus.onLaunch = function(handler) { - console.log("onLaunch method from factory called"); - $rootScope.$on("CUSTOM_URL_LAUNCH", function(event, url) { - var urlComponents = parseURL(url); - handler(event, url, urlComponents); - }); - }; - - return cus; -}); From 46301f7db29f191c2489b8c49ec78ad078a3c0dc Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Thu, 28 Sep 2023 11:42:11 -0700 Subject: [PATCH 014/134] write new customURL function --- www/js/splash/customURL.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 www/js/splash/customURL.ts diff --git a/www/js/splash/customURL.ts b/www/js/splash/customURL.ts new file mode 100644 index 000000000..46460a6d7 --- /dev/null +++ b/www/js/splash/customURL.ts @@ -0,0 +1,18 @@ +type UrlComponents = { + [key : string] : string +} + +type OnLaunchCustomURL = (rawUrl: string, callback: (url: string, urlComponents: UrlComponents) => void ) => void; + + +export const onLaunchCustomURL: OnLaunchCustomURL = (rawUrl, handler) => { + const url = rawUrl.split('//')[1]; + const [ route, paramString ] = url.split('?'); + const paramsList = paramString.split('&'); + const urlComponents: UrlComponents = { route : route }; + for (let i = 0; i < paramsList.length; i++) { + const [key, value] = paramsList[i].split('='); + urlComponents[key] = value; + } + handler(url, urlComponents); +}; \ No newline at end of file From ea5b214a4d3976274e1634ed7d5f6aab3a601a53 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Thu, 28 Sep 2023 11:42:36 -0700 Subject: [PATCH 015/134] Call onLaunchCustomURL directly in join-ctrl because customURL only gets called in join-ctrl. --- www/js/join/join-ctrl.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/www/js/join/join-ctrl.js b/www/js/join/join-ctrl.js index 85d6424c1..e99829b13 100644 --- a/www/js/join/join-ctrl.js +++ b/www/js/join/join-ctrl.js @@ -1,4 +1,5 @@ import angular from 'angular'; +import { onLaunchCustomURL } from '../splash/customURL'; angular.module('emission.join.ctrl', ['emission.splash.startprefs', 'emission.splash.pushnotify', @@ -7,7 +8,7 @@ angular.module('emission.join.ctrl', ['emission.splash.startprefs', 'emission.splash.remotenotify', 'emission.stats.clientstats']) .controller('JoinCtrl', function($scope, $state, $interval, $rootScope, - $ionicPlatform, $ionicPopup, $ionicPopover) { + $ionicPlatform, $ionicPopup, $ionicPopover, ReferralHandler, DynamicConfig) { console.log('JoinCtrl invoked'); // alert("attach debugger!"); // PushNotify.startupInit(); @@ -37,9 +38,15 @@ angular.module('emission.join.ctrl', ['emission.splash.startprefs', function handleOpenURL(url) { console.log("onLaunch method from external function called"); - var c = document.querySelectorAll("[ng-app]")[0]; - var scope = angular.element(c).scope(); - scope.$broadcast("CUSTOM_URL_LAUNCH", url); + onLaunchCustomURL(url, function(url, urlComponents){ + console.log("GOT URL:"+url); + if (urlComponents.route == 'join') { + ReferralHandler.setupGroupReferral(urlComponents); + StartPrefs.loadWithPrefs(); + } else if (urlComponents.route == 'login_token') { + DynamicConfig.initByUser(urlComponents); + } + }) }; $scope.scanCode = function() { From c9f1552feca8c533e2a3d1125402e6e560987fcf Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Thu, 28 Sep 2023 11:42:48 -0700 Subject: [PATCH 016/134] Remove calling customURL in app.js because customURL gets called in join-ctrl now --- www/js/app.js | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/www/js/app.js b/www/js/app.js index a52edaa12..2d0b1d08d 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -27,14 +27,12 @@ import 'ng-i18next'; angular.module('emission', ['ionic', 'jm.i18next', 'emission.controllers','emission.services', 'emission.plugin.logger', - 'emission.splash.customURLScheme', 'emission.splash.referral', - 'emission.services.email', + 'emission.splash.referral','emission.services.email', 'emission.intro', 'emission.main', 'emission.config.dynamic', 'emission.config.server_conn', 'emission.join.ctrl', 'pascalprecht.translate', 'LocalStorageModule']) -.run(function($ionicPlatform, $rootScope, $http, Logger, - CustomURLScheme, ReferralHandler, DynamicConfig, localStorageService, ServerConnConfig) { +.run(function($ionicPlatform, $rootScope, $http, Logger, localStorageService, ServerConnConfig) { console.log("Starting run"); // ensure that plugin events are delivered after the ionicPlatform is ready // https://github.com/katzer/cordova-plugin-local-notifications#launch-details @@ -44,17 +42,6 @@ angular.module('emission', ['ionic', 'jm.i18next', // TODO: Although the onLaunch call doesn't need to wait for the platform the // handlers do. Can we rely on the fact that the event is generated from // native code, so will only be launched after the platform is ready? - CustomURLScheme.onLaunch(function(event, url, urlComponents){ - console.log("GOT URL:"+url); - // alert("GOT URL:"+url); - - if (urlComponents.route == 'join') { - ReferralHandler.setupGroupReferral(urlComponents); - StartPrefs.loadWithPrefs(); - } else if (urlComponents.route == 'login_token') { - DynamicConfig.initByUser(urlComponents); - } - }); // END: Global listeners $ionicPlatform.ready(function() { // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard From 7f6b73704748e806c9ed79c9f3d3c18dc8057c50 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Thu, 28 Sep 2023 14:23:20 -0700 Subject: [PATCH 017/134] Recentered & Resized "inferred trip" checkmark See [Issue #979](https://github.com/e-mission/e-mission-docs/issues/979#issuecomment-1736386591) for more details. Only slight difference in comment; removed the `-0.2rem` in this commit, the buttons look crowded otherwise. --- www/js/survey/multilabel/MultiLabelButtonGroup.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/www/js/survey/multilabel/MultiLabelButtonGroup.tsx b/www/js/survey/multilabel/MultiLabelButtonGroup.tsx index cb14c61ea..268d16eb7 100644 --- a/www/js/survey/multilabel/MultiLabelButtonGroup.tsx +++ b/www/js/survey/multilabel/MultiLabelButtonGroup.tsx @@ -100,10 +100,10 @@ const MultilabelButtonGroup = ({ trip, buttonsInline=false }) => { ) })} - - + + style={{width: 24, height: 24, margin: 3}}/> dismiss()}> From 92ce70760b3eed6b1c05b4fea9a82f5a1b361649 Mon Sep 17 00:00:00 2001 From: niccolopaganini Date: Fri, 29 Sep 2023 09:54:52 -0600 Subject: [PATCH 018/134] 1. reformatted to the original structure 2. removed floating links --- README.md | 56 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 0b19e492e..ca3e9c93b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ __This is the phone component of the e-mission system.__ -:sparkles: This has now been upgraded to cordova android@12.0.0 and iOS@6.2.0. It has also been upgraded to [android API 33 and the latest iOS versions](https://github.com/e-mission/e-mission-docs/issues/934), [cordova-lib@10.0.0 and the most recent node and npm versions](). It also now supports CI, so we should not have any build issues in the future. __This should be ready to build out of the box.__ +:sparkles: This has now been upgraded to cordova android@12.0.0 and iOS@6.2.0. It has also been upgraded to the **latest Android & iOS versions**, **cordova-lib@10.0.0 and the most recent node and npm versions**. It also now supports CI, so we should not have any build issues in the future. __This should be ready to build out of the box.__ + +For the latest versions, refer [`package.cordovabuild.json`](https://github.com/e-mission/e-mission-phone/blob/fce117ff859abd995613bd405dbc7d27c703b09b/package.cordovabuild.json) ## Additional Documentation Additional documentation has been moved to its own repository [e-mission-docs](https://github.com/e-mission/e-mission-docs). Specific e-mission-phone wikis can be found here: @@ -22,14 +24,6 @@ https://github.com/e-mission/e-mission-docs/tree/master/docs/e-mission-phone --- -## Creating logos - -If you are building your own version of the app, you must have your own logo to -avoid app store conficts. Updating the logo is very simple using the [`ionic -cordova resources`](https://ionicframework.com/docs/v3/cli/cordova/resources/) -command. - -**Note**: You may have to install the [`cordova-res` package](https://github.com/ionic-team/cordova-res) for the command to work ## Updating the UI only [![osx-serve-install](https://github.com/e-mission/e-mission-phone/workflows/osx-serve-install/badge.svg)](https://github.com/e-mission/e-mission-phone/actions?query=workflow%3Aosx-serve-install) @@ -84,6 +78,21 @@ source setup/activate_serve.sh **Note1**: You may need to scroll up, past all the warnings about `Content Security Policy has been added` to find the port that the server is listening to. +## End to End Testing + +A lot of the visualizations that we display in the phone client come from the server. In order to do end to end testing, we need to run a local server and connect to it. Instructions for: + +1. installing a local server, +2. running it, +3. loading it with test data, and +4. running analysis on it + +are available in the [e-mission-server README](https://github.com/e-mission/e-mission-server/blob/master/README.md). + +In order to make end to end testing easy, if the local server is started on a HTTP (versus HTTPS port), it is in development mode. By default, the phone app connects to the local server (localhost on iOS, [10.0.2.2 on android](https://stackoverflow.com/questions/5806220/how-to-connect-to-my-http-localhost-web-server-from-android-emulator-in-eclips)) with the `prompted-auth` authentication method. To connect to a different server, or to use a different authentication method, you need to create a `www/json/connectionConfig.json` file. More details on configuring authentication [can be found in the docs](https://github.com/e-mission/e-mission-docs/blob/master/docs/install/configuring_authentication.md). + +One advantage of using `skip` authentication in development mode is that any user email can be entered without a password. Developers can use one of the emails that they loaded test data for in step (3) above. So if the test data loaded was with `-u shankari@eecs.berkeley.edu`, then the login email for the phone app would also be `shankari@eecs.berkeley.edu`. + ## Updating the e-mission-\* plugins or adding new plugins [![osx-build-ios](https://github.com/e-mission/e-mission-phone/actions/workflows/ios-build.yml/badge.svg)](https://github.com/e-mission/e-mission-phone/actions/workflows/ios-build.yml) @@ -109,7 +118,7 @@ Pre-requisites - **NOTE**: the basic xcode install on Catalina was messed up for me due to a prior installation of command line tools. [These workarounds helped](https://github.com/nodejs/node-gyp/blob/master/macOS_Catalina.md). - git - Java 17. Tested with [OpenJDK 17 (Temurin) using AdoptOpenJDK](https://adoptium.net). -- if you are not on the most recent version of OSX: `homebrew` +- Always use [homebrew](https://brew.sh) in addition to CLI - this allows us to install the current version of cocoapods without running into ruby incompatibilities - e.g. https://github.com/CocoaPods/CocoaPods/issues/11763 @@ -234,21 +243,16 @@ Built the following apk(s): +
-## End to End Testing - -A lot of the visualizations that we display in the phone client come from the server. In order to do end to end testing, we need to run a local server and connect to it. Instructions for: - -1. installing a local server, -2. running it, -3. loading it with test data, and -4. running analysis on it - -are available in the [e-mission-server README](https://github.com/e-mission/e-mission-server/blob/master/README.md). +## Creating logos -In order to make end to end testing easy, if the local server is started on a HTTP (versus HTTPS port), it is in development mode. By default, the phone app connects to the local server (localhost on iOS, [10.0.2.2 on android](https://stackoverflow.com/questions/5806220/how-to-connect-to-my-http-localhost-web-server-from-android-emulator-in-eclips)) with the `prompted-auth` authentication method. To connect to a different server, or to use a different authentication method, you need to create a `www/json/connectionConfig.json` file. More details on configuring authentication [can be found in the docs](https://github.com/e-mission/e-mission-docs/blob/master/docs/install/configuring_authentication.md). +If you are building your own version of the app, you must have your own logo to +avoid app store conficts. Updating the logo is very simple using the [`ionic +cordova resources`](https://ionicframework.com/docs/v3/cli/cordova/resources/) +command. -One advantage of using `skip` authentication in development mode is that any user email can be entered without a password. Developers can use one of the emails that they loaded test data for in step (3) above. So if the test data loaded was with `-u shankari@eecs.berkeley.edu`, then the login email for the phone app would also be `shankari@eecs.berkeley.edu`. +**Note**: You may have to install the [`cordova-res` package](https://github.com/ionic-team/cordova-res) for the command to work ## Beta-testing debugging If users run into problems, they have the ability to email logs to the @@ -288,7 +292,7 @@ less /tmp/loggerDB..withdate.log __\*__Address my review comments__\*__ -Once I merge the pull request, pull the changes to your fork and delete the branch +Once I merge the pull request :smiley: :tada:, pull the changes to your fork and delete the branch ``` git checkout master ``` @@ -304,7 +308,7 @@ git branch -d --- ### Troubleshooting -1. Xcode command line tools +__1. Xcode command line tools__ ``` Warning: No developer tools installed. You should install the Command Line Tools. @@ -313,7 +317,7 @@ You should install the Command Line Tools. xcode-select --install ``` -2. Creating Logos +__2. Creating Logos__ - Make sure to use `npx ionic` and `npx cordova`. This is because the setup script installs all the modules locally in a self-contained environment using `npm install` and not `npm install -g` @@ -323,3 +327,5 @@ xcode-select --install - Another workaround is to delete the local environment and recreate it - javascript errors: `rm -rf node_modules && npm install` - native code compile errors: `rm -rf plugins && rm -rf platforms && npx cordova prepare` + +3. From 3d5e99bb974a641f144f1cbfedf9d7ac7247ea71 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Fri, 29 Sep 2023 10:02:33 -0600 Subject: [PATCH 019/134] Update README.md 1. Removed instructions on JAVA_HOME as it is no longer required 2. Quality of life changes --- README.md | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index ca3e9c93b..34184ab09 100644 --- a/README.md +++ b/README.md @@ -132,19 +132,6 @@ export ANDROID_HOME="/Users//Library/Android/sdk" ``` aka the path where you want the SDK to be installed. -To setup JAVA_HOME (after installing the latest JDK ), run this command: -``` -/usr/libexec/java_home -``` -Find the location of the Java installation (Default will look something like this:) -``` -/Library/Java/JavaVirtualMachines/... -``` -and then export the package as: -``` -export JAVA_HOME="" -``` - - android SDK; install manually or use setup script below (**recommended**). Note that you only need to run this once **per computer**. ``` bash setup/prereq_android_sdk_install.sh @@ -178,7 +165,9 @@ __2. Installing (one time only)__ ``` bash setup/setup_android_native.sh +``` AND/OR +``` bash setup/setup_ios_native.sh ``` From 9606f57c4a5aee68c57f0a3fdb55eaa1937c9260 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 29 Sep 2023 11:20:27 -0600 Subject: [PATCH 020/134] change the name in index.js could not find file "uploadService.js", needed to carry over the name change to "nguploadService.js" --- www/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/index.js b/www/index.js index 66a0d45df..371d70ed5 100644 --- a/www/index.js +++ b/www/index.js @@ -43,7 +43,7 @@ import './js/survey/enketo/enketo-add-note-button.js'; import './js/metrics.js'; import './js/control/general-settings.js'; import './js/control/emailService.js'; -import './js/control/uploadService.js'; +import './js/control/nguploadService.js'; import './js/control/collect-settings.js'; import './js/control/sync-settings.js'; import './js/metrics-factory.js'; From eaf4be5dfa45925c638ac9dfdcb23f7a4e2b8e53 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 29 Sep 2023 11:27:07 -0600 Subject: [PATCH 021/134] adapt promise handling to async/await using async await is a clearer way of handling the asynchronous processes in this file, so it is easier to figure out what is happening and properly set up the typescript version of this service. --- www/js/control/uploadService.ts | 172 +++++++++++++------------------- 1 file changed, 68 insertions(+), 104 deletions(-) diff --git a/www/js/control/uploadService.ts b/www/js/control/uploadService.ts index e359bed88..0a7b5c96b 100644 --- a/www/js/control/uploadService.ts +++ b/www/js/control/uploadService.ts @@ -4,25 +4,27 @@ import { useTranslation } from "react-i18next"; /** * @returns A promise that resolves with an upload URL or rejects with an error */ -function getUploadConfig() { - return new Promise(function (resolve, reject) { - //logInfo( "About to get email config"); +async function getUploadConfig() { + return new Promise(async function (resolve, reject) { + logInfo( "About to get email config"); let url = []; - fetch("json/uploadConfig.json").then( function (uploadConfig) { - //logDebug("uploadConfigString = " + JSON.stringify(uploadConfig['data'])); + try { + let uploadConfig = await fetch("json/uploadConfig.json"); + logDebug("uploadConfigString = " + JSON.stringify(uploadConfig['data'])); url.push(uploadConfig["data"].url); resolve(url); - }).catch(function (err) { - fetch("json/uploadConfig.json.sample"). then(function (uploadConfig) { - //logDebug("default uploadConfigString = " + JSON.stringify(uploadConfig['data'])); + } catch (err) { + try{ + let uploadConfig = await fetch("json/uploadConfig.json.sample"); + logDebug("default uploadConfigString = " + JSON.stringify(uploadConfig['data'])); console.log("default uploadConfigString = " + JSON.stringify(uploadConfig['data'])) url.push(uploadConfig["data"].url); resolve(url); - }).catch(function (err) { - //logError("Error while reading default upload config" + err); + } catch (err) { + logError("Error while reading default upload config" + err); reject(err); - }) - }) + } + } }) } @@ -80,101 +82,63 @@ const sendToServer = function upload(url, binArray, params) { transformRequest: identity, params: params }; - return fetch (url, config); + return fetch(url, config); } //only export of this file, used in ProfileSettings and passed the argument (""loggerDB"") -export function uploadFile(database) { +export async function uploadFile(database, reason) { const { t } = useTranslation(); - getUploadConfig().then((uploadConfig) => { - var parentDir = "unknown"; - - if (window['cordova'].platformId.toLowerCase() == "android") { - parentDir = window['cordova'].file.applicationStorageDirectory+"/databases"; - } - else if (window['cordova'].platformId.toLowerCase() == "ios") { - parentDir = window['cordova'].file.dataDirectory + "../LocalDatabase"; - } else { - alert("parentDir unexpectedly = " + parentDir + "!") - } - - const newScope = {}; - newScope["data"] = {}; - newScope["fromDirText"] = t('upload-service.upload-from-dir', {parentDir: parentDir}); - newScope["toServerText"] = t('upload-service.upload-to-server', {serverURL: uploadConfig}); - - let didCancel = true; - let detailsPopup = () => console.log("I need a popup"); - - // const detailsPopup = $ionicPopup.show({ - // title: i18next.t("upload-service.upload-database", { db: database }), - // template: newScope.toServerText - // + '', - // scope: newScope, - // buttons: [ - // { - // text: 'Cancel', - // onTap: function(e) { - // didCancel = true; - // detailsPopup.close(); - // } - // }, - // { - // text: 'Upload', - // type: 'button-positive', - // onTap: function(e) { - // if (!newScope.data.reason) { - // //don't allow the user to close unless he enters wifi password - // didCancel = false; - // e.preventDefault(); - // } else { - // didCancel = false; - // return newScope.data.reason; - // } - // } - // } - // ] - // }); - - logInfo("Going to upload " + database); - const readFileAndInfo = [readDBFile(parentDir, database, detailsPopup)]; - Promise.all(readFileAndInfo).then(([binString, reason]) => { - if(!didCancel) - { - console.log("Uploading file of size "+binString['byteLength']); - const progressScope = {...newScope}; //make a child copy of the current scope - const params = { - reason: reason, - tz: Intl.DateTimeFormat().resolvedOptions().timeZone - } - uploadConfig.forEach((url) => { - alert(t("upload-service.upload-database", {db: database}) - + "\n" - + t("upload-service.upload-progress", {filesizemb: binString['byteLength'] / (1000 * 1000), serverURL: uploadConfig}) - ); - // const progressPopup = $ionicPopup.show({ - // title: t("upload-service.upload-database", - // {db: database}), - // template: t("upload-service.upload-progress", - // {filesizemb: binString['byteLength'] / (1000 * 1000), - // serverURL: uploadConfig}) - // + '
', - // scope: progressScope, - // buttons: [ - // { text: 'Cancel', type: 'button-cancel', }, - // ] - // }); - sendToServer(url, binString, params).then((response) => { - console.log(response); - //progressPopup.close(); - displayErrorMsg(t("upload-service.upload-details", - {filesizemb: binString['byteLength'] / (1000 * 1000), serverURL: uploadConfig}), - t("upload-service.upload-success")); - }).catch(onUploadError); - }); + try { + let uploadConfig = await getUploadConfig(); + var parentDir = "unknown"; + + if (window['cordova'].platformId.toLowerCase() == "android") { + parentDir = window['cordova'].file.applicationStorageDirectory+"/databases"; + } + else if (window['cordova'].platformId.toLowerCase() == "ios") { + parentDir = window['cordova'].file.dataDirectory + "../LocalDatabase"; + } else { + alert("parentDir unexpectedly = " + parentDir + "!") + } + + const newScope = {}; + newScope["data"] = {}; + newScope["fromDirText"] = t('upload-service.upload-from-dir', {parentDir: parentDir}); + newScope["toServerText"] = t('upload-service.upload-to-server', {serverURL: uploadConfig}); + + logInfo("Going to upload " + database); + try { + let binString = await readDBFile(parentDir, database, undefined); + console.log("Uploading file of size "+binString['byteLength']); + const progressScope = {...newScope}; //make a child copy of the current scope + const params = { + reason: reason, + tz: Intl.DateTimeFormat().resolvedOptions().timeZone + } + uploadConfig.forEach(async (url) => { + alert(t("upload-service.upload-database", {db: database}) + + "\n" + + t("upload-service.upload-progress", {filesizemb: binString['byteLength'] / (1000 * 1000), serverURL: uploadConfig}) + ); + + window.alert(t("upload-service.upload-database", {db: database})); + + try { + let response = await sendToServer(url, binString, params); + console.log(response); + window.alert(t("upload-service.upload-details", + {filesizemb: binString['byteLength'] / (1000 * 1000), serverURL: uploadConfig}) + + t("upload-service.upload-success")); + } catch (error) { + onUploadError(error); + } + }); + } + catch (error){ + onReadError(error); } - }).catch(onReadError); - }).catch(onReadError); - }; \ No newline at end of file + } catch (error) { + onReadError(error); + } +}; \ No newline at end of file From a07e7f5269ad08336c76c7f03c01341860798ca8 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Fri, 29 Sep 2023 14:26:53 -0700 Subject: [PATCH 022/134] add test with valid url and invalid url --- www/__tests__/customURL.test.ts | 38 +++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 www/__tests__/customURL.test.ts diff --git a/www/__tests__/customURL.test.ts b/www/__tests__/customURL.test.ts new file mode 100644 index 000000000..68ce3c47d --- /dev/null +++ b/www/__tests__/customURL.test.ts @@ -0,0 +1,38 @@ +import { onLaunchCustomURL } from '../js/splash/customURL'; + +describe('onLaunchCustomURL', () => { + let mockHandler; + + beforeEach(() => { + // create a new mock handler before each test case. + mockHandler = jest.fn(); + }); + + it('tests valid url 1 - should call handler callback with valid URL and the handler should be called with correct parameters', () => { + const validURL = 'emission://login_token?token=nrelop_dev-emulator-program'; + const expectedURL = 'login_token?token=nrelop_dev-emulator-program'; + const expectedComponents = { route: 'login_token', token: 'nrelop_dev-emulator-program' }; + onLaunchCustomURL(validURL, mockHandler); + expect(mockHandler).toHaveBeenCalledWith(expectedURL, expectedComponents); + }); + + it('tests valid url 2 - should call handler callback with valid URL and the handler should be called with correct parameters', () => { + const validURL = 'emission://test?param1=first¶m2=second'; + const expectedURL = 'test?param1=first¶m2=second'; + const expectedComponents = { route: 'test', param1: 'first', param2: 'second' }; + onLaunchCustomURL(validURL, mockHandler); + expect(mockHandler).toHaveBeenCalledWith(expectedURL, expectedComponents); + }); + + it('test invalid url 1 - should not call handler callback with invalid URL', () => { + const invalidURL = 'invalid_url'; + onLaunchCustomURL(invalidURL, mockHandler); + expect(mockHandler).not.toHaveBeenCalled(); + }); + + it('tests invalid url 2 - should not call handler callback with invalid URL', () => { + const invalidURL = ''; + onLaunchCustomURL(invalidURL, mockHandler); + expect(mockHandler).not.toHaveBeenCalled(); + }); +}) \ No newline at end of file From b7bc41e1aaeffafdfcfc309aee9354a634ac5eca Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Fri, 29 Sep 2023 14:27:28 -0700 Subject: [PATCH 023/134] add error-handling block --- www/js/splash/customURL.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/www/js/splash/customURL.ts b/www/js/splash/customURL.ts index 46460a6d7..bc3d93f3e 100644 --- a/www/js/splash/customURL.ts +++ b/www/js/splash/customURL.ts @@ -6,13 +6,17 @@ type OnLaunchCustomURL = (rawUrl: string, callback: (url: string, urlComponents: export const onLaunchCustomURL: OnLaunchCustomURL = (rawUrl, handler) => { - const url = rawUrl.split('//')[1]; - const [ route, paramString ] = url.split('?'); - const paramsList = paramString.split('&'); - const urlComponents: UrlComponents = { route : route }; - for (let i = 0; i < paramsList.length; i++) { - const [key, value] = paramsList[i].split('='); - urlComponents[key] = value; + try { + const url = rawUrl.split('//')[1]; + const [ route, paramString ] = url.split('?'); + const paramsList = paramString.split('&'); + const urlComponents: UrlComponents = { route : route }; + for (let i = 0; i < paramsList.length; i++) { + const [key, value] = paramsList[i].split('='); + urlComponents[key] = value; + } + handler(url, urlComponents); + }catch { + console.log('not a valid url'); } - handler(url, urlComponents); }; \ No newline at end of file From 4fdd18f329d114546435fc3dcb986c618e13f1fb Mon Sep 17 00:00:00 2001 From: louisg1337 Date: Fri, 29 Sep 2023 17:34:14 -0400 Subject: [PATCH 024/134] Bumped up version for battery integration optimization --- package.cordovabuild.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.cordovabuild.json b/package.cordovabuild.json index 20672bad0..8aa92d54f 100644 --- a/package.cordovabuild.json +++ b/package.cordovabuild.json @@ -127,7 +127,7 @@ "cordova-plugin-app-version": "0.1.14", "cordova-plugin-customurlscheme": "5.0.2", "cordova-plugin-device": "2.1.0", - "cordova-plugin-em-datacollection": "git+https://github.com/e-mission/e-mission-data-collection.git#v1.7.9", + "cordova-plugin-em-datacollection": "git+https://github.com/e-mission/e-mission-data-collection.git#v1.8.0", "cordova-plugin-em-opcodeauth": "git+https://github.com/e-mission/cordova-jwt-auth.git#v1.7.2", "cordova-plugin-em-server-communication": "git+https://github.com/e-mission/cordova-server-communication.git#v1.2.6", "cordova-plugin-em-serversync": "git+https://github.com/e-mission/cordova-server-sync.git#v1.3.2", From 03bc5c368c465c7ba8e6ec68873de51ae2d0a630 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Fri, 29 Sep 2023 16:21:34 -0600 Subject: [PATCH 025/134] Update README.md 1. Changed the content section 2. Updated links in "Contributing" section 3. Updated "end to end testing" section --- README.md | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 34184ab09..6292c41b1 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # [e-mission phone app](https://github.com/e-mission/e-mission-phone/tree/master) -__This is the phone component of the e-mission system.__ +__This is the phone component of the e-mission system.__ :sparkles: This has now been upgraded to cordova android@12.0.0 and iOS@6.2.0. It has also been upgraded to the **latest Android & iOS versions**, **cordova-lib@10.0.0 and the most recent node and npm versions**. It also now supports CI, so we should not have any build issues in the future. __This should be ready to build out of the box.__ +✨ We constantly upgrade the repo to the latest cordova versions of android, iOS, cordova-lib, and the most recent node and npm versions. The CI will be up-to-date. + For the latest versions, refer [`package.cordovabuild.json`](https://github.com/e-mission/e-mission-phone/blob/fce117ff859abd995613bd405dbc7d27c703b09b/package.cordovabuild.json) ## Additional Documentation @@ -15,12 +17,13 @@ https://github.com/e-mission/e-mission-docs/tree/master/docs/e-mission-phone :sparkles: Check [Contributing](#contributing) if you're interested in contributing for this project :sparkles: ## Contents -#### 1. [Creating logos](#creating-logos) -#### 2. [Updating the UI only](#updating-the-ui-only) -#### 3. [Updating the e-mission-* plugins or adding new plugins](#updating-the-e-mission--plugins-or-adding-new-plugins) +#### 1. [Updating the UI only](#updating-the-ui-only) +#### 2. [Updating the e-mission-* plugins or adding new plugins](#updating-the-e-mission--plugins-or-adding-new-plugins) +#### 3. [Creating logos](#creating-logos) #### 4. [End to End Testing](#end-to-end-testing) #### 5. [Beta-testing debugging](#beta-testing-debugging) #### 6. [Contributing](#contributing) +#### 7. [Troubleshooting](#troubleshooting) --- @@ -80,7 +83,7 @@ source setup/activate_serve.sh ## End to End Testing -A lot of the visualizations that we display in the phone client come from the server. In order to do end to end testing, we need to run a local server and connect to it. Instructions for: +A lot of the visualizations that we display in the phone client come from the server. In order to do end-to-end testing, we need to run a local server and connect to it. Instructions for: 1. installing a local server, 2. running it, @@ -89,7 +92,11 @@ A lot of the visualizations that we display in the phone client come from the se are available in the [e-mission-server README](https://github.com/e-mission/e-mission-server/blob/master/README.md). -In order to make end to end testing easy, if the local server is started on a HTTP (versus HTTPS port), it is in development mode. By default, the phone app connects to the local server (localhost on iOS, [10.0.2.2 on android](https://stackoverflow.com/questions/5806220/how-to-connect-to-my-http-localhost-web-server-from-android-emulator-in-eclips)) with the `prompted-auth` authentication method. To connect to a different server, or to use a different authentication method, you need to create a `www/json/connectionConfig.json` file. More details on configuring authentication [can be found in the docs](https://github.com/e-mission/e-mission-docs/blob/master/docs/install/configuring_authentication.md). +In order to make end-to-end testing easy, if the local server is started on a HTTP (versus HTTPS port), it is in development mode. By default, the phone app connects to the local server (localhost on iOS, [10.0.2.2 on android](https://stackoverflow.com/questions/5806220/how-to-connect-to-my-http-localhost-web-server-from-android-emulator-in-eclips)) with the `prompted-auth` authentication method. To connect to a different server, or to use a different authentication method, you need to modify the [nrel-commute.nrel-op.json](https://github.com/e-mission/nrel-openpath-deploy-configs/blob/482971d9715e8d52862e689658f9b4f2437e6401/configs/nrel-commute.nrel-op.json) file's dynamic config. +``` +"connectUrl": "https://nrel-commute-openpath.nrel.gov/api/" +``` +More details on configuring authentication [can be found in the docs](https://github.com/e-mission/e-mission-docs/blob/master/docs/install/configuring_authentication.md). One advantage of using `skip` authentication in development mode is that any user email can be entered without a password. Developers can use one of the emails that they loaded test data for in step (3) above. So if the test data loaded was with `-u shankari@eecs.berkeley.edu`, then the login email for the phone app would also be `shankari@eecs.berkeley.edu`. @@ -115,7 +122,6 @@ Pre-requisites - The version of xcode used by the CI. - to install a particular version, use [xcode-select](https://www.unix.com/man-page/OSX/1/xcode-select/) - or this [supposedly easier to use repo](https://github.com/xcpretty/xcode-install) - - **NOTE**: the basic xcode install on Catalina was messed up for me due to a prior installation of command line tools. [These workarounds helped](https://github.com/nodejs/node-gyp/blob/master/macOS_Catalina.md). - git - Java 17. Tested with [OpenJDK 17 (Temurin) using AdoptOpenJDK](https://adoptium.net). - Always use [homebrew](https://brew.sh) in addition to CLI @@ -263,21 +269,23 @@ less /tmp/loggerDB..withdate.log 1. Add the main repo as upstream ``` -2. git remote add upstream +git remote add upstream https://github.com/e-mission/e-mission-phone ``` -3. Create a new branch (IMPORTANT). Please do not submit pull requests from master +2. Create a new branch (IMPORTANT). Please do not submit pull requests from master ``` -4. git checkout -b +git checkout -b ``` -5. Make changes to the branch and commit them +3. Make changes to the branch and commit them ``` -6. git commit +git commit ``` - 7. Push the changes to your local fork +4. Push the changes to your local fork ``` -8. git push origin +git push origin ``` -9. Generate a pull request from the UI +5. Generate a pull request from the UI + +
__\*__Address my review comments__\*__ @@ -317,4 +325,6 @@ __2. Creating Logos__ - javascript errors: `rm -rf node_modules && npm install` - native code compile errors: `rm -rf plugins && rm -rf platforms && npx cordova prepare` -3. +3. (For updating the e-mission-plugins or adding new plugins) **NOTE**: the basic xcode install on Catalina was messed up for me due to a prior installation of command line tools. [These workarounds helped](https://github.com/nodejs/node-gyp/blob/master/macOS_Catalina.md). + +4. From 838f6da87095ef35a34aba1ea2b11c4fef921ccb Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 29 Sep 2023 16:38:55 -0600 Subject: [PATCH 026/134] adding user-input component of upload to profile we need to take a reason, so why not take it and then call the function? This workflow attempts to take user input in a modal and then pass it to the service's uploadFile function -- note, currently broken!!! --- www/js/control/ProfileSettings.jsx | 38 ++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index e3dab82e3..6e878296e 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from "react"; import { Modal, StyleSheet, ScrollView } from "react-native"; -import { Dialog, Button, useTheme, Text, Appbar, IconButton } from "react-native-paper"; +import { Dialog, Button, useTheme, Text, Appbar, IconButton, TextInput } from "react-native-paper"; import { angularize, getAngularService } from "../angular-react-helper"; import { useTranslation } from "react-i18next"; import ExpansionSection from "./ExpandMenu"; @@ -15,6 +15,8 @@ import DataDatePicker from "./DataDatePicker"; import AppStatusModal from "./AppStatusModal"; import PrivacyPolicyModal from "./PrivacyPolicyModal"; +import {uploadFile} from "./uploadService"; + let controlUpdateCompleteListenerRegistered = false; //any pure functions can go outside @@ -34,7 +36,6 @@ const ProfileSettings = () => { //angular services needed const CarbonDatasetHelper = getAngularService('CarbonDatasetHelper'); - const UploadHelper = getAngularService('UploadHelper'); const EmailHelper = getAngularService('EmailHelper'); const ControlCollectionHelper = getAngularService('ControlCollectionHelper'); const ControlSyncHelper = getAngularService('ControlSyncHelper'); @@ -73,6 +74,7 @@ const ProfileSettings = () => { const [consentVis, setConsentVis] = useState(false); const [dateDumpVis, setDateDumpVis] = useState(false); const [privacyVis, setPrivacyVis] = useState(false); + const [uploadVis, setUploadVis] = useState(false); const [collectSettings, setCollectSettings] = useState({}); const [notificationSettings, setNotificationSettings] = useState({}); @@ -85,6 +87,7 @@ const ProfileSettings = () => { const [consentDoc, setConsentDoc] = useState({}); const [dumpDate, setDumpDate] = useState(new Date()); const [toggleTime, setToggleTime] = useState(new Date()); + const [uploadReason, setUploadReason] = useState(""); let carbonDatasetString = t('general-settings.carbon-dataset') + ": " + CarbonDatasetHelper.getCurrentCarbonDatasetCode(); const carbonOptions = CarbonDatasetHelper.getCarbonDatasetOptions(); @@ -219,8 +222,12 @@ const ProfileSettings = () => { //methods that control the settings const uploadLog = function () { - UploadHelper.uploadFile("loggerDB") - }; + if(uploadReason != "") { + let reason = uploadReason.split('').join(''); + uploadFile("loggerDB", reason); + setUploadVis(false); + } + } const emailLog = function () { // Passing true, we want to send logs @@ -463,7 +470,7 @@ const ProfileSettings = () => { let logUploadSection; console.debug("appConfg: support_upload:", appConfig?.profile_controls?.support_upload); if (appConfig?.profile_controls?.support_upload) { - logUploadSection = ; + logUploadSection = setUploadVis(true)}>; } let timePicker; @@ -598,6 +605,27 @@ const ProfileSettings = () => {
+ {/* upload reason input */} + setUploadVis(false)} + transparent={true}> + setUploadVis(false)} + style={styles.dialog(colors.elevation.level3)}> + {t('upload-service.upload-database')} + + setUploadReason(uploadReason)} + placeholder={t('upload-service.please-fill-in-what-is-wrong')}> + + + + + + + + + {/* opcode viewing popup */} From 43b1b984ae7ff024df1300a9f163062d4a34a474 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 29 Sep 2023 17:01:48 -0600 Subject: [PATCH 027/134] resolve leftover issue from merge --- www/js/control/ProfileSettings.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index bbb713c57..691215366 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -451,7 +451,7 @@ const ProfileSettings = () => { transparent={true}> setUploadVis(false)} - style={styles.dialog(colors.elevation.level3)}> + style={settingStyles.dialog(colors.elevation.level3)}> {t('upload-service.upload-database')} Date: Fri, 29 Sep 2023 20:48:46 -0600 Subject: [PATCH 028/134] Update README.md 1. Updated buttons to point to the right CI 2. Removed point no. 4 in troubleshooting (it was empty) --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 6292c41b1..80abc7872 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ https://github.com/e-mission/e-mission-docs/tree/master/docs/e-mission-phone ## Updating the UI only -[![osx-serve-install](https://github.com/e-mission/e-mission-phone/workflows/osx-serve-install/badge.svg)](https://github.com/e-mission/e-mission-phone/actions?query=workflow%3Aosx-serve-install) +[![osx-serve-install](https://github.com/e-mission/e-mission-phone/workflows/osx-serve-install/badge.svg)](https://github.com/e-mission/e-mission-phone/actions/workflows/serve-install.yml) If you want to make only UI changes, (as opposed to modifying the existing plugins, adding new plugins, etc), you can use the **new and improved** (as of June 2018) [e-mission dev app](https://github.com/e-mission/e-mission-devapp/) and install the most recent version from [releases](https://github.com/e-mission/e-mission-devapp/releases). @@ -326,5 +326,3 @@ __2. Creating Logos__ - native code compile errors: `rm -rf plugins && rm -rf platforms && npx cordova prepare` 3. (For updating the e-mission-plugins or adding new plugins) **NOTE**: the basic xcode install on Catalina was messed up for me due to a prior installation of command line tools. [These workarounds helped](https://github.com/nodejs/node-gyp/blob/master/macOS_Catalina.md). - -4. From bbf18bdb939f9a30670cda10c54de4c66a251c84 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Fri, 29 Sep 2023 20:50:39 -0600 Subject: [PATCH 029/134] Update README.md Made sure the links for the buttons are correct --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 80abc7872..25f54d061 100644 --- a/README.md +++ b/README.md @@ -325,4 +325,4 @@ __2. Creating Logos__ - javascript errors: `rm -rf node_modules && npm install` - native code compile errors: `rm -rf plugins && rm -rf platforms && npx cordova prepare` -3. (For updating the e-mission-plugins or adding new plugins) **NOTE**: the basic xcode install on Catalina was messed up for me due to a prior installation of command line tools. [These workarounds helped](https://github.com/nodejs/node-gyp/blob/master/macOS_Catalina.md). +3. (For updating the e-mission-plugins or adding new plugins) **NOTE**: the basic xcode install on Mac OS Catalina was messed up for me due to a prior installation of command line tools. [These workarounds helped](https://github.com/nodejs/node-gyp/blob/master/macOS_Catalina.md). From 4da919f8c37a0802da85476532888a281ce0e33e Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Sat, 30 Sep 2023 15:51:09 -0600 Subject: [PATCH 030/134] Update README.md 1. Removed manual numbering 2. Removed any form of version numbers 3. Removed certain Permalinks and swapped it with relative PATHs 4. Made sure the structure is same --- README.md | 79 ++++++++++++++++++++++++++----------------------------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 25f54d061..7f8b62c29 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ __This is the phone component of the e-mission system.__ -:sparkles: This has now been upgraded to cordova android@12.0.0 and iOS@6.2.0. It has also been upgraded to the **latest Android & iOS versions**, **cordova-lib@10.0.0 and the most recent node and npm versions**. It also now supports CI, so we should not have any build issues in the future. __This should be ready to build out of the box.__ +:sparkles: This has been upgraded to the latest **Android**, **iOS**, **cordova-lib**, **node** and **npm** versions. __This is ready to build out of the box.__ ✨ We constantly upgrade the repo to the latest cordova versions of android, iOS, cordova-lib, and the most recent node and npm versions. The CI will be up-to-date. -For the latest versions, refer [`package.cordovabuild.json`](https://github.com/e-mission/e-mission-phone/blob/fce117ff859abd995613bd405dbc7d27c703b09b/package.cordovabuild.json) +For the latest versions, refer [`package.cordovabuild.json`](package.cordovabuild.json) ## Additional Documentation Additional documentation has been moved to its own repository [e-mission-docs](https://github.com/e-mission/e-mission-docs). Specific e-mission-phone wikis can be found here: @@ -35,7 +35,7 @@ If you want to make only UI changes, (as opposed to modifying the existing plugi ### Installing (one-time) -1. Run the setup script +:point_right:Run the setup script ``` bash setup/setup_serve.sh @@ -50,17 +50,16 @@ cp ..... www/json/connectionConfig.json ``` ### Activation (after install, and in every new shell) -2. Run this to activate ``` source setup/activate_serve.sh ``` ### Running -1. Start the phonegap deployment server and note the URL(s) that the server is listening to. +Start the phonegap deployment server and note the URL(s) that the server is listening to. - ``` - npm run serve + + npm run serve .... [phonegap] listening on 10.0.0.14:3000 [phonegap] listening on 192.168.162.1:3000 @@ -68,12 +67,11 @@ source setup/activate_serve.sh [phonegap] ctrl-c to stop the server [phonegap] .... - ``` -2. Change the devapp connection URL to one of these (e.g. 192.168.162.1:3000) and press "Connect" -3. The app will now display the version of e-mission app that is in your local directory - 4. The console logs will be displayed back in the server window (prefaced by `[console]`) - 5. Breakpoints can be added by connecting through the browser +Change the devapp connection URL to one of these (e.g. 192.168.162.1:3000) and press "Connect" +The app will now display the version of e-mission app that is in your local directory +The console logs will be displayed back in the server window (prefaced by `[console]`) +Breakpoints can be added by connecting through the browser - Safari ([enable develop menu](https://support.apple.com/guide/safari/use-the-safari-develop-menu-sfri20948/mac)): Develop -> Simulator -> index.html - Chrome: chrome://inspect -> Remote target (emulator) @@ -105,18 +103,6 @@ One advantage of using `skip` authentication in development mode is that any use [![osx-build-ios](https://github.com/e-mission/e-mission-phone/actions/workflows/ios-build.yml/badge.svg)](https://github.com/e-mission/e-mission-phone/actions/workflows/ios-build.yml) [![osx-build-android](https://github.com/e-mission/e-mission-phone/actions/workflows/android-build.yml/badge.svg)](https://github.com/e-mission/e-mission-phone/actions/workflows/android-build.yml) -__Important__ - -Most of the recent issues encountered have been due to incompatible setup. We -have now: -- locked down the dependencies, -- created setup and teardown scripts to setup self-contained environments with - those dependencies, and -- CI enabled to validate that they continue work. - -If you have setup failures, please compare the configuration in the **passing CI -builds** with your configuration. That is almost certainly the source of the error. - Pre-requisites --- - The version of xcode used by the CI. @@ -129,7 +115,19 @@ Pre-requisites running into ruby incompatibilities - e.g. https://github.com/CocoaPods/CocoaPods/issues/11763 -__1. Export statements__ +:triangular_flag_on_post: __Important__ + +Most of the recent issues encountered have been due to incompatible setup. We +have now: +- locked down the dependencies, +- created setup and teardown scripts to setup self-contained environments with + those dependencies, and +- CI enabled to validate that they continue work. + +If you have setup failures, please compare the configuration in the **passing CI +builds** with your configuration. That is almost certainly the source of the error. + +__Export statements__ ``` export ANDROID_SDK_ROOT="/Users//Library/Android/sdk" ``` @@ -165,7 +163,7 @@ aka the path where you want the SDK to be installed. -__2. Installing (one time only)__ +__Installing (one time only)__ - Run the setup script for the platform you want to build @@ -193,7 +191,7 @@ If connecting to a development server over http, make sure to turn on http suppo ``` -__3. Run this in every new shell for Activation__ +__Run this in every new shell for Activation__ ``` source setup/activate_native.sh @@ -202,9 +200,9 @@ source setup/activate_native.sh ``` Activating nvm -Using version 19.5.0 -Now using node v19.5.0 (npm v9.3.1) -npm version = 9.3.1 +Using version +Now using node (npm ) +npm version = Adding cocoapods to the path Verifying /Users//Library/Android/sk or /Users//Library/Android/sdk is set Activating sdkman, and by default, gradle @@ -216,9 +214,9 @@ Copied config.cordovabuild.xml -> config.xml and package.cordovabuild.json -> pa
- __4. Pick a type of build and execute the following:__ + __Pick a type of build and execute the following:__ -More "versions" are available in [`package.cordovabuild.json`](https://github.com/e-mission/e-mission-phone/blob/fce117ff859abd995613bd405dbc7d27c703b09b/package.cordovabuild.json) +More "versions" are available in [`package.cordovabuild.json`](package.cordovabuild.json) ``` npm run ``` @@ -266,24 +264,23 @@ less /tmp/loggerDB..withdate.log ## Contributing - -1. Add the main repo as upstream +:point_right:Add the main repo as upstream ``` git remote add upstream https://github.com/e-mission/e-mission-phone ``` -2. Create a new branch (IMPORTANT). Please do not submit pull requests from master +:point_right:Create a new branch (IMPORTANT). Please do not submit pull requests from master ``` git checkout -b ``` -3. Make changes to the branch and commit them +:point_right:Make changes to the branch and commit them ``` git commit ``` -4. Push the changes to your local fork +:point_right:Push the changes to your local fork ``` git push origin ``` -5. Generate a pull request from the UI +:point_right:Generate a pull request from the UI
@@ -305,7 +302,7 @@ git branch -d --- ### Troubleshooting -__1. Xcode command line tools__ +:point_right:Xcode command line tools ``` Warning: No developer tools installed. You should install the Command Line Tools. @@ -314,7 +311,7 @@ You should install the Command Line Tools. xcode-select --install ``` -__2. Creating Logos__ +:point_right:Creating Logos - Make sure to use `npx ionic` and `npx cordova`. This is because the setup script installs all the modules locally in a self-contained environment using `npm install` and not `npm install -g` @@ -325,4 +322,4 @@ __2. Creating Logos__ - javascript errors: `rm -rf node_modules && npm install` - native code compile errors: `rm -rf plugins && rm -rf platforms && npx cordova prepare` -3. (For updating the e-mission-plugins or adding new plugins) **NOTE**: the basic xcode install on Mac OS Catalina was messed up for me due to a prior installation of command line tools. [These workarounds helped](https://github.com/nodejs/node-gyp/blob/master/macOS_Catalina.md). +(For updating the e-mission-plugins or adding new plugins) **NOTE**: the basic xcode install on Mac OS Catalina was messed up for me due to a prior installation of command line tools. [These workarounds helped](https://github.com/nodejs/node-gyp/blob/master/macOS_Catalina.md). From e8cbcb5cb4a2feaf20f7d6762e43ba8d47b44533 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Sun, 1 Oct 2023 12:06:04 -0600 Subject: [PATCH 031/134] Update README.md iter 1 - fix end to end testing --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7f8b62c29..f079fe014 100644 --- a/README.md +++ b/README.md @@ -90,11 +90,9 @@ A lot of the visualizations that we display in the phone client come from the se are available in the [e-mission-server README](https://github.com/e-mission/e-mission-server/blob/master/README.md). -In order to make end-to-end testing easy, if the local server is started on a HTTP (versus HTTPS port), it is in development mode. By default, the phone app connects to the local server (localhost on iOS, [10.0.2.2 on android](https://stackoverflow.com/questions/5806220/how-to-connect-to-my-http-localhost-web-server-from-android-emulator-in-eclips)) with the `prompted-auth` authentication method. To connect to a different server, or to use a different authentication method, you need to modify the [nrel-commute.nrel-op.json](https://github.com/e-mission/nrel-openpath-deploy-configs/blob/482971d9715e8d52862e689658f9b4f2437e6401/configs/nrel-commute.nrel-op.json) file's dynamic config. -``` -"connectUrl": "https://nrel-commute-openpath.nrel.gov/api/" -``` -More details on configuring authentication [can be found in the docs](https://github.com/e-mission/e-mission-docs/blob/master/docs/install/configuring_authentication.md). +In order to make end-to-end testing even easier, we have moved from phone app to a dynamic config setting. We use the dynamic [config](configs) where each file upholds a function. Refer [Doc](https://github.com/e-mission/nrel-openpath-deploy-configs) + +If you have the [e-mission-server](https://github.com/e-mission/e-mission-server) running at a specific URL or IP address, and they want to connect to it, you would have to specify that URL or IP in the server field of their dynamic config file. One advantage of using `skip` authentication in development mode is that any user email can be entered without a password. Developers can use one of the emails that they loaded test data for in step (3) above. So if the test data loaded was with `-u shankari@eecs.berkeley.edu`, then the login email for the phone app would also be `shankari@eecs.berkeley.edu`. From 498e04e9da46c5ad55ff2c9df98ff20c59aea840 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Mon, 2 Oct 2023 08:53:27 -0600 Subject: [PATCH 032/134] Delete Build_ss.png Not required anymore --- Build_ss.png | Bin 83719 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Build_ss.png diff --git a/Build_ss.png b/Build_ss.png deleted file mode 100644 index 18ab48b232d04ae28e72f5677d393045be584659..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 83719 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 From f4f3e6858e6696b3d0cdae4f6f7ab37d90333a44 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Mon, 2 Oct 2023 09:07:07 -0600 Subject: [PATCH 033/134] Update README.md - Better wordings --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f079fe014..6de8fcec1 100644 --- a/README.md +++ b/README.md @@ -90,9 +90,9 @@ A lot of the visualizations that we display in the phone client come from the se are available in the [e-mission-server README](https://github.com/e-mission/e-mission-server/blob/master/README.md). -In order to make end-to-end testing even easier, we have moved from phone app to a dynamic config setting. We use the dynamic [config](configs) where each file upholds a function. Refer [Doc](https://github.com/e-mission/nrel-openpath-deploy-configs) +In order to make end-to-end testing even easier, we have moved from phone app to a dynamic config setting. We use the dynamic [config](configs) to specify the server locations. Refer [Doc](https://github.com/e-mission/nrel-openpath-deploy-configs) -If you have the [e-mission-server](https://github.com/e-mission/e-mission-server) running at a specific URL or IP address, and they want to connect to it, you would have to specify that URL or IP in the server field of their dynamic config file. +If you have the [e-mission-server](https://github.com/e-mission/e-mission-server) running at a specific URL or IP address, and they want to connect to it, you would have to specify that URL or IP in the server of your dynamic config file. One advantage of using `skip` authentication in development mode is that any user email can be entered without a password. Developers can use one of the emails that they loaded test data for in step (3) above. So if the test data loaded was with `-u shankari@eecs.berkeley.edu`, then the login email for the phone app would also be `shankari@eecs.berkeley.edu`. From e1a6a32db1861ce686c85abc81d2a84db4ce3a35 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Mon, 2 Oct 2023 09:08:22 -0600 Subject: [PATCH 034/134] Update README.md - Removed relative PATH for dynamic links as they don't seem to work --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6de8fcec1..db401d00b 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ A lot of the visualizations that we display in the phone client come from the se are available in the [e-mission-server README](https://github.com/e-mission/e-mission-server/blob/master/README.md). -In order to make end-to-end testing even easier, we have moved from phone app to a dynamic config setting. We use the dynamic [config](configs) to specify the server locations. Refer [Doc](https://github.com/e-mission/nrel-openpath-deploy-configs) +In order to make end-to-end testing even easier, we have moved from phone app to a dynamic config setting. We use the dynamic config to specify the server locations. Refer [Doc](https://github.com/e-mission/nrel-openpath-deploy-configs) If you have the [e-mission-server](https://github.com/e-mission/e-mission-server) running at a specific URL or IP address, and they want to connect to it, you would have to specify that URL or IP in the server of your dynamic config file. From d3f8970ef8e6f27d1e56b7b94d7ae02e3b8ebbd1 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Mon, 2 Oct 2023 10:40:04 -0700 Subject: [PATCH 035/134] Checkmark dynamically appears, color matches Updated the button to only appear if you can verify the trip. Button color now matches the "inferred trip" data. --- www/js/survey/multilabel/MultiLabelButtonGroup.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/www/js/survey/multilabel/MultiLabelButtonGroup.tsx b/www/js/survey/multilabel/MultiLabelButtonGroup.tsx index 268d16eb7..33842f48c 100644 --- a/www/js/survey/multilabel/MultiLabelButtonGroup.tsx +++ b/www/js/survey/multilabel/MultiLabelButtonGroup.tsx @@ -100,11 +100,12 @@ const MultilabelButtonGroup = ({ trip, buttonsInline=false }) => { ) })} - - - + {trip.verifiability === 'can-verify' && ( + + + + )} dismiss()}> dismiss()}> From 693852dda0d1310998029a2f1da4ce8a3347b2e8 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Mon, 2 Oct 2023 13:13:58 -0600 Subject: [PATCH 036/134] Update README.md Modified the server to `server` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index db401d00b..9ac9ceb1d 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ are available in the [e-mission-server README](https://github.com/e-mission/e-mi In order to make end-to-end testing even easier, we have moved from phone app to a dynamic config setting. We use the dynamic config to specify the server locations. Refer [Doc](https://github.com/e-mission/nrel-openpath-deploy-configs) -If you have the [e-mission-server](https://github.com/e-mission/e-mission-server) running at a specific URL or IP address, and they want to connect to it, you would have to specify that URL or IP in the server of your dynamic config file. +If you have the [e-mission-server](https://github.com/e-mission/e-mission-server) running at a specific URL or IP address, and you want to connect to it, you would have to specify that URL or IP in the `server` field of your dynamic config file. One advantage of using `skip` authentication in development mode is that any user email can be entered without a password. Developers can use one of the emails that they loaded test data for in step (3) above. So if the test data loaded was with `-u shankari@eecs.berkeley.edu`, then the login email for the phone app would also be `shankari@eecs.berkeley.edu`. From 5a8d24854e0d507ac6b8505fdb86b856494c55b6 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 2 Oct 2023 15:26:25 -0600 Subject: [PATCH 037/134] correct message in reason modal --- www/js/control/ProfileSettings.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 691215366..cd8906ea3 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -452,7 +452,7 @@ const ProfileSettings = () => { setUploadVis(false)} style={settingStyles.dialog(colors.elevation.level3)}> - {t('upload-service.upload-database')} + {t('upload-service.upload-database', {db: "loggerDB"})} Date: Mon, 2 Oct 2023 15:27:45 -0600 Subject: [PATCH 038/134] upload service working This now appears to be working with the dev server, as the POST requests are going through successfully The next step is testing and potentially polishing the message popups to the user --- www/js/control/uploadService.ts | 61 +++++++++++++-------------------- 1 file changed, 24 insertions(+), 37 deletions(-) diff --git a/www/js/control/uploadService.ts b/www/js/control/uploadService.ts index 0a7b5c96b..e0c00a713 100644 --- a/www/js/control/uploadService.ts +++ b/www/js/control/uploadService.ts @@ -1,6 +1,7 @@ import { logDebug, logInfo, logError, displayError, displayErrorMsg } from "../plugin/logger"; -import { useTranslation } from "react-i18next"; +import i18next from "i18next"; +// const { t } = useTranslation(); /** * @returns A promise that resolves with an upload URL or rejects with an error */ @@ -9,16 +10,18 @@ async function getUploadConfig() { logInfo( "About to get email config"); let url = []; try { - let uploadConfig = await fetch("json/uploadConfig.json"); - logDebug("uploadConfigString = " + JSON.stringify(uploadConfig['data'])); - url.push(uploadConfig["data"].url); + let response = await fetch("json/uploadConfig.json"); + let uploadConfig = await response.json(); + logDebug("uploadConfigString = " + JSON.stringify(uploadConfig['url'])); + url.push(uploadConfig["url"]); resolve(url); } catch (err) { try{ - let uploadConfig = await fetch("json/uploadConfig.json.sample"); - logDebug("default uploadConfigString = " + JSON.stringify(uploadConfig['data'])); - console.log("default uploadConfigString = " + JSON.stringify(uploadConfig['data'])) - url.push(uploadConfig["data"].url); + let response = await fetch("json/uploadConfig.json.sample"); + let uploadConfig = await response.json(); + logDebug("default uploadConfigString = " + JSON.stringify(uploadConfig['url'])); + console.log("default uploadConfigString = " + JSON.stringify(uploadConfig['url'])) + url.push(uploadConfig["url"]); resolve(url); } catch (err) { logError("Error while reading default upload config" + err); @@ -70,25 +73,18 @@ function readDBFile(parentDir, database, callbackFn) { } const sendToServer = function upload(url, binArray, params) { - //attempting to replace angular.identity - var identity = function() { - return arguments[0]; - } - - var config = { - method: "POST", - body: binArray, + //this was the best way I could find to contact the database, + //had to modify the way it gets handled on the other side + //the original way it could not find "reason" + return fetch(url, { + method: 'POST', headers: {'Content-Type': undefined }, - transformRequest: identity, - params: params - }; - return fetch(url, config); + body: JSON.stringify({ data: binArray, params: params }) + } ) } - //only export of this file, used in ProfileSettings and passed the argument (""loggerDB"") export async function uploadFile(database, reason) { - const { t } = useTranslation(); try { let uploadConfig = await getUploadConfig(); var parentDir = "unknown"; @@ -102,34 +98,25 @@ export async function uploadFile(database, reason) { alert("parentDir unexpectedly = " + parentDir + "!") } - const newScope = {}; - newScope["data"] = {}; - newScope["fromDirText"] = t('upload-service.upload-from-dir', {parentDir: parentDir}); - newScope["toServerText"] = t('upload-service.upload-to-server', {serverURL: uploadConfig}); - logInfo("Going to upload " + database); try { let binString = await readDBFile(parentDir, database, undefined); console.log("Uploading file of size "+binString['byteLength']); - const progressScope = {...newScope}; //make a child copy of the current scope const params = { reason: reason, tz: Intl.DateTimeFormat().resolvedOptions().timeZone } uploadConfig.forEach(async (url) => { - alert(t("upload-service.upload-database", {db: database}) - + "\n" - + t("upload-service.upload-progress", {filesizemb: binString['byteLength'] / (1000 * 1000), serverURL: uploadConfig}) - ); - - window.alert(t("upload-service.upload-database", {db: database})); + //have alert for starting upload, but not progress + window.alert(i18next.t("upload-service.upload-database", {db: database})); try { + //const binArray = {byteLength: binString.byteLength, byteOffset: binString.byteOffset} let response = await sendToServer(url, binString, params); console.log(response); - window.alert(t("upload-service.upload-details", - {filesizemb: binString['byteLength'] / (1000 * 1000), serverURL: uploadConfig}) - + t("upload-service.upload-success")); + window.alert(i18next.t("upload-service.upload-details", + {filesizemb: binString['byteLength'] / (1000 * 1000), serverURL: url}) + + i18next.t("upload-service.upload-success")); } catch (error) { onUploadError(error); } From a5451e9bfd377ec511e355984a07c52ae197f1a5 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 2 Oct 2023 16:41:52 -0600 Subject: [PATCH 039/134] resolve errors there were two errors in VScode, which were also tripping the tests up when I tried to run the uploadFile function -- resolving these did not make the tests run, but it did get past the first of the errors! --- www/js/control/uploadService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/control/uploadService.ts b/www/js/control/uploadService.ts index e0c00a713..a09fdaed3 100644 --- a/www/js/control/uploadService.ts +++ b/www/js/control/uploadService.ts @@ -61,8 +61,8 @@ function readDBFile(parentDir, database, callbackFn) { } reader.onload = function() { - console.log("Successful file read with " + this.result.byteLength +" characters"); - resolve(new DataView(this.result)); + console.log("Successful file read with " + this.result['byteLength'] +" characters"); + resolve(new DataView(this.result as ArrayBuffer)); } reader.readAsArrayBuffer(file); From a5bd54ef10989b335a01ab22eae503a552d2ecc9 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 3 Oct 2023 10:14:07 -0400 Subject: [PATCH 040/134] fix permissions popup on 'summary' page; refactor https://github.com/e-mission/e-mission-docs/issues/998 Adding the 'summary' page caused a regression in which the permissions popup could appear before the consent page, while the onboarding route is 'summary'. This is because when it was added, I forgot to add it to the conditional statement in App.tsx, line 79. Semantically we want the routes to be named, but also ordered. So a cleaner way to handle these routes is to declare them as enum values instead of strings. Behind the scense, TypeScript enums automatically get assigned an integer value (WELCOME=0, SUMMARY=1, etc). This way, to see if a route is past consent or not, we can use `<` and `>` comparisons instead of specifically listing out and checking against different string values. So, this commit switches over to using `enum` for the onboarding state route and uses `>` in the conditional to ensure that the AppStatusModal can only show if we are past the consent page. Also tidied up all the code comments around the onboarding states and routes. --- www/js/App.tsx | 6 ++++-- www/js/onboarding/OnboardingStack.tsx | 19 ++++++++----------- www/js/onboarding/onboardingHelper.ts | 20 +++++++++++++------- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/www/js/App.tsx b/www/js/App.tsx index aa0068509..1ace61531 100644 --- a/www/js/App.tsx +++ b/www/js/App.tsx @@ -7,7 +7,7 @@ import MetricsTab from './metrics/MetricsTab'; import ProfileSettings from './control/ProfileSettings'; import useAppConfig from './useAppConfig'; import OnboardingStack from './onboarding/OnboardingStack'; -import { OnboardingState, getPendingOnboardingState } from './onboarding/onboardingHelper'; +import { OnboardingRoute, OnboardingState, getPendingOnboardingState } from './onboarding/onboardingHelper'; import { setServerConnSettings } from './config/serverConn'; import AppStatusModal from './control/AppStatusModal'; @@ -76,7 +76,9 @@ const App = () => { : } - {(pendingOnboardingState == null || pendingOnboardingState.route != 'welcome' && pendingOnboardingState.route != 'consent') && + { /* if onboarding is done (state == null), or if is in progress but we are past the + consent page (route > CONSENT), the permissions popup can show if needed */ } + {(pendingOnboardingState == null || pendingOnboardingState.route > OnboardingRoute.CONSENT) && } diff --git a/www/js/onboarding/OnboardingStack.tsx b/www/js/onboarding/OnboardingStack.tsx index 5afb69f30..643744ed3 100644 --- a/www/js/onboarding/OnboardingStack.tsx +++ b/www/js/onboarding/OnboardingStack.tsx @@ -6,30 +6,27 @@ import ConsentPage from "./ConsentPage"; import SurveyPage from "./SurveyPage"; import SaveQrPage from "./SaveQrPage"; import SummaryPage from "./SummaryPage"; +import { OnboardingRoute } from "./onboardingHelper"; +import { displayErrorMsg } from "../plugin/logger"; -// true if loading/undetermined -// 'welcome' if no config present -// 'consent' if config present, but not consented -// 'survey' if consented but intro not done -// null if intro done const OnboardingStack = () => { const { pendingOnboardingState } = useContext(AppContext); console.debug('pendingOnboardingState in OnboardingStack', pendingOnboardingState); - if (pendingOnboardingState.route == 'welcome') { + if (pendingOnboardingState.route == OnboardingRoute.WELCOME) { return ; - } else if (pendingOnboardingState.route == 'summary') { + } else if (pendingOnboardingState.route == OnboardingRoute.SUMMARY) { return ; - } else if (pendingOnboardingState.route == 'consent') { + } else if (pendingOnboardingState.route == OnboardingRoute.CONSENT) { return ; - } else if (pendingOnboardingState.route == 'save-qr') { + } else if (pendingOnboardingState.route == OnboardingRoute.SAVE_QR) { return ; - } else if (pendingOnboardingState.route == 'survey') { + } else if (pendingOnboardingState.route == OnboardingRoute.SURVEY) { return ; } else { - return 'TODO' + displayErrorMsg('OnboardingStack: unknown route', pendingOnboardingState.route); } } diff --git a/www/js/onboarding/onboardingHelper.ts b/www/js/onboarding/onboardingHelper.ts index 324e4ae3e..7874ab9f8 100644 --- a/www/js/onboarding/onboardingHelper.ts +++ b/www/js/onboarding/onboardingHelper.ts @@ -4,7 +4,13 @@ import { getConfig } from "../config/dynamicConfig"; export const INTRO_DONE_KEY = 'intro_done'; -type OnboardingRoute = 'welcome' | 'summary' | 'consent' | 'survey' | 'save-qr' | false; +// state = null if onboarding is done +// route = WELCOME if no config present +// route = SUMMARY if config present, but not consented and summary not done +// route = CONSENT if config present, but not consented and summary done +// route = SAVE_QR if config present, consented, but save qr not done +// route = SURVEY if config present, consented and save qr done +export enum OnboardingRoute { WELCOME, SUMMARY, CONSENT, SAVE_QR, SURVEY, NONE }; export type OnboardingState = { opcode: string, route: OnboardingRoute, @@ -22,17 +28,17 @@ export const setRegisterUserDone = (b) => registerUserDone = b; export function getPendingOnboardingState(): Promise { return Promise.all([getConfig(), readConsented(), readIntroDone()]).then(([config, isConsented, isIntroDone]) => { if (isIntroDone) return null; // onboarding is done; no pending state - let route: OnboardingRoute = false; + let route: OnboardingRoute = OnboardingRoute.NONE; if (!config) { - route = 'welcome'; + route = OnboardingRoute.WELCOME; } else if (!isConsented && !summaryDone) { - route = 'summary'; + route = OnboardingRoute.SUMMARY; } else if (!isConsented) { - route = 'consent'; + route = OnboardingRoute.CONSENT; } else if (!saveQrDone) { - route = 'save-qr'; + route = OnboardingRoute.SAVE_QR; } else { - route = 'survey'; + route = OnboardingRoute.SURVEY; } return { route, opcode: config?.joined?.opcode }; }); From 6262d2e6d80a2bff87132649dd4882215f8b3299 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Tue, 3 Oct 2023 08:17:57 -0700 Subject: [PATCH 041/134] Update www/js/survey/multilabel/MultiLabelButtonGroup.tsx See conversation in PR `rem` doesn't work properly in react-native, updated Co-authored-by: Jack Greenlee --- www/js/survey/multilabel/MultiLabelButtonGroup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/survey/multilabel/MultiLabelButtonGroup.tsx b/www/js/survey/multilabel/MultiLabelButtonGroup.tsx index 33842f48c..ecfcf7ea6 100644 --- a/www/js/survey/multilabel/MultiLabelButtonGroup.tsx +++ b/www/js/survey/multilabel/MultiLabelButtonGroup.tsx @@ -101,7 +101,7 @@ const MultilabelButtonGroup = ({ trip, buttonsInline=false }) => { })} {trip.verifiability === 'can-verify' && ( - + From 9c36f7dfb894122810165443f97c1c4d27d306a1 Mon Sep 17 00:00:00 2001 From: Katie Rischpater <98350084+the-bay-kay@users.noreply.github.com> Date: Tue, 3 Oct 2023 08:19:43 -0700 Subject: [PATCH 042/134] Update www/js/survey/multilabel/MultiLabelButtonGroup.tsx iconButtonProp & Border Color Changed prop to use native property, adjusted border color. Co-authored-by: Jack Greenlee --- www/js/survey/multilabel/MultiLabelButtonGroup.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/www/js/survey/multilabel/MultiLabelButtonGroup.tsx b/www/js/survey/multilabel/MultiLabelButtonGroup.tsx index ecfcf7ea6..0ce031bee 100644 --- a/www/js/survey/multilabel/MultiLabelButtonGroup.tsx +++ b/www/js/survey/multilabel/MultiLabelButtonGroup.tsx @@ -103,7 +103,8 @@ const MultilabelButtonGroup = ({ trip, buttonsInline=false }) => { {trip.verifiability === 'can-verify' && ( + containerColor={colors.secondaryContainer} + style={{width: 24, height: 24, margin: 3, borderColor: colors.secondary}} /> )} From 1e19ab24d137ad8cfb660c9d57ddefd44e592d3d Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Tue, 3 Oct 2023 10:03:12 -0700 Subject: [PATCH 043/134] Add testId for unit test --- www/js/diary/list/LoadMoreButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/diary/list/LoadMoreButton.tsx b/www/js/diary/list/LoadMoreButton.tsx index 05fa2ecd1..f3d6db082 100644 --- a/www/js/diary/list/LoadMoreButton.tsx +++ b/www/js/diary/list/LoadMoreButton.tsx @@ -6,7 +6,7 @@ const LoadMoreButton = ({ children, onPressFn, ...otherProps }) => { const { colors } = useTheme(); return ( - From 693361f438ade26b027eacbd4196e7f2636588d3 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Tue, 3 Oct 2023 10:04:01 -0700 Subject: [PATCH 044/134] Add LoadMoreButton component unit test --- www/__tests__/LoadMoreButton.test.tsx | 30 +++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 www/__tests__/LoadMoreButton.test.tsx diff --git a/www/__tests__/LoadMoreButton.test.tsx b/www/__tests__/LoadMoreButton.test.tsx new file mode 100644 index 000000000..5acb4a700 --- /dev/null +++ b/www/__tests__/LoadMoreButton.test.tsx @@ -0,0 +1,30 @@ +/** + * @jest-environment jsdom + */ +import React from 'react' +import {render, fireEvent, waitFor, screen} from '@testing-library/react-native' +import LoadMoreButton from '../js/diary/list/LoadMoreButton' + + +describe("LoadMoreButton", () => { + it("renders correctly", async () => { + render( + {}}>{} + ); + await waitFor(() => { + expect(screen.getByTestId("load-button")).toBeTruthy(); + }); + }); + + it("calls onPressFn when clicked", () => { + const mockFn = jest.fn(); + const { getByTestId } = render( + {} + ); + const loadButton = getByTestId("load-button"); + fireEvent.press(loadButton); + expect(mockFn).toHaveBeenCalled(); + }); +}); + + From 8e28d47a2da8be00c834912102e7adadaf18bedf Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Tue, 3 Oct 2023 11:43:21 -0600 Subject: [PATCH 045/134] add await to forceTransition process there was an issue with the alert, which was showing a promise object rather than the actual result. This change works to resolve that issue --- www/js/control/ControlCollectionHelper.tsx | 2 +- www/js/control/ProfileSettings.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/control/ControlCollectionHelper.tsx b/www/js/control/ControlCollectionHelper.tsx index 99318b1ac..d94f12448 100644 --- a/www/js/control/ControlCollectionHelper.tsx +++ b/www/js/control/ControlCollectionHelper.tsx @@ -21,7 +21,7 @@ type collectionConfig = { export async function forceTransition(transition) { try { - let result = forceTransitionWrapper(transition); + let result = await forceTransitionWrapper(transition); window.alert('success -> '+result); } catch (err) { window.alert('error -> '+err); diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 8035c9462..5f4347e21 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -252,7 +252,7 @@ const ProfileSettings = () => { async function userStartStopTracking() { const transitionToForce = collectSettings.trackingOn ? 'STOP_TRACKING' : 'START_TRACKING'; - forceTransition(transitionToForce); + await forceTransition(transitionToForce); refreshCollectSettings(); } From f0b3a3c3de071b45aaf7f96133731cbd9f16a6f9 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Tue, 3 Oct 2023 10:50:59 -0700 Subject: [PATCH 046/134] Change jest setup for component test --- jest.config.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/jest.config.json b/jest.config.json index 78dc839b4..21e5f6a05 100644 --- a/jest.config.json +++ b/jest.config.json @@ -11,5 +11,10 @@ }, "moduleNameMapper": { "^react-native$": "react-native-web" - } + }, + "preset": "react-native", + "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"], + "transformIgnorePatterns": [ + "node_modules/(?!(jest-)?@?react-native|@react-native-community|@react-navigation)" + ] } From a7d4ec3b07e0f7f472d1a288c9362ad426caeb95 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Tue, 3 Oct 2023 16:30:49 -0600 Subject: [PATCH 047/134] set up testing for uploadService tests are not fully running yet, but making progress and need to save before getting the mocks from a remote branch --- www/__tests__/uploadService.tests.ts | 29 +++++++++++++++++++++++++++- www/json/uploadConfig.json.sample | 2 +- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/www/__tests__/uploadService.tests.ts b/www/__tests__/uploadService.tests.ts index 50a253a2e..0544d22cd 100644 --- a/www/__tests__/uploadService.tests.ts +++ b/www/__tests__/uploadService.tests.ts @@ -1,2 +1,29 @@ -import {} from "../js/control/uploadService"; +import {uploadFile} from "../js/control/uploadService"; +import { mockLogger } from '../__mocks__/globalMocks'; +mockLogger(); + +// mock for JavaScript 'fetch' +// we emulate a 100ms delay when i) fetching data and ii) parsing it as text +global.fetch = (url: string, options: {method: string, headers: {}, body: string}) => new Promise((rs, rj) => { + setTimeout(() => rs({ + text: () => new Promise((rs, rj) => { + setTimeout(() => rs(new Response('sent ' + options.method + options.body + ' to ' + url)), 100); + }) + })); +}) as any; + +global.fetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve({ url: "http://localhost:5647/phonelogs" }), + }), +) as any + +//this is never used in production right now +//however, tests are still important to make sure the code works +//at some point we hope to restore this functionality +it('posts the logs to the configured database', async () => { + const posted = await uploadFile("loggerDB", "HelloWorld"); + expect(posted).toEqual(expect.stringContaining("HelloWorld")); + expect(posted).toEqual(expect.stringContaining("POST")); +}); \ No newline at end of file diff --git a/www/json/uploadConfig.json.sample b/www/json/uploadConfig.json.sample index 53cead55e..a3c8b7210 100644 --- a/www/json/uploadConfig.json.sample +++ b/www/json/uploadConfig.json.sample @@ -1,3 +1,3 @@ { - "url": "http://fill.me.in " + "url": "http://localhost:5647/phonelogs" } From 3c22583af87c06f73f40836a45d6aa6c059db62b Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 3 Oct 2023 19:38:12 -0400 Subject: [PATCH 048/134] OnboardingState=DONE if done; null if undetermined We had this issue (https://github.com/e-mission/e-mission-docs/issues/999) where if the app was exited in the middle of onboarding, and then opened again, this error would show. I found out that this occured because the LabelTab was being rendered for a split second until the onboarding state was still being resolved. It takes some time to determine the onboarding state, and it requires waiting for native calls, which is why `getPendingOnboardingState` in onboardingHelper returns a Promise. But until this promise is resolved, we don't really know if we should show a) onboarding flow or b) the main app with tab navigation. We had been keeping `pendingOnboardingState` as `null` if the onboarding was finished. In this case, there was no 'pending' onboarding state and we would just continue to the main app. But a safer thing to do here is have onboarding state be null **only** if it hasn't been determined yet. If it *has* been determined, then we will explictly mark it with a route of DONE. So if it's DONE we show the main app, if it's something other than DONE, we show the onboarding flow, and if it's null, we can show a big loading spinner while we determine what state we are in. In doing this, we no longer just keep track of *pending* onboarding states - we must always keep track of an onboarding state, whether it is DONE or not. So while making this adjustment, I renamed the variable throughout as simply 'onboardingState'. --- www/js/App.tsx | 58 ++++++++++++++++----------- www/js/onboarding/OnboardingStack.tsx | 16 ++++---- www/js/onboarding/SaveQrPage.tsx | 10 ++--- www/js/onboarding/onboardingHelper.ts | 11 ++--- 4 files changed, 54 insertions(+), 41 deletions(-) diff --git a/www/js/App.tsx b/www/js/App.tsx index 1ace61531..628baf21b 100644 --- a/www/js/App.tsx +++ b/www/js/App.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, createContext, useMemo } from 'react'; import { getAngularService } from './angular-react-helper'; -import { BottomNavigation, Button, useTheme } from 'react-native-paper'; +import { ActivityIndicator, BottomNavigation, useTheme } from 'react-native-paper'; import { useTranslation } from 'react-i18next'; import LabelTab from './diary/LabelTab'; import MetricsTab from './metrics/MetricsTab'; @@ -22,7 +22,8 @@ export const AppContext = createContext({}); const App = () => { const [index, setIndex] = useState(0); - const [pendingOnboardingState, setPendingOnboardingState] = useState(null); + // will remain null while the onboarding state is still being determined + const [onboardingState, setOnboardingState] = useState(null); const [permissionsPopupVis, setPermissionsPopupVis] = useState(false); const appConfig = useAppConfig(); const { colors } = useTheme(); @@ -41,7 +42,7 @@ const App = () => { control: ProfileSettings, }); - const refreshOnboardingState = () => getPendingOnboardingState().then(setPendingOnboardingState); + const refreshOnboardingState = () => getPendingOnboardingState().then(setOnboardingState); useEffect(() => { refreshOnboardingState() }, []); useEffect(() => { @@ -53,32 +54,43 @@ const App = () => { const appContextValue = { appConfig, - pendingOnboardingState, setPendingOnboardingState, refreshOnboardingState, + onboardingState, setOnboardingState, refreshOnboardingState, permissionsPopupVis, setPermissionsPopupVis, } - console.debug('pendingOnboardingState in App', pendingOnboardingState); + console.debug('onboardingState in App', onboardingState); + + let appContent; + if (onboardingState == null) { + // if onboarding state is not yet determined, show a loading spinner + appContent = + } else if (onboardingState?.route == OnboardingRoute.DONE) { + // if onboarding route is DONE, show the main app with navigation between tabs + appContent = ( + + ); + } else { + // if there is an onboarding route that is not DONE, show the onboarding stack + appContent = + } return (<> - {pendingOnboardingState == null ? - - : - - } - { /* if onboarding is done (state == null), or if is in progress but we are past the - consent page (route > CONSENT), the permissions popup can show if needed */ } - {(pendingOnboardingState == null || pendingOnboardingState.route > OnboardingRoute.CONSENT) && + {appContent} + + { /* If we are past the consent page (route > CONSENT), the permissions popup can show if needed. + This also includes if onboarding is DONE altogether (because "DONE" is > "CONSENT") */ } + {(onboardingState && onboardingState.route > OnboardingRoute.CONSENT) && } diff --git a/www/js/onboarding/OnboardingStack.tsx b/www/js/onboarding/OnboardingStack.tsx index 643744ed3..a49bde3ab 100644 --- a/www/js/onboarding/OnboardingStack.tsx +++ b/www/js/onboarding/OnboardingStack.tsx @@ -11,22 +11,22 @@ import { displayErrorMsg } from "../plugin/logger"; const OnboardingStack = () => { - const { pendingOnboardingState } = useContext(AppContext); + const { onboardingState } = useContext(AppContext); - console.debug('pendingOnboardingState in OnboardingStack', pendingOnboardingState); + console.debug('onboardingState in OnboardingStack', onboardingState); - if (pendingOnboardingState.route == OnboardingRoute.WELCOME) { + if (onboardingState.route == OnboardingRoute.WELCOME) { return ; - } else if (pendingOnboardingState.route == OnboardingRoute.SUMMARY) { + } else if (onboardingState.route == OnboardingRoute.SUMMARY) { return ; - } else if (pendingOnboardingState.route == OnboardingRoute.CONSENT) { + } else if (onboardingState.route == OnboardingRoute.CONSENT) { return ; - } else if (pendingOnboardingState.route == OnboardingRoute.SAVE_QR) { + } else if (onboardingState.route == OnboardingRoute.SAVE_QR) { return ; - } else if (pendingOnboardingState.route == OnboardingRoute.SURVEY) { + } else if (onboardingState.route == OnboardingRoute.SURVEY) { return ; } else { - displayErrorMsg('OnboardingStack: unknown route', pendingOnboardingState.route); + displayErrorMsg('OnboardingStack: unknown route', onboardingState.route); } } diff --git a/www/js/onboarding/SaveQrPage.tsx b/www/js/onboarding/SaveQrPage.tsx index 157ff4093..8a3fab92e 100644 --- a/www/js/onboarding/SaveQrPage.tsx +++ b/www/js/onboarding/SaveQrPage.tsx @@ -14,13 +14,13 @@ import { preloadDemoSurveyResponse } from "./SurveyPage"; const SaveQrPage = ({ }) => { const { t } = useTranslation(); - const { pendingOnboardingState, refreshOnboardingState } = useContext(AppContext); + const { onboardingState, refreshOnboardingState } = useContext(AppContext); const { overallStatus } = usePermissionStatus(); useEffect(() => { if (overallStatus == true && !registerUserDone) { logDebug('permissions done, going to log in'); - login(pendingOnboardingState.opcode).then((response) => { + login(onboardingState.opcode).then((response) => { logDebug('login done, refreshing onboarding state'); setRegisterUserDone(true); preloadDemoSurveyResponse(); @@ -63,13 +63,13 @@ const SaveQrPage = ({ }) => { - + - {pendingOnboardingState.opcode} + {onboardingState.opcode} - From 0e3a0e5bb9328d9790f12175342bed97f821c698 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 4 Oct 2023 07:46:42 -0600 Subject: [PATCH 050/134] more mocks for testing needed to mock window['resolvelocalfilesystem'] as well as window['cordova'].file --- www/__mocks__/cordovaMocks.ts | 5 +++++ www/__mocks__/fileSystemMocks.ts | 5 +++++ www/__tests__/uploadService.tests.ts | 9 +++++++++ 3 files changed, 19 insertions(+) create mode 100644 www/__mocks__/fileSystemMocks.ts diff --git a/www/__mocks__/cordovaMocks.ts b/www/__mocks__/cordovaMocks.ts index 44c21677c..34cb732bb 100644 --- a/www/__mocks__/cordovaMocks.ts +++ b/www/__mocks__/cordovaMocks.ts @@ -22,6 +22,11 @@ export const mockGetAppVersion = () => { window['cordova'].getAppVersion = mockGetAppVersion; } +export const mockFile = () => { + window['cordova'].file = { "dataDirectory" : "../path/to/data/directory", + "applicationStorageDirectory" : "../path/to/app/storage/directory"}; +} + export const mockBEMUserCache = () => { const _cache = {}; const messages = []; diff --git a/www/__mocks__/fileSystemMocks.ts b/www/__mocks__/fileSystemMocks.ts new file mode 100644 index 000000000..99682cba9 --- /dev/null +++ b/www/__mocks__/fileSystemMocks.ts @@ -0,0 +1,5 @@ +export const mockFileSystem = () => { + window['resolveLocalFileSystemURL'] = function (parentDir, handleFS) { + return new DataView({byteLength: 100} as ArrayBuffer) + } + } \ No newline at end of file diff --git a/www/__tests__/uploadService.tests.ts b/www/__tests__/uploadService.tests.ts index 0544d22cd..b1efcea1d 100644 --- a/www/__tests__/uploadService.tests.ts +++ b/www/__tests__/uploadService.tests.ts @@ -1,5 +1,14 @@ import {uploadFile} from "../js/control/uploadService"; import { mockLogger } from '../__mocks__/globalMocks'; +import { mockDevice, mockGetAppVersion, mockCordova, mockFile } from "../__mocks__/cordovaMocks"; +import { mockFileSystem } from "../__mocks__/fileSystemMocks"; + +mockDevice(); +// this mocks cordova-plugin-app-version, generating a "Mock App", version "1.2.3" +mockGetAppVersion(); +mockCordova(); +mockFile(); +mockFileSystem(); mockLogger(); From cd8b3833412874d19a17e8d505b32830d024693f Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 4 Oct 2023 14:37:43 -0600 Subject: [PATCH 051/134] rename component --- www/js/control/ControlCollectionHelper.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/control/ControlCollectionHelper.tsx b/www/js/control/ControlCollectionHelper.tsx index d94f12448..29f175a06 100644 --- a/www/js/control/ControlCollectionHelper.tsx +++ b/www/js/control/ControlCollectionHelper.tsx @@ -132,7 +132,7 @@ const formatConfigForDisplay = function(config, accuracyOptions) { return retVal; } -const ControlSyncHelper = ({ editVis, setEditVis }) => { +const ControlCollectionHelper = ({ editVis, setEditVis }) => { const {colors} = useTheme(); const Logger = getAngularService("Logger"); @@ -282,4 +282,4 @@ const ControlSyncHelper = ({ editVis, setEditVis }) => { ); }; -export default ControlSyncHelper; +export default ControlCollectionHelper; From 877f8cb1c4d9c7bdde11ab1b0ac72950c101248f Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 4 Oct 2023 14:38:22 -0600 Subject: [PATCH 052/134] move closing modal to reload function trying to ensure that the updates are complete by moving anything that happens after into the same function, after the await --- www/js/control/ControlCollectionHelper.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/control/ControlCollectionHelper.tsx b/www/js/control/ControlCollectionHelper.tsx index 29f175a06..fe6821f4b 100644 --- a/www/js/control/ControlCollectionHelper.tsx +++ b/www/js/control/ControlCollectionHelper.tsx @@ -172,6 +172,7 @@ const ControlCollectionHelper = ({ editVis, setEditVis }) => { console.log("new config = ", localConfig); try{ let set = await setConfig(localConfig); + setEditVis(false); //TODO find way to not need control.update.complete event broadcast } catch(err) { Logger.displayError("Error while setting collection config", err); @@ -268,8 +269,7 @@ const ControlCollectionHelper = ({ editVis, setEditVis }) => { {geofenceComponent} - From 304ceaa84fcbfa51e7557a1a3d067ea1963aabba Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 4 Oct 2023 14:38:49 -0600 Subject: [PATCH 053/134] only refresh on close, not on open editing modal --- www/js/control/ProfileSettings.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 5f4347e21..072d4267f 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -156,7 +156,10 @@ const ProfileSettings = () => { //ensure ui table updated when editor closes useEffect(() => { - refreshCollectSettings(); + if(editCollection == false) { + console.log("closed editor, time to refresh collect"); + refreshCollectSettings(); + } }, [editCollection]) async function refreshNotificationSettings() { From 019d715e6080ec723831ecd01b19270119ab5dbe Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 4 Oct 2023 16:43:01 -0600 Subject: [PATCH 054/134] working mocks and tests! After a lot of work ironing out a mock for the file system layers, a working mock and test has been established the "reason" passed through remains present throughout the process, confirming in at least a small way that this works --- www/__mocks__/fileSystemMocks.ts | 15 +++++++++- ...Service.tests.ts => uploadService.test.ts} | 28 ++++++++++--------- www/js/control/uploadService.ts | 15 +++++----- 3 files changed, 36 insertions(+), 22 deletions(-) rename www/__tests__/{uploadService.tests.ts => uploadService.test.ts} (62%) diff --git a/www/__mocks__/fileSystemMocks.ts b/www/__mocks__/fileSystemMocks.ts index 99682cba9..fb1258692 100644 --- a/www/__mocks__/fileSystemMocks.ts +++ b/www/__mocks__/fileSystemMocks.ts @@ -1,5 +1,18 @@ export const mockFileSystem = () => { window['resolveLocalFileSystemURL'] = function (parentDir, handleFS) { - return new DataView({byteLength: 100} as ArrayBuffer) + const fs = {"filesystem": + {"root": + {"getFile": (path, options, onSuccess) => { + let fileEntry = {"file": (handleFile) => { + let file = new File(["this is a mock"], "loggerDB"); + handleFile(file); + }} + onSuccess(fileEntry); + } + } + } + } + console.log("in mock, fs is ", fs, " get File is ", fs.filesystem.root.getFile); + handleFS(fs); } } \ No newline at end of file diff --git a/www/__tests__/uploadService.tests.ts b/www/__tests__/uploadService.test.ts similarity index 62% rename from www/__tests__/uploadService.tests.ts rename to www/__tests__/uploadService.test.ts index b1efcea1d..13e343efa 100644 --- a/www/__tests__/uploadService.tests.ts +++ b/www/__tests__/uploadService.test.ts @@ -12,27 +12,29 @@ mockFileSystem(); mockLogger(); -// mock for JavaScript 'fetch' -// we emulate a 100ms delay when i) fetching data and ii) parsing it as text +//use this message to verify that the post went through +let message = ""; + global.fetch = (url: string, options: {method: string, headers: {}, body: string}) => new Promise((rs, rj) => { + if(options) { + console.log("returning with options!", options); + message = "got " + options.method + options.body; + rs('sent ' + options.method + options.body + ' to ' + url); + } else { setTimeout(() => rs({ - text: () => new Promise((rs, rj) => { - setTimeout(() => rs(new Response('sent ' + options.method + options.body + ' to ' + url)), 100); + json: () => new Promise((rs, rj) => { + setTimeout(() => rs('mock data for ' + url), 100); }) })); + } }) as any; -global.fetch = jest.fn(() => - Promise.resolve({ - json: () => Promise.resolve({ url: "http://localhost:5647/phonelogs" }), - }), -) as any - //this is never used in production right now //however, tests are still important to make sure the code works //at some point we hope to restore this functionality it('posts the logs to the configured database', async () => { const posted = await uploadFile("loggerDB", "HelloWorld"); - expect(posted).toEqual(expect.stringContaining("HelloWorld")); - expect(posted).toEqual(expect.stringContaining("POST")); -}); \ No newline at end of file + console.log(posted); + expect(message).toEqual(expect.stringContaining("HelloWorld")); + expect(message).toEqual(expect.stringContaining("POST")); +}, 10000); \ No newline at end of file diff --git a/www/js/control/uploadService.ts b/www/js/control/uploadService.ts index a09fdaed3..cbd182b3c 100644 --- a/www/js/control/uploadService.ts +++ b/www/js/control/uploadService.ts @@ -1,7 +1,5 @@ import { logDebug, logInfo, logError, displayError, displayErrorMsg } from "../plugin/logger"; -import i18next from "i18next"; -// const { t } = useTranslation(); /** * @returns A promise that resolves with an upload URL or rejects with an error */ @@ -42,6 +40,7 @@ function onUploadError(err) { function readDBFile(parentDir, database, callbackFn) { return new Promise(function(resolve, reject) { window['resolveLocalFileSystemURL'](parentDir, function(fs) { + console.log("resolving file system as ", fs); fs.filesystem.root.getFile(fs.fullPath+database, null, (fileEntry) => { console.log(fileEntry); fileEntry.file(function(file) { @@ -108,15 +107,15 @@ export async function uploadFile(database, reason) { } uploadConfig.forEach(async (url) => { //have alert for starting upload, but not progress - window.alert(i18next.t("upload-service.upload-database", {db: database})); + // window.alert(i18next.t("upload-service.upload-database", {db: database})); try { - //const binArray = {byteLength: binString.byteLength, byteOffset: binString.byteOffset} let response = await sendToServer(url, binString, params); - console.log(response); - window.alert(i18next.t("upload-service.upload-details", - {filesizemb: binString['byteLength'] / (1000 * 1000), serverURL: url}) - + i18next.t("upload-service.upload-success")); + console.log("after post got", response); + return response; + // window.alert(i18next.t("upload-service.upload-details", + // {filesizemb: binString['byteLength'] / (1000 * 1000), serverURL: url}) + // + i18next.t("upload-service.upload-success")); } catch (error) { onUploadError(error); } From d3c08298ce3c5c4f2c1d9d8b107bcd04c34f9127 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 4 Oct 2023 17:14:58 -0600 Subject: [PATCH 055/134] fix bad merge conflifct the dev ui server wouldn't run because there were two imports from react-nativ-paper, probably form a bad merge resolution --- www/js/control/ProfileSettings.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 10cd9f80a..2552b37f0 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -1,7 +1,6 @@ import React, { useState, useEffect, useContext, useRef } from "react"; import { Modal, StyleSheet, ScrollView } from "react-native"; import { Dialog, Button, useTheme, Text, Appbar, IconButton, TextInput } from "react-native-paper"; -import { Dialog, Button, useTheme, Text, Appbar, IconButton } from "react-native-paper"; import { getAngularService } from "../angular-react-helper"; import { useTranslation } from "react-i18next"; import ExpansionSection from "./ExpandMenu"; From 26bd469119e3ae3e066f747989135a569397f1b5 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 5 Oct 2023 12:37:33 -0400 Subject: [PATCH 056/134] add backwards-compat for old versions blank config the old "Logout" functionality cleared the UI config but not other storage keys, such as those for marking consented and marking intro done. A backwards-compat check is needed: if the UI config is blank but intro done or consented are marked true, we need to clear everything. --- www/js/config/dynamicConfig.ts | 2 +- www/js/onboarding/onboardingHelper.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/www/js/config/dynamicConfig.ts b/www/js/config/dynamicConfig.ts index f88255d4c..f706302ad 100644 --- a/www/js/config/dynamicConfig.ts +++ b/www/js/config/dynamicConfig.ts @@ -231,7 +231,7 @@ export function getConfig() { if (storedConfig) return Promise.resolve(storedConfig); const KVStore = getAngularService('KVStore'); return KVStore.get(CONFIG_PHONE_UI_KVSTORE).then((config) => { - if (config) { + if (config && Object.keys(config).length) { storedConfig = config; return config; } diff --git a/www/js/onboarding/onboardingHelper.ts b/www/js/onboarding/onboardingHelper.ts index 06b744af8..cca0da1d4 100644 --- a/www/js/onboarding/onboardingHelper.ts +++ b/www/js/onboarding/onboardingHelper.ts @@ -1,6 +1,6 @@ import { DateTime } from "luxon"; import { getAngularService } from "../angular-react-helper"; -import { getConfig } from "../config/dynamicConfig"; +import { getConfig, resetDataAndRefresh } from "../config/dynamicConfig"; export const INTRO_DONE_KEY = 'intro_done'; @@ -28,6 +28,13 @@ export const setRegisterUserDone = (b) => registerUserDone = b; export function getPendingOnboardingState(): Promise { return Promise.all([getConfig(), readConsented(), readIntroDone()]).then(([config, isConsented, isIntroDone]) => { let route: OnboardingRoute; + + // backwards compat - prev. versions might have config cleared but still have intro_done set + if (!config && (isIntroDone || isConsented)) { + resetDataAndRefresh(); // if there's no config, we need to reset everything + return null; + } + if (isIntroDone) { route = OnboardingRoute.DONE; } else if (!config) { From 2990b7ba1e52b1a2eb7946d7c634f497c97b5a17 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 5 Oct 2023 12:45:30 -0400 Subject: [PATCH 057/134] use babel-jest instead of ts-jest Some of the core React and React Native code is written in Flow, and the official, public packages have untranspiled Flow code. Bad! Jest can't understand this when it tries to test our React code. So unfortunately we have to transpile it ourselves before running tests, using Babel with a couple different presets and plugins - add babel.config.js - change Jest config from json -> js - add packages to support the Babel transpilation --- babel.config.js | 4 ++++ jest.config.js | 19 +++++++++++++++++++ jest.config.json | 20 -------------------- package.serve.json | 5 ++++- 4 files changed, 27 insertions(+), 21 deletions(-) create mode 100644 babel.config.js create mode 100644 jest.config.js delete mode 100644 jest.config.json diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 000000000..b3c97fe4c --- /dev/null +++ b/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ['@babel/preset-env', '@babel/preset-typescript', '@babel/preset-react'], + plugins: ['@babel/plugin-transform-flow-strip-types'], +} diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 000000000..21184bdd7 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,19 @@ +module.exports = { + testEnvironment: 'jsdom', + testPathIgnorePatterns: [ + "/node_modules/", + "/platforms/", + "/plugins/", + "/lib/", + "/manual_lib/" + ], + preset: 'react-native', + transform: { + "^.+\\.(ts|tsx|js|jsx)$": "babel-jest" + }, + transformIgnorePatterns: [ + "node_modules/(?!((jest-)?react-native(-.*)?|@react-native(-community)?)/)" + ], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + moduleDirectories: ["node_modules", "src"], +}; diff --git a/jest.config.json b/jest.config.json deleted file mode 100644 index 21e5f6a05..000000000 --- a/jest.config.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "testPathIgnorePatterns": [ - "/node_modules/", - "/platforms/", - "/plugins/", - "/lib/", - "/manual_lib/" - ], - "transform": { - "^.+\\.(ts|tsx|js|jsx)$": "ts-jest" - }, - "moduleNameMapper": { - "^react-native$": "react-native-web" - }, - "preset": "react-native", - "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"], - "transformIgnorePatterns": [ - "node_modules/(?!(jest-)?@?react-native|@react-native-community|@react-navigation)" - ] -} diff --git a/package.serve.json b/package.serve.json index a4e3194f3..d74afedef 100644 --- a/package.serve.json +++ b/package.serve.json @@ -18,13 +18,16 @@ "@babel/core": "^7.21.3", "@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-proposal-object-rest-spread": "^7.20.7", + "@babel/plugin-transform-flow-strip-types": "^7.22.5", "@babel/preset-env": "^7.21.4", "@babel/preset-flow": "^7.21.4", "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.21.4", "@ionic/cli": "6.20.8", + "@testing-library/react-native": "^12.3.0", "@types/luxon": "^3.3.0", "@types/react": "^18.2.20", + "babel-jest": "^29.7.0", "babel-loader": "^9.1.2", "babel-plugin-angularjs-annotate": "^0.10.0", "babel-plugin-optional-require": "^0.3.1", @@ -40,8 +43,8 @@ "sass": "^1.62.1", "sass-loader": "^13.3.1", "style-loader": "^3.3.3", - "ts-jest": "^29.1.1", "ts-loader": "^9.4.2", + "ts-node": "^10.9.1", "typescript": "^5.0.3", "url-loader": "^4.1.1", "webpack": "^5.0.1", From e3ebe8519be4209c5e277f3ec00f62b18f5c6894 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 5 Oct 2023 10:47:58 -0600 Subject: [PATCH 058/134] restored alerts, added mock, added comments wrapping up the basic implementation and testing of this upload service, adding comments for clarity --- www/__tests__/uploadService.test.ts | 24 +++++++++++++++--------- www/js/control/uploadService.ts | 10 +++++----- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/www/__tests__/uploadService.test.ts b/www/__tests__/uploadService.test.ts index 13e343efa..c8c0e6ebc 100644 --- a/www/__tests__/uploadService.test.ts +++ b/www/__tests__/uploadService.test.ts @@ -1,23 +1,25 @@ +//this is never used in production right now +//however, tests are still important to make sure the code works +//at some point we hope to restore this functionality + import {uploadFile} from "../js/control/uploadService"; import { mockLogger } from '../__mocks__/globalMocks'; import { mockDevice, mockGetAppVersion, mockCordova, mockFile } from "../__mocks__/cordovaMocks"; import { mockFileSystem } from "../__mocks__/fileSystemMocks"; mockDevice(); -// this mocks cordova-plugin-app-version, generating a "Mock App", version "1.2.3" mockGetAppVersion(); mockCordova(); -mockFile(); -mockFileSystem(); mockLogger(); +mockFile(); //mocks the base directory +mockFileSystem(); //comnplex mock, allows the readDBFile to work in testing //use this message to verify that the post went through let message = ""; global.fetch = (url: string, options: {method: string, headers: {}, body: string}) => new Promise((rs, rj) => { if(options) { - console.log("returning with options!", options); message = "got " + options.method + options.body; rs('sent ' + options.method + options.body + ' to ' + url); } else { @@ -29,12 +31,16 @@ global.fetch = (url: string, options: {method: string, headers: {}, body: string } }) as any; -//this is never used in production right now -//however, tests are still important to make sure the code works -//at some point we hope to restore this functionality +window.alert = (message) => { + console.log(message); +} + +//very basic tests - difficult to do too much since there's a lot of mocking involved it('posts the logs to the configured database', async () => { - const posted = await uploadFile("loggerDB", "HelloWorld"); - console.log(posted); + let posted = await uploadFile("loggerDB", "HelloWorld"); expect(message).toEqual(expect.stringContaining("HelloWorld")); expect(message).toEqual(expect.stringContaining("POST")); + posted = await uploadFile("loggerDB", "second test"); + expect(message).toEqual(expect.stringContaining("second test")); + expect(message).toEqual(expect.stringContaining("POST")); }, 10000); \ No newline at end of file diff --git a/www/js/control/uploadService.ts b/www/js/control/uploadService.ts index cbd182b3c..7eee1217a 100644 --- a/www/js/control/uploadService.ts +++ b/www/js/control/uploadService.ts @@ -1,4 +1,5 @@ import { logDebug, logInfo, logError, displayError, displayErrorMsg } from "../plugin/logger"; +import i18next from "i18next"; /** * @returns A promise that resolves with an upload URL or rejects with an error @@ -107,15 +108,14 @@ export async function uploadFile(database, reason) { } uploadConfig.forEach(async (url) => { //have alert for starting upload, but not progress - // window.alert(i18next.t("upload-service.upload-database", {db: database})); + window.alert(i18next.t("upload-service.upload-database", {db: database})); try { let response = await sendToServer(url, binString, params); - console.log("after post got", response); + window.alert(i18next.t("upload-service.upload-details", + {filesizemb: binString['byteLength'] / (1000 * 1000), serverURL: url}) + + i18next.t("upload-service.upload-success")); return response; - // window.alert(i18next.t("upload-service.upload-details", - // {filesizemb: binString['byteLength'] / (1000 * 1000), serverURL: url}) - // + i18next.t("upload-service.upload-success")); } catch (error) { onUploadError(error); } From e0dd682f8afc78ec14aa5276d7419abb5edcedf9 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 5 Oct 2023 11:13:17 -0600 Subject: [PATCH 059/134] remove old angular upload service --- www/index.js | 1 - www/js/control/nguploadService.js | 171 ------------------------------ www/js/main.js | 3 +- 3 files changed, 1 insertion(+), 174 deletions(-) delete mode 100644 www/js/control/nguploadService.js diff --git a/www/index.js b/www/index.js index 17974900c..68c89ea9c 100644 --- a/www/index.js +++ b/www/index.js @@ -26,7 +26,6 @@ import './js/survey/enketo/infinite_scroll_filters.js'; import './js/survey/enketo/enketo-trip-button.js'; import './js/survey/enketo/enketo-add-note-button.js'; import './js/control/emailService.js'; -import './js/control/nguploadService.js'; import './js/metrics-factory.js'; import './js/metrics-mappings.js'; import './js/plugin/logger.ts'; diff --git a/www/js/control/nguploadService.js b/www/js/control/nguploadService.js deleted file mode 100644 index 6f95503c1..000000000 --- a/www/js/control/nguploadService.js +++ /dev/null @@ -1,171 +0,0 @@ -'use strict'; - -import angular from 'angular'; - -angular.module('emission.services.upload', ['emission.plugin.logger']) - - .service('UploadHelper', function ($window, $http, $rootScope, $ionicPopup, Logger) { - const getUploadConfig = function () { - return new Promise(function (resolve, reject) { - Logger.log(Logger.LEVEL_INFO, "About to get email config"); - var url = []; - $http.get("json/uploadConfig.json").then(function (uploadConfig) { - Logger.log(Logger.LEVEL_DEBUG, "uploadConfigString = " + JSON.stringify(uploadConfig.data)); - url.push(uploadConfig.data.url) - resolve(url); - }).catch(function (err) { - $http.get("json/uploadConfig.json.sample").then(function (uploadConfig) { - Logger.log(Logger.LEVEL_DEBUG, "default uploadConfigString = " + JSON.stringify(uploadConfig.data)); - url.push(uploadConfig.data.url) - resolve(url); - }).catch(function (err) { - Logger.log(Logger.LEVEL_ERROR, "Error while reading default upload config" + err); - reject(err); - }); - }); - }); - } - - const onReadError = function(err) { - Logger.displayError("Error while reading log", err); - } - - const onUploadError = function(err) { - Logger.displayError("Error while uploading log", err); - } - - const readDBFile = function(parentDir, database, callbackFn) { - return new Promise(function(resolve, reject) { - window.resolveLocalFileSystemURL(parentDir, function(fs) { - fs.filesystem.root.getFile(fs.fullPath+database, null, (fileEntry) => { - console.log(fileEntry); - fileEntry.file(function(file) { - console.log(file); - var reader = new FileReader(); - - reader.onprogress = function(report) { - console.log("Current progress is "+JSON.stringify(report)); - if (callbackFn != undefined) { - callbackFn(report.loaded * 100 / report.total); - } - } - - reader.onerror = function(error) { - console.log(this.error); - reject({"error": {"message": this.error}}); - } - - reader.onload = function() { - console.log("Successful file read with " + this.result.byteLength +" characters"); - resolve(new DataView(this.result)); - } - - reader.readAsArrayBuffer(file); - }, reject); - }, reject); - }); - }); - } - - const sendToServer = function upload(url, binArray, params) { - var config = { - headers: {'Content-Type': undefined }, - transformRequest: angular.identity, - params: params - }; - return $http.post(url, binArray, config); - } - - this.uploadFile = function (database) { - getUploadConfig().then((uploadConfig) => { - var parentDir = "unknown"; - - if (ionic.Platform.isAndroid()) { - parentDir = cordova.file.applicationStorageDirectory+"/databases"; - } - if (ionic.Platform.isIOS()) { - parentDir = cordova.file.dataDirectory + "../LocalDatabase"; - } - - if (parentDir === "unknown") { - alert("parentDir unexpectedly = " + parentDir + "!") - } - - const newScope = $rootScope.$new(); - newScope.data = {}; - newScope.fromDirText = i18next.t('upload-service.upload-from-dir', {parentDir: parentDir}); - newScope.toServerText = i18next.t('upload-service.upload-to-server', {serverURL: uploadConfig}); - - var didCancel = true; - - const detailsPopup = $ionicPopup.show({ - title: i18next.t("upload-service.upload-database", { db: database }), - template: newScope.toServerText - + '', - scope: newScope, - buttons: [ - { - text: 'Cancel', - onTap: function(e) { - didCancel = true; - detailsPopup.close(); - } - }, - { - text: 'Upload', - type: 'button-positive', - onTap: function(e) { - if (!newScope.data.reason) { - //don't allow the user to close unless he enters wifi password - didCancel = false; - e.preventDefault(); - } else { - didCancel = false; - return newScope.data.reason; - } - } - } - ] - }); - - Logger.log(Logger.LEVEL_INFO, "Going to upload " + database); - const readFileAndInfo = [readDBFile(parentDir, database), detailsPopup]; - Promise.all(readFileAndInfo).then(([binString, reason]) => { - if(!didCancel) - { - console.log("Uploading file of size "+binString.byteLength); - const progressScope = $rootScope.$new(); - const params = { - reason: reason, - tz: Intl.DateTimeFormat().resolvedOptions().timeZone - } - uploadConfig.forEach((url) => { - const progressPopup = $ionicPopup.show({ - title: i18next.t("upload-service.upload-database", - {db: database}), - template: i18next.t("upload-service.upload-progress", - {filesizemb: binString.byteLength / (1000 * 1000), - serverURL: uploadConfig}) - + '
', - scope: progressScope, - buttons: [ - { text: 'Cancel', type: 'button-cancel', }, - ] - }); - sendToServer(url, binString, params).then((response) => { - console.log(response); - progressPopup.close(); - const successPopup = $ionicPopup.alert({ - title: i18next.t("upload-service.upload-success"), - template: i18next.t("upload-service.upload-details", - {filesizemb: binString.byteLength / (1000 * 1000), - serverURL: uploadConfig}) - }); - }).catch(onUploadError); - }); - } - }).catch(onReadError); - }).catch(onReadError); - }; -}); diff --git a/www/js/main.js b/www/js/main.js index 94bb8aeaf..91437a07a 100644 --- a/www/js/main.js +++ b/www/js/main.js @@ -7,8 +7,7 @@ angular.module('emission.main', ['emission.main.diary', 'emission.splash.notifscheduler', 'emission.main.metrics.factory', 'emission.main.metrics.mappings', - 'emission.services', - 'emission.services.upload']) + 'emission.services']) .config(function($stateProvider) { $stateProvider.state('root.main', { From c159b2fa0b4be2b61785185cf5cfbbbdb4ebbe74 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Thu, 5 Oct 2023 10:43:20 -0700 Subject: [PATCH 060/134] We need jest-environment-jsdom since "jest-environment-jsdom" is no longer shipped by default --- package.serve.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.serve.json b/package.serve.json index d74afedef..ac2162e71 100644 --- a/package.serve.json +++ b/package.serve.json @@ -38,6 +38,7 @@ "expose-loader": "^4.1.0", "file-loader": "^6.2.0", "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "phonegap": "9.0.0+cordova.9.0.0", "process": "^0.11.10", "sass": "^1.62.1", From c3c15b44b2942d0563d7f1a49170d677a4f4e002 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Thu, 5 Oct 2023 15:36:11 -0600 Subject: [PATCH 061/134] Added basic testing for useImperialConfig.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useImperialConfig.ts - Added exports to the functions, so that we can test them _(can't use `export function useImperialConfig()` as it gets caught in invalid React hooks with `useState`)_ useImperialConfig.test.ts - Using mock of `useAppConfig` so that the testing doesn't terminate by following the chain of import statements within `useAppConfig` - Added basic testing of the functions **2 of the tests are failing, as they are not rounding properly** 1: ``` convertSpeed › should convert meters per second to miles per hour when imperial flag is true expect(received).toBe(expected) // Object.is equality Expected: 15 Received: 14.99999535936 ``` 2: ``` convertDistance › should convert meters to miles when imperial flag is true expect(received).toBe(expected) // Object.is equality Expected: 1 Received: 0.99999720514 ``` --- www/__tests__/useImperialConfig.test.ts | 49 +++++++++++++++++++++++++ www/js/config/useImperialConfig.ts | 4 +- 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 www/__tests__/useImperialConfig.test.ts diff --git a/www/__tests__/useImperialConfig.test.ts b/www/__tests__/useImperialConfig.test.ts new file mode 100644 index 000000000..f8a944c02 --- /dev/null +++ b/www/__tests__/useImperialConfig.test.ts @@ -0,0 +1,49 @@ +import { convertDistance, convertSpeed, formatForDisplay } from '../js/config/useImperialConfig'; + + +// This mock is required, or else the test will dive into the import chain of useAppConfig.ts and fail when it gets to the root +jest.mock('../js/useAppConfig', () => { + return jest.fn(() => ({ + appConfig: { + use_imperial: false + }, + loading: false + })); +}); + +describe('formatForDisplay', () => { + it('should round to the nearest integer when value is >= 100', () => { + expect(formatForDisplay(105)).toBe('105'); + expect(formatForDisplay(119)).toBe('119'); + }); + + it('should round to 3 significant digits when 1 <= value < 100', () => { + expect(formatForDisplay(7.02)).toBe('7.02'); + expect(formatForDisplay(11.3)).toBe('11.3'); + }); + + it('should round to 2 decimal places when value < 1', () => { + expect(formatForDisplay(0.07)).toBe('0.07'); + expect(formatForDisplay(0.75)).toBe('0.75'); + }); +}); + +describe('convertDistance', () => { + it('should convert meters to kilometers by default', () => { + expect(convertDistance(1000, false)).toBe(1); + }); + + it('should convert meters to miles when imperial flag is true', () => { + expect(convertDistance(1609.34, true)).toBe(1); // Approximately 1 mile + }); +}); + +describe('convertSpeed', () => { + it('should convert meters per second to kilometers per hour by default', () => { + expect(convertSpeed(10, false)).toBe(36); + }); + + it('should convert meters per second to miles per hour when imperial flag is true', () => { + expect(convertSpeed(6.7056, true)).toBe(15); // Approximately 15 mph + }); +}); diff --git a/www/js/config/useImperialConfig.ts b/www/js/config/useImperialConfig.ts index 7ad0d37ac..a9680048c 100644 --- a/www/js/config/useImperialConfig.ts +++ b/www/js/config/useImperialConfig.ts @@ -24,13 +24,13 @@ export const formatForDisplay = (value: number): string => { return Intl.NumberFormat(i18next.language, opts).format(value); } -const convertDistance = (distMeters: number, imperial: boolean): number => { +export const convertDistance = (distMeters: number, imperial: boolean): number => { if (imperial) return (distMeters / 1000) * KM_TO_MILES; return distMeters / 1000; } -const convertSpeed = (speedMetersPerSec: number, imperial: boolean): number => { +export const convertSpeed = (speedMetersPerSec: number, imperial: boolean): number => { if (imperial) return speedMetersPerSec * MPS_TO_KMPH * KM_TO_MILES; return speedMetersPerSec * MPS_TO_KMPH; From 76b6eb8b5ae8472a85fd16e82d7b5bf3a9da380e Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Thu, 5 Oct 2023 15:56:04 -0600 Subject: [PATCH 062/134] Fixed test fails due to floating point approximations Changed the `toBe`s for the floating point tests that fail due to rounding differences to `toBeCloseTo`s --- www/__tests__/useImperialConfig.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/__tests__/useImperialConfig.test.ts b/www/__tests__/useImperialConfig.test.ts index f8a944c02..b78fe974d 100644 --- a/www/__tests__/useImperialConfig.test.ts +++ b/www/__tests__/useImperialConfig.test.ts @@ -34,7 +34,7 @@ describe('convertDistance', () => { }); it('should convert meters to miles when imperial flag is true', () => { - expect(convertDistance(1609.34, true)).toBe(1); // Approximately 1 mile + expect(convertDistance(1609.34, true)).toBeCloseTo(1); // Approximately 1 mile }); }); @@ -44,6 +44,6 @@ describe('convertSpeed', () => { }); it('should convert meters per second to miles per hour when imperial flag is true', () => { - expect(convertSpeed(6.7056, true)).toBe(15); // Approximately 15 mph + expect(convertSpeed(6.7056, true)).toBeCloseTo(15); // Approximately 15 mph }); }); From 51533ef94365f7f1baa96c577ec70e869663cb38 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 5 Oct 2023 16:40:18 -0600 Subject: [PATCH 063/134] update fetch POST need to pass the file in its original form and not stringify it in order for the data to actually make it to the server This change is pending restoration of the reason and tz paramenters --- www/js/control/uploadService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/control/uploadService.ts b/www/js/control/uploadService.ts index 7eee1217a..e51d64068 100644 --- a/www/js/control/uploadService.ts +++ b/www/js/control/uploadService.ts @@ -79,7 +79,7 @@ const sendToServer = function upload(url, binArray, params) { return fetch(url, { method: 'POST', headers: {'Content-Type': undefined }, - body: JSON.stringify({ data: binArray, params: params }) + body: binArray } ) } From de3c4910a1107bc5115b72ccac505087a73d7aea Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 5 Oct 2023 20:11:59 -0600 Subject: [PATCH 064/134] adding log statements --- www/js/config/dynamicConfig.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/www/js/config/dynamicConfig.ts b/www/js/config/dynamicConfig.ts index f706302ad..312f02c0b 100644 --- a/www/js/config/dynamicConfig.ts +++ b/www/js/config/dynamicConfig.ts @@ -191,6 +191,7 @@ function loadNewConfig(newToken, existingVersion = null) { const storeConfigPromise = window['cordova'].plugins.BEMUserCache.putRWDocument( CONFIG_PHONE_UI, toSaveConfig); const storeInKVStorePromise = KVStore.set(CONFIG_PHONE_UI_KVSTORE, toSaveConfig); + logDebug("UI_CONFIG: about to store " + JSON.stringify(toSaveConfig)); // loaded new config, so it is both ready and changed return Promise.all([storeConfigPromise, storeInKVStorePromise]).then( ([result, kvStoreResult]) => { @@ -232,12 +233,14 @@ export function getConfig() { const KVStore = getAngularService('KVStore'); return KVStore.get(CONFIG_PHONE_UI_KVSTORE).then((config) => { if (config && Object.keys(config).length) { + logDebug("Got config from KVStore: " + JSON.stringify(config)); storedConfig = config; return config; } logDebug("No config found in KVStore, fetching from native storage"); return window['cordova'].plugins.BEMUserCache.getDocument(CONFIG_PHONE_UI, false).then((config) => { if (config && Object.keys(config).length) { + logDebug("Got config from native storage: " + JSON.stringify(config)); storedConfig = config; return config; } From 459f63b435a50bb34c9ca06e96d7abeb32acb2b2 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 5 Oct 2023 21:06:47 -0600 Subject: [PATCH 065/134] log statements around storing the auth token --- www/js/onboarding/SaveQrPage.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/www/js/onboarding/SaveQrPage.tsx b/www/js/onboarding/SaveQrPage.tsx index 8a3fab92e..67ddd74e2 100644 --- a/www/js/onboarding/SaveQrPage.tsx +++ b/www/js/onboarding/SaveQrPage.tsx @@ -36,8 +36,10 @@ const SaveQrPage = ({ }) => { const KVStore = getAngularService('KVStore'); const EXPECTED_METHOD = "prompted-auth"; const dbStorageObject = {"token": token}; + logDebug("about to login with token"); return KVStore.set(EXPECTED_METHOD, dbStorageObject).then((r) => { CommHelper.registerUser((successResult) => { + logDebug("registered user in CommHelper result " + successResult); refreshOnboardingState(); }, function(errorResult) { displayError(errorResult, "User registration error"); From 6a9158e61a993a7b22ef5035dff8f76e6079c7cc Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 5 Oct 2023 21:07:18 -0600 Subject: [PATCH 066/134] log statement for storing key/value in local storage --- www/js/plugin/storage.js | 1 + 1 file changed, 1 insertion(+) diff --git a/www/js/plugin/storage.js b/www/js/plugin/storage.js index a14b1db83..e4d23042e 100644 --- a/www/js/plugin/storage.js +++ b/www/js/plugin/storage.js @@ -35,6 +35,7 @@ angular.module('emission.plugin.kvstore', ['emission.plugin.logger', kvstoreJs.set = function(key, value) { // add checks for data type var store_val = mungeValue(key, value); + logger.log("adding key " + key + " and value " + value + " to local storage"); /* * How should we deal with consistency here? Have the threads be * independent so that there is greater chance that one will succeed, From 98d7324c84cf9488d04d29882c366ff61256d17e Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 5 Oct 2023 21:15:19 -0600 Subject: [PATCH 067/134] log statement in refreshing onboarding state in the logs for the bug runs: https://github.com/e-mission/e-mission-docs/issues/1002 I'm not seeing log statements indicating that the user is logged in (which I should see) and so I'm wondering how the onboarding state is moving on in that case. Adding this log statement to investigate. --- www/js/onboarding/onboardingHelper.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/www/js/onboarding/onboardingHelper.ts b/www/js/onboarding/onboardingHelper.ts index cca0da1d4..cfbebb40b 100644 --- a/www/js/onboarding/onboardingHelper.ts +++ b/www/js/onboarding/onboardingHelper.ts @@ -1,6 +1,7 @@ import { DateTime } from "luxon"; import { getAngularService } from "../angular-react-helper"; import { getConfig, resetDataAndRefresh } from "../config/dynamicConfig"; +import { logDebug } from "../plugin/logger"; export const INTRO_DONE_KEY = 'intro_done'; @@ -48,6 +49,9 @@ export function getPendingOnboardingState(): Promise { } else { route = OnboardingRoute.SURVEY; } + + logDebug("pending onboarding state is " + route + " intro, config, consent, qr saved : " + isIntroDone + config + isConsented + saveQrDone); + return { route, opcode: config?.joined?.opcode }; }); }; From 6c92e80a92b92548954fbb04cfef0fc92d23a56c Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Fri, 6 Oct 2023 12:52:00 -0400 Subject: [PATCH 068/134] make permissionStatus app-wide Now that we have an App component, we can think about hoisting state higher in the component tree so it can be shared instead of being redundant. Rather than hooking into usePermissionStatus from multiple different components, it will be cleaner to hook into it once in the App component and share it downstream via the AppContext. When there were multiple instances of usePermissionStatus, we ran into issues where only one was recieving updates (https://github.com/e-mission/e-mission-docs/issues/1002) Hoisting the permission state to the App component resolves these issues because now there is only one instance of usePermissionStatus --- www/js/App.tsx | 3 +++ www/js/appstatus/PermissionsControls.tsx | 10 ++++++---- www/js/control/AppStatusModal.tsx | 7 ++++--- www/js/onboarding/SaveQrPage.tsx | 5 ++--- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/www/js/App.tsx b/www/js/App.tsx index 628baf21b..2187118fa 100644 --- a/www/js/App.tsx +++ b/www/js/App.tsx @@ -10,6 +10,7 @@ import OnboardingStack from './onboarding/OnboardingStack'; import { OnboardingRoute, OnboardingState, getPendingOnboardingState } from './onboarding/onboardingHelper'; import { setServerConnSettings } from './config/serverConn'; import AppStatusModal from './control/AppStatusModal'; +import usePermissionStatus from './usePermissionStatus'; const defaultRoutes = (t) => [ { key: 'label', title: t('diary.label-tab'), focusedIcon: 'check-bold', unfocusedIcon: 'check-outline' }, @@ -26,6 +27,7 @@ const App = () => { const [onboardingState, setOnboardingState] = useState(null); const [permissionsPopupVis, setPermissionsPopupVis] = useState(false); const appConfig = useAppConfig(); + const permissionStatus = usePermissionStatus(); const { colors } = useTheme(); const { t } = useTranslation(); @@ -55,6 +57,7 @@ const App = () => { const appContextValue = { appConfig, onboardingState, setOnboardingState, refreshOnboardingState, + permissionStatus, permissionsPopupVis, setPermissionsPopupVis, } diff --git a/www/js/appstatus/PermissionsControls.tsx b/www/js/appstatus/PermissionsControls.tsx index ded51b898..97ce7081a 100644 --- a/www/js/appstatus/PermissionsControls.tsx +++ b/www/js/appstatus/PermissionsControls.tsx @@ -1,17 +1,19 @@ //component to view and manage permission settings -import React, { useState } from "react"; +import React, { useContext, useState } from "react"; import { StyleSheet, ScrollView, View } from "react-native"; import { Button, Text } from 'react-native-paper'; import { useTranslation } from "react-i18next"; import PermissionItem from "./PermissionItem"; -import usePermissionStatus, { refreshAllChecks } from "../usePermissionStatus"; +import { refreshAllChecks } from "../usePermissionStatus"; import ExplainPermissions from "./ExplainPermissions"; import AlertBar from "../control/AlertBar"; +import { AppContext } from "../App"; const PermissionsControls = ({ onAccept }) => { const { t } = useTranslation(); const [explainVis, setExplainVis] = useState(false); - const { checkList, overallStatus, error, errorVis, setErrorVis, explanationList } = usePermissionStatus(); + const { permissionStatus } = useContext(AppContext); + const { checkList, overallStatus, error, errorVis, setErrorVis, explanationList } = permissionStatus; return ( <> @@ -62,4 +64,4 @@ const styles = StyleSheet.create({ } }); -export default PermissionsControls; \ No newline at end of file +export default PermissionsControls; diff --git a/www/js/control/AppStatusModal.tsx b/www/js/control/AppStatusModal.tsx index c3f625f4e..e7f5aa97b 100644 --- a/www/js/control/AppStatusModal.tsx +++ b/www/js/control/AppStatusModal.tsx @@ -1,14 +1,15 @@ -import React, { useEffect } from "react"; +import React, { useContext, useEffect } from "react"; import { Modal, useWindowDimensions } from "react-native"; import { Dialog, useTheme } from 'react-native-paper'; import PermissionsControls from "../appstatus/PermissionsControls"; -import usePermissionStatus from "../usePermissionStatus"; import { settingStyles } from "./ProfileSettings"; +import { AppContext } from "../App"; //TODO -- import settings styles for dialog const AppStatusModal = ({ permitVis, setPermitVis }) => { const { height: windowHeight } = useWindowDimensions(); - const { overallStatus, checkList } = usePermissionStatus(); + const { permissionStatus } = useContext(AppContext); + const { overallStatus, checkList } = permissionStatus; const { colors } = useTheme(); /* Listen for permissions status changes to determine if we should show the modal. */ diff --git a/www/js/onboarding/SaveQrPage.tsx b/www/js/onboarding/SaveQrPage.tsx index 67ddd74e2..28af8fa77 100644 --- a/www/js/onboarding/SaveQrPage.tsx +++ b/www/js/onboarding/SaveQrPage.tsx @@ -3,7 +3,6 @@ import { View, StyleSheet } from "react-native"; import { ActivityIndicator, Button, Surface, Text } from "react-native-paper"; import { registerUserDone, setRegisterUserDone, setSaveQrDone } from "./onboardingHelper"; import { AppContext } from "../App"; -import usePermissionStatus from "../usePermissionStatus"; import { getAngularService } from "../angular-react-helper"; import { displayError, logDebug } from "../plugin/logger"; import { useTranslation } from "react-i18next"; @@ -14,8 +13,8 @@ import { preloadDemoSurveyResponse } from "./SurveyPage"; const SaveQrPage = ({ }) => { const { t } = useTranslation(); - const { onboardingState, refreshOnboardingState } = useContext(AppContext); - const { overallStatus } = usePermissionStatus(); + const { permissionStatus, onboardingState, refreshOnboardingState } = useContext(AppContext); + const { overallStatus } = permissionStatus; useEffect(() => { if (overallStatus == true && !registerUserDone) { From b8865a0f464be9a7726ee752fa6ec90079160958 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Fri, 6 Oct 2023 15:17:51 -0400 Subject: [PATCH 069/134] handle registration failure, add error boundaries If the token that the user enters belongs to a valid program, but is not a valid user token for that program, registration will fail. We should catch this and not allow the user to go any further through onboarding - rather, we should give them a message explaining what happened and send them back to the "Welcome" page so they can try again with a valid OPCode. Unfortunately they have to go through onboarding again - they will not have to re-do permissions though. The best UX would be achieved if we could ensure that the token is valid BEFORE they go through everything else. However, this would require contacting the server before the user consented, which we cannot do. --In addition, we'll add an error message if the user has not yet registered, but has advanced to preloading survey responses. This will not happen unless something is broken. --- www/i18n/en.json | 2 ++ www/js/onboarding/SaveQrPage.tsx | 7 ++++++- www/js/onboarding/SurveyPage.tsx | 8 +++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index 3299a2207..fc056e37e 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -362,6 +362,8 @@ "survey-missing-formpath": "Error while fetching resources in config: survey_info.surveys has a survey without a formPath" }, "errors": { + "registration-check-token": "User registration error. Please check your token and try again.", + "unable-preload-not-registered": "Unable to preload survey before registration", "while-populating-composite": "Error while populating composite trips", "while-loading-another-week": "Error while loading travel of {{when}} week", "while-loading-specific-week": "Error while loading travel for the week of {{day}}", diff --git a/www/js/onboarding/SaveQrPage.tsx b/www/js/onboarding/SaveQrPage.tsx index 28af8fa77..51f884886 100644 --- a/www/js/onboarding/SaveQrPage.tsx +++ b/www/js/onboarding/SaveQrPage.tsx @@ -9,6 +9,8 @@ import { useTranslation } from "react-i18next"; import QrCode, { shareQR } from "../components/QrCode"; import { onboardingStyles } from "./OnboardingStack"; import { preloadDemoSurveyResponse } from "./SurveyPage"; +import { resetDataAndRefresh } from "../config/dynamicConfig"; +import i18next from "i18next"; const SaveQrPage = ({ }) => { @@ -41,7 +43,10 @@ const SaveQrPage = ({ }) => { logDebug("registered user in CommHelper result " + successResult); refreshOnboardingState(); }, function(errorResult) { - displayError(errorResult, "User registration error"); + /* if registration fails, we should take the user back to the welcome page + so they can try again with a valid token */ + displayError(errorResult, i18next.t('errors.registration-check-token')); + resetDataAndRefresh(); }); }).catch((e) => { displayError(e, "Sign in error"); diff --git a/www/js/onboarding/SurveyPage.tsx b/www/js/onboarding/SurveyPage.tsx index 11e58c94a..41f714a6d 100644 --- a/www/js/onboarding/SurveyPage.tsx +++ b/www/js/onboarding/SurveyPage.tsx @@ -5,14 +5,20 @@ import EnketoModal from "../survey/enketo/EnketoModal"; import { DEMOGRAPHIC_SURVEY_DATAKEY, DEMOGRAPHIC_SURVEY_NAME } from "../control/DemographicsSettingRow"; import { loadPreviousResponseForSurvey } from "../survey/enketo/enketoHelper"; import { AppContext } from "../App"; -import { markIntroDone } from "./onboardingHelper"; +import { markIntroDone, registerUserDone } from "./onboardingHelper"; import { useTranslation } from "react-i18next"; import { DateTime } from "luxon"; import { onboardingStyles } from "./OnboardingStack"; +import { displayErrorMsg } from "../plugin/logger"; +import i18next from "i18next"; let preloadedResponsePromise: Promise = null; export const preloadDemoSurveyResponse = () => { if (!preloadedResponsePromise) { + if (!registerUserDone) { + displayErrorMsg(i18next.t('unable-preload-not-registered')); + return Promise.resolve(null); + } preloadedResponsePromise = loadPreviousResponseForSurvey(DEMOGRAPHIC_SURVEY_DATAKEY); } return preloadedResponsePromise; From 69ada008b37a4f0dfb38fca060b79038f8dcdb74 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 6 Oct 2023 13:20:34 -0600 Subject: [PATCH 070/134] restore url encoding for additional params --- www/js/control/uploadService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/www/js/control/uploadService.ts b/www/js/control/uploadService.ts index e51d64068..93557bdfe 100644 --- a/www/js/control/uploadService.ts +++ b/www/js/control/uploadService.ts @@ -76,7 +76,8 @@ const sendToServer = function upload(url, binArray, params) { //this was the best way I could find to contact the database, //had to modify the way it gets handled on the other side //the original way it could not find "reason" - return fetch(url, { + const urlParams = "?reason=" + params.reason + "&tz=" + params.tz; + return fetch(url+urlParams, { method: 'POST', headers: {'Content-Type': undefined }, body: binArray From 75aeb48e91ecc2c647c4e3b8d842abdf6e5e172e Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 6 Oct 2023 13:28:37 -0600 Subject: [PATCH 071/134] update mock In the previous commits, I altered the call to POST, so I needed to alter the way I mocked that call! --- www/__tests__/uploadService.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/__tests__/uploadService.test.ts b/www/__tests__/uploadService.test.ts index c8c0e6ebc..c0c7f9d04 100644 --- a/www/__tests__/uploadService.test.ts +++ b/www/__tests__/uploadService.test.ts @@ -20,7 +20,7 @@ let message = ""; global.fetch = (url: string, options: {method: string, headers: {}, body: string}) => new Promise((rs, rj) => { if(options) { - message = "got " + options.method + options.body; + message = "sent " + options.method + options.body + " for " + url;; rs('sent ' + options.method + options.body + ' to ' + url); } else { setTimeout(() => rs({ From 297a3bd3c9f1ef2fa4a6ab398e6f22c95663487a Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 6 Oct 2023 14:22:50 -0600 Subject: [PATCH 072/134] ios "why we need this" update on staging, I noticed that "why we need this" had an explanation of "how to fix" rather than what the permissions are used for. I have fixed this by allowing the default explanation to persist for ios -- custom messages for how to fix permissions are maintained in the descriptions of the permissions in `checklist` --- www/js/usePermissionStatus.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/www/js/usePermissionStatus.ts b/www/js/usePermissionStatus.ts index e98aff423..7d16f00f5 100644 --- a/www/js/usePermissionStatus.ts +++ b/www/js/usePermissionStatus.ts @@ -299,11 +299,6 @@ const usePermissionStatus = () => { let locExplanation = t('intro.appstatus.overall-loc-description'); if(window['device'].platform.toLowerCase() == "ios") { overallFitnessName = t('intro.appstatus.overall-fitness-name-ios'); - if(window['device'].version.split(".")[0] < 13) { - locExplanation = (t("intro.permissions.locationPermExplanation-ios-lt-13")); - } else { - locExplanation = (t("intro.permissions.locationPermExplanation-ios-gte-13")); - } } tempExplanations.push({name: t('intro.appstatus.overall-loc-name'), desc: locExplanation}); tempExplanations.push({name: overallFitnessName, desc: t('intro.appstatus.overall-fitness-description')}); From 86bed083dd76224b99675d79cc2a63c088bd55bb Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 6 Oct 2023 14:38:57 -0600 Subject: [PATCH 073/134] add scrollview for smaller form factors addressing comment from: Cleanup notes for new onboarding --- www/js/onboarding/WelcomePage.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/www/js/onboarding/WelcomePage.tsx b/www/js/onboarding/WelcomePage.tsx index 8e7e43425..3589923c8 100644 --- a/www/js/onboarding/WelcomePage.tsx +++ b/www/js/onboarding/WelcomePage.tsx @@ -58,6 +58,7 @@ const WelcomePage = () => { + }} /> @@ -81,6 +82,7 @@ const WelcomePage = () => { {t('join.paste-hint')} + setPasteModalVis(false)}> setPasteModalVis(false)}> From c349ab78fc80730ced1a345fa015792d4f68599f Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 6 Oct 2023 14:53:15 -0600 Subject: [PATCH 074/134] "please allow" instead of scroll instructions As a result of https://github.com/e-mission/e-mission-data-collection/pull/209, we now have a popup for this permission, so all the user needs to do is allow it! Translate PR to follow with this small change to en.json --- www/i18n/en.json | 5 +---- www/js/usePermissionStatus.ts | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index fc056e37e..6a034780b 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -284,10 +284,7 @@ }, "ignorebatteryopt": { "name": "Ignore battery optimizations", - "description": { - "android-disable": "On the optimization page, go to all apps, search for this app and turn off optimizations.", - "ios": "Please allow." - } + "description": "Please allow." } }, "permissions": { diff --git a/www/js/usePermissionStatus.ts b/www/js/usePermissionStatus.ts index 7d16f00f5..035ba6b16 100644 --- a/www/js/usePermissionStatus.ts +++ b/www/js/usePermissionStatus.ts @@ -283,7 +283,7 @@ const usePermissionStatus = () => { } let ignoreBatteryOptCheck = { name: t("intro.appstatus.ignorebatteryopt.name"), - desc: t("intro.appstatus.ignorebatteryopt.description.android-disable"), + desc: t("intro.appstatus.ignorebatteryopt.description"), fix: fixBatteryOpt, refresh: checkBatteryOpt } From 69f327a8d803309df6ad9ef8b1a0996f9e90e1e9 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 9 Oct 2023 10:13:45 -0600 Subject: [PATCH 075/134] band-aid fix for collect settings setting a delay does prevent the collect settings changes from turning off tracking, but this is not a very principled fix, still looking for a better way to fix this --- www/js/control/ControlCollectionHelper.tsx | 1 - www/js/control/ProfileSettings.jsx | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/www/js/control/ControlCollectionHelper.tsx b/www/js/control/ControlCollectionHelper.tsx index fe6821f4b..d93c498a9 100644 --- a/www/js/control/ControlCollectionHelper.tsx +++ b/www/js/control/ControlCollectionHelper.tsx @@ -173,7 +173,6 @@ const ControlCollectionHelper = ({ editVis, setEditVis }) => { try{ let set = await setConfig(localConfig); setEditVis(false); - //TODO find way to not need control.update.complete event broadcast } catch(err) { Logger.displayError("Error while setting collection config", err); } diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 072d4267f..0b00efc75 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -157,8 +157,10 @@ const ProfileSettings = () => { //ensure ui table updated when editor closes useEffect(() => { if(editCollection == false) { - console.log("closed editor, time to refresh collect"); - refreshCollectSettings(); + setTimeout(function() { + console.log("closed editor, time to refresh collect"); + refreshCollectSettings(); + }, 1000); } }, [editCollection]) From 32596a8e36174d0f98f7e9ad211627a3f9c99c7e Mon Sep 17 00:00:00 2001 From: niccolopaganini Date: Mon, 9 Oct 2023 12:00:03 -0600 Subject: [PATCH 076/134] Updated README file: Restored to the original - 1. Removed versions 2. Added CI badge 3. Plugins installation guide updated --- README.md | 238 +++++++++++++++++++++++++----------------------------- 1 file changed, 108 insertions(+), 130 deletions(-) diff --git a/README.md b/README.md index 9ac9ceb1d..7fca214aa 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,41 @@ -# [e-mission phone app](https://github.com/e-mission/e-mission-phone/tree/master) +e-mission phone app +-------------------- -__This is the phone component of the e-mission system.__ +This is the phone component of the e-mission system. :sparkles: This has been upgraded to the latest **Android**, **iOS**, **cordova-lib**, **node** and **npm** versions. __This is ready to build out of the box.__ -✨ We constantly upgrade the repo to the latest cordova versions of android, iOS, cordova-lib, and the most recent node and npm versions. The CI will be up-to-date. - For the latest versions, refer [`package.cordovabuild.json`](package.cordovabuild.json) -## Additional Documentation +Additional Documentation +--- Additional documentation has been moved to its own repository [e-mission-docs](https://github.com/e-mission/e-mission-docs). Specific e-mission-phone wikis can be found here: https://github.com/e-mission/e-mission-docs/tree/master/docs/e-mission-phone **Issues:** Since this repository is part of a larger project, all issues are tracked [in the central docs repository](https://github.com/e-mission/e-mission-docs/issues). If you have a question, [as suggested by the open source guide](https://opensource.guide/how-to-contribute/#communicating-effectively), please file an issue instead of sending an email. Since issues are public, other contributors can try to answer the question and benefit from the answer. -:sparkles: Check [Contributing](#contributing) if you're interested in contributing for this project :sparkles: - ## Contents #### 1. [Updating the UI only](#updating-the-ui-only) -#### 2. [Updating the e-mission-* plugins or adding new plugins](#updating-the-e-mission--plugins-or-adding-new-plugins) -#### 3. [Creating logos](#creating-logos) -#### 4. [End to End Testing](#end-to-end-testing) +#### 2. [End to End Testing](#end-to-end-testing) +#### 3. [Updating the e-mission-* plugins or adding new plugins](#updating-the-e-mission--plugins-or-adding-new-plugins) +#### 4. [Creating logos](#creating-logos) #### 5. [Beta-testing debugging](#beta-testing-debugging) #### 6. [Contributing](#contributing) -#### 7. [Troubleshooting](#troubleshooting) +Updating the UI only --- - - -## Updating the UI only -[![osx-serve-install](https://github.com/e-mission/e-mission-phone/workflows/osx-serve-install/badge.svg)](https://github.com/e-mission/e-mission-phone/actions/workflows/serve-install.yml) +[![osx-serve-install](https://github.com/e-mission/e-mission-phone/workflows/osx-serve-install/badge.svg)](https://github.com/e-mission/e-mission-phone/actions?query=workflow%3Aosx-serve-install) If you want to make only UI changes, (as opposed to modifying the existing plugins, adding new plugins, etc), you can use the **new and improved** (as of June 2018) [e-mission dev app](https://github.com/e-mission/e-mission-devapp/) and install the most recent version from [releases](https://github.com/e-mission/e-mission-devapp/releases). ### Installing (one-time) -:point_right:Run the setup script +Run the setup script ``` bash setup/setup_serve.sh ``` + **(optional)** Configure by changing the files in `www/json`. Defaults are in `www/json/*.sample` @@ -50,15 +46,16 @@ cp ..... www/json/connectionConfig.json ``` ### Activation (after install, and in every new shell) + ``` source setup/activate_serve.sh ``` ### Running -Start the phonegap deployment server and note the URL(s) that the server is listening to. +1. Start the phonegap deployment server and note the URL(s) that the server is listening to. - + ``` npm run serve .... [phonegap] listening on 10.0.0.14:3000 @@ -67,11 +64,12 @@ Start the phonegap deployment server and note the URL(s) that the server is list [phonegap] ctrl-c to stop the server [phonegap] .... + ``` -Change the devapp connection URL to one of these (e.g. 192.168.162.1:3000) and press "Connect" -The app will now display the version of e-mission app that is in your local directory -The console logs will be displayed back in the server window (prefaced by `[console]`) -Breakpoints can be added by connecting through the browser +1. Change the devapp connection URL to one of these (e.g. 192.168.162.1:3000) and press "Connect" +1. The app will now display the version of e-mission app that is in your local directory + 1. The console logs will be displayed back in the server window (prefaced by `[console]`) + 1. Breakpoints can be added by connecting through the browser - Safari ([enable develop menu](https://support.apple.com/guide/safari/use-the-safari-develop-menu-sfri20948/mac)): Develop -> Simulator -> index.html - Chrome: chrome://inspect -> Remote target (emulator) @@ -79,9 +77,9 @@ Breakpoints can be added by connecting through the browser **Note1**: You may need to scroll up, past all the warnings about `Content Security Policy has been added` to find the port that the server is listening to. -## End to End Testing - -A lot of the visualizations that we display in the phone client come from the server. In order to do end-to-end testing, we need to run a local server and connect to it. Instructions for: +End to end testing +--- +A lot of the visualizations that we display in the phone client come from the server. In order to do end to end testing, we need to run a local server and connect to it. Instructions for: 1. installing a local server, 2. running it, @@ -90,51 +88,25 @@ A lot of the visualizations that we display in the phone client come from the se are available in the [e-mission-server README](https://github.com/e-mission/e-mission-server/blob/master/README.md). -In order to make end-to-end testing even easier, we have moved from phone app to a dynamic config setting. We use the dynamic config to specify the server locations. Refer [Doc](https://github.com/e-mission/nrel-openpath-deploy-configs) - -If you have the [e-mission-server](https://github.com/e-mission/e-mission-server) running at a specific URL or IP address, and you want to connect to it, you would have to specify that URL or IP in the `server` field of your dynamic config file. +In order to make end to end testing easy, if the local server is started on a HTTP (versus HTTPS port), it is in development mode. By default, the phone app connects to the local server (localhost on iOS, [10.0.2.2 on android](https://stackoverflow.com/questions/5806220/how-to-connect-to-my-http-localhost-web-server-from-android-emulator-in-eclips)) with the `prompted-auth` authentication method. To connect to a different server, or to use a different authentication method, you need to create a `www/json/connectionConfig.json` file. More details on configuring authentication [can be found in the docs](https://github.com/e-mission/e-mission-docs/blob/master/docs/install/configuring_authentication.md). One advantage of using `skip` authentication in development mode is that any user email can be entered without a password. Developers can use one of the emails that they loaded test data for in step (3) above. So if the test data loaded was with `-u shankari@eecs.berkeley.edu`, then the login email for the phone app would also be `shankari@eecs.berkeley.edu`. -## Updating the e-mission-\* plugins or adding new plugins - +Updating the e-mission-\* plugins or adding new plugins +--- [![osx-build-ios](https://github.com/e-mission/e-mission-phone/actions/workflows/ios-build.yml/badge.svg)](https://github.com/e-mission/e-mission-phone/actions/workflows/ios-build.yml) [![osx-build-android](https://github.com/e-mission/e-mission-phone/actions/workflows/android-build.yml/badge.svg)](https://github.com/e-mission/e-mission-phone/actions/workflows/android-build.yml) +[![osx-android-prereq-sdk-install](https://github.com/e-mission/e-mission-phone/actions/workflows/android-automated-sdk-install.yml/badge.svg)](https://github.com/e-mission/e-mission-phone/actions/workflows/android-automated-sdk-install.yml) Pre-requisites --- -- The version of xcode used by the CI. +- the version of xcode used by the CI - to install a particular version, use [xcode-select](https://www.unix.com/man-page/OSX/1/xcode-select/) - or this [supposedly easier to use repo](https://github.com/xcpretty/xcode-install) + - **NOTE**: the basic xcode install on Catalina was messed up for me due to a prior installation of command line tools. [These workarounds helped](https://github.com/nodejs/node-gyp/blob/master/macOS_Catalina.md). - git -- Java 17. Tested with [OpenJDK 17 (Temurin) using AdoptOpenJDK](https://adoptium.net). -- Always use [homebrew](https://brew.sh) in addition to CLI - - this allows us to install the current version of cocoapods without - running into ruby incompatibilities - e.g. - https://github.com/CocoaPods/CocoaPods/issues/11763 - -:triangular_flag_on_post: __Important__ - -Most of the recent issues encountered have been due to incompatible setup. We -have now: -- locked down the dependencies, -- created setup and teardown scripts to setup self-contained environments with - those dependencies, and -- CI enabled to validate that they continue work. - -If you have setup failures, please compare the configuration in the **passing CI -builds** with your configuration. That is almost certainly the source of the error. - -__Export statements__ -``` -export ANDROID_SDK_ROOT="/Users//Library/Android/sdk" -``` -``` -export ANDROID_HOME="/Users//Library/Android/sdk" -``` -aka the path where you want the SDK to be installed. - -- android SDK; install manually or use setup script below (**recommended**). Note that you only need to run this once **per computer**. +- Java 17. Tested with [OpenJDK 17 (Temurin) using Adoptium](https://adoptium.net). +- android SDK; install manually or use setup script below. Note that you only need to run this once **per computer**. ``` bash setup/prereq_android_sdk_install.sh ``` @@ -159,11 +131,26 @@ aka the path where you want the SDK to be installed. ``` +- if you are not on the most recent version of OSX, `homebrew` + - this allows us to install the current version of cocoapods without + running into ruby incompatibilities - e.g. + https://github.com/CocoaPods/CocoaPods/issues/11763 +Important +--- +Most of the recent issues encountered have been due to incompatible setup. We +have now: +- locked down the dependencies, +- created setup and teardown scripts to setup self-contained environments with + those dependencies, and +- CI enabled to validate that they continue work. -__Installing (one time only)__ +If you have setup failures, please compare the configuration in the passing CI +builds with your configuration. That is almost certainly the source of the error. -- Run the setup script for the platform you want to build +Installing (one time only) +--- +Run the setup script for the platform you want to build ``` bash setup/setup_android_native.sh @@ -182,18 +169,12 @@ cp www/json/startupConfig.json.sample www/json/startupConfig.json cp ..... www/json/connectionConfig.json ``` -If connecting to a development server over http, make sure to turn on http support on android - -``` - - - -``` -__Run this in every new shell for Activation__ +### Activation (after install, and in every new shell) ``` source setup/activate_native.sh ``` +
Expected Output ``` @@ -210,20 +191,30 @@ Copied config.cordovabuild.xml -> config.xml and package.cordovabuild.json -> pa ```
-
- __Pick a type of build and execute the following:__ -More "versions" are available in [`package.cordovabuild.json`](package.cordovabuild.json) +### Activation (after install, and in every new shell) + +If connecting to a development server over http, make sure to turn on http support on android + ``` -npm run + + + ``` -For instance: (build-dev-android) +### Run in the emulator + ``` -npm run build-dev-android +npm run ``` -
Your expected output should look something like this +AND/OR +``` +npm run +``` +for builds, refer [`package.cordovabuild.json`](package.cordovabuild.json) + +
Expected output ``` BUILD SUCCESSFUL in 2m 48s @@ -234,10 +225,8 @@ Built the following apk(s):
-
- -## Creating logos - +Creating logos +--- If you are building your own version of the app, you must have your own logo to avoid app store conficts. Updating the logo is very simple using the [`ionic cordova resources`](https://ionicframework.com/docs/v3/cli/cordova/resources/) @@ -245,7 +234,21 @@ command. **Note**: You may have to install the [`cordova-res` package](https://github.com/ionic-team/cordova-res) for the command to work -## Beta-testing debugging + +Troubleshooting +--- +- Make sure to use `npx ionic` and `npx cordova`. This is + because the setup script installs all the modules locally in a self-contained + environment using `npm install` and not `npm install -g` +- Check the CI to see whether there is a known issue +- Run the commands from the script one by one and see which fails + - compare the failed command with the CI logs +- Another workaround is to delete the local environment and recreate it + - javascript errors: `rm -rf node_modules && npm install` + - native code compile errors: `rm -rf plugins && rm -rf platforms && npx cordova prepare` + +Beta-testing debugging +--- If users run into problems, they have the ability to email logs to the maintainer. These logs are in the form of an sqlite3 database, so they have to be opened using `sqlite3`. Alternatively, you can export it to a csv with @@ -253,38 +256,37 @@ dates using the `bin/csv_export_add_date.py` script. ``` -mv ~/Downloads/loggerDB /tmp/logger. -pwd +$ mv ~/Downloads/loggerDB /tmp/logger. +$ pwd .../e-mission-phone -python bin/csv_export_add_date.py /tmp/loggerDB. -less /tmp/loggerDB..withdate.log +$ python bin/csv_export_add_date.py /tmp/loggerDB. +$ less /tmp/loggerDB..withdate.log ``` -## Contributing +Contributing +--- -:point_right:Add the main repo as upstream -``` -git remote add upstream https://github.com/e-mission/e-mission-phone -``` -:point_right:Create a new branch (IMPORTANT). Please do not submit pull requests from master -``` -git checkout -b -``` -:point_right:Make changes to the branch and commit them -``` -git commit -``` -:point_right:Push the changes to your local fork -``` -git push origin -``` -:point_right:Generate a pull request from the UI +Add the main repo as upstream + + git remote add upstream https://github.com/e-mission/e-mission-phone.git + +Create a new branch (IMPORTANT). Please do not submit pull requests from master + + git checkout -b mybranch -
+Make changes to the branch and commit them -__\*__Address my review comments__\*__ + git commit -Once I merge the pull request :smiley: :tada:, pull the changes to your fork and delete the branch +Push the changes to your local fork + + git push origin mybranch + +Generate a pull request from the UI + +Address my review comments + +Once I merge the pull request, pull the changes to your fork and delete the branch ``` git checkout master ``` @@ -297,27 +299,3 @@ git push origin master ``` git branch -d ``` - ---- -### Troubleshooting -:point_right:Xcode command line tools -``` -Warning: No developer tools installed. -You should install the Command Line Tools. -``` -``` -xcode-select --install -``` - -:point_right:Creating Logos -- Make sure to use `npx ionic` and `npx cordova`. This is - because the setup script installs all the modules locally in a self-contained - environment using `npm install` and not `npm install -g` -- Check the CI to see whether there is a known issue -- Run the commands from the script one by one and see which fails - - compare the failed command with the CI logs -- Another workaround is to delete the local environment and recreate it - - javascript errors: `rm -rf node_modules && npm install` - - native code compile errors: `rm -rf plugins && rm -rf platforms && npx cordova prepare` - -(For updating the e-mission-plugins or adding new plugins) **NOTE**: the basic xcode install on Mac OS Catalina was messed up for me due to a prior installation of command line tools. [These workarounds helped](https://github.com/nodejs/node-gyp/blob/master/macOS_Catalina.md). From 6ed84b3fda8c3e2f29a69ee547b37e4e8e98e663 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 9 Oct 2023 17:58:08 -0400 Subject: [PATCH 077/134] revise verbage of 'not registered' error msg https://github.com/e-mission/e-mission-phone/pull/1059#discussion_r1350797906 --- www/i18n/en.json | 2 +- www/js/onboarding/SurveyPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index 6a034780b..e47fdd62d 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -360,7 +360,7 @@ }, "errors": { "registration-check-token": "User registration error. Please check your token and try again.", - "unable-preload-not-registered": "Unable to preload survey before registration", + "not-registered-cant-contact": "User is not registered, so the server cannot be contacted.", "while-populating-composite": "Error while populating composite trips", "while-loading-another-week": "Error while loading travel of {{when}} week", "while-loading-specific-week": "Error while loading travel for the week of {{day}}", diff --git a/www/js/onboarding/SurveyPage.tsx b/www/js/onboarding/SurveyPage.tsx index 41f714a6d..c02439cbf 100644 --- a/www/js/onboarding/SurveyPage.tsx +++ b/www/js/onboarding/SurveyPage.tsx @@ -16,7 +16,7 @@ let preloadedResponsePromise: Promise = null; export const preloadDemoSurveyResponse = () => { if (!preloadedResponsePromise) { if (!registerUserDone) { - displayErrorMsg(i18next.t('unable-preload-not-registered')); + displayErrorMsg(i18next.t('errors.not-registered-cant-contact')); return Promise.resolve(null); } preloadedResponsePromise = loadPreviousResponseForSurvey(DEMOGRAPHIC_SURVEY_DATAKEY); From 3d875aa026160195a962bc1802a05843320b0f98 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Tue, 10 Oct 2023 12:00:34 -0600 Subject: [PATCH 078/134] Infinite Scroll Rewrite multilabel/infinite_scroll_filters.js - Removed this file and replaced with .ts file multilabel/infinite_scroll_filters.ts - Rewrote this file in React enketo/infinite_scroll_filters.js - Removed this file and replaced with .ts file enketo/infinite_scroll_filters.ts - Rewrote this file in React index.js - Removed Angular reference to multilabel/infinite_scroll_filters.js - Removed Angular reference to enketo/infinite_scroll_filters.js diary.js - Removed angular reference to multilabel.infscrollfilters - Removed angular reference to enketo.infscrollfilters LabelTab.tsx - Changed the tripFilterFactory to reflect the new way of representing the surveyOpt object from survey.ts survey.ts - imported the multilabel and enketo filters the "React way", and changed the typescript to reflect that the filters property are arrays containing the label filters --- www/index.js | 2 - www/js/diary.js | 2 - www/js/diary/LabelTab.tsx | 4 +- .../survey/enketo/infinite_scroll_filters.js | 40 ----------- .../survey/enketo/infinite_scroll_filters.ts | 31 +++++++++ .../multilabel/infinite_scroll_filters.js | 67 ------------------- .../multilabel/infinite_scroll_filters.ts | 58 ++++++++++++++++ www/js/survey/survey.ts | 9 ++- 8 files changed, 97 insertions(+), 116 deletions(-) delete mode 100644 www/js/survey/enketo/infinite_scroll_filters.js create mode 100644 www/js/survey/enketo/infinite_scroll_filters.ts delete mode 100644 www/js/survey/multilabel/infinite_scroll_filters.js create mode 100644 www/js/survey/multilabel/infinite_scroll_filters.ts diff --git a/www/index.js b/www/index.js index 55cb233b5..06916142d 100644 --- a/www/index.js +++ b/www/index.js @@ -18,12 +18,10 @@ import './js/services.js'; import './js/i18n-utils.js'; import './js/main.js'; import './js/survey/input-matcher.js'; -import './js/survey/multilabel/infinite_scroll_filters.js'; import './js/survey/multilabel/multi-label-ui.js'; import './js/diary.js'; import './js/diary/services.js'; import './js/survey/enketo/answer.js'; -import './js/survey/enketo/infinite_scroll_filters.js'; import './js/survey/enketo/enketo-trip-button.js'; import './js/survey/enketo/enketo-add-note-button.js'; import './js/control/emailService.js'; diff --git a/www/js/diary.js b/www/js/diary.js index 3a150cfff..5e1d191e5 100644 --- a/www/js/diary.js +++ b/www/js/diary.js @@ -3,9 +3,7 @@ import LabelTab from './diary/LabelTab'; angular.module('emission.main.diary',['emission.main.diary.services', 'emission.survey.multilabel.buttons', - 'emission.survey.multilabel.infscrollfilters', 'emission.survey.enketo.add-note-button', - 'emission.survey.enketo.trip.infscrollfilters', 'emission.plugin.logger']) .config(function($stateProvider) { diff --git a/www/js/diary/LabelTab.tsx b/www/js/diary/LabelTab.tsx index 42b173017..1914feda4 100644 --- a/www/js/diary/LabelTab.tsx +++ b/www/js/diary/LabelTab.tsx @@ -68,8 +68,8 @@ const LabelTab = () => { // https://github.com/e-mission/e-mission-docs/issues/894 if (appConfig.survey_info?.buttons == undefined) { // initalize filters - const tripFilterFactory = getAngularService(surveyOpt.filter); - const allFalseFilters = tripFilterFactory.configuredFilters.map((f, i) => ({ + const tripFilter = surveyOpt.filter; + const allFalseFilters = tripFilter.map((f, i) => ({ ...f, state: (i == 0 ? true : false) // only the first filter will have state true on init })); setFilterInputs(allFalseFilters); diff --git a/www/js/survey/enketo/infinite_scroll_filters.js b/www/js/survey/enketo/infinite_scroll_filters.js deleted file mode 100644 index 8e45db8e4..000000000 --- a/www/js/survey/enketo/infinite_scroll_filters.js +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - -/* - * The general structure of this code is that all the timeline information for - * a particular day is retrieved from the Timeline factory and put into the scope. - * For best performance, all data should be loaded into the in-memory timeline, - * and in addition to writing to storage, the data should be written to memory. - * All UI elements should only use $scope variables. - */ - -import angular from 'angular'; - -angular.module('emission.survey.enketo.trip.infscrollfilters',[ - 'emission.survey.enketo.trip.button', - 'emission.plugin.logger' - ]) -.factory('EnketoTripInfScrollFilters', function(Logger, EnketoTripButtonService){ - var sf = {}; - var unlabeledCheck = function(t) { - return !angular.isDefined(t.userInput[EnketoTripButtonService.SINGLE_KEY]); - } - - sf.UNLABELED = { - key: "unlabeled", - text: i18next.t("diary.unlabeled"), - filter: unlabeledCheck - } - - sf.TO_LABEL = { - key: "to_label", - text: i18next.t("diary.to-label"), - filter: unlabeledCheck - } - - sf.configuredFilters = [ - sf.TO_LABEL, - sf.UNLABELED, - ]; - return sf; -}); diff --git a/www/js/survey/enketo/infinite_scroll_filters.ts b/www/js/survey/enketo/infinite_scroll_filters.ts new file mode 100644 index 000000000..5ec72eae7 --- /dev/null +++ b/www/js/survey/enketo/infinite_scroll_filters.ts @@ -0,0 +1,31 @@ +/* + * The general structure of this code is that all the timeline information for + * a particular day is retrieved from the Timeline factory and put into the scope. + * For best performance, all data should be loaded into the in-memory timeline, + * and in addition to writing to storage, the data should be written to memory. + * All UI elements should only use $scope variables. + */ + +import i18next from "i18next"; +import { getAngularService } from "../../angular-react-helper"; + +const unlabeledCheck = (t) => { + return typeof t.userInput[getAngularService('EnketoTripButtonService').SINGLE_KEY] === 'undefined'; +} + +const UNLABELED = { + key: "unlabeled", + text: i18next.t("diary.unlabeled"), + filter: unlabeledCheck +} + +const TO_LABEL = { + key: "to_label", + text: i18next.t("diary.to-label"), + filter: unlabeledCheck +} + +export const configuredFilters = [ + TO_LABEL, + UNLABELED +]; \ No newline at end of file diff --git a/www/js/survey/multilabel/infinite_scroll_filters.js b/www/js/survey/multilabel/infinite_scroll_filters.js deleted file mode 100644 index bc588ecc2..000000000 --- a/www/js/survey/multilabel/infinite_scroll_filters.js +++ /dev/null @@ -1,67 +0,0 @@ -'use strict'; - -/* - * The general structure of this code is that all the timeline information for - * a particular day is retrieved from the Timeline factory and put into the scope. - * For best performance, all data should be loaded into the in-memory timeline, - * and in addition to writing to storage, the data should be written to memory. - * All UI elements should only use $scope variables. - */ - -import angular from 'angular'; - -angular.module('emission.survey.multilabel.infscrollfilters',[ - 'emission.plugin.logger' - ]) -.factory('MultiLabelInfScrollFilters', function(Logger){ - var sf = {}; - var unlabeledCheck = function(t) { - return t.INPUTS - .map((inputType, index) => !angular.isDefined(t.userInput[inputType])) - .reduce((acc, val) => acc || val, false); - } - - var invalidCheck = function(t) { - const retVal = - (angular.isDefined(t.userInput['MODE']) && t.userInput['MODE'].value === 'pilot_ebike') && - (!angular.isDefined(t.userInput['REPLACED_MODE']) || - t.userInput['REPLACED_MODE'].value === 'pilot_ebike' || - t.userInput['REPLACED_MODE'].value === 'same_mode'); - return retVal; - } - - var toLabelCheck = function(trip) { - if (angular.isDefined(trip.expectation)) { - console.log(trip.expectation.to_label) - return trip.expectation.to_label && unlabeledCheck(trip); - } else { - return true; - } - } - - sf.UNLABELED = { - key: "unlabeled", - text: i18next.t("diary.unlabeled"), - filter: unlabeledCheck, - width: "col-50" - } - - sf.INVALID_EBIKE = { - key: "invalid_ebike", - text: i18next.t("diary.invalid-ebike"), - filter: invalidCheck - } - - sf.TO_LABEL = { - key: "to_label", - text: i18next.t("diary.to-label"), - filter: toLabelCheck, - width: "col-50" - } - - sf.configuredFilters = [ - sf.TO_LABEL, - sf.UNLABELED - ]; - return sf; -}); diff --git a/www/js/survey/multilabel/infinite_scroll_filters.ts b/www/js/survey/multilabel/infinite_scroll_filters.ts new file mode 100644 index 000000000..68c28b35e --- /dev/null +++ b/www/js/survey/multilabel/infinite_scroll_filters.ts @@ -0,0 +1,58 @@ +/* + * The general structure of this code is that all the timeline information for + * a particular day is retrieved from the Timeline factory and put into the scope. + * For best performance, all data should be loaded into the in-memory timeline, + * and in addition to writing to storage, the data should be written to memory. + * All UI elements should only use $scope variables. + */ + +import i18next from "i18next"; + +const unlabeledCheck = (t) => { + return t.INPUTS + .map((inputType, index) => typeof t.userInput[inputType] === 'undefined') + .reduce((acc, val) => acc || val, false); +} + +const invalidCheck = (t) => { + const retVal = + (typeof t.userInput['MODE'] !== 'undefined' && t.userInput['MODE'].value === 'pilot_ebike') && + (typeof t.userInput['REPLACED_MODE'] === 'undefined' || + t.userInput['REPLACED_MODE'].value === 'pilot_ebike' || + t.userInput['REPLACED_MODE'].value === 'same_mode'); + return retVal; +} + +const toLabelCheck = (trip) => { + if (typeof trip.expectation !== 'undefined') { + console.log(trip.expectation.to_label) + return trip.expectation.to_label && unlabeledCheck(trip); + } else { + return true; + } +} + +const UNLABELED = { + key: "unlabeled", + text: i18next.t("diary.unlabeled"), + filter: unlabeledCheck, + width: "col-50" +} + +const INVALID_EBIKE = { + key: "invalid_ebike", + text: i18next.t("diary.invalid-ebike"), + filter: invalidCheck +} + +const TO_LABEL = { + key: "to_label", + text: i18next.t("diary.to-label"), + filter: toLabelCheck, + width: "col-50" +} + +export const configuredFilters = [ + TO_LABEL, + UNLABELED +]; \ No newline at end of file diff --git a/www/js/survey/survey.ts b/www/js/survey/survey.ts index e6692983f..66f662082 100644 --- a/www/js/survey/survey.ts +++ b/www/js/survey/survey.ts @@ -1,12 +1,15 @@ -type SurveyOption = { filter: string, service: string, elementTag: string } +import { configuredFilters as multilabelConfiguredFilters } from "./multilabel/infinite_scroll_filters"; +import { configuredFilters as enketoConfiguredFilters } from "./enketo/infinite_scroll_filters"; + +type SurveyOption = { filter: Array, service: string, elementTag: string } export const SurveyOptions: {[key: string]: SurveyOption} = { MULTILABEL: { - filter: "MultiLabelInfScrollFilters", + filter: multilabelConfiguredFilters, service: "MultiLabelService", elementTag: "multilabel" }, ENKETO: { - filter: "EnketoTripInfScrollFilters", + filter: enketoConfiguredFilters, service: "EnketoTripButtonService", elementTag: "enketo-trip-button" } From 6ffb92633b8b962106985e44b45929005143cf81 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Thu, 12 Oct 2023 10:01:38 -0600 Subject: [PATCH 079/134] Fixing enketo label filters diary.js - Added emission.survey.enketo.trip.button to diary.js dependency array so that it can be used in enketo/infinite_scroll_filters.ts enketo/infinite_scroll_filters.ts - Added a try/catch statement to catch loading errors and make the code more readable --- www/js/diary.js | 1 + www/js/survey/enketo/infinite_scroll_filters.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/www/js/diary.js b/www/js/diary.js index 5e1d191e5..c0b7bce35 100644 --- a/www/js/diary.js +++ b/www/js/diary.js @@ -4,6 +4,7 @@ import LabelTab from './diary/LabelTab'; angular.module('emission.main.diary',['emission.main.diary.services', 'emission.survey.multilabel.buttons', 'emission.survey.enketo.add-note-button', + 'emission.survey.enketo.trip.button', 'emission.plugin.logger']) .config(function($stateProvider) { diff --git a/www/js/survey/enketo/infinite_scroll_filters.ts b/www/js/survey/enketo/infinite_scroll_filters.ts index 5ec72eae7..98eba65db 100644 --- a/www/js/survey/enketo/infinite_scroll_filters.ts +++ b/www/js/survey/enketo/infinite_scroll_filters.ts @@ -10,7 +10,14 @@ import i18next from "i18next"; import { getAngularService } from "../../angular-react-helper"; const unlabeledCheck = (t) => { - return typeof t.userInput[getAngularService('EnketoTripButtonService').SINGLE_KEY] === 'undefined'; + try { + const EnketoTripButtonService = getAngularService("EnketoTripButtonService"); + const etbsSingleKey = EnketoTripButtonService.SINGLE_KEY; + return typeof t.userInput[etbsSingleKey] === 'undefined'; + } + catch (e) { + console.log("Error in retrieving EnketoTripButtonService: ", e); + } } const UNLABELED = { From acb36aad8434a6d1ff91bf33b19f9ec86a8509aa Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 12 Oct 2023 13:57:18 -0400 Subject: [PATCH 080/134] cordovaMocks: use cordova-ios version from json --- www/__mocks__/cordovaMocks.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/www/__mocks__/cordovaMocks.ts b/www/__mocks__/cordovaMocks.ts index 44c21677c..4a9189ecd 100644 --- a/www/__mocks__/cordovaMocks.ts +++ b/www/__mocks__/cordovaMocks.ts @@ -1,7 +1,9 @@ +import packageJsonBuild from '../../package.cordovabuild.json'; + export const mockCordova = () => { window['cordova'] ||= {}; window['cordova'].platformId ||= 'ios'; - window['cordova'].platformVersion ||= '6.2.0'; + window['cordova'].platformVersion ||= packageJsonBuild.dependencies['cordova-ios']; window['cordova'].plugins ||= {}; } From e546d737a2a67e7e64660dbcb6b2fd4b79e4dfbf Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Thu, 12 Oct 2023 14:05:59 -0400 Subject: [PATCH 081/134] commHelper: promisify 'registerUser' Per https://github.com/e-mission/e-mission-phone/pull/1040#discussion_r1350911732 --- www/js/commHelper.ts | 9 +++++++-- www/js/onboarding/SaveQrPage.tsx | 6 +++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/www/js/commHelper.ts b/www/js/commHelper.ts index 259677090..b9584a044 100644 --- a/www/js/commHelper.ts +++ b/www/js/commHelper.ts @@ -129,8 +129,13 @@ export function getAggregateData(path: string, data: any) { }); } -export function registerUser(successCallback, errorCallback) { - window['cordova'].plugins.BEMServerComm.getUserPersonalData("/profile/create", successCallback, errorCallback); +export function registerUser() { + return new Promise((rs, rj) => { + window['cordova'].plugins.BEMServerComm.getUserPersonalData("/profile/create", rs, rj); + }).catch(error => { + error = `While registering user, ${error}`; + throw(error); + }); } export function updateUser(updateDoc) { diff --git a/www/js/onboarding/SaveQrPage.tsx b/www/js/onboarding/SaveQrPage.tsx index d8b555f14..6fcf06e8c 100644 --- a/www/js/onboarding/SaveQrPage.tsx +++ b/www/js/onboarding/SaveQrPage.tsx @@ -37,10 +37,10 @@ const SaveQrPage = ({ }) => { const EXPECTED_METHOD = "prompted-auth"; const dbStorageObject = {"token": token}; return storageSet(EXPECTED_METHOD, dbStorageObject).then((r) => { - registerUser((successResult) => { + registerUser().then((r) => { refreshOnboardingState(); - }, function(errorResult) { - displayError(errorResult, "User registration error"); + }).catch((e) => { + displayError(e, "User registration error"); }); }).catch((e) => { displayError(e, "Sign in error"); From 37be42648bf500aca3bf35150c4a2dc563a16716 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Thu, 12 Oct 2023 13:16:09 -0600 Subject: [PATCH 082/134] Adding default label-options confirmHelper.ts - Removed archaic behavior or using old Angular i18nUtils module to get the filename of the translations' trip_confirm_options.json file - The "else" default "no label_options found in config" behavior is almost identical to the if(appConfig.label_options), but it just uses the default label options URL at label-options.json.sample - Pulled the language-specific text handling behavior out of the if statement, because either way (label_options or no label_options) the JSON data model will look the same - The for loop for filling in the translatied text for label-options checks 1. The label-options file first, then 2. The i18next file for the translation label-options.json.sample - Created new file - Replaces trip_confirm_options.json.sample (this location in confirmHelper.ts was the only place in the codebase using this file) - Modeled after https://github.com/e-mission/nrel-openpath-deploy-configs/blob/main/label_options/example-program-label-options.json - Only has translations for EN and ES trip_confirm_options.json.sample - Removed this file --- www/js/survey/multilabel/confirmHelper.ts | 37 +++---- www/json/label-options.json.sample | 124 ++++++++++++++++++++++ www/json/trip_confirm_options.json.sample | 52 --------- 3 files changed, 140 insertions(+), 73 deletions(-) create mode 100644 www/json/label-options.json.sample delete mode 100644 www/json/trip_confirm_options.json.sample diff --git a/www/js/survey/multilabel/confirmHelper.ts b/www/js/survey/multilabel/confirmHelper.ts index 6350745eb..c7cf74c26 100644 --- a/www/js/survey/multilabel/confirmHelper.ts +++ b/www/js/survey/multilabel/confirmHelper.ts @@ -36,29 +36,24 @@ export async function getLabelOptions(appConfigParam?) { if (appConfig.label_options) { const labelOptionsJson = await fetchUrlCached(appConfig.label_options); + logDebug("label_options found in config, using dynamic label options at " + appConfig.label_options); labelOptions = JSON.parse(labelOptionsJson) as LabelOptions; - /* fill in the translations to the 'text' fields of the labelOptions, - according to the current language */ - const lang = i18next.language; - for (const opt in labelOptions) { - labelOptions[opt]?.forEach?.((o, i) => { - const translationKey = o.value; - const translation = labelOptions.translations[lang][translationKey]; - labelOptions[opt][i].text = translation; - }); - } } else { - // backwards compat: if dynamic config doesn't have label_options, use the old way - const i18nUtils = getAngularService("i18nUtils"); - const optionFileName = await i18nUtils.geti18nFileName("json/", "trip_confirm_options", ".json"); - try { - const optionJson = await fetch(optionFileName).then(r => r.json()); - labelOptions = optionJson as LabelOptions; - } catch (e) { - logDebug("error "+JSON.stringify(e)+" while reading confirm options, reverting to defaults"); - const optionJson = await fetch("json/trip_confirm_options.json.sample").then(r => r.json()); - labelOptions = optionJson as LabelOptions; - } + const defaultLabelOptionsURL = 'json/label-options.json.sample'; + logDebug("No label_options found in config, using default label options at " + defaultLabelOptionsURL); + const defaultLabelOptionsJson = await fetchUrlCached(defaultLabelOptionsURL); + labelOptions = JSON.parse(defaultLabelOptionsJson) as LabelOptions; + } + /* fill in the translations to the 'text' fields of the labelOptions, + according to the current language */ + const lang = i18next.language; + for (const opt in labelOptions) { + labelOptions[opt]?.forEach?.((o, i) => { + const translationKey = o.value; + // If translation exists in i18next, use that. Otherwise, use the one in the labelOptions. + const translation = labelOptions.translations[lang][translationKey] || i18next.t(`multilabel.${translationKey}`); + labelOptions[opt][i].text = translation; + }); } return labelOptions; } diff --git a/www/json/label-options.json.sample b/www/json/label-options.json.sample new file mode 100644 index 000000000..9d3447bda --- /dev/null +++ b/www/json/label-options.json.sample @@ -0,0 +1,124 @@ +{ + "MODE": [ + {"value":"walk", "baseMode":"WALKING", "met_equivalent":"WALKING", "kgCo2PerKm": 0}, + {"value":"e-bike", "baseMode":"E_BIKE", "met": {"ALL": {"range": [0, -1], "mets": 4.9}}, "kgCo2PerKm": 0.00728}, + {"value":"bike", "baseMode":"BICYCLING", "met_equivalent":"BICYCLING", "kgCo2PerKm": 0}, + {"value":"bikeshare", "baseMode":"BICYCLING", "met_equivalent":"BICYCLING", "kgCo2PerKm": 0}, + {"value":"scootershare", "baseMode":"E_SCOOTER", "met_equivalent":"IN_VEHICLE", "kgCo2PerKm": 0.00894}, + {"value":"drove_alone", "baseMode":"CAR", "met_equivalent":"IN_VEHICLE", "kgCo2PerKm": 0.22031}, + {"value":"shared_ride", "baseMode":"CAR", "met_equivalent":"IN_VEHICLE", "kgCo2PerKm": 0.11015}, + {"value":"e_car_drove_alone", "baseMode":"E_CAR", "met_equivalent":"IN_VEHICLE", "kgCo2PerKm": 0.08216}, + {"value":"e_car_shared_ride", "baseMode":"E_CAR", "met_equivalent":"IN_VEHICLE", "kgCo2PerKm": 0.04108}, + {"value":"moped", "baseMode":"MOPED", "met_equivalent":"IN_VEHICLE", "kgCo2PerKm": 0.05555}, + {"value":"taxi", "baseMode":"TAXI", "met_equivalent":"IN_VEHICLE", "kgCo2PerKm": 0.30741}, + {"value":"bus", "baseMode":"BUS", "met_equivalent":"IN_VEHICLE", "kgCo2PerKm": 0.20727}, + {"value":"train", "baseMode":"TRAIN", "met_equivalent":"IN_VEHICLE", "kgCo2PerKm": 0.12256}, + {"value":"free_shuttle", "baseMode":"BUS", "met_equivalent":"IN_VEHICLE", "kgCo2PerKm": 0.20727}, + {"value":"air", "baseMode":"AIR", "met_equivalent":"IN_VEHICLE", "kgCo2PerKm": 0.09975}, + {"value":"not_a_trip", "baseMode":"UNKNOWN", "met_equivalent":"UNKNOWN", "kgCo2PerKm": 0}, + {"value":"other", "baseMode":"OTHER", "met_equivalent":"UNKNOWN", "kgCo2PerKm": 0} + ], + "PURPOSE": [ + {"value":"home"}, + {"value":"work"}, + {"value":"at_work"}, + {"value":"school"}, + {"value":"transit_transfer"}, + {"value":"shopping"}, + {"value":"meal"}, + {"value":"pick_drop_person"}, + {"value":"pick_drop_item"}, + {"value":"personal_med"}, + {"value":"access_recreation"}, + {"value":"exercise"}, + {"value":"entertainment"}, + {"value":"religious"}, + {"value":"other"} + ], + "REPLACED_MODE": [ + {"value":"no_travel"}, + {"value":"walk"}, + {"value":"bike"}, + {"value":"bikeshare"}, + {"value":"scootershare"}, + {"value":"drove_alone"}, + {"value":"shared_ride"}, + {"value":"e_car_drove_alone"}, + {"value":"e_car_shared_ride"}, + {"value":"taxi"}, + {"value":"bus"}, + {"value":"train"}, + {"value":"free_shuttle"}, + {"value":"other"} + ], + "translations": { + "en": { + "walk": "Walk", + "e-bike": "E-bike", + "bike": "Regular Bike", + "bikeshare": "Bikeshare", + "scootershare": "Scooter share", + "drove_alone": "Gas Car Drove Alone", + "shared_ride": "Gas Car Shared Ride", + "e_car_drove_alone": "E-Car Drove Alone", + "e_car_shared_ride": "E-Car Shared Ride", + "moped": "Moped", + "taxi": "Taxi/Uber/Lyft", + "bus": "Bus", + "train": "Train", + "free_shuttle": "Free Shuttle", + "air": "Air", + "not_a_trip": "Not a trip", + "no_travel": "No travel", + "home": "Home", + "work": "To Work", + "at_work": "At Work", + "school": "School", + "transit_transfer": "Transit transfer", + "shopping": "Shopping", + "meal": "Meal", + "pick_drop_person": "Pick-up/ Drop off Person", + "pick_drop_item": "Pick-up/ Drop off Item", + "personal_med": "Personal/ Medical", + "access_recreation": "Access Recreation", + "exercise": "Recreation/ Exercise", + "entertainment": "Entertainment/ Social", + "religious": "Religious", + "other": "Other" + }, + "es": { + "walk": "Caminando", + "e-bike": "e-bicicleta", + "bike": "Bicicleta", + "bikeshare": "Bicicleta compartida", + "scootershare": "Motoneta compartida", + "drove_alone": "Coche de Gas, Condujo solo", + "shared_ride": "Coche de Gas, Condujo con otros", + "e_car_drove_alone": "e-coche, Condujo solo", + "e_car_shared_ride": "e-coche, Condujo con ontras", + "moped": "Ciclomotor", + "taxi": "Taxi/Uber/Lyft", + "bus": "Autobús", + "train": "Tren", + "free_shuttle": "Colectivo gratuito", + "air": "Avión", + "not_a_trip": "No es un viaje", + "no_travel": "No viajar", + "home": "Inicio", + "work": "Trabajo", + "at_work": "En el trabajo", + "school": "Escuela", + "transit_transfer": "Transbordo", + "shopping": "Compras", + "meal": "Comida", + "pick_drop_person": "Recoger/ Entregar Individuo", + "pick_drop_item": "Recoger/ Entregar Objeto", + "personal_med": "Personal/ Médico", + "access_recreation": "Acceder a Recreación", + "exercise": "Recreación/ Ejercicio", + "entertainment": "Entretenimiento/ Social", + "religious": "Religioso", + "other": "Otros" + } + } +} \ No newline at end of file diff --git a/www/json/trip_confirm_options.json.sample b/www/json/trip_confirm_options.json.sample deleted file mode 100644 index 1e90bc1bb..000000000 --- a/www/json/trip_confirm_options.json.sample +++ /dev/null @@ -1,52 +0,0 @@ -{ - "MODE" : [ - {"text":"Walk", "value":"walk", "baseMode":"WALKING", "met_equivalent": "WALKING", "kgCo2PerKm": 0}, - {"text":"E-bike","value":"e-bike", "baseMode": "E_BIKE", "met": { - "ALL": {"range": [0, -1], "mets": 4.9} - }, "kgCo2PerKm": 0.00728}, - {"text":"Regular Bike","value":"bike", "baseMode":"BICYCLING", "met_equivalent": "BICYCLING", "kgCo2PerKm": 0}, - {"text":"Bikeshare","value":"bikeshare", "baseMode":"BICYCLING", "met_equivalent": "BICYCLING", "kgCo2PerKm": 0}, - {"text":"Scooter share","value":"scootershare", "baseMode":"E_SCOOTER", "met_equivalent": "IN_VEHICLE", "kgCo2PerKm": 0.00894}, - {"text":"Gas Car Drove Alone","value":"drove_alone", "baseMode":"CAR", "met_equivalent": "IN_VEHICLE", "kgCo2PerKm": 0.22031}, - {"text":"Gas Car Shared Ride","value":"shared_ride", "baseMode":"CAR", "met_equivalent": "IN_VEHICLE", "kgCo2PerKm": 0.11015}, - {"text":"E-Car Drove Alone","value":"e_car_drove_alone", "baseMode":"E_CAR", "met_equivalent": "IN_VEHICLE", "kgCo2PerKm": 0.08216}, - {"text":"E-Car Shared Ride","value":"e_car_shared_ride", "baseMode":"E_CAR", "met_equivalent": "IN_VEHICLE", "kgCo2PerKm": 0.04108}, - {"text":"Taxi/Uber/Lyft","value":"taxi", "baseMode":"TAXI", "met_equivalent": "IN_VEHICLE", "kgCo2PerKm": 0.30741}, - {"text":"Bus","value":"bus", "baseMode":"BUS", "met_equivalent": "IN_VEHICLE", "kgCo2PerKm": 0.20727}, - {"text":"Train","value":"train", "baseMode":"TRAIN", "met_equivalent": "IN_VEHICLE", "kgCo2PerKm": 0.12256}, - {"text":"Free Shuttle","value":"free_shuttle", "baseMode":"BUS", "met_equivalent": "IN_VEHICLE", "kgCo2PerKm": 0.20727}, - {"text":"Air","value":"air", "baseMode":"AIR", "met_equivalent": "IN_VEHICLE", "kgCo2PerKm": 0.09975}, - {"text":"Not a Trip","value":"not_a_trip", "baseMode":"UNKNOWN", "met_equivalent": "UNKNOWN", "kgCo2PerKm": 0}, - {"text":"Other","value":"other", "baseMode":"OTHER", "met_equivalent": "UNKNOWN", "kgCo2PerKm": 0}], - "REPLACED_MODE" : [ - {"text":"No travel", "value":"no_travel"}, - {"text":"Walk", "value":"walk"}, - {"text":"Regular Bike","value":"bike"}, - {"text":"Bikeshare","value":"bikeshare"}, - {"text":"Scooter share","value":"scootershare"}, - {"text":"Gas Car, drove alone","value":"drove_alone"}, - {"text":"Gas Car, with others","value":"shared_ride"}, - {"text":"E-Car, drove alone","value":"e_car_drove_alone"}, - {"text":"E-Car, with others","value":"e_car_shared_ride"}, - {"text":"Taxi/Uber/Lyft","value":"taxi"}, - {"text":"Bus","value":"bus"}, - {"text":"Train","value":"train"}, - {"text":"Free Shuttle","value":"free_shuttle"}, - {"text":"Other","value":"other"}], - "PURPOSE" : [ - {"text":"Home", "value":"home"}, - {"text":"To Work","value":"work"}, - {"text":"At Work","value":"at_work"}, - {"text":"School","value":"school"}, - {"text":"Transit transfer", "value":"transit_transfer"}, - {"text":"Shopping","value":"shopping"}, - {"text":"Meal","value":"meal"}, - {"text":"Pick-up/ Drop off Person","value":"pick_drop_person"}, - {"text":"Pick-up/ Drop off Item","value":"pick_drop_item"}, - {"text":"Personal/ Medical","value":"personal_med"}, - {"text":"Access Recreation","value":"access_recreation"}, - {"text":"Recreation/ Exercise","value":"exercise"}, - {"text":"Entertainment/ Social","value":"entertainment"}, - {"text":"Religious", "value":"religious"}, - {"text":"Other","value":"other"}] -} From 650851763f6146ec8cb934ba63d0e1683e37e6b9 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Thu, 12 Oct 2023 13:17:08 -0600 Subject: [PATCH 083/134] Adding examples of multilabel translations to en.json en.json - Added multilabel filed filled with a few multilabel translations in EN --- www/i18n/en.json | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/www/i18n/en.json b/www/i18n/en.json index e47fdd62d..07d31c2b3 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -146,6 +146,41 @@ "no-travel-hint": "To see more, change the filters above or go record some travel!" }, + "multilabel":{ + "walk": "Walk", + "e-bike": "E-bike", + "bike": "Regular Bike", + "bikeshare": "Bikeshare", + "scootershare": "Scooter share", + "drove_alone": "Gas Car Drove Alone", + "shared_ride": "Gas Car Shared Ride", + "e_car_drove_alone": "E-Car Drove Alone", + "e_car_shared_ride": "E-Car Shared Ride", + "moped": "Moped", + "taxi": "Taxi/Uber/Lyft", + "bus": "Bus", + "train": "Train", + "free_shuttle": "Free Shuttle", + "air": "Air", + "not_a_trip": "Not a trip", + "no_travel": "No travel", + "home": "Home", + "work": "To Work", + "at_work": "At Work", + "school": "School", + "transit_transfer": "Transit transfer", + "shopping": "Shopping", + "meal": "Meal", + "pick_drop_person": "Pick-up/ Drop off Person", + "pick_drop_item": "Pick-up/ Drop off Item", + "personal_med": "Personal/ Medical", + "access_recreation": "Access Recreation", + "exercise": "Recreation/ Exercise", + "entertainment": "Entertainment/ Social", + "religious": "Religious", + "other": "Other" + }, + "main-metrics":{ "summary": "My Summary", "chart": "Chart", From 4d86b3727623413f113eb3207ac1691ad3f9499e Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Fri, 13 Oct 2023 01:37:28 -0400 Subject: [PATCH 084/134] Merge branch 'service_rewrite_2023' of https://github.com/e-mission/e-mission-phone into rewrite_angular_service --- .bowerrc | 3 - .gitignore | 4 - bin/download_settings_controls.js | 33 - package.cordovabuild.json | 5 +- package.serve.json | 4 +- resources/android/ic_mood_question.png | Bin 516 -> 0 bytes .../drawable-hdpi-v11/ic_mood_question.png | Bin 1005 -> 0 bytes .../drawable-hdpi-v9/ic_mood_question.png | Bin 671 -> 0 bytes .../drawable-hdpi/ic_mood_question.png | Bin 1455 -> 0 bytes .../drawable-mdpi-v11/ic_mood_question.png | Bin 571 -> 0 bytes .../drawable-mdpi-v9/ic_mood_question.png | Bin 469 -> 0 bytes .../drawable-mdpi/ic_mood_question.png | Bin 834 -> 0 bytes .../drawable-xhdpi-v11/ic_mood_question.png | Bin 1540 -> 0 bytes .../drawable-xhdpi-v9/ic_mood_question.png | Bin 1272 -> 0 bytes .../drawable-xhdpi/ic_mood_question.png | Bin 2140 -> 0 bytes .../drawable-xxhdpi-v11/ic_mood_question.png | Bin 1632 -> 0 bytes .../drawable-xxhdpi-v9/ic_mood_question.png | Bin 1359 -> 0 bytes .../drawable-xxhdpi/ic_mood_question.png | Bin 1706 -> 0 bytes .../drawable-hdpi-v11/ic_question_answer.png | Bin 318 -> 0 bytes .../drawable-hdpi-v9/ic_question_answer.png | Bin 236 -> 0 bytes .../drawable-hdpi/ic_question_answer.png | Bin 895 -> 0 bytes .../drawable-mdpi-v11/ic_question_answer.png | Bin 238 -> 0 bytes .../drawable-mdpi-v9/ic_question_answer.png | Bin 265 -> 0 bytes .../drawable-mdpi/ic_question_answer.png | Bin 631 -> 0 bytes .../drawable-xhdpi-v11/ic_question_answer.png | Bin 351 -> 0 bytes .../drawable-xhdpi-v9/ic_question_answer.png | Bin 390 -> 0 bytes .../drawable-xhdpi/ic_question_answer.png | Bin 1218 -> 0 bytes .../ic_question_answer.png | Bin 509 -> 0 bytes .../drawable-xxhdpi-v9/ic_question_answer.png | Bin 357 -> 0 bytes .../drawable-xxhdpi/ic_question_answer.png | Bin 439 -> 0 bytes resources/minus.gif | Bin 4635 -> 0 bytes resources/plus.gif | Bin 4633 -> 0 bytes scss/ionic.app.scss | 23 - setup/setup_shared_native.sh | 2 - webpack.config.js | 1 + www/css/appstatus.css | 12 - www/css/intro.css | 87 - www/css/main.recent.css | 12 - www/css/style.css | 17 - www/i18n/en.json | 86 +- .../enketo => img}/enketo_bare_150x56.png | Bin www/index.html | 15 +- www/index.js | 21 +- www/js/App.tsx | 88 + www/js/app.js | 111 - www/js/appTheme.ts | 5 +- www/js/appstatus/PermissionsControls.tsx | 65 + www/js/appstatus/permissioncheck.js | 429 -- www/js/components/ActionMenu.tsx | 41 + www/js/components/BarChart.tsx | 206 +- www/js/components/Carousel.tsx | 42 + www/js/components/Chart.tsx | 196 + .../{LeafletView.jsx => LeafletView.tsx} | 13 +- www/js/components/LineChart.tsx | 11 + www/js/components/QrCode.jsx | 19 - www/js/components/QrCode.tsx | 41 + www/js/components/charting.ts | 161 + www/js/config/dynamicConfig.ts | 248 + www/js/config/dynamic_config.js | 348 -- www/js/config/imperial.js | 51 - www/js/config/serverConn.ts | 13 + www/js/config/server_conn.js | 36 - www/js/config/useImperialConfig.ts | 65 +- www/js/control/AlertBar.jsx | 2 +- www/js/control/AppStatusModal.tsx | 466 +- www/js/control/ControlCollectionHelper.tsx | 285 + www/js/control/ControlSyncHelper.tsx | 284 + www/js/control/DemographicsSettingRow.jsx | 42 +- www/js/control/LogPage.tsx | 153 + www/js/control/PopOpCode.jsx | 7 +- www/js/control/PrivacyPolicyModal.tsx | 182 +- www/js/control/ProfileSettings.jsx | 352 +- www/js/control/SensedPage.tsx | 91 + www/js/control/SettingRow.jsx | 2 +- www/js/control/general-settings.js | 44 - www/js/diary.js | 4 +- www/js/diary/LabelTab.tsx | 27 +- www/js/diary/cards/PlaceCard.tsx | 2 +- www/js/diary/cards/TimestampBadge.tsx | 16 +- www/js/diary/cards/TripCard.tsx | 2 +- www/js/diary/diaryHelper.ts | 29 +- www/js/diary/list/LabelListScreen.tsx | 2 +- www/js/diary/services.js | 5 +- www/js/intro.js | 243 - www/js/join/join-ctrl.js | 109 - www/js/main.js | 97 +- www/js/metrics-factory.js | 9 +- www/js/metrics-mappings.js | 10 +- www/js/metrics.js | 1449 ----- www/js/metrics/ActiveMinutesTableCard.tsx | 99 + www/js/metrics/CarbonFootprintCard.tsx | 168 + www/js/metrics/CarbonTextCard.tsx | 151 + www/js/metrics/ChangeIndicator.tsx | 79 + www/js/metrics/DailyActiveMinutesCard.tsx | 64 + www/js/metrics/MetricsCard.tsx | 133 + www/js/metrics/MetricsDateSelect.tsx | 71 + www/js/metrics/MetricsTab.tsx | 152 + www/js/metrics/WeeklyActiveMinutesCard.tsx | 78 + www/js/metrics/metricsHelper.ts | 212 + www/js/metrics/metricsTypes.ts | 14 + www/js/ngApp.js | 82 + www/js/onboarding/ConsentPage.tsx | 43 + www/js/onboarding/OnboardingStack.tsx | 53 + www/js/onboarding/PrivacyPolicy.tsx | 177 + www/js/onboarding/SaveQrPage.tsx | 92 + www/js/onboarding/StudySummary.tsx | 47 + www/js/onboarding/SummaryPage.tsx | 34 + www/js/onboarding/SurveyPage.tsx | 95 + www/js/onboarding/WelcomePage.tsx | 202 + www/js/onboarding/onboardingHelper.ts | 61 + www/js/recent.js | 157 - www/js/splash/notifScheduler.js | 8 +- www/js/splash/startprefs.js | 92 +- www/js/survey/enketo/EnketoModal.tsx | 20 +- www/js/survey/enketo/answer.js | 10 +- .../survey/enketo/enketo-add-note-button.js | 2 - www/js/survey/enketo/enketo-demographics.js | 150 - www/js/survey/enketo/enketo-preview.js | 67 - www/js/survey/enketo/enketo-trip-button.js | 1 - www/js/survey/enketo/enketoHelper.ts | 23 + www/js/survey/enketo/launch.js | 147 - www/js/survey/enketo/service.js | 243 - www/js/survey/external/launch.js | 250 - www/js/survey/external/time_insert.js | 22 - www/js/survey/external/uuid_insert_id.js | 22 - www/js/survey/external/uuid_insert_xpath.js | 27 - .../multilabel/MultiLabelButtonGroup.tsx | 2 +- www/js/survey/multilabel/confirmHelper.ts | 10 +- www/js/survey/multilabel/multi-label-ui.js | 5 +- www/js/useAppConfig.ts | 28 +- www/js/usePermissionStatus.ts | 357 ++ www/manual_lib/fontawesome/css/all.min.css | 5 - .../fontawesome/webfonts/fa-brands-400.eot | Bin 129352 -> 0 bytes .../fontawesome/webfonts/fa-brands-400.svg | 3442 ------------ .../fontawesome/webfonts/fa-brands-400.ttf | Bin 129048 -> 0 bytes .../fontawesome/webfonts/fa-brands-400.woff | Bin 87352 -> 0 bytes .../fontawesome/webfonts/fa-brands-400.woff2 | Bin 74508 -> 0 bytes .../fontawesome/webfonts/fa-regular-400.eot | Bin 34388 -> 0 bytes .../fontawesome/webfonts/fa-regular-400.svg | 803 --- .../fontawesome/webfonts/fa-regular-400.ttf | Bin 34092 -> 0 bytes .../fontawesome/webfonts/fa-regular-400.woff | Bin 16804 -> 0 bytes .../fontawesome/webfonts/fa-regular-400.woff2 | Bin 13580 -> 0 bytes .../fontawesome/webfonts/fa-solid-900.eot | Bin 192116 -> 0 bytes .../fontawesome/webfonts/fa-solid-900.svg | 4649 ----------------- .../fontawesome/webfonts/fa-solid-900.ttf | Bin 191832 -> 0 bytes .../fontawesome/webfonts/fa-solid-900.woff | Bin 98020 -> 0 bytes .../fontawesome/webfonts/fa-solid-900.woff2 | Bin 75440 -> 0 bytes www/templates/appstatus/permissioncheck.html | 80 - www/templates/caloriePopup.html | 24 - www/templates/control/app-status-modal.html | 15 - www/templates/control/main-consent.html | 5 - www/templates/control/qrc.html | 28 - www/templates/intro/changes.html | 21 - www/templates/intro/consent-text.html | 136 - www/templates/intro/consent.html | 21 - www/templates/intro/intro.html | 19 - www/templates/intro/reconsent.html | 10 - www/templates/intro/saveTokenFile.html | 31 - www/templates/intro/sensor_explanation.html | 21 - www/templates/intro/summary.html | 24 - www/templates/intro/survey.html | 1 - www/templates/join/about-app.html | 42 - www/templates/join/request_join.html | 39 - www/templates/main-metrics.html | 225 - www/templates/main.html | 26 - .../metrics/arrow-greater-lesser.html | 38 - www/templates/metrics/metrics-control.html | 80 - www/templates/metrics/range-display.html | 2 - www/templates/recent/log.html | 21 - www/templates/recent/sensedData.html | 21 - www/templates/splash/splash.html | 7 - .../survey/enketo/demographics-button.html | 6 - www/templates/survey/enketo/form-base.html | 43 - www/templates/survey/enketo/inline.html | 40 - www/templates/survey/enketo/modal.html | 13 - www/templates/survey/enketo/preview.html | 6 - 176 files changed, 4564 insertions(+), 15558 deletions(-) delete mode 100644 .bowerrc delete mode 100755 bin/download_settings_controls.js delete mode 100644 resources/android/ic_mood_question.png delete mode 100644 resources/android/ic_mood_question/drawable-hdpi-v11/ic_mood_question.png delete mode 100644 resources/android/ic_mood_question/drawable-hdpi-v9/ic_mood_question.png delete mode 100644 resources/android/ic_mood_question/drawable-hdpi/ic_mood_question.png delete mode 100644 resources/android/ic_mood_question/drawable-mdpi-v11/ic_mood_question.png delete mode 100644 resources/android/ic_mood_question/drawable-mdpi-v9/ic_mood_question.png delete mode 100644 resources/android/ic_mood_question/drawable-mdpi/ic_mood_question.png delete mode 100644 resources/android/ic_mood_question/drawable-xhdpi-v11/ic_mood_question.png delete mode 100644 resources/android/ic_mood_question/drawable-xhdpi-v9/ic_mood_question.png delete mode 100644 resources/android/ic_mood_question/drawable-xhdpi/ic_mood_question.png delete mode 100644 resources/android/ic_mood_question/drawable-xxhdpi-v11/ic_mood_question.png delete mode 100644 resources/android/ic_mood_question/drawable-xxhdpi-v9/ic_mood_question.png delete mode 100644 resources/android/ic_mood_question/drawable-xxhdpi/ic_mood_question.png delete mode 100644 resources/android/ic_question_answer/drawable-hdpi-v11/ic_question_answer.png delete mode 100644 resources/android/ic_question_answer/drawable-hdpi-v9/ic_question_answer.png delete mode 100644 resources/android/ic_question_answer/drawable-hdpi/ic_question_answer.png delete mode 100644 resources/android/ic_question_answer/drawable-mdpi-v11/ic_question_answer.png delete mode 100644 resources/android/ic_question_answer/drawable-mdpi-v9/ic_question_answer.png delete mode 100644 resources/android/ic_question_answer/drawable-mdpi/ic_question_answer.png delete mode 100644 resources/android/ic_question_answer/drawable-xhdpi-v11/ic_question_answer.png delete mode 100644 resources/android/ic_question_answer/drawable-xhdpi-v9/ic_question_answer.png delete mode 100644 resources/android/ic_question_answer/drawable-xhdpi/ic_question_answer.png delete mode 100644 resources/android/ic_question_answer/drawable-xxhdpi-v11/ic_question_answer.png delete mode 100644 resources/android/ic_question_answer/drawable-xxhdpi-v9/ic_question_answer.png delete mode 100644 resources/android/ic_question_answer/drawable-xxhdpi/ic_question_answer.png delete mode 100644 resources/minus.gif delete mode 100644 resources/plus.gif delete mode 100644 scss/ionic.app.scss delete mode 100644 www/css/appstatus.css delete mode 100644 www/css/intro.css delete mode 100644 www/css/main.recent.css rename www/{templates/survey/enketo => img}/enketo_bare_150x56.png (100%) create mode 100644 www/js/App.tsx delete mode 100644 www/js/app.js create mode 100644 www/js/appstatus/PermissionsControls.tsx delete mode 100644 www/js/appstatus/permissioncheck.js create mode 100644 www/js/components/ActionMenu.tsx create mode 100644 www/js/components/Carousel.tsx create mode 100644 www/js/components/Chart.tsx rename www/js/components/{LeafletView.jsx => LeafletView.tsx} (92%) create mode 100644 www/js/components/LineChart.tsx delete mode 100644 www/js/components/QrCode.jsx create mode 100644 www/js/components/QrCode.tsx create mode 100644 www/js/components/charting.ts create mode 100644 www/js/config/dynamicConfig.ts delete mode 100644 www/js/config/dynamic_config.js delete mode 100644 www/js/config/imperial.js create mode 100644 www/js/config/serverConn.ts delete mode 100644 www/js/config/server_conn.js create mode 100644 www/js/control/ControlCollectionHelper.tsx create mode 100644 www/js/control/ControlSyncHelper.tsx create mode 100644 www/js/control/LogPage.tsx create mode 100644 www/js/control/SensedPage.tsx delete mode 100644 www/js/control/general-settings.js delete mode 100644 www/js/intro.js delete mode 100644 www/js/join/join-ctrl.js delete mode 100644 www/js/metrics.js create mode 100644 www/js/metrics/ActiveMinutesTableCard.tsx create mode 100644 www/js/metrics/CarbonFootprintCard.tsx create mode 100644 www/js/metrics/CarbonTextCard.tsx create mode 100644 www/js/metrics/ChangeIndicator.tsx create mode 100644 www/js/metrics/DailyActiveMinutesCard.tsx create mode 100644 www/js/metrics/MetricsCard.tsx create mode 100644 www/js/metrics/MetricsDateSelect.tsx create mode 100644 www/js/metrics/MetricsTab.tsx create mode 100644 www/js/metrics/WeeklyActiveMinutesCard.tsx create mode 100644 www/js/metrics/metricsHelper.ts create mode 100644 www/js/metrics/metricsTypes.ts create mode 100644 www/js/ngApp.js create mode 100644 www/js/onboarding/ConsentPage.tsx create mode 100644 www/js/onboarding/OnboardingStack.tsx create mode 100644 www/js/onboarding/PrivacyPolicy.tsx create mode 100644 www/js/onboarding/SaveQrPage.tsx create mode 100644 www/js/onboarding/StudySummary.tsx create mode 100644 www/js/onboarding/SummaryPage.tsx create mode 100644 www/js/onboarding/SurveyPage.tsx create mode 100644 www/js/onboarding/WelcomePage.tsx create mode 100644 www/js/onboarding/onboardingHelper.ts delete mode 100644 www/js/recent.js delete mode 100644 www/js/survey/enketo/enketo-demographics.js delete mode 100644 www/js/survey/enketo/enketo-preview.js delete mode 100644 www/js/survey/enketo/launch.js delete mode 100644 www/js/survey/enketo/service.js delete mode 100644 www/js/survey/external/launch.js delete mode 100644 www/js/survey/external/time_insert.js delete mode 100644 www/js/survey/external/uuid_insert_id.js delete mode 100644 www/js/survey/external/uuid_insert_xpath.js create mode 100644 www/js/usePermissionStatus.ts delete mode 100644 www/manual_lib/fontawesome/css/all.min.css delete mode 100644 www/manual_lib/fontawesome/webfonts/fa-brands-400.eot delete mode 100644 www/manual_lib/fontawesome/webfonts/fa-brands-400.svg delete mode 100644 www/manual_lib/fontawesome/webfonts/fa-brands-400.ttf delete mode 100644 www/manual_lib/fontawesome/webfonts/fa-brands-400.woff delete mode 100644 www/manual_lib/fontawesome/webfonts/fa-brands-400.woff2 delete mode 100644 www/manual_lib/fontawesome/webfonts/fa-regular-400.eot delete mode 100644 www/manual_lib/fontawesome/webfonts/fa-regular-400.svg delete mode 100644 www/manual_lib/fontawesome/webfonts/fa-regular-400.ttf delete mode 100644 www/manual_lib/fontawesome/webfonts/fa-regular-400.woff delete mode 100644 www/manual_lib/fontawesome/webfonts/fa-regular-400.woff2 delete mode 100644 www/manual_lib/fontawesome/webfonts/fa-solid-900.eot delete mode 100644 www/manual_lib/fontawesome/webfonts/fa-solid-900.svg delete mode 100644 www/manual_lib/fontawesome/webfonts/fa-solid-900.ttf delete mode 100644 www/manual_lib/fontawesome/webfonts/fa-solid-900.woff delete mode 100644 www/manual_lib/fontawesome/webfonts/fa-solid-900.woff2 delete mode 100644 www/templates/appstatus/permissioncheck.html delete mode 100644 www/templates/caloriePopup.html delete mode 100644 www/templates/control/app-status-modal.html delete mode 100644 www/templates/control/main-consent.html delete mode 100644 www/templates/control/qrc.html delete mode 100644 www/templates/intro/changes.html delete mode 100644 www/templates/intro/consent-text.html delete mode 100644 www/templates/intro/consent.html delete mode 100644 www/templates/intro/intro.html delete mode 100644 www/templates/intro/reconsent.html delete mode 100644 www/templates/intro/saveTokenFile.html delete mode 100644 www/templates/intro/sensor_explanation.html delete mode 100644 www/templates/intro/summary.html delete mode 100644 www/templates/intro/survey.html delete mode 100644 www/templates/join/about-app.html delete mode 100644 www/templates/join/request_join.html delete mode 100644 www/templates/main-metrics.html delete mode 100644 www/templates/main.html delete mode 100644 www/templates/metrics/arrow-greater-lesser.html delete mode 100644 www/templates/metrics/metrics-control.html delete mode 100644 www/templates/metrics/range-display.html delete mode 100644 www/templates/recent/log.html delete mode 100644 www/templates/recent/sensedData.html delete mode 100644 www/templates/splash/splash.html delete mode 100644 www/templates/survey/enketo/demographics-button.html delete mode 100644 www/templates/survey/enketo/form-base.html delete mode 100644 www/templates/survey/enketo/inline.html delete mode 100644 www/templates/survey/enketo/modal.html delete mode 100644 www/templates/survey/enketo/preview.html diff --git a/.bowerrc b/.bowerrc deleted file mode 100644 index e28246d45..000000000 --- a/.bowerrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "directory": "www/lib" -} diff --git a/.gitignore b/.gitignore index e626e9a48..6801f890d 100644 --- a/.gitignore +++ b/.gitignore @@ -14,9 +14,5 @@ app-settings.json *.app.zip *.ipa www/dist/ -www/js/control/collect-settings.js -www/templates/control/main-collect-settings.html -www/js/control/sync-settings.js -www/templates/control/main-sync-settings.html config.xml package.json diff --git a/bin/download_settings_controls.js b/bin/download_settings_controls.js deleted file mode 100755 index fce3fb675..000000000 --- a/bin/download_settings_controls.js +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env node - -var https = require('https'); -var fs = require('fs'); - -var download = function(url, dest, cb) { - var file = fs.createWriteStream(dest); - var request = https.get(url, function(response) { - response.pipe(file); - file.on('finish', function() { - file.close(cb); // close() is async, call cb after close completes. - }); - }).on('error', function(err) { // Handle errors - fs.unlink(dest); // Delete the file async. (But we don't check the result) - if (cb) cb(err.message); - }); -}; - -download("https://raw.githubusercontent.com/e-mission/e-mission-data-collection/master/www/ui/ionic/js/collect-settings.js", "www/js/control/collect-settings.js", function(message) { - console.log("Data collection settings javascript updated"); -}); - -download("https://raw.githubusercontent.com/e-mission/e-mission-data-collection/master/www/ui/ionic/templates/main-collect-settings.html", "www/templates/control/main-collect-settings.html", function(message) { - console.log("Data collection settings template updated"); -}); - -download("https://raw.githubusercontent.com/e-mission/cordova-server-sync/master/www/ui/ionic/js/sync-settings.js", "www/js/control/sync-settings.js", function(message) { - console.log("Sync collection settings javascript updated"); -}); - -download("https://raw.githubusercontent.com/e-mission/cordova-server-sync/master/www/ui/ionic/templates/main-sync-settings.html", "www/templates/control/main-sync-settings.html", function(message) { - console.log("Sync collection settings template updated"); -}); diff --git a/package.cordovabuild.json b/package.cordovabuild.json index 20672bad0..b5d69872f 100644 --- a/package.cordovabuild.json +++ b/package.cordovabuild.json @@ -8,7 +8,6 @@ "url": "git+https://github.com/e-mission/e-mission-phone.git" }, "scripts": { - "setup-native": "./bin/download_settings_controls.js", "build": "npx webpack --config webpack.prod.js && npx cordova build", "build-dev": "npx webpack --config webpack.dev.js && npx cordova build", "build-dev-android": "npx webpack --config webpack.dev.js && npx cordova build android", @@ -109,7 +108,6 @@ "angular": "1.6.7", "angular-animate": "1.6.7", "angular-local-storage": "^0.7.1", - "angular-nvd3": "^1.0.7", "angular-sanitize": "1.6.7", "angular-simple-logger": "^0.1.7", "angular-translate": "^2.18.1", @@ -127,7 +125,7 @@ "cordova-plugin-app-version": "0.1.14", "cordova-plugin-customurlscheme": "5.0.2", "cordova-plugin-device": "2.1.0", - "cordova-plugin-em-datacollection": "git+https://github.com/e-mission/e-mission-data-collection.git#v1.7.9", + "cordova-plugin-em-datacollection": "git+https://github.com/e-mission/e-mission-data-collection.git#v1.8.0", "cordova-plugin-em-opcodeauth": "git+https://github.com/e-mission/cordova-jwt-auth.git#v1.7.2", "cordova-plugin-em-server-communication": "git+https://github.com/e-mission/cordova-server-communication.git#v1.2.6", "cordova-plugin-em-serversync": "git+https://github.com/e-mission/cordova-server-sync.git#v1.3.2", @@ -158,7 +156,6 @@ "moment-timezone": "^0.5.43", "ng-i18next": "^1.0.7", "npm": "^9.6.3", - "nvd3": "^1.8.6", "phonegap-plugin-barcodescanner": "git+https://github.com/phonegap/phonegap-plugin-barcodescanner#v8.1.0", "prop-types": "^15.8.1", "react": "^18.2.*", diff --git a/package.serve.json b/package.serve.json index f16c8bd66..1a2ef6cb0 100644 --- a/package.serve.json +++ b/package.serve.json @@ -8,7 +8,7 @@ "url": "git+https://github.com/e-mission/e-mission-phone.git" }, "scripts": { - "setup-serve": "./bin/download_settings_controls.js && ./bin/setup_autodeploy.js", + "setup-serve": "./bin/setup_autodeploy.js", "serve": "webpack --config webpack.dev.js && concurrently -k \"phonegap --verbose serve\" \"webpack --config webpack.dev.js --watch\"", "serve-prod": "webpack --config webpack.prod.js && concurrently -k \"phonegap --verbose serve\" \"webpack --config webpack.prod.js --watch\"", "serve-only": "phonegap --verbose serve", @@ -55,7 +55,6 @@ "angular": "1.6.7", "angular-animate": "1.6.7", "angular-local-storage": "^0.7.1", - "angular-nvd3": "^1.0.7", "angular-sanitize": "1.6.7", "angular-simple-logger": "^0.1.7", "angular-translate": "^2.18.1", @@ -83,7 +82,6 @@ "moment-timezone": "^0.5.43", "ng-i18next": "^1.0.7", "npm": "^9.6.3", - "nvd3": "^1.8.6", "prop-types": "^15.8.1", "react": "^18.2.*", "react-chartjs-2": "^5.2.0", diff --git a/resources/android/ic_mood_question.png b/resources/android/ic_mood_question.png deleted file mode 100644 index 8c7790f2e60045b85171650b80d7d8345213b95e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 516 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 JPDHLkV1kv7`E&pP diff --git a/resources/android/ic_mood_question/drawable-mdpi-v9/ic_mood_question.png b/resources/android/ic_mood_question/drawable-mdpi-v9/ic_mood_question.png deleted file mode 100644 index ecbbe1b3a0502af4ba0faf0efbcab8e10567620c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 469 zcmV;`0V@89P)X1^@s6IQ*`u0004;NklsQ_IkZu)HKa(x7)Rx$Vg6VTg{zL=a_iJJa04_ zlUl9zA|YpTHlSgv)e3JjKO2TIE0s!#N~IFS=$DXlxm->Hn*kcFL1eUcyZwOqUBsIv zl$ta^d%v=d>_@?_R;!76y&hszooOzW`jzCQ1|%g~COPo{jFe@>3FIK>IHP_xn@vrr z>zLjE#00{MQ*s#(fWZPFm*p=OiviAYbro_U0Oz?}Zol8}A5b5Q#{)3Bg<%O$#y(`C z7@cm2GdC{)^kejQ9suHU1+^=%Q^KT^$?vP1cUM54Rsm6xHpQfR>#`0|gZe-Lr$kFW zvho@Wg~Cu?(YM_Lx~^YiI&!;H|3Euf`zq-Y#xO!WlP)4AZh;!?eBtE8{3Mt9<_Jle=#t^Xy00000 LNkvXXu0mjfROZi7 diff --git a/resources/android/ic_mood_question/drawable-mdpi/ic_mood_question.png b/resources/android/ic_mood_question/drawable-mdpi/ic_mood_question.png deleted file mode 100644 index 5030bedfcd9a6b672dc338c84cdad464ebe43a1a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 834 zcmV-I1HJr-P)P000>X1^@s6#OZ}&0009CNklI?F|%@+k`j_AER-m_cTYsQ z4pVBT`~}bJ`PQ6_Gv0;SnR+_T_wsqp?|0@L6BYIAJ|>gNoSvTkH!Upt&CN}z|J+YrxQ$1BeZ3MP1nlqc3*d7u zD8`v*ozv+I$&sg-R99D{uC7kh+S-cy`+J<8p32{2V`Cz&j(Lv37@zFy>hlAoTQmio!bNyQ*fGbt@CMR|F- zXl-pxHkFTERaJ$ZogM7%?xLoqM)DjtHa0LbGlPnX3Nd9p`+^MeG?U`uVtkxZ5jUK# zi>nF7{wLP4ersz>#mLi43JVJ*L9MK;;Njr`X0utvsPXY}*{73}6STLttGLBtk^1H3 zWfdb&Gs(-#LqS1-XlQ6i_F;HN1G4eE%tgI~L z=H`m{xYyU$I5;>!NlA%{b9Hlbb2vIWl52x8<_`}K#rShAd74RTYASC}rGbF~`2BvY zuCAh}s7PY@`T6oSt*op>Mn;CjJsywL_4oIO^pmHV7>!0GCnqcAwpCE52Xeth_pwmX>7Yot>S?%*_0@hdj+BIyxE&2?>aaiHTU` zkf)iXq@;Mck+HF{Fc=I!7W>H|FWf(^KOc9HZ+3PTk%}Dhe%~*le}oZ{T6Y2~j{pDw M07*qoM6N<$g5>L*Q~&?~ diff --git a/resources/android/ic_mood_question/drawable-xhdpi-v11/ic_mood_question.png b/resources/android/ic_mood_question/drawable-xhdpi-v11/ic_mood_question.png deleted file mode 100644 index 78f2f253891c9254efe40bbec325da0ef7712e03..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1540 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 MvA#Kyp!)##Urr=Em;e9( diff --git a/resources/android/ic_mood_question/drawable-xxhdpi-v11/ic_mood_question.png b/resources/android/ic_mood_question/drawable-xxhdpi-v11/ic_mood_question.png deleted file mode 100644 index 913025e64ba5fe8dc71d5502ac69a4295387e688..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1632 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 diff --git a/resources/android/ic_mood_question/drawable-xxhdpi/ic_mood_question.png b/resources/android/ic_mood_question/drawable-xxhdpi/ic_mood_question.png deleted file mode 100644 index cd2b1614093dd2ee6e3826a61ea7c73ba85a6a45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1706 zcmV;b237fqP)002t}1^@s6I8J)%000JYNkl3CH#EBCpPMkP#;>3v)Cr+F=apJ_u-y{?Y#R`LA20Sh-6IKiBgesxNLO-t+%7qf4 zShyCnuK?SHNreF_lg}zN;+SdC`=b#giqms z@V(G3922^PZi_Ci9}$`vZ|1IqsX~T6XCNpmMYvUX0L3W%AqU}J;bviYP+zw=VU#dS zxLepLd?fq~ZLkgBR3W@3lnKvSlySX+aoeo9Kfz|9QYaL1cy4SUUQZMiFHGW<+Ze_w z_P~?EB4LVfr7%J`&te4Er!Y>P{9f>i`o0ys@^L{?)+iM9pinF96ix}9LIXStxmZIw z^OA*gERwmN&bVC5>dd|LzCA*tP%Ye#!bS!`S+lUHT7Gf;A}53&g@-|Z`M7^!xymEZ zBAgU@dEQpx1y!!!cfAzLN9{yW9l|c*6Cn@!Ls@g6j?kh5g*EU>-lb08m%AjUKiBe6 zr?9A91d*9%>A6`1xJ_7C56`benG^iB`zh1#7ay|NPF_8=sLZpqXkx4^EUbm+s(c{a zBV_w#cO>OVAV6$pJ6G8uJS5EikJh19v;ar1a#Z+^@I%)(*eNSu%2I!H8!1SLfN>9@ z!C#JC{v9Tig30|}Aq*H3;TdC@_o_A5gb>3#@tuy_N1mxtPX;mFDJx*gS|Yqjk{co= z(a1d4Gj6nS2zsGa_#x6x$p&$V@n){cGt)d{^C}_FOglCLn-UjMhN)aS^#-^j(3f9w33|8(3Wv$25J6N1n39j(wX80`Q zXN-ltjBktNv&Jy*B{BnL0wj3GrU_RGt60x53XALfpsXsKdKZf;qgpV;GcLjUf_K0q zJ|{(ZMiS#ROSlm(fH=?CA;N`(G9lKb>#OpEvZ}F?ZWdR{mrMMg){{t{TFQF5SzonN zR(NG4Tgv*KO~ffH%zDm6S<5YDsXyHs7@rm49;w15B;)x6xmsc=;ByZDpBBZvhI)Kf zC-K>)!0d)mWC7aVdY|C7jq;Ak?>?sdb3+vO^7U>QRh$*!9yihb(OBvW;d9|LVK))J zh6+nHQI6tX>9ofxtl-r*aaIWp(B{)c@Dc8@h`g^1pEsA-s+eNlNq&soQI@qAV~wU| zrrqwPeo$61onkvTEb-=s5$>Tyx@WMSC0N>6OIauIJ55pCOLN1gNT5T!g4+IH?3b#; zJWthOFA;bAnbg`zjV=?OAZgaE|10f1MySpARdrBHo#vDkFlE`5(t}o|w1HODoPThy z%V|DT;HPxsc$yyMNDWC&Ggm{5g`P_30UEEm#z9V50aKRg?&x+spR)%h7;K^IqTQE_ zC>5W`Yj`mV8cMD=DBKsOyEB>RwDR0ug5@Gw|Nd&_v*>&R%aFC@H(@PV{qduOxvq-3 z4l6uKzp|Ht*D~JBJqlIUshOGVpusr#7e;w0IO6%%Ine%) z15m{48N4&4ee9AM#9`)yv3**(nKDYa)5@Km)5`nVCF=<`R}e>y_w}xKhOehNyKc_1 zuVI^Sq8Hd^wdaqs(|t`nYdy8ITVTNd(SZ4a=C34*Dd&_^__P1QLTme*xc)ljo5!%w zseMKCPMkP#;>3v)Cr+F=apJ^@6DLlbIQdil0MMkZ?jm*9iU0rr07*qoM6N<$f@%^+ AdH?_b diff --git a/resources/android/ic_question_answer/drawable-hdpi-v11/ic_question_answer.png b/resources/android/ic_question_answer/drawable-hdpi-v11/ic_question_answer.png deleted file mode 100644 index 3ae9173bdd553eedb13988839bfa12a6f82283b1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 318 zcmV-E0m1%>P)l?jFF&)lE8j0B3PWXGYw@u7XO*ApdRf^Nn5uDDx>C`^^FzK2WX#Aamse+ z!Uke^yDDf#FN=K;p^AjMIssD7sEV=+(ol&LAnoyWUHXq84|>7>QOWeY0G=nP)bo?- Q;Q#;t07*qoM6N<$f*F^CHUIzs diff --git a/resources/android/ic_question_answer/drawable-hdpi-v9/ic_question_answer.png b/resources/android/ic_question_answer/drawable-hdpi-v9/ic_question_answer.png deleted file mode 100644 index 3d580d05ff1bf03248c59ad96cba3e6bd9ebff6e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 236 zcmVCF5JmIp*=T&B3qj)-HT|lomP%7v|6%0D`Sb zV2lB!R5R&bl>k$osS-#j0n23q?>!`(b6XcdK5MPNrxHQ{mQu&{0?s*LDRumm;8rDI zj6r5umKpQ8*>@a1#Fn25V(~}0wH8`=bv!8v=7JE->A3Nwt*f9lgpiq13YjiTYcJ`x m8IvReUDt1tjGvH^k#`=!sa_-i diff --git a/resources/android/ic_question_answer/drawable-hdpi/ic_question_answer.png b/resources/android/ic_question_answer/drawable-hdpi/ic_question_answer.png deleted file mode 100644 index 4ddd1ed8b33fcc3a67b969812ee55c8bce4d1057..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 895 zcmeAS@N?(olHy`uVBq!ia0vp^Y9P$P1|(P5zFfz^!0hJf;uunK>+KA0@2o(HHtD&E zDHFW{FS2$wu{jE6UijG-SfJyYuXt$%Gwzg@Iwx2w2$QfN8{Ag)iV4Et?r@OlMDJ5)9N={CGQnq{PoO$yuUA`>ry7;31^H;CBu3fuUGWR7@*|O@M zWy_W=v9YteH2>W1-`2A7@~^EfvjWX4DGAY8X;$&vjV&oFeOJTTuHK%W38$Yl zA#?Gf`Q0~{Ss=`Jyjf2)LB5~Dw<96-X@uOtRZ9c0|kPWf1vZf{`9+vj@%U`~HdFA!q zy|zYH+qZAG0!m8Q$y~X1&FkR5MYVf(?$ne^?>Te!Y;PYhSi&MBf8O>t{qg$djT-{e z&)kd4%G^Fy#0UjfA51%X>h$TOto7Cm$<4h-lUCM0dSwcX@TE(ahJ=Mpn;H@x-tGJC z?OWfDmCu1{*Mwv+G~{{QrcDLOoUPxRXlalnkl;OXk; Jvd$@?2>`KctTq4u diff --git a/resources/android/ic_question_answer/drawable-mdpi-v11/ic_question_answer.png b/resources/android/ic_question_answer/drawable-mdpi-v11/ic_question_answer.png deleted file mode 100644 index f21a94577c984e9cc5fe22ec3b5b5a5147570413..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 238 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjTRmMILn>}1CrFq*`2YXEkHZ>< zBezr%d=;LlU1OT?aQ(l@Z2y=FSvQJh{8#7qk(Co;$kbQJV|bLJ@N7XVYw*H9_6trd zU&^Ymz&499RObi7*@_<@j1~G+k2hU*&~Z)*bl$?M-VnhVRbnxr;ks}#>c(#MY=!&0t1*)FP+T itETgIM7H@SGchnqnnrwCcYF@e0}P(7elF{r5}E)bcUS!Y diff --git a/resources/android/ic_question_answer/drawable-mdpi-v9/ic_question_answer.png b/resources/android/ic_question_answer/drawable-mdpi-v9/ic_question_answer.png deleted file mode 100644 index ccc3c7f0a94480b006503f94628145245f0090de..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 265 zcmV+k0rvihP)X1^@s6IQ*`u0002cNkl|42C&D57C~2gD~_qUBVlI0^%WcZ{>@1?g`xALs$Y>q8&P5=~FBv{*ow$AUNVr zVjRcoG)=Sx);uv@76onFQqwf7(f56{D8M9@Wyu<2ffUDa9BMs-yGK~-3GS}A z>pI>-`oR-eYfmmulH>(g1ARfe+TIv*!)fJJX!o}xlCK03Xr}P^4gDxSEZbQH<-g$d P00000NkvXXu0mjfGrerw diff --git a/resources/android/ic_question_answer/drawable-mdpi/ic_question_answer.png b/resources/android/ic_question_answer/drawable-mdpi/ic_question_answer.png deleted file mode 100644 index a5943266e768a7cdcb5ae8651b90607587b68953..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 631 zcmV--0*L*IP)P000>X1^@s6#OZ}&0006$Nkl(2-83`P?b!JGbx<4u^^9 zbSk(6uYI%Ggw19HjYflappPl>`Fs!v1c=FGBDf?Pjlys^q&em|2l|*2m&-*5koWg@ z!6m_95c>T-%`wM0(8rWG91d{1-Nbl27F^=@`=Q(I(j0S~1AR=1)oO*mPbq=7rjW7U z@4@5oz}wp!ZDWpeppPkeczB=#$Y3y_(003pVzEdM*EwRb7}#t!Xt&#RKIS+F`j`@v z$pmJzndtR;u-onE4k?vNP%f9h<@58CN-)Pc(8rV*3g{wdJTuefl6LqU-=yRm=dK@32L?azrk1g%gYND z;o5Nf(Z`g?<#JFc6c?dVso?qf8CtCtzYl#(iBu{DnM`&uD0VuX{5JZS5|Ky*cXxLn z7K^V2J?LXfG#brsi^T#Gi3DzMZ!ZS(=t1APkJhhBrSgRztZP9J`hMP@&>z?&44JB~ Ra|Zwb002ovPDHLkV1nebD@yFVk1214BWrKyp#-D6lzgtI5lSJKn z&6&=rZ>hUEpSor-6N|~n$V6|HQc2W6UF2D;gQ_TQ8lab-p}Vp>H#9{B(}1pn<^717 zX@IBPPF(;35I{r#`gh>$f+ncU6`<#g+MEG;JOKzm00Izz0Kx`%pbe_H1RT)9P;=Bn zDOZ3k8d`p#U=6U=%cBHGz>1bnJ9wpI9I&M2v%d3@#l|K9diep$1fq4oL2zJ2!bh$vY%_As3mAr|<@D15#E&GBjfbZKp^^tRH^T zB&7eZE1m9Svk{3zB9UlR6h#+p+uk)zV=#Ezb=|cV0EL%j`BoGKFnFlz`bi4_!;LTu zAqWB%r)k=218{|tlO*|L09lq9zV8E<&k6u1WM5SkjDF4Y{B}_QYES2JqJLo4!)gFz z%9;i6JP)`W$MGJ3+Uhj{I11ppE^rwq+a7@0>a~9XFyLzVs0Dzpis3j89C&BjwxMeb z+2Zj^1yK8?tZxCZlOA`p8;);{-vI2m!nFYoM~>?@oNC$~e*}Qf?aRpV9B_LUfP&}B zjyDX8034HLSuflWS^%fYG|gum#5)=$FA9K<^>Tz*2Vkxes{l-uK+`_4Jb+pWTo>+F k1E5Nf{&-&ssI2007*qoM6N<$f{j0(;{X5v diff --git a/resources/android/ic_question_answer/drawable-xhdpi/ic_question_answer.png b/resources/android/ic_question_answer/drawable-xhdpi/ic_question_answer.png deleted file mode 100644 index c2b8a6368ba8ed857670b6abc30377b29b20e3ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1218 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2o;fuylI5IEGZ*dOPc+mx{MY+i%7r zkuF??VRHfs7r6*N7ks{O&Z$a=5El`V!lg|eU0lC7lsfz$x!u`Ocyo2ru_C6U5AXcq zzkWd-v}3>%|i%3Z7=N?|(km$Rj*loR^R9(uE5XQXAvT%gr^LEoAtlBqTQ2 z0L^&$=FO9CuO<8U&reHB%gD@p=^YUhv*hK=%z5G4<_Uf<+2yuk`}Xj|SFVV}#>HKF z{aU-$b?=@%7mglfo&R~$*Pe#uMjqMO+Vks`xVgFG#UAZ7xPRnGONp&A$SHU3oOwR~ z#GgMlGXs;8lP^J3CnqbHmXu`7m_6HAmBUZ>W|iB4%P-na+*UKPw$Anv*b)R136@;4 zZe8DYUl!TZJxJS6;1*9~E?JJxk2SK-8i_Obr%w3ur$$Rh=gNnT zwsolsnG@?|QrAX4V*&=$q}j7iXS$}m{g}VsATBnxQ2OYfty{NVx~2nm?#XlKo^4Uu z@wuzCy!?3iqqAqv`fuL2@!~4G6 zn?KG61qLeSo~?TP5NOT3d47?Rky5vA-Kx`nP_{d>ynOnnPoI_~|7R&t+PGoay$WEk z0Yf#&&8@BN`?nuIew?sw3}#xte$AQ-H*emIFRrW8%Z2*o$&(Z>Z|}C?%pn7%yo8k zExK{zMn0=-Mt*+(ym|Bf3A20`+xs1u8dQvoCM{aLc>mlZM_lq=nwy*NZ+@_Dy}2CF zShLx^?3K)0uFRb)TUA&0OrfIUk~B9kJ~}!$>Sfe?svL`RBRqH|j04klFu% zr`P_{Z4~@VaocY6_*>CyfZM$|=?b)~Q-(}{=j`<&&=D+_RT=UiUD}T(Z WwnvY-U)%teO$?r{elF{r5}E)K;zwKn diff --git a/resources/android/ic_question_answer/drawable-xxhdpi-v11/ic_question_answer.png b/resources/android/ic_question_answer/drawable-xxhdpi-v11/ic_question_answer.png deleted file mode 100644 index 2586cd25db2bb2d923f489356fa5c56d56f5957b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 509 zcmV;3a?)f|vHdccooT0y1|ftHLI?>a&-14ASlX7R(#%oyp3bEQF@Xx4 zv2>RE%0&7oy^IM|;CRiuv>OvBbvSb7^C^9f36$mj(*p<~fB*srAb@HGB-d{P+mqB5 zzel=_syk3G@ZOniQSKE9pz`SUsSQwhbR?w!ElvgyKmY**5I_I{1Q0*~0R#|0n~46h z+5xB>rz^dy0?FkX~;&KRy@*=*>os&N+L5AL@FJI^8xH9 zr}cZz0qFZfw^n_W=l)N`t{>=+n0l;a0#G@Sp1ILuCF6>Wq^$!r4JqdTI&{8#aS7yP z^?Wk-4q&`69P^X480BG?#E(2(_Ezn0(EA9jdLI@#*kZ`^Lp)u;_`+pyu00000NkvXXu0mjfg^SpE diff --git a/resources/android/ic_question_answer/drawable-xxhdpi-v9/ic_question_answer.png b/resources/android/ic_question_answer/drawable-xxhdpi-v9/ic_question_answer.png deleted file mode 100644 index e80a4e042cf3c8247bb276ced0f6638a2759f006..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 357 zcmeAS@N?(olHy`uVBq!ia0vp^20-l1!3HGFp0l(vFfi(Px;TbZ+F=$=RT3s-mums#*G5mm4x}&6xK*Z1Fb}wS?Vf!tXfZ`jo7nwluEX`xFSEAg zUf=h=){N=*>sO|$S7jw?Z}8k8cHvjpB@mYZ$G)Q#!N9+D+t-QE_>6&^{gqGRcLP-j)rbKTI73It+MT>gcrTyjQv5({)ZIqIY+n%8j#c xz6t5D_j@_Dh1~G-H|M;joS@q1XaVtW!ed7MHBGL*F_Tn50-mmZF6*2UngC|cjHLho diff --git a/resources/android/ic_question_answer/drawable-xxhdpi/ic_question_answer.png b/resources/android/ic_question_answer/drawable-xxhdpi/ic_question_answer.png deleted file mode 100644 index 799f0e8bad39bb70ca1dfcac4a7068d8796bd3c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmeAS@N?(olHy`uVBq!ia0vp^-XP4u1|%)~s$MZLFxGjxIEGZ*dV6!P7qcUS>%#^Y zT^8+)0*e;4PL1dkxVWM>H}25$4I2d3)G%!f&e`Il7~SEY$Pp1C{V~4fBcD$~zYSa6JhksC<8G$Y z{HMjh%HV`$^4<*54VS8~p4+-I@nBZA`8(b()yHqGGPB}|$o-y^Z~gc+C*NyL{uKfn z=gD3P^Sdt0b8U9>t)994xA%YNDHmkAHoZB@f6nzY2_^v%Vp(fXTSjoMG0uJ3U%Vkn zqFT&Lno(Oe+e!O>aa`&5n1srNa~bTnlp|h7q_T(k+GxJ}&bRgVf{jLh0<>=Jy9RV# z&b>y{gP)kA3lho`em3fsF*2WKUc(udAAeFTV(;G<^A|m=kKWkLA_xVdE9JfB%xRHu SvNQok2!p4qpUXO@geCxd=CM)$ diff --git a/resources/minus.gif b/resources/minus.gif deleted file mode 100644 index 0115810b967dea899c60d902875799227699703e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4635 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 E2mOAAEdT%j diff --git a/resources/plus.gif b/resources/plus.gif deleted file mode 100644 index 6879c87437a5e0f5e79cf1227dd074e178f87a69..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4633 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 div { --accent-dark: hsl(200 100% 30%); } -body.platform-ios { - padding-top: calc(env(safe-area-inset-top) / 2); - margin: 0 10px 0 0; -} - .view-container.tab-content { height: auto !important; bottom: 50px !important; @@ -746,15 +738,6 @@ timestamp-badge[light-bg] { padding: 5% 10%; } -svg { - display: block; -} -#chart, #chart svg { - margin-right: 10px; -} -.nvd3, nv-noData { - font-weight: 300 !important; -} .metric-datepicker { /*height: 33px;*/ display: flex; /* establish flex container */ diff --git a/www/i18n/en.json b/www/i18n/en.json index 758960ade..3299a2207 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -12,7 +12,7 @@ }, "control":{ - "profile": "Profile", + "profile-tab": "Profile", "edit-demographics": "Edit Demographics", "tracking": "Tracking", "app-status": "App Status", @@ -36,7 +36,9 @@ "nuke-all": "Nuke all buffers and cache", "test-notification": "Test local notification", "check-log": "Check log", + "log-title" : "Log", "check-sensed-data": "Check sensed data", + "sensed-title": "Sensed Data: Transitions", "collection": "Collection", "sync": "Sync", "button-accept": "I accept", @@ -71,6 +73,7 @@ }, "metrics":{ + "dashboard-tab": "Dashboard", "cancel": "Cancel", "confirm": "Confirm", "get": "Get", @@ -101,13 +104,16 @@ "less-than": " less than ", "less": " less ", "week-before": "vs. week before", + "this-week": "this week", "pick-a-date": "Pick a date", "trips": "trips", "hours": "hours", + "minutes": "minutes", "custom": "Custom" }, "diary": { + "label-tab": "Label", "distance-in-time": "{{distance}} {{distsuffix}} in {{time}}", "distance": "Distance", "time": "Time", @@ -140,46 +146,42 @@ "no-travel-hint": "To see more, change the filters above or go record some travel!" }, - "user-gender": "Gender", - "gender-male": "Male", - "gender-female": "Female", - "user-height": "Height", - "user-weight": "Weight", - "user-age": "Age", - "main-metrics":{ - "dashboard": "Dashboard", "summary": "My Summary", "chart": "Chart", "change-data": "Change dates:", - "distance": "My Distance", - "trips": "My Trips", - "duration": "My Duration", + "distance": "Distance", + "trips": "Trips", + "duration": "Duration", "fav-mode": "My Favorite Mode", "speed": "My Speed", "footprint": "My Footprint", + "estimated-emissions": "Estimated CO₂ emissions", "how-it-compares": "Ballpark comparisons", - "optimal": "Optimal (perfect mode choice for all my trips):", - "average": "Average for group:", - "avoided": "CO₂ avoided (vs. all 'taxi'):", + "optimal": "Optimal (perfect mode choice for all my trips)", + "average": "Group Avg.", + "worst-case": "Worse Case", "label-to-squish": "Label trips to collapse the range into a single number", + "range-uncertain-footnote": "²Due to the uncertainty of unlabeled trips, estimates may fall anywhere within the shown range. Label more trips for richer estimates.", "lastweek": "My last week value:", - "us-2030-goal": "US 2030 Goal Estimate:", - "us-2050-goal": "US 2050 Goal Estimate:", - "calories": "My Calories", - "calibrate": "Calibrate", + "us-2030-goal": "2030 Guideline¹", + "us-2050-goal": "2050 Guideline¹", + "us-goals-footnote": "¹Guidelines based on US decarbonization goals, scaled to per-capita travel-related emissions.", + "past-week" : "Past Week", + "prev-week" : "Prev. Week", "no-summary-data": "No summary data", "mean-speed": "My Average Speed", - "equals-cookies_one": "Equals at least {{count}} homemade chocolate chip cookie", - "equals-cookies_other": "Equals at least {{count}} homemade chocolate chip cookies", - "equals-icecream_one": "Equals at least {{count}} half cup vanilla ice cream", - "equals-icecream_other": "Equals at least {{count}} half cups vanilla ice cream", - "equals-bananas_one": "Equals at least {{count}} banana", - "equals-bananas_other": "Equals at least {{count}} bananas" - }, - - "main-inf-scroll" : { - "tab": "Label" + "user-totals": "My Totals", + "group-totals": "Group Totals", + "active-minutes": "Active Minutes", + "weekly-active-minutes": "Weekly minutes of active travel", + "daily-active-minutes": "Daily minutes of active travel", + "active-minutes-table": "Table of active minutes metrics", + "weekly-goal": "Weekly Goal³", + "weekly-goal-footnote": "³Weekly goal based on CDC recommendation of 150 minutes of moderate activity per week.", + "labeled": "Labeled", + "unlabeled": "Unlabeled²", + "footprint-label": "Footprint (kg CO₂)" }, "details":{ @@ -222,6 +224,7 @@ }, "intro": { + "proceed": "Proceed", "appstatus": { "fix": "Fix", "refresh":"Refresh", @@ -327,22 +330,23 @@ "enketo-timestamps-invalid": "The times you entered are invalid. Please ensure that the start time is before the end time." }, "join": { - "welcome-to-nrel-openpath": "Welcome to NREL OpenPATH", - "proceed-further": "To proceed further, you need to enter a valid OPcode (token)", - "what-is-opcode": "The OPcode is a long string starting with 'nrelop' that has been provided by your program admin through a website, email, text or printout.", - "or": "or", - "scan-button": "Scan the QR code ", - "scan-details": "The OPcode will be written at the top of the image", - "paste-button": "Paste the OPcode", - "paste-details": "We suggest copy-pasting instead of typing since the OPcode is long and jumbled", + "welcome-to-app": "Welcome to {{appName}}!", + "app-name": "NREL OpenPATH", + "to-proceed-further": "To proceed further, please scan or paste an access code that has been provided by your program administrator through a website, email, text message, or printout.", + "code-hint": "The code begins with ‘nrelop’ and may be in barcode or text format.", + "scan-code": "Scan code", + "paste-code": "Paste code", + "scan-hint": "Scan the barcode with your phone camera", + "paste-hint": "Or, paste the code as text", + "about-app-title": "About {{appName}}", "about-app-para-1": "The National Renewable Energy Laboratory’s Open Platform for Agile Trip Heuristics (NREL OpenPATH) enables people to track their travel modes—car, bus, bike, walking, etc.—and measure their associated energy use and carbon footprint.", "about-app-para-2": "The app empowers communities to understand their travel mode choices and patterns, experiment with options to make them more sustainable, and evaluate the results. Such results can inform effective transportation policy and planning and be used to build more sustainable and accessible cities.", "about-app-para-3": "It does so by building an automatic diary of all your trips, across all transportation modes. It reads multiple sensors, including location, in the background, and turns GPS tracking on and off automatically for minimal power consumption. The choice of the travel pattern information and the carbon footprint display style are study-specific.", + "tips-title": "Tip(s) for correct operation:", "all-green-status": "Make sure that all status checks are green", "dont-force-kill": "Do not force kill the app", "background-restrictions": "On Samsung and Huwaei phones, make sure that background restrictions are turned off", - "close": "Close", - "tips-title": "Tip(s) for correct operation:" + "close": "Close" }, "config": { "unable-read-saved-config": "Unable to read saved config", @@ -360,7 +364,9 @@ "errors": { "while-populating-composite": "Error while populating composite trips", "while-loading-another-week": "Error while loading travel of {{when}} week", - "while-loading-specific-week": "Error while loading travel for the week of {{day}}" + "while-loading-specific-week": "Error while loading travel for the week of {{day}}", + "while-log-messages": "While getting messages from the log ", + "while-max-index" : "While getting max index " }, "consent-text": { "title":"NREL OPENPATH PRIVACY POLICY/TERMS OF USE", diff --git a/www/templates/survey/enketo/enketo_bare_150x56.png b/www/img/enketo_bare_150x56.png similarity index 100% rename from www/templates/survey/enketo/enketo_bare_150x56.png rename to www/img/enketo_bare_150x56.png diff --git a/www/index.html b/www/index.html index d5d3266ad..451c3047f 100644 --- a/www/index.html +++ b/www/index.html @@ -11,19 +11,8 @@ - - - - - - - + +
diff --git a/www/index.js b/www/index.js index 578b3dc75..d91357d18 100644 --- a/www/index.js +++ b/www/index.js @@ -1,16 +1,9 @@ import './manual_lib/ionic/css/ionic.css'; import './css/style.css'; -import './css/intro.css'; -import './css/appstatus.css'; -import './css/main.recent.css'; import './css/main.diary.css'; -import './manual_lib/fontawesome/css/all.min.css'; import 'leaflet/dist/leaflet.css'; -import './js/app.js'; -import './js/config/dynamic_config.js'; -import './js/config/imperial.js'; -import './js/config/server_conn.js'; +import './js/ngApp.js'; import './js/stats/clientstats.js'; import './js/splash/referral.js'; import './js/splash/startprefs.js'; @@ -19,34 +12,22 @@ import './js/splash/storedevicesettings.js'; import './js/splash/localnotify.js'; import './js/splash/remotenotify.js'; import './js/splash/notifScheduler.js'; -import './js/join/join-ctrl.js'; import './js/controllers.js'; import './js/services.js'; import './js/i18n-utils.js'; -import './js/intro.js'; import './js/main.js'; import './js/survey/input-matcher.js'; import './js/survey/multilabel/infinite_scroll_filters.js'; import './js/survey/multilabel/multi-label-ui.js'; import './js/diary.js'; -import './js/recent.js'; import './js/diary/services.js'; -import './js/survey/external/launch.js'; import './js/survey/enketo/answer.js'; -import './js/survey/enketo/launch.js'; -import './js/survey/enketo/service.js'; import './js/survey/enketo/infinite_scroll_filters.js'; import './js/survey/enketo/enketo-trip-button.js'; -import './js/survey/enketo/enketo-demographics.js'; import './js/survey/enketo/enketo-add-note-button.js'; -import './js/metrics.js'; -import './js/control/general-settings.js'; import './js/control/emailService.js'; import './js/control/uploadService.js'; -import './js/control/collect-settings.js'; -import './js/control/sync-settings.js'; import './js/metrics-factory.js'; import './js/metrics-mappings.js'; import './js/plugin/logger.ts'; import './js/plugin/storage.js'; -import './js/appstatus/permissioncheck.js'; diff --git a/www/js/App.tsx b/www/js/App.tsx new file mode 100644 index 000000000..1ace61531 --- /dev/null +++ b/www/js/App.tsx @@ -0,0 +1,88 @@ +import React, { useEffect, useState, createContext, useMemo } from 'react'; +import { getAngularService } from './angular-react-helper'; +import { BottomNavigation, Button, useTheme } from 'react-native-paper'; +import { useTranslation } from 'react-i18next'; +import LabelTab from './diary/LabelTab'; +import MetricsTab from './metrics/MetricsTab'; +import ProfileSettings from './control/ProfileSettings'; +import useAppConfig from './useAppConfig'; +import OnboardingStack from './onboarding/OnboardingStack'; +import { OnboardingRoute, OnboardingState, getPendingOnboardingState } from './onboarding/onboardingHelper'; +import { setServerConnSettings } from './config/serverConn'; +import AppStatusModal from './control/AppStatusModal'; + +const defaultRoutes = (t) => [ + { key: 'label', title: t('diary.label-tab'), focusedIcon: 'check-bold', unfocusedIcon: 'check-outline' }, + { key: 'metrics', title: t('metrics.dashboard-tab'), focusedIcon: 'chart-box', unfocusedIcon: 'chart-box-outline' }, + { key: 'control', title: t('control.profile-tab'), focusedIcon: 'account', unfocusedIcon: 'account-outline' }, +]; + +export const AppContext = createContext({}); + +const App = () => { + + const [index, setIndex] = useState(0); + const [pendingOnboardingState, setPendingOnboardingState] = useState(null); + const [permissionsPopupVis, setPermissionsPopupVis] = useState(false); + const appConfig = useAppConfig(); + const { colors } = useTheme(); + const { t } = useTranslation(); + + const StartPrefs = getAngularService('StartPrefs'); + + const routes = useMemo(() => { + const showMetrics = appConfig?.survey_info?.['trip-labels'] == 'MULTILABEL'; + return showMetrics ? defaultRoutes(t) : defaultRoutes(t).filter(r => r.key != 'metrics'); + }, [appConfig, t]); + + const renderScene = BottomNavigation.SceneMap({ + label: LabelTab, + metrics: MetricsTab, + control: ProfileSettings, + }); + + const refreshOnboardingState = () => getPendingOnboardingState().then(setPendingOnboardingState); + useEffect(() => { refreshOnboardingState() }, []); + + useEffect(() => { + if (!appConfig) return; + setServerConnSettings(appConfig).then(() => { + refreshOnboardingState(); + }); + }, [appConfig]); + + const appContextValue = { + appConfig, + pendingOnboardingState, setPendingOnboardingState, refreshOnboardingState, + permissionsPopupVis, setPermissionsPopupVis, + } + + console.debug('pendingOnboardingState in App', pendingOnboardingState); + + return (<> + + {pendingOnboardingState == null ? + + : + + } + { /* if onboarding is done (state == null), or if is in progress but we are past the + consent page (route > CONSENT), the permissions popup can show if needed */ } + {(pendingOnboardingState == null || pendingOnboardingState.route > OnboardingRoute.CONSENT) && + + } + + ); +} + +export default App; diff --git a/www/js/app.js b/www/js/app.js deleted file mode 100644 index 2d0b1d08d..000000000 --- a/www/js/app.js +++ /dev/null @@ -1,111 +0,0 @@ -// Ionic E-Mission App - -'use strict'; - -import angular from 'angular'; -import 'angular-animate'; -import 'angular-sanitize'; -import 'angular-translate'; -import '../manual_lib/angular-ui-router/angular-ui-router.js'; -import 'angular-local-storage'; -import 'angular-translate-loader-static-files'; - -import 'moment'; -import 'moment-timezone'; -import 'chartjs-adapter-luxon'; - -import 'ionic-toast'; -import 'ionic-datepicker'; -import 'angular-simple-logger'; - -import '../manual_lib/ionic/js/ionic.js'; -import '../manual_lib/ionic/js/ionic-angular.js'; - -import initializedI18next from './i18nextInit'; -window.i18next = initializedI18next; -import 'ng-i18next'; - -angular.module('emission', ['ionic', 'jm.i18next', - 'emission.controllers','emission.services', 'emission.plugin.logger', - 'emission.splash.referral','emission.services.email', - 'emission.intro', 'emission.main', 'emission.config.dynamic', - 'emission.config.server_conn', 'emission.join.ctrl', - 'pascalprecht.translate', 'LocalStorageModule']) - -.run(function($ionicPlatform, $rootScope, $http, Logger, localStorageService, ServerConnConfig) { - console.log("Starting run"); - // ensure that plugin events are delivered after the ionicPlatform is ready - // https://github.com/katzer/cordova-plugin-local-notifications#launch-details - window.skipLocalNotificationReady = true; - // alert("Starting run"); - // BEGIN: Global listeners, no need to wait for the platform - // TODO: Although the onLaunch call doesn't need to wait for the platform the - // handlers do. Can we rely on the fact that the event is generated from - // native code, so will only be launched after the platform is ready? - // END: Global listeners - $ionicPlatform.ready(function() { - // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard - // for form inputs) - Logger.log("ionicPlatform is ready"); - - if (window.StatusBar) { - // org.apache.cordova.statusbar required - StatusBar.styleDefault(); - } - cordova.plugin.http.setDataSerializer('json'); - // backwards compat hack to be consistent with - // https://github.com/e-mission/e-mission-data-collection/commit/92f41145e58c49e3145a9222a78d1ccacd16d2a7#diff-962320754eba07107ecd413954411f725c98fd31cddbb5defd4a542d1607e5a3R160 - // remove during migration to react native - localStorageService.remove("OP_GEOFENCE_CFG"); - cordova.plugins.BEMUserCache.removeLocalStorage("OP_GEOFENCE_CFG"); - }); - console.log("Ending run"); -}) - -.config(function($stateProvider, $urlRouterProvider, $compileProvider) { - console.log("Starting config"); - // alert("config"); - - // Ionic uses AngularUI Router which uses the concept of states - // Learn more here: https://github.com/angular-ui/ui-router - // Set a few states which the app can be in. - // The 'intro' and 'diary' states are found in their respective modules - // Each state's controller can be found in controllers.js - $compileProvider.imgSrcSanitizationWhitelist(/^\s*(https?|ftp|file|blob|ionic):|data:image/); - $stateProvider - // set up a state for the splash screen. This has no parents and no children - // because it is basically just used to load the user's preferred screen. - // This cannot directly use plugins - has to check for them first. - .state('splash', { - url: '/splash', - templateUrl: 'templates/splash/splash.html', - controller: 'SplashCtrl' - }) - - // add the join screen to the list of initially defined states - // we can't put it in intro since it comes before it - // we can't put it in main because it is also a temporary screen that only - // shows up when we have no config. - // so we put it in here - .state('root.join', { - url: '/join', - templateUrl: 'templates/join/request_join.html', - controller: 'JoinCtrl' - }) - - // setup an abstract state for the root. Only children of this can be loaded - // as preferred screens, and all children of this can assume that the device - // is ready. - .state('root', { - url: '/root', - abstract: true, - template: '', - controller: 'RootCtrl' - }); - - // alert("about to fall back to otherwise"); - // if none of the above states are matched, use this as the fallback - $urlRouterProvider.otherwise('/splash'); - - console.log("Ending config"); -}); diff --git a/www/js/appTheme.ts b/www/js/appTheme.ts index 7571a564c..a8660e811 100644 --- a/www/js/appTheme.ts +++ b/www/js/appTheme.ts @@ -8,7 +8,7 @@ const AppTheme = { colors: { ...DefaultTheme.colors, primary: '#0080b9', // lch(50% 50 250) - primaryContainer: '#90ceff', // lch(80% 40 250) + primaryContainer: '#c0e2ff', // lch(88% 30 250) onPrimaryContainer: '#001e30', // lch(10% 50 250) secondary: '#c08331', // lch(60% 55 70) secondaryContainer: '#fcefda', // lch(95% 12 80) @@ -26,7 +26,8 @@ const AppTheme = { level4: '#e0f0ff', // lch(94% 50 250) level5: '#d6ebff', // lch(92% 50 250) }, - success: '#38872e', // lch(50% 55 135) + success: '#00a665', // lch(60% 55 155) + warn: '#f8cf53', //lch(85% 65 85) danger: '#f23934' // lch(55% 85 35) }, roundness: 5, diff --git a/www/js/appstatus/PermissionsControls.tsx b/www/js/appstatus/PermissionsControls.tsx new file mode 100644 index 000000000..ded51b898 --- /dev/null +++ b/www/js/appstatus/PermissionsControls.tsx @@ -0,0 +1,65 @@ +//component to view and manage permission settings +import React, { useState } from "react"; +import { StyleSheet, ScrollView, View } from "react-native"; +import { Button, Text } from 'react-native-paper'; +import { useTranslation } from "react-i18next"; +import PermissionItem from "./PermissionItem"; +import usePermissionStatus, { refreshAllChecks } from "../usePermissionStatus"; +import ExplainPermissions from "./ExplainPermissions"; +import AlertBar from "../control/AlertBar"; + +const PermissionsControls = ({ onAccept }) => { + const { t } = useTranslation(); + const [explainVis, setExplainVis] = useState(false); + const { checkList, overallStatus, error, errorVis, setErrorVis, explanationList } = usePermissionStatus(); + + return ( + <> + {t('consent.permissions')} + + {t('intro.appstatus.overall-description')} + + + {checkList?.map((lc) => + + + )} + + + + + + + + + ) +} + +const styles = StyleSheet.create({ + title: { + fontWeight: "bold", + fontSize: 22, + paddingBottom: 10 + }, + buttonBox: { + paddingHorizontal: 15, + paddingVertical: 10, + flexDirection: "row", + justifyContent: "space-evenly" + } + }); + +export default PermissionsControls; \ No newline at end of file diff --git a/www/js/appstatus/permissioncheck.js b/www/js/appstatus/permissioncheck.js deleted file mode 100644 index 84067a701..000000000 --- a/www/js/appstatus/permissioncheck.js +++ /dev/null @@ -1,429 +0,0 @@ -/* - * Directive to enable the permissions required for the app to function properly. - */ - -import angular from 'angular'; - -angular.module('emission.appstatus.permissioncheck', - []) -.directive('permissioncheck', function() { - return { - scope: { - overallstatus: "=", - }, - controller: "PermissionCheckControl", - templateUrl: "templates/appstatus/permissioncheck.html" - }; -}). -controller("PermissionCheckControl", function($scope, $element, $attrs, - $ionicPlatform, $ionicPopup, $window) { - console.log("PermissionCheckControl initialized with status "+$scope.overallstatus); - - $scope.setupLocChecks = function(platform, version) { - if (platform.toLowerCase() == "android") { - return $scope.setupAndroidLocChecks(version); - } else if (platform.toLowerCase() == "ios") { - return $scope.setupIOSLocChecks(version); - } else { - alert("Unknown platform, no tracking"); - } - } - - $scope.setupFitnessChecks = function(platform, version) { - if (platform.toLowerCase() == "android") { - return $scope.setupAndroidFitnessChecks(version); - } else if (platform.toLowerCase() == "ios") { - return $scope.setupIOSFitnessChecks(version); - } else { - alert("Unknown platform, no tracking"); - } - } - - $scope.setupNotificationChecks = function(platform, version) { - return $scope.setupAndroidNotificationChecks(version); - } - - $scope.setupBackgroundRestrictionChecks = function(platform, version) { - if (platform.toLowerCase() == "android") { - $scope.backgroundUnrestrictionsNeeded = true; - return $scope.setupAndroidBackgroundRestrictionChecks(version); - } else if (platform.toLowerCase() == "ios") { - $scope.backgroundUnrestrictionsNeeded = false; - $scope.overallBackgroundRestrictionStatus = true; - $scope.backgroundRestrictionChecks = []; - return true; - } else { - alert("Unknown platform, no tracking"); - } - } - - let iconMap = (statusState) => statusState? "✅" : "❌"; - let classMap = (statusState) => statusState? "status-green" : "status-red"; - - $scope.recomputeOverallStatus = function() { - $scope.overallstatus = $scope.overallLocStatus - && $scope.overallFitnessStatus - && $scope.overallNotificationStatus - && $scope.overallBackgroundRestrictionStatus; - } - - $scope.recomputeLocStatus = function() { - $scope.locChecks.forEach((lc) => { - lc.statusIcon = iconMap(lc.statusState); - lc.statusClass = classMap(lc.statusState) - }); - $scope.overallLocStatus = $scope.locChecks.map((lc) => lc.statusState).reduce((pv, cv) => pv && cv); - console.log("overallLocStatus = "+$scope.overallLocStatus+" from ", $scope.locChecks); - $scope.overallLocStatusIcon = iconMap($scope.overallLocStatus); - $scope.overallLocStatusClass = classMap($scope.overallLocStatus); - $scope.recomputeOverallStatus(); - } - - $scope.recomputeFitnessStatus = function() { - $scope.fitnessChecks.forEach((fc) => { - fc.statusIcon = iconMap(fc.statusState); - fc.statusClass = classMap(fc.statusState) - }); - $scope.overallFitnessStatus = $scope.fitnessChecks.map((fc) => fc.statusState).reduce((pv, cv) => pv && cv); - console.log("overallFitnessStatus = "+$scope.overallFitnessStatus+" from ", $scope.fitnessChecks); - $scope.overallFitnessStatusIcon = iconMap($scope.overallFitnessStatus); - $scope.overallFitnessStatusClass = classMap($scope.overallFitnessStatus); - $scope.recomputeOverallStatus(); - } - - $scope.recomputeNotificationStatus = function() { - $scope.notificationChecks.forEach((nc) => { - nc.statusIcon = iconMap(nc.statusState); - nc.statusClass = classMap(nc.statusState) - }); - $scope.overallNotificationStatus = $scope.notificationChecks.map((nc) => nc.statusState).reduce((pv, cv) => pv && cv); - console.log("overallNotificationStatus = "+$scope.overallNotificationStatus+" from ", $scope.notificationChecks); - $scope.overallNotificationStatusIcon = iconMap($scope.overallNotificationStatus); - $scope.overallNotificationStatusClass = classMap($scope.overallNotificationStatus); - $scope.recomputeOverallStatus(); - } - - $scope.recomputeBackgroundRestrictionStatus = function() { - if (!$scope.backgroundRestrictionChecks) return; - $scope.backgroundRestrictionChecks.forEach((brc) => { - brc.statusIcon = iconMap(brc.statusState); - brc.statusClass = classMap(brc.statusState) - }); - $scope.overallBackgroundRestrictionStatus = $scope.backgroundRestrictionChecks.map((nc) => nc.statusState).reduce((pv, cv) => pv && cv); - console.log("overallBackgroundRestrictionStatus = "+$scope.overallBackgroundRestrictionStatus+" from ", $scope.backgroundRestrictionChecks); - $scope.overallBackgroundRestrictionStatusIcon = iconMap($scope.overallBackgroundRestrictionStatus); - $scope.overallBackgroundRestrictionStatusClass = classMap($scope.overallBackgroundRestrictionStatus); - $scope.recomputeOverallStatus(); - } - - let checkOrFix = function(checkObj, nativeFn, recomputeFn, showError=true) { - return nativeFn() - .then((status) => { - console.log("availability ", status) - $scope.$apply(() => { - checkObj.statusState = true; - recomputeFn(); - }); - return status; - }).catch((error) => { - console.log("Error", error) - if (showError) { - $ionicPopup.alert({ - title: "Error", - template: "
"+error+"
", - okText: "Please fix again" - }); - }; - $scope.$apply(() => { - checkObj.statusState = false; - recomputeFn(); - }); - return error; - }); - } - - let refreshChecks = function(checksList, recomputeFn) { - // without this, even if the checksList is [] - // the reduce in the recomputeFn fails because it is called on a zero - // length array without a default value - // we should be able to also specify a default value of True - // but I don't want to mess with that at this last minute - if (!checksList || checksList.length == 0) { - return Promise.resolve(true); - } - let checkPromises = checksList?.map((lc) => lc.refresh()); - console.log(checkPromises); - return Promise.all(checkPromises) - .then((result) => recomputeFn()) - .catch((error) => recomputeFn()) - } - - $scope.setupAndroidLocChecks = function(platform, version) { - let fixSettings = function() { - console.log("Fix and refresh location settings"); - return checkOrFix(locSettingsCheck, $window.cordova.plugins.BEMDataCollection.fixLocationSettings, - $scope.recomputeLocStatus, true); - }; - let checkSettings = function() { - console.log("Refresh location settings"); - return checkOrFix(locSettingsCheck, $window.cordova.plugins.BEMDataCollection.isValidLocationSettings, - $scope.recomputeLocStatus, false); - }; - let fixPerms = function() { - console.log("fix and refresh location permissions"); - return checkOrFix(locPermissionsCheck, $window.cordova.plugins.BEMDataCollection.fixLocationPermissions, - $scope.recomputeLocStatus, true).then((error) => locPermissionsCheck.desc = error); - }; - let checkPerms = function() { - console.log("fix and refresh location permissions"); - return checkOrFix(locPermissionsCheck, $window.cordova.plugins.BEMDataCollection.isValidLocationPermissions, - $scope.recomputeLocStatus, false); - }; - var androidSettingsDescTag = "intro.appstatus.locsettings.description.android-gte-9"; - if (version < 9) { - androidSettingsDescTag = "intro.appstatus.locsettings.description.android-lt-9"; - } - var androidPermDescTag = "intro.appstatus.locperms.description.android-gte-12"; - if($scope.osver < 6) { - androidPermDescTag = 'intro.appstatus.locperms.description.android-lt-6'; - } else if ($scope.osver < 10) { - androidPermDescTag = "intro.appstatus.locperms.description.android-6-9"; - } else if ($scope.osver < 11) { - androidPermDescTag= "intro.appstatus.locperms.description.android-10"; - } else if ($scope.osver < 12) { - androidPermDescTag= "intro.appstatus.locperms.description.android-11"; - } - console.log("description tags are "+androidSettingsDescTag+" "+androidPermDescTag); - // location settings - let locSettingsCheck = { - name: i18next.t("intro.appstatus.locsettings.name"), - desc: i18next.t(androidSettingsDescTag), - statusState: false, - fix: fixSettings, - refresh: checkSettings - } - let locPermissionsCheck = { - name: i18next.t("intro.appstatus.locperms.name"), - desc: i18next.t(androidPermDescTag), - statusState: false, - fix: fixPerms, - refresh: checkPerms - } - $scope.locChecks = [locSettingsCheck, locPermissionsCheck]; - refreshChecks($scope.locChecks, $scope.recomputeLocStatus); - } - - $scope.setupIOSLocChecks = function(platform, version) { - let fixSettings = function() { - console.log("Fix and refresh location settings"); - return checkOrFix(locSettingsCheck, $window.cordova.plugins.BEMDataCollection.fixLocationSettings, - $scope.recomputeLocStatus, true); - }; - let checkSettings = function() { - console.log("Refresh location settings"); - return checkOrFix(locSettingsCheck, $window.cordova.plugins.BEMDataCollection.isValidLocationSettings, - $scope.recomputeLocStatus, false); - }; - let fixPerms = function() { - console.log("fix and refresh location permissions"); - return checkOrFix(locPermissionsCheck, $window.cordova.plugins.BEMDataCollection.fixLocationPermissions, - $scope.recomputeLocStatus, true).then((error) => locPermissionsCheck.desc = error); - }; - let checkPerms = function() { - console.log("fix and refresh location permissions"); - return checkOrFix(locPermissionsCheck, $window.cordova.plugins.BEMDataCollection.isValidLocationPermissions, - $scope.recomputeLocStatus, false); - }; - var iOSSettingsDescTag = "intro.appstatus.locsettings.description.ios"; - var iOSPermDescTag = "intro.appstatus.locperms.description.ios-gte-13"; - if($scope.osver < 13) { - iOSPermDescTag = 'intro.appstatus.locperms.description.ios-lt-13'; - } - console.log("description tags are "+iOSSettingsDescTag+" "+iOSPermDescTag); - // location settings - let locSettingsCheck = { - name: i18next.t("intro.appstatus.locsettings.name"), - desc: i18next.t(iOSSettingsDescTag), - statusState: false, - fix: fixSettings, - refresh: checkSettings - } - let locPermissionsCheck = { - name: i18next.t("intro.appstatus.locperms.name"), - desc: i18next.t(iOSPermDescTag), - statusState: false, - fix: fixPerms, - refresh: checkPerms - } - $scope.locChecks = [locSettingsCheck, locPermissionsCheck]; - refreshChecks($scope.locChecks, $scope.recomputeLocStatus); - } - - $scope.setupAndroidFitnessChecks = function(platform, version) { - $scope.fitnessPermNeeded = ($scope.osver >= 10); - - let fixPerms = function() { - console.log("fix and refresh fitness permissions"); - return checkOrFix(fitnessPermissionsCheck, $window.cordova.plugins.BEMDataCollection.fixFitnessPermissions, - $scope.recomputeFitnessStatus, true).then((error) => fitnessPermissionsCheck.desc = error); - }; - let checkPerms = function() { - console.log("fix and refresh fitness permissions"); - return checkOrFix(fitnessPermissionsCheck, $window.cordova.plugins.BEMDataCollection.isValidFitnessPermissions, - $scope.recomputeFitnessStatus, false); - }; - - let fitnessPermissionsCheck = { - name: i18next.t("intro.appstatus.fitnessperms.name"), - desc: i18next.t("intro.appstatus.fitnessperms.description.android"), - fix: fixPerms, - refresh: checkPerms - } - $scope.overallFitnessName = i18next.t("intro.appstatus.overall-fitness-name-android"); - $scope.fitnessChecks = [fitnessPermissionsCheck]; - refreshChecks($scope.fitnessChecks, $scope.recomputeFitnessStatus); - } - - $scope.setupIOSFitnessChecks = function(platform, version) { - $scope.fitnessPermNeeded = true; - - let fixPerms = function() { - console.log("fix and refresh fitness permissions"); - return checkOrFix(fitnessPermissionsCheck, $window.cordova.plugins.BEMDataCollection.fixFitnessPermissions, - $scope.recomputeFitnessStatus, true).then((error) => fitnessPermissionsCheck.desc = error); - }; - let checkPerms = function() { - console.log("fix and refresh fitness permissions"); - return checkOrFix(fitnessPermissionsCheck, $window.cordova.plugins.BEMDataCollection.isValidFitnessPermissions, - $scope.recomputeFitnessStatus, false); - }; - - let fitnessPermissionsCheck = { - name: i18next.t("intro.appstatus.fitnessperms.name"), - desc: i18next.t("intro.appstatus.fitnessperms.description.ios"), - fix: fixPerms, - refresh: checkPerms - } - $scope.overallFitnessName = i18next.t("intro.appstatus.overall-fitness-name-ios"); - $scope.fitnessChecks = [fitnessPermissionsCheck]; - refreshChecks($scope.fitnessChecks, $scope.recomputeFitnessStatus); - } - - $scope.setupAndroidNotificationChecks = function() { - let fixPerms = function() { - console.log("fix and refresh notification permissions"); - return checkOrFix(appAndChannelNotificationsCheck, $window.cordova.plugins.BEMDataCollection.fixShowNotifications, - $scope.recomputeNotificationStatus, true); - }; - let checkPerms = function() { - console.log("fix and refresh notification permissions"); - return checkOrFix(appAndChannelNotificationsCheck, $window.cordova.plugins.BEMDataCollection.isValidShowNotifications, - $scope.recomputeNotificationStatus, false); - }; - let appAndChannelNotificationsCheck = { - name: i18next.t("intro.appstatus.notificationperms.app-enabled-name"), - desc: i18next.t("intro.appstatus.notificationperms.description.android-enable"), - fix: fixPerms, - refresh: checkPerms - } - $scope.notificationChecks = [appAndChannelNotificationsCheck]; - refreshChecks($scope.notificationChecks, $scope.recomputeNotificationStatus); - } - - $scope.setupAndroidBackgroundRestrictionChecks = function() { - let fixPerms = function() { - console.log("fix and refresh backgroundRestriction permissions"); - return checkOrFix(unusedAppsUnrestrictedCheck, $window.cordova.plugins.BEMDataCollection.fixUnusedAppRestrictions, - $scope.recomputeBackgroundRestrictionStatus, true); - }; - let checkPerms = function() { - console.log("fix and refresh backgroundRestriction permissions"); - return checkOrFix(unusedAppsUnrestrictedCheck, $window.cordova.plugins.BEMDataCollection.isUnusedAppUnrestricted, - $scope.recomputeBackgroundRestrictionStatus, false); - }; - let fixBatteryOpt = function() { - console.log("fix and refresh battery optimization permissions"); - return checkOrFix(ignoreBatteryOptCheck, $window.cordova.plugins.BEMDataCollection.fixIgnoreBatteryOptimizations, - $scope.recomputeBackgroundRestrictionStatus, true); - }; - let checkBatteryOpt = function() { - console.log("fix and refresh battery optimization permissions"); - return checkOrFix(ignoreBatteryOptCheck, $window.cordova.plugins.BEMDataCollection.isIgnoreBatteryOptimizations, - $scope.recomputeBackgroundRestrictionStatus, false); - }; - var androidUnusedDescTag = "intro.appstatus.unusedapprestrict.description.android-disable-gte-13"; - if ($scope.osver == 12) { - androidUnusedDescTag= "intro.appstatus.unusedapprestrict.description.android-disable-12"; - } - else if ($scope.osver < 12) { - androidUnusedDescTag= "intro.appstatus.unusedapprestrict.description.android-disable-lt-12"; - } - let unusedAppsUnrestrictedCheck = { - name: i18next.t("intro.appstatus.unusedapprestrict.name"), - desc: i18next.t(androidUnusedDescTag), - fix: fixPerms, - refresh: checkPerms - } - let ignoreBatteryOptCheck = { - name: i18next.t("intro.appstatus.ignorebatteryopt.name"), - desc: i18next.t("intro.appstatus.ignorebatteryopt.description.android-disable"), - fix: fixBatteryOpt, - refresh: checkBatteryOpt - } - $scope.backgroundRestrictionChecks = [unusedAppsUnrestrictedCheck, ignoreBatteryOptCheck]; - refreshChecks($scope.backgroundRestrictionChecks, $scope.recomputeBackgroundRestrictionStatus); - } - - $scope.setupPermissionText = function() { - if($scope.platform.toLowerCase() == "ios") { - if($scope.osver < 13) { - $scope.locationPermExplanation = i18next.t("intro.permissions.locationPermExplanation-ios-lt-13"); - } else { - $scope.locationPermExplanation = i18next.t("intro.permissions.locationPermExplanation-ios-gte-13"); - } - } - - $scope.backgroundRestricted = false; - if($window.device.manufacturer.toLowerCase() == "samsung") { - $scope.backgroundRestricted = true; - $scope.allowBackgroundInstructions = i18next.t("intro.allow_background.samsung"); - } - - console.log("Explanation = "+$scope.locationPermExplanation); - } - - $scope.checkLocationServicesEnabled = function() { - console.log("About to see if location services are enabled"); - } - $ionicPlatform.ready().then(function() { - console.log("app is launched, should refresh"); - $scope.platform = $window.device.platform; - $scope.osver = $window.device.version.split(".")[0]; - $scope.setupPermissionText(); - $scope.setupLocChecks($scope.platform, $scope.osver); - $scope.setupFitnessChecks($scope.platform, $scope.osver); - $scope.setupNotificationChecks($scope.platform, $scope.osver); - $scope.setupBackgroundRestrictionChecks($scope.platform, $scope.osver); - }); - - $ionicPlatform.on("resume", function() { - console.log("PERMISSION CHECK: app has resumed, should refresh"); - refreshChecks($scope.locChecks, $scope.recomputeLocStatus); - refreshChecks($scope.fitnessChecks, $scope.recomputeFitnessStatus); - refreshChecks($scope.notificationChecks, $scope.recomputeNotificationStatus); - refreshChecks($scope.backgroundRestrictionChecks, $scope.recomputeBackgroundRestrictionStatus); - }); - - $scope.$on("recomputeAppStatus", function(e, callback) { - console.log("PERMISSION CHECK: recomputing state"); - Promise.all([ - refreshChecks($scope.locChecks, $scope.recomputeLocStatus), - refreshChecks($scope.fitnessChecks, $scope.recomputeFitnessStatus), - refreshChecks($scope.notificationChecks, $scope.recomputeNotificationStatus), - refreshChecks($scope.backgroundRestrictionChecks, $scope.recomputeBackgroundRestrictionStatus) - ]).then( () => { - callback($scope.overallstatus) - } - ); - }); -}); diff --git a/www/js/components/ActionMenu.tsx b/www/js/components/ActionMenu.tsx new file mode 100644 index 000000000..296717a00 --- /dev/null +++ b/www/js/components/ActionMenu.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { Modal } from "react-native"; +import { Dialog, Button, useTheme } from "react-native-paper"; +import { useTranslation } from "react-i18next"; +import { settingStyles } from "../control/ProfileSettings"; + +const ActionMenu = ({vis, setVis, title, actionSet, onAction, onExit}) => { + + const { t } = useTranslation(); + const { colors } = useTheme(); + + return ( + setVis(false)} + transparent={true}> + setVis(false)} + style={settingStyles.dialog(colors.elevation.level3)}> + {title} + + {actionSet?.map((e) => + + )} + + + + + + + ) +} + +export default ActionMenu; \ No newline at end of file diff --git a/www/js/components/BarChart.tsx b/www/js/components/BarChart.tsx index 6da3d2a2b..1e957923b 100644 --- a/www/js/components/BarChart.tsx +++ b/www/js/components/BarChart.tsx @@ -1,201 +1,27 @@ +import React from "react"; +import Chart, { Props as ChartProps } from "./Chart"; +import { useTheme } from "react-native-paper"; +import { getGradient } from "./charting"; -import React, { useRef, useState } from 'react'; -import { array, string, bool } from 'prop-types'; -import { angularize } from '../angular-react-helper'; -import { View } from 'react-native'; -import { useTheme } from 'react-native-paper'; -import { Chart, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, TimeScale } from 'chart.js'; -import { Bar } from 'react-chartjs-2'; -import Annotation, { AnnotationOptions } from 'chartjs-plugin-annotation'; - -Chart.register( - CategoryScale, - LinearScale, - TimeScale, - BarElement, - Title, - Tooltip, - Legend, - Annotation, -); - -const BarChart = ({ chartData, axisTitle, lineAnnotations=null, isHorizontal=false }) => { +type Props = Omit & { + meter?: {high: number, middle: number, dash_key: string}, +} +const BarChart = ({ meter, ...rest }: Props) => { const { colors } = useTheme(); - const [ numVisibleDatasets, setNumVisibleDatasets ] = useState(1); - - const barChartRef = useRef(null); - - const defaultPalette = [ - '#c95465', // red oklch(60% 0.15 14) - '#4a71b1', // blue oklch(55% 0.11 260) - '#d2824e', // orange oklch(68% 0.12 52) - '#856b5d', // brown oklch(55% 0.04 50) - '#59894f', // green oklch(58% 0.1 140) - '#e0cc55', // yellow oklch(84% 0.14 100) - '#b273ac', // purple oklch(64% 0.11 330) - '#f09da6', // pink oklch(78% 0.1 12) - '#b3aca8', // grey oklch(75% 0.01 55) - '#80afad', // teal oklch(72% 0.05 192) - ] - - const indexAxis = isHorizontal ? 'y' : 'x'; - function getChartHeight() { - /* when horizontal charts have more data, they should get taller - so they don't look squished */ - if (isHorizontal) { - // 'ideal' chart height is based on the number of datasets and number of unique index values - const uniqueIndexVals = []; - chartData.forEach(e => e.records.forEach(r => { - if (!uniqueIndexVals.includes(r[indexAxis])) uniqueIndexVals.push(r[indexAxis]); - })); - const numIndexVals = uniqueIndexVals.length; - const idealChartHeight = numVisibleDatasets * numIndexVals * 8; - - /* each index val should be at least 20px tall for visibility, - and the graph itself should be at least 250px tall */ - const minChartHeight = Math.max(numIndexVals * 20, 250); - - // return whichever is greater - return { height: Math.max(idealChartHeight, minChartHeight) }; + if (meter) { + rest.getColorForChartEl = (chart, dataset, ctx, colorFor) => { + const darkenDegree = colorFor == 'border' ? 0.25 : 0; + const alpha = colorFor == 'border' ? 1 : 0; + return getGradient(chart, meter, dataset, ctx, alpha, darkenDegree); } - // vertical charts will just match the parent container - return { height: '100%' }; + rest.borderWidth = 3; } return ( - - ({ - label: d.label, - data: d.records, - // cycle through the default palette, repeat if necessary - backgroundColor: defaultPalette[i % defaultPalette.length], - })) - }} - options={{ - indexAxis: indexAxis, - responsive: true, - maintainAspectRatio: false, - resizeDelay: 1, - scales: { - ...(isHorizontal ? { - y: { - offset: true, - type: 'time', - adapters: { - date: { zone: 'utc' }, - }, - time: { - unit: 'day', - tooltipFormat: 'DDD', // Luxon "localized date with full month": e.g. August 6, 2014 - }, - beforeUpdate: (axis) => { - setNumVisibleDatasets(axis.chart.getVisibleDatasetCount()) - }, - reverse: true, - }, - x: { - title: { display: true, text: axisTitle }, - }, - } : { - x: { - offset: true, - type: 'time', - adapters: { - date: { zone: 'utc' }, - }, - time: { - unit: 'day', - tooltipFormat: 'DDD', // Luxon "localized date with full month": e.g. August 6, 2014 - }, - }, - y: { - title: { display: true, text: axisTitle }, - }, - }), - }, - plugins: { - ...(lineAnnotations?.length > 0 && { - annotation: { - annotations: lineAnnotations.map((a, i) => ({ - type: 'line', - label: { - display: true, - padding: { x: 3, y: 1 }, - borderRadius: 0, - backgroundColor: 'rgba(0,0,0,.7)', - color: 'rgba(255,255,255,1)', - font: { size: 10 }, - position: 'start', - content: a.label, - }, - ...(isHorizontal ? { xMin: a.value, xMax: a.value } - : { yMin: a.value, yMax: a.value }), - borderColor: colors.onBackground, - borderWidth: 2, - borderDash: [3, 3], - } satisfies AnnotationOptions)), - } - }), - } - }} /> - - ) + + ); } -BarChart.propTypes = { - chartData: array, - axisTitle: string, - lineAnnotations: array, - isHorizontal: bool, -}; - -angularize(BarChart, 'BarChart', 'emission.main.barchart'); export default BarChart; - -// const sampleAnnotations = [ -// { value: 35, label: 'Target1' }, -// { value: 65, label: 'Target2' }, -// ]; - -// const sampleChartData = [ -// { -// label: 'Primary', -// records: [ -// { x: moment('2023-06-20'), y: 20 }, -// { x: moment('2023-06-21'), y: 30 }, -// { x: moment('2023-06-23'), y: 80 }, -// { x: moment('2023-06-24'), y: 40 }, -// ], -// }, -// { -// label: 'Secondary', -// records: [ -// { x: moment('2023-06-21'), y: 10 }, -// { x: moment('2023-06-22'), y: 50 }, -// { x: moment('2023-06-23'), y: 30 }, -// { x: moment('2023-06-25'), y: 40 }, -// ], -// }, -// { -// label: 'Tertiary', -// records: [ -// { x: moment('2023-06-20'), y: 30 }, -// { x: moment('2023-06-22'), y: 40 }, -// { x: moment('2023-06-24'), y: 10 }, -// { x: moment('2023-06-25'), y: 60 }, -// ], -// }, -// { -// label: 'Quaternary', -// records: [ -// { x: moment('2023-06-22'), y: 10 }, -// { x: moment('2023-06-23'), y: 20 }, -// { x: moment('2023-06-24'), y: 30 }, -// { x: moment('2023-06-25'), y: 40 }, -// ], -// }, -// ]; diff --git a/www/js/components/Carousel.tsx b/www/js/components/Carousel.tsx new file mode 100644 index 000000000..28a31ff6a --- /dev/null +++ b/www/js/components/Carousel.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { ScrollView, View } from 'react-native'; + +type Props = { + children: React.ReactNode, + cardWidth: number, + cardMargin: number, +} +const Carousel = ({ children, cardWidth, cardMargin }: Props) => { + const numCards = React.Children.count(children); + return ( + + {React.Children.map(children, (child, i) => ( + + {child} + + ))} + + ) +}; + +export const s = { + carouselScroll: (cardMargin) => ({ + // @ts-ignore, RN doesn't recognize `scrollSnapType`, but it does work on RN Web + scrollSnapType: 'x mandatory', + paddingVertical: 10, + }), + carouselCard: (cardWidth, cardMargin, isFirst, isLast) => ({ + marginLeft: isFirst ? cardMargin : cardMargin/2, + marginRight: isLast ? cardMargin : cardMargin/2, + width: cardWidth, + scrollSnapAlign: 'center', + scrollSnapStop: 'always', + }), +}; + +export default Carousel; diff --git a/www/js/components/Chart.tsx b/www/js/components/Chart.tsx new file mode 100644 index 000000000..79c6e40e4 --- /dev/null +++ b/www/js/components/Chart.tsx @@ -0,0 +1,196 @@ + +import React, { useEffect, useRef, useState, useMemo } from 'react'; +import { View } from 'react-native'; +import { useTheme } from 'react-native-paper'; +import { Chart as ChartJS, registerables } from 'chart.js'; +import { Chart as ChartJSChart } from 'react-chartjs-2'; +import Annotation, { AnnotationOptions, LabelPosition } from 'chartjs-plugin-annotation'; +import { dedupColors, getChartHeight, darkenOrLighten } from './charting'; + +ChartJS.register(...registerables, Annotation); + +type XYPair = { x: number|string, y: number|string }; +type ChartDataset = { + label: string, + data: XYPair[], +}; + +export type Props = { + records: { label: string, x: number|string, y: number|string }[], + axisTitle: string, + type: 'bar'|'line', + getColorForLabel?: (label: string) => string, + getColorForChartEl?: (chart, currDataset: ChartDataset, ctx: ScriptableContext<'bar'|'line'>, colorFor: 'background'|'border') => string|CanvasGradient|null, + borderWidth?: number, + lineAnnotations?: { value: number, label?: string, color?:string, position?: LabelPosition }[], + isHorizontal?: boolean, + timeAxis?: boolean, + stacked?: boolean, +} +const Chart = ({ records, axisTitle, type, getColorForLabel, getColorForChartEl, borderWidth, lineAnnotations, isHorizontal, timeAxis, stacked }: Props) => { + + const { colors } = useTheme(); + const [ numVisibleDatasets, setNumVisibleDatasets ] = useState(1); + + const indexAxis = isHorizontal ? 'y' : 'x'; + const chartRef = useRef>(null); + const [chartDatasets, setChartDatasets] = useState([]); + + const chartData = useMemo>(() => { + let labelColorMap; // object mapping labels to colors + if (getColorForLabel) { + const colorEntries = chartDatasets.map(d => [d.label, getColorForLabel(d.label)] ); + labelColorMap = dedupColors(colorEntries); + } + return { + datasets: chartDatasets.map((e, i) => ({ + ...e, + backgroundColor: (barCtx) => ( + labelColorMap?.[e.label] || getColorForChartEl(chartRef.current, e, barCtx, 'background') + ), + borderColor: (barCtx) => ( + darkenOrLighten(labelColorMap?.[e.label], -.5) || getColorForChartEl(chartRef.current, e, barCtx, 'border') + ), + borderWidth: borderWidth || 2, + borderRadius: 3, + })), + }; + }, [chartDatasets, getColorForLabel]); + + // group records by label (this is the format that Chart.js expects) + useEffect(() => { + const d = records?.reduce((acc, record) => { + const existing = acc.find(e => e.label == record.label); + if (!existing) { + acc.push({ + label: record.label, + data: [{ + x: record.x, + y: record.y, + }], + }); + } else { + existing.data.push({ + x: record.x, + y: record.y, + }); + } + return acc; + }, [] as ChartDataset[]); + setChartDatasets(d); + }, [records]); + + const annotationsAtTop = isHorizontal && lineAnnotations?.some(a => (!a.position || a.position == 'start')); + + return ( + + { + setNumVisibleDatasets(axis.chart.getVisibleDatasetCount()) + }, + ticks: timeAxis ? {} : { + callback: (value, i) => { + const label = chartDatasets[0].data[i].y; + if (typeof label == 'string' && label.includes('\n')) + return label.split('\n'); + return label; + }, + font: { size: 11 }, // default is 12, we want a tad smaller + }, + reverse: true, + stacked, + }, + x: { + title: { display: true, text: axisTitle }, + stacked, + }, + } : { + x: { + offset: true, + type: timeAxis ? 'time' : 'category', + adapters: timeAxis ? { + date: { zone: 'utc' }, + } : {}, + time: timeAxis ? { + unit: 'day', + tooltipFormat: 'DDD', // Luxon "localized date with full month": e.g. August 6, 2014 + } : {}, + ticks: timeAxis ? {} : { + callback: (value, i) => { + console.log("testing vertical", chartData, i); + const label = chartDatasets[0].data[i].x; + if (typeof label == 'string' && label.includes('\n')) + return label.split('\n'); + return label; + }, + }, + stacked, + }, + y: { + title: { display: true, text: axisTitle }, + stacked, + }, + }), + }, + plugins: { + ...(lineAnnotations?.length > 0 && { + annotation: { + clip: false, + annotations: lineAnnotations.map((a, i) => ({ + type: 'line', + label: { + display: true, + padding: { x: 3, y: 1 }, + borderRadius: 0, + backgroundColor: 'rgba(0,0,0,.7)', + color: 'rgba(255,255,255,1)', + font: { size: 10 }, + position: a.position || 'start', + content: a.label, + yAdjust: annotationsAtTop ? -12 : 0, + }, + ...(isHorizontal ? { xMin: a.value, xMax: a.value } + : { yMin: a.value, yMax: a.value }), + borderColor: a.color || colors.onBackground, + borderWidth: 3, + borderDash: [3, 3], + } satisfies AnnotationOptions)), + } + }), + } + }} + // if there are annotations at the top of the chart, it overlaps with the legend + // so we need to increase the spacing between the legend and the chart + // https://stackoverflow.com/a/73498454 + plugins={annotationsAtTop && [{ + id: "increase-legend-spacing", + beforeInit(chart) { + const originalFit = (chart.legend as any).fit; + (chart.legend as any).fit = function fit() { + originalFit.bind(chart.legend)(); + this.height += 12; + }; + } + }]} /> + + ) +} +export default Chart; diff --git a/www/js/components/LeafletView.jsx b/www/js/components/LeafletView.tsx similarity index 92% rename from www/js/components/LeafletView.jsx rename to www/js/components/LeafletView.tsx index eb0c0bb78..cf26cb933 100644 --- a/www/js/components/LeafletView.jsx +++ b/www/js/components/LeafletView.tsx @@ -1,10 +1,9 @@ import React, { useEffect, useRef, useState } from "react"; -import { angularize } from "../angular-react-helper"; -import { object, string } from "prop-types"; import { View } from "react-native"; import { useTheme } from "react-native-paper"; +import L from "leaflet"; -const mapSet = new Set(); +const mapSet = new Set(); export function invalidateMaps() { mapSet.forEach(map => map.invalidateSize()); } @@ -55,7 +54,7 @@ const LeafletView = ({ geojson, opts, ...otherProps }) => { const mapElId = `map-${geojson.data.id.replace(/[^a-zA-Z0-9]/g, '')}`; return ( - + + + + + + ); + }); + console.log("Ending run"); +}); diff --git a/www/js/onboarding/ConsentPage.tsx b/www/js/onboarding/ConsentPage.tsx new file mode 100644 index 000000000..08aa3ab48 --- /dev/null +++ b/www/js/onboarding/ConsentPage.tsx @@ -0,0 +1,43 @@ +import React, { useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import { View, ScrollView } from 'react-native'; +import { Button, Surface } from 'react-native-paper'; +import { resetDataAndRefresh } from '../config/dynamicConfig'; +import { AppContext } from '../App'; +import { getAngularService } from '../angular-react-helper'; +import PrivacyPolicy from './PrivacyPolicy'; +import { onboardingStyles } from './OnboardingStack'; + +const ConsentPage = () => { + + const { t } = useTranslation(); + const context = useContext(AppContext); + const { refreshOnboardingState } = context; + + /* If the user does not consent, we boot them back out to the join screen */ + function disagree() { + resetDataAndRefresh(); + }; + + function agree() { + const StartPrefs = getAngularService('StartPrefs'); + StartPrefs.markConsented().then((response) => { + refreshOnboardingState(); + }); + }; + + // privacy policy and data collection info, followed by accept/reject buttons + return (<> + + + + + + + + + + ); +} + +export default ConsentPage; diff --git a/www/js/onboarding/OnboardingStack.tsx b/www/js/onboarding/OnboardingStack.tsx new file mode 100644 index 000000000..643744ed3 --- /dev/null +++ b/www/js/onboarding/OnboardingStack.tsx @@ -0,0 +1,53 @@ +import React, { useContext } from "react"; +import { StyleSheet } from "react-native"; +import { AppContext } from "../App"; +import WelcomePage from "./WelcomePage"; +import ConsentPage from "./ConsentPage"; +import SurveyPage from "./SurveyPage"; +import SaveQrPage from "./SaveQrPage"; +import SummaryPage from "./SummaryPage"; +import { OnboardingRoute } from "./onboardingHelper"; +import { displayErrorMsg } from "../plugin/logger"; + +const OnboardingStack = () => { + + const { pendingOnboardingState } = useContext(AppContext); + + console.debug('pendingOnboardingState in OnboardingStack', pendingOnboardingState); + + if (pendingOnboardingState.route == OnboardingRoute.WELCOME) { + return ; + } else if (pendingOnboardingState.route == OnboardingRoute.SUMMARY) { + return ; + } else if (pendingOnboardingState.route == OnboardingRoute.CONSENT) { + return ; + } else if (pendingOnboardingState.route == OnboardingRoute.SAVE_QR) { + return ; + } else if (pendingOnboardingState.route == OnboardingRoute.SURVEY) { + return ; + } else { + displayErrorMsg('OnboardingStack: unknown route', pendingOnboardingState.route); + } +} + +export const onboardingStyles = StyleSheet.create({ + page: { + flex: 1, + paddingHorizontal: 15, + paddingVertical: 20, + }, + pageSection: { + marginVertical: 15, + alignItems: 'center', + }, + buttonRow: { + flexDirection: 'row', + flexWrap: 'wrap', + marginVertical: 15, + alignItems: 'center', + gap: 8, + margin: 'auto', + }, +}); + +export default OnboardingStack diff --git a/www/js/onboarding/PrivacyPolicy.tsx b/www/js/onboarding/PrivacyPolicy.tsx new file mode 100644 index 000000000..f237e359c --- /dev/null +++ b/www/js/onboarding/PrivacyPolicy.tsx @@ -0,0 +1,177 @@ +import React, { useMemo } from "react"; +import { StyleSheet, Text } from "react-native"; +import { useTranslation } from "react-i18next"; +import useAppConfig from "../useAppConfig"; +import { getTemplateText } from "./StudySummary"; + +const PrivacyPolicy = () => { + const { t, i18n } = useTranslation(); + const appConfig = useAppConfig(); + + let opCodeText; + if(appConfig?.opcode?.autogen) { + opCodeText = {t('consent-text.opcode.autogen')}; + + } else { + opCodeText = {t('consent-text.opcode.not-autogen')}; + } + + let yourRightsText; + if(appConfig?.intro?.app_required) { + yourRightsText = {t('consent-text.rights.app-required', {program_admin_contact: appConfig?.intro?.program_admin_contact})}; + + } else { + yourRightsText = {t('consent-text.rights.app-not-required', {program_or_study: appConfig?.intro?.program_or_study})}; + } + + const templateText = useMemo(() => getTemplateText(appConfig, i18n.language), [appConfig]); + + return ( + <> + {t('consent-text.title')} + {t('consent-text.introduction.header')} + {templateText?.short_textual_description} + {'\n'} + {t('consent-text.introduction.what-is-openpath')} + {'\n'} + {t('consent-text.introduction.what-is-NREL', {program_or_study: appConfig?.intro?.program_or_study})} + {'\n'} + {t('consent-text.introduction.if-disagree')} + {'\n'} + + {t('consent-text.why.header')} + {templateText?.why_we_collect} + {'\n'} + + {t('consent-text.what.header')} + {t('consent-text.what.no-pii')} + {'\n'} + {t('consent-text.what.phone-sensor')} + {'\n'} + {t('consent-text.what.labeling')} + {'\n'} + {t('consent-text.what.demographics')} + {'\n'} + {t('consent-text.what.on-nrel-site')} + {/* Linking is broken, look into enabling after migration + + {t('consent-text.what.open-source-data')} + { + Linking.openURL('https://github.com/e-mission/e-mission-data-collection.git'); + }}> + {' '}https://github.com/e-mission/e-mission-data-collection.git{' '} + + {t('consent-text.what.open-source-analysis')} + { + Linking.openURL('https://github.com/e-mission/e-mission-server.git'); + }}> + {' '}https://github.com/e-mission/e-mission-server.git{' '} + + {t('consent-text.what.open-source-dashboard')} + { + Linking.openURL('https://github.com/e-mission/em-public-dashboard.git'); + }}> + {' '}https://github.com/e-mission/em-public-dashboard.git{' '} + + */} + {'\n'} + + {t('consent-text.opcode.header')} + {opCodeText} + {'\n'} + + {t('consent-text.who-sees.header')} + {t('consent-text.who-sees.public-dash')} + {'\n'} + {t('consent-text.who-sees.individual-info')} + {'\n'} + {t('consent-text.who-sees.program-admins', { + deployment_partner_name: appConfig?.intro?.deployment_partner_name, + raw_data_use: templateText?.raw_data_use})} + {t('consent-text.who-sees.nrel-devs')} + {'\n'} + {t('consent-text.who-sees.TSDC-info')} + {/* Linking is broken, look into enabling after migration + { + Linking.openURL('https://nrel.gov/tsdc'); + }}> + {t('consent-text.who-sees.on-website')} + + {t('consent-text.who-sees.and-in')} + { + Linking.openURL('https://www.sciencedirect.com/science/article/pii/S2352146515002999'); + }}> + {t('consent-text.who-sees.this-pub')} + + {t('consent-text.who-sees.and')} + { + Linking.openURL('https://www.nrel.gov/docs/fy18osti/70723.pdf'); + }}> + {t('consent-text.who-sees.fact-sheet')} + */} + {t('consent-text.who-sees.on-nrel-site')} + + {'\n'} + + {t('consent-text.rights.header')} + {yourRightsText} + {'\n'} + {t('consent-text.rights.destroy-data-pt1')} + {/* Linking is broken, look into enabling after migration + { + Linking.openURL("mailto:k.shankari@nrel.gov"); + }}> + k.shankari@nrel.gov + */} + (k.shankari@nrel.gov) + {t('consent-text.rights.destroy-data-pt2')} + + {'\n'} + + {t('consent-text.questions.header')} + {t('consent-text.questions.for-questions', {program_admin_contact: appConfig?.intro?.program_admin_contact})} + {'\n'} + + {t('consent-text.consent.header')} + {t('consent-text.consent.press-button-to-consent', {program_or_study: appConfig?.intro?.program_or_study})} + + ) +} + +const styles = StyleSheet.create({ + hyperlinkStyle: (linkColor) => ({ + color: linkColor + }), + text: { + fontSize: 14, + }, + header: { + fontWeight: "bold", + fontSize: 18 + }, + title: { + fontWeight: "bold", + fontSize: 22, + paddingBottom: 10, + textAlign: "center" + }, + divider: { + marginVertical: 10 + } + }); + +export default PrivacyPolicy; diff --git a/www/js/onboarding/SaveQrPage.tsx b/www/js/onboarding/SaveQrPage.tsx new file mode 100644 index 000000000..157ff4093 --- /dev/null +++ b/www/js/onboarding/SaveQrPage.tsx @@ -0,0 +1,92 @@ +import React, { useContext, useEffect, useState } from "react"; +import { View, StyleSheet } from "react-native"; +import { ActivityIndicator, Button, Surface, Text } from "react-native-paper"; +import { registerUserDone, setRegisterUserDone, setSaveQrDone } from "./onboardingHelper"; +import { AppContext } from "../App"; +import usePermissionStatus from "../usePermissionStatus"; +import { getAngularService } from "../angular-react-helper"; +import { displayError, logDebug } from "../plugin/logger"; +import { useTranslation } from "react-i18next"; +import QrCode, { shareQR } from "../components/QrCode"; +import { onboardingStyles } from "./OnboardingStack"; +import { preloadDemoSurveyResponse } from "./SurveyPage"; + +const SaveQrPage = ({ }) => { + + const { t } = useTranslation(); + const { pendingOnboardingState, refreshOnboardingState } = useContext(AppContext); + const { overallStatus } = usePermissionStatus(); + + useEffect(() => { + if (overallStatus == true && !registerUserDone) { + logDebug('permissions done, going to log in'); + login(pendingOnboardingState.opcode).then((response) => { + logDebug('login done, refreshing onboarding state'); + setRegisterUserDone(true); + preloadDemoSurveyResponse(); + refreshOnboardingState(); + }); + } else { + logDebug('permissions not done, waiting'); + } + }, [overallStatus]); + + function login(token) { + const CommHelper = getAngularService('CommHelper'); + const KVStore = getAngularService('KVStore'); + const EXPECTED_METHOD = "prompted-auth"; + const dbStorageObject = {"token": token}; + return KVStore.set(EXPECTED_METHOD, dbStorageObject).then((r) => { + CommHelper.registerUser((successResult) => { + refreshOnboardingState(); + }, function(errorResult) { + displayError(errorResult, "User registration error"); + }); + }).catch((e) => { + displayError(e, "Sign in error"); + }); + }; + + function onFinish() { + setSaveQrDone(true); + refreshOnboardingState(); + } + + return ( + + + + {t('login.make-sure-save-your-opcode')} + + + {t('login.cannot-retrieve')} + + + + + + {pendingOnboardingState.opcode} + + + + + + + + ); +} + +const s = StyleSheet.create({ + opcodeText: { + fontFamily: 'monospace', + marginVertical: 8, + maxWidth: '100%', + wordBreak: 'break-all', + }, +}); + +export default SaveQrPage; diff --git a/www/js/onboarding/StudySummary.tsx b/www/js/onboarding/StudySummary.tsx new file mode 100644 index 000000000..02a1797b4 --- /dev/null +++ b/www/js/onboarding/StudySummary.tsx @@ -0,0 +1,47 @@ +import React, { useMemo } from "react"; +import { View, StyleSheet } from "react-native"; +import { Text } from "react-native-paper"; +import { useTranslation } from "react-i18next"; +import useAppConfig from "../useAppConfig"; + +export function getTemplateText(configObject, lang) { + if (configObject && (configObject.name)) { + return configObject.intro.translated_text[lang]; + } +} + +const StudySummary = () => { + + const { i18n } = useTranslation(); + const appConfig = useAppConfig(); + + const templateText = useMemo(() => getTemplateText(appConfig, i18n.language), [appConfig]); + + return (<> + {templateText?.deployment_name} + {appConfig?.intro?.deployment_partner_name + " " + templateText?.deployment_name} + + {"✔️ " + templateText?.summary_line_1} + {"✔️ " + templateText?.summary_line_2} + {"✔️ " + templateText?.summary_line_3} + + ) +}; + +const styles = StyleSheet.create({ + title: { + fontWeight: "bold", + fontSize: 22, + paddingBottom: 10, + textAlign: "center" + }, + text: { + fontSize: 14, + }, + studyName: { + fontWeight: "bold", + fontSize: 16 + }, +}); + +export default StudySummary; diff --git a/www/js/onboarding/SummaryPage.tsx b/www/js/onboarding/SummaryPage.tsx new file mode 100644 index 000000000..90e8d858e --- /dev/null +++ b/www/js/onboarding/SummaryPage.tsx @@ -0,0 +1,34 @@ +import React, { useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import { View, ScrollView } from 'react-native'; +import { Button, Surface } from 'react-native-paper'; +import { AppContext } from '../App'; +import { onboardingStyles } from './OnboardingStack'; +import StudySummary from './StudySummary'; +import { setSummaryDone } from './onboardingHelper'; + +const SummaryPage = () => { + + const { t } = useTranslation(); + const context = useContext(AppContext); + const { refreshOnboardingState } = context; + + function next() { + setSummaryDone(true); + refreshOnboardingState(); + }; + + // summary of the study, followed by 'next' button + return (<> + + + + + + + + + ); +} + +export default SummaryPage; diff --git a/www/js/onboarding/SurveyPage.tsx b/www/js/onboarding/SurveyPage.tsx new file mode 100644 index 000000000..11e58c94a --- /dev/null +++ b/www/js/onboarding/SurveyPage.tsx @@ -0,0 +1,95 @@ +import React, { useState, useEffect, useContext, useMemo } from "react"; +import { View, StyleSheet } from "react-native"; +import { ActivityIndicator, Button, Surface, Text } from "react-native-paper"; +import EnketoModal from "../survey/enketo/EnketoModal"; +import { DEMOGRAPHIC_SURVEY_DATAKEY, DEMOGRAPHIC_SURVEY_NAME } from "../control/DemographicsSettingRow"; +import { loadPreviousResponseForSurvey } from "../survey/enketo/enketoHelper"; +import { AppContext } from "../App"; +import { markIntroDone } from "./onboardingHelper"; +import { useTranslation } from "react-i18next"; +import { DateTime } from "luxon"; +import { onboardingStyles } from "./OnboardingStack"; + +let preloadedResponsePromise: Promise = null; +export const preloadDemoSurveyResponse = () => { + if (!preloadedResponsePromise) { + preloadedResponsePromise = loadPreviousResponseForSurvey(DEMOGRAPHIC_SURVEY_DATAKEY); + } + return preloadedResponsePromise; +} + +const SurveyPage = () => { + + const { t } = useTranslation(); + const { refreshOnboardingState } = useContext(AppContext); + const [surveyModalVisible, setSurveyModalVisible] = useState(false); + const [prevSurveyResponse, setPrevSurveyResponse] = useState(null); + const prevSurveyResponseDate = useMemo(() => { + if (prevSurveyResponse) { + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(prevSurveyResponse, "text/xml"); + const surveyEndDt = xmlDoc.querySelector('end')?.textContent; // ISO datetime of survey completion + return DateTime.fromISO(surveyEndDt).toLocaleString(DateTime.DATE_FULL); + } + }, [prevSurveyResponse]); + + useEffect(() => { + /* If we came from the SaveQrPage, we should have already initiated loading the previous survey + response from there, and preloadDemographicsSurvey() will just return the promise that was + already started. + Otherwise, it will start a new promise. Either way, we wait for it to finish before proceeding. */ + preloadDemoSurveyResponse().then((lastSurvey) => { + if (lastSurvey?.data?.xmlResponse) { + setPrevSurveyResponse(lastSurvey.data.xmlResponse); + } else { + // if there is no prev response, we show the blank survey to be filled out for the first time + setSurveyModalVisible(true); + } + }); + }, []); + + function onFinish() { + setSurveyModalVisible(false); + markIntroDone(); + refreshOnboardingState(); + } + + return (<> + + {prevSurveyResponse ? + + + {t('survey.prev-survey-found')} + {prevSurveyResponseDate} + + + + + + + : + + + + {t('survey.loading-prior-survey')} + + + } + + setSurveyModalVisible(false)} + onResponseSaved={onFinish} surveyName={DEMOGRAPHIC_SURVEY_NAME} + opts={{ + /* If there is no prev response, we need an initial response from the user and should + not allow them to dismiss the modal by the "<- Dismiss" button */ + undismissable: !prevSurveyResponse, + prefilledSurveyResponse: prevSurveyResponse, + dataKey: DEMOGRAPHIC_SURVEY_DATAKEY, + }} /> + ); +}; + +export default SurveyPage; diff --git a/www/js/onboarding/WelcomePage.tsx b/www/js/onboarding/WelcomePage.tsx new file mode 100644 index 000000000..8e7e43425 --- /dev/null +++ b/www/js/onboarding/WelcomePage.tsx @@ -0,0 +1,202 @@ +import React, { useContext, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { View, Image, Modal, ScrollView, StyleSheet, ViewStyle, useWindowDimensions } from 'react-native'; +import { Button, Dialog, Divider, IconButton, Surface, Text, TextInput, TouchableRipple, useTheme } from 'react-native-paper'; +import color from 'color'; +import { initByUser } from '../config/dynamicConfig'; +import { AppContext } from '../App'; +import { displayError } from "../plugin/logger"; +import { onboardingStyles } from './OnboardingStack'; +import { Icon } from '../components/Icon'; + +const WelcomePage = () => { + + const { t } = useTranslation(); + const { colors } = useTheme(); + const { width: windowWidth } = useWindowDimensions(); + const context = useContext(AppContext); + const { refreshOnboardingState } = context; + const [pasteModalVis, setPasteModalVis] = useState(false); + const [infoPopupVis, setInfoPopupVis] = useState(false); + const [existingToken, setExistingToken] = useState(''); + + const scanCode = function() { + window.cordova.plugins.barcodeScanner.scan( + function (result) { + console.debug("scanned code", result); + if (result.format == "QR_CODE" && + result.cancelled == false) { + let text = result.text.split("=")[1]; + console.log("found code", text); + loginWithToken(text); + } else { + displayError(result.text, "invalid study reference") ; + } + }, + function (error) { + displayError(error, "Scanning failed: "); + }); + }; + + function loginWithToken(token) { + initByUser({token}).then((configUpdated) => { + if (configUpdated) { + setPasteModalVis(false); + refreshOnboardingState(); + } + }).catch(err => { + console.error('Error logging in with token', err); + setExistingToken(''); + }); + } + + return (<> + + + setInfoPopupVis(true)} /> + + + + + }} /> + + + {t('join.to-proceed-further')} + {t('join.code-hint')} + + + + + {t('join.scan-code')} + + {t('join.scan-hint')} + + + + setPasteModalVis(true)} icon='content-paste'> + {t('join.paste-code')} + + {t('join.paste-hint')} + + + + setPasteModalVis(false)}> + setPasteModalVis(false)}> + + + + + + + + setInfoPopupVis(false)}> + setInfoPopupVis(false)}> + + {t('join.about-app-title', {appName: t('join.app-name')})} + + + + {t('join.about-app-para-1')} + {t('join.about-app-para-2')} + {t('join.about-app-para-3')} + {t('join.tips-title')} + - {t('join.all-green-status')} + - {t('join.dont-force-kill')} + - {t('join.background-restrictions')} + + + + + + + + ); +} + +const s: any = StyleSheet.create({ + headerArea: ((windowWidth, colors) => ({ + width: windowWidth * 2.5, + height: windowWidth, + left: -windowWidth * .75, + borderBottomRightRadius: '50%', + borderBottomLeftRadius: '50%', + position: 'absolute', + top: windowWidth * -2/3, + backgroundColor: colors.primary, + boxShadow: `0 16px ${color(colors.primary).alpha(0.3).rgb().string()}`, + })) as ViewStyle, + appIconWrapper: ((colors): ViewStyle => ({ + marginTop: 20, + width: 200, + height: 200, + alignSelf: 'center', + backgroundColor: color(colors.onPrimary).darken(0.1).alpha(0.4).rgb().string(), + padding: 10, + borderRadius: 32, + })) as ViewStyle, + infoButton: { + position: 'absolute', + top: 10, + right: 10, + width: 40, + height: 40, + elevation: 2, + }, + appIcon: ((colors): ViewStyle => ({ + width: '100%', + height: '100%', + backgroundColor: colors.onPrimary, + borderRadius: 24, + })) as ViewStyle, + welcomeTitle: { + marginTop: 20, + textAlign: 'center', + paddingVertical: 20, + }, + buttonsSection: { + flexDirection: 'row', + justifyContent: 'center', + marginVertical: 20, + }, +}); + + +const WelcomePageButton = ({ onPress, icon, children }) => { + + const { colors } = useTheme(); + const { width: windowWidth } = useWindowDimensions(); + + return ( + + + + + {children} + + + + ); +} + +const welcomeButtonStyles: any = StyleSheet.create({ + btn: ((colors): ViewStyle => ({ + justifyContent: 'center', + alignItems: 'center', + backgroundColor: colors.primary, + borderRadius: 21, + padding: 20, + gap: 8, + })) as ViewStyle, + wrapper: ((colors): ViewStyle => ({ + borderRadius: 26, + padding: 5, + backgroundColor: color(colors.primary).alpha(0.4).rgb().string(), + })) as ViewStyle, +}); + +export default WelcomePage; diff --git a/www/js/onboarding/onboardingHelper.ts b/www/js/onboarding/onboardingHelper.ts new file mode 100644 index 000000000..7874ab9f8 --- /dev/null +++ b/www/js/onboarding/onboardingHelper.ts @@ -0,0 +1,61 @@ +import { DateTime } from "luxon"; +import { getAngularService } from "../angular-react-helper"; +import { getConfig } from "../config/dynamicConfig"; + +export const INTRO_DONE_KEY = 'intro_done'; + +// state = null if onboarding is done +// route = WELCOME if no config present +// route = SUMMARY if config present, but not consented and summary not done +// route = CONSENT if config present, but not consented and summary done +// route = SAVE_QR if config present, consented, but save qr not done +// route = SURVEY if config present, consented and save qr done +export enum OnboardingRoute { WELCOME, SUMMARY, CONSENT, SAVE_QR, SURVEY, NONE }; +export type OnboardingState = { + opcode: string, + route: OnboardingRoute, +} + +export let summaryDone = false; +export const setSummaryDone = (b) => summaryDone = b; + +export let saveQrDone = false; +export const setSaveQrDone = (b) => saveQrDone = b; + +export let registerUserDone = false; +export const setRegisterUserDone = (b) => registerUserDone = b; + +export function getPendingOnboardingState(): Promise { + return Promise.all([getConfig(), readConsented(), readIntroDone()]).then(([config, isConsented, isIntroDone]) => { + if (isIntroDone) return null; // onboarding is done; no pending state + let route: OnboardingRoute = OnboardingRoute.NONE; + if (!config) { + route = OnboardingRoute.WELCOME; + } else if (!isConsented && !summaryDone) { + route = OnboardingRoute.SUMMARY; + } else if (!isConsented) { + route = OnboardingRoute.CONSENT; + } else if (!saveQrDone) { + route = OnboardingRoute.SAVE_QR; + } else { + route = OnboardingRoute.SURVEY; + } + return { route, opcode: config?.joined?.opcode }; + }); +}; + +async function readConsented() { + const StartPrefs = getAngularService('StartPrefs'); + return StartPrefs.readConsentState().then(StartPrefs.isConsented) as Promise; +} + +async function readIntroDone() { + const KVStore = getAngularService('KVStore'); + return KVStore.get(INTRO_DONE_KEY).then((read_val) => !!read_val) as Promise; +} + +export async function markIntroDone() { + const currDateTime = DateTime.now().toISO(); + const KVStore = getAngularService('KVStore'); + return KVStore.set(INTRO_DONE_KEY, currDateTime); +} diff --git a/www/js/recent.js b/www/js/recent.js deleted file mode 100644 index 5fbfbf66c..000000000 --- a/www/js/recent.js +++ /dev/null @@ -1,157 +0,0 @@ -angular.module('emission.main.recent', ['emission.services']) - -.controller('logCtrl', function(ControlHelper, $scope, EmailHelper) { - console.log("Launching logCtr"); - var RETRIEVE_COUNT = 100; - $scope.logCtrl = {}; - - $scope.refreshEntries = function() { - window.Logger.getMaxIndex().then(function(maxIndex) { - console.log("maxIndex = "+maxIndex); - $scope.logCtrl.currentStart = maxIndex; - $scope.logCtrl.gotMaxIndex = true; - $scope.logCtrl.reachedEnd = false; - $scope.entries = []; - $scope.addEntries(); - }, function (e) { - var errStr = "While getting max index "+JSON.stringify(e, null, 2); - console.log(errStr); - alert(errStr); - }); - } - - $scope.moreDataCanBeLoaded = function() { - return $scope.logCtrl.gotMaxIndex && !($scope.logCtrl.reachedEnd); - } - - $scope.clear = function() { - window.Logger.clearAll(); - window.Logger.log(window.Logger.LEVEL_INFO, "Finished clearing entries from unified log"); - $scope.refreshEntries(); - } - - $scope.addEntries = function() { - console.log("calling addEntries"); - window.Logger.getMessagesFromIndex($scope.logCtrl.currentStart, RETRIEVE_COUNT) - .then(function(entryList) { - $scope.$apply($scope.processEntries(entryList)); - console.log("entry list size = "+$scope.entries.length); - console.log("Broadcasting infinite scroll complete"); - $scope.$broadcast('scroll.infiniteScrollComplete') - }, function(e) { - var errStr = "While getting messages from the log "+JSON.stringify(e, null, 2); - console.log(errStr); - alert(errStr); - $scope.$broadcast('scroll.infiniteScrollComplete') - } - ) - } - - $scope.processEntries = function(entryList) { - for (let i = 0; i < entryList.length; i++) { - var currEntry = entryList[i]; - currEntry.fmt_time = moment.unix(currEntry.ts).format("llll"); - $scope.entries.push(currEntry); - } - if (entryList.length == 0) { - console.log("Reached the end of the scrolling"); - $scope.logCtrl.reachedEnd = true; - } else { - $scope.logCtrl.currentStart = entryList[entryList.length-1].ID - console.log("new start index = "+$scope.logCtrl.currentStart); - } - } - - $scope.emailLog = function () { - EmailHelper.sendEmail("loggerDB"); - } - - $scope.refreshEntries(); -}) - -.controller('sensedDataCtrl', function($scope, $ionicActionSheet, EmailHelper) { - var currentStart = 0; - - /* Let's keep a reference to the database for convenience */ - var db = window.cordova.plugins.BEMUserCache; - - $scope.config = {} - $scope.config.key_data_mapping = { - "Transitions": { - fn: db.getAllMessages, - key: "statemachine/transition" - }, - "Locations": { - fn: db.getAllSensorData, - key: "background/location" - }, - "Motion Type": { - fn: db.getAllSensorData, - key: "background/motion_activity" - }, - } - - $scope.emailCache = function () { - EmailHelper.sendEmail("userCacheDB"); - } - - $scope.config.keys = [] - for (let key in $scope.config.key_data_mapping) { - $scope.config.keys.push(key); - } - - $scope.selected = {} - $scope.selected.key = $scope.config.keys[0] - - $scope.changeSelection = function() { - $ionicActionSheet.show({ - buttons: [ - { text: 'Locations' }, - { text: 'Motion Type' }, - { text: 'Transitions' }, - ], - buttonClicked: function(index, button) { - $scope.setSelected(button.text); - return true; - } - }); - } - - $scope.setSelected = function(newVal) { - $scope.selected.key = newVal; - $scope.updateEntries(); - } - - $scope.updateEntries = function() { - let usercacheFn, usercacheKey; - if (angular.isUndefined($scope.selected.key)) { - usercacheFn = db.getAllMessages; - usercacheKey = "statemachine/transition"; - } else { - usercacheFn = $scope.config.key_data_mapping[$scope.selected.key]["fn"] - usercacheKey = $scope.config.key_data_mapping[$scope.selected.key]["key"] - } - usercacheFn(usercacheKey, true).then(function(entryList) { - $scope.entries = []; - $scope.$apply(function() { - for (let i = 0; i < entryList.length; i++) { - // $scope.entries.push({metadata: {write_ts: 1, write_fmt_time: "1"}, data: "1"}) - var currEntry = entryList[i]; - currEntry.metadata.write_fmt_time = moment.unix(currEntry.metadata.write_ts) - .tz(currEntry.metadata.time_zone) - .format("llll"); - currEntry.data = JSON.stringify(currEntry.data, null, 2); - // window.Logger.log(window.Logger.LEVEL_DEBUG, - // "currEntry.data = "+currEntry.data); - $scope.entries.push(currEntry); - } - }) - // This should really be within a try/catch/finally block - $scope.$broadcast('scroll.refreshComplete'); - }, function(error) { - window.Logger.log(window.Logger.LEVEL_ERROR, "Error updating entries"+ error); - }) - } - - $scope.updateEntries(); -}) diff --git a/www/js/splash/notifScheduler.js b/www/js/splash/notifScheduler.js index 069af7a18..821b6fb09 100644 --- a/www/js/splash/notifScheduler.js +++ b/www/js/splash/notifScheduler.js @@ -1,15 +1,15 @@ 'use strict'; import angular from 'angular'; +import { getConfig } from '../config/dynamicConfig'; angular.module('emission.splash.notifscheduler', ['emission.services', 'emission.plugin.logger', - 'emission.stats.clientstats', - 'emission.config.dynamic']) + 'emission.stats.clientstats']) .factory('NotificationScheduler', function($http, $window, $ionicPlatform, - ClientStats, DynamicConfig, CommHelper, Logger) { + ClientStats, CommHelper, Logger) { const scheduler = {}; let _config; @@ -258,7 +258,7 @@ angular.module('emission.splash.notifscheduler', } $ionicPlatform.ready().then(async () => { - _config = await DynamicConfig.configReady(); + _config = await getConfig(); if (!_config.reminderSchemes) { Logger.log("No reminder schemes found in config, not scheduling notifications"); return; diff --git a/www/js/splash/startprefs.js b/www/js/splash/startprefs.js index 223c82579..e535d179a 100644 --- a/www/js/splash/startprefs.js +++ b/www/js/splash/startprefs.js @@ -1,14 +1,13 @@ import angular from 'angular'; +import { getConfig } from '../config/dynamicConfig'; angular.module('emission.splash.startprefs', ['emission.plugin.logger', 'emission.splash.referral', - 'emission.plugin.kvstore', - 'emission.config.dynamic']) + 'emission.plugin.kvstore']) .factory('StartPrefs', function($window, $state, $interval, $rootScope, $ionicPlatform, - $ionicPopup, KVStore, $http, Logger, ReferralHandler, DynamicConfig) { + $ionicPopup, KVStore, $http, Logger, ReferralHandler) { var logger = Logger; - var nTimesCalled = 0; var startprefs = {}; // Boolean: represents that the "intro" - the one page summary // and the login are done @@ -95,7 +94,7 @@ angular.module('emission.splash.startprefs', ['emission.plugin.logger', } startprefs.readConfig = function() { - return DynamicConfig.loadSavedConfig().then((savedConfig) => $rootScope.app_ui_label = savedConfig); + return getConfig().then((savedConfig) => $rootScope.app_ui_label = savedConfig); } startprefs.hasConfig = function() { @@ -112,33 +111,6 @@ angular.module('emission.splash.startprefs', ['emission.plugin.logger', } } - /* - * getNextState() returns a promise, since reading the startupConfig is - * async. The promise returns an onboarding state to navigate to, or - * null for the default state - */ - - startprefs.getPendingOnboardingState = function() { - return startprefs.readStartupState().then(function([is_intro_done, is_consented, has_config]) { - if (!has_config) { - console.assert(!$rootScope.has_config, "in getPendingOnboardingState first check, $rootScope.has_config", JSON.stringify($rootScope.has_config)); - return 'root.join'; - } else if (!is_intro_done) { - console.assert(!$rootScope.intro_done, "in getPendingOnboardingState second check, $rootScope.intro_done", JSON.stringify($rootScope.intro_done)); - return 'root.intro'; - } else { - // intro is done. Now let's check consent - console.assert(is_intro_done, "in getPendingOnboardingState, local is_intro_done", is_intro_done); - console.assert($rootScope.is_intro_done, "in getPendingOnboardingState, $rootScope.intro_done", $rootScope.intro_done); - if (is_consented) { - return null; - } else { - return 'root.reconsent'; - } - } - }); - }; - /* * Read the intro_done and consent_done variables into the $rootScope so that * we can use them without making multiple native calls @@ -179,28 +151,6 @@ angular.module('emission.splash.startprefs', ['emission.plugin.logger', }); } - startprefs.getNextState = function() { - return startprefs.getPendingOnboardingState().then(function(result){ - if (result == null) { - if (angular.isDefined($rootScope.redirectTo)) { - var redirState = $rootScope.redirectTo; - var redirParams = $rootScope.redirectParams; - $rootScope.redirectTo = undefined; - $rootScope.redirectParams = undefined; - return {state: redirState, params: redirParams}; - } else { - return {state: 'root.main.inf_scroll', params: {}}; - } - } else { - return {state: result, params: {}}; - } - }) - .catch((err) => { - Logger.displayError("error getting next state", err); - return "root.intro"; - }); - }; - var changeState = function(destState) { logger.log('changing state to '+destState); console.log("loading "+destState); @@ -217,39 +167,5 @@ angular.module('emission.splash.startprefs', ['emission.plugin.logger', }); }; - // Currently loads main or intro based on whether onboarding is complete. - // But easily extensible to storing the last screen that the user was on, - // or the users' preferred screen - - startprefs.loadPreferredScreen = function() { - logger.log("About to navigate to preferred tab"); - startprefs.getNextState().then(changeState).catch(function(error) { - logger.displayError("Error loading preferred tab, loading root.intro", error); - // logger.log("error "+error+" loading finding tab, loading root.intro"); - changeState('root.intro'); - }); - }; - - startprefs.loadWithPrefs = function() { - // alert("attach debugger!"); - console.log("Checking to see whether we are ready to load the screen"); - if (!angular.isDefined($window.Logger)) { - alert("ionic is ready, but logger not present?"); - } - logger = Logger; - startprefs.loadPreferredScreen(); - }; - - startprefs.startWithPrefs = function() { - startprefs.loadWithPrefs(); - } - - $ionicPlatform.ready().then(function() { - Logger.log("ionicPlatform.ready() called " + nTimesCalled+" times!"); - nTimesCalled = nTimesCalled + 1; - startprefs.startWithPrefs(); - Logger.log("startprefs startup done"); - }); - return startprefs; }); diff --git a/www/js/survey/enketo/EnketoModal.tsx b/www/js/survey/enketo/EnketoModal.tsx index b4bf8f024..8b80b6dfe 100644 --- a/www/js/survey/enketo/EnketoModal.tsx +++ b/www/js/survey/enketo/EnketoModal.tsx @@ -21,7 +21,7 @@ const EnketoModal = ({ surveyName, onResponseSaved, opts, ...rest } : Props) => const headerEl = useRef(null); const surveyJson = useRef(null); const enketoForm = useRef
(null); - const { appConfig, loading } = useAppConfig(); + const appConfig = useAppConfig(); async function fetchSurveyJson(url) { const responseText = await fetchUrlCached(url); @@ -76,9 +76,9 @@ const EnketoModal = ({ surveyName, onResponseSaved, opts, ...rest } : Props) => useEffect(() => { if (!rest.visible) return; - if (!appConfig || loading) return console.error('App config not loaded yet'); + if (!appConfig) return console.error('App config not loaded yet'); initSurvey(); - }, [appConfig, loading, rest.visible]); + }, [appConfig, rest.visible]); /* adapted from the template given by enketo-core: https://github.com/enketo/enketo-core/blob/master/src/index.html */ @@ -89,11 +89,13 @@ const EnketoModal = ({ surveyName, onResponseSaved, opts, ...rest } : Props) => Just make sure to keep a .form-language-selector element into which the form language selector ( -
-
cm
-
ft
-
- -
{{'user-weight'}}
-
- -
-
kg
-
lb
-
-
-
{{'user-age'}}
- diff --git a/www/templates/control/app-status-modal.html b/www/templates/control/app-status-modal.html deleted file mode 100644 index 965b2857d..000000000 --- a/www/templates/control/app-status-modal.html +++ /dev/null @@ -1,15 +0,0 @@ - - -

Permissions

-
- - - -
diff --git a/www/templates/control/main-consent.html b/www/templates/control/main-consent.html deleted file mode 100644 index f991eab7f..000000000 --- a/www/templates/control/main-consent.html +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/www/templates/control/qrc.html b/www/templates/control/qrc.html deleted file mode 100644 index 3d189cece..000000000 --- a/www/templates/control/qrc.html +++ /dev/null @@ -1,28 +0,0 @@ - - - -
-
-
-

{{'general-settings.qrcode'}}

-
-
-
-
- -
-

-
-
- -
-
-

-
-
- -
-
- - -
diff --git a/www/templates/intro/changes.html b/www/templates/intro/changes.html deleted file mode 100644 index 686076b52..000000000 --- a/www/templates/intro/changes.html +++ /dev/null @@ -1,21 +0,0 @@ - - -
-

E-Mission: Data driven carbon emission reduction

-
- -
- Changes between the previously approved protocol and the current one are: -
-
    -
  1. Switch from moves to our own data collection
  2. -
  3. Define policies when used as a platform for an external study
  4. -
  5. Specify policies for time-delayed access of datasets
  6. -
-
- -
diff --git a/www/templates/intro/consent-text.html b/www/templates/intro/consent-text.html deleted file mode 100644 index fa4c2f6d9..000000000 --- a/www/templates/intro/consent-text.html +++ /dev/null @@ -1,136 +0,0 @@ - -
diff --git a/www/templates/intro/consent.html b/www/templates/intro/consent.html deleted file mode 100644 index 3c33eeca3..000000000 --- a/www/templates/intro/consent.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - diff --git a/www/templates/intro/intro.html b/www/templates/intro/intro.html deleted file mode 100644 index eaa806b6d..000000000 --- a/www/templates/intro/intro.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/www/templates/intro/reconsent.html b/www/templates/intro/reconsent.html deleted file mode 100644 index b7adfe168..000000000 --- a/www/templates/intro/reconsent.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/www/templates/intro/saveTokenFile.html b/www/templates/intro/saveTokenFile.html deleted file mode 100644 index b1bbd9d51..000000000 --- a/www/templates/intro/saveTokenFile.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - diff --git a/www/templates/intro/sensor_explanation.html b/www/templates/intro/sensor_explanation.html deleted file mode 100644 index 9f1d725ab..000000000 --- a/www/templates/intro/sensor_explanation.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - diff --git a/www/templates/intro/summary.html b/www/templates/intro/summary.html deleted file mode 100644 index fcff5f5d2..000000000 --- a/www/templates/intro/summary.html +++ /dev/null @@ -1,24 +0,0 @@ - - -
-

{{template_text.deployment_name}}

-
- -
- The {{ui_config.intro.deployment_partner_name}} {{template_text.deployment_name}}: -
-
    -
  1. ✔️ {{template_text.summary_line_1}} -
    -
  2. ✔️ {{template_text.summary_line_2}} -
    -
  3. ✔️ {{template_text.summary_line_3}} -
-
-
- - - - diff --git a/www/templates/intro/survey.html b/www/templates/intro/survey.html deleted file mode 100644 index 14416ee75..000000000 --- a/www/templates/intro/survey.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/www/templates/join/about-app.html b/www/templates/join/about-app.html deleted file mode 100644 index 63ab20ba1..000000000 --- a/www/templates/join/about-app.html +++ /dev/null @@ -1,42 +0,0 @@ - - -
{{'join.about-app-para-1'}}
- -
{{'join.about-app-para-2'}}
- -
{{'join.about-app-para-3'}}
- -
- {{'join.tips-title'}} -
- - - - {{'join.all-green-status'}} - - - - {{'join.dont-force-kill'}} - - -
-
- -
- - {{'join.all-green-status'}} - -
- - {{'join.background-restrictions'}} - - -
-
- - - - - diff --git a/www/templates/join/request_join.html b/www/templates/join/request_join.html deleted file mode 100644 index 6afd6940d..000000000 --- a/www/templates/join/request_join.html +++ /dev/null @@ -1,39 +0,0 @@ - - - -
-
NREL OpenPATH icon
-
-
-
-

{{'join.proceed-further'}}

- -

{{'join.what-is-opcode'}}

- -
- - - -
- - {{'join.scan-details'}} -
-
- -
- {{'join.or'}} -
-
- -
- - {{'join.paste-details'}} -
-
-
-
-
-
-
diff --git a/www/templates/main-metrics.html b/www/templates/main-metrics.html deleted file mode 100644 index d3c8b7ce3..000000000 --- a/www/templates/main-metrics.html +++ /dev/null @@ -1,225 +0,0 @@ - - - - - - - - - - -
-
-
{{'main-metrics.summary'}}
-
{{'main-metrics.chart'}}
-
-
-
-
{{'main-metrics.change-data'}}
-
-
-
-
{{ selectCtrl.fromDateTimestamp.format('ll') }} ➡️ {{ selectCtrl.toDateTimestamp.format('ll') }}
-
-
-
-
- - - -
-
-
-
-
{{'main-metrics.distance'}}
-
{{'main-metrics.trips'}}
-
{{'main-metrics.duration'}}
-
{{'main-metrics.speed'}}
-
-
-
-
- - - - -
-
-
-
-
-
-
-
-

{{'main-metrics.footprint'}}

-
-
-
kg CO₂
-
{{ 'main-metrics.label-to-squish' | i18next }}
- -
-
-
-
{{'main-metrics.how-it-compares'}}
- -
{{'main-metrics.average'}} kg CO₂
-
{{'main-metrics.avoided'}} kg CO₂
-
{{'main-metrics.lastweek'}} kg CO₂
- -
{{'main-metrics.us-2030-goal'}} {{carbonData.us2030 | number}} kg CO₂
-
{{'main-metrics.us-2050-goal'}} {{carbonData.us2050 | number}} kg CO₂
-
-
-
- -
-
-
-
-
- -
-
- -
-
- -
-

{{'main-metrics.calories'}}

-
- -
-
-
kcal
-
{{'main-metrics.equals-cookies' | i18next:{count: numberOfCookies.low} }}
-
{{'main-metrics.equals-icecream' | i18next:{count: numberOfIceCreams.low} }}
-
{{'main-metrics.equals-bananas' | i18next:{count: numberOfBananas.low} }}
- -
-
-
-
{{'main-metrics.average'}} cal
-
{{'main-metrics.lastweek'}} cal
-
-
- -
-
- -
-
- -
-
-
-
- -
-
- - -
-

{{'main-metrics.distance'}}

-
{{'main-metrics.no-summary-data'}}
-
-
- -
- -
-
-
- {{ summaryData.defaultSummary.distance[dIndex + i].key }} -
-
- {{ formatDistance(summaryData.defaultSummary.distance[dIndex + i].values).slice(-1)[0] }} -
-
-
-
-
-
- -
-

{{'main-metrics.trips'}}

-
{{'main-metrics.no-summary-data'}}
-
-
- -
- -
-
-
- {{ summaryData.defaultSummary.count[dIndex + i].key }} -
-
- {{ formatCount(summaryData.defaultSummary.count[dIndex + i].values).slice(-1)[0] }} -
-
-
-
-
-
- -
-

{{'main-metrics.duration'}}

-
{{'main-metrics.no-summary-data'}}
-
-
- -
- -
-
-
- {{ summaryData.defaultSummary.duration[dIndex + i].key }} -
-
- {{ formatDuration(summaryData.defaultSummary.duration[dIndex + i].values).slice(-1)[0] }} -
-
-
-
-
-
- -
-

{{'main-metrics.mean-speed'}}

-
{{'main-metrics.no-summary-data'}}
-
-
- -
- -
-
-
- {{ summaryData.defaultSummary.mean_speed[dIndex + i].key }} -
-
- {{ formatMeanSpeed(summaryData.defaultSummary.mean_speed[dIndex + i].values).slice(-1)[0] }} -
-
-
-
-
-
-
-
-
-
-
diff --git a/www/templates/main.html b/www/templates/main.html deleted file mode 100644 index c3de4adcb..000000000 --- a/www/templates/main.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/www/templates/metrics/arrow-greater-lesser.html b/www/templates/metrics/arrow-greater-lesser.html deleted file mode 100644 index bdb0cf940..000000000 --- a/www/templates/metrics/arrow-greater-lesser.html +++ /dev/null @@ -1,38 +0,0 @@ - - -
- -
-
-
{{ change.low | number:0 }}% {{'metrics.greater-than' | i18next }} {{ 'metrics.last-week' | i18next }}
-
-
-
-
{{ (-1) * change.low | number:0 }}% {{'metrics.less-than' | i18next }} {{ 'metrics.last-week' | i18next }}
-
-
-
- -
-
-
-
-
=
-
{{ (-1) * change.low | number:0 }}% {{'metrics.less' | i18next }}
-
{{ change.low | number:0 }}% {{'metrics.greater' | i18next }}
-
-
-
{{'metrics.or' | i18next }}
-
{{'metrics.week-before' | i18next }}
-
-
-
-
-
=
-
{{ (-1) * change.high | number:0 }}% {{'metrics.less' | i18next }}
-
{{ change.high | number:0 }}% {{'metrics.greater' | i18next }}
-
-
-
diff --git a/www/templates/metrics/metrics-control.html b/www/templates/metrics/metrics-control.html deleted file mode 100644 index 6d2be64dc..000000000 --- a/www/templates/metrics/metrics-control.html +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - -
-
-
{{ uictrl.currentString }}
- -
-
-
-
-
-
-
{{'metrics.from'}}
-
-
- -
-
-
- - -
-
-
-
{{'metrics.to'}} -
-
-
-
-
-
-
-
{{'metrics.frequency'}}
-
-
-
{{selectCtrl.pandaFreqString}}
-
-
- - - -
-
diff --git a/www/templates/metrics/range-display.html b/www/templates/metrics/range-display.html deleted file mode 100644 index fa8c3581e..000000000 --- a/www/templates/metrics/range-display.html +++ /dev/null @@ -1,2 +0,0 @@ -{{ lowFmt }} -{{ lowFmt }} - {{ highFmt }} diff --git a/www/templates/recent/log.html b/www/templates/recent/log.html deleted file mode 100644 index 455294705..000000000 --- a/www/templates/recent/log.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - -
- - - -
- - -
{{entry.fmt_time}}
-
{{entry.ID}} | {{entry.level}} | {{entry.message}}
-
- -
-
-
diff --git a/www/templates/recent/sensedData.html b/www/templates/recent/sensedData.html deleted file mode 100644 index 5f631635f..000000000 --- a/www/templates/recent/sensedData.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - -
- - -
- - -
{{entry.metadata.write_fmt_time}}
-
{{entry.data}}
-
-
-
-
diff --git a/www/templates/splash/splash.html b/www/templates/splash/splash.html deleted file mode 100644 index 901f6359c..000000000 --- a/www/templates/splash/splash.html +++ /dev/null @@ -1,7 +0,0 @@ - - -
- -
-
-
diff --git a/www/templates/survey/enketo/demographics-button.html b/www/templates/survey/enketo/demographics-button.html deleted file mode 100644 index 4396410e2..000000000 --- a/www/templates/survey/enketo/demographics-button.html +++ /dev/null @@ -1,6 +0,0 @@ -
-
{{'control.edit-demographics'}}
- -
diff --git a/www/templates/survey/enketo/form-base.html b/www/templates/survey/enketo/form-base.html deleted file mode 100644 index bd933cc4a..000000000 --- a/www/templates/survey/enketo/form-base.html +++ /dev/null @@ -1,43 +0,0 @@ -
- -
diff --git a/www/templates/survey/enketo/inline.html b/www/templates/survey/enketo/inline.html deleted file mode 100644 index 34b61e282..000000000 --- a/www/templates/survey/enketo/inline.html +++ /dev/null @@ -1,40 +0,0 @@ - - -
{{'survey.loading-prior-survey'}}
-
-
- -
- -
{{'survey.prev-survey-found'}}
-
-
- -
-
- -
-
- -
-
- - -
-
- -
- -
-
-
-
- - -
-
-
diff --git a/www/templates/survey/enketo/modal.html b/www/templates/survey/enketo/modal.html deleted file mode 100644 index cc9c6c02d..000000000 --- a/www/templates/survey/enketo/modal.html +++ /dev/null @@ -1,13 +0,0 @@ - - -

{{'survey.survey'}}

- -
- - - -
diff --git a/www/templates/survey/enketo/preview.html b/www/templates/survey/enketo/preview.html deleted file mode 100644 index 0a51a4141..000000000 --- a/www/templates/survey/enketo/preview.html +++ /dev/null @@ -1,6 +0,0 @@ - From 4f3d9fb1a1ecc2982ce0b81f2f80d80cc262d502 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 16 Oct 2023 11:52:34 -0600 Subject: [PATCH 085/134] fix indentations now consistent 2-space indentation --- www/__mocks__/fileSystemMocks.ts | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/www/__mocks__/fileSystemMocks.ts b/www/__mocks__/fileSystemMocks.ts index fb1258692..a7610cba7 100644 --- a/www/__mocks__/fileSystemMocks.ts +++ b/www/__mocks__/fileSystemMocks.ts @@ -1,18 +1,17 @@ export const mockFileSystem = () => { - window['resolveLocalFileSystemURL'] = function (parentDir, handleFS) { - const fs = {"filesystem": - {"root": - {"getFile": (path, options, onSuccess) => { - let fileEntry = {"file": (handleFile) => { - let file = new File(["this is a mock"], "loggerDB"); - handleFile(file); - }} - onSuccess(fileEntry); - } - } - } - } - console.log("in mock, fs is ", fs, " get File is ", fs.filesystem.root.getFile); - handleFS(fs); + window['resolveLocalFileSystemURL'] = function (parentDir, handleFS) { + const fs = {"filesystem": + {"root": + {"getFile": (path, options, onSuccess) => { + let fileEntry = {"file": (handleFile) => { + let file = new File(["this is a mock"], "loggerDB"); + handleFile(file); + }} + onSuccess(fileEntry); + }} + } } - } \ No newline at end of file + console.log("in mock, fs is ", fs, " get File is ", fs.filesystem.root.getFile); + handleFS(fs); + } +} \ No newline at end of file From 185813f17ba6e030616b08da58d0bbf3c3ebc775 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 16 Oct 2023 11:59:12 -0600 Subject: [PATCH 086/134] better object declaration https://github.com/e-mission/e-mission-phone/pull/1053#discussion_r1361009142 --- www/__mocks__/fileSystemMocks.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/www/__mocks__/fileSystemMocks.ts b/www/__mocks__/fileSystemMocks.ts index a7610cba7..4b0e8cd3f 100644 --- a/www/__mocks__/fileSystemMocks.ts +++ b/www/__mocks__/fileSystemMocks.ts @@ -1,9 +1,9 @@ export const mockFileSystem = () => { window['resolveLocalFileSystemURL'] = function (parentDir, handleFS) { - const fs = {"filesystem": - {"root": - {"getFile": (path, options, onSuccess) => { - let fileEntry = {"file": (handleFile) => { + const fs = {filesystem: + {root: + {getFile: (path, options, onSuccess) => { + let fileEntry = {file: (handleFile) => { let file = new File(["this is a mock"], "loggerDB"); handleFile(file); }} From 655ddda715cff56b491a12b4cd68946ac9e427cc Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 16 Oct 2023 12:01:15 -0600 Subject: [PATCH 087/134] additional indentation improvement --- www/__mocks__/fileSystemMocks.ts | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/www/__mocks__/fileSystemMocks.ts b/www/__mocks__/fileSystemMocks.ts index 4b0e8cd3f..d7c2743ac 100644 --- a/www/__mocks__/fileSystemMocks.ts +++ b/www/__mocks__/fileSystemMocks.ts @@ -1,17 +1,23 @@ export const mockFileSystem = () => { window['resolveLocalFileSystemURL'] = function (parentDir, handleFS) { - const fs = {filesystem: - {root: - {getFile: (path, options, onSuccess) => { - let fileEntry = {file: (handleFile) => { - let file = new File(["this is a mock"], "loggerDB"); - handleFile(file); - }} - onSuccess(fileEntry); - }} + const fs = { + filesystem: + { + root: + { + getFile: (path, options, onSuccess) => { + let fileEntry = { + file: (handleFile) => { + let file = new File(["this is a mock"], "loggerDB"); + handleFile(file); + } + } + onSuccess(fileEntry); + } + } } } - console.log("in mock, fs is ", fs, " get File is ", fs.filesystem.root.getFile); - handleFS(fs); + console.log("in mock, fs is ", fs, " get File is ", fs.filesystem.root.getFile); + handleFS(fs); } } \ No newline at end of file From 4026423b36f5098f3f2d1121742177cd74e36174 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 16 Oct 2023 12:16:08 -0600 Subject: [PATCH 088/134] remove logError https://github.com/e-mission/e-mission-phone/pull/1053#discussion_r1361017447 --- www/js/control/uploadService.ts | 4 ++-- www/js/plugin/logger.ts | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/www/js/control/uploadService.ts b/www/js/control/uploadService.ts index 93557bdfe..c3a7a520e 100644 --- a/www/js/control/uploadService.ts +++ b/www/js/control/uploadService.ts @@ -1,4 +1,4 @@ -import { logDebug, logInfo, logError, displayError, displayErrorMsg } from "../plugin/logger"; +import { logDebug, logInfo, displayError } from "../plugin/logger"; import i18next from "i18next"; /** @@ -23,7 +23,7 @@ async function getUploadConfig() { url.push(uploadConfig["url"]); resolve(url); } catch (err) { - logError("Error while reading default upload config" + err); + displayError(err, "Error while reading default upload config"); reject(err); } } diff --git a/www/js/plugin/logger.ts b/www/js/plugin/logger.ts index a8a461bb6..d127f5549 100644 --- a/www/js/plugin/logger.ts +++ b/www/js/plugin/logger.ts @@ -33,9 +33,6 @@ export const logInfo = (message: string) => export const logWarn = (message: string) => window['Logger'].log(window['Logger'].LEVEL_WARN, message); -export const logError = (message: string) => - window['Logger'].log(window['Logger'].LEVEL_ERROR, message); - export function displayError(error: Error, title?: string) { const errorMsg = error.message ? error.message + '\n' + error.stack : JSON.stringify(error); displayErrorMsg(errorMsg, title); From 0c1bac8c9198c415673887a86d2918a6d59cfa59 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 16 Oct 2023 13:52:28 -0600 Subject: [PATCH 089/134] remove uneeded code --- www/js/control/ProfileSettings.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 2552b37f0..99c7c4a8a 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -222,7 +222,7 @@ const ProfileSettings = () => { //methods that control the settings const uploadLog = function () { if(uploadReason != "") { - let reason = uploadReason.split('').join(''); + let reason = uploadReason; uploadFile("loggerDB", reason); setUploadVis(false); } From 82f7be6f8d1236a4b995ce9edb666ffab688664e Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Mon, 16 Oct 2023 17:09:58 -0700 Subject: [PATCH 090/134] json.config.json --- jest.config.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 jest.config.json diff --git a/jest.config.json b/jest.config.json new file mode 100644 index 000000000..e69de29bb From 6b594916ce5aa16a708bebe2d72be599e1bc9a27 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Mon, 16 Oct 2023 17:11:12 -0700 Subject: [PATCH 091/134] delete jest.config.json --- jest.config.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 jest.config.json diff --git a/jest.config.json b/jest.config.json deleted file mode 100644 index e69de29bb..000000000 From 140ff671122b5523090142a65ee7bc3026bbdd0e Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Mon, 16 Oct 2023 17:14:58 -0700 Subject: [PATCH 092/134] add file to resolve deleted file merge conflict --- jest.config.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 jest.config.json diff --git a/jest.config.json b/jest.config.json new file mode 100644 index 000000000..e69de29bb From a289789ca5620c23319f6a7fdbcfa41e132e68f1 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Mon, 16 Oct 2023 17:16:50 -0700 Subject: [PATCH 093/134] delete jest.config.json --- jest.config.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 jest.config.json diff --git a/jest.config.json b/jest.config.json deleted file mode 100644 index e69de29bb..000000000 From b6e3c65f74fa31f6dc7e0c2d9044875da82db8e7 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:42:13 -0600 Subject: [PATCH 094/134] Update README.md Adding suggested changes Co-authored-by: shankari --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7fca214aa..6fdf01891 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ This is the phone component of the e-mission system. :sparkles: This has been upgraded to the latest **Android**, **iOS**, **cordova-lib**, **node** and **npm** versions. __This is ready to build out of the box.__ -For the latest versions, refer [`package.cordovabuild.json`](package.cordovabuild.json) +The currently supported versions are in [`package.cordovabuild.json`](package.cordovabuild.json) Additional Documentation --- From 2b5735e411a514e0e5aa1aa74dadf2902ee6cc38 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:43:10 -0600 Subject: [PATCH 095/134] Update README.md Committing suggested changes Co-authored-by: Jack Greenlee --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6fdf01891..9db9d3798 100644 --- a/README.md +++ b/README.md @@ -212,7 +212,7 @@ AND/OR ``` npm run ``` -for builds, refer [`package.cordovabuild.json`](package.cordovabuild.json) +For other options of build scripts, refer to [`package.cordovabuild.json`](package.cordovabuild.json)
Expected output From cb954d1604a34cbc897992835539ebaaecc2bdd2 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:54:17 -0600 Subject: [PATCH 096/134] Update README.md Committing changes based on suggestions - Wording - Listing common build types --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 9db9d3798..59f725934 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,12 @@ AND/OR npm run ``` For other options of build scripts, refer to [`package.cordovabuild.json`](package.cordovabuild.json) +Common ones listed below (both, Android, iOS) +``` +npm run build +npm run build-prod-android +npm run build-prod-ios +```
Expected output From ebb6eae5071af6fdfdd354febbe187c1ad9a01c4 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Tue, 17 Oct 2023 10:59:20 -0600 Subject: [PATCH 097/134] Adding more thorough ImperialConfig testing useImperialConfig.test.ts - Used more values to test the limit more on the formatForDisplay unit tests --- www/__tests__/useImperialConfig.test.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/www/__tests__/useImperialConfig.test.ts b/www/__tests__/useImperialConfig.test.ts index b78fe974d..cab1b5a11 100644 --- a/www/__tests__/useImperialConfig.test.ts +++ b/www/__tests__/useImperialConfig.test.ts @@ -14,17 +14,24 @@ jest.mock('../js/useAppConfig', () => { describe('formatForDisplay', () => { it('should round to the nearest integer when value is >= 100', () => { expect(formatForDisplay(105)).toBe('105'); - expect(formatForDisplay(119)).toBe('119'); + expect(formatForDisplay(119.01)).toBe('119'); + expect(formatForDisplay(119.91)).toBe('120'); }); it('should round to 3 significant digits when 1 <= value < 100', () => { expect(formatForDisplay(7.02)).toBe('7.02'); - expect(formatForDisplay(11.3)).toBe('11.3'); + expect(formatForDisplay(9.6262)).toBe('9.63'); + expect(formatForDisplay(11.333)).toBe('11.3'); + expect(formatForDisplay(99.99)).toBe('100'); }); it('should round to 2 decimal places when value < 1', () => { - expect(formatForDisplay(0.07)).toBe('0.07'); + expect(formatForDisplay(0.07178)).toBe('0.07'); + expect(formatForDisplay(0.08978)).toBe('0.09'); expect(formatForDisplay(0.75)).toBe('0.75'); + expect(formatForDisplay(0.001)).toBe('0'); + expect(formatForDisplay(0.006)).toBe('0.01'); + expect(formatForDisplay(0.00001)).toBe('0'); }); }); From 36b56daad2be26a6ce0ed1241797f6048707a65e Mon Sep 17 00:00:00 2001 From: Sebastian Barry <61334340+sebastianbarry@users.noreply.github.com> Date: Tue, 17 Oct 2023 11:07:17 -0600 Subject: [PATCH 098/134] Correct text in comment > JGreenlee > It's the other way around. labelOptions first, then i18next. > The code is correct but the comment is not --- www/js/survey/multilabel/confirmHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/survey/multilabel/confirmHelper.ts b/www/js/survey/multilabel/confirmHelper.ts index c7cf74c26..cb94c1f2e 100644 --- a/www/js/survey/multilabel/confirmHelper.ts +++ b/www/js/survey/multilabel/confirmHelper.ts @@ -50,7 +50,7 @@ export async function getLabelOptions(appConfigParam?) { for (const opt in labelOptions) { labelOptions[opt]?.forEach?.((o, i) => { const translationKey = o.value; - // If translation exists in i18next, use that. Otherwise, use the one in the labelOptions. + // If translation exists in labelOptions, use that. Otherwise, use the one in the i18next. const translation = labelOptions.translations[lang][translationKey] || i18next.t(`multilabel.${translationKey}`); labelOptions[opt][i].text = translation; }); From 202f6dbbe71db89cce6e9e83a9a4a1dc5362d082 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Tue, 17 Oct 2023 11:11:45 -0600 Subject: [PATCH 099/134] Adding spaces between slashes in text > JGreenlee > While we are rewriting and have this opportunity, can we make all the text with slashes look "like / this" instead of "like/ this"? > I believe they were made like that a while ago to force a line break. But we don't do that anymore in the UI so the slashes end up looking weird. --- www/i18n/en.json | 12 ++++++------ www/json/label-options.json.sample | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/www/i18n/en.json b/www/i18n/en.json index 07d31c2b3..9217339f7 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -157,7 +157,7 @@ "e_car_drove_alone": "E-Car Drove Alone", "e_car_shared_ride": "E-Car Shared Ride", "moped": "Moped", - "taxi": "Taxi/Uber/Lyft", + "taxi": "Taxi / Uber / Lyft", "bus": "Bus", "train": "Train", "free_shuttle": "Free Shuttle", @@ -171,12 +171,12 @@ "transit_transfer": "Transit transfer", "shopping": "Shopping", "meal": "Meal", - "pick_drop_person": "Pick-up/ Drop off Person", - "pick_drop_item": "Pick-up/ Drop off Item", - "personal_med": "Personal/ Medical", + "pick_drop_person": "Pick-up / Drop off Person", + "pick_drop_item": "Pick-up / Drop off Item", + "personal_med": "Personal / Medical", "access_recreation": "Access Recreation", - "exercise": "Recreation/ Exercise", - "entertainment": "Entertainment/ Social", + "exercise": "Recreation / Exercise", + "entertainment": "Entertainment / Social", "religious": "Religious", "other": "Other" }, diff --git a/www/json/label-options.json.sample b/www/json/label-options.json.sample index 9d3447bda..0f4292c7f 100644 --- a/www/json/label-options.json.sample +++ b/www/json/label-options.json.sample @@ -63,7 +63,7 @@ "e_car_drove_alone": "E-Car Drove Alone", "e_car_shared_ride": "E-Car Shared Ride", "moped": "Moped", - "taxi": "Taxi/Uber/Lyft", + "taxi": "Taxi / Uber / Lyft", "bus": "Bus", "train": "Train", "free_shuttle": "Free Shuttle", @@ -77,12 +77,12 @@ "transit_transfer": "Transit transfer", "shopping": "Shopping", "meal": "Meal", - "pick_drop_person": "Pick-up/ Drop off Person", - "pick_drop_item": "Pick-up/ Drop off Item", - "personal_med": "Personal/ Medical", + "pick_drop_person": "Pick-up / Drop off Person", + "pick_drop_item": "Pick-up / Drop off Item", + "personal_med": "Personal / Medical", "access_recreation": "Access Recreation", - "exercise": "Recreation/ Exercise", - "entertainment": "Entertainment/ Social", + "exercise": "Recreation / Exercise", + "entertainment": "Entertainment / Social", "religious": "Religious", "other": "Other" }, @@ -97,7 +97,7 @@ "e_car_drove_alone": "e-coche, Condujo solo", "e_car_shared_ride": "e-coche, Condujo con ontras", "moped": "Ciclomotor", - "taxi": "Taxi/Uber/Lyft", + "taxi": "Taxi / Uber / Lyft", "bus": "Autobús", "train": "Tren", "free_shuttle": "Colectivo gratuito", @@ -111,12 +111,12 @@ "transit_transfer": "Transbordo", "shopping": "Compras", "meal": "Comida", - "pick_drop_person": "Recoger/ Entregar Individuo", - "pick_drop_item": "Recoger/ Entregar Objeto", - "personal_med": "Personal/ Médico", + "pick_drop_person": "Recoger / Entregar Individuo", + "pick_drop_item": "Recoger / Entregar Objeto", + "personal_med": "Personal / Médico", "access_recreation": "Acceder a Recreación", - "exercise": "Recreación/ Ejercicio", - "entertainment": "Entretenimiento/ Social", + "exercise": "Recreación / Ejercicio", + "entertainment": "Entretenimiento / Social", "religious": "Religioso", "other": "Otros" } From 9be110d47e85b9d3aeecad5ee68502eabae535f6 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 17 Oct 2023 15:45:54 -0400 Subject: [PATCH 100/134] i18n: fix merge translations if structure differs If the structure is different between lang and fallbackLang such that a key is an object in fallbackLang and a string in lang, we experience an error when we attempt to assign fill in the property to the string. This change will skip filling in if the key is not an object in *both* lang and fallbackLang, thus preventing the error. --- www/js/i18nextInit.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/i18nextInit.ts b/www/js/i18nextInit.ts index 48177caf5..a2688d66e 100644 --- a/www/js/i18nextInit.ts +++ b/www/js/i18nextInit.ts @@ -22,14 +22,14 @@ const mergeInTranslations = (lang, fallbackLang) => { if (__DEV__) { if (typeof value === 'string') { lang[key] = `🌐${value}` - } else if (typeof value === 'object') { + } else if (typeof value === 'object' && typeof lang[key] === 'object') { lang[key] = {}; mergeInTranslations(lang[key], value); } } else { lang[key] = value; } - } else if (typeof value === 'object') { + } else if (typeof value === 'object' && typeof lang[key] === 'object') { mergeInTranslations(lang[key], fallbackLang[key]) } }); From 259778a0d994846693de0ec405e051f8ab09f040 Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Tue, 17 Oct 2023 14:22:47 -0600 Subject: [PATCH 101/134] Changed "typeof x === 'undefined'" to !x This handles all falsy/truthy values instead of only checking for undefined --- www/js/survey/multilabel/infinite_scroll_filters.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/www/js/survey/multilabel/infinite_scroll_filters.ts b/www/js/survey/multilabel/infinite_scroll_filters.ts index 68c28b35e..8d71266d9 100644 --- a/www/js/survey/multilabel/infinite_scroll_filters.ts +++ b/www/js/survey/multilabel/infinite_scroll_filters.ts @@ -10,21 +10,21 @@ import i18next from "i18next"; const unlabeledCheck = (t) => { return t.INPUTS - .map((inputType, index) => typeof t.userInput[inputType] === 'undefined') + .map((inputType, index) => !t.userInput[inputType]) .reduce((acc, val) => acc || val, false); } const invalidCheck = (t) => { const retVal = - (typeof t.userInput['MODE'] !== 'undefined' && t.userInput['MODE'].value === 'pilot_ebike') && - (typeof t.userInput['REPLACED_MODE'] === 'undefined' || + (t.userInput['MODE'] && t.userInput['MODE'].value === 'pilot_ebike') && + (!t.userInput['REPLACED_MODE'] || t.userInput['REPLACED_MODE'].value === 'pilot_ebike' || t.userInput['REPLACED_MODE'].value === 'same_mode'); return retVal; } const toLabelCheck = (trip) => { - if (typeof trip.expectation !== 'undefined') { + if (trip.expectation) { console.log(trip.expectation.to_label) return trip.expectation.to_label && unlabeledCheck(trip); } else { From 46439496147ce1dac70a4c82bbe7615f5357fce6 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 18 Oct 2023 00:18:41 -0400 Subject: [PATCH 102/134] Apply suggestions from code review --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 59f725934..28f6fe615 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ If connecting to a development server over http, make sure to turn on http suppo ``` -### Run in the emulator +### Building the app ``` npm run From 685e1b65b718f04f224945514ade63c43743da0e Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 18 Oct 2023 00:33:28 -0400 Subject: [PATCH 103/134] Update README.md Reword the "Building the app" section to make it clearer what scripts are available and what they do --- README.md | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 28f6fe615..9e3b865c0 100644 --- a/README.md +++ b/README.md @@ -205,22 +205,17 @@ If connecting to a development server over http, make sure to turn on http suppo ### Building the app -``` -npm run -``` -AND/OR -``` -npm run -``` -For other options of build scripts, refer to [`package.cordovabuild.json`](package.cordovabuild.json) -Common ones listed below (both, Android, iOS) -``` -npm run build -npm run build-prod-android -npm run build-prod-ios -``` +We offer a set of build scripts you can pick from, each of which i) bundle the JS with Webpack and then ii) proceed with a Cordova build. +You can bundle JS in 'production' or 'dev' mode, and you can build Android or iOS or both. +The common use cases will be: + +- `npm run build` (to build for production on both Android and iOS platforms) +- `npm run build-prod-android` (to build for production on Android platform only) +- `npm run build-prod-ios` (to build for production on iOS platform only) + +Find the full list of these scripts in [`package.cordovabuild.json`](package.cordovabuild.json) -
Expected output +
Expected output (Android build) ``` BUILD SUCCESSFUL in 2m 48s From 047c2b72150bcd373f08100b569376ca82a3efca Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 18 Oct 2023 00:38:52 -0400 Subject: [PATCH 104/134] Update README.md Remove the note about putting your own configuration files in the `/json` directory. We use the dynamic config now and encourage people to use it rather than making local code changes. --- README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/README.md b/README.md index 9e3b865c0..28d2940df 100644 --- a/README.md +++ b/README.md @@ -36,15 +36,6 @@ Run the setup script bash setup/setup_serve.sh ``` -**(optional)** Configure by changing the files in `www/json`. -Defaults are in `www/json/*.sample` - -``` -ls www/json/*.sample -cp www/json/startupConfig.json.sample www/json/startupConfig.json -cp ..... www/json/connectionConfig.json -``` - ### Activation (after install, and in every new shell) ``` From 5f9d189fb6e1723587aa2907dc1f8b9fffd5e51a Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 18 Oct 2023 00:59:57 -0400 Subject: [PATCH 105/134] Update README.md update outdated comments about connectionConfig.json to reflect that we don't use connectionConfig.json anymore; this is replaced by the 'server' field of the dynamic config. If not present, it uses localhost. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 28d2940df..f15d413c5 100644 --- a/README.md +++ b/README.md @@ -79,9 +79,9 @@ A lot of the visualizations that we display in the phone client come from the se are available in the [e-mission-server README](https://github.com/e-mission/e-mission-server/blob/master/README.md). -In order to make end to end testing easy, if the local server is started on a HTTP (versus HTTPS port), it is in development mode. By default, the phone app connects to the local server (localhost on iOS, [10.0.2.2 on android](https://stackoverflow.com/questions/5806220/how-to-connect-to-my-http-localhost-web-server-from-android-emulator-in-eclips)) with the `prompted-auth` authentication method. To connect to a different server, or to use a different authentication method, you need to create a `www/json/connectionConfig.json` file. More details on configuring authentication [can be found in the docs](https://github.com/e-mission/e-mission-docs/blob/master/docs/install/configuring_authentication.md). +The dynamic config (see https://github.com/e-mission/nrel-openpath-deploy-configs) controls the server endpoint that the phone app will connect to. If you are running the app in an emulator on the same machine as your local server (i.e. they share a `localhost`), you can use one of the `dev-emulator-*` configs (these configs have no `server` specified so `localhost` is assumed). -One advantage of using `skip` authentication in development mode is that any user email can be entered without a password. Developers can use one of the emails that they loaded test data for in step (3) above. So if the test data loaded was with `-u shankari@eecs.berkeley.edu`, then the login email for the phone app would also be `shankari@eecs.berkeley.edu`. +If you wish to connect to a different server, create your own config file according to https://github.com/e-mission/nrel-openpath-deploy-configs and specify the `server` field accordingly. Updating the e-mission-\* plugins or adding new plugins --- From c6dc2e5ab2bceb4081835383d537861714648860 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 18 Oct 2023 01:04:57 -0400 Subject: [PATCH 106/134] Update README.md reword "Building the app" --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f15d413c5..1282b2c6e 100644 --- a/README.md +++ b/README.md @@ -196,14 +196,14 @@ If connecting to a development server over http, make sure to turn on http suppo ### Building the app -We offer a set of build scripts you can pick from, each of which i) bundle the JS with Webpack and then ii) proceed with a Cordova build. -You can bundle JS in 'production' or 'dev' mode, and you can build Android or iOS or both. +We offer a set of build scripts to pick from, each of which: (i) bundle the JS with Webpack, and then (ii) proceed with a Cordova build. The common use cases will be: - `npm run build` (to build for production on both Android and iOS platforms) - `npm run build-prod-android` (to build for production on Android platform only) - `npm run build-prod-ios` (to build for production on iOS platform only) +There are a variety of options because Webpack can bundle the JS in 'production' or 'dev' mode, and you can build Android or iOS or both. Find the full list of these scripts in [`package.cordovabuild.json`](package.cordovabuild.json)
Expected output (Android build) From 96d29e6c887489381e910cad8441df3791a798b8 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 18 Oct 2023 01:15:50 -0400 Subject: [PATCH 107/134] Update README.md make "Updating the UI only" a bit clearer: - you don't have to manually type the IP address in the devapp if it's on localhost - you do have to type it manually if it's on a different device, and you should make sure that it's on the same network --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1282b2c6e..0f2b4642e 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,9 @@ source setup/activate_serve.sh .... ``` -1. Change the devapp connection URL to one of these (e.g. 192.168.162.1:3000) and press "Connect" +1. Change the devapp connection URL and press "Connect" + - If you are running the devapp in an emulator on the same machine as the devapp server, you may simply use localhost, which would be `127.0.0.1:3000` on iOS and `10.0.2.2:3000` on Android. + - If you are running the devapp on a different device, you must type the address manually (e.g. `192.168.162.1:3000`). Note that this is a local IP address; the devices must be on the same network 1. The app will now display the version of e-mission app that is in your local directory 1. The console logs will be displayed back in the server window (prefaced by `[console]`) 1. Breakpoints can be added by connecting through the browser @@ -66,7 +68,7 @@ source setup/activate_serve.sh **Ta-da!** :gift: If you change any of the files in the `www` directory, the app will automatically be re-loaded without manually restarting either the server or the app :tada: -**Note1**: You may need to scroll up, past all the warnings about `Content Security Policy has been added` to find the port that the server is listening to. +**Note**: You may need to scroll up, past all the warnings about `Content Security Policy has been added` to find the port that the server is listening to. End to end testing --- From 079f4ea667a876b06a23475f52041d78c58ffe4f Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 18 Oct 2023 01:22:00 -0400 Subject: [PATCH 108/134] Update README.md Remove the note about putting your own configuration files in the `/json` directory. We use the dynamic config now and encourage people to use it rather than making local code changes. --- README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/README.md b/README.md index 0f2b4642e..3c5a12e8c 100644 --- a/README.md +++ b/README.md @@ -153,15 +153,6 @@ AND/OR bash setup/setup_ios_native.sh ``` -**(optional)** Configure by changing the files in `www/json`. -Defaults are in `www/json/*.sample` - -``` -ls www/json/*.sample -cp www/json/startupConfig.json.sample www/json/startupConfig.json -cp ..... www/json/connectionConfig.json -``` - ### Activation (after install, and in every new shell) ``` From bbb0f68ff6e83e40b8e563089ceb99afbb712628 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 18 Oct 2023 12:33:44 -0600 Subject: [PATCH 109/134] renameConsentPage to protocolPage --- www/js/onboarding/{ConsentPage.tsx => ProtocolPage.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename www/js/onboarding/{ConsentPage.tsx => ProtocolPage.tsx} (100%) diff --git a/www/js/onboarding/ConsentPage.tsx b/www/js/onboarding/ProtocolPage.tsx similarity index 100% rename from www/js/onboarding/ConsentPage.tsx rename to www/js/onboarding/ProtocolPage.tsx From 18f384f224a377381ce53743050ecc91d4a62044 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 18 Oct 2023 12:37:51 -0600 Subject: [PATCH 110/134] alter timing of markConsented in order to fix the bug, we're changing when we mark the consent in storage. Moving the call to after the user has agreed to both parts -- protocol AND permissions, prevents the early popup. For more discussion see: https://github.com/e-mission/e-mission-docs/issues/1006#issuecomment-1769073063 --- www/js/App.tsx | 6 +++--- www/js/onboarding/OnboardingStack.tsx | 6 +++--- www/js/onboarding/ProtocolPage.tsx | 11 +++++------ www/js/onboarding/SaveQrPage.tsx | 15 +++++++++------ www/js/onboarding/onboardingHelper.ts | 17 ++++++++++------- 5 files changed, 30 insertions(+), 25 deletions(-) diff --git a/www/js/App.tsx b/www/js/App.tsx index 2187118fa..3c6c8bec9 100644 --- a/www/js/App.tsx +++ b/www/js/App.tsx @@ -91,9 +91,9 @@ const App = () => { {appContent} - { /* If we are past the consent page (route > CONSENT), the permissions popup can show if needed. - This also includes if onboarding is DONE altogether (because "DONE" is > "CONSENT") */ } - {(onboardingState && onboardingState.route > OnboardingRoute.CONSENT) && + { /* If we are fully consented, (route > PROTOCOL), the permissions popup can show if needed. + This also includes if onboarding is DONE altogether (because "DONE" is > "PROTOCOL") */ } + {(onboardingState && onboardingState.route > OnboardingRoute.PROTOCOL) && } diff --git a/www/js/onboarding/OnboardingStack.tsx b/www/js/onboarding/OnboardingStack.tsx index a49bde3ab..c547fd074 100644 --- a/www/js/onboarding/OnboardingStack.tsx +++ b/www/js/onboarding/OnboardingStack.tsx @@ -2,7 +2,7 @@ import React, { useContext } from "react"; import { StyleSheet } from "react-native"; import { AppContext } from "../App"; import WelcomePage from "./WelcomePage"; -import ConsentPage from "./ConsentPage"; +import ProtocolPage from "./ProtocolPage"; import SurveyPage from "./SurveyPage"; import SaveQrPage from "./SaveQrPage"; import SummaryPage from "./SummaryPage"; @@ -19,8 +19,8 @@ const OnboardingStack = () => { return ; } else if (onboardingState.route == OnboardingRoute.SUMMARY) { return ; - } else if (onboardingState.route == OnboardingRoute.CONSENT) { - return ; + } else if (onboardingState.route == OnboardingRoute.PROTOCOL) { + return ; } else if (onboardingState.route == OnboardingRoute.SAVE_QR) { return ; } else if (onboardingState.route == OnboardingRoute.SURVEY) { diff --git a/www/js/onboarding/ProtocolPage.tsx b/www/js/onboarding/ProtocolPage.tsx index 08aa3ab48..73961245a 100644 --- a/www/js/onboarding/ProtocolPage.tsx +++ b/www/js/onboarding/ProtocolPage.tsx @@ -7,8 +7,9 @@ import { AppContext } from '../App'; import { getAngularService } from '../angular-react-helper'; import PrivacyPolicy from './PrivacyPolicy'; import { onboardingStyles } from './OnboardingStack'; +import { setProtocolDone } from './onboardingHelper'; -const ConsentPage = () => { +const ProtocolPage = () => { const { t } = useTranslation(); const context = useContext(AppContext); @@ -20,10 +21,8 @@ const ConsentPage = () => { }; function agree() { - const StartPrefs = getAngularService('StartPrefs'); - StartPrefs.markConsented().then((response) => { - refreshOnboardingState(); - }); + setProtocolDone(true); + refreshOnboardingState(); }; // privacy policy and data collection info, followed by accept/reject buttons @@ -40,4 +39,4 @@ const ConsentPage = () => { ); } -export default ConsentPage; +export default ProtocolPage; diff --git a/www/js/onboarding/SaveQrPage.tsx b/www/js/onboarding/SaveQrPage.tsx index 51f884886..658c66993 100644 --- a/www/js/onboarding/SaveQrPage.tsx +++ b/www/js/onboarding/SaveQrPage.tsx @@ -20,12 +20,15 @@ const SaveQrPage = ({ }) => { useEffect(() => { if (overallStatus == true && !registerUserDone) { - logDebug('permissions done, going to log in'); - login(onboardingState.opcode).then((response) => { - logDebug('login done, refreshing onboarding state'); - setRegisterUserDone(true); - preloadDemoSurveyResponse(); - refreshOnboardingState(); + const StartPrefs = getAngularService('StartPrefs'); + StartPrefs.markConsented().then((response) => { + logDebug('permissions done, going to log in'); + login(onboardingState.opcode).then((response) => { + logDebug('login done, refreshing onboarding state'); + setRegisterUserDone(true); + preloadDemoSurveyResponse(); + refreshOnboardingState(); + }); }); } else { logDebug('permissions not done, waiting'); diff --git a/www/js/onboarding/onboardingHelper.ts b/www/js/onboarding/onboardingHelper.ts index cfbebb40b..4a6ec202c 100644 --- a/www/js/onboarding/onboardingHelper.ts +++ b/www/js/onboarding/onboardingHelper.ts @@ -6,12 +6,12 @@ import { logDebug } from "../plugin/logger"; export const INTRO_DONE_KEY = 'intro_done'; // route = WELCOME if no config present -// route = SUMMARY if config present, but not consented and summary not done -// route = CONSENT if config present, but not consented and summary done -// route = SAVE_QR if config present, consented, but save qr not done +// route = SUMMARY if config present, but protocol not done and summary not done +// route = PROTOCOL if config present, but protocol not done and summary done +// route = SAVE_QR if config present, protocol done, but save qr not done // route = SURVEY if config present, consented and save qr done // route = DONE if onboarding is finished (intro_done marked) -export enum OnboardingRoute { WELCOME, SUMMARY, CONSENT, SAVE_QR, SURVEY, DONE }; +export enum OnboardingRoute { WELCOME, SUMMARY, PROTOCOL, SAVE_QR, SURVEY, DONE }; export type OnboardingState = { opcode: string, route: OnboardingRoute, @@ -20,6 +20,9 @@ export type OnboardingState = { export let summaryDone = false; export const setSummaryDone = (b) => summaryDone = b; +export let protocolDone = false; +export const setProtocolDone = (b) => protocolDone = b; + export let saveQrDone = false; export const setSaveQrDone = (b) => saveQrDone = b; @@ -40,10 +43,10 @@ export function getPendingOnboardingState(): Promise { route = OnboardingRoute.DONE; } else if (!config) { route = OnboardingRoute.WELCOME; - } else if (!isConsented && !summaryDone) { + } else if (!protocolDone && !summaryDone) { route = OnboardingRoute.SUMMARY; - } else if (!isConsented) { - route = OnboardingRoute.CONSENT; + } else if (!protocolDone) { + route = OnboardingRoute.PROTOCOL; } else if (!saveQrDone) { route = OnboardingRoute.SAVE_QR; } else { From 86fe118eb42a0f5db940d8b4fbd4acdf60fcd0ea Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 19 Oct 2023 09:16:48 -0600 Subject: [PATCH 111/134] correct outdated comments https://github.com/e-mission/e-mission-phone/pull/1053#discussion_r1365711315 --- www/js/control/uploadService.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/www/js/control/uploadService.ts b/www/js/control/uploadService.ts index c3a7a520e..038bd8efc 100644 --- a/www/js/control/uploadService.ts +++ b/www/js/control/uploadService.ts @@ -73,9 +73,7 @@ function readDBFile(parentDir, database, callbackFn) { } const sendToServer = function upload(url, binArray, params) { - //this was the best way I could find to contact the database, - //had to modify the way it gets handled on the other side - //the original way it could not find "reason" + //use url encoding to pass additional params in the post const urlParams = "?reason=" + params.reason + "&tz=" + params.tz; return fetch(url+urlParams, { method: 'POST', From cac185f788d580c9f09fa4a7b4b1bf5f07f9ba50 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 19 Oct 2023 09:33:33 -0600 Subject: [PATCH 112/134] add comments and timeout this mock was confusing, so I've added comments to indicate what the two parts do. Perviously, there was no timeout in the if, only in the else, now they are consistent --- www/__tests__/uploadService.test.ts | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/www/__tests__/uploadService.test.ts b/www/__tests__/uploadService.test.ts index c0c7f9d04..b845d1ba9 100644 --- a/www/__tests__/uploadService.test.ts +++ b/www/__tests__/uploadService.test.ts @@ -18,16 +18,22 @@ mockFileSystem(); //comnplex mock, allows the readDBFile to work in testing //use this message to verify that the post went through let message = ""; -global.fetch = (url: string, options: {method: string, headers: {}, body: string}) => new Promise((rs, rj) => { - if(options) { - message = "sent " + options.method + options.body + " for " + url;; - rs('sent ' + options.method + options.body + ' to ' + url); - } else { - setTimeout(() => rs({ - json: () => new Promise((rs, rj) => { - setTimeout(() => rs('mock data for ' + url), 100); - }) - })); +//each have a slight delay to mimic a real fetch request +global.fetch = (url: string, options: { method: string, headers: {}, body: string }) => new Promise((rs, rj) => { + //if there's options, that means there is a post request + if (options) { + setTimeout(() => { + message = "sent " + options.method + options.body + " for " + url; + rs('sent ' + options.method + options.body + ' to ' + url); + }, 100); + } + //else it is a fetch request + else { + setTimeout(() => rs({ + json: () => new Promise((rs, rj) => { + setTimeout(() => rs('mock data for ' + url), 100); + }) + })); } }) as any; From 4b148c224a61f658f5d26156a0c03d4b426e89a1 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 19 Oct 2023 10:00:54 -0600 Subject: [PATCH 113/134] fix location of timeout we want to set the message right away, then have a delay on the response itself --- www/__tests__/uploadService.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/__tests__/uploadService.test.ts b/www/__tests__/uploadService.test.ts index b845d1ba9..6aea5805d 100644 --- a/www/__tests__/uploadService.test.ts +++ b/www/__tests__/uploadService.test.ts @@ -22,8 +22,8 @@ let message = ""; global.fetch = (url: string, options: { method: string, headers: {}, body: string }) => new Promise((rs, rj) => { //if there's options, that means there is a post request if (options) { + message = "sent " + options.method + options.body + " for " + url; setTimeout(() => { - message = "sent " + options.method + options.body + " for " + url; rs('sent ' + options.method + options.body + ' to ' + url); }, 100); } From d8a3b33768b06840e9de146fc4bcd90ad7312d29 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 19 Oct 2023 10:24:54 -0600 Subject: [PATCH 114/134] revert uploadSample json file https://github.com/e-mission/e-mission-phone/pull/1053/files/0c1bac8c9198c415673887a86d2918a6d59cfa59#r1364808637 --- www/json/uploadConfig.json.sample | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/json/uploadConfig.json.sample b/www/json/uploadConfig.json.sample index a3c8b7210..53cead55e 100644 --- a/www/json/uploadConfig.json.sample +++ b/www/json/uploadConfig.json.sample @@ -1,3 +1,3 @@ { - "url": "http://localhost:5647/phonelogs" + "url": "http://fill.me.in " } From 58a50a6b941204a93e344305c32cd153d00afe6c Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:05:19 -0600 Subject: [PATCH 115/134] Update README.md Committing suggested change Co-authored-by: K. Shankari --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c5a12e8c..609777cc5 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ Copied config.cordovabuild.xml -> config.xml and package.cordovabuild.json -> pa
-### Activation (after install, and in every new shell) +### Enable HTTP support on android by editing `config.xml` If connecting to a development server over http, make sure to turn on http support on android From 9d3965a1f9607837cc6989cebfdfdad8eaf28743 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:11:56 -0600 Subject: [PATCH 116/134] Update README.md Committing change based on suggestion (adding a link that points to Noel-openpath-deploy-configs) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 609777cc5..111e0d82a 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ are available in the [e-mission-server README](https://github.com/e-mission/e-mi The dynamic config (see https://github.com/e-mission/nrel-openpath-deploy-configs) controls the server endpoint that the phone app will connect to. If you are running the app in an emulator on the same machine as your local server (i.e. they share a `localhost`), you can use one of the `dev-emulator-*` configs (these configs have no `server` specified so `localhost` is assumed). -If you wish to connect to a different server, create your own config file according to https://github.com/e-mission/nrel-openpath-deploy-configs and specify the `server` field accordingly. +If you wish to connect to a different server, create your own config file according to https://github.com/e-mission/nrel-openpath-deploy-configs and specify the `server` field accordingly. [deploy-configs](https://github.com/e-mission/nrel-openpath-deploy-configs/#testing-configs) has more information on this. Updating the e-mission-\* plugins or adding new plugins --- From ca0d569db01f2b7fdd825423fe4f212938494384 Mon Sep 17 00:00:00 2001 From: "K. Shankari" Date: Thu, 19 Oct 2023 13:00:14 -0700 Subject: [PATCH 117/134] clarify link to repo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 111e0d82a..121684e0a 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ are available in the [e-mission-server README](https://github.com/e-mission/e-mi The dynamic config (see https://github.com/e-mission/nrel-openpath-deploy-configs) controls the server endpoint that the phone app will connect to. If you are running the app in an emulator on the same machine as your local server (i.e. they share a `localhost`), you can use one of the `dev-emulator-*` configs (these configs have no `server` specified so `localhost` is assumed). -If you wish to connect to a different server, create your own config file according to https://github.com/e-mission/nrel-openpath-deploy-configs and specify the `server` field accordingly. [deploy-configs](https://github.com/e-mission/nrel-openpath-deploy-configs/#testing-configs) has more information on this. +If you wish to connect to a different server, create your own config file according to https://github.com/e-mission/nrel-openpath-deploy-configs and specify the `server` field accordingly. The [deploy-configs](https://github.com/e-mission/nrel-openpath-deploy-configs/#testing-configs) repo has more information on this. Updating the e-mission-\* plugins or adding new plugins --- From 2c6739a185c109fb24d70ea20ff57236dfa80c4d Mon Sep 17 00:00:00 2001 From: Sebastian Barry Date: Fri, 20 Oct 2023 12:41:24 -0600 Subject: [PATCH 118/134] Remove translations from label-options.json.sample Since the default translations are in 18next files, we don't need a duplicate of the default label options translations in this file Also changed the logic because I didn't have it right when labelOptions.translations is undefined --- www/js/survey/multilabel/confirmHelper.ts | 4 +- www/json/label-options.json.sample | 72 +---------------------- 2 files changed, 3 insertions(+), 73 deletions(-) diff --git a/www/js/survey/multilabel/confirmHelper.ts b/www/js/survey/multilabel/confirmHelper.ts index cb94c1f2e..b668669bf 100644 --- a/www/js/survey/multilabel/confirmHelper.ts +++ b/www/js/survey/multilabel/confirmHelper.ts @@ -50,8 +50,8 @@ export async function getLabelOptions(appConfigParam?) { for (const opt in labelOptions) { labelOptions[opt]?.forEach?.((o, i) => { const translationKey = o.value; - // If translation exists in labelOptions, use that. Otherwise, use the one in the i18next. - const translation = labelOptions.translations[lang][translationKey] || i18next.t(`multilabel.${translationKey}`); + // If translation exists in labelOptions, use that. Otherwise, use the one in the i18next. If there is not "translations" field in labelOptions, defaultly use the one in the i18next. + const translation = labelOptions.translations ? labelOptions.translations[lang][translationKey] || i18next.t(`multilabel.${translationKey}`) : i18next.t(`multilabel.${translationKey}`); labelOptions[opt][i].text = translation; }); } diff --git a/www/json/label-options.json.sample b/www/json/label-options.json.sample index 0f4292c7f..9870b744f 100644 --- a/www/json/label-options.json.sample +++ b/www/json/label-options.json.sample @@ -50,75 +50,5 @@ {"value":"train"}, {"value":"free_shuttle"}, {"value":"other"} - ], - "translations": { - "en": { - "walk": "Walk", - "e-bike": "E-bike", - "bike": "Regular Bike", - "bikeshare": "Bikeshare", - "scootershare": "Scooter share", - "drove_alone": "Gas Car Drove Alone", - "shared_ride": "Gas Car Shared Ride", - "e_car_drove_alone": "E-Car Drove Alone", - "e_car_shared_ride": "E-Car Shared Ride", - "moped": "Moped", - "taxi": "Taxi / Uber / Lyft", - "bus": "Bus", - "train": "Train", - "free_shuttle": "Free Shuttle", - "air": "Air", - "not_a_trip": "Not a trip", - "no_travel": "No travel", - "home": "Home", - "work": "To Work", - "at_work": "At Work", - "school": "School", - "transit_transfer": "Transit transfer", - "shopping": "Shopping", - "meal": "Meal", - "pick_drop_person": "Pick-up / Drop off Person", - "pick_drop_item": "Pick-up / Drop off Item", - "personal_med": "Personal / Medical", - "access_recreation": "Access Recreation", - "exercise": "Recreation / Exercise", - "entertainment": "Entertainment / Social", - "religious": "Religious", - "other": "Other" - }, - "es": { - "walk": "Caminando", - "e-bike": "e-bicicleta", - "bike": "Bicicleta", - "bikeshare": "Bicicleta compartida", - "scootershare": "Motoneta compartida", - "drove_alone": "Coche de Gas, Condujo solo", - "shared_ride": "Coche de Gas, Condujo con otros", - "e_car_drove_alone": "e-coche, Condujo solo", - "e_car_shared_ride": "e-coche, Condujo con ontras", - "moped": "Ciclomotor", - "taxi": "Taxi / Uber / Lyft", - "bus": "Autobús", - "train": "Tren", - "free_shuttle": "Colectivo gratuito", - "air": "Avión", - "not_a_trip": "No es un viaje", - "no_travel": "No viajar", - "home": "Inicio", - "work": "Trabajo", - "at_work": "En el trabajo", - "school": "Escuela", - "transit_transfer": "Transbordo", - "shopping": "Compras", - "meal": "Comida", - "pick_drop_person": "Recoger / Entregar Individuo", - "pick_drop_item": "Recoger / Entregar Objeto", - "personal_med": "Personal / Médico", - "access_recreation": "Acceder a Recreación", - "exercise": "Recreación / Ejercicio", - "entertainment": "Entretenimiento / Social", - "religious": "Religioso", - "other": "Otros" - } - } + ] } \ No newline at end of file From f2f0cfb94dd8675635b622627aec73ca31941b47 Mon Sep 17 00:00:00 2001 From: "K. Shankari" Date: Fri, 20 Oct 2023 16:17:45 -0700 Subject: [PATCH 119/134] FETCH -> GET to match HTTP request types --- www/__tests__/uploadService.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/__tests__/uploadService.test.ts b/www/__tests__/uploadService.test.ts index 6aea5805d..5c64fae0e 100644 --- a/www/__tests__/uploadService.test.ts +++ b/www/__tests__/uploadService.test.ts @@ -27,7 +27,7 @@ global.fetch = (url: string, options: { method: string, headers: {}, body: strin rs('sent ' + options.method + options.body + ' to ' + url); }, 100); } - //else it is a fetch request + //else it is a get request else { setTimeout(() => rs({ json: () => new Promise((rs, rj) => { From aebb1c33aec2ac370507bf496dd3c71d2f51e5a2 Mon Sep 17 00:00:00 2001 From: Shankari Date: Sat, 21 Oct 2023 11:23:26 -0700 Subject: [PATCH 120/134] Build the release version of android when creating the build to upload --- bin/sign_and_align_keys.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/sign_and_align_keys.sh b/bin/sign_and_align_keys.sh index 261058bd5..74c04c020 100644 --- a/bin/sign_and_align_keys.sh +++ b/bin/sign_and_align_keys.sh @@ -10,7 +10,7 @@ fi # Sign and release the L+ version # Make sure the highest supported version has the biggest version code -npm run build-prod-android +npm run build-prod-android-release # cp platforms/android/app/build/outputs/apk/release/app-release-unsigned.aab platforms/android/app/build/outputs/apk/app-release-signed-unaligned.apk jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 -keystore ../config_files/production.keystore ./platforms/android/app/build/outputs/bundle/release/app-release.aab androidproductionkey cp platforms/android/app/build/outputs/bundle/release/app-release.aab $1-build-$2.aab From 284c47c4dfd0e1a2fe7a97d0b15175fedfc2397b Mon Sep 17 00:00:00 2001 From: Shankari Date: Sat, 21 Oct 2023 11:24:12 -0700 Subject: [PATCH 121/134] Improve error message --- bin/sign_and_align_keys.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/sign_and_align_keys.sh b/bin/sign_and_align_keys.sh index 74c04c020..9b60c3ade 100644 --- a/bin/sign_and_align_keys.sh +++ b/bin/sign_and_align_keys.sh @@ -4,7 +4,7 @@ PROJECT=$1 VERSION=$2 if [[ $# -eq 0 ]]; then - echo "No arguments supplied" + echo "sign_and_align_keys " exit 1 fi From a26698fb044c839ab9d83541609780535fe9d7d7 Mon Sep 17 00:00:00 2001 From: Shankari Date: Sat, 21 Oct 2023 11:33:30 -0700 Subject: [PATCH 122/134] Run the CI on UI feature branches as well So that we can check that the tests are passing before we merge --- .github/workflows/serve-install.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/serve-install.yml b/.github/workflows/serve-install.yml index a5e634821..d180a9384 100644 --- a/.github/workflows/serve-install.yml +++ b/.github/workflows/serve-install.yml @@ -9,10 +9,12 @@ on: branches: - master - maint_upgrade_** + - ui_feature_** pull_request: branches: - master - maint_upgrade_** + - ui_feature_** schedule: # * is a special character in YAML so you have to quote this string - cron: '5 4 * * 0' From 34cd182fefa5e89d0596d01a1ed7109bf4e5f9db Mon Sep 17 00:00:00 2001 From: Shankari Date: Sat, 21 Oct 2023 11:53:52 -0700 Subject: [PATCH 123/134] Temporarily add `service_rewrite_2023` to the list of branches that we run the CI on To avoid having to make a new branch and making everybody switch --- .github/workflows/serve-install.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/serve-install.yml b/.github/workflows/serve-install.yml index d180a9384..51852ed71 100644 --- a/.github/workflows/serve-install.yml +++ b/.github/workflows/serve-install.yml @@ -15,6 +15,7 @@ on: - master - maint_upgrade_** - ui_feature_** + - service_rewrite_2023 schedule: # * is a special character in YAML so you have to quote this string - cron: '5 4 * * 0' From 07b7d20ba7bbd385cc0c74600b72928234158146 Mon Sep 17 00:00:00 2001 From: Shankari Date: Sat, 21 Oct 2023 11:57:21 -0700 Subject: [PATCH 124/134] Replace tabs with spaces --- .github/workflows/serve-install.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/serve-install.yml b/.github/workflows/serve-install.yml index 51852ed71..c78ce1f86 100644 --- a/.github/workflows/serve-install.yml +++ b/.github/workflows/serve-install.yml @@ -9,13 +9,13 @@ on: branches: - master - maint_upgrade_** - - ui_feature_** + - ui_feature_** pull_request: branches: - master - maint_upgrade_** - - ui_feature_** - - service_rewrite_2023 + - ui_feature_** + - service_rewrite_2023 schedule: # * is a special character in YAML so you have to quote this string - cron: '5 4 * * 0' From 6da202ce5e13b186c24cd128a80532478a0f566b Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Sat, 21 Oct 2023 17:29:58 -0600 Subject: [PATCH 125/134] add timeout to med accuracy medium accuracy sometimes alters the tracking settings as well, this timeout resolves that --- www/js/control/ProfileSettings.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 0b00efc75..993c43814 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -261,9 +261,11 @@ const ProfileSettings = () => { refreshCollectSettings(); } - async function toggleLowAccuracy() { + async function toggleLowAccuracy() { let toggle = await helperToggleLowAccuracy(); - refreshCollectSettings(); + setTimeout(function() { + refreshCollectSettings(); + }, 1500); } const viewQRCode = function(e) { From 5386a2dc651079f4e0e1139a57531d7b4cb76341 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Sat, 21 Oct 2023 20:50:02 -0600 Subject: [PATCH 126/134] update variable name the prior variable name was inconsistent with the rest of the file, and therefore created confusion --- www/js/control/ProfileSettings.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 993c43814..7cf22a154 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -60,7 +60,7 @@ const ProfileSettings = () => { const [showingSensed, setShowingSensed] = useState(false); const [showingLog, setShowingLog] = useState(false); const [editSync, setEditSync] = useState(false); - const [editCollection, setEditCollection] = useState(false); + const [editCollectionVis, setEditCollectionVis] = useState(false); // const [collectConfig, setCollectConfig] = useState({}); const [collectSettings, setCollectSettings] = useState({}); @@ -156,13 +156,13 @@ const ProfileSettings = () => { //ensure ui table updated when editor closes useEffect(() => { - if(editCollection == false) { + if(editCollectionVis == false) { setTimeout(function() { console.log("closed editor, time to refresh collect"); refreshCollectSettings(); }, 1000); } - }, [editCollection]) + }, [editCollectionVis]) async function refreshNotificationSettings() { console.debug('about to refreshNotificationSettings, notificationSettings = ', notificationSettings); @@ -496,7 +496,7 @@ const ProfileSettings = () => { - + ); From a9d98ed20e3ecda235e81b2f80f81e91826c9b6d Mon Sep 17 00:00:00 2001 From: louisg1337 Date: Sun, 22 Oct 2023 22:03:50 -0400 Subject: [PATCH 127/134] Bumped up version for new location permissions release --- package.cordovabuild.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.cordovabuild.json b/package.cordovabuild.json index 943f06520..25145b5ed 100644 --- a/package.cordovabuild.json +++ b/package.cordovabuild.json @@ -126,7 +126,7 @@ "cordova-plugin-app-version": "0.1.14", "cordova-plugin-customurlscheme": "5.0.2", "cordova-plugin-device": "2.1.0", - "cordova-plugin-em-datacollection": "git+https://github.com/e-mission/e-mission-data-collection.git#v1.7.9", + "cordova-plugin-em-datacollection": "git+https://github.com/e-mission/e-mission-data-collection.git#v1.8.1", "cordova-plugin-em-opcodeauth": "git+https://github.com/e-mission/cordova-jwt-auth.git#v1.7.2", "cordova-plugin-em-server-communication": "git+https://github.com/e-mission/cordova-server-communication.git#v1.2.6", "cordova-plugin-em-serversync": "git+https://github.com/e-mission/cordova-server-sync.git#v1.3.2", From ea49921bb3e473efdc5a62fb25d3f7a21890d056 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Tue, 24 Oct 2023 14:10:51 -0600 Subject: [PATCH 128/134] add more url checks we need to make sure that nobody scans a random qr code and gets into the app with an invalid token, we can help prevent that by checking that the qr code contains the right elements --- www/js/onboarding/WelcomePage.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/www/js/onboarding/WelcomePage.tsx b/www/js/onboarding/WelcomePage.tsx index 3589923c8..a6df7f2ee 100644 --- a/www/js/onboarding/WelcomePage.tsx +++ b/www/js/onboarding/WelcomePage.tsx @@ -20,12 +20,25 @@ const WelcomePage = () => { const [infoPopupVis, setInfoPopupVis] = useState(false); const [existingToken, setExistingToken] = useState(''); + const checkURL = function (result) { + let notCancelled = result.cancelled == false; + let isQR = result.format == "QR_CODE"; + let hasPrefix = false; + if (__DEV__) { + hasPrefix = result.text.startsWith("emission"); + } else { + hasPrefix = result.text.startsWith("nrelopenpath"); + } + let hasToken = result.text.includes("login_token?token"); + + return notCancelled && isQR && hasPrefix && hasToken; + } + const scanCode = function() { - window.cordova.plugins.barcodeScanner.scan( + window['cordova'].plugins.barcodeScanner.scan( function (result) { console.debug("scanned code", result); - if (result.format == "QR_CODE" && - result.cancelled == false) { + if (checkURL(result)) { let text = result.text.split("=")[1]; console.log("found code", text); loginWithToken(text); From 5da390968328cd493c46ba10c67b30fb703f7ff4 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Tue, 24 Oct 2023 17:21:03 -0600 Subject: [PATCH 129/134] updates to check code added a log statement, and verifying that the first part of the opcode is "nrelopenpath" or "emission" -- the staging opcodes start with "emission", but sometimes we use production opcodes to test things in develpoment --- www/js/onboarding/WelcomePage.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/www/js/onboarding/WelcomePage.tsx b/www/js/onboarding/WelcomePage.tsx index a6df7f2ee..cd3734059 100644 --- a/www/js/onboarding/WelcomePage.tsx +++ b/www/js/onboarding/WelcomePage.tsx @@ -5,7 +5,7 @@ import { Button, Dialog, Divider, IconButton, Surface, Text, TextInput, Touchabl import color from 'color'; import { initByUser } from '../config/dynamicConfig'; import { AppContext } from '../App'; -import { displayError } from "../plugin/logger"; +import { displayError, logDebug } from "../plugin/logger"; import { onboardingStyles } from './OnboardingStack'; import { Icon } from '../components/Icon'; @@ -23,14 +23,11 @@ const WelcomePage = () => { const checkURL = function (result) { let notCancelled = result.cancelled == false; let isQR = result.format == "QR_CODE"; - let hasPrefix = false; - if (__DEV__) { - hasPrefix = result.text.startsWith("emission"); - } else { - hasPrefix = result.text.startsWith("nrelopenpath"); - } + let hasPrefix = result.text.split(":")[0] == "nrelopenpath" || result.text.split(":")[0] == "emission"; let hasToken = result.text.includes("login_token?token"); + logDebug("QR code " + result.text + " checks: cancel, format, prefix, params " + notCancelled + isQR + hasPrefix + hasToken); + return notCancelled && isQR && hasPrefix && hasToken; } From 636051936971075d7b5179e5919ad7b234866b72 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Tue, 24 Oct 2023 17:51:28 -0600 Subject: [PATCH 130/134] Update QrCode.tsx make sure qr code is made with the whole url link --- www/js/components/QrCode.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/www/js/components/QrCode.tsx b/www/js/components/QrCode.tsx index edd120c22..0499b4c46 100644 --- a/www/js/components/QrCode.tsx +++ b/www/js/components/QrCode.tsx @@ -35,6 +35,11 @@ export function shareQR(message) { } const QrCode = ({ value, ...rest }) => { + let hasLink = value.toString().includes("//"); + if(!hasLink) { + value = "nrelopenpath://login_token?token=" + value; + } + return ; }; From 2d930f881041211c59c34011b815b58b03370f3e Mon Sep 17 00:00:00 2001 From: "K. Shankari" Date: Tue, 24 Oct 2023 20:00:06 -0700 Subject: [PATCH 131/134] Use only `emission` externally Long term, this should be part of the app config https://github.com/e-mission/e-mission-docs/issues/985#issuecomment-1769790309 --- www/js/components/QrCode.tsx | 2 +- www/js/onboarding/WelcomePage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/components/QrCode.tsx b/www/js/components/QrCode.tsx index 0499b4c46..74c66863f 100644 --- a/www/js/components/QrCode.tsx +++ b/www/js/components/QrCode.tsx @@ -37,7 +37,7 @@ export function shareQR(message) { const QrCode = ({ value, ...rest }) => { let hasLink = value.toString().includes("//"); if(!hasLink) { - value = "nrelopenpath://login_token?token=" + value; + value = "emission://login_token?token=" + value; } return ; diff --git a/www/js/onboarding/WelcomePage.tsx b/www/js/onboarding/WelcomePage.tsx index cd3734059..5653218d7 100644 --- a/www/js/onboarding/WelcomePage.tsx +++ b/www/js/onboarding/WelcomePage.tsx @@ -23,7 +23,7 @@ const WelcomePage = () => { const checkURL = function (result) { let notCancelled = result.cancelled == false; let isQR = result.format == "QR_CODE"; - let hasPrefix = result.text.split(":")[0] == "nrelopenpath" || result.text.split(":")[0] == "emission"; + let hasPrefix = result.text.split(":")[0] == "emission"; let hasToken = result.text.includes("login_token?token"); logDebug("QR code " + result.text + " checks: cancel, format, prefix, params " + notCancelled + isQR + hasPrefix + hasToken); From 29fac8a5182286b25d0d95dfcb27c8ba22007e9e Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 25 Oct 2023 09:04:54 -0600 Subject: [PATCH 132/134] fix naming error when I updated the name of this visibility state, I did not update it's setter. This caused the popup to fail to launch --- www/js/control/ProfileSettings.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 7cf22a154..e79a95d8d 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -42,7 +42,7 @@ const ProfileSettings = () => { const StartPrefs = getAngularService('StartPrefs'); //functions that come directly from an Angular service - const editCollectionConfig = () => setEditCollection(true); + const editCollectionConfig = () => setEditCollectionVis(true); const editSyncConfig = () => setEditSync(true); //states and variables used to control/create the settings From 4f58aa8f9efeb6fab4a4435a5d980fc3a46d7b0a Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 25 Oct 2023 14:12:32 -0600 Subject: [PATCH 133/134] re-work methods for checking code In a cleanup pass, we're working towards using URL methods rather than the "hacky" parts that I had before https://developer.mozilla.org/en-US/docs/Web/API/URL Also re-work the method to pull out the token from the url components and return the code if it's good, or false if the url is bad --- www/js/onboarding/WelcomePage.tsx | 34 ++++++++++++++++++------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/www/js/onboarding/WelcomePage.tsx b/www/js/onboarding/WelcomePage.tsx index 5653218d7..cb317c5bc 100644 --- a/www/js/onboarding/WelcomePage.tsx +++ b/www/js/onboarding/WelcomePage.tsx @@ -20,28 +20,34 @@ const WelcomePage = () => { const [infoPopupVis, setInfoPopupVis] = useState(false); const [existingToken, setExistingToken] = useState(''); - const checkURL = function (result) { + const getCode = function (result) { + let url = new window.URL(result.text); let notCancelled = result.cancelled == false; let isQR = result.format == "QR_CODE"; - let hasPrefix = result.text.split(":")[0] == "emission"; - let hasToken = result.text.includes("login_token?token"); + let hasPrefix = url.protocol == "emission:"; + let hasToken = url.searchParams.has("token"); + let code = url.searchParams.get("token"); - logDebug("QR code " + result.text + " checks: cancel, format, prefix, params " + notCancelled + isQR + hasPrefix + hasToken); + logDebug("QR code " + result.text + " checks: cancel, format, prefix, params, code " + notCancelled + isQR + hasPrefix + hasToken + code); - return notCancelled && isQR && hasPrefix && hasToken; - } + if (notCancelled && isQR && hasPrefix && hasToken) { + return code; + } else { + return false; + } + }; - const scanCode = function() { + const scanCode = function () { window['cordova'].plugins.barcodeScanner.scan( function (result) { console.debug("scanned code", result); - if (checkURL(result)) { - let text = result.text.split("=")[1]; - console.log("found code", text); - loginWithToken(text); - } else { - displayError(result.text, "invalid study reference") ; - } + let code = getCode(result); + if (code != false) { + console.log("found code", code); + loginWithToken(code); + } else { + displayError(result.text, "invalid study reference"); + } }, function (error) { displayError(error, "Scanning failed: "); From 9f5c02e92be6af2fab9774cdd7bc60a449e13d87 Mon Sep 17 00:00:00 2001 From: "K. Shankari" Date: Sat, 28 Oct 2023 16:18:33 -0700 Subject: [PATCH 134/134] Change the path where we expect gems to be installed This fixes https://github.com/e-mission/e-mission-docs/issues/1022 --- setup/export_shared_dep_versions.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/export_shared_dep_versions.sh b/setup/export_shared_dep_versions.sh index 930216b6e..2ac27d61b 100644 --- a/setup/export_shared_dep_versions.sh +++ b/setup/export_shared_dep_versions.sh @@ -11,4 +11,4 @@ export GRADLE_VERSION=7.6 export OSX_EXP_VERSION=12 export NVM_DIR="$HOME/.nvm" -export RUBY_PATH=$HOME/.gem/ruby/$RUBY_VERSION.0/bin +export RUBY_PATH=$HOME/.local/share/gem/ruby/$RUBY_VERSION.0/bin