diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
index 801ccf845f7..08a817ea162 100644
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1293,3 +1293,7 @@ pref("geo.wifi.uri", "https://www.googleapis.com/geolocation/v1/geolocate?key=%G
// Necko IPC security checks only needed for app isolation for cookies/cache/etc:
// currently irrelevant for desktop e10s
pref("network.disable.ipc.security", true);
+
+// The URL where remote content that composes the UI for Firefox Accounts should
+// be fetched.
+pref("firefox.accounts.remoteUrl", "http://accounts.dev.lcip.org/flow");
diff --git a/browser/base/content/aboutaccounts/aboutaccounts.css b/browser/base/content/aboutaccounts/aboutaccounts.css
new file mode 100644
index 00000000000..cf2bfa7b7c1
--- /dev/null
+++ b/browser/base/content/aboutaccounts/aboutaccounts.css
@@ -0,0 +1,21 @@
+* {
+ margin: 0;
+ padding: 0;
+}
+
+html, body {
+ height: 100%;
+}
+
+#content {
+ width: 100%;
+ height: 100%;
+ border: 0;
+ display: flex;
+}
+
+#remote {
+ width: 100%;
+ height: 100%;
+ border: 0;
+}
diff --git a/browser/base/content/aboutaccounts/aboutaccounts.js b/browser/base/content/aboutaccounts/aboutaccounts.js
new file mode 100644
index 00000000000..500616771f5
--- /dev/null
+++ b/browser/base/content/aboutaccounts/aboutaccounts.js
@@ -0,0 +1,89 @@
+/* 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/. */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+function log(msg) {
+ //dump("FXA: " + msg + "\n");
+};
+
+let wrapper = {
+ iframe: null,
+
+ init: function () {
+ let iframe = document.getElementById("remote");
+ this.iframe = iframe;
+ iframe.addEventListener("load", this);
+ iframe.src = this._getAccountsURI();
+ },
+
+ handleEvent: function (evt) {
+ switch (evt.type) {
+ case "load":
+ this.iframe.contentWindow.addEventListener("FirefoxAccountsCommand", this);
+ this.iframe.removeEventListener("load", this);
+ break;
+ case "FirefoxAccountsCommand":
+ this.handleRemoteCommand(evt);
+ break;
+ }
+ },
+
+ onLogin: function (data) {
+ log("Received: 'login'. Data:" + JSON.stringify(data));
+ this.injectData("message", { status: "login" });
+ },
+
+ onCreate: function (data) {
+ log("Received: 'create'. Data:" + JSON.stringify(data));
+ this.injectData("message", { status: "create" });
+ },
+
+ onVerified: function (data) {
+ log("Received: 'verified'. Data:" + JSON.stringify(data));
+ this.injectData("message", { status: "verified" });
+ },
+
+ _getAccountsURI: function () {
+ return Services.urlFormatter.formatURLPref("firefox.accounts.remoteUrl");
+ },
+
+ handleRemoteCommand: function (evt) {
+ log('command: ' + evt.detail.command);
+ let data = evt.detail.data;
+
+ switch (evt.detail.command) {
+ case "create":
+ this.onCreate(data);
+ break;
+ case "login":
+ this.onLogin(data);
+ break;
+ case "verified":
+ this.onVerified(data);
+ break;
+ default:
+ log("Unexpected remote command received: " + evt.detail.command + ". Ignoring command.");
+ break;
+ }
+ },
+
+ injectData: function (type, content) {
+ let authUrl = this._getAccountsURI();
+
+ let data = {
+ type: type,
+ content: content
+ };
+
+ this.iframe.contentWindow.postMessage(data, authUrl);
+ },
+};
+
+wrapper.init();
+
diff --git a/browser/base/content/aboutaccounts/aboutaccounts.xhtml b/browser/base/content/aboutaccounts/aboutaccounts.xhtml
new file mode 100644
index 00000000000..839ab0e141e
--- /dev/null
+++ b/browser/base/content/aboutaccounts/aboutaccounts.xhtml
@@ -0,0 +1,28 @@
+
+
+
+ %htmlDTD;
+
+ %brandDTD;
+
+ %aboutAccountsDTD;
+]>
+
+
+
+ &aboutAccounts.pageTitle;
+
+
+
+
+
+
+
+
diff --git a/browser/base/content/test/Makefile.in b/browser/base/content/test/Makefile.in
index 22c6671dcea..47b65377b63 100644
--- a/browser/base/content/test/Makefile.in
+++ b/browser/base/content/test/Makefile.in
@@ -65,6 +65,7 @@ endif
MOCHITEST_BROWSER_FILES = \
head.js \
+ accounts_testRemoteCommands.html \
alltabslistener.html \
app_bug575561.html \
app_subframe_bug575561.html \
@@ -73,6 +74,7 @@ MOCHITEST_BROWSER_FILES = \
blockPluginHard.xml \
blockPluginVulnerableNoUpdate.xml \
blockPluginVulnerableUpdatable.xml \
+ browser_aboutAccounts.js \
browser_aboutHealthReport.js \
browser_aboutHome.js \
browser_aboutSyncProgress.js \
diff --git a/browser/base/content/test/accounts_testRemoteCommands.html b/browser/base/content/test/accounts_testRemoteCommands.html
new file mode 100644
index 00000000000..5172d45504b
--- /dev/null
+++ b/browser/base/content/test/accounts_testRemoteCommands.html
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
diff --git a/browser/base/content/test/browser_aboutAccounts.js b/browser/base/content/test/browser_aboutAccounts.js
new file mode 100644
index 00000000000..6292fe68001
--- /dev/null
+++ b/browser/base/content/test/browser_aboutAccounts.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+registerCleanupFunction(function() {
+ // Ensure we don't pollute prefs for next tests.
+ Services.prefs.clearUserPref("firefox.accounts.remoteUrl");
+});
+
+let gTests = [
+
+{
+ desc: "Test the remote commands",
+ setup: function ()
+ {
+ Services.prefs.setCharPref("firefox.accounts.remoteUrl",
+ "https://example.com/browser/browser/base/content/test/accounts_testRemoteCommands.html");
+ },
+ run: function ()
+ {
+ let deferred = Promise.defer();
+
+ let results = 0;
+ try {
+ let win = gBrowser.contentWindow;
+ win.addEventListener("message", function testLoad(e) {
+ if (e.data.type == "testResult") {
+ ok(e.data.pass, e.data.info);
+ results++;
+ }
+ else if (e.data.type == "testsComplete") {
+ is(results, e.data.count, "Checking number of results received matches the number of tests that should have run");
+ win.removeEventListener("message", testLoad, false, true);
+ deferred.resolve();
+ }
+
+ }, false, true);
+
+ } catch(e) {
+ ok(false, "Failed to get all commands");
+ deferred.reject();
+ }
+ return deferred.promise;
+ }
+},
+
+
+]; // gTests
+
+function test()
+{
+ waitForExplicitFinish();
+
+ Task.spawn(function () {
+ for (let test of gTests) {
+ info(test.desc);
+ test.setup();
+
+ yield promiseNewTabLoadEvent("about:accounts");
+
+ yield test.run();
+
+ gBrowser.removeCurrentTab();
+ }
+
+ finish();
+ });
+}
+
+function promiseNewTabLoadEvent(aUrl, aEventType="load")
+{
+ let deferred = Promise.defer();
+ let tab = gBrowser.selectedTab = gBrowser.addTab(aUrl);
+ tab.linkedBrowser.addEventListener(aEventType, function load(event) {
+ tab.linkedBrowser.removeEventListener(aEventType, load, true);
+ let iframe = tab.linkedBrowser.contentDocument.getElementById("remote");
+ iframe.addEventListener("load", function frameLoad(e) {
+ iframe.removeEventListener("load", frameLoad, false);
+ deferred.resolve();
+ }, false);
+ }, true);
+ return deferred.promise;
+}
+
diff --git a/browser/base/jar.mn b/browser/base/jar.mn
index 670215956cc..949c711b947 100644
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -52,6 +52,9 @@ browser.jar:
content/browser/abouthealthreport/abouthealth.js (content/abouthealthreport/abouthealth.js)
content/browser/abouthealthreport/abouthealth.css (content/abouthealthreport/abouthealth.css)
#endif
+ content/browser/aboutaccounts/aboutaccounts.xhtml (content/aboutaccounts/aboutaccounts.xhtml)
+ content/browser/aboutaccounts/aboutaccounts.js (content/aboutaccounts/aboutaccounts.js)
+ content/browser/aboutaccounts/aboutaccounts.css (content/aboutaccounts/aboutaccounts.css)
content/browser/aboutRobots-icon.png (content/aboutRobots-icon.png)
content/browser/aboutRobots-widget-left.png (content/aboutRobots-widget-left.png)
content/browser/aboutSocialError.xhtml (content/aboutSocialError.xhtml)
diff --git a/browser/components/about/AboutRedirector.cpp b/browser/components/about/AboutRedirector.cpp
index 9000e6ac30b..7714deb2fde 100644
--- a/browser/components/about/AboutRedirector.cpp
+++ b/browser/components/about/AboutRedirector.cpp
@@ -86,6 +86,8 @@ static RedirEntry kRedirMap[] = {
{ "healthreport", "chrome://browser/content/abouthealthreport/abouthealth.xhtml",
nsIAboutModule::ALLOW_SCRIPT },
#endif
+ { "accounts", "chrome://browser/content/aboutaccounts/aboutaccounts.xhtml",
+ nsIAboutModule::ALLOW_SCRIPT },
};
static const int kRedirTotal = NS_ARRAY_LENGTH(kRedirMap);
diff --git a/browser/components/build/nsModule.cpp b/browser/components/build/nsModule.cpp
index 82172fd2d52..4c02e18bd0e 100644
--- a/browser/components/build/nsModule.cpp
+++ b/browser/components/build/nsModule.cpp
@@ -105,6 +105,7 @@ static const mozilla::Module::ContractIDEntry kBrowserContracts[] = {
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "permissions", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "preferences", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "downloads", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
+ { NS_ABOUT_MODULE_CONTRACTID_PREFIX "accounts", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
#ifdef MOZ_SERVICES_HEALTHREPORT
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "healthreport", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
#endif
diff --git a/browser/locales/en-US/chrome/browser/aboutAccounts.dtd b/browser/locales/en-US/chrome/browser/aboutAccounts.dtd
new file mode 100644
index 00000000000..c5bd829fbc3
--- /dev/null
+++ b/browser/locales/en-US/chrome/browser/aboutAccounts.dtd
@@ -0,0 +1,5 @@
+
+
+
diff --git a/browser/locales/jar.mn b/browser/locales/jar.mn
index e090202348a..8ae034d880a 100644
--- a/browser/locales/jar.mn
+++ b/browser/locales/jar.mn
@@ -6,6 +6,7 @@
@AB_CD@.jar:
% locale browser @AB_CD@ %locale/browser/
+ locale/browser/aboutAccounts.dtd (%chrome/browser/aboutAccounts.dtd)
locale/browser/aboutCertError.dtd (%chrome/browser/aboutCertError.dtd)
locale/browser/aboutDialog.dtd (%chrome/browser/aboutDialog.dtd)
locale/browser/aboutPrivateBrowsing.dtd (%chrome/browser/aboutPrivateBrowsing.dtd)