diff --git a/www/css/style.css b/www/css/style.css index 9a6f708bd..5e923f5bd 100644 --- a/www/css/style.css +++ b/www/css/style.css @@ -26,16 +26,12 @@ position: relative; } -.fill-container div[class*='css-'] { +.fill-container > div[class*='css-'] { height: 100%; width: 100%; position: absolute; } -.fill-container div[class*='css-'] > div[class*='css-'] { - position: static; -} - /* Without this, the LabelTab does not fill the entire height of the screen. It has something to do with React Navigation's NavigationContainer. This should not be necessary once the entire app's routing has been diff --git a/www/i18n/en.json b/www/i18n/en.json index b0b5426d6..b4c688b55 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -44,7 +44,8 @@ "app-version": "App Version", "reminders-time-of-day": "Time of Day for Reminders ({{time}})", "upcoming-notifications": "Upcoming Notifications", - "dummy-notification" : "Dummy Notification in 5 Seconds" + "dummy-notification" : "Dummy Notification in 5 Seconds", + "log-out": "Log Out" }, "general-settings":{ @@ -65,9 +66,6 @@ "consent-found": "Consent found!", "consented-to": "Consented to protocol {{protocol_id}}, {{approval_date}}", "consented-ok": "OK", - "share-message": "Join me in making transportation greener and healthier \nDownload the emission app:", - "share-subject": "Emission - UC Berkeley Research Project", - "share-url": "https://bic2cal.eecs.berkeley.edu/#download", "qrcode": "My OPcode", "qrcode-share-title": "You can save your OPcode to login easily in the future!" }, @@ -228,6 +226,7 @@ "fix": "Fix", "refresh":"Refresh", "overall-description": "This app works in the background to automatically build a travel diary for you. Make sure that all the settings below are green so that the app can work properly!", + "explanation-title": "What are these used for?", "overall-loc-name": "Location", "overall-loc-description": "We use the background location permission to track your location in the background, even when the app is closed. Reading background locations removes the need to turn tracking on and off, making the app easier to use and preventing battery drain.", "locsettings": { @@ -356,5 +355,62 @@ "invalid-opcode-format": "Invalid OPcode format", "error-loading-config-app-start": "Error loading config on app start", "survey-missing-formpath": "Error while fetching resources in config: survey_info.surveys has a survey without a formPath" + }, + "consent-text": { + "title":"NREL OPENPATH PRIVACY POLICY/TERMS OF USE", + "introduction":{ + "header":"Introduction and Purpose", + "what-is-openpath":"This data is being collected through OpenPATH, an NREL open-sourced platform. The smart phone application, NREL OpenPATH (“App”), combines data from smartphone sensors, semantic user labels and a short demographic survey.", + "what-is-NREL":"NREL is a national laboratory of the U.S. Department of Energy, Office of Energy Efficiency and Renewable Energy, operated by Alliance for Sustainable Energy, LLC under Prime Contract No. DE-AC36-08GO28308. This Privacy Policy applies to the App provided by Alliance for Sustainable Energy, LLC. This App is provided solely for the purposes of collecting travel behavior data for the {{program_or_study}} and for research to inform public policy. None of the data collected by the App will never be sold or used for any commercial purposes, including advertising.", + "if-disagree":"IF YOU DO NOT AGREE WITH THE TERMS OF THIS PRIVACY POLICY, PLEASE DELETE THE APP" + }, + "why":{ + "header":"Why we collect this information" + }, + "what":{ + "header":"What information we collect", + "no-pii":"The App will never ask for any Personally Identifying Information (PII) such as name, email, address, or phone number.", + "phone-sensor":"It collects phone sensor data pertaining to your location (including background location), accelerometer, device-generated activity and mode recognition, App usage time, and battery usage. The App will create a “travel diary” based on your background location data to determine your travel patterns and location history.", + "labeling":"It will also ask you to periodically annotate these sensed trips with semantic labels, such as the trip mode, purpose, and replaced mode.", + "demographics":"It will also request sociodemographic information such as your approximate age, gender, and household type. The sociodemographic factors can be used to understand the influence of lifestyle on travel behavior, as well as generalize the results to a broader population.", + "open-source-data":"For the greatest transparency, the App is based on an open source platform, NREL’s OpenPATH. you can inspect the data that OpenPATH collects in the background at", + "open-source-analysis":"the analysis pipeline at", + "open-source-dashboard":"and the dashboard metrics at", + "on-nrel-site": "For the greatest transparency, the App is based on an open source platform, NREL’s OpenPATH. you can inspect the data that OpenPATH collects in the background, the analysis pipeline, and the dashboard metrics through links on the NREL OpenPATH website." + }, + "opcode":{ + "header":"How we associate information with you", + "not-autogen":"Program administrators will provide you with a 'opcode' that you will use to log in to the system. This long random string will be used for all further communication with the server. If you forget or lose your opcode, you may request it by providing your name and/or email address to the program administrator. Please do not contact NREL staff with opcode retrieval requests since we do not have access to the connection between your name/email and your opcode. The data that NREL automatically collects (phone sensor data, semidemographic data, etc.) will only be associated with your 'opcode'", + "autogen":"You are logging in with a randomly generated 'opcode' that has been generated by the platform. This long random string will used for all further communication with the server. Only you know the opcode that is associated with you. There is no “Forgot password” option, and NREL staff cannot retrieve your opcode, even if you provide your name or email address. This means that, unless you store your opcode in a safe place, you will not have access to your prior data if you switch phones or uninstall and reinstall the App." + }, + "who-sees":{ + "header":"Who gets to see the information", + "public-dash":"Aggregate metrics derived from the travel patterns will be made available on a public dashboard to provide transparency into the impact of the program. These metrics will focus on information summaries such as counts, distances and durations, and will not display individual travel locations or times.", + "individual-info":"Individual labeling rates and trip level information will only be made available to:", + "program-admins":"🧑 Program administrators from {{deployment_partner_name}} to {{raw_data_use}}, and", + "nrel-devs":"💻 NREL OpenPATH developers for debugging", + "TSDC-info":"The data will also be periodically archived in NREL’s Transportation Secure Data Center (TSDC) after a delay of 3 to 6 months. It will then be made available for legitimate research through existing, privacy-preserving TSDC operating procedures. Further information on the procedures is available", + "on-website":" on the website ", + "and-in":"and in", + "this-pub":" this publication ", + "and":"and", + "fact-sheet":" fact sheet", + "on-nrel-site": " through links on the NREL OpenPATH website." + }, + "rights":{ + "header":"Your rights", + "app-required":"You are required to track your travel patterns using the App as a condition of participation in the Program. If you wish to withdraw from the Program, you should contact the program administrator, {{program_admin_contact}} to discuss termination options. If you wish to stay in the program but not use the app, please contact your program administrator to negotiate an alternative data collection procedure before uninstalling the app. If you uninstall the app without approval from the program administrator, you may not have access to the benefits provided by the program.", + "app-not-required":"Participation in the {{program_or_study}} is completely voluntary. You have the right to decline to participate or to withdraw at any point in the Study without providing notice to NREL or the point of contact. If you do not wish to participate in the Study or to discontinue your participation in the Study, please delete the App.", + "destroy-data-pt1":"If you would like to have your data destroyed, please contact K. Shankari ", + "destroy-data-pt2":" requesting deletion. You must include your token in the request for deletion. Because we do not connect your identity with your token, we cannot delete your information without obtaining the token as part of the deletion request. We will then destroy all data associated with that deletion request, both in the online and archived datasets." + }, + "questions":{ + "header":"Questions", + "for-questions":"If you have any questions about the data collection goals and results, please contact the primary point of contact for the study, {{program_admin_contact}}. If you have any technical questions about app operation, please contact NREL’s K. Shankari (k.shankari@nrel.gov)." + }, + "consent":{ + "header":"Consent", + "press-button-to-consent":"Please select the button below to indicate that you have read and agree to this Privacy Policy, consent to the collection of your information, and want to participate in the {{program_or_study}}." + } } } diff --git a/www/js/appTheme.ts b/www/js/appTheme.ts index 8ef1a8925..2fa1ab3f7 100644 --- a/www/js/appTheme.ts +++ b/www/js/appTheme.ts @@ -26,6 +26,8 @@ const AppTheme = { level4: '#e0f0ff', // lch(94% 50 250) level5: '#d6ebff', // lch(92% 50 250) }, + success: '#38872e', // lch(50% 55 135) + danger: '#f23934' // lch(55% 85 35) }, roundness: 5, }; diff --git a/www/js/appstatus/ExplainPermissions.tsx b/www/js/appstatus/ExplainPermissions.tsx new file mode 100644 index 000000000..cb0db4bba --- /dev/null +++ b/www/js/appstatus/ExplainPermissions.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { Modal, ScrollView, useWindowDimensions, View } from "react-native"; +import { Button, Dialog, Text } from 'react-native-paper'; +import { useTranslation } from "react-i18next"; + +const ExplainPermissions = ({ explanationList, visible, setVisible }) => { + const { t } = useTranslation(); + const { height: windowHeight } = useWindowDimensions(); + + return ( + setVisible(false)} > + setVisible(false)} > + {t('intro.appstatus.explanation-title')} + + + {explanationList?.map((li) => + + + {li.name} + + + {li.desc} + + + )} + + + + + + + + ); +}; + +export default ExplainPermissions; \ No newline at end of file diff --git a/www/js/appstatus/PermissionItem.tsx b/www/js/appstatus/PermissionItem.tsx new file mode 100644 index 000000000..2899943f1 --- /dev/null +++ b/www/js/appstatus/PermissionItem.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { List, Button } from 'react-native-paper'; +import { useTranslation } from "react-i18next"; + +const PermissionItem = ({ check }) => { + const { t } = useTranslation(); + + return ( + } + right={() => } + /> + ); +}; + +export default PermissionItem; \ No newline at end of file diff --git a/www/js/components/QrCode.jsx b/www/js/components/QrCode.jsx index a73fb780f..0084b334f 100644 --- a/www/js/components/QrCode.jsx +++ b/www/js/components/QrCode.jsx @@ -8,7 +8,7 @@ import { string } from "prop-types"; import QRCode from "react-qr-code"; const QrCode = ({ value }) => { - return ; + return ; }; QrCode.propTypes = { diff --git a/www/js/control/AlertBar.jsx b/www/js/control/AlertBar.jsx index f640317a4..c56db7ad2 100644 --- a/www/js/control/AlertBar.jsx +++ b/www/js/control/AlertBar.jsx @@ -1,24 +1,36 @@ import React from "react"; -import { Modal, Snackbar} from 'react-native-paper'; +import { Modal } from "react-native"; +import { Snackbar } from 'react-native-paper'; import { useTranslation } from "react-i18next"; +import { SafeAreaView } from "react-native-safe-area-context"; -const AlertBar = ({visible, setVisible, messageKey}) => { +const AlertBar = ({visible, setVisible, messageKey, messageAddition}) => { const { t } = useTranslation(); const onDismissSnackBar = () => setVisible(false); + + let text = ""; + if(messageAddition){ + text = t(messageKey) + messageAddition; + } + else { + text = t(messageKey); + } return ( - setVisible(false)}> - { - onDismissSnackBar() - }, - }}> - {t(messageKey)} - + setVisible(false)} transparent={true}> + + { + onDismissSnackBar() + }, + }}> + {text} + + ); }; diff --git a/www/js/control/AppStatusModal.tsx b/www/js/control/AppStatusModal.tsx new file mode 100644 index 000000000..0d5b94a51 --- /dev/null +++ b/www/js/control/AppStatusModal.tsx @@ -0,0 +1,458 @@ +//component to view and manage permission settings +import React, { useState, useEffect, useMemo } from "react"; +import { Modal, useWindowDimensions, ScrollView } from "react-native"; +import { Dialog, Button, Text, useTheme } from 'react-native-paper'; +import { useTranslation } from "react-i18next"; +import PermissionItem from "../appstatus/PermissionItem"; +import useAppConfig from "../useAppConfig"; +import useAppStateChange from "../useAppStateChange"; +import ExplainPermissions from "../appstatus/ExplainPermissions"; +import AlertBar from "./AlertBar"; + +const AppStatusModal = ({permitVis, setPermitVis, dialogStyle, settingsScope}) => { + const { t } = useTranslation(); + const { colors } = useTheme(); + const { appConfig, loading } = useAppConfig(); + + console.log("settings scope in app status modal", settingsScope); + + const { height: windowHeight } = useWindowDimensions(); + const [osver, setOsver] = useState(0); + const [platform, setPlatform] = useState(""); + + const [error, setError] = useState(""); + const [errorVis, setErrorVis] = useState(false); + + const [explainVis, setExplainVis] = useState(false); + + const [backgroundRestricted, setBackgroundRestricted] = useState(false); + const [allowBackgroundInstructions, setAllowBackgroundInstructions] = useState>([]); + + const [checkList, setCheckList] = useState([]); + const [explanationList, setExplanationList] = useState>([]); + const [haveSetText, setHaveSetText] = useState(false); + + let iconMap = (statusState) => statusState ? "check-circle-outline" : "alpha-x-circle-outline"; + let colorMap = (statusState) => statusState ? colors.success : colors.danger; + + const overallStatus = useMemo(() => { + let status = true; + checkList.forEach((lc) => { + if(!lc.statusState){ + status = false; + } + }) + return status; + }, [checkList]) + + //using this function to update checks rather than mutate + //this cues React to update UI + function updateCheck(newObject) { + var tempList = [...checkList]; //make a copy rather than mutate + tempList.forEach((item, i) => { + if(item.name == newObject.name){ + tempList[i] = newObject; + } + }); + setCheckList(tempList); + } + + async function checkOrFix(checkObj, nativeFn, showError=true) { + console.log("checking object", checkObj.name, checkObj); + let newCheck = checkObj; + return nativeFn() + .then((status) => { + console.log("availability ", status) + newCheck.statusState = true; + updateCheck(newCheck); + console.log("after checking object", checkObj.name, checkList); + return status; + }).catch((error) => { + console.log("Error", error) + if (showError) { + console.log("please fix again"); + setError(error); + setErrorVis(true); + }; + newCheck.statusState = false; + updateCheck(newCheck); + console.log("after checking object", checkObj.name, checkList); + return error; + }); + } + + function setupAndroidLocChecks() { + let fixSettings = function() { + console.log("Fix and refresh location settings"); + return checkOrFix(locSettingsCheck, window['cordova'].plugins.BEMDataCollection.fixLocationSettings, true); + }; + let checkSettings = function() { + console.log("Refresh location settings"); + return checkOrFix(locSettingsCheck, window['cordova'].plugins.BEMDataCollection.isValidLocationSettings, false); + }; + let fixPerms = function() { + console.log("fix and refresh location permissions"); + return checkOrFix(locPermissionsCheck, window['cordova'].plugins.BEMDataCollection.fixLocationPermissions, + true).then((error) => {if(error){locPermissionsCheck.desc = error}}); + }; + let checkPerms = function() { + console.log("fix and refresh location permissions"); + return checkOrFix(locPermissionsCheck, window['cordova'].plugins.BEMDataCollection.isValidLocationPermissions, false); + }; + var androidSettingsDescTag = "intro.appstatus.locsettings.description.android-gte-9"; + if (osver < 9) { + androidSettingsDescTag = "intro.appstatus.locsettings.description.android-lt-9"; + } + var androidPermDescTag = "intro.appstatus.locperms.description.android-gte-12"; + if(osver < 6) { + androidPermDescTag = 'intro.appstatus.locperms.description.android-lt-6'; + } else if (osver < 10) { + androidPermDescTag = "intro.appstatus.locperms.description.android-6-9"; + } else if (osver < 11) { + androidPermDescTag= "intro.appstatus.locperms.description.android-10"; + } else if (osver < 12) { + androidPermDescTag= "intro.appstatus.locperms.description.android-11"; + } + console.log("description tags are "+androidSettingsDescTag+" "+androidPermDescTag); + // location settings + let locSettingsCheck = { + name: t("intro.appstatus.locsettings.name"), + desc: t(androidSettingsDescTag), + statusState: false, + fix: fixSettings, + refresh: checkSettings + } + let locPermissionsCheck = { + name: t("intro.appstatus.locperms.name"), + desc: t(androidPermDescTag), + statusState: false, + fix: fixPerms, + refresh: checkPerms + } + let tempChecks = checkList; + tempChecks.push(locSettingsCheck, locPermissionsCheck); + setCheckList(tempChecks); + } + + function setupIOSLocChecks() { + let fixSettings = function() { + console.log("Fix and refresh location settings"); + return checkOrFix(locSettingsCheck, window['cordova'].plugins.BEMDataCollection.fixLocationSettings, + true); + }; + let checkSettings = function() { + console.log("Refresh location settings"); + return checkOrFix(locSettingsCheck, window['cordova'].plugins.BEMDataCollection.isValidLocationSettings, + false); + }; + let fixPerms = function() { + console.log("fix and refresh location permissions"); + return checkOrFix(locPermissionsCheck, window['cordova'].plugins.BEMDataCollection.fixLocationPermissions, + true).then((error) => {if(error){locPermissionsCheck.desc = error}}); + }; + let checkPerms = function() { + console.log("fix and refresh location permissions"); + return checkOrFix(locPermissionsCheck, window['cordova'].plugins.BEMDataCollection.isValidLocationPermissions, + false); + }; + var iOSSettingsDescTag = "intro.appstatus.locsettings.description.ios"; + var iOSPermDescTag = "intro.appstatus.locperms.description.ios-gte-13"; + if(osver < 13) { + iOSPermDescTag = 'intro.appstatus.locperms.description.ios-lt-13'; + } + console.log("description tags are "+iOSSettingsDescTag+" "+iOSPermDescTag); + + const locSettingsCheck = { + name: t("intro.appstatus.locsettings.name"), + desc: t(iOSSettingsDescTag), + statusState: false, + fix: fixSettings, + refresh: checkSettings + }; + const locPermissionsCheck = { + name: t("intro.appstatus.locperms.name"), + desc: t(iOSPermDescTag), + statusState: false, + fix: fixPerms, + refresh: checkPerms + }; + let tempChecks = checkList; + tempChecks.push(locSettingsCheck, locPermissionsCheck); + setCheckList(tempChecks); + } + + function setupAndroidFitnessChecks() { + if(osver >= 10){ + let fixPerms = function() { + console.log("fix and refresh fitness permissions"); + return checkOrFix(fitnessPermissionsCheck, window['cordova'].plugins.BEMDataCollection.fixFitnessPermissions, + true).then((error) => {if(error){fitnessPermissionsCheck.desc = error}}); + }; + let checkPerms = function() { + console.log("fix and refresh fitness permissions"); + return checkOrFix(fitnessPermissionsCheck, window['cordova'].plugins.BEMDataCollection.isValidFitnessPermissions, + false); + }; + + let fitnessPermissionsCheck = { + name: t("intro.appstatus.fitnessperms.name"), + desc: t("intro.appstatus.fitnessperms.description.android"), + fix: fixPerms, + refresh: checkPerms + } + let tempChecks = checkList; + tempChecks.push(fitnessPermissionsCheck); + setCheckList(tempChecks); + } + } + + function setupIOSFitnessChecks() { + let fixPerms = function() { + console.log("fix and refresh fitness permissions"); + return checkOrFix(fitnessPermissionsCheck, window['cordova'].plugins.BEMDataCollection.fixFitnessPermissions, + true).then((error) => {if(error){fitnessPermissionsCheck.desc = error}}); + }; + let checkPerms = function() { + console.log("fix and refresh fitness permissions"); + return checkOrFix(fitnessPermissionsCheck, window['cordova'].plugins.BEMDataCollection.isValidFitnessPermissions, + false); + }; + + let fitnessPermissionsCheck = { + name: t("intro.appstatus.fitnessperms.name"), + desc: t("intro.appstatus.fitnessperms.description.ios"), + fix: fixPerms, + refresh: checkPerms + } + let tempChecks = checkList; + tempChecks.push(fitnessPermissionsCheck); + setCheckList(tempChecks); + } + + function setupAndroidNotificationChecks() { + let fixPerms = function() { + console.log("fix and refresh notification permissions"); + return checkOrFix(appAndChannelNotificationsCheck, window['cordova'].plugins.BEMDataCollection.fixShowNotifications, + true); + }; + let checkPerms = function() { + console.log("fix and refresh notification permissions"); + return checkOrFix(appAndChannelNotificationsCheck, window['cordova'].plugins.BEMDataCollection.isValidShowNotifications, + false); + }; + let appAndChannelNotificationsCheck = { + name: t("intro.appstatus.notificationperms.app-enabled-name"), + desc: t("intro.appstatus.notificationperms.description.android-enable"), + fix: fixPerms, + refresh: checkPerms + } + let tempChecks = checkList; + tempChecks.push(appAndChannelNotificationsCheck); + setCheckList(tempChecks); + } + + function setupAndroidBackgroundRestrictionChecks() { + let fixPerms = function() { + console.log("fix and refresh backgroundRestriction permissions"); + return checkOrFix(unusedAppsUnrestrictedCheck, window['cordova'].plugins.BEMDataCollection.fixUnusedAppRestrictions, + true); + }; + let checkPerms = function() { + console.log("fix and refresh backgroundRestriction permissions"); + return checkOrFix(unusedAppsUnrestrictedCheck, window['cordova'].plugins.BEMDataCollection.isUnusedAppUnrestricted, + false); + }; + let fixBatteryOpt = function() { + console.log("fix and refresh battery optimization permissions"); + return checkOrFix(ignoreBatteryOptCheck, window['cordova'].plugins.BEMDataCollection.fixIgnoreBatteryOptimizations, + true); + }; + let checkBatteryOpt = function() { + console.log("fix and refresh battery optimization permissions"); + return checkOrFix(ignoreBatteryOptCheck, window['cordova'].plugins.BEMDataCollection.isIgnoreBatteryOptimizations, + false); + }; + var androidUnusedDescTag = "intro.appstatus.unusedapprestrict.description.android-disable-gte-12"; + if (osver < 12) { + androidUnusedDescTag= "intro.appstatus.unusedapprestrict.description.android-disable-lt-12"; + } + let unusedAppsUnrestrictedCheck = { + name: t("intro.appstatus.unusedapprestrict.name"), + desc: t(androidUnusedDescTag), + fix: fixPerms, + refresh: checkPerms + } + let ignoreBatteryOptCheck = { + name: t("intro.appstatus.ignorebatteryopt.name"), + desc: t("intro.appstatus.ignorebatteryopt.description.android-disable"), + fix: fixBatteryOpt, + refresh: checkBatteryOpt + } + let tempChecks = checkList; + tempChecks.push(unusedAppsUnrestrictedCheck, ignoreBatteryOptCheck); + setCheckList(tempChecks); + } + + function setupPermissionText() { + let tempExplanations = explanationList; + + let overallFitnessName = t('intro.appstatus.overall-fitness-name-android'); + let locExplanation = t('intro.appstatus.overall-loc-description'); + if(platform == "ios") { + overallFitnessName = t('intro.appstatus.overall-fitness-name-ios'); + if(osver < 13) { + locExplanation = (t("intro.permissions.locationPermExplanation-ios-lt-13")); + } else { + locExplanation = (t("intro.permissions.locationPermExplanation-ios-gte-13")); + } + } + tempExplanations.push({name: t('intro.appstatus.overall-loc-name'), desc: locExplanation}); + tempExplanations.push({name: overallFitnessName, desc: t('intro.appstatus.overall-fitness-description')}); + tempExplanations.push({name: t('intro.appstatus.overall-notification-name'), desc: t('intro.appstatus.overall-notification-description')}); + tempExplanations.push({name: t('intro.appstatus.overall-background-restrictions-name'), desc: t('intro.appstatus.overall-background-restrictions-description')}); + + setExplanationList(tempExplanations); + + //waiting on samsung feedback, need more information + setBackgroundRestricted(false); + if(window['device'].manufacturer.toLowerCase() == "samsung") { + setBackgroundRestricted(true); + setAllowBackgroundInstructions(t("intro.allow_background.samsung")); + } + + console.log("Explanation = "+explanationList); + } + + function createChecklist(){ + if(platform == "android") { + setupAndroidLocChecks(); + setupAndroidFitnessChecks(); + setupAndroidNotificationChecks(); + setupAndroidBackgroundRestrictionChecks(); + } else if (platform == "ios") { + setupIOSLocChecks(); + setupIOSFitnessChecks(); + setupAndroidNotificationChecks(); + } else { + setError("Alert! unknownplatform, no tracking"); + setErrorVis(true); + console.log("Alert! unknownplatform, no tracking"); //need an alert, can use AlertBar? + } + + refreshAllChecks(); + } + + //refreshing checks with the plugins to update the check's statusState + function refreshAllChecks() { + //refresh each check + checkList.forEach((lc) => { + lc.refresh(); + }); + console.log("setting checks are", checkList); + } + + //recomputing checks updates the visual cues of their status + function recomputeAllChecks() { + console.log("recomputing checks", checkList); + checkList.forEach((lc) => { + lc.statusIcon = iconMap(lc.statusState); + lc.statusColor = colorMap(lc.statusState) + }); + } + + //anytime the status changes, may need to show modal + useEffect(() => { + let currentlyOpen = window?.appStatusModalOpened; + if(!currentlyOpen && overallStatus == false && appConfig && haveSetText) { //trying to block early cases from throwing modal + window.appStatusModalOpened = true; + setPermitVis(true); + } + }, [overallStatus]); + + useAppStateChange( function() { + console.log("PERMISSION CHECK: app has resumed, should refresh"); + refreshAllChecks(); + }); + + //refresh when recompute message is broadcast + settingsScope.$on("recomputeAppStatus", function() { + console.log("PERMISSION CHECK: recomputing state"); + refreshAllChecks(); + }); + + //load when ready + useEffect(() => { + if (appConfig && window['device']?.platform) { + setPlatform(window['device'].platform.toLowerCase()); + setOsver(window['device'].version.split(".")[0]); + + if(!haveSetText) + { + //window.appStatusModalOpened = false; + setupPermissionText(); + setHaveSetText(true); + } + else{ + console.log("setting up permissions"); + createChecklist(); + } + + } + }, [appConfig]); + + useEffect (() => { + if(!permitVis) { + window.appStatusModalOpened = false; + } + }, [permitVis]); + + //anytime the checks change (mostly when refreshed), recompute the visual pieces + useEffect(() => { + console.log("checklist changed, updating", checkList); + recomputeAllChecks(); + }, [checkList]) + + return ( + <> + setPermitVis(false)} transparent={true}> + setPermitVis(false)} + style={dialogStyle}> + {t('consent.permissions')} + + + {t('intro.appstatus.overall-description')} + + + {checkList?.map((lc) => + + + )} + + + + + + + + + + + + ) +} + +export default AppStatusModal; \ No newline at end of file diff --git a/www/js/control/DataDatePicker.tsx b/www/js/control/DataDatePicker.tsx new file mode 100644 index 000000000..b36054d26 --- /dev/null +++ b/www/js/control/DataDatePicker.tsx @@ -0,0 +1,47 @@ +// this date picker element is set up to handle the "download data from day" in ProfileSettings +// it relies on an angular service (Control Helper) but when we migrate that we might want to download a range instead of single + +import React from "react"; +import { DatePickerModal } from 'react-native-paper-dates'; +import { useTranslation } from "react-i18next"; +import { getAngularService } from "../angular-react-helper"; + +const DataDatePicker = ({date, setDate, open, setOpen}) => { + const { t, i18n } = useTranslation(); //able to pull lang from this + const ControlHelper = getAngularService("ControlHelper"); + + const onDismiss = React.useCallback(() => { + setOpen(false); + }, [setOpen]); + + const onConfirm = React.useCallback( + (params) => { + setOpen(false); + setDate(params.date); + ControlHelper.getMyData(params.date); + }, + [setOpen, setDate] + ); + + const minDate = new Date(2015, 1, 1); + const maxDate = new Date(); + + return ( + <> + + + ); +} + +export default DataDatePicker; \ No newline at end of file diff --git a/www/js/control/PopOpCode.jsx b/www/js/control/PopOpCode.jsx index 23368459d..04a866f18 100644 --- a/www/js/control/PopOpCode.jsx +++ b/www/js/control/PopOpCode.jsx @@ -1,30 +1,53 @@ -import React from "react"; +import React, { useState } from "react"; import { Modal, StyleSheet } from 'react-native'; import { Button, Text, IconButton, Dialog, useTheme } from 'react-native-paper'; import { useTranslation } from "react-i18next"; import QrCode from "../components/QrCode"; +import AlertBar from "./AlertBar"; const PopOpCode = ({visibilityValue, tokenURL, action, setVis}) => { const { t } = useTranslation(); const { colors } = useTheme(); + + const opcodeList = tokenURL.split("="); + const opcode = opcodeList[opcodeList.length - 1]; + const [copyAlertVis, setCopyAlertVis] = useState(false); + + const copyText = function(textToCopy){ + navigator.clipboard.writeText(textToCopy).then(() => { + setCopyAlertvis(true); + }) + } + + let copyButton; + if (window.cordova.platformId == "ios"){ + copyButton = {copyText(opcode); setCopyAlertVis(true)}} style={styles.button}/> + } + return ( - setVis(false)} - transparent={true}> - setVis(false)} - style={styles.dialog(colors.elevation.level3)}> - {t("general-settings.qrcode")} - - - {t("general-settings.qrcode-share-title")} - action()} style={styles.button}/> - - - - - - + <> + setVis(false)} + transparent={true}> + setVis(false)} + style={styles.dialog(colors.elevation.level3)}> + {t("general-settings.qrcode")} + + {t("general-settings.qrcode-share-title")} + + {opcode} + + + action()} style={styles.button}/> + {copyButton} + + + + + + + ) } const styles = StyleSheet.create({ @@ -40,9 +63,19 @@ const styles = StyleSheet.create({ content: { alignItems: 'center', justifyContent: 'center', + margin: 5 }, button: { margin: 'auto', + }, + opcode: { + fontFamily: "monospace", + wordBreak: "break-word", + marginTop: 5 + }, + text : { + fontWeight: 'bold', + marginBottom: 5 } }); diff --git a/www/js/control/PrivacyPolicyModal.tsx b/www/js/control/PrivacyPolicyModal.tsx new file mode 100644 index 000000000..7451581a6 --- /dev/null +++ b/www/js/control/PrivacyPolicyModal.tsx @@ -0,0 +1,195 @@ +import React, { useMemo } from "react"; +import { Modal, useWindowDimensions, ScrollView, Linking, StyleSheet, Text } from "react-native"; +import { Dialog, Button, useTheme } from 'react-native-paper'; +import { useTranslation } from "react-i18next"; +import useAppConfig from "../useAppConfig"; +import i18next from "i18next"; + +const PrivacyPolicyModal = ({ privacyVis, setPrivacyVis, dialogStyle }) => { + const { t } = useTranslation(); + const { height: windowHeight } = useWindowDimensions(); + const { colors } = useTheme(); + const { appConfig, loading } = useAppConfig(); + + const getTemplateText = function(configObject) { + if (configObject && (configObject.name)) { + return configObject.intro.translated_text[i18next.language]; + } + } + + 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), [appConfig]); + + return ( + <> + setPrivacyVis(false)} transparent={true}> + setPrivacyVis(false)} + style={dialogStyle}> + {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 + } + }); + +export default PrivacyPolicyModal; \ No newline at end of file diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 7249e93e0..62f79e67a 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from "react"; -import { Modal, StyleSheet } from "react-native"; -import { Dialog, Button, useTheme } from "react-native-paper"; +import { Modal, StyleSheet, ScrollView } from "react-native"; +import { Dialog, Button, useTheme, Text, Appbar, IconButton } from "react-native-paper"; import { angularize, getAngularService } from "../angular-react-helper"; import { useTranslation } from "react-i18next"; import ExpansionSection from "./ExpandMenu"; @@ -10,6 +10,10 @@ import DemographicsSettingRow from "./DemographicsSettingRow"; import PopOpCode from "./PopOpCode"; import ReminderTime from "./ReminderTime" import useAppConfig from "../useAppConfig"; +import AlertBar from "./AlertBar"; +import DataDatePicker from "./DataDatePicker"; +import AppStatusModal from "./AppStatusModal"; +import PrivacyPolicyModal from "./PrivacyPolicyModal"; let controlUpdateCompleteListenerRegistered = false; @@ -21,14 +25,12 @@ const ProfileSettings = () => { const { colors } = useTheme(); // get the scope of the general-settings.js file - const mainControlEl = document.getElementById('main-control').querySelector('ion-view'); - const settingsScope = angular.element(mainControlEl).scope(); + const mainControlEl = document.getElementById('main-control'); + const settingsScope = angular.element(mainControlEl.querySelector('profile-settings')).scope(); + console.log("settings scope", settingsScope); + // grab any variables or functions we need from it like this: - const { settings, logOut, viewPrivacyPolicy, - fixAppStatus, forceSync, openDatePicker, - eraseUserData, refreshScreen, endForceSync, checkConsent, - dummyNotification, invalidateCache, showLog, showSensed, - parseState, userDataSaved, userData, ui_config } = settingsScope; + const { showLog, showSensed } = settingsScope; //angular services needed const CarbonDatasetHelper = getAngularService('CarbonDatasetHelper'); @@ -36,9 +38,12 @@ const ProfileSettings = () => { const EmailHelper = getAngularService('EmailHelper'); const ControlCollectionHelper = getAngularService('ControlCollectionHelper'); const ControlSyncHelper = getAngularService('ControlSyncHelper'); - const CalorieCal = getAngularService('CalorieCal'); const KVStore = getAngularService('KVStore'); const NotificationScheduler = getAngularService('NotificationScheduler'); + const ControlHelper = getAngularService('ControlHelper'); + const ClientStats = getAngularService('ClientStats'); + const StartPrefs = getAngularService('StartPrefs'); + const DynamicConfig = getAngularService('DynamicConfig'); if (!controlUpdateCompleteListenerRegistered) { settingsScope.$on('control.update.complete', function() { @@ -58,8 +63,28 @@ const ProfileSettings = () => { const [nukeSetVis, setNukeVis] = useState(false); const [carbonDataVis, setCarbonDataVis] = useState(false); const [forceStateVis, setForceStateVis] = useState(false); + const [permitVis, setPermitVis] = useState(false); + const [logoutVis, setLogoutVis] = useState(false); + const [dataPendingVis, setDataPendingVis] = useState(false); + const [dataPushedVis, setDataPushedVis] = useState(false); + const [invalidateSuccessVis, setInvalidateSuccessVis] = useState(false); + const [noConsentVis, setNoConsentVis] = useState(false); + const [noConsentMessageVis, setNoConsentMessageVis] = useState(false); + const [consentVis, setConsentVis] = useState(false); + const [dateDumpVis, setDateDumpVis] = useState(false); + const [privacyVis, setPrivacyVis] = useState(false); + const [collectSettings, setCollectSettings] = useState({}); const [notificationSettings, setNotificationSettings] = useState({}); + const [authSettings, setAuthSettings] = useState({}); + const [syncSettings, setSyncSettings] = useState({}); + const [cacheResult, setCacheResult] = useState(""); + const [connectSettings, setConnectSettings] = useState({}); + const [appVersion, setAppVersion] = useState({}); + const [uiConfig, setUiConfig] = useState({}); + const [consentDoc, setConsentDoc] = useState({}); + const [dumpDate, setDumpDate] = useState(new Date()); + const [toggleTime, setToggleTime] = useState(new Date()); let carbonDatasetString = t('general-settings.carbon-dataset') + ": " + CarbonDatasetHelper.getCurrentCarbonDatasetCode(); const carbonOptions = CarbonDatasetHelper.getCarbonDatasetOptions(); @@ -71,12 +96,50 @@ const ProfileSettings = () => { {text: 'Remote push', transition: "RECEIVED_SILENT_PUSH"}] useEffect(() => { - if (appConfig) { - refreshCollectSettings(); - refreshNotificationSettings(); + //added appConfig.name needed to be defined because appConfig was defined but empty + if (appConfig && (appConfig.name)) { + whenReady(appConfig); } }, [appConfig]); + const refreshScreen = function() { + refreshCollectSettings(); + refreshNotificationSettings(); + getOPCode(); + getSyncSettings(); + getConnectURL(); + setAppVersion(ClientStats.getAppVersion()); + } + + const whenReady = function(newAppConfig){ + var tempUiConfig = newAppConfig; + + // backwards compat hack to fill in the raw_data_use for programs that don't have it + const default_raw_data_use = { + "en": `to monitor the ${tempUiConfig.intro.program_or_study}, send personalized surveys or provide recommendations to participants`, + "es": `para monitorear el ${tempUiConfig.intro.program_or_study}, enviar encuestas personalizadas o proporcionar recomendaciones a los participantes` + } + Object.entries(tempUiConfig.intro.translated_text).forEach(([lang, val]) => { + val.raw_data_use = val.raw_data_use || default_raw_data_use[lang]; + }); + + // Backwards compat hack to fill in the `app_required` based on the + // old-style "program_or_study" + // remove this at the end of 2023 when all programs have been migrated over + if (tempUiConfig.intro.app_required == undefined) { + tempUiConfig.intro.app_required = tempUiConfig?.intro.program_or_study == 'program'; + } + tempUiConfig.opcode = tempUiConfig.opcode || {}; + if (tempUiConfig.opcode.autogen == undefined) { + tempUiConfig.opcode.autogen = tempUiConfig?.intro.program_or_study == 'study'; + } + + // setTemplateText(tempUiConfig.intro.translated_text); + // console.log("translated text is??", templateText); + setUiConfig(tempUiConfig); + refreshScreen(); + } + async function refreshCollectSettings() { console.debug('about to refreshCollectSettings, collectSettings = ', collectSettings); const newCollectSettings = {}; @@ -90,11 +153,7 @@ const ProfileSettings = () => { newCollectSettings.trackingOn = collectionPluginState != "local.state.tracking_stopped" && collectionPluginState != "STATE_TRACKING_STOPPED"; - // I am not sure that this is actually needed anymore since https://github.com/e-mission/e-mission-data-collection/commit/92f41145e58c49e3145a9222a78d1ccacd16d2a7 - const geofenceConfig = await KVStore.get("OP_GEOFENCE_CFG"); - newCollectSettings.experimentalGeofenceOn = geofenceConfig != null; - - const isLowAccuracy = ControlCollectionHelper.isMediumAccuracy(); + const isLowAccuracy = await ControlCollectionHelper.isMediumAccuracy(); if (typeof isLowAccuracy != 'undefined') { newCollectSettings.lowAccuracy = isLowAccuracy; } @@ -106,7 +165,7 @@ const ProfileSettings = () => { console.debug('about to refreshNotificationSettings, notificationSettings = ', notificationSettings); const newNotificationSettings ={}; - if (ui_config?.reminderSchemes) { + if (uiConfig?.reminderSchemes) { const prefs = await NotificationScheduler.getReminderPrefs(); const m = moment(prefs.reminder_time_of_day, 'HH:mm'); newNotificationSettings.prefReminderTimeVal = m.toDate(); @@ -120,6 +179,38 @@ const ProfileSettings = () => { console.log("notification settings before and after", notificationSettings, newNotificationSettings); setNotificationSettings(newNotificationSettings); } + + async function getSyncSettings() { + console.log("getting sync settings"); + var newSyncSettings = {}; + ControlSyncHelper.getSyncSettings().then(function(showConfig) { + newSyncSettings.show_config = showConfig; + setSyncSettings(newSyncSettings); + console.log("sync settings are ", syncSettings); + }); + }; + + async function getConnectURL() { + ControlHelper.getSettings().then(function(response) { + var newConnectSettings ={} + newConnectSettings.url = response.connectUrl; + console.log(response); + setConnectSettings(newConnectSettings); + }, function(error) { + Logger.displayError("While getting connect url", error); + }); + } + + async function getOPCode() { + const newAuthSettings = {}; + const opcode = await ControlHelper.getOPCode(); + if(opcode == null){ + newAuthSettings.opcode = "Not logged in"; + } else { + newAuthSettings.opcode = opcode; + } + setAuthSettings(newAuthSettings); + }; //methods that control the settings const uploadLog = function () { @@ -142,6 +233,20 @@ const ProfileSettings = () => { } } + function dummyNotification() { + cordova.plugins.notification.local.addActions('dummy-actions', [ + { id: 'action', title: 'Yes' }, + { id: 'cancel', title: 'No' } + ]); + cordova.plugins.notification.local.schedule({ + id: new Date().getTime(), + title: 'Dummy Title', + text: 'Dummy text', + actions: 'dummy-actions', + trigger: {at: new Date(new Date().getTime() + 5000)}, + }); + } + async function userStartStopTracking() { const transitionToForce = collectSettings.trackingOn ? 'STOP_TRACKING' : 'START_TRACKING'; ControlCollectionHelper.forceTransition(transitionToForce); @@ -150,54 +255,206 @@ const ProfileSettings = () => { So we don't need to call refreshCollectSettings here. */ } - const toggleLowAccuracy = function() { + + const safeToggle = function() { + if(toggleTime){ + const prevTime = toggleTime.getTime(); + const currTime = new Date().getTime(); + if(prevTime + 2000 < currTime ){ + toggleLowAccuracy(); + setToggleTime(new Date()); + } + } + else { + toggleLowAccuracy(); + setToggleTime(new Date()); + } + } + + async function toggleLowAccuracy() { ControlCollectionHelper.toggleLowAccuracy(); refreshCollectSettings(); } const shareQR = function() { - var prepopulateQRMessage = {}; - var qrAddress = "emission://login_token?token="+settings?.auth?.opcode; - prepopulateQRMessage.files = [qrAddress]; - prepopulateQRMessage.url = settings.auth.opcode; - - window.plugins.socialsharing.shareWithOptions(prepopulateQRMessage, function(result) { - console.log("Share completed? " + result.completed); // On Android apps mostly return false even while it's true - console.log("Shared to app: " + result.app); // On Android result.app is currently empty. On iOS it's empty when sharing is cancelled (result.completed=false) - }, function(msg) { - console.log("Sharing failed with message: " + msg); - }); + /*code adapted from demo of react-qr-code*/ + const svg = document.querySelector(".qr-code"); + const svgData = new XMLSerializer().serializeToString(svg); + const img = new Image(); + + img.onload = () => { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + canvas.width = img.width; + canvas.height = img.height; + ctx.drawImage(img, 0, 0); + const pngFile = canvas.toDataURL("image/png"); + + var prepopulateQRMessage = {}; + prepopulateQRMessage.files = [pngFile]; + prepopulateQRMessage.url = authSettings.opcode; + prepopulateQRMessage.message = authSettings.opcode; //text saved to files with image! + + window.plugins.socialsharing.shareWithOptions(prepopulateQRMessage, function(result) { + console.log("Share completed? " + result.completed); // On Android apps mostly return false even while it's true + console.log("Shared to app: " + result.app); // On Android result.app is currently empty. On iOS it's empty when sharing is cancelled (result.completed=false) + }, function(msg) { + console.log("Sharing failed with message: " + msg); + }); + } + img.src = `data:image/svg+xml;base64,${btoa(svgData)}`; } const viewQRCode = function(e) { setOpCodeVis(true); } + + const clearNotifications = function() { + window.cordova.plugins.notification.local.clearAll(); + } - var prepopulateMessage = { - message: t('general-settings.share-message'), - subject: t('general-settings.share-subject'), - url: t('general-settings.share-url') + //Platform.OS returns "web" now, but could be used once it's fully a Native app + //for now, use window.cordova.platformId + + // helper functions for endForceSync + const getStartTransitionKey = function() { + if(window.cordova.platformId == 'android') { + return "local.transition.exited_geofence"; + } + else if(window.cordova.platformId == 'ios') { + return "T_EXITED_GEOFENCE"; + } } - const share = function() { - window.plugins.socialsharing.shareWithOptions(prepopulateMessage, function(result) { - console.log("Share completed? " + result.completed); // On Android apps mostly return false even while it's true - console.log("Shared to app: " + result.app); // On Android result.app is currently empty. On iOS it's empty when sharing is cancelled (result.completed=false) - }, function(msg) { - console.log("Sharing failed with message: " + msg); + const getEndTransitionKey = function() { + if(window.cordova.platformId == 'android') { + return "local.transition.stopped_moving"; + } + else if(window.cordova.platformId == 'ios') { + return "T_TRIP_ENDED"; + } + } + + const getOngoingTransitionState = function() { + if(window.cordova.platformId == 'android') { + return "local.state.ongoing_trip"; + } + else if(window.cordova.platformId == 'ios') { + return "STATE_ONGOING_TRIP"; + } + } + + async function getTransition(transKey) { + var entry_data = {}; + const curr_state = await ControlCollectionHelper.getState(); + entry_data.curr_state = curr_state; + if (transKey == getEndTransitionKey()) { + entry_data.curr_state = getOngoingTransitionState(); + } + entry_data.transition = transKey; + entry_data.ts = moment().unix(); + return entry_data; + } + + const parseState = function(state) { + console.log("state in parse state is", state); + if (state) { + console.log("state in parse state exists", window.cordova.platformId); + if(window.cordova.platformId == 'android') { + console.log("ANDROID state in parse state is", state.substring(12)); + return state.substring(12); + } + else if(window.cordova.platformId == 'ios') { + console.log("IOS state in parse state is", state.substring(6)); + return state.substring(6); + } + } + } + + async function endForceSync() { + /* First, quickly start and end the trip. Let's listen to the promise + * result for start so that we ensure ordering */ + var sensorKey = "statemachine/transition"; + return getTransition(getStartTransitionKey()).then(function(entry_data) { + return window.cordova.plugins.BEMUserCache.putMessage(sensorKey, entry_data); + }).then(function() { + return getTransition(getEndTransitionKey()).then(function(entry_data) { + return window.cordova.plugins.BEMUserCache.putMessage(sensorKey, entry_data); + }) + }).then(forceSync); + } + + //showing up in an odd space on the screen!! + async function forceSync() { + ClientStats.addEvent(ClientStats.getStatKeys().BUTTON_FORCE_SYNC).then( + function() { + console.log("Added "+ClientStats.getStatKeys().BUTTON_FORCE_SYNC+" event"); }); + ControlSyncHelper.forceSync().then(function() { + /* + * Change to sensorKey to "background/location" after fixing issues + * with getLastSensorData and getLastMessages in the usercache + * See https://github.com/e-mission/e-mission-phone/issues/279 for details + */ + var sensorKey = "statemachine/transition"; + return window.cordova.plugins.BEMUserCache.getAllMessages(sensorKey, true); + }).then(function(sensorDataList) { + Logger.log("sensorDataList = "+JSON.stringify(sensorDataList)); + // If everything has been pushed, we should + // only have one entry for the battery, which is the one that was + // inserted on the last successful push. + var isTripEnd = function(entry) { + if (entry.metadata.key == getEndTransitionKey()) { + return true; + } else { + return false; + } + }; + var syncLaunchedCalls = sensorDataList.filter(isTripEnd); + var syncPending = (syncLaunchedCalls.length > 0); + Logger.log("sensorDataList.length = "+sensorDataList.length+ + ", syncLaunchedCalls.length = "+syncLaunchedCalls.length+ + ", syncPending? = "+syncPending); + return syncPending; + }).then(function(syncPending) { + Logger.log("sync launched = "+syncPending); + if (syncPending) { + Logger.log("data is pending, showing confirm dialog"); + setDataPendingVis(true); //consent handling in modal + } else { + setDataPushedVis(true); + } + }).catch(function(error) { + Logger.displayError("Error while forcing sync", error); + }); + }; + + async function invalidateCache() { + window.cordova.plugins.BEMUserCache.invalidateAllCache().then(function(result) { + console.log("invalidate result", result); + setCacheResult(result); + setInvalidateSuccessVis(true); + }, function(error) { + Logger.displayError("while invalidating cache, error->", error); + }); } - //conditional creation of setting sections - let userDataSection; - if(userDataSaved()) - { - userDataSection = - - - ; + //in ProfileSettings in DevZone (above two functions are helpers) + async function checkConsent() { + StartPrefs.getConsentDocument().then(function(resultDoc){ + setConsentDoc(resultDoc); + if (resultDoc == null) { + setNoConsentVis(true); + } else { + setConsentVis(true); + } + }, function(error) { + Logger.displayError("Error reading consent document from cache", error) + }); } + //conditional creation of setting sections + let logUploadSection; console.debug("appConfg: support_upload:", appConfig?.profile_controls?.support_upload); if (appConfig?.profile_controls?.support_upload) { @@ -215,42 +472,46 @@ const ProfileSettings = () => { return ( <> - - - - - {timePicker} - - - - setCarbonDataVis(true)}> - - - - {logUploadSection} - - - {userDataSection} + + + {t('control.log-out')} + setLogoutVis(true)}> + + + + + + setPrivacyVis(true)}> + {timePicker} + + setPermitVis(true)}> + + setCarbonDataVis(true)}> + + setDateDumpVis(true)}> + {logUploadSection} + - - - - - - {notifSchedule} - - setNukeVis(true)}> - setForceStateVis(true)}> - - - - - - - console.log("")} desc={settings?.clientAppVer}> - - - {/* menu for "nuke data" */} + + + + + + {notifSchedule} + + setNukeVis(true)}> + setForceStateVis(true)}> + + + + + + + console.log("")} desc={appVersion}> + + + + {/* menu for "nuke data" */} setNukeVis(false)} transparent={true}> { - {/* menu for "set carbon dataset - only somewhat working" */} + {/* menu for "set carbon dataset - only somewhat working" */} setCarbonDataVis(false)} transparent={true}> { )} - + @@ -332,14 +594,110 @@ const ProfileSettings = () => { {/* opcode viewing popup */} - + + + {/* {view permissions} */} + + + {/* {view privacy} */} + + + {/* logout menu */} + setLogoutVis(false)} transparent={true}> + setLogoutVis(false)} + style={styles.dialog(colors.elevation.level3)}> + {t('general-settings.are-you-sure')} + + {t('general-settings.log-out-warning')} + + + + + + + + + {/* dataPending */} + setDataPendingVis(false)} transparent={true}> + setDataPendingVis(false)} + style={styles.dialog(colors.elevation.level3)}> + {t('data pending for push')} + + + + + + + + {/* handle no consent */} + setNoConsentVis(false)} transparent={true}> + setNoConsentVis(false)} + style={styles.dialog(colors.elevation.level3)}> + {t('general-settings.consent-not-found')} + + + + + + + + {/* handle consent */} + setConsentVis(false)} transparent={true}> + setConsentVis(false)} + style={styles.dialog(colors.elevation.level3)}> + {t('general-settings.consented-to', {protocol_id: consentDoc.protocol_id, approval_date: consentDoc.approval_date})} + + + + + + + + + + + + ); }; const styles = StyleSheet.create({ dialog: (surfaceColor) => ({ backgroundColor: surfaceColor, - margin: 1, + margin: 5, + marginLeft: 25, + marginRight: 25 }), monoDesc: { fontSize: 12, diff --git a/www/js/control/general-settings.js b/www/js/control/general-settings.js index 023aca98d..5225af1c6 100644 --- a/www/js/control/general-settings.js +++ b/www/js/control/general-settings.js @@ -9,546 +9,36 @@ angular.module('emission.main.control',['emission.services', 'emission.main.control.sync', 'emission.splash.localnotify', 'emission.splash.notifscheduler', - 'ionic-datepicker', - 'ionic-toast', - 'ionic-datepicker.provider', 'emission.splash.startprefs', 'emission.main.metrics.factory', 'emission.stats.clientstats', 'emission.plugin.kvstore', - 'emission.survey.enketo.demographics', 'emission.plugin.logger', 'emission.config.dynamic', ProfileSettings.module]) -.controller('ControlCtrl', function($scope, $window, - $ionicScrollDelegate, $ionicPlatform, - $state, $ionicPopup, $ionicActionSheet, $ionicPopover, - $ionicModal, $stateParams, - $rootScope, KVStore, ionicDatePicker, ionicToast, +.controller('ControlCtrl', function($scope, $ionicPlatform, + $state, $ionicPopover, i18nUtils, + $ionicModal, $stateParams, Logger, + KVStore, CalorieCal, ClientStats, StartPrefs, ControlHelper, EmailHelper, UploadHelper, ControlCollectionHelper, ControlSyncHelper, - CarbonDatasetHelper, NotificationScheduler, LocalNotify, - i18nUtils, - CalorieCal, ClientStats, CommHelper, Logger, DynamicConfig) { + CarbonDatasetHelper, NotificationScheduler, DynamicConfig) { console.log("controller ControlCtrl called without params"); - var datepickerObject = { - todayLabel: i18next.t('list-datepicker-today'), //Optional - closeLabel: i18next.t('list-datepicker-close'), //Optional - setLabel: i18next.t('list-datepicker-set'), //Optional - monthsList: moment.monthsShort(), - weeksList: moment.weekdaysMin(), - titleLabel: i18next.t('general-settings.choose-date'), - setButtonType : 'button-positive', //Optional - todayButtonType : 'button-stable', //Optional - closeButtonType : 'button-stable', //Optional - inputDate: moment().subtract(1, 'week').toDate(), //Optional - from: new Date(2015, 1, 1), - to: new Date(), - mondayFirst: true, //Optional - templateType: 'popup', //Optional - showTodayButton: 'true', //Optional - modalHeaderColor: 'bar-positive', //Optional - modalFooterColor: 'bar-positive', //Optional - callback: ControlHelper.getMyData, //Mandatory - dateFormat: 'dd MMM yyyy', //Optional - closeOnSelect: true //Optional - } - - $scope.overallAppStatus = false; - - $ionicModal.fromTemplateUrl('templates/control/app-status-modal.html', { - scope: $scope - }).then(function(modal) { - $scope.appStatusModal = modal; - if ($stateParams.launchAppStatusModal == true) { - $scope.$broadcast("recomputeAppStatus"); - $scope.appStatusModal.show(); - } - }); - - $scope.openDatePicker = function(){ - ionicDatePicker.openDatePicker(datepickerObject); - }; - - //this function used in ProfileSettings to viewPrivacyPolicy - $scope.viewPrivacyPolicy = function($event) { - // button -> list element -> scroll - // const targetEl = $event.currentTarget.parentElement.parentElement; - if ($scope.ppp) { - $scope.ppp.show($event); - } else { - i18nUtils.geti18nFileName("templates/", "intro/consent-text", ".html").then((consentFileName) => { - $scope.consentTextFile = consentFileName; - $ionicPopover.fromTemplateUrl("templates/control/main-consent.html", {scope: $scope}).then((p) => { - $scope.ppp = p; - $scope.ppp.show($event); - }); - }).catch((err) => Logger.displayError("Error while displaying privacy policy", err)); - } - } - - //this function used in ProfileSettings to send DummyNotification - $scope.dummyNotification = () => { - cordova.plugins.notification.local.addActions('dummy-actions', [ - { id: 'action', title: 'Yes' }, - { id: 'cancel', title: 'No' } - ]); - cordova.plugins.notification.local.schedule({ - id: new Date().getTime(), - title: 'Dummy Title', - text: 'Dummy text', - actions: 'dummy-actions', - trigger: {at: new Date(new Date().getTime() + 5000)}, - }); - } - - //called in ProfileSettings on the AppStatus row - $scope.fixAppStatus = function() { - $scope.$broadcast("recomputeAppStatus"); - $scope.appStatusModal.show(); - } - - $scope.appStatusChecked = function() { - // Hardcoded value so we can publish the hacky version today and then debug/fix the - // infinite loop around waiting_for_trip_start -> tracking_error - $window.cordova.plugins.notification.local.clearAll(); - $scope.appStatusModal.hide(); - } - - $scope.userData = [] - $scope.getUserData = function() { - return CalorieCal.get().then(function(userDataFromStorage) { - $scope.rawUserData = userDataFromStorage; - if ($scope.userDataSaved()) { - $scope.userData = [] - var height = userDataFromStorage.height.toString(); - var weight = userDataFromStorage.weight.toString(); - var temp = { - age: userDataFromStorage.age, - height: height + (userDataFromStorage.heightUnit == 1? ' cm' : ' ft'), - weight: weight + (userDataFromStorage.weightUnit == 1? ' kg' : ' lb'), - gender: userDataFromStorage.gender == 1? i18next.t('gender-male') : i18next.t('gender-female') - } - for (var i in temp) { - $scope.userData.push({key: i, val: temp[i]}); //needs to be val for the data table! - } - } - }); - } - - $scope.userDataSaved = function() { - if (angular.isDefined($scope.rawUserData) && $scope.rawUserData != null) { - return $scope.rawUserData.userDataSaved; - } else { - return false; - } - } - $ionicPlatform.ready().then(function() { - DynamicConfig.configReady().then(function(newConfig) { - $scope.ui_config = newConfig; - // backwards compat hack to fill in the raw_data_use for programs that don't have it - const default_raw_data_use = { - "en": `to monitor the ${newConfig.intro.program_or_study}, send personalized surveys or provide recommendations to participants`, - "es": `para monitorear el ${newConfig.intro.program_or_study}, enviar encuestas personalizadas o proporcionar recomendaciones a los participantes` - } - Object.entries(newConfig.intro.translated_text).forEach(([lang, val]) => { - val.raw_data_use = val.raw_data_use || default_raw_data_use[lang]; - }); - // TODO: we should be able to use $translate for this, right? - $scope.template_text = newConfig.intro.translated_text[$scope.lang]; - if (!$scope.template_text) { - $scope.template_text = newConfig.intro.translated_text["en"] - } - // Backwards compat hack to fill in the `app_required` based on the - // old-style "program_or_study" - // remove this at the end of 2023 when all programs have been migrated over - if ($scope.ui_config.intro.app_required == undefined) { - $scope.ui_config.intro.app_required = $scope.ui_config?.intro.program_or_study == 'program'; - } - $scope.ui_config.opcode = $scope.ui_config.opcode || {}; - if ($scope.ui_config.opcode.autogen == undefined) { - $scope.ui_config.opcode.autogen = $scope.ui_config?.intro.program_or_study == 'study'; - } - $scope.refreshScreen(); - }); - }); + //used to have on "afterEnter" that checked for 2 things + //modal launch -> migrated into AppStatusModal w/ use of custom hook! + //stateParams.openTimeOfDayPicker -> functionality lost for now + //to change reminder time if accessing profile by specific android notification flow + //would open the date picker - $scope.getConnectURL = function() { - ControlHelper.getSettings().then(function(response) { - $scope.$apply(function() { - $scope.settings.connect.url = response.connectUrl; - console.log(response); - }); - }, function(error) { - Logger.displayError("While getting connect url", error); - }); - }; - - $scope.getSyncSettings = function() { - ControlSyncHelper.getSyncSettings().then(function(showConfig) { - $scope.$apply(function() { - $scope.settings.sync.show_config = showConfig; - }) - }); - }; - - $scope.getOPCode = function() { - ControlHelper.getOPCode().then(function(opcode) { - console.log("opcode = "+opcode); - $scope.$apply(function() { - if (opcode == null) { - $scope.settings.auth.opcode = "Not logged in"; - } else { - $scope.settings.auth.opcode = opcode; - } - }); - }, function(error) { - Logger.displayError("while getting opcode, ",error); - }); - }; - //in ProfileSettings in DevZone + //TODO create React pages and use React routing $scope.showLog = function() { $state.go("root.main.log"); } - //inProfileSettings in DevZone $scope.showSensed = function() { $state.go("root.main.sensed"); } - $scope.getState = function() { - return ControlCollectionHelper.getState().then(function(response) { - /* collect state is now stored in ProfileSettings' collectSettings */ - // $scope.$apply(function() { - // $scope.settings.collect.state = response; - // }); - return response; - }, function(error) { - Logger.displayError("while getting current state", error); - }); - }; - - var clearUsercache = function() { - $ionicPopup.alert({template: "WATCH OUT! If there is unsynced data, you may lose it. If you want to keep the data, use 'Force Sync' before doing this"}) - .then(function(result) { - if (result) { - window.cordova.plugins.BEMUserCache.clearAll() - .then(function(result) { - $scope.$apply(function() { - $ionicPopup.alert({template: 'success -> '+result}); - }); - }, function(error) { - Logger.displayError("while clearing user cache, error ->", error); - }); - } - }); - } - - //in ProfileSettings in DevZone - $scope.invalidateCache = function() { - window.cordova.plugins.BEMUserCache.invalidateAllCache().then(function(result) { - $scope.$apply(function() { - $ionicPopup.alert({template: 'success -> '+result}); - }); - }, function(error) { - Logger.displayError("while invalidating cache, error->", error); - }); - } - - $scope.$on('$ionicView.afterEnter', function() { - console.log("afterEnter called with stateparams", $stateParams); - $ionicPlatform.ready().then(function() { - $scope.refreshScreen(); - if ($stateParams.launchAppStatusModal == true) { - $scope.$broadcast("recomputeAppStatus"); - $scope.appStatusModal.show(); - $stateParams.launchAppStatusModal = false; - } - if ($stateParams.openTimeOfDayPicker) { - $('input[name=timeOfDay]').focus(); - } - }); - }) - - // Execute action on hidden popover - $scope.$on('control.update.complete', function() { - $scope.refreshScreen(); - }); - - $scope.$on('popover.hidden', function() { - $scope.refreshScreen(); - }); - - //in ProfileSettings in DevZone - $scope.refreshScreen = function() { - console.log("Refreshing screen"); - $scope.settings = {}; - $scope.settings.sync = {}; - $scope.settings.auth = {}; - $scope.settings.connect = {}; - $scope.settings.clientAppVer = ClientStats.getAppVersion(); - $scope.getConnectURL(); - $scope.getSyncSettings(); - $scope.getOPCode(); - $scope.getUserData(); - }; - - //this feature has been eliminated (as of right now) - // $scope.copyToClipboard = (textToCopy) => { - // navigator.clipboard.writeText(textToCopy).then(() => { - // ionicToast.show('{Copied to clipboard!}', 'bottom', false, 2000); - // }); - // } - - //used in ProfileSettings at the profile/logout/opcode row - $scope.logOut = function() { - $ionicPopup.confirm({ - title: i18next.t('general-settings.are-you-sure'), - template: i18next.t('general-settings.log-out-warning'), - cancelText: i18next.t('general-settings.cancel'), - okText: i18next.t('general-settings.confirm') - }).then(function(res) { - if (!res) return; // user cancelled - - // reset the saved config, then trigger a hard refresh - DynamicConfig.resetConfigAndRefresh(); - }); - }; - - var getStartTransitionKey = function() { - if($scope.isAndroid()) { - return "local.transition.exited_geofence"; - } - else if($scope.isIOS()) { - return "T_EXITED_GEOFENCE"; - } - } - - var getEndTransitionKey = function() { - if($scope.isAndroid()) { - return "local.transition.stopped_moving"; - } - else if($scope.isIOS()) { - return "T_TRIP_ENDED"; - } - } - - var getOngoingTransitionState = function() { - if($scope.isAndroid()) { - return "local.state.ongoing_trip"; - } - else if($scope.isIOS()) { - return "STATE_ONGOING_TRIP"; - } - } - - $scope.forceSync = function() { - ClientStats.addEvent(ClientStats.getStatKeys().BUTTON_FORCE_SYNC).then( - function() { - console.log("Added "+ClientStats.getStatKeys().BUTTON_FORCE_SYNC+" event"); - }); - ControlSyncHelper.forceSync().then(function() { - /* - * Change to sensorKey to "background/location" after fixing issues - * with getLastSensorData and getLastMessages in the usercache - * See https://github.com/e-mission/e-mission-phone/issues/279 for details - */ - var sensorKey = "statemachine/transition"; - return window.cordova.plugins.BEMUserCache.getAllMessages(sensorKey, true); - }).then(function(sensorDataList) { - Logger.log("sensorDataList = "+JSON.stringify(sensorDataList)); - // If everything has been pushed, we should - // only have one entry for the battery, which is the one that was - // inserted on the last successful push. - var isTripEnd = function(entry) { - if (entry.metadata.key == getEndTransitionKey()) { - return true; - } else { - return false; - } - }; - var syncLaunchedCalls = sensorDataList.filter(isTripEnd); - var syncPending = (syncLaunchedCalls.length > 0); - Logger.log("sensorDataList.length = "+sensorDataList.length+ - ", syncLaunchedCalls.length = "+syncLaunchedCalls.length+ - ", syncPending? = "+syncPending); - return syncPending; - }).then(function(syncPending) { - Logger.log("sync launched = "+syncPending); - if (syncPending) { - Logger.log("data is pending, showing confirm dialog"); - $ionicPopup.confirm({template: 'data pending for push'}).then(function(res) { - if (res) { - $scope.forceSync(); - } else { - Logger.log("user refused to re-sync"); - } - }); - } else { - $ionicPopup.alert({template: 'all data pushed!'}); - } - }).catch(function(error) { - Logger.displayError("Error while forcing sync", error); - }); - }; - - var getTransition = function(transKey) { - var entry_data = {}; - return $scope.getState().then(function(curr_state) { - entry_data.curr_state = curr_state; - if (transKey == getEndTransitionKey()) { - entry_data.curr_state = getOngoingTransitionState(); - } - entry_data.transition = transKey; - entry_data.ts = moment().unix(); - return entry_data; - }) - } - - //in ProfileSettings in DevZone - $scope.endForceSync = function() { - /* First, quickly start and end the trip. Let's listen to the promise - * result for start so that we ensure ordering */ - var sensorKey = "statemachine/transition"; - return getTransition(getStartTransitionKey()).then(function(entry_data) { - return window.cordova.plugins.BEMUserCache.putMessage(sensorKey, entry_data); - }).then(function() { - return getTransition(getEndTransitionKey()).then(function(entry_data) { - return window.cordova.plugins.BEMUserCache.putMessage(sensorKey, entry_data); - }) - }).then($scope.forceSync); - } - - $scope.isAndroid = function() { - return ionic.Platform.isAndroid(); - } - - $scope.isIOS = function() { - return ionic.Platform.isIOS(); - } - - $ionicPopover.fromTemplateUrl('templates/control/main-sync-settings.html', { - scope: $scope - }).then(function(popover) { - $scope.syncSettingsPopup = popover; - }); - - //in ProfileSettings in UserData - $scope.eraseUserData = function() { - CalorieCal.delete().then(function() { - $ionicPopup.alert({template: i18next.t('general-settings.user-data-erased')}); - }); - } - //in ProfileSettings in DevZone -- part of force/edit state - $scope.parseState = function(state) { - if (state) { - if($scope.isAndroid()){ - return state.substring(12); - } else if ($scope.isIOS()) { - return state.substring(6); - } - } - } - // //in ProfileSettings change carbon set - // $scope.changeCarbonDataset = function() { - // $ionicActionSheet.show({ - // buttons: CarbonDatasetHelper.getCarbonDatasetOptions(), - // titleText: i18next.t('general-settings.choose-dataset'), - // cancelText: i18next.t('general-settings.cancel'), - // buttonClicked: function(index, button) { - // console.log("changeCarbonDataset(): chose locale " + button.value); - // CarbonDatasetHelper.saveCurrentCarbonDatasetLocale(button.value); - // $scope.carbonDatasetString = i18next.t('general-settings.carbon-dataset') + ": " + CarbonDatasetHelper.getCurrentCarbonDatasetCode(); - // return true; - // } - // }); - // }; - - var handleNoConsent = function(resultDoc) { - $ionicPopup.confirm({template: i18next.t('general-settings.consent-not-found')}) - .then(function(res){ - if (res) { - $state.go("root.reconsent"); - } else { - $ionicPopup.alert({ - template: i18next.t('general-settings.no-consent-message')}); - } - }); - } - - var handleConsent = function(resultDoc) { - $scope.consentDoc = resultDoc; - $ionicPopup.confirm({ - template: i18next.t('general-settings.consented-to',{protocol_id: $scope.consentDoc.protocol_id,approval_date: $scope.consentDoc.approval_date}), - scope: $scope, - title: i18next.t('general-settings.consent-found'), - buttons: [ - // {text: "View", - // type: 'button-calm'}, - {text: ""+ i18next.t('general-settings.consented-ok') +"", - type: 'button-positive'} ] - }).finally(function(res) { - $scope.consentDoc = null; - }); - } - - //in ProfileSettings in DevZone (above two functions are helpers) - $scope.checkConsent = function() { - StartPrefs.getConsentDocument().then(function(resultDoc){ - if (resultDoc == null) { - handleNoConsent(resultDoc); - } else { - handleConsent(resultDoc); - } - }, function(error) { - Logger.displayError("Error reading consent document from cache", error) - }); - } - - var prepopulateMessage = { - message: i18next.t('general-settings.share-message'), // not supported on some apps (Facebook, Instagram) - subject: i18next.t('general-settings.share-subject'), // fi. for email - url: i18next.t('general-settings.share-url') - } - - $scope.share = function() { - window.plugins.socialsharing.shareWithOptions(prepopulateMessage, function(result) { - console.log("Share completed? " + result.completed); // On Android apps mostly return false even while it's true - console.log("Shared to app: " + result.app); // On Android result.app is currently empty. On iOS it's empty when sharing is cancelled (result.completed=false) - }, function(msg) { - console.log("Sharing failed with message: " + msg); - }); - } - - $scope.shareQR = function() { - /*code adapted from demo of react-qr-code - selector below gets svg element out of angularized QRCode - this will change upon later migration*/ - const svg = document.querySelector("qr-code svg"); - const svgData = new XMLSerializer().serializeToString(svg); - const img = new Image(); - - img.onload = () => { - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - canvas.width = img.width; - canvas.height = img.height; - ctx.drawImage(img, 0, 0); - const pngFile = canvas.toDataURL("image/png"); - - var prepopulateQRMessage = {}; - prepopulateQRMessage.files = [pngFile]; - prepopulateQRMessage.url = $scope.settings.auth.opcode; - window.plugins.socialsharing.shareWithOptions(prepopulateQRMessage, function(result) { - console.log("Share completed? " + result.completed); // On Android apps mostly return false even while it's true - console.log("Shared to app: " + result.app); // On Android result.app is currently empty. On iOS it's empty when sharing is cancelled (result.completed=false) - }, function(msg) { - console.log("Sharing failed with message: " + msg); - }); - } - img.src = `data:image/svg+xml;base64,${btoa(svgData)}`; - } - }); diff --git a/www/js/diary/LabelTab.tsx b/www/js/diary/LabelTab.tsx index c214e1f88..37e5d300b 100644 --- a/www/js/diary/LabelTab.tsx +++ b/www/js/diary/LabelTab.tsx @@ -20,6 +20,8 @@ import { compositeTrips2TimelineMap, getAllUnprocessedInputs, getLocalUnprocesse import { fillLocationNamesOfTrip, resetNominatimLimiter } from "./addressNamesHelper"; import { SurveyOptions } from "../survey/survey"; import { getLabelOptions } from "../survey/multilabel/confirmHelper"; +import AppStatusModal from "../control/AppStatusModal"; +import { useTheme } from "react-native-paper"; let labelPopulateFactory, labelsResultMap, notesResultMap, showPlaces; const ONE_DAY = 24 * 60 * 60; // seconds @@ -29,6 +31,7 @@ export const LabelTabContext = React.createContext(null); const LabelTab = () => { const { appConfig, loading } = useAppConfig(); const { t } = useTranslation(); + const { colors } = useTheme(); const [surveyOpt, setSurveyOpt] = useState(null); const [labelOptions, setLabelOptions] = useState(null); @@ -48,6 +51,8 @@ const LabelTab = () => { const CommHelper = getAngularService('CommHelper'); const enbs = getAngularService('EnketoNotesButtonService'); + const [permissionVis, setPermissionVis] = useState(false); + // initialization, once the appConfig is loaded useEffect(() => { if (loading) return; @@ -216,19 +221,7 @@ const LabelTab = () => { function checkPermissionsStatus() { $rootScope.$broadcast("recomputeAppStatus", (status) => { if (!status) { - $ionicPopup.show({ - title: t('control.incorrect-app-status'), - template: t('control.fix-app-status'), - scope: $rootScope, - buttons: [{ - text: t('control.fix'), - type: 'button-assertive', - onTap: function (e) { - $state.go('root.main.control', { launchAppStatusModal: 1 }); - return false; - } - }] - }); + setPermissionVis(true); //if the status is false, popup modal } }); } @@ -286,6 +279,10 @@ const LabelTab = () => { This is what `detachPreviousScreen:false` does. */ options={{detachPreviousScreen: false}} /> + ); diff --git a/www/js/intro.js b/www/js/intro.js index 5d74e7a5d..0bc7fa01d 100644 --- a/www/js/intro.js +++ b/www/js/intro.js @@ -164,6 +164,7 @@ angular.module('emission.intro', ['emission.splash.startprefs', var prepopulateQRMessage = {}; prepopulateQRMessage.files = [pngFile]; prepopulateQRMessage.url = $scope.currentToken; + prepopulateQRMessage.message = $scope.currentToken; window.plugins.socialsharing.shareWithOptions(prepopulateQRMessage, function(result) { console.log("Share completed? " + result.completed); // On Android apps mostly return false even while it's true diff --git a/www/js/main.js b/www/js/main.js index 652d46054..79bfdee0d 100644 --- a/www/js/main.js +++ b/www/js/main.js @@ -37,7 +37,7 @@ angular.module('emission.main', ['emission.main.diary', }, views: { 'main-control': { - templateUrl: 'templates/control/main-control.html', + template: ``, controller: 'ControlCtrl' } } diff --git a/www/js/useAppStateChange.ts b/www/js/useAppStateChange.ts new file mode 100644 index 000000000..8b9c6497c --- /dev/null +++ b/www/js/useAppStateChange.ts @@ -0,0 +1,29 @@ +//this is a custom hook that listens for change in app state +//detects when the app changed to active from inactive (ios) or background (android) +//the executes "onResume" function that is passed in +//https://reactnative.dev/docs/appstate based on react's example of detecting becoming active + +import { useEffect, useRef } from 'react'; +import { AppState } from 'react-native'; + +const useAppStateChange = (onResume) => { + + const appState = useRef(AppState.currentState); + + useEffect(() => { + const subscription = AppState.addEventListener('change', nextAppState => { + if ( appState.current != 'active' && nextAppState === 'active') { + onResume(); + } + + appState.current = nextAppState; + console.log('AppState', appState.current); + }); + + }, []); + + return {}; + } + + export default useAppStateChange; + \ No newline at end of file diff --git a/www/templates/control/main-control.html b/www/templates/control/main-control.html deleted file mode 100644 index 665eaf7e3..000000000 --- a/www/templates/control/main-control.html +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/www/templates/main-metrics.html b/www/templates/main-metrics.html index 40e731d91..d3c8b7ce3 100644 --- a/www/templates/main-metrics.html +++ b/www/templates/main-metrics.html @@ -100,7 +100,7 @@
{{'main-metrics.how-it-compares'}}

{{'main-metrics.calories'}}

-
{{'main-metrics.calibrate'}}
+
kcal