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/.github/workflows/serve-install.yml b/.github/workflows/serve-install.yml index a5e634821..c78ce1f86 100644 --- a/.github/workflows/serve-install.yml +++ b/.github/workflows/serve-install.yml @@ -9,10 +9,13 @@ on: branches: - master - maint_upgrade_** + - ui_feature_** pull_request: branches: - 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' 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/README.md b/README.md index a1f23e99a..121684e0a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,9 @@ e-mission phone app 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 been upgraded to the latest **Android**, **iOS**, **cordova-lib**, **node** and **npm** versions. __This is ready to build out of the box.__ + +The currently supported versions are in [`package.cordovabuild.json`](package.cordovabuild.json) Additional Documentation --- @@ -12,6 +14,14 @@ 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. +## Contents +#### 1. [Updating the UI only](#updating-the-ui-only) +#### 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) + 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) @@ -23,22 +33,13 @@ If you want to make only UI changes, (as opposed to modifying the existing plugi 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 +bash setup/setup_serve.sh ``` ### Activation (after install, and in every new shell) ``` -$ source setup/activate_serve.sh +source setup/activate_serve.sh ``` ### Running @@ -46,7 +47,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,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 @@ -65,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 --- @@ -78,14 +81,15 @@ 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. 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 --- [![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 --- @@ -97,7 +101,7 @@ Pre-requisites - 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 + bash setup/prereq_android_sdk_install.sh ```
Expected output @@ -142,27 +146,38 @@ Installing (one time only) Run the setup script for the platform you want to build ``` -$ bash setup/setup_android_native.sh -AND/OR -$ bash setup/setup_ios_native.sh +bash setup/setup_android_native.sh ``` - -**(optional)** Configure by changing the files in `www/json`. -Defaults are in `www/json/*.sample` - +AND/OR ``` -$ ls www/json/*.sample -$ cp www/json/startupConfig.json.sample www/json/startupConfig.json -$ cp ..... www/json/connectionConfig.json +bash setup/setup_ios_native.sh ``` ### Activation (after install, and in every new shell) ``` -$ source setup/activate_native.sh +source setup/activate_native.sh ``` -### Activation (after install, and in every new shell) +
Expected Output + +``` +Activating nvm +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 +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 +``` + +
+ + +### 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 @@ -172,14 +187,29 @@ If connecting to a development server over http, make sure to turn on http suppo ``` -### Run in the emulator +### Building the app + +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) ``` -$ npx cordova emulate ios -AND/OR -$ npx cordova emulate android +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 ``` +
+ Creating logos --- If you are building your own version of the app, you must have your own logo to @@ -223,19 +253,19 @@ Contributing Add the main repo as upstream - $ git remote add upstream https://github.com/covid19database/phone-app.git + 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 + git checkout -b mybranch Make changes to the branch and commit them - $ git commit + git commit Push the changes to your local fork - $ git push origin mybranch + git push origin mybranch Generate a pull request from the UI @@ -243,8 +273,14 @@ 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 ``` 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/bin/sign_and_align_keys.sh b/bin/sign_and_align_keys.sh index 261058bd5..9b60c3ade 100644 --- a/bin/sign_and_align_keys.sh +++ b/bin/sign_and_align_keys.sh @@ -4,13 +4,13 @@ PROJECT=$1 VERSION=$2 if [[ $# -eq 0 ]]; then - echo "No arguments supplied" + echo "sign_and_align_keys " exit 1 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 diff --git a/package.cordovabuild.json b/package.cordovabuild.json index 943f06520..12da8b81a 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", @@ -126,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.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", diff --git a/package.serve.json b/package.serve.json index 2bba5509f..8597cb920 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", diff --git a/resources/android/ic_mood_question.png b/resources/android/ic_mood_question.png deleted file mode 100644 index 8c7790f2e..000000000 Binary files a/resources/android/ic_mood_question.png and /dev/null differ diff --git a/resources/android/ic_mood_question/drawable-hdpi-v11/ic_mood_question.png b/resources/android/ic_mood_question/drawable-hdpi-v11/ic_mood_question.png deleted file mode 100644 index 964fcf139..000000000 Binary files a/resources/android/ic_mood_question/drawable-hdpi-v11/ic_mood_question.png and /dev/null differ diff --git a/resources/android/ic_mood_question/drawable-hdpi-v9/ic_mood_question.png b/resources/android/ic_mood_question/drawable-hdpi-v9/ic_mood_question.png deleted file mode 100644 index c1fd404eb..000000000 Binary files a/resources/android/ic_mood_question/drawable-hdpi-v9/ic_mood_question.png and /dev/null differ diff --git a/resources/android/ic_mood_question/drawable-hdpi/ic_mood_question.png b/resources/android/ic_mood_question/drawable-hdpi/ic_mood_question.png deleted file mode 100644 index ac05d2e2d..000000000 Binary files a/resources/android/ic_mood_question/drawable-hdpi/ic_mood_question.png and /dev/null differ diff --git a/resources/android/ic_mood_question/drawable-mdpi-v11/ic_mood_question.png b/resources/android/ic_mood_question/drawable-mdpi-v11/ic_mood_question.png deleted file mode 100644 index 07348588b..000000000 Binary files a/resources/android/ic_mood_question/drawable-mdpi-v11/ic_mood_question.png and /dev/null differ 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 ecbbe1b3a..000000000 Binary files a/resources/android/ic_mood_question/drawable-mdpi-v9/ic_mood_question.png and /dev/null differ 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 5030bedfc..000000000 Binary files a/resources/android/ic_mood_question/drawable-mdpi/ic_mood_question.png and /dev/null differ 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 78f2f2538..000000000 Binary files a/resources/android/ic_mood_question/drawable-xhdpi-v11/ic_mood_question.png and /dev/null differ diff --git a/resources/android/ic_mood_question/drawable-xhdpi-v9/ic_mood_question.png b/resources/android/ic_mood_question/drawable-xhdpi-v9/ic_mood_question.png deleted file mode 100644 index e3e393f1e..000000000 Binary files a/resources/android/ic_mood_question/drawable-xhdpi-v9/ic_mood_question.png and /dev/null differ diff --git a/resources/android/ic_mood_question/drawable-xhdpi/ic_mood_question.png b/resources/android/ic_mood_question/drawable-xhdpi/ic_mood_question.png deleted file mode 100644 index ec9c4faf6..000000000 Binary files a/resources/android/ic_mood_question/drawable-xhdpi/ic_mood_question.png and /dev/null differ 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 913025e64..000000000 Binary files a/resources/android/ic_mood_question/drawable-xxhdpi-v11/ic_mood_question.png and /dev/null differ diff --git a/resources/android/ic_mood_question/drawable-xxhdpi-v9/ic_mood_question.png b/resources/android/ic_mood_question/drawable-xxhdpi-v9/ic_mood_question.png deleted file mode 100644 index a1d1c94d7..000000000 Binary files a/resources/android/ic_mood_question/drawable-xxhdpi-v9/ic_mood_question.png and /dev/null differ 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 cd2b16140..000000000 Binary files a/resources/android/ic_mood_question/drawable-xxhdpi/ic_mood_question.png and /dev/null differ 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 3ae9173bd..000000000 Binary files a/resources/android/ic_question_answer/drawable-hdpi-v11/ic_question_answer.png and /dev/null differ 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 3d580d05f..000000000 Binary files a/resources/android/ic_question_answer/drawable-hdpi-v9/ic_question_answer.png and /dev/null differ 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 4ddd1ed8b..000000000 Binary files a/resources/android/ic_question_answer/drawable-hdpi/ic_question_answer.png and /dev/null differ 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 f21a94577..000000000 Binary files a/resources/android/ic_question_answer/drawable-mdpi-v11/ic_question_answer.png and /dev/null differ 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 ccc3c7f0a..000000000 Binary files a/resources/android/ic_question_answer/drawable-mdpi-v9/ic_question_answer.png and /dev/null differ 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 a5943266e..000000000 Binary files a/resources/android/ic_question_answer/drawable-mdpi/ic_question_answer.png and /dev/null differ diff --git a/resources/android/ic_question_answer/drawable-xhdpi-v11/ic_question_answer.png b/resources/android/ic_question_answer/drawable-xhdpi-v11/ic_question_answer.png deleted file mode 100644 index f4a92b43f..000000000 Binary files a/resources/android/ic_question_answer/drawable-xhdpi-v11/ic_question_answer.png and /dev/null differ diff --git a/resources/android/ic_question_answer/drawable-xhdpi-v9/ic_question_answer.png b/resources/android/ic_question_answer/drawable-xhdpi-v9/ic_question_answer.png deleted file mode 100644 index 1013050b6..000000000 Binary files a/resources/android/ic_question_answer/drawable-xhdpi-v9/ic_question_answer.png and /dev/null differ 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 c2b8a6368..000000000 Binary files a/resources/android/ic_question_answer/drawable-xhdpi/ic_question_answer.png and /dev/null differ 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 2586cd25d..000000000 Binary files a/resources/android/ic_question_answer/drawable-xxhdpi-v11/ic_question_answer.png and /dev/null differ 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 e80a4e042..000000000 Binary files a/resources/android/ic_question_answer/drawable-xxhdpi-v9/ic_question_answer.png and /dev/null differ 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 799f0e8ba..000000000 Binary files a/resources/android/ic_question_answer/drawable-xxhdpi/ic_question_answer.png and /dev/null differ diff --git a/resources/minus.gif b/resources/minus.gif deleted file mode 100644 index 0115810b9..000000000 Binary files a/resources/minus.gif and /dev/null differ diff --git a/resources/plus.gif b/resources/plus.gif deleted file mode 100644 index 6879c8743..000000000 Binary files a/resources/plus.gif and /dev/null differ diff --git a/scss/ionic.app.scss b/scss/ionic.app.scss deleted file mode 100644 index 9eb2f7820..000000000 --- a/scss/ionic.app.scss +++ /dev/null @@ -1,23 +0,0 @@ -/* -To customize the look and feel of Ionic, you can override the variables -in ionic's _variables.scss file. - -For example, you might change some of the default colors: - -$light: #fff !default; -$stable: #f8f8f8 !default; -$positive: #387ef5 !default; -$calm: #11c1f3 !default; -$balanced: #33cd5f !default; -$energized: #ffc900 !default; -$assertive: #ef473a !default; -$royal: #886aea !default; -$dark: #444 !default; -*/ - -// The path for our ionicons font files, relative to the built CSS in www/css -$ionicons-font-path: "../lib/ionic/fonts" !default; - -// Include all of Ionic -@import "www/lib/ionic/scss/ionic"; - diff --git a/setup/setup_shared_native.sh b/setup/setup_shared_native.sh index 1ce5c64b3..00c72a375 100644 --- a/setup/setup_shared_native.sh +++ b/setup/setup_shared_native.sh @@ -10,8 +10,6 @@ cp setup/google-services.fake.for_ci.json google-services.json echo "Setting up all npm packages" npm install -npm run setup-native - # By default, node doesn't fail if any of the steps fail. This makes it hard to # use in a CI environment, and leads to people reporting the node error rather # than the underlying error. One solution is to pass in a command line argument to node diff --git a/webpack.config.js b/webpack.config.js index d6e36fb18..1e504ac5f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -50,6 +50,7 @@ module.exports = { { test: /\.(jpg|png|woff|woff2|eot|ttf|svg)$/, include: [path.resolve(__dirname, 'www'), + path.resolve(__dirname, 'resources'), path.resolve(__dirname, 'node_modules/react-native-vector-icons')], type: 'asset/resource', }, diff --git a/www/css/appstatus.css b/www/css/appstatus.css deleted file mode 100644 index b5aa0cc41..000000000 --- a/www/css/appstatus.css +++ /dev/null @@ -1,12 +0,0 @@ -.status-red { - background-color: #ED2D3A; - color: white; -} -.status-yellow { - background-color: #FFC108; - color: white; -} -.status-green { - background-color: #30A64A; - color: white; -} diff --git a/www/css/intro.css b/www/css/intro.css deleted file mode 100644 index b636a38d6..000000000 --- a/www/css/intro.css +++ /dev/null @@ -1,87 +0,0 @@ -.slider { - height: 100%; -} - -.slider-slide { - padding-top: 80px; - color: #000; - background-color: #fff; - text-align: center; - font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; - font-weight: 300; -} - -.scroll { - min-width: 100%; -} -.nav-placeholder { - height: 30px; -} -.wide-as-needed { - overflow-y: scroll; - white-space: nowrap; -} -.intro-view { - /*background-color: #eeeeee;*/ -} - -.intro-title { - padding-top: 10%; - padding-right: 8%; - padding-left: 8%; - font-size: 18px; -} -.intro-text { - padding-top: 5%; - padding-right: 8%; - padding-left: 8%; - font-size: 15px; - line-height: 1.5; - text-align: left; -} -.intro-space { - height: 15px; -} -#intro-footer { - padding: 15px; - height: 90px; -} -.consent-title { - padding-top: 10%; - padding-right: 8%; - padding-left: 8%; - font-size: 18px; -} -.consent-text { - padding-top: 5%; - padding-right: 8%; - padding-left: 8%; - font-size: 15px; - line-height: 1.5; - text-align: left; -} -.consent-space { - height: 15px; -} -#consent-footer { - padding: 15px; - height: 90px; - position: absolute; -} -#consent-button { - height: 35px; -} -.refuse-popup { - font-size: 13px; - line-height: 1.5; - text-align: left; - font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; -} -.refuse-popup .popup-buttons { - margin-left: 15%; - width: 70% !important; -} -.refuse-popup .popup-body { - padding-left: 8%; - padding-right: 8%; -} diff --git a/www/css/main.recent.css b/www/css/main.recent.css deleted file mode 100644 index da75745bc..000000000 --- a/www/css/main.recent.css +++ /dev/null @@ -1,12 +0,0 @@ -/* Empty. Add your own CSS if you like */ - -.timestamp { - background: whitesmoke; - font-size: small; - font-family: monospace; -} - -.detail { - font-size: x-small; - font-family: monospace; -} diff --git a/www/css/style.css b/www/css/style.css index 8910b2258..dea003e7b 100644 --- a/www/css/style.css +++ b/www/css/style.css @@ -44,11 +44,6 @@ label-tab > 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; diff --git a/www/i18n/en.json b/www/i18n/en.json index fe0df617a..e47fdd62d 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", @@ -73,6 +73,7 @@ }, "metrics":{ + "dashboard-tab": "Dashboard", "cancel": "Cancel", "confirm": "Confirm", "get": "Get", @@ -112,6 +113,7 @@ }, "diary": { + "label-tab": "Label", "distance-in-time": "{{distance}} {{distsuffix}} in {{time}}", "distance": "Distance", "time": "Time", @@ -145,7 +147,6 @@ }, "main-metrics":{ - "dashboard": "Dashboard", "summary": "My Summary", "chart": "Chart", "change-data": "Change dates:", @@ -183,10 +184,6 @@ "footprint-label": "Footprint (kg CO₂)" }, - "main-inf-scroll" : { - "tab": "Label" - }, - "details":{ "speed": "Speed", "time": "Time" @@ -227,6 +224,7 @@ }, "intro": { + "proceed": "Proceed", "appstatus": { "fix": "Fix", "refresh":"Refresh", @@ -286,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": { @@ -332,22 +327,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", @@ -363,6 +359,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.", + "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/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 17a5326d7..55cb233b5 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/customURL.js'; @@ -20,32 +13,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/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/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..3c6c8bec9 --- /dev/null +++ b/www/js/App.tsx @@ -0,0 +1,103 @@ +import React, { useEffect, useState, createContext, useMemo } from 'react'; +import { getAngularService } from './angular-react-helper'; +import { ActivityIndicator, BottomNavigation, 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'; +import usePermissionStatus from './usePermissionStatus'; + +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); + // 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 permissionStatus = usePermissionStatus(); + 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(setOnboardingState); + useEffect(() => { refreshOnboardingState() }, []); + + useEffect(() => { + if (!appConfig) return; + setServerConnSettings(appConfig).then(() => { + refreshOnboardingState(); + }); + }, [appConfig]); + + const appContextValue = { + appConfig, + onboardingState, setOnboardingState, refreshOnboardingState, + permissionStatus, + permissionsPopupVis, setPermissionsPopupVis, + } + + 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 (<> + + {appContent} + + { /* 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) && + + } + + ); +} + +export default App; diff --git a/www/js/appTheme.ts b/www/js/appTheme.ts index 5f47f00b1..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) diff --git a/www/js/appstatus/PermissionsControls.tsx b/www/js/appstatus/PermissionsControls.tsx new file mode 100644 index 000000000..97ce7081a --- /dev/null +++ b/www/js/appstatus/PermissionsControls.tsx @@ -0,0 +1,67 @@ +//component to view and manage permission settings +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 { 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 { permissionStatus } = useContext(AppContext); + const { checkList, overallStatus, error, errorVis, setErrorVis, explanationList } = permissionStatus; + + 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; 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/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 ( - + + + + + + ); }); - - // 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"); + console.log("Ending run"); }); diff --git a/www/js/onboarding/OnboardingStack.tsx b/www/js/onboarding/OnboardingStack.tsx new file mode 100644 index 000000000..c547fd074 --- /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 ProtocolPage from "./ProtocolPage"; +import SurveyPage from "./SurveyPage"; +import SaveQrPage from "./SaveQrPage"; +import SummaryPage from "./SummaryPage"; +import { OnboardingRoute } from "./onboardingHelper"; +import { displayErrorMsg } from "../plugin/logger"; + +const OnboardingStack = () => { + + const { onboardingState } = useContext(AppContext); + + console.debug('onboardingState in OnboardingStack', onboardingState); + + if (onboardingState.route == OnboardingRoute.WELCOME) { + return ; + } else if (onboardingState.route == OnboardingRoute.SUMMARY) { + return ; + } else if (onboardingState.route == OnboardingRoute.PROTOCOL) { + return ; + } else if (onboardingState.route == OnboardingRoute.SAVE_QR) { + return ; + } else if (onboardingState.route == OnboardingRoute.SURVEY) { + return ; + } else { + displayErrorMsg('OnboardingStack: unknown route', onboardingState.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/ProtocolPage.tsx b/www/js/onboarding/ProtocolPage.tsx new file mode 100644 index 000000000..73961245a --- /dev/null +++ b/www/js/onboarding/ProtocolPage.tsx @@ -0,0 +1,42 @@ +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'; +import { setProtocolDone } from './onboardingHelper'; + +const ProtocolPage = () => { + + 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() { + setProtocolDone(true); + refreshOnboardingState(); + }; + + // privacy policy and data collection info, followed by accept/reject buttons + return (<> + + + + + + + + + + ); +} + +export default ProtocolPage; diff --git a/www/js/onboarding/SaveQrPage.tsx b/www/js/onboarding/SaveQrPage.tsx new file mode 100644 index 000000000..658c66993 --- /dev/null +++ b/www/js/onboarding/SaveQrPage.tsx @@ -0,0 +1,101 @@ +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 { 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"; +import { resetDataAndRefresh } from "../config/dynamicConfig"; +import i18next from "i18next"; + +const SaveQrPage = ({ }) => { + + const { t } = useTranslation(); + const { permissionStatus, onboardingState, refreshOnboardingState } = useContext(AppContext); + const { overallStatus } = permissionStatus; + + useEffect(() => { + if (overallStatus == true && !registerUserDone) { + 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'); + } + }, [overallStatus]); + + function login(token) { + const CommHelper = getAngularService('CommHelper'); + 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) { + /* 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"); + }); + }; + + function onFinish() { + setSaveQrDone(true); + refreshOnboardingState(); + } + + return ( + + + + {t('login.make-sure-save-your-opcode')} + + + {t('login.cannot-retrieve')} + + + + + + {onboardingState.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..3996ba076 --- /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: 24, + paddingBottom: 10, + textAlign: "center" + }, + text: { + fontSize: 15, + }, + studyName: { + fontWeight: "bold", + fontSize: 17, + }, +}); + +export default StudySummary; diff --git a/www/js/onboarding/SummaryPage.tsx b/www/js/onboarding/SummaryPage.tsx new file mode 100644 index 000000000..d15e9f60e --- /dev/null +++ b/www/js/onboarding/SummaryPage.tsx @@ -0,0 +1,36 @@ +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..c02439cbf --- /dev/null +++ b/www/js/onboarding/SurveyPage.tsx @@ -0,0 +1,101 @@ +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, 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('errors.not-registered-cant-contact')); + return Promise.resolve(null); + } + 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..3589923c8 --- /dev/null +++ b/www/js/onboarding/WelcomePage.tsx @@ -0,0 +1,204 @@ +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..4a6ec202c --- /dev/null +++ b/www/js/onboarding/onboardingHelper.ts @@ -0,0 +1,76 @@ +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'; + +// route = WELCOME if no config present +// 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, PROTOCOL, SAVE_QR, SURVEY, DONE }; +export type OnboardingState = { + opcode: string, + route: OnboardingRoute, +} + +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; + +export let registerUserDone = false; +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) { + route = OnboardingRoute.WELCOME; + } else if (!protocolDone && !summaryDone) { + route = OnboardingRoute.SUMMARY; + } else if (!protocolDone) { + route = OnboardingRoute.PROTOCOL; + } else if (!saveQrDone) { + route = OnboardingRoute.SAVE_QR; + } 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 }; + }); +}; + +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/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, 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.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/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 @@ -