diff --git a/src/assets/img/onboarding-1.svg b/src/assets/img/onboarding-1.svg new file mode 100644 index 00000000..1387beb5 --- /dev/null +++ b/src/assets/img/onboarding-1.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/img/onboarding-2.svg b/src/assets/img/onboarding-2.svg new file mode 100644 index 00000000..51eaf0fe --- /dev/null +++ b/src/assets/img/onboarding-2.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/img/onboarding-3.svg b/src/assets/img/onboarding-3.svg new file mode 100644 index 00000000..1356d279 --- /dev/null +++ b/src/assets/img/onboarding-3.svg @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/background/main.js b/src/background/main.js index 58dac872..7070cdac 100644 --- a/src/background/main.js +++ b/src/background/main.js @@ -10,6 +10,7 @@ import { ToolbarIconHandler } from "./toolbarIconHandler.js"; import { VPNController } from "./vpncontroller/index.js"; import { ExtensionController } from "./extensionController/index.js"; +import { OnboardingController } from "./onboarding.js"; import { expose } from "../shared/ipc.js"; import { TabReloader } from "./tabReloader.js"; @@ -27,6 +28,7 @@ class Main { conflictObserver = new ConflictObserver(); vpnController = new VPNController(this); extController = new ExtensionController(this, this.vpnController); + onboardingController = new OnboardingController(this); logger = new Logger(this); proxyHandler = new ProxyHandler(this, this.vpnController); requestHandler = new RequestHandler( @@ -67,6 +69,7 @@ class Main { expose(this.extController); expose(this.proxyHandler); expose(this.conflictObserver); + expose(this.onboardingController); expose(this.butterBarService); expose(this.telemetry); diff --git a/src/background/onboarding.js b/src/background/onboarding.js new file mode 100644 index 00000000..8fa0a96a --- /dev/null +++ b/src/background/onboarding.js @@ -0,0 +1,60 @@ +/* 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/. */ + +// @ts-check + +import { Component } from "./component.js"; +// import { VPNController, VPNState } from "../vpncontroller/index.js"; +import { property } from "../shared/property.js"; +import { PropertyType } from "./../shared/ipc.js"; +import { fromStorage, putIntoStorage } from "./vpncontroller/vpncontroller.js"; + +const ONBOARDING_KEY = "mozillaVpnOnboarding"; +const FIRST_PAGE = 1; +export const NUMBER_OF_ONBOARDING_PAGES = 3; +const FIRST_UNUSED_PAGE = NUMBER_OF_ONBOARDING_PAGES + 1; + +/** + * Handles onboarding. + * + */ +export class OnboardingController extends Component { + static properties = { + nextOnboardingPage: PropertyType.Function, + finishOnboarding: PropertyType.Function, + currentOnboardingPage: PropertyType.Bindable, + }; + + /** + * + * @param {*} receiver + */ + constructor(receiver) { + super(receiver); + this.#mCurrentOnboardingPage = property(FIRST_PAGE); + } + + async init() { + this.#mCurrentOnboardingPage.value = await fromStorage( + browser.storage.local, + ONBOARDING_KEY, + FIRST_PAGE + ); + } + + get currentOnboardingPage() { + return this.#mCurrentOnboardingPage.readOnly; + } + + nextOnboardingPage() { + this.#mCurrentOnboardingPage.set(this.#mCurrentOnboardingPage.value + 1); + } + + finishOnboarding() { + this.#mCurrentOnboardingPage.set(FIRST_UNUSED_PAGE); + putIntoStorage(FIRST_UNUSED_PAGE, browser.storage.local, ONBOARDING_KEY); + } + + #mCurrentOnboardingPage = property(FIRST_PAGE); +} diff --git a/src/background/vpncontroller/vpncontroller.js b/src/background/vpncontroller/vpncontroller.js index 9c024c57..ed8b31f6 100644 --- a/src/background/vpncontroller/vpncontroller.js +++ b/src/background/vpncontroller/vpncontroller.js @@ -318,17 +318,22 @@ const MOZILLA_VPN_SERVERS_KEY = "mozillaVpnServers"; * @param {T} defaultValue - The Default value, in case it does not exist. * @returns {Promise} - Returns a copy of the state, or the same in case of missing data. */ -async function fromStorage( +export async function fromStorage( storage = browser.storage.local, - key = MOZILLA_VPN_SERVERS_KEY, + key, defaultValue ) { - const { mozillaVpnServers } = await storage.get(key); - if (typeof mozillaVpnServers === "undefined") { + const storageRetrieval = await storage.get(key); + if (typeof storageRetrieval === "undefined") { + return defaultValue; + } + const returnValue = storageRetrieval[key]; + + if (typeof returnValue === "undefined") { return defaultValue; } // @ts-ignore - return mozillaVpnServers; + return returnValue; } /** data into storage, to make sure we can recreate it next time using @@ -336,10 +341,10 @@ async function fromStorage( * @param {browser.storage.StorageArea} storage - The storage area to look for * @param {String} key - The key to put the state in */ -function putIntoStorage( +export function putIntoStorage( data = {}, storage = browser.storage.local, - key = MOZILLA_VPN_SERVERS_KEY + key ) { // @ts-ignore storage.set({ [key]: data }); diff --git a/src/components/message-screen.js b/src/components/message-screen.js index 1a8c3780..9ded05d6 100644 --- a/src/components/message-screen.js +++ b/src/components/message-screen.js @@ -2,7 +2,7 @@ * 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 { html, LitElement, when, css } from "../vendor/lit-all.min.js"; +import { html, LitElement, when, repeat, css } from "../vendor/lit-all.min.js"; import { fontStyling } from "./styles.js"; import "./titlebar.js"; @@ -17,6 +17,8 @@ import "./titlebar.js"; * - onPrimaryAction -> A function to call when the primary button is clicked * - secondaryAction -> The 2ndary button text * - onSecondaryAction -> A function to call when the 2ndary action is clicked. + * - totalPages -> The number of pages to show in pagination + * - currentPage -> The active page for pagination */ export class MessageScreen extends LitElement { @@ -29,6 +31,8 @@ export class MessageScreen extends LitElement { secondaryAction: { type: String }, onSecondaryAction: { type: Function }, identifier: { type: String }, + totalPages: { type: Number }, + currentPage: { type: Number }, }; constructor() { super(); @@ -40,9 +44,18 @@ export class MessageScreen extends LitElement { this.onPrimaryAction = () => {}; this.onSecondaryAction = () => {}; this.identifier = ""; + this.totalPages = 0; + this.currentPage = 0; } render() { + let paginationIndicators = []; + for (let i = 0; i < this.totalPages; i++) { + paginationIndicators.push( + i + 1 === this.currentPage ? "circle active" : "circle" + ); + } + return html`
@@ -51,6 +64,14 @@ export class MessageScreen extends LitElement {

${this.heading}

+
${when( this.primaryAction, @@ -59,7 +80,6 @@ export class MessageScreen extends LitElement { class="primarybtn" @click=${(e) => { this.onPrimaryAction(this, e); - window.close(); }} > ${this.primaryAction} @@ -73,7 +93,6 @@ export class MessageScreen extends LitElement { class="secondarybtn" @click=${(e) => { this.onSecondaryAction(this, e); - window.close(); }} > ${this.secondaryAction} @@ -135,6 +154,32 @@ export class MessageScreen extends LitElement { inline-size: 111px; } + .pagination { + box-sizing: border-box; + position: relative; + width: 100%; + margin: 0px 0px 25px; + justify-content: center; + right: 4px; // This must be half the width of .circle to truly center it. + } + + .holder { + display: inline-block; + width: 14px; + } + + .circle { + position: absolute; + width: 8px; + height: 8px; + background: var(--grey30); + border-radius: 100%; + } + + .active { + background: var(--action-button-color); + } + h1 { font-family: var(--font-family-bold); margin-block: 16px; diff --git a/src/components/prefab-screens.js b/src/components/prefab-screens.js index e3fa88cb..10866617 100644 --- a/src/components/prefab-screens.js +++ b/src/components/prefab-screens.js @@ -5,6 +5,8 @@ import { html, render } from "../vendor/lit-all.min.js"; import { MessageScreen } from "./message-screen.js"; import { tr } from "../shared/i18n.js"; +import { onboardingController } from "../ui/browserAction/backend.js"; +import { NUMBER_OF_ONBOARDING_PAGES } from "../background/onboarding.js"; const open = (url) => { browser.tabs.create({ @@ -14,6 +16,13 @@ const open = (url) => { const sumoLink = "https://support.mozilla.org/products/firefox-private-network-vpn"; +const closeAfter = (f) => { + if(f){ + f(); + } + window.close(); +} + const defineMessageScreen = ( tag, img, @@ -21,8 +30,10 @@ const defineMessageScreen = ( bodyText, primaryAction, onPrimaryAction, - secondarAction = tr("getHelp"), - onSecondaryAction = () => open(sumoLink) + secondaryAction = tr("getHelp"), + onSecondaryAction = () => closeAfter (()=>open(sumoLink)), + totalPages = 0, + currentPage = 0 ) => { const body = typeof bodyText === "string" ? html`

${bodyText}

` : bodyText; @@ -34,10 +45,12 @@ const defineMessageScreen = ( this.img = img; this.heading = heading; this.primaryAction = primaryAction; + this.secondaryAction = secondaryAction; this.onPrimaryAction = onPrimaryAction; - this.secondaryAction = secondarAction; this.onSecondaryAction = onSecondaryAction; this.identifier = tag; + this.totalPages = totalPages; + this.currentPage = currentPage; render(body, this); } } @@ -60,7 +73,7 @@ defineMessageScreen( tr("bodySubscribeNow"), tr("btnSubscribeNow"), () => { - open("https://www.mozilla.org/products/vpn#pricing"); + () => closeAfter (()=>open("https://www.mozilla.org/products/vpn#pricing")); } ); @@ -71,7 +84,7 @@ defineMessageScreen( tr("bodyNeedsUpdate2"), tr("btnDownloadNow"), () => { - open("https://www.mozilla.org/products/vpn/download/"); + () => closeAfter (()=>open("https://www.mozilla.org/products/vpn/download/")); } ); @@ -92,7 +105,7 @@ defineMessageScreen( `, tr("btnDownloadNow"), () => { - open("https://www.mozilla.org/products/vpn/download/"); + () => closeAfter (()=>open("https://www.mozilla.org/products/vpn/download/")); } ); @@ -105,6 +118,29 @@ defineMessageScreen( null ); +// Need to start loop at 1 because of how the strings were added to l10n repo. +for (let i = 1; i <= NUMBER_OF_ONBOARDING_PAGES; i++) { + const isFinalScreen = i === NUMBER_OF_ONBOARDING_PAGES; + defineMessageScreen( + `onboarding-screen-${i}`, + `onboarding-${i}.svg`, + tr(`onboarding${i}_title`), + html`

${tr(`onboarding${i}_body`)}

`, + isFinalScreen ? tr("done") : tr("next"), + () => { + isFinalScreen + ? onboardingController.finishOnboarding() + : onboardingController.nextOnboardingPage(); + }, + isFinalScreen ? tr(" ") : tr("skip"), // For final screen need a space - when using something like `null` there is a large vertical gap + () => { + isFinalScreen ? null : onboardingController.finishOnboarding(); + }, + NUMBER_OF_ONBOARDING_PAGES, + i + ); +} + defineMessageScreen( "unsupported-os-message-screen", "message-os.svg", diff --git a/src/ui/browserAction/backend.js b/src/ui/browserAction/backend.js index 16fd14e3..384f8c0f 100644 --- a/src/ui/browserAction/backend.js +++ b/src/ui/browserAction/backend.js @@ -27,6 +27,9 @@ import { getExposedObject } from "../../shared/ipc.js"; export const vpnController = await getExposedObject("VPNController"); export const extController = await getExposedObject("ExtensionController"); export const proxyHandler = await getExposedObject("ProxyHandler"); +export const onboardingController = await getExposedObject( + "OnboardingController" +); export const butterBarService = await getExposedObject("ButterBarService"); /** diff --git a/src/ui/browserAction/popup.html b/src/ui/browserAction/popup.html index 1f7f5eb8..0cebbaec 100644 --- a/src/ui/browserAction/popup.html +++ b/src/ui/browserAction/popup.html @@ -35,7 +35,10 @@ - + + + + diff --git a/src/ui/browserAction/popupConditional.js b/src/ui/browserAction/popupConditional.js index 7c35b94d..3f185e8d 100644 --- a/src/ui/browserAction/popupConditional.js +++ b/src/ui/browserAction/popupConditional.js @@ -5,7 +5,8 @@ import { ConditionalView } from "../../components/conditional-view.js"; import { propertySum } from "../../shared/property.js"; import { Utils } from "../../shared/utils.js"; -import { vpnController, telemetry } from "./backend.js"; +import { vpnController, onboardingController, telemetry } from "./backend.js"; +import { NUMBER_OF_ONBOARDING_PAGES } from "../../background/onboarding.js"; export class PopUpConditionalView extends ConditionalView { constructor() { @@ -18,11 +19,12 @@ export class PopUpConditionalView extends ConditionalView { const supportedPlatform = Utils.isSupportedOs(deviceOs.os); propertySum( - (state, features) => { + (state, features, currentPage) => { this.slotName = PopUpConditionalView.toSlotname( state, features, - supportedPlatform + supportedPlatform, + currentPage ); if (this.slotName == !"default") { requestIdleCallback(() => { @@ -31,7 +33,8 @@ export class PopUpConditionalView extends ConditionalView { } }, vpnController.state, - vpnController.featureList + vpnController.featureList, + onboardingController.currentOnboardingPage ); // Messages may dispatch an event requesting to send a Command to the VPN @@ -53,9 +56,10 @@ export class PopUpConditionalView extends ConditionalView { * @param {State} state * @param {FeatureFlags} features * @param {Boolean} supportedPlatform + * @param {Number} currentOnboardingPage * @returns {String} */ - static toSlotname(state, features, supportedPlatform) { + static toSlotname(state, features, supportedPlatform, currentOnboardingPage) { if (!supportedPlatform && !features.webExtension) { return "MessageOSNotSupported"; } @@ -77,12 +81,13 @@ export class PopUpConditionalView extends ConditionalView { if (!state.subscribed) { return "MessageSubscription"; } - /** - * TODO: - * if( did not have onboarding){ - * return "onBoarding" - * } - */ + if ( + currentOnboardingPage >= 1 && + currentOnboardingPage <= NUMBER_OF_ONBOARDING_PAGES + ) { + return `onboarding-${currentOnboardingPage}`; + } + return "default"; } }