Skip to content

Commit

Permalink
FXVPN-10: onboarding flow (#142)
Browse files Browse the repository at this point in the history
These should match [onboarding mocks from
figma](https://www.figma.com/design/s13B7zs27cadZXUyvxobgW/Mozilla-VPN-Extension?node-id=1848-41802&node-type=frame&t=k3T4A0zHBsqtwmbk-0).

Associated PR is up for the string:
mozilla-l10n/mozilla-vpn-extension-l10n#3

Updated video:
https://github.com/user-attachments/assets/317e48e7-9982-4489-ad19-8651dd5f21a6

The scroll bars in the video on page 2 are due to my browser settings,
and won't be seen by most people.
  • Loading branch information
mcleinman authored Dec 16, 2024
1 parent 4af12cf commit 1ffa427
Show file tree
Hide file tree
Showing 11 changed files with 486 additions and 28 deletions.
55 changes: 55 additions & 0 deletions src/assets/img/onboarding-1.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 79 additions & 0 deletions src/assets/img/onboarding-2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
164 changes: 164 additions & 0 deletions src/assets/img/onboarding-3.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/background/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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(
Expand Down Expand Up @@ -67,6 +69,7 @@ class Main {
expose(this.extController);
expose(this.proxyHandler);
expose(this.conflictObserver);
expose(this.onboardingController);
expose(this.butterBarService);
expose(this.telemetry);

Expand Down
60 changes: 60 additions & 0 deletions src/background/onboarding.js
Original file line number Diff line number Diff line change
@@ -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);
}
19 changes: 12 additions & 7 deletions src/background/vpncontroller/vpncontroller.js
Original file line number Diff line number Diff line change
Expand Up @@ -318,28 +318,33 @@ const MOZILLA_VPN_SERVERS_KEY = "mozillaVpnServers";
* @param {T} defaultValue - The Default value, in case it does not exist.
* @returns {Promise<T>} - 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
* @param {any} data - The state to replicate
* @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 });
Expand Down
51 changes: 48 additions & 3 deletions src/components/message-screen.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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 {
Expand All @@ -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();
Expand All @@ -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`
<vpn-titlebar title=${this.titleHeader}></vpn-titlebar>
<div class="inner">
Expand All @@ -51,6 +64,14 @@ export class MessageScreen extends LitElement {
<h1>${this.heading}</h1>
<slot></slot>
</div>
<div class="pagination">
${repeat(
paginationIndicators,
(item) => item.id,
(item) =>
html` <span class="holder"><span class="${item}"></span></span>`
)}
</div>
<div class="lower">
${when(
this.primaryAction,
Expand All @@ -59,7 +80,6 @@ export class MessageScreen extends LitElement {
class="primarybtn"
@click=${(e) => {
this.onPrimaryAction(this, e);
window.close();
}}
>
${this.primaryAction}
Expand All @@ -73,7 +93,6 @@ export class MessageScreen extends LitElement {
class="secondarybtn"
@click=${(e) => {
this.onSecondaryAction(this, e);
window.close();
}}
>
${this.secondaryAction}
Expand Down Expand Up @@ -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;
Expand Down
48 changes: 42 additions & 6 deletions src/components/prefab-screens.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -14,15 +16,24 @@ 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,
heading,
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`<p>${bodyText}</p>` : bodyText;
Expand All @@ -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);
}
}
Expand All @@ -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"));
}
);

Expand All @@ -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/"));
}
);

Expand All @@ -92,7 +105,7 @@ defineMessageScreen(
`,
tr("btnDownloadNow"),
() => {
open("https://www.mozilla.org/products/vpn/download/");
() => closeAfter (()=>open("https://www.mozilla.org/products/vpn/download/"));
}
);

Expand All @@ -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` <p>${tr(`onboarding${i}_body`)}</p> `,
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",
Expand Down
3 changes: 3 additions & 0 deletions src/ui/browserAction/backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");

/**
Expand Down
5 changes: 4 additions & 1 deletion src/ui/browserAction/popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@
<open-mozilla-vpn-message-screen
slot="MessageOpenMozillaVPN"
></open-mozilla-vpn-message-screen>
<!-- TODO: Split Tunnel Message, Onboarding, Update Message -->
<onboarding-screen-1 slot="onboarding-1"></onboarding-screen-1>
<onboarding-screen-2 slot="onboarding-2"></onboarding-screen-2>
<onboarding-screen-3 slot="onboarding-3"></onboarding-screen-3>
<!-- TODO: Split Tunnel Message, Update Message -->
<popup-browseraction slot="default"> </popup-browseraction>
</popup-condview>
</body>
Expand Down
Loading

0 comments on commit 1ffa427

Please sign in to comment.