| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import {assert, assertNotReached} from 'chrome://resources/js/assert.js'; |
| import type {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js'; |
| |
| import {isBrowserSigninAllowed, isForceSigninEnabled, isSignInProfileCreationSupported} from './profile_picker_flags.js'; |
| |
| /** |
| * ProfilePickerPages enum. |
| * These values are persisted to logs and should not be renumbered or |
| * re-used. |
| * See tools/metrics/histograms/enums.xml. |
| */ |
| enum Pages { |
| MAIN_VIEW = 0, |
| PROFILE_TYPE_CHOICE = 1, |
| LOCAL_PROFILE_CUSTOMIZATION = 2, |
| LOAD_SIGNIN = 3, |
| LOAD_FORCE_SIGNIN = 4, |
| PROFILE_SWITCH = 5, |
| } |
| |
| /** |
| * Valid route pathnames. |
| */ |
| export enum Routes { |
| MAIN = 'main-view', |
| NEW_PROFILE = 'new-profile', |
| PROFILE_SWITCH = 'profile-switch', |
| } |
| |
| /** |
| * Valid profile creation flow steps. |
| */ |
| export enum ProfileCreationSteps { |
| PROFILE_TYPE_CHOICE = 'profileTypeChoice', |
| LOCAL_PROFILE_CUSTOMIZATION = 'localProfileCustomization', |
| LOAD_SIGNIN = 'loadSignIn', |
| LOAD_FORCE_SIGNIN = 'loadForceSignIn', |
| } |
| |
| function computeStep(route: Routes): string { |
| switch (route) { |
| case Routes.MAIN: |
| return 'mainView'; |
| case Routes.NEW_PROFILE: |
| if (isForceSigninEnabled()) { |
| return ProfileCreationSteps.LOAD_FORCE_SIGNIN; |
| } |
| // TODO(msalama): Adjust once sign in profile creation is supported. |
| if (!isSignInProfileCreationSupported() || !isBrowserSigninAllowed()) { |
| return ProfileCreationSteps.LOCAL_PROFILE_CUSTOMIZATION; |
| } |
| return ProfileCreationSteps.PROFILE_TYPE_CHOICE; |
| case Routes.PROFILE_SWITCH: |
| return 'profileSwitch'; |
| default: |
| assertNotReached(); |
| } |
| } |
| |
| // Sets up history state based on the url path, unless it's already set. |
| if (!history.state || !history.state.route || !history.state.step) { |
| const path = window.location.pathname.replace(/\/$/, ''); |
| switch (path) { |
| case `/${Routes.NEW_PROFILE}`: |
| assert(history.length === 1); |
| // Enable accessing the main page when navigating back. |
| history.replaceState( |
| {route: Routes.MAIN, step: computeStep(Routes.MAIN), isFirst: true}, |
| '', '/'); |
| history.pushState( |
| { |
| route: Routes.NEW_PROFILE, |
| step: computeStep(Routes.NEW_PROFILE), |
| isFirst: false, |
| }, |
| '', path); |
| break; |
| case `/${Routes.PROFILE_SWITCH}`: |
| history.replaceState( |
| { |
| route: Routes.PROFILE_SWITCH, |
| step: computeStep(Routes.PROFILE_SWITCH), |
| isFirst: true, |
| }, |
| ''); |
| break; |
| default: |
| history.replaceState( |
| {route: Routes.MAIN, step: computeStep(Routes.MAIN), isFirst: true}, |
| '', '/'); |
| } |
| recordPageVisited(history.state.step); |
| } |
| |
| export function recordPageVisited(step: string) { |
| let page = Pages.MAIN_VIEW; |
| switch (step) { |
| case 'mainView': |
| page = Pages.MAIN_VIEW; |
| break; |
| case ProfileCreationSteps.PROFILE_TYPE_CHOICE: |
| page = Pages.PROFILE_TYPE_CHOICE; |
| break; |
| case ProfileCreationSteps.LOCAL_PROFILE_CUSTOMIZATION: |
| page = Pages.LOCAL_PROFILE_CUSTOMIZATION; |
| break; |
| case ProfileCreationSteps.LOAD_SIGNIN: |
| page = Pages.LOAD_SIGNIN; |
| break; |
| case ProfileCreationSteps.LOAD_FORCE_SIGNIN: |
| page = Pages.LOAD_FORCE_SIGNIN; |
| break; |
| case 'profileSwitch': |
| page = Pages.PROFILE_SWITCH; |
| break; |
| default: |
| assertNotReached(); |
| } |
| chrome.metricsPrivate.recordEnumerationValue( |
| 'ProfilePicker.UiVisited', page, Object.keys(Pages).length); |
| } |
| |
| const routeObservers: Set<NavigationMixinInterface> = new Set(); |
| |
| // Notifies all the elements that extended NavigationBehavior. |
| function notifyObservers() { |
| const route = history.state.route; |
| const step = history.state.step; |
| recordPageVisited(step); |
| routeObservers.forEach(observer => observer.onRouteChange(route, step)); |
| } |
| |
| // Notifies all elements when browser history is popped. |
| window.addEventListener('popstate', notifyObservers); |
| |
| export function navigateTo(route: Routes) { |
| assert([ |
| Routes.MAIN, |
| Routes.NEW_PROFILE, |
| Routes.PROFILE_SWITCH, |
| ].includes(route)); |
| navigateToStep(route, computeStep(route)); |
| } |
| |
| /** |
| * Navigates to the previous route if it belongs to the profile picker. |
| */ |
| export function navigateToPreviousRoute() { |
| window.history.back(); |
| } |
| |
| /** |
| * Returns whether there's a previous route. This is true iff some navigation |
| * within the app already took place. |
| */ |
| export function hasPreviousRoute() { |
| return !history.state.isFirst; |
| } |
| |
| export function navigateToStep(route: Routes, step: string) { |
| history.pushState( |
| { |
| route: route, |
| step: step, |
| isFirst: false, |
| }, |
| '', route === Routes.MAIN ? '/' : `/${route}`); |
| notifyObservers(); |
| } |
| |
| type Constructor<T> = new (...args: any[]) => T; |
| |
| export const NavigationMixin = |
| <T extends Constructor<CrLitElement>>(superClass: T): T& |
| Constructor<NavigationMixinInterface> => { |
| class NavigationMixin extends superClass { |
| override connectedCallback() { |
| super.connectedCallback(); |
| |
| assert(!routeObservers.has(this)); |
| routeObservers.add(this); |
| |
| // history state was set when page loaded, so when the element first |
| // attaches, call the route-change handler to initialize first. |
| this.onRouteChange(history.state.route, history.state.step); |
| } |
| |
| override disconnectedCallback() { |
| super.disconnectedCallback(); |
| |
| assert(routeObservers.delete(this)); |
| } |
| |
| /** |
| * Elements can override onRouteChange to handle route changes. |
| */ |
| onRouteChange(_route: Routes, _step: string) {} |
| } |
| |
| return NavigationMixin; |
| }; |
| |
| export interface NavigationMixinInterface { |
| onRouteChange(route: Routes, step: string): void; |
| } |