diff --git a/package.cordovabuild.json b/package.cordovabuild.json index 5018b3b05..c8fa65973 100644 --- a/package.cordovabuild.json +++ b/package.cordovabuild.json @@ -36,6 +36,7 @@ "expose-loader": "^4.1.0", "file-loader": "^6.2.0", "phonegap": "9.0.0+cordova.9.0.0", + "prettier": "3.1.0", "process": "^0.11.10", "sass": "^1.62.1", "sass-loader": "^13.3.1", @@ -57,6 +58,7 @@ "FCM_VERSION": "23.+", "IOS_FIREBASE_MESSAGING_VERSION": "~> 8.1.1" }, + "com.unarin.cordova.beacon": {}, "cordova-plugin-ionic-keyboard": {}, "cordova-plugin-app-version": {}, "cordova-plugin-file": {}, @@ -97,8 +99,7 @@ "ANDROID_SUPPORT_V4_VERSION": "27.+" }, "cordova-plugin-bluetooth-classic-serial-port": {}, - "cordova-custom-config": {}, - "com.unarin.cordova.beacon": {} + "cordova-custom-config": {} } }, "dependencies": { @@ -113,6 +114,7 @@ "chart.js": "^4.3.0", "chartjs-adapter-luxon": "^1.3.1", "chartjs-plugin-annotation": "^3.0.1", + "com.unarin.cordova.beacon": "github:e-mission/cordova-plugin-ibeacon", "cordova-android": "13.0.0", "cordova-ios": "7.1.1", "cordova-plugin-advanced-http": "3.3.1", @@ -120,7 +122,7 @@ "cordova-plugin-app-version": "0.1.14", "cordova-plugin-customurlscheme": "5.0.2", "cordova-plugin-device": "2.1.0", - "cordova-plugin-em-datacollection": "git+https://github.com/e-mission/e-mission-data-collection.git#v1.9.1", + "cordova-plugin-em-datacollection": "git+https://github.com/e-mission/e-mission-data-collection.git#v1.9.2", "cordova-plugin-em-opcodeauth": "git+https://github.com/e-mission/cordova-jwt-auth.git#v1.7.2", "cordova-plugin-em-server-communication": "git+https://github.com/e-mission/cordova-server-communication.git#v1.2.7", "cordova-plugin-em-serversync": "git+https://github.com/e-mission/cordova-server-sync.git#v1.3.3", @@ -135,7 +137,6 @@ "cordova-plugin-x-socialsharing": "6.0.4", "cordova-plugin-bluetooth-classic-serial-port": "git+https://github.com/e-mission/cordova-plugin-bluetooth-classic-serial-port.git", "cordova-custom-config": "^5.1.1", - "com.unarin.cordova.beacon": "github:e-mission/cordova-plugin-ibeacon", "core-js": "^2.5.7", "e-mission-common": "github:JGreenlee/e-mission-common#semver:0.6.2", "enketo-core": "^6.1.7", diff --git a/www/i18n/en.json b/www/i18n/en.json index b9318b16b..afd4e2600 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -296,7 +296,7 @@ "description": { "android-lt-9": "Location services should be enabled and set to High Accuracy. This allows us to accurately record the trajectory of the travel", "android-gte-9": "Location services should be enabled. This allows us to access location data and generate the trip log", - "ios": "Location services should be enabled. This allows us to access location data and generate the trip log" + "ios": "Turn on location services to enable location tracking" } }, "locperms": { @@ -307,27 +307,28 @@ "android-10": "Please select 'Allow all the time'", "android-11": "On the app settings page, choose the 'Location' permission and set it to 'Allow all the time'", "android-gte-12": "On the app settings page, choose the 'Location' permission and set it to 'Allow all the time' and 'Precise'", - "ios-lt-13": "Please select 'Always allow'", - "ios-gte-13": "On the app settings page, please select 'Always' and 'Precise' and return here to continue" + "ios-lt-13": "Select 'Always Allow' to enable location tracking", + "ios-13-13.3": "Select 'Always' location permissions on the next screen to enable location tracking, then return here to continue", + "ios-gte-13.4": "Select 'Precise' and 'Allow While Using App' on the next screen, then 'Change to Always Allow' to enable location tracking" } }, "overall-fitness-name-android": "Physical activity", "overall-fitness-name-ios": "Motion and Fitness", - "overall-fitness-description": "The fitness sensors distinguish between walking, bicycling and motorized modes. We use this data in order to separate the parts of multi-modal travel such as transit. We also use it to as a cross-check potentially spurious trips - if the location sensor jumps across town but the fitness sensor is stationary, we can guess that the trip was invalid.", + "overall-fitness-description": "The fitness sensors distinguish between walking, bicycling and motorized modes. We use this data in order to separate the parts of multi-modal travel such as transit. We also use it to as a cross-check potentially spurious trips - if the location sensor jumps across town but the fitness sensor is stationary, we can guess that the trip was invalid.", "fitnessperms": { "name": "Fitness Permission", "description": { - "android": "Please allow.", - "ios": "Please allow." + "android": "Allow motion and fitness tracking to enable mode detection", + "ios": "Allow motion and fitness tracking to enable mode detection" } }, "overall-notification-name": "Notifications", - "overall-notification-description": "We need to use notifications to inform you if the settings are incorrect. We also use hourly invisible push notifications to wake up the app and allow it to upload data and check app status. We also use notifications to remind you to label your trips.", + "overall-notification-description": "We use notifications to alert you if settings are incorrect and to remind you to label your trips.", "notificationperms": { "app-enabled-name": "App Notifications", "description": { - "android-enable": "On the app settings page, ensure that all notifications and channels are enabled.", - "ios-enable": "Please allow, on the popup or the app settings page if necessary" + "android-enable": "On the app settings page, enable notifications for reminders and app status alerts.", + "ios-enable": "Allow notifications for reminders and app status alerts" } }, "overall-background-restrictions-name": "Background restrictions", diff --git a/www/js/appTheme.ts b/www/js/appTheme.ts index 430848907..a3268c510 100644 --- a/www/js/appTheme.ts +++ b/www/js/appTheme.ts @@ -31,7 +31,7 @@ const AppTheme = { level5: '#e1e9f1', // lch(92% 5 250) }, success: '#00a665', // lch(60% 55 155) - warn: '#f8cf53', //lch(85% 65 85) + warn: '#ebac3e', // lch(75% 65 75) danger: '#f23934', // lch(55% 85 35), silver: '#d9d9d9', skyblue: '#7fcaea', diff --git a/www/js/appstatus/PermissionItem.tsx b/www/js/appstatus/PermissionItem.tsx index cd111f3b3..0bca015b0 100644 --- a/www/js/appstatus/PermissionItem.tsx +++ b/www/js/appstatus/PermissionItem.tsx @@ -1,17 +1,28 @@ import React from 'react'; -import { List, Button } from 'react-native-paper'; +import { List } from 'react-native-paper'; import { useTranslation } from 'react-i18next'; +import { colors } from '../appTheme'; + +function iconAndColorForCheck(check) { + if (check.status) return ['check-circle', colors.success]; + if (!check.isOptional) return ['alert-circle', colors.error]; + return [check.wasRequested ? 'minus-circle-off' : 'minus-circle-off-outline', colors.warn]; +} const PermissionItem = ({ check }) => { const { t } = useTranslation(); + const [icon, color] = iconAndColorForCheck(check); return ( check.fix()} title={t(check.name)} - description={t(check.desc)} + description={!check.status ? t(check.desc) : null} descriptionNumberOfLines={5} - left={() => } - right={() => } + left={() => } + right={() => } + style={{ paddingHorizontal: 0 }} + contentStyle={{ paddingHorizontal: 16 }} /> ); }; diff --git a/www/js/appstatus/PermissionsControls.tsx b/www/js/appstatus/PermissionsControls.tsx index 0eb9fdd60..9e3a9a8fc 100644 --- a/www/js/appstatus/PermissionsControls.tsx +++ b/www/js/appstatus/PermissionsControls.tsx @@ -13,14 +13,7 @@ const PermissionsControls = ({ onAccept }) => { const { t } = useTranslation(); const [explainVis, setExplainVis] = useState(false); const { permissionStatus } = useContext(AppContext); - const { checkList, overallStatus, error, explanationList } = permissionStatus; - - useEffect(() => { - if (!error) return; - AlertManager.addMessage({ - text: error, - }); - }, [error]); + const { checkList, overallStatus, explanationList } = permissionStatus; return ( <> diff --git a/www/js/usePermissionStatus.ts b/www/js/usePermissionStatus.ts index f8fef085b..636d9c5e9 100644 --- a/www/js/usePermissionStatus.ts +++ b/www/js/usePermissionStatus.ts @@ -4,8 +4,12 @@ import useAppConfig from './useAppConfig'; import { useTranslation } from 'react-i18next'; import { useAppTheme } from './appTheme'; import { logDebug, logWarn } from './plugin/logger'; +import { AlertManager } from './components/AlertBar'; -//refreshing checks with the plugins to update the check's statusState +let DEVICE_PLATFORM: 'android' | 'ios'; +let DEVICE_VERSION: number; + +//refreshing checks with the plugins to update the check's status export function refreshAllChecks(checkList) { //refresh each check checkList.forEach((lc) => { @@ -19,149 +23,79 @@ type Check = { desc: string; fix: () => Promise; refresh: () => Promise; - statusState?: boolean; - statusIcon?: string; - statusColor?: string; + status?: boolean; + isOptional?: boolean; + wasRequested?: boolean; }; const usePermissionStatus = () => { const { t } = useTranslation(); - const { colors } = useAppTheme(); const appConfig = useAppConfig(); - const [error, setError] = useState(''); - const [errorVis, setErrorVis] = useState(false); - 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; if (!checkList?.length) return undefined; // if checks not loaded yet, status is undetermined - checkList.forEach((lc) => { - logDebug('check in permission status for ' + lc.name + ':' + lc.statusState); - if (lc.statusState === false) { - status = false; - } - }); - return status; + return checkList.every((check) => check.status || (check.isOptional && check.wasRequested)); }, [checkList]); //using this function to update checks rather than mutate //this cues React to update UI - function updateCheck(newObject) { - const tempList = [...checkList]; //make a copy rather than mutate - //update the visiblility pieces here, rather than mutating - newObject.statusIcon = iconMap(newObject.statusState); - newObject.statusColor = colorMap(newObject.statusState); + function updateCheck(newCheck) { + const tempList = [...checkList]; //"find and replace" the check - tempList.forEach((item, i) => { - if (item.name == newObject.name) { - tempList[i] = newObject; - } - }); + const i = tempList.findIndex((item) => item.name == newCheck.name); + tempList[i] = newCheck; setCheckList(tempList); } async function checkOrFix(checkObj, nativeFn, showError = true) { logDebug('checking object ' + checkObj.name + ' ' + JSON.stringify(checkObj)); let newCheck = checkObj; - return nativeFn() - .then((status) => { - logDebug('availability = ' + status); - newCheck.statusState = true; - updateCheck(newCheck); - logDebug(`after checking obj ${checkObj.name}, checkList is ${JSON.stringify(checkList)}`); - return status; - }) - .catch((error) => { - if (showError) { - setError(error); - setErrorVis(true); - } - newCheck.statusState = false; - updateCheck(newCheck); - logDebug(`after checking obj ${checkObj.name}, checkList is ${JSON.stringify(checkList)}`); - return error; - }); + try { + const status = await nativeFn(); + logDebug(`${checkObj.name} status = ${status}`); + newCheck.status = true; + updateCheck(newCheck); + return status; + } catch (error) { + if (showError) { + AlertManager.addMessage({ text: error }); + } + newCheck.status = false; + updateCheck(newCheck); + return error; + } } - function setupAndroidLocChecks() { - let fixSettings = () => { - logDebug('Fix and refresh location settings'); - return checkOrFix( - locSettingsCheck, - window['cordova'].plugins.BEMDataCollection.fixLocationSettings, - true, - ); - }; - let checkSettings = () => { - logDebug('Refresh location settings'); - return checkOrFix( - locSettingsCheck, - window['cordova'].plugins.BEMDataCollection.isValidLocationSettings, - false, - ); - }; - let fixPerms = () => { - logDebug('fix and refresh location permissions'); - return checkOrFix( - locPermissionsCheck, - window['cordova'].plugins.BEMDataCollection.fixLocationPermissions, - true, - ).then((error) => { - if (error) { - locPermissionsCheck.desc = error; - } - }); - }; - let checkPerms = () => { - logDebug('fix and refresh location permissions'); - return checkOrFix( - locPermissionsCheck, - window['cordova'].plugins.BEMDataCollection.isValidLocationPermissions, - false, - ); - }; - const androidVersion = window['device'].version.split('.')[0]; - const androidSettingsDescTag = - androidVersion < 9 + function getLocSettingsDescriptionTag() { + if (DEVICE_PLATFORM == 'android') { + return DEVICE_VERSION < 9 ? 'intro.appstatus.locsettings.description.android-lt-9' : 'intro.appstatus.locsettings.description.android-gte-9'; - const androidPermDescTag = - androidVersion < 6 - ? 'intro.appstatus.locperms.description.android-lt-6' - : androidVersion < 10 - ? 'intro.appstatus.locperms.description.android-6-9' - : androidVersion < 11 - ? 'intro.appstatus.locperms.description.android-10' - : androidVersion < 12 - ? 'intro.appstatus.locperms.description.android-11' - : 'intro.appstatus.locperms.description.android-gte-12'; - logDebug('description tags are ' + androidSettingsDescTag + ' ' + androidPermDescTag); - // location settings - let locSettingsCheck = { - name: t('intro.appstatus.locsettings.name'), - desc: t(androidSettingsDescTag), - fix: fixSettings, - refresh: checkSettings, - }; - let locPermissionsCheck = { - name: t('intro.appstatus.locperms.name'), - desc: t(androidPermDescTag), - fix: fixPerms, - refresh: checkPerms, - }; - let tempChecks = checkList; - tempChecks.push(locSettingsCheck, locPermissionsCheck); - setCheckList(tempChecks); + } else if (DEVICE_PLATFORM == 'ios') { + return 'intro.appstatus.locsettings.description.ios'; + } + throw new Error(`Unknown platform ${DEVICE_PLATFORM} – unable to proceed`); + } + + function getLocPermissionsDescriptionTag() { + if (DEVICE_PLATFORM == 'android') { + if (DEVICE_VERSION < 6) return 'intro.appstatus.locperms.description.android-lt-6'; + if (DEVICE_VERSION < 10) return 'intro.appstatus.locperms.description.android-6-9'; + if (DEVICE_VERSION < 11) return 'intro.appstatus.locperms.description.android-10'; + if (DEVICE_VERSION < 12) return 'intro.appstatus.locperms.description.android-11'; + return 'intro.appstatus.locperms.description.android-gte-12'; + } else if (DEVICE_PLATFORM == 'ios') { + if (DEVICE_VERSION < 13) return 'intro.appstatus.locperms.description.ios-lt-13'; + if (DEVICE_VERSION < 13.4) return 'intro.appstatus.locperms.description.ios-13-13.3'; + return 'intro.appstatus.locperms.description.ios-gte-13.4'; + } + throw new Error(`Unknown platform ${DEVICE_PLATFORM} – unable to proceed`); } - function setupIOSLocChecks() { + function setupLocChecks() { let fixSettings = () => { logDebug('Fix and refresh location settings'); return checkOrFix( @@ -191,30 +125,24 @@ const usePermissionStatus = () => { }); }; let checkPerms = () => { - logDebug('fix and refresh location permissions'); + logDebug('refresh location permissions'); return checkOrFix( locPermissionsCheck, window['cordova'].plugins.BEMDataCollection.isValidLocationPermissions, false, ); }; - const iOSVersion = window['device'].version.split('.')[0]; - const iOSSettingsDescTag = 'intro.appstatus.locsettings.description.ios'; - const iOSPermDescTag = - iOSVersion < 13 - ? 'intro.appstatus.locperms.description.ios-lt-13' - : 'intro.appstatus.locperms.description.ios-gte-13'; - logDebug('description tags are ' + iOSSettingsDescTag + ' ' + iOSPermDescTag); - const locSettingsCheck = { + // location settings + let locSettingsCheck = { name: t('intro.appstatus.locsettings.name'), - desc: t(iOSSettingsDescTag), + desc: t(getLocSettingsDescriptionTag()), fix: fixSettings, refresh: checkSettings, }; - const locPermissionsCheck = { + let locPermissionsCheck = { name: t('intro.appstatus.locperms.name'), - desc: t(iOSPermDescTag), + desc: t(getLocPermissionsDescriptionTag()), fix: fixPerms, refresh: checkPerms, }; @@ -223,42 +151,12 @@ const usePermissionStatus = () => { setCheckList(tempChecks); } - function setupAndroidFitnessChecks() { - if (window['device'].version.split('.')[0] >= 10) { - let fixPerms = () => { - logDebug('fix and refresh fitness permissions'); - return checkOrFix( - fitnessPermissionsCheck, - window['cordova'].plugins.BEMDataCollection.fixFitnessPermissions, - true, - ).then((error) => { - if (error) { - fitnessPermissionsCheck.desc = error; - } - }); - }; - let checkPerms = () => { - logDebug('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 setupFitnessChecks() { + if (DEVICE_PLATFORM == 'android' && DEVICE_VERSION < 10) { + logDebug('Android version < 10, skipping fitness checks'); + return; } - } - function setupIOSFitnessChecks() { let fixPerms = () => { logDebug('fix and refresh fitness permissions'); return checkOrFix( @@ -272,7 +170,7 @@ const usePermissionStatus = () => { }); }; let checkPerms = () => { - logDebug('fix and refresh fitness permissions'); + logDebug('refresh fitness permissions'); return checkOrFix( fitnessPermissionsCheck, window['cordova'].plugins.BEMDataCollection.isValidFitnessPermissions, @@ -282,7 +180,7 @@ const usePermissionStatus = () => { let fitnessPermissionsCheck = { name: t('intro.appstatus.fitnessperms.name'), - desc: t('intro.appstatus.fitnessperms.description.ios'), + desc: t(`intro.appstatus.fitnessperms.description.${DEVICE_PLATFORM}`), fix: fixPerms, refresh: checkPerms, }; @@ -292,7 +190,7 @@ const usePermissionStatus = () => { } function setupAndroidBluetoothChecks() { - if (window['device'].version.split('.')[0] >= 10) { + if (DEVICE_VERSION >= 10) { let fixPerms = () => { logDebug('fix and refresh bluetooth permissions'); return checkOrFix( @@ -306,7 +204,7 @@ const usePermissionStatus = () => { }); }; let checkPerms = () => { - logDebug('fix and refresh bluetooth permissions'); + logDebug('refresh bluetooth permissions'); return checkOrFix( bluetoothPermissionsCheck, window['cordova'].plugins.BEMDataCollection.isValidBluetoothPermissions, @@ -326,17 +224,22 @@ const usePermissionStatus = () => { } } - function setupAndroidNotificationChecks() { + function setupNotificationChecks() { let fixPerms = () => { logDebug('fix and refresh notification permissions'); + appAndChannelNotificationsCheck.wasRequested = true; return checkOrFix( appAndChannelNotificationsCheck, window['cordova'].plugins.BEMDataCollection.fixShowNotifications, - true, - ); + false, + ).then((error) => { + if (error) { + appAndChannelNotificationsCheck.desc = error; + } + }); }; let checkPerms = () => { - logDebug('fix and refresh notification permissions'); + logDebug('refresh notification permissions'); return checkOrFix( appAndChannelNotificationsCheck, window['cordova'].plugins.BEMDataCollection.isValidShowNotifications, @@ -345,9 +248,11 @@ const usePermissionStatus = () => { }; let appAndChannelNotificationsCheck = { name: t('intro.appstatus.notificationperms.app-enabled-name'), - desc: t('intro.appstatus.notificationperms.description.android-enable'), + desc: t(`intro.appstatus.notificationperms.description.${DEVICE_PLATFORM}-enable`), fix: fixPerms, refresh: checkPerms, + isOptional: true, + wasRequested: false, }; let tempChecks = checkList; tempChecks.push(appAndChannelNotificationsCheck); @@ -364,7 +269,7 @@ const usePermissionStatus = () => { ); }; let checkPerms = () => { - logDebug('fix and refresh backgroundRestriction permissions'); + logDebug('refresh backgroundRestriction permissions'); return checkOrFix( unusedAppsUnrestrictedCheck, window['cordova'].plugins.BEMDataCollection.isUnusedAppUnrestricted, @@ -380,18 +285,17 @@ const usePermissionStatus = () => { ); }; let checkBatteryOpt = () => { - logDebug('fix and refresh battery optimization permissions'); + logDebug('refresh battery optimization permissions'); return checkOrFix( ignoreBatteryOptCheck, window['cordova'].plugins.BEMDataCollection.isIgnoreBatteryOptimizations, false, ); }; - const androidVersion = window['device'].version.split('.')[0]; const androidUnusedDescTag = - androidVersion == 12 + DEVICE_VERSION == 12 ? 'intro.appstatus.unusedapprestrict.description.android-disable-12' - : androidVersion < 12 + : DEVICE_VERSION < 12 ? 'intro.appstatus.unusedapprestrict.description.android-disable-lt-12' : 'intro.appstatus.unusedapprestrict.description.android-disable-gte-13'; let unusedAppsUnrestrictedCheck = { @@ -416,7 +320,7 @@ const usePermissionStatus = () => { let overallFitnessName = t('intro.appstatus.overall-fitness-name-android'); let locExplanation = t('intro.appstatus.overall-loc-description'); - if (window['device'].platform.toLowerCase() == 'ios') { + if (DEVICE_PLATFORM == 'ios') { overallFitnessName = t('intro.appstatus.overall-fitness-name-ios'); } tempExplanations.push({ name: t('intro.appstatus.overall-loc-name'), desc: locExplanation }); @@ -441,24 +345,15 @@ const usePermissionStatus = () => { } function createChecklist() { - if (window['device'].platform.toLowerCase() == 'android') { - setupAndroidLocChecks(); - setupAndroidFitnessChecks(); + setupLocChecks(); + setupFitnessChecks(); + if (DEVICE_PLATFORM == 'android') { if (appConfig.tracking?.bluetooth_only) { setupAndroidBluetoothChecks(); } - setupAndroidNotificationChecks(); setupAndroidBackgroundRestrictionChecks(); - } else if (window['device'].platform.toLowerCase() == 'ios') { - setupIOSLocChecks(); - setupIOSFitnessChecks(); - setupAndroidNotificationChecks(); - } else { - setError('Alert! unknownplatform, no tracking'); - setErrorVis(true); - logWarn('Alert! unknownplatform, no tracking'); //need an alert, can use AlertBar? } - + setupNotificationChecks(); refreshAllChecks(checkList); } @@ -470,14 +365,15 @@ const usePermissionStatus = () => { //load when ready useEffect(() => { if (appConfig && window['device']?.platform) { + DEVICE_PLATFORM = window['device'].platform.toLowerCase(); + DEVICE_VERSION = window['device'].version.split('.')[0]; setupPermissionText(); - setHaveSetText(true); logDebug('setting up permissions'); createChecklist(); } }, [appConfig]); - return { checkList, overallStatus, error, errorVis, setErrorVis, explanationList }; + return { checkList, overallStatus, explanationList }; }; export default usePermissionStatus;