diff --git a/.bowerrc b/.bowerrc deleted file mode 100644 index e28246d45..000000000 --- a/.bowerrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "directory": "www/lib" -} diff --git a/.gitignore b/.gitignore index e626e9a48..6801f890d 100644 --- a/.gitignore +++ b/.gitignore @@ -14,9 +14,5 @@ app-settings.json *.app.zip *.ipa www/dist/ -www/js/control/collect-settings.js -www/templates/control/main-collect-settings.html -www/js/control/sync-settings.js -www/templates/control/main-sync-settings.html config.xml package.json diff --git a/bin/download_settings_controls.js b/bin/download_settings_controls.js deleted file mode 100755 index fce3fb675..000000000 --- a/bin/download_settings_controls.js +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env node - -var https = require('https'); -var fs = require('fs'); - -var download = function(url, dest, cb) { - var file = fs.createWriteStream(dest); - var request = https.get(url, function(response) { - response.pipe(file); - file.on('finish', function() { - file.close(cb); // close() is async, call cb after close completes. - }); - }).on('error', function(err) { // Handle errors - fs.unlink(dest); // Delete the file async. (But we don't check the result) - if (cb) cb(err.message); - }); -}; - -download("https://raw.githubusercontent.com/e-mission/e-mission-data-collection/master/www/ui/ionic/js/collect-settings.js", "www/js/control/collect-settings.js", function(message) { - console.log("Data collection settings javascript updated"); -}); - -download("https://raw.githubusercontent.com/e-mission/e-mission-data-collection/master/www/ui/ionic/templates/main-collect-settings.html", "www/templates/control/main-collect-settings.html", function(message) { - console.log("Data collection settings template updated"); -}); - -download("https://raw.githubusercontent.com/e-mission/cordova-server-sync/master/www/ui/ionic/js/sync-settings.js", "www/js/control/sync-settings.js", function(message) { - console.log("Sync collection settings javascript updated"); -}); - -download("https://raw.githubusercontent.com/e-mission/cordova-server-sync/master/www/ui/ionic/templates/main-sync-settings.html", "www/templates/control/main-sync-settings.html", function(message) { - console.log("Sync collection settings template updated"); -}); diff --git a/package.cordovabuild.json b/package.cordovabuild.json index 25145b5ed..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", diff --git a/package.serve.json b/package.serve.json index a4e3194f3..1a2ef6cb0 100644 --- a/package.serve.json +++ b/package.serve.json @@ -8,7 +8,7 @@ "url": "git+https://github.com/e-mission/e-mission-phone.git" }, "scripts": { - "setup-serve": "./bin/download_settings_controls.js && ./bin/setup_autodeploy.js", + "setup-serve": "./bin/setup_autodeploy.js", "serve": "webpack --config webpack.dev.js && concurrently -k \"phonegap --verbose serve\" \"webpack --config webpack.dev.js --watch\"", "serve-prod": "webpack --config webpack.prod.js && concurrently -k \"phonegap --verbose serve\" \"webpack --config webpack.prod.js --watch\"", "serve-only": "phonegap --verbose serve", 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 @@ -