diff --git a/www/i18n/en.json b/www/i18n/en.json index ec432d89e..6be5a0dac 100644 --- a/www/i18n/en.json +++ b/www/i18n/en.json @@ -405,7 +405,8 @@ "dont-force-kill": "Do not force kill the app", "background-restrictions": "On Samsung and Huwaei phones, make sure that background restrictions are turned off", "close": "Close", - "proceeding-with-token": "Proceeding with OPcode: {{token}}" + "proceeding-with-token": "Proceeding with OPcode: {{token}}", + "already-logged-in": "You are already logged in with OPcode {{token}}. If you wish to use a different OPcode, please log out first." }, "config": { "unable-read-saved-config": "Unable to read saved config", diff --git a/www/js/App.tsx b/www/js/App.tsx index 328e7ab29..a311ce482 100644 --- a/www/js/App.tsx +++ b/www/js/App.tsx @@ -18,6 +18,9 @@ import AlertBar from './components/AlertBar'; import Main from './Main'; import { joinWithTokenOrUrl } from './config/dynamicConfig'; import { addStatReading } from './plugin/clientStats'; +import useAppState from './useAppState'; +import { displayErrorMsg, logDebug } from './plugin/logger'; +import i18next from 'i18next'; export const AppContext = createContext({}); const CUSTOM_LABEL_KEYS_IN_DATABASE = ['mode', 'purpose']; @@ -34,21 +37,33 @@ const App = () => { const appConfig = useAppConfig(); const permissionStatus = usePermissionStatus(); - const refreshOnboardingState = () => getPendingOnboardingState().then(setOnboardingState); + const refreshOnboardingState = () => + getPendingOnboardingState().then((state) => { + setOnboardingState(state); + return state; + }); + useEffect(() => { refreshOnboardingState(); }, []); - // handleOpenURL function must be provided globally for cordova-plugin-customurlscheme - // https://www.npmjs.com/package/cordova-plugin-customurlscheme - window['handleOpenURL'] = async (url: string, joinMethod: OnboardingJoinMethod = 'external') => { - const configUpdated = await joinWithTokenOrUrl(url); + async function handleTokenOrUrl(tokenOrUrl: string, joinMethod: OnboardingJoinMethod) { + const onboardingState = await refreshOnboardingState(); + logDebug(`handleTokenOrUrl: onboardingState = ${JSON.stringify(onboardingState)}`); + if (onboardingState.route > OnboardingRoute.WELCOME) { + displayErrorMsg(i18next.t('join.already-logged-in', { token: onboardingState.opcode })); + return; + } + const configUpdated = await joinWithTokenOrUrl(tokenOrUrl); addStatReading('onboard', { configUpdated, joinMethod }); if (configUpdated) { refreshOnboardingState(); } return configUpdated; - }; + } + // handleOpenURL function must be provided globally for cordova-plugin-customurlscheme + // https://www.npmjs.com/package/cordova-plugin-customurlscheme + window['handleOpenURL'] = (url: string) => handleTokenOrUrl(url, 'external'); useEffect(() => { if (!appConfig) return; @@ -61,9 +76,24 @@ const App = () => { // getUserCustomLabels(CUSTOM_LABEL_KEYS_IN_DATABASE).then((res) => setCustomLabelMap(res)); }, [appConfig]); + const appState = useAppState({}); + if (appState != 'active') { + // Render nothing if the app state is not 'active'. + // On iOS, the UI can run if the app is launched by the OS in response to a notification, + // in which case the appState will be 'background'. In this case, we definitely do not want + // to load the UI because it is not visible. + // On Android, the UI can only be initiated by the user - but even so, the user can send it to + // the background and we don't need the UI to stay active. + // In the future, we may want to persist some UI states when the app is sent to the background; + // i.e. the user opens the app, navigates away, and back again. + // But currently, we're relying on a 'fresh' UI every time the app goes to 'active' state. + logDebug(`App: appState = ${appState}; returning null`); + return null; + } + const appContextValue = { appConfig, - handleOpenURL: window['handleOpenURL'], + handleTokenOrUrl, onboardingState, setOnboardingState, refreshOnboardingState, diff --git a/www/js/TimelineContext.ts b/www/js/TimelineContext.ts index 63a43f1ce..fc463bbee 100644 --- a/www/js/TimelineContext.ts +++ b/www/js/TimelineContext.ts @@ -21,7 +21,6 @@ import { getPipelineRangeTs } from './services/commHelper'; import { getNotDeletedCandidates, mapInputsToTimelineEntries } from './survey/inputMatcher'; import { EnketoUserInputEntry } from './survey/enketo/enketoHelper'; import { primarySectionForTrip } from './diary/diaryHelper'; -import useAppStateChange from './useAppStateChange'; import { isoDateRangeToTsRange, isoDateWithOffset } from './datetimeUtil'; import { base_modes } from 'e-mission-common'; @@ -55,7 +54,6 @@ type ContextProps = { export const useTimelineContext = (): ContextProps => { const { t } = useTranslation(); const appConfig = useAppConfig(); - useAppStateChange(() => refreshTimeline()); const [labelOptions, setLabelOptions] = useState(null); // timestamp range that has been processed by the pipeline on the server diff --git a/www/js/onboarding/WelcomePage.tsx b/www/js/onboarding/WelcomePage.tsx index 2698df5d4..ff8214578 100644 --- a/www/js/onboarding/WelcomePage.tsx +++ b/www/js/onboarding/WelcomePage.tsx @@ -34,7 +34,7 @@ const WelcomePage = () => { const { t } = useTranslation(); const { colors } = useTheme(); const { width: windowWidth } = useWindowDimensions(); - const { handleOpenURL } = useContext(AppContext); + const { handleTokenOrUrl } = useContext(AppContext); const [pasteModalVis, setPasteModalVis] = useState(false); const [infoPopupVis, setInfoPopupVis] = useState(false); const [existingToken, setExistingToken] = useState(''); @@ -52,7 +52,7 @@ const WelcomePage = () => { AlertManager.addMessage({ text: 'No QR code found in scan. Please try again.' }); return; } - handleOpenURL(result.text, 'scan'); + handleTokenOrUrl(result.text, 'scan'); }, (error) => { barcodeScannerIsOpen = false; @@ -68,7 +68,7 @@ const WelcomePage = () => { if (!clipboardContent?.startsWith('nrelop_') && !clipboardContent?.includes('://')) { throw new Error('Clipboard content is not a valid token or URL'); } - handleOpenURL(clipboardContent, 'paste'); + handleTokenOrUrl(clipboardContent, 'paste'); } catch (e) { logWarn(`Tried using clipboard content ${clipboardContent}: ${e}`); setPasteModalVis(true); @@ -141,7 +141,7 @@ const WelcomePage = () => {