/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
import React, { useState, useEffect, useRef } from "react";
import { Localized } from "./MSLocalized";
import { AboutWelcomeUtils } from "../../lib/aboutwelcome-utils";
import { MultiStageProtonScreen } from "./MultiStageProtonScreen";
import { useLanguageSwitcher } from "./LanguageSwitcher";
import {
BASE_PARAMS,
addUtmParams,
} from "../../asrouter/templates/FirstRun/addUtmParams";
// Amount of milliseconds for all transitions to complete (including delays).
const TRANSITION_OUT_TIME = 1000;
const LANGUAGE_MISMATCH_SCREEN_ID = "AW_LANGUAGE_MISMATCH";
export const MultiStageAboutWelcome = props => {
let { defaultScreens } = props;
const didFilter = useRef(false);
const [didMount, setDidMount] = useState(false);
const [screens, setScreens] = useState(defaultScreens);
const [index, setScreenIndex] = useState(props.startScreen);
const [previousOrder, setPreviousOrder] = useState(props.startScreen - 1);
useEffect(() => {
(async () => {
// If we want to load index from history state, we don't want to send impression yet
if (!didMount) {
return;
}
// On about:welcome first load, screensVisited should be empty
let screensVisited = didFilter.current ? screens.slice(0, index) : [];
let upcomingScreens = defaultScreens
.filter(s => !screensVisited.find(v => v.id === s.id))
// Filter out Language Mismatch screen from upcoming
// screens if screens set from useLanguageSwitcher hook
// has filtered language screen
.filter(
upcomingScreen =>
!(
!screens.find(s => s.id === LANGUAGE_MISMATCH_SCREEN_ID) &&
upcomingScreen.id === LANGUAGE_MISMATCH_SCREEN_ID
)
);
let filteredScreens = screensVisited.concat(
(await window.AWEvaluateScreenTargeting(upcomingScreens)) ??
upcomingScreens
);
// Use existing screen for the filtered screen to carry over any modification
// e.g. if AW_LANGUAGE_MISMATCH exists, use it from existing screens
setScreens(
filteredScreens.map(
filtered => screens.find(s => s.id === filtered.id) ?? filtered
)
);
didFilter.current = true;
const screenInitials = filteredScreens
.map(({ id }) => id?.split("_")[1]?.[0])
.join("");
// Send impression ping when respective screen first renders
filteredScreens.forEach((screen, order) => {
if (index === order) {
AboutWelcomeUtils.sendImpressionTelemetry(
`${props.message_id}_${order}_${screen.id}_${screenInitials}`
);
window.AWAddScreenImpression?.(screen);
}
});
// Remember that a new screen has loaded for browser navigation
if (props.updateHistory && index > window.history.state) {
window.history.pushState(index, "");
}
// Remember the previous screen index so we can animate the transition
setPreviousOrder(index);
})();
}, [index, didMount]); // eslint-disable-line react-hooks/exhaustive-deps
const [flowParams, setFlowParams] = useState(null);
const { metricsFlowUri } = props;
useEffect(() => {
(async () => {
if (metricsFlowUri) {
setFlowParams(await AboutWelcomeUtils.fetchFlowParams(metricsFlowUri));
}
})();
}, [metricsFlowUri]);
// Allow "in" style to render to actually transition towards regular state,
// which also makes using browser back/forward navigation skip transitions.
const [transition, setTransition] = useState(props.transitions ? "in" : "");
useEffect(() => {
if (transition === "in") {
requestAnimationFrame(() =>
requestAnimationFrame(() => setTransition(""))
);
}
}, [transition]);
// Transition to next screen, opening about:home on last screen button CTA
const handleTransition = () => {
// Only handle transitioning out from a screen once.
if (transition === "out") {
return;
}
// Start transitioning things "out" immediately when moving forwards.
setTransition(props.transitions ? "out" : "");
// Actually move forwards after all transitions finish.
setTimeout(
() => {
if (index < screens.length - 1) {
setTransition(props.transitions ? "in" : "");
setScreenIndex(prevState => prevState + 1);
} else {
window.AWFinish();
}
},
props.transitions ? TRANSITION_OUT_TIME : 0
);
};
useEffect(() => {
// When about:welcome loads (on refresh or pressing back button
// from about:home), ensure history state usEffect runs before
// useEffect hook that send impression telemetry
setDidMount(true);
if (props.updateHistory) {
// Switch to the screen tracked in state (null for initial state)
// or last screen index if a user navigates by pressing back
// button from about:home
const handler = ({ state }) => {
if (transition === "out") {
return;
}
setTransition(props.transitions ? "out" : "");
setTimeout(
() => {
setTransition(props.transitions ? "in" : "");
setScreenIndex(Math.min(state, screens.length - 1));
},
props.transitions ? TRANSITION_OUT_TIME : 0
);
};
// Handle page load, e.g., going back to about:welcome from about:home
const { state } = window.history;
if (state) {
setScreenIndex(Math.min(state, screens.length - 1));
setPreviousOrder(Math.min(state, screens.length - 1));
}
// Watch for browser back/forward button navigation events
window.addEventListener("popstate", handler);
return () => window.removeEventListener("popstate", handler);
}
return false;
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// Save the active multi select state containing array of checkbox ids
// used in handleAction to update MULTI_ACTION data
const [activeMultiSelect, setActiveMultiSelect] = useState(null);
// Get the active theme so the rendering code can make it selected
// by default.
const [activeTheme, setActiveTheme] = useState(null);
const [initialTheme, setInitialTheme] = useState(null);
useEffect(() => {
(async () => {
let theme = await window.AWGetSelectedTheme();
setInitialTheme(theme);
setActiveTheme(theme);
})();
}, []);
const { negotiatedLanguage, langPackInstallPhase, languageFilteredScreens } =
useLanguageSwitcher(
props.appAndSystemLocaleInfo,
screens,
index,
setScreenIndex
);
useEffect(() => {
setScreens(languageFilteredScreens);
}, [languageFilteredScreens]);
return (