Bug 1201335 - Display notification when a new device is added to Sync account. r?markh
MozReview-Commit-ID: D03prgIdL1M
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1294,16 +1294,19 @@ pref("identity.fxaccounts.remote.webchan
// discovery is enabled.
pref("identity.fxaccounts.contextParam", "fx_desktop_v3");
// The URL we take the user to when they opt to "manage" their Firefox Account.
// Note that this will always need to be in the same TLD as the
// "identity.fxaccounts.remote.signup.uri" pref.
pref("identity.fxaccounts.settings.uri", "https://accounts.firefox.com/settings?service=sync&context=fx_desktop_v3");
+// The URL of the FxA device manager page
+pref("identity.fxaccounts.settings.devices.uri", "https://accounts.firefox.com/settings/clients?service=sync&context=fx_desktop_v3");
+
// The remote URL of the FxA Profile Server
pref("identity.fxaccounts.remote.profile.uri", "https://profile.accounts.firefox.com/v1");
// The remote URL of the FxA OAuth Server
pref("identity.fxaccounts.remote.oauth.uri", "https://oauth.accounts.firefox.com/v1");
// Whether we display profile images in the UI or not.
pref("identity.fxaccounts.profile_image.enabled", true);
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -246,16 +246,19 @@ BrowserGlue.prototype = {
}
break;
case "weave:service:ready":
this._setSyncAutoconnectDelay();
break;
case "fxaccounts:onverified":
this._showSyncStartedDoorhanger();
break;
+ case "fxaccounts:device_connected":
+ this._onDeviceConnected(data);
+ break;
case "fxaccounts:device_disconnected":
this._onDeviceDisconnected();
break;
case "weave:engine:clients:display-uris":
this._onDisplaySyncURIs(subject);
break;
case "session-save":
this._setPrefToSaveSession(true);
@@ -403,16 +406,17 @@ BrowserGlue.prototype = {
os.addObserver(this, "quit-application-requested", false);
os.addObserver(this, "quit-application-granted", false);
if (OBSERVE_LASTWINDOW_CLOSE_TOPICS) {
os.addObserver(this, "browser-lastwindow-close-requested", false);
os.addObserver(this, "browser-lastwindow-close-granted", false);
}
os.addObserver(this, "weave:service:ready", false);
os.addObserver(this, "fxaccounts:onverified", false);
+ os.addObserver(this, "fxaccounts:device_connected", false);
os.addObserver(this, "fxaccounts:device_disconnected", false);
os.addObserver(this, "weave:engine:clients:display-uris", false);
os.addObserver(this, "session-save", false);
os.addObserver(this, "places-init-complete", false);
this._isPlacesInitObserver = true;
os.addObserver(this, "places-database-locked", false);
this._isPlacesLockedObserver = true;
os.addObserver(this, "distribution-customization-complete", false);
@@ -455,16 +459,17 @@ BrowserGlue.prototype = {
os.removeObserver(this, "quit-application-granted");
os.removeObserver(this, "restart-in-safe-mode");
if (OBSERVE_LASTWINDOW_CLOSE_TOPICS) {
os.removeObserver(this, "browser-lastwindow-close-requested");
os.removeObserver(this, "browser-lastwindow-close-granted");
}
os.removeObserver(this, "weave:service:ready");
os.removeObserver(this, "fxaccounts:onverified");
+ os.removeObserver(this, "fxaccounts:device_connected");
os.removeObserver(this, "fxaccounts:device_disconnected");
os.removeObserver(this, "weave:engine:clients:display-uris");
os.removeObserver(this, "session-save");
if (this._bookmarksBackupIdleTime) {
this._idleService.removeIdleObserver(this, this._bookmarksBackupIdleTime);
delete this._bookmarksBackupIdleTime;
}
if (this._isPlacesInitObserver)
@@ -2325,16 +2330,42 @@ BrowserGlue.prototype = {
imageURL = "chrome://branding/content/icon64.png";
}
AlertsService.showAlertNotification(imageURL, title, body, true, null, clickCallback);
} catch (ex) {
Cu.reportError("Error displaying tab(s) received by Sync: " + ex);
}
},
+ _onDeviceConnected(deviceName) {
+ let accountsBundle = Services.strings.createBundle(
+ "chrome://browser/locale/accounts.properties"
+ );
+ let title = accountsBundle.GetStringFromName("deviceConnectedTitle");
+ let body = accountsBundle.formatStringFromName("deviceConnectedBody", [deviceName], 1);
+ let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.settings.devices.uri");
+
+ function clickCallback(subject, topic, data) {
+ if (topic != "alertclickcallback")
+ return;
+ let win = RecentWindow.getMostRecentBrowserWindow({private: false});
+ if (!win) {
+ Services.appShell.hiddenDOMWindow.open(url);
+ } else {
+ win.gBrowser.addTab(url);
+ }
+ }
+
+ try {
+ AlertsService.showAlertNotification(null, title, body, true, url, clickCallback);
+ } catch (ex) {
+ Cu.reportError("Error notifying of a new Sync device: " + ex);
+ }
+ },
+
_onDeviceDisconnected() {
let bundle = Services.strings.createBundle("chrome://browser/locale/accounts.properties");
let title = bundle.GetStringFromName("deviceDisconnectedNotification.title");
let body = bundle.GetStringFromName("deviceDisconnectedNotification.body");
let clickCallback = (subject, topic, data) => {
if (topic != "alertclickcallback")
return;
--- a/browser/locales/en-US/chrome/browser/accounts.properties
+++ b/browser/locales/en-US/chrome/browser/accounts.properties
@@ -19,16 +19,21 @@ verifyDescription = Verify %S
# These strings are shown in a desktop notification after the
# user requests we resend a verification email.
verificationSentTitle = Verification Sent
# LOCALIZATION NOTE (verificationSentBody) - %S = Email address of user's Firefox Account
verificationSentBody = A verification link has been sent to %S.
verificationNotSentTitle = Unable to Send Verification
verificationNotSentBody = We are unable to send a verification mail at this time, please try again later.
+# LOCALIZATION NOTE (deviceConnectedTitle, deviceConnectedBody)
+# These strings are used in a notification shown when a new device joins the Sync account.
+deviceConnectedTitle = Firefox Sync
+deviceConnectedBody = This computer is now syncing with %S.
+
# LOCALIZATION NOTE (syncStartNotification.title, syncStartNotification.body)
# These strings are used in a notification shown after Sync is connected.
syncStartNotification.title = Sync enabled
# %S is brandShortName
syncStartNotification.body2 = %S will begin syncing momentarily.
# LOCALIZATION NOTE (deviceDisconnectedNotification.title, deviceDisconnectedNotification.body)
# These strings are used in a notification shown after Sync was disconnected remotely.
--- a/services/fxaccounts/FxAccountsCommon.js
+++ b/services/fxaccounts/FxAccountsCommon.js
@@ -84,16 +84,17 @@ exports.KEY_LIFETIME = 1000 * 3600
exports.POLL_SESSION = 1000 * 60 * 20; // 20 minutes
// Observer notifications.
exports.ONLOGIN_NOTIFICATION = "fxaccounts:onlogin";
exports.ONVERIFIED_NOTIFICATION = "fxaccounts:onverified";
exports.ONLOGOUT_NOTIFICATION = "fxaccounts:onlogout";
// Internal to services/fxaccounts only
exports.ON_FXA_UPDATE_NOTIFICATION = "fxaccounts:update";
+exports.ON_DEVICE_CONNECTED_NOTIFICATION = "fxaccounts:device_connected";
exports.ON_DEVICE_DISCONNECTED_NOTIFICATION = "fxaccounts:device_disconnected";
exports.ON_PASSWORD_CHANGED_NOTIFICATION = "fxaccounts:password_changed";
exports.ON_PASSWORD_RESET_NOTIFICATION = "fxaccounts:password_reset";
exports.ON_COLLECTION_CHANGED_NOTIFICATION = "sync:collection_changed";
exports.FXA_PUSH_SCOPE_ACCOUNT_UPDATE = "chrome://fxa-device-update";
exports.ON_PROFILE_CHANGE_NOTIFICATION = "fxaccounts:profilechange";
--- a/services/fxaccounts/FxAccountsPush.js
+++ b/services/fxaccounts/FxAccountsPush.js
@@ -159,16 +159,19 @@ FxAccountsPushService.prototype = {
if (!message.data) {
// Use the empty signal to check the verification state of the account right away
this.log.debug("empty push message - checking account status");
return this.fxAccounts.checkVerificationStatus();
}
let payload = message.data.json();
this.log.debug(`push command: ${payload.command}`);
switch (payload.command) {
+ case ON_DEVICE_CONNECTED_NOTIFICATION:
+ Services.obs.notifyObservers(null, ON_DEVICE_CONNECTED_NOTIFICATION, payload.data.deviceName);
+ break;
case ON_DEVICE_DISCONNECTED_NOTIFICATION:
return this.fxAccounts.handleDeviceDisconnection(payload.data.id);
break;
case ON_PASSWORD_CHANGED_NOTIFICATION:
case ON_PASSWORD_RESET_NOTIFICATION:
return this._onPasswordChanged();
break;
case ON_COLLECTION_CHANGED_NOTIFICATION:
--- a/services/fxaccounts/moz.build
+++ b/services/fxaccounts/moz.build
@@ -3,16 +3,18 @@
# 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/.
DIRS += ['interfaces']
MOCHITEST_CHROME_MANIFESTS += ['tests/mochitest/chrome.ini']
+BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
+
XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
EXTRA_COMPONENTS += [
'FxAccountsComponents.manifest',
'FxAccountsPush.js',
]
EXTRA_JS_MODULES += [
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/tests/browser/browser.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+support-files =
+ head.js
+[browser_device_connected.js]
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/tests/browser/browser_device_connected.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const { MockRegistrar } =
+ Cu.import("resource://testing-common/MockRegistrar.jsm", {});
+
+const StubAlertsService = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAlertsService, Ci.nsISupports]),
+
+ showAlertNotification(image, title, text, clickable, cookie, clickCallback) {
+ // We can't simulate a click on the alert popup,
+ // so instead we call the click listener ourselves directly
+ clickCallback.observe.call(clickCallback, null, "alertclickcallback", null);
+ }
+}
+
+add_task(function*() {
+ gBrowser.selectedBrowser.loadURI("about:robots");
+ yield waitForDocLoadComplete();
+
+ MockRegistrar.register("@mozilla.org/alerts-service;1", StubAlertsService);
+ Preferences.set("identity.fxaccounts.settings.devices.uri", "http://localhost/devices");
+
+ let waitForTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+
+ Services.obs.notifyObservers(null, "fxaccounts:device_connected", "My phone");
+
+ let tab = yield waitForTabPromise;
+ Assert.ok("Tab successfully opened");
+
+ let expectedURI = Preferences.get("identity.fxaccounts.settings.devices.uri",
+ "prefundefined");
+ Assert.equal(tab.linkedBrowser.currentURI.spec, expectedURI);
+
+ yield BrowserTestUtils.removeTab(tab);
+});
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/tests/browser/head.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Waits for the next load to complete in any browser or the given browser.
+ * If a <tabbrowser> is given it waits for a load in any of its browsers.
+ *
+ * @return promise
+ */
+function waitForDocLoadComplete(aBrowser = gBrowser) {
+ return new Promise(resolve => {
+ let listener = {
+ onStateChange(webProgress, req, flags, status) {
+ let docStop = Ci.nsIWebProgressListener.STATE_IS_NETWORK |
+ Ci.nsIWebProgressListener.STATE_STOP;
+ info("Saw state " + flags.toString(16) + " and status " + status.toString(16));
+
+ // When a load needs to be retargetted to a new process it is cancelled
+ // with NS_BINDING_ABORTED so ignore that case
+ if ((flags & docStop) == docStop && status != Cr.NS_BINDING_ABORTED) {
+ aBrowser.removeProgressListener(this);
+ waitForDocLoadComplete.listeners.delete(this);
+
+ let chan = req.QueryInterface(Ci.nsIChannel);
+ info("Browser loaded " + chan.originalURI.spec);
+ resolve();
+ }
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference])
+ };
+ aBrowser.addProgressListener(listener);
+ waitForDocLoadComplete.listeners.add(listener);
+ info("Waiting for browser load");
+ });
+}
+
+// Keep a set of progress listeners for waitForDocLoadComplete() to make sure
+// they're not GC'ed before we saw the page load.
+waitForDocLoadComplete.listeners = new Set();
+registerCleanupFunction(() => waitForDocLoadComplete.listeners.clear());
--- a/services/fxaccounts/tests/xpcshell/test_push_service.js
+++ b/services/fxaccounts/tests/xpcshell/test_push_service.js
@@ -138,16 +138,44 @@ add_test(function observePushTopicVerify
let pushService = new FxAccountsPushService({
pushService: mockPushService,
fxAccounts: customAccounts,
});
pushService.observe(emptyMsg, mockPushService.pushTopic, FXA_PUSH_SCOPE_ACCOUNT_UPDATE);
});
+add_test(function observePushTopicDeviceConnected() {
+ let msg = {
+ data: {
+ json: () => ({
+ command: ON_DEVICE_CONNECTED_NOTIFICATION,
+ data: {
+ deviceName: "My phone"
+ }
+ })
+ },
+ QueryInterface: function() {
+ return this;
+ }
+ };
+ let obs = (subject, topic, data) => {
+ Services.obs.removeObserver(obs, topic);
+ run_next_test();
+ };
+ Services.obs.addObserver(obs, ON_DEVICE_CONNECTED_NOTIFICATION, false);
+
+ let pushService = new FxAccountsPushService({
+ pushService: mockPushService,
+ fxAccounts: mockFxAccounts,
+ });
+
+ pushService.observe(msg, mockPushService.pushTopic, FXA_PUSH_SCOPE_ACCOUNT_UPDATE);
+});
+
add_test(function observePushTopicDeviceDisconnected() {
const deviceId = "bogusid";
let msg = {
data: {
json: () => ({
command: ON_DEVICE_DISCONNECTED_NOTIFICATION,
data: {
id: deviceId