diff --git a/www/js/App.tsx b/www/js/App.tsx index 1ace61531..628baf21b 100644 --- a/www/js/App.tsx +++ b/www/js/App.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, createContext, useMemo } from 'react'; import { getAngularService } from './angular-react-helper'; -import { BottomNavigation, Button, useTheme } from 'react-native-paper'; +import { ActivityIndicator, BottomNavigation, useTheme } from 'react-native-paper'; import { useTranslation } from 'react-i18next'; import LabelTab from './diary/LabelTab'; import MetricsTab from './metrics/MetricsTab'; @@ -22,7 +22,8 @@ export const AppContext = createContext({}); const App = () => { const [index, setIndex] = useState(0); - const [pendingOnboardingState, setPendingOnboardingState] = useState(null); + // will remain null while the onboarding state is still being determined + const [onboardingState, setOnboardingState] = useState(null); const [permissionsPopupVis, setPermissionsPopupVis] = useState(false); const appConfig = useAppConfig(); const { colors } = useTheme(); @@ -41,7 +42,7 @@ const App = () => { control: ProfileSettings, }); - const refreshOnboardingState = () => getPendingOnboardingState().then(setPendingOnboardingState); + const refreshOnboardingState = () => getPendingOnboardingState().then(setOnboardingState); useEffect(() => { refreshOnboardingState() }, []); useEffect(() => { @@ -53,32 +54,43 @@ const App = () => { const appContextValue = { appConfig, - pendingOnboardingState, setPendingOnboardingState, refreshOnboardingState, + onboardingState, setOnboardingState, refreshOnboardingState, permissionsPopupVis, setPermissionsPopupVis, } - console.debug('pendingOnboardingState in App', pendingOnboardingState); + console.debug('onboardingState in App', onboardingState); + + let appContent; + if (onboardingState == null) { + // if onboarding state is not yet determined, show a loading spinner + appContent = + } else if (onboardingState?.route == OnboardingRoute.DONE) { + // if onboarding route is DONE, show the main app with navigation between tabs + appContent = ( + + ); + } else { + // if there is an onboarding route that is not DONE, show the onboarding stack + appContent = + } return (<> - {pendingOnboardingState == null ? - - : - - } - { /* if onboarding is done (state == null), or if is in progress but we are past the - consent page (route > CONSENT), the permissions popup can show if needed */ } - {(pendingOnboardingState == null || pendingOnboardingState.route > OnboardingRoute.CONSENT) && + {appContent} + + { /* If we are past the consent page (route > CONSENT), the permissions popup can show if needed. + This also includes if onboarding is DONE altogether (because "DONE" is > "CONSENT") */ } + {(onboardingState && onboardingState.route > OnboardingRoute.CONSENT) && } diff --git a/www/js/onboarding/OnboardingStack.tsx b/www/js/onboarding/OnboardingStack.tsx index 643744ed3..a49bde3ab 100644 --- a/www/js/onboarding/OnboardingStack.tsx +++ b/www/js/onboarding/OnboardingStack.tsx @@ -11,22 +11,22 @@ import { displayErrorMsg } from "../plugin/logger"; const OnboardingStack = () => { - const { pendingOnboardingState } = useContext(AppContext); + const { onboardingState } = useContext(AppContext); - console.debug('pendingOnboardingState in OnboardingStack', pendingOnboardingState); + console.debug('onboardingState in OnboardingStack', onboardingState); - if (pendingOnboardingState.route == OnboardingRoute.WELCOME) { + if (onboardingState.route == OnboardingRoute.WELCOME) { return ; - } else if (pendingOnboardingState.route == OnboardingRoute.SUMMARY) { + } else if (onboardingState.route == OnboardingRoute.SUMMARY) { return ; - } else if (pendingOnboardingState.route == OnboardingRoute.CONSENT) { + } else if (onboardingState.route == OnboardingRoute.CONSENT) { return ; - } else if (pendingOnboardingState.route == OnboardingRoute.SAVE_QR) { + } else if (onboardingState.route == OnboardingRoute.SAVE_QR) { return ; - } else if (pendingOnboardingState.route == OnboardingRoute.SURVEY) { + } else if (onboardingState.route == OnboardingRoute.SURVEY) { return ; } else { - displayErrorMsg('OnboardingStack: unknown route', pendingOnboardingState.route); + displayErrorMsg('OnboardingStack: unknown route', onboardingState.route); } } diff --git a/www/js/onboarding/SaveQrPage.tsx b/www/js/onboarding/SaveQrPage.tsx index 157ff4093..8a3fab92e 100644 --- a/www/js/onboarding/SaveQrPage.tsx +++ b/www/js/onboarding/SaveQrPage.tsx @@ -14,13 +14,13 @@ import { preloadDemoSurveyResponse } from "./SurveyPage"; const SaveQrPage = ({ }) => { const { t } = useTranslation(); - const { pendingOnboardingState, refreshOnboardingState } = useContext(AppContext); + const { onboardingState, refreshOnboardingState } = useContext(AppContext); const { overallStatus } = usePermissionStatus(); useEffect(() => { if (overallStatus == true && !registerUserDone) { logDebug('permissions done, going to log in'); - login(pendingOnboardingState.opcode).then((response) => { + login(onboardingState.opcode).then((response) => { logDebug('login done, refreshing onboarding state'); setRegisterUserDone(true); preloadDemoSurveyResponse(); @@ -63,13 +63,13 @@ const SaveQrPage = ({ }) => { - + - {pendingOnboardingState.opcode} + {onboardingState.opcode} - diff --git a/www/js/onboarding/onboardingHelper.ts b/www/js/onboarding/onboardingHelper.ts index 7874ab9f8..06b744af8 100644 --- a/www/js/onboarding/onboardingHelper.ts +++ b/www/js/onboarding/onboardingHelper.ts @@ -4,13 +4,13 @@ import { getConfig } from "../config/dynamicConfig"; export const INTRO_DONE_KEY = 'intro_done'; -// state = null if onboarding is done // route = WELCOME if no config present // route = SUMMARY if config present, but not consented and summary not done // route = CONSENT if config present, but not consented and summary done // route = SAVE_QR if config present, consented, but save qr not done // route = SURVEY if config present, consented and save qr done -export enum OnboardingRoute { WELCOME, SUMMARY, CONSENT, SAVE_QR, SURVEY, NONE }; +// route = DONE if onboarding is finished (intro_done marked) +export enum OnboardingRoute { WELCOME, SUMMARY, CONSENT, SAVE_QR, SURVEY, DONE }; export type OnboardingState = { opcode: string, route: OnboardingRoute, @@ -27,9 +27,10 @@ export const setRegisterUserDone = (b) => registerUserDone = b; export function getPendingOnboardingState(): Promise { return Promise.all([getConfig(), readConsented(), readIntroDone()]).then(([config, isConsented, isIntroDone]) => { - if (isIntroDone) return null; // onboarding is done; no pending state - let route: OnboardingRoute = OnboardingRoute.NONE; - if (!config) { + let route: OnboardingRoute; + if (isIntroDone) { + route = OnboardingRoute.DONE; + } else if (!config) { route = OnboardingRoute.WELCOME; } else if (!isConsented && !summaryDone) { route = OnboardingRoute.SUMMARY; diff --git a/www/js/survey/multilabel/confirmHelper.ts b/www/js/survey/multilabel/confirmHelper.ts index 6350745eb..3c6236996 100644 --- a/www/js/survey/multilabel/confirmHelper.ts +++ b/www/js/survey/multilabel/confirmHelper.ts @@ -37,29 +37,24 @@ export async function getLabelOptions(appConfigParam?) { if (appConfig.label_options) { const labelOptionsJson = await fetchUrlCached(appConfig.label_options); labelOptions = JSON.parse(labelOptionsJson) as LabelOptions; - /* fill in the translations to the 'text' fields of the labelOptions, - according to the current language */ - const lang = i18next.language; - for (const opt in labelOptions) { - labelOptions[opt]?.forEach?.((o, i) => { - const translationKey = o.value; - const translation = labelOptions.translations[lang][translationKey]; - labelOptions[opt][i].text = translation; - }); - } - } else { - // backwards compat: if dynamic config doesn't have label_options, use the old way - const i18nUtils = getAngularService("i18nUtils"); - const optionFileName = await i18nUtils.geti18nFileName("json/", "trip_confirm_options", ".json"); - try { - const optionJson = await fetch(optionFileName).then(r => r.json()); - labelOptions = optionJson as LabelOptions; - } catch (e) { - logDebug("error "+JSON.stringify(e)+" while reading confirm options, reverting to defaults"); - const optionJson = await fetch("json/trip_confirm_options.json.sample").then(r => r.json()); - labelOptions = optionJson as LabelOptions; - } + } else { // if dynamic config doesn't have label_options, use default label options + const defaultLabelOptionsURL = 'json/label-options.json.sample'; + logDebug("No label_options found in config, using default label options at " + defaultLabelOptionsURL); + const defaultLabelOptionsJson = await fetchUrlCached(defaultLabelOptionsURL); + labelOptions = JSON.parse(defaultLabelOptionsJson) as LabelOptions; + } + + /* fill in the translations to the 'text' fields of the labelOptions, + according to the current language */ + const lang = i18next.language; + for (const opt in labelOptions) { + labelOptions[opt]?.forEach?.((o, i) => { + const translationKey = o.value; + const translation = labelOptions.translations[lang][translationKey]; + labelOptions[opt][i].text = translation; + }); } + return labelOptions; } diff --git a/www/json/label-options.json.sample b/www/json/label-options.json.sample new file mode 100644 index 000000000..9d3447bda --- /dev/null +++ b/www/json/label-options.json.sample @@ -0,0 +1,124 @@ +{ + "MODE": [ + {"value":"walk", "baseMode":"WALKING", "met_equivalent":"WALKING", "kgCo2PerKm": 0}, + {"value":"e-bike", "baseMode":"E_BIKE", "met": {"ALL": {"range": [0, -1], "mets": 4.9}}, "kgCo2PerKm": 0.00728}, + {"value":"bike", "baseMode":"BICYCLING", "met_equivalent":"BICYCLING", "kgCo2PerKm": 0}, + {"value":"bikeshare", "baseMode":"BICYCLING", "met_equivalent":"BICYCLING", "kgCo2PerKm": 0}, + {"value":"scootershare", "baseMode":"E_SCOOTER", "met_equivalent":"IN_VEHICLE", "kgCo2PerKm": 0.00894}, + {"value":"drove_alone", "baseMode":"CAR", "met_equivalent":"IN_VEHICLE", "kgCo2PerKm": 0.22031}, + {"value":"shared_ride", "baseMode":"CAR", "met_equivalent":"IN_VEHICLE", "kgCo2PerKm": 0.11015}, + {"value":"e_car_drove_alone", "baseMode":"E_CAR", "met_equivalent":"IN_VEHICLE", "kgCo2PerKm": 0.08216}, + {"value":"e_car_shared_ride", "baseMode":"E_CAR", "met_equivalent":"IN_VEHICLE", "kgCo2PerKm": 0.04108}, + {"value":"moped", "baseMode":"MOPED", "met_equivalent":"IN_VEHICLE", "kgCo2PerKm": 0.05555}, + {"value":"taxi", "baseMode":"TAXI", "met_equivalent":"IN_VEHICLE", "kgCo2PerKm": 0.30741}, + {"value":"bus", "baseMode":"BUS", "met_equivalent":"IN_VEHICLE", "kgCo2PerKm": 0.20727}, + {"value":"train", "baseMode":"TRAIN", "met_equivalent":"IN_VEHICLE", "kgCo2PerKm": 0.12256}, + {"value":"free_shuttle", "baseMode":"BUS", "met_equivalent":"IN_VEHICLE", "kgCo2PerKm": 0.20727}, + {"value":"air", "baseMode":"AIR", "met_equivalent":"IN_VEHICLE", "kgCo2PerKm": 0.09975}, + {"value":"not_a_trip", "baseMode":"UNKNOWN", "met_equivalent":"UNKNOWN", "kgCo2PerKm": 0}, + {"value":"other", "baseMode":"OTHER", "met_equivalent":"UNKNOWN", "kgCo2PerKm": 0} + ], + "PURPOSE": [ + {"value":"home"}, + {"value":"work"}, + {"value":"at_work"}, + {"value":"school"}, + {"value":"transit_transfer"}, + {"value":"shopping"}, + {"value":"meal"}, + {"value":"pick_drop_person"}, + {"value":"pick_drop_item"}, + {"value":"personal_med"}, + {"value":"access_recreation"}, + {"value":"exercise"}, + {"value":"entertainment"}, + {"value":"religious"}, + {"value":"other"} + ], + "REPLACED_MODE": [ + {"value":"no_travel"}, + {"value":"walk"}, + {"value":"bike"}, + {"value":"bikeshare"}, + {"value":"scootershare"}, + {"value":"drove_alone"}, + {"value":"shared_ride"}, + {"value":"e_car_drove_alone"}, + {"value":"e_car_shared_ride"}, + {"value":"taxi"}, + {"value":"bus"}, + {"value":"train"}, + {"value":"free_shuttle"}, + {"value":"other"} + ], + "translations": { + "en": { + "walk": "Walk", + "e-bike": "E-bike", + "bike": "Regular Bike", + "bikeshare": "Bikeshare", + "scootershare": "Scooter share", + "drove_alone": "Gas Car Drove Alone", + "shared_ride": "Gas Car Shared Ride", + "e_car_drove_alone": "E-Car Drove Alone", + "e_car_shared_ride": "E-Car Shared Ride", + "moped": "Moped", + "taxi": "Taxi/Uber/Lyft", + "bus": "Bus", + "train": "Train", + "free_shuttle": "Free Shuttle", + "air": "Air", + "not_a_trip": "Not a trip", + "no_travel": "No travel", + "home": "Home", + "work": "To Work", + "at_work": "At Work", + "school": "School", + "transit_transfer": "Transit transfer", + "shopping": "Shopping", + "meal": "Meal", + "pick_drop_person": "Pick-up/ Drop off Person", + "pick_drop_item": "Pick-up/ Drop off Item", + "personal_med": "Personal/ Medical", + "access_recreation": "Access Recreation", + "exercise": "Recreation/ Exercise", + "entertainment": "Entertainment/ Social", + "religious": "Religious", + "other": "Other" + }, + "es": { + "walk": "Caminando", + "e-bike": "e-bicicleta", + "bike": "Bicicleta", + "bikeshare": "Bicicleta compartida", + "scootershare": "Motoneta compartida", + "drove_alone": "Coche de Gas, Condujo solo", + "shared_ride": "Coche de Gas, Condujo con otros", + "e_car_drove_alone": "e-coche, Condujo solo", + "e_car_shared_ride": "e-coche, Condujo con ontras", + "moped": "Ciclomotor", + "taxi": "Taxi/Uber/Lyft", + "bus": "Autobús", + "train": "Tren", + "free_shuttle": "Colectivo gratuito", + "air": "Avión", + "not_a_trip": "No es un viaje", + "no_travel": "No viajar", + "home": "Inicio", + "work": "Trabajo", + "at_work": "En el trabajo", + "school": "Escuela", + "transit_transfer": "Transbordo", + "shopping": "Compras", + "meal": "Comida", + "pick_drop_person": "Recoger/ Entregar Individuo", + "pick_drop_item": "Recoger/ Entregar Objeto", + "personal_med": "Personal/ Médico", + "access_recreation": "Acceder a Recreación", + "exercise": "Recreación/ Ejercicio", + "entertainment": "Entretenimiento/ Social", + "religious": "Religioso", + "other": "Otros" + } + } +} \ No newline at end of file diff --git a/www/json/trip_confirm_options.json.sample b/www/json/trip_confirm_options.json.sample deleted file mode 100644 index 1e90bc1bb..000000000 --- a/www/json/trip_confirm_options.json.sample +++ /dev/null @@ -1,52 +0,0 @@ -{ - "MODE" : [ - {"text":"Walk", "value":"walk", "baseMode":"WALKING", "met_equivalent": "WALKING", "kgCo2PerKm": 0}, - {"text":"E-bike","value":"e-bike", "baseMode": "E_BIKE", "met": { - "ALL": {"range": [0, -1], "mets": 4.9} - }, "kgCo2PerKm": 0.00728}, - {"text":"Regular Bike","value":"bike", "baseMode":"BICYCLING", "met_equivalent": "BICYCLING", "kgCo2PerKm": 0}, - {"text":"Bikeshare","value":"bikeshare", "baseMode":"BICYCLING", "met_equivalent": "BICYCLING", "kgCo2PerKm": 0}, - {"text":"Scooter share","value":"scootershare", "baseMode":"E_SCOOTER", "met_equivalent": "IN_VEHICLE", "kgCo2PerKm": 0.00894}, - {"text":"Gas Car Drove Alone","value":"drove_alone", "baseMode":"CAR", "met_equivalent": "IN_VEHICLE", "kgCo2PerKm": 0.22031}, - {"text":"Gas Car Shared Ride","value":"shared_ride", "baseMode":"CAR", "met_equivalent": "IN_VEHICLE", "kgCo2PerKm": 0.11015}, - {"text":"E-Car Drove Alone","value":"e_car_drove_alone", "baseMode":"E_CAR", "met_equivalent": "IN_VEHICLE", "kgCo2PerKm": 0.08216}, - {"text":"E-Car Shared Ride","value":"e_car_shared_ride", "baseMode":"E_CAR", "met_equivalent": "IN_VEHICLE", "kgCo2PerKm": 0.04108}, - {"text":"Taxi/Uber/Lyft","value":"taxi", "baseMode":"TAXI", "met_equivalent": "IN_VEHICLE", "kgCo2PerKm": 0.30741}, - {"text":"Bus","value":"bus", "baseMode":"BUS", "met_equivalent": "IN_VEHICLE", "kgCo2PerKm": 0.20727}, - {"text":"Train","value":"train", "baseMode":"TRAIN", "met_equivalent": "IN_VEHICLE", "kgCo2PerKm": 0.12256}, - {"text":"Free Shuttle","value":"free_shuttle", "baseMode":"BUS", "met_equivalent": "IN_VEHICLE", "kgCo2PerKm": 0.20727}, - {"text":"Air","value":"air", "baseMode":"AIR", "met_equivalent": "IN_VEHICLE", "kgCo2PerKm": 0.09975}, - {"text":"Not a Trip","value":"not_a_trip", "baseMode":"UNKNOWN", "met_equivalent": "UNKNOWN", "kgCo2PerKm": 0}, - {"text":"Other","value":"other", "baseMode":"OTHER", "met_equivalent": "UNKNOWN", "kgCo2PerKm": 0}], - "REPLACED_MODE" : [ - {"text":"No travel", "value":"no_travel"}, - {"text":"Walk", "value":"walk"}, - {"text":"Regular Bike","value":"bike"}, - {"text":"Bikeshare","value":"bikeshare"}, - {"text":"Scooter share","value":"scootershare"}, - {"text":"Gas Car, drove alone","value":"drove_alone"}, - {"text":"Gas Car, with others","value":"shared_ride"}, - {"text":"E-Car, drove alone","value":"e_car_drove_alone"}, - {"text":"E-Car, with others","value":"e_car_shared_ride"}, - {"text":"Taxi/Uber/Lyft","value":"taxi"}, - {"text":"Bus","value":"bus"}, - {"text":"Train","value":"train"}, - {"text":"Free Shuttle","value":"free_shuttle"}, - {"text":"Other","value":"other"}], - "PURPOSE" : [ - {"text":"Home", "value":"home"}, - {"text":"To Work","value":"work"}, - {"text":"At Work","value":"at_work"}, - {"text":"School","value":"school"}, - {"text":"Transit transfer", "value":"transit_transfer"}, - {"text":"Shopping","value":"shopping"}, - {"text":"Meal","value":"meal"}, - {"text":"Pick-up/ Drop off Person","value":"pick_drop_person"}, - {"text":"Pick-up/ Drop off Item","value":"pick_drop_item"}, - {"text":"Personal/ Medical","value":"personal_med"}, - {"text":"Access Recreation","value":"access_recreation"}, - {"text":"Recreation/ Exercise","value":"exercise"}, - {"text":"Entertainment/ Social","value":"entertainment"}, - {"text":"Religious", "value":"religious"}, - {"text":"Other","value":"other"}] -}