Skip to content
This repository has been archived by the owner on Feb 29, 2020. It is now read-only.

Commit

Permalink
Fix Bug 1432676 - Create AS Router UI Surface / template for Onboarding
Browse files Browse the repository at this point in the history
overlay
  • Loading branch information
sarracini committed May 17, 2018
1 parent e2b0917 commit 49d13ab
Show file tree
Hide file tree
Showing 15 changed files with 627 additions and 29 deletions.
13 changes: 13 additions & 0 deletions system-addon/common/Actions.jsm
Expand Up @@ -113,6 +113,17 @@ for (const type of [
actionTypes[type] = type;
}

// These are acceptable actions for AS Router messages to have. They can show up
// as call-to-action buttons in snippets, onboarding tour, etc.
const ASRouterActions = {};
for (const type of [
"OPEN_PRIVATE_BROWSER_WINDOW",
"OPEN_URL",
"OPEN_ABOUT_PAGE"
]) {
ASRouterActions[type] = type;
}

// Helper function for creating routed actions between content and main
// Not intended to be used by consumers
function _RouteMessage(action, options) {
Expand Down Expand Up @@ -308,6 +319,7 @@ function WebExtEvent(type, data, importContext = globalImportContext) {
}

this.actionTypes = actionTypes;
this.ASRouterActions = ASRouterActions;

this.actionCreators = {
BroadcastToContent,
Expand Down Expand Up @@ -375,6 +387,7 @@ const EXPORTED_SYMBOLS = [
"actionTypes",
"actionCreators",
"actionUtils",
"ASRouterActions",
"globalImportContext",
"UI_CODE",
"BACKGROUND_PROCESS",
Expand Down
78 changes: 58 additions & 20 deletions system-addon/content-src/asrouter/asrouter-content.jsx
@@ -1,6 +1,7 @@
import {actionCreators as ac} from "common/Actions.jsm";
import {actionCreators as ac, ASRouterActions as ra} from "common/Actions.jsm";
import {OUTGOING_MESSAGE_NAME as AS_GENERAL_OUTGOING_MESSAGE_NAME} from "content-src/lib/init-store";
import {ImpressionsWrapper} from "./components/ImpressionsWrapper/ImpressionsWrapper";
import {OnboardingMessage} from "./templates/OnboardingMessage/OnboardingMessage";
import React from "react";
import ReactDOM from "react-dom";
import {SimpleSnippet} from "./templates/SimpleSnippet/SimpleSnippet";
Expand All @@ -21,9 +22,20 @@ export const ASRouterUtils = {
blockById(id) {
ASRouterUtils.sendMessage({type: "BLOCK_MESSAGE_BY_ID", data: {id}});
},
blockBundle(bundle) {
ASRouterUtils.sendMessage({type: "BLOCK_BUNDLE", data: {bundle}});
},
executeAction({button_action, button_action_params}) {
if (button_action in ra) {
ASRouterUtils.sendMessage({type: button_action, data: {button_action_params}});
}
},
unblockById(id) {
ASRouterUtils.sendMessage({type: "UNBLOCK_MESSAGE_BY_ID", data: {id}});
},
unblockBundle(bundle) {
ASRouterUtils.sendMessage({type: "UNBLOCK_BUNDLE", data: {bundle}});
},
getNextMessage() {
ASRouterUtils.sendMessage({type: "GET_NEXT_MESSAGE"});
},
Expand All @@ -47,15 +59,18 @@ export class ASRouterUISurface extends React.PureComponent {
this.onMessageFromParent = this.onMessageFromParent.bind(this);
this.sendImpression = this.sendImpression.bind(this);
this.sendUserActionTelemetry = this.sendUserActionTelemetry.bind(this);
this.state = {message: {}};
this.state = {message: {}, bundle: {}};
}

sendUserActionTelemetry(extraProps = {}) {
const {message} = this.state;
const eventType = `${message.provider}_user_event`;
const {message, bundle} = this.state;
if (!message && !extraProps.message_id) {
throw new Error(`You must provide a message_id for bundled messages`);
}
const eventType = `${message.provider || bundle.provider}_user_event`;

ASRouterUtils.sendTelemetry(Object.assign({
message_id: message.id,
message_id: message.id || extraProps.message_id,
source: this.props.id,
action: eventType
}, extraProps));
Expand All @@ -69,13 +84,20 @@ export class ASRouterUISurface extends React.PureComponent {
return () => ASRouterUtils.blockById(id);
}

clearBundle(bundle) {
return () => ASRouterUtils.blockBundle(bundle);
}

onMessageFromParent({data: action}) {
switch (action.type) {
case "SET_MESSAGE":
this.setState({message: action.data});
break;
case "SET_BUNDLED_MESSAGES":
this.setState({bundle: action.data});
break;
case "CLEAR_MESSAGE":
this.setState({message: {}});
this.setState({message: {}, bundle: {}});
break;
}
}
Expand All @@ -89,28 +111,44 @@ export class ASRouterUISurface extends React.PureComponent {
ASRouterUtils.removeListener(this.onMessageFromParent);
}

render() {
const {message} = this.state;
if (!message.id) { return null; }
return (<ImpressionsWrapper
message={message}
renderSnippets() {
return (
<ImpressionsWrapper
message={this.state.message}
sendImpression={this.sendImpression}
shouldSendImpressionOnUpdate={shouldSendImpressionOnUpdate}
// This helps with testing
document={this.props.document}>
<SimpleSnippet
{...message}
UISurface={this.props.id}
getNextMessage={ASRouterUtils.getNextMessage}
onBlock={this.onBlockById(message.id)}
sendUserActionTelemetry={this.sendUserActionTelemetry} />
</ImpressionsWrapper>
);
<SimpleSnippet
{...this.state.message}
UISurface="NEWTAB_FOOTER_BAR"
getNextMessage={ASRouterUtils.getNextMessage}
onBlock={this.onBlockById(this.state.message.id)}
sendUserActionTelemetry={this.sendUserActionTelemetry} />
</ImpressionsWrapper>);
}

renderOnboarding() {
return (
<OnboardingMessage
{...this.state.bundle}
UISurface="NEWTAB_OVERLAY"
onAction={ASRouterUtils.executeAction}
onDoneButton={this.clearBundle(this.state.bundle.bundle)}
getNextMessage={ASRouterUtils.getNextMessage}
sendUserActionTelemetry={this.sendUserActionTelemetry} />);
}

render() {
const {message, bundle} = this.state;
if (!message.id && !bundle.template) { return null; }
if (bundle.template === "onboarding") { return this.renderOnboarding(); }
return this.renderSnippets();
}
}

ASRouterUISurface.defaultProps = {document: global.document};

export function initASRouter() {
ReactDOM.render(<ASRouterUISurface id="NEWTAB_FOOTER_BAR" />, document.getElementById("snippets-container"));
ReactDOM.render(<ASRouterUISurface />, document.getElementById("snippets-container"));
}
@@ -0,0 +1,30 @@
import React from "react";

export class ModalOverlay extends React.PureComponent {
componentWillMount() {
this.setState({active: true});
document.body.classList.add("modal-open");
}

componentWillUnmount() {
document.body.classList.remove("modal-open");
this.setState({active: false});
}

render() {
const {active} = this.state;
const {title, button_label} = this.props;
return (
<div>
<div className={`modalOverlayOuter ${active ? "active" : ""}`} />
<div className={`modalOverlayInner ${active ? "active" : ""}`}>
<h2> {title} </h2>
{this.props.children}
<div className="footer">
<button onClick={this.props.onDoneButton} className="button primary modalButton"> {button_label} </button>
</div>
</div>
</div>
);
}
}
@@ -0,0 +1,93 @@
.activity-stream {
&.modal-open {
overflow: hidden;
}
}
.modalOverlayOuter {
background: $white;
opacity: 0.93;
height: 100%;
position: fixed;
top: 0;
width: 100%;
display: none;
z-index: 100000;

&.active {
display: block;
}
}

.modalOverlayInner {
width: 960px;
height: 510px;
position: fixed;
top: calc(50% - 255px); // halfway down minus half the height of the modal
left: calc(50% - 480px); // halfway across minus half the width of the modal
background: $white;
box-shadow: 0 1px 15px 0 $black-30;
border-radius: 4px;
display: none;
z-index: 100001;


// modal takes over entire screen
@media(max-width: 960px) {
width: 100%;
height: 100%;
top: 0;
left: 0;
box-shadow: none;
border-radius: none;
}

// if modal is short enough, add a vertical scroll bar
@media(max-width: 850px) and (max-height: 730px) {
overflow-y: scroll;
}

&.active {
display: block;
}

h2 {
color: $grey-60;
text-align: center;
font-weight: 200;
margin-top: 30px;
font-size: 28px;
line-height: 37px;
letter-spacing: -0.13px;

@media(max-width: 960px) {
margin-top: 100px;
}

@media(max-width: 850px) {
margin-top: 30px;
}
}

.footer {
border-top: 1px solid $grey-30;
height: 70px;
width: 100%;
position: absolute;
bottom: 0;
text-align: center;
background-color: $white;

// if modal is short enough, footer becomes sticky
@media(max-width: 850px) and (max-height: 730px) {
position: sticky;
}

.modalButton {
margin-top: 20px;
width: 150px;
height: 30px;
padding: 4px 0 6px 0;
font-size: 15px;
}
}
}
@@ -0,0 +1,48 @@
import {ModalOverlay} from "../../components/ModalOverlay/ModalOverlay";
import React from "react";

class OnboardingCard extends React.PureComponent {
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
}

onClick() {
const {props} = this;
props.sendUserActionTelemetry({event: "TRY_NOW", message_id: props.id});
props.onAction(props.content);
}

render() {
const {content} = this.props;
return (
<div className="onboardingMessage">
<img src={`resource://activity-stream/data/content/assets/illustration-${content.icon}@2x.png`} />
<div className="onboardingContent">
<span>
<h3> {content.title} </h3>
<p> {content.text} </p>
</span>
<span>
<button className="button onboardingButton" onClick={this.onClick}> {content.button_label} </button>
</span>
</div>
</div>
);
}
}

export class OnboardingMessage extends React.PureComponent {
render() {
const {props} = this;
return (
<ModalOverlay {...props} button_label={"Start Browsing"} title={"Welcome to Firefox"}>
<div className="onboardingMessageContainer">
{props.bundle.map(message => (
<OnboardingCard key={message.id} sendUserActionTelemetry={props.sendUserActionTelemetry} onAction={props.onAction} {...message} />
))}
</div>
</ModalOverlay>
);
}
}

0 comments on commit 49d13ab

Please sign in to comment.