Bug 1139677 - Display the user's FxA profile image in the Sync Pref pane r=markh,rfeeley
authorZachary Carter <zack.carter@gmail.com>
Fri, 27 Mar 2015 02:37:55 -0700
changeset 236036 ae08455b713c0fa1dc2b91beb52fe2db99303e63
parent 236035 776865752a675fbb707a9c8bb880af90fa27d029
child 236037 cfe959cdfb212d2e0e890cf1bdfe8d391a583fc4
push id57574
push userzcarter@mozilla.com
push dateFri, 27 Mar 2015 09:39:22 +0000
treeherdermozilla-inbound@ae08455b713c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmarkh, rfeeley
bugs1139677
milestone39.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1139677 - Display the user's FxA profile image in the Sync Pref pane r=markh,rfeeley
browser/components/preferences/in-content/sync.js
browser/components/preferences/in-content/sync.xul
browser/themes/linux/jar.mn
browser/themes/osx/jar.mn
browser/themes/shared/incontentprefs/default-profile-image.svg
browser/themes/shared/incontentprefs/preferences.inc.css
browser/themes/windows/jar.mn
services/fxaccounts/FxAccounts.jsm
services/fxaccounts/tests/xpcshell/test_accounts.js
--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -90,17 +90,19 @@ let gSyncPane = {
   },
 
   _init: function () {
     let topics = ["weave:service:login:error",
                   "weave:service:login:finish",
                   "weave:service:start-over:finish",
                   "weave:service:setup-complete",
                   "weave:service:logout:finish",
-                  FxAccountsCommon.ONVERIFIED_NOTIFICATION];
+                  FxAccountsCommon.ONVERIFIED_NOTIFICATION,
+                  FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION,
+                  ];
     let migrateTopic = "fxa-migration:state-changed";
 
     // Add the observers now and remove them on unload
     //XXXzpao This should use Services.obs.* but Weave's Obs does nice handling
     //        of `this`. Fix in a followup. (bug 583347)
     topics.forEach(function (topic) {
       Weave.Svc.Obs.add(topic, this.updateWeavePrefs, this);
     }, this);
@@ -118,16 +120,18 @@ let gSyncPane = {
       return Services.strings.createBundle("chrome://browser/locale/preferences/preferences.properties");
     }),
 
     XPCOMUtils.defineLazyGetter(this, '_accountsStringBundle', () => {
       return Services.strings.createBundle("chrome://browser/locale/accounts.properties");
     }),
 
     this.updateWeavePrefs();
+
+    this._initProfileImageUI();
   },
 
   _setupEventListeners: function() {
     function setEventListener(aId, aEventType, aCallback)
     {
       document.getElementById(aId)
               .addEventListener(aEventType, aCallback.bind(gSyncPane));
     }
@@ -219,16 +223,24 @@ let gSyncPane = {
       fxaMigrator.forgetFxAccount();
     });
     setEventListener("sync-migrate-resend", "click", function () {
       let win = Services.wm.getMostRecentWindow("navigator:browser");
       fxaMigrator.resendVerificationMail(win);
     });
   },
 
+  _initProfileImageUI: function () {
+    try {
+      if (Services.prefs.getBoolPref("identity.fxaccounts.profile_image.enabled")) {
+        document.getElementById("fxaProfileImage").hidden = false;
+      }
+    } catch (e) { }
+  },
+
   updateWeavePrefs: function () {
     // ask the migration module to broadcast its current state (and nothing will
     // happen if it's not loaded - which is good, as that means no migration
     // is pending/necessary) - we don't want to suck that module in just to
     // find there's nothing to do.
     Services.obs.notifyObservers(null, "fxa-migration:state-request", null);
 
     let service = Components.classes["@mozilla.org/weave/service;1"]
@@ -239,20 +251,21 @@ let gSyncPane = {
     if (service.fxAccountsEnabled) {
       // unhide the reading-list engine if readinglist is enabled (note we do
       // it here as it must remain disabled for legacy sync users)
       if (Services.prefs.getBoolPref("browser.readinglist.enabled")) {
         document.getElementById("readinglist-engine").removeAttribute("hidden");
       }
       // determine the fxa status...
       this.page = PAGE_PLEASE_WAIT;
+
       fxAccounts.getSignedInUser().then(data => {
         if (!data) {
           this.page = FXA_PAGE_LOGGED_OUT;
-          return;
+          return false;
         }
         this.page = FXA_PAGE_LOGGED_IN;
         // We are logged in locally, but maybe we are in a state where the
         // server rejected our credentials (eg, password changed on the server)
         let fxaLoginStatus = document.getElementById("fxaLoginStatus");
         let enginesListDisabled;
         // Not Verfied implies login error state, so check that first.
         if (!data.verified) {
@@ -276,17 +289,46 @@ let gSyncPane = {
         document.getElementById("fxaEmailAddress1").textContent = data.email;
         document.getElementById("fxaEmailAddress2").textContent = data.email;
         document.getElementById("fxaEmailAddress3").textContent = data.email;
         document.getElementById("fxaSyncComputerName").value = Weave.Service.clientsEngine.localName;
         let engines = document.getElementById("fxaSyncEngines")
         for (let checkbox of engines.querySelectorAll("checkbox")) {
           checkbox.disabled = enginesListDisabled;
         }
+
+        // Clear the profile image (if any) of the previously logged in account.
+        document.getElementById("fxaProfileImage").style.removeProperty("background-image");
+
+        // If the account is verified the next promise in the chain will
+        // fetch profile data.
+        return data.verified;
+      }).then(shouldGetProfile => {
+        if (shouldGetProfile) {
+          return fxAccounts.getSignedInUserProfile();
+        }
+      }).then(data => {
+        if (data && data.avatar) {
+          // Make sure the image is available before displaying it,
+          // as we don't want to overwrite the default profile image
+          // with a broken/unavailable image
+          let img = new Image();
+          img.onload = () => {
+            let bgImage = "url('" + data.avatar + "')";
+            document.getElementById("fxaProfileImage").style.backgroundImage = bgImage;
+          };
+          img.src = data.avatar;
+        }
+      }, err => {
+        FxAccountsCommon.log.error(err);
+      }).catch(err => {
+        // If we get here something's really busted
+        Cu.reportError(String(err));
       });
+
     // If fxAccountEnabled is false and we are in a "not configured" state,
     // then fxAccounts is probably fully disabled rather than just unconfigured,
     // so handle this case.  This block can be removed once we remove support
     // for fxAccounts being disabled.
     } else if (Weave.Status.service == Weave.CLIENT_NOT_CONFIGURED ||
                Weave.Svc.Prefs.get("firstSync", "") == "notReady") {
       this.page = PAGE_NO_ACCOUNT;
     // else: sync was previously configured for the legacy provider, so we
@@ -516,16 +558,25 @@ let gSyncPane = {
   },
 
   reSignIn: function() {
     this.openContentInBrowser("about:accounts?action=reauth&entrypoint=preferences", {
       replaceQueryString: true
     });
   },
 
+  openChangeProfileImage: function() {
+    fxAccounts.promiseAccountsChangeProfileURI("avatar")
+      .then(url => {
+        this.openContentInBrowser(url, {
+          replaceQueryString: true
+        });
+      });
+  },
+
   manageFirefoxAccount: function() {
     let url = Services.prefs.getCharPref("identity.fxaccounts.settings.uri");
     this.openContentInBrowser(url);
   },
 
   verifyFirefoxAccount: function() {
     fxAccounts.resendVerificationEmail().then(() => {
       fxAccounts.getSignedInUser().then(data => {
--- a/browser/components/preferences/in-content/sync.xul
+++ b/browser/components/preferences/in-content/sync.xul
@@ -228,17 +228,21 @@
     <groupbox id="fxaGroup">
       <caption><label>&syncBrand.fxAccount.label;</label></caption>
 
       <deck id="fxaLoginStatus">
 
         <!-- logged in and verified and all is good -->
         <hbox id="fxaLoginVerified"
               align="center">
-          <label id="fxaEmailAddress1"/>
+          <hbox align="center">
+            <image id="fxaProfileImage"
+              onclick="gSyncPane.openChangeProfileImage();" hidden="true"/>
+            <label id="fxaEmailAddress1"/>
+          </hbox>
           <spacer flex="1"/>
           <button id="verifiedManage"
                   label="&manage.label;"/>
           <button id="fxaUnlinkButton"
                   label="&disconnect.label;"/>
         </hbox>
 
         <!-- logged in to an unverified account -->
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -174,16 +174,17 @@ browser.jar:
   skin/classic/browser/preferences/Options-sync.png   (preferences/Options-sync.png)
 #endif
 * skin/classic/browser/preferences/preferences.css    (preferences/preferences.css)
 * skin/classic/browser/preferences/in-content/preferences.css (preferences/in-content/preferences.css)
 * skin/classic/browser/preferences/in-content/dialog.css      (preferences/in-content/dialog.css)
   skin/classic/browser/preferences/in-content/favicon.ico     (../shared/incontentprefs/favicon.ico)
   skin/classic/browser/preferences/in-content/icons.svg       (../shared/incontentprefs/icons.svg)
   skin/classic/browser/preferences/in-content/search.css      (../shared/incontentprefs/search.css)
+  skin/classic/browser/preferences/in-content/default-profile-image.svg   (../shared/incontentprefs/default-profile-image.svg)
   skin/classic/browser/preferences/applications.css   (preferences/applications.css)
   skin/classic/browser/preferences/aboutPermissions.css (preferences/aboutPermissions.css)
   skin/classic/browser/preferences/search.css         (preferences/search.css)
   skin/classic/browser/social/services-16.png         (social/services-16.png)
   skin/classic/browser/social/services-64.png         (social/services-64.png)
   skin/classic/browser/social/share-button.png        (social/share-button.png)
   skin/classic/browser/social/share-button-active.png (social/share-button-active.png)
   skin/classic/browser/social/chat-icons.svg          (../shared/social/chat-icons.svg)
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -279,16 +279,17 @@ browser.jar:
 #endif
   skin/classic/browser/preferences/saveFile.png             (preferences/saveFile.png)
 * skin/classic/browser/preferences/preferences.css          (preferences/preferences.css)
 * skin/classic/browser/preferences/in-content/preferences.css (preferences/in-content/preferences.css)
 * skin/classic/browser/preferences/in-content/dialog.css      (preferences/in-content/dialog.css)
   skin/classic/browser/preferences/in-content/favicon.ico     (../shared/incontentprefs/favicon.ico)
   skin/classic/browser/preferences/in-content/icons.svg       (../shared/incontentprefs/icons.svg)
   skin/classic/browser/preferences/in-content/search.css      (../shared/incontentprefs/search.css)
+  skin/classic/browser/preferences/in-content/default-profile-image.svg   (../shared/incontentprefs/default-profile-image.svg)
   skin/classic/browser/preferences/applications.css         (preferences/applications.css)
   skin/classic/browser/preferences/aboutPermissions.css     (preferences/aboutPermissions.css)
   skin/classic/browser/preferences/search.css               (preferences/search.css)
   skin/classic/browser/preferences/checkbox.png             (preferences/checkbox.png)
   skin/classic/browser/preferences/checkbox@2x.png          (preferences/checkbox@2x.png)
   skin/classic/browser/yosemite/preferences/checkbox.png    (preferences/checkbox-yosemite.png)
   skin/classic/browser/yosemite/preferences/checkbox@2x.png (preferences/checkbox-yosemite@2x.png)
   skin/classic/browser/social/services-16.png               (social/services-16.png)
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/incontentprefs/default-profile-image.svg
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
+<path fill="#C3CFD8" d="M500-0.3c276.1,0,500,223.9,500,500s-223.9,500-500,500S0,775.8,0,499.7C0,223.5,223.9-0.3,500-0.3z"/>
+<circle fill="#FFFFFF" cx="500" cy="317" r="139.1"/>
+<path fill="#FFFFFF" d="M751.8,643.6L751.8,643.6c0.1-2.3,0.2-4.6,0.2-6.9c0-68-55.3-127-136.2-156.3L505.9,590.4h0
+	c-0.4,29.8-1.4,58.8-2.8,86.6c-1,0.1-2,0.3-3.1,0.3s-2-0.2-3.1-0.3c-1.4-27.9-2.4-56.9-2.8-86.7h0L384.3,480.4
+	C303.3,509.7,248,568.7,248,636.7c0,2.3,0.1,4.6,0.2,6.9l7.4,49.7c57.1,72,145.4,118.2,244.4,118.2c99,0,187.3-46.2,244.4-118.2
+	L751.8,643.6z"/>
+</svg>
--- a/browser/themes/shared/incontentprefs/preferences.inc.css
+++ b/browser/themes/shared/incontentprefs/preferences.inc.css
@@ -211,16 +211,32 @@ treecol {
 }
 
 /* XXX This style is for bug 740213 and should be removed once that
    bug has a solution. */
 description > html|a {
   cursor: pointer;
 }
 
+#fxaProfileImage {
+  width: 60px;
+  height: 60px;
+  border-radius: 50%;
+  border-width: 5px;
+  border-color: red;
+  background-image: url(chrome://browser/skin/preferences/in-content/default-profile-image.svg);
+  background-size: contain;
+  cursor: pointer;
+  -moz-margin-end: 15px;
+}
+
+#fxaProfileImage:hover {
+  border-color: blue;
+}
+
 #noFxaAccount {
   /* Overriding the margins from the base preferences.css theme file.
      These overrides can be simplified by fixing bug 1027174 */
   margin: 0;
 }
 
 #weavePrefsDeck > vbox > label,
 #weavePrefsDeck > vbox > groupbox,
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -202,16 +202,17 @@ browser.jar:
 #endif
         skin/classic/browser/preferences/saveFile.png                (preferences/saveFile.png)
 *       skin/classic/browser/preferences/preferences.css             (preferences/preferences.css)
 *       skin/classic/browser/preferences/in-content/preferences.css  (preferences/in-content/preferences.css)
 *       skin/classic/browser/preferences/in-content/dialog.css       (preferences/in-content/dialog.css)
         skin/classic/browser/preferences/in-content/favicon.ico      (../shared/incontentprefs/favicon.ico)
         skin/classic/browser/preferences/in-content/icons.svg        (../shared/incontentprefs/icons.svg)
         skin/classic/browser/preferences/in-content/search.css       (../shared/incontentprefs/search.css)
+        skin/classic/browser/preferences/in-content/default-profile-image.svg   (../shared/incontentprefs/default-profile-image.svg)
         skin/classic/browser/preferences/applications.css            (preferences/applications.css)
         skin/classic/browser/preferences/aboutPermissions.css        (preferences/aboutPermissions.css)
         skin/classic/browser/preferences/search.css                  (preferences/search.css)
         skin/classic/browser/preferences/checkbox.png                (preferences/checkbox-xp.png)
         skin/classic/browser/preferences/checkbox-classic.png        (preferences/checkbox-classic.png)
         skin/classic/browser/social/services-16.png                  (social/services-16.png)
         skin/classic/browser/social/services-64.png                  (social/services-64.png)
         skin/classic/browser/social/chat-icons.svg                   (../shared/social/chat-icons.svg)
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -39,16 +39,17 @@ let publicProperties = [
   "getKeys",
   "getSignedInUser",
   "getOAuthToken",
   "getSignedInUserProfile",
   "loadAndPoll",
   "localtimeOffsetMsec",
   "now",
   "promiseAccountsForceSigninURI",
+  "promiseAccountsChangeProfileURI",
   "resendVerificationEmail",
   "setSignedInUser",
   "signOut",
   "version",
   "whenVerified"
 ];
 
 // An AccountState object holds all state related to one specific account.
@@ -956,16 +957,44 @@ FxAccountsInternal.prototype = {
         return null;
       }
       let newQueryPortion = url.indexOf("?") == -1 ? "?" : "&";
       newQueryPortion += "email=" + encodeURIComponent(accountData.email);
       return url + newQueryPortion;
     }).then(result => currentState.resolve(result));
   },
 
+  // Returns a promise that resolves with the URL to use to change
+  // the current account's profile image.
+  // if settingToEdit is set, the profile page should hightlight that setting
+  // for the user to edit.
+  promiseAccountsChangeProfileURI: function(settingToEdit = null) {
+    let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.settings.uri");
+
+    if (settingToEdit) {
+      url += (url.indexOf("?") == -1 ? "?" : "&") +
+             "setting=" + encodeURIComponent(settingToEdit);
+    }
+
+    if (this._requireHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
+      throw new Error("Firefox Accounts server must use HTTPS");
+    }
+    let currentState = this.currentAccountState;
+    // but we need to append the email address onto a query string.
+    return this.getSignedInUser().then(accountData => {
+      if (!accountData) {
+        return null;
+      }
+      let newQueryPortion = url.indexOf("?") == -1 ? "?" : "&";
+      newQueryPortion += "email=" + encodeURIComponent(accountData.email);
+      newQueryPortion += "&uid=" + encodeURIComponent(accountData.uid);
+      return url + newQueryPortion;
+    }).then(result => currentState.resolve(result));
+  },
+
   /**
    * Get an OAuth token for the user
    *
    * @param options
    *        {
    *          scope: (string) the oauth scope being requested
    *        }
    *
@@ -1073,40 +1102,23 @@ FxAccountsInternal.prototype = {
    *          UNVERIFIED_ACCOUNT
    *          NETWORK_ERROR
    *          AUTH_ERROR
    *          UNKNOWN_ERROR
    */
   getSignedInUserProfile: function () {
     let accountState = this.currentAccountState;
     return accountState.getProfile()
-      .then(
-        (profileData) => {
-          let profile = JSON.parse(JSON.stringify(profileData));
-          // profileData doesn't include "verified", but it must be true
-          // if we've gotten this far.
-          profile.verified = true;
-          return accountState.resolve(profile);
-        },
-        (error) => {
-          log.error("Could not retrieve profile data", error);
-
-          return this.getSignedInUser().then(data => {
-            let profile = null;
-            if (data) {
-              // If we fail to fetch the profile and have no profile cached
-              // we resort to using the account data for basic profile data.
-              profile = {
-                email: data.email,
-                uid: data.uid,
-                verified: data.verified
-              };
-            }
-            return accountState.resolve(profile);
-          });
+      .then((profileData) => {
+        let profile = JSON.parse(JSON.stringify(profileData));
+        return accountState.resolve(profile);
+      },
+      (error) => {
+        log.error("Could not retrieve profile data", error);
+        return accountState.reject(error);
       })
       .then(null, err => this._errorToErrorClass(err));
   },
 };
 
 /**
  * JSONStorage constructor that creates instances that may set/get
  * to a specified file, in a directory that will be created if it
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -918,17 +918,16 @@ add_test(function test_getSignedInUserPr
     let accountState = fxa.internal.currentAccountState;
     accountState.getProfile = function () {
       return Promise.resolve({ avatar: "image" });
     };
 
     fxa.getSignedInUserProfile()
       .then(result => {
          do_check_eq(result.avatar, "image");
-         do_check_true(result.verified);
          run_next_test();
       });
   });
 
 });
 
 add_test(function test_getSignedInUserProfile_error_uses_account_data() {
   let fxa = new MockFxAccounts();
@@ -941,44 +940,50 @@ add_test(function test_getSignedInUserPr
 
   fxa.setSignedInUser(alice).then(() => {
     let accountState = fxa.internal.currentAccountState;
     accountState.getProfile = function () {
       return Promise.reject("boom");
     };
 
     fxa.getSignedInUserProfile()
-      .then(result => {
-         do_check_eq(typeof result.avatar, "undefined");
-         do_check_eq(result.email, "foo@bar.com");
+      .catch(error => {
+         do_check_eq(error.message, "UNKNOWN_ERROR");
+         run_next_test();
+      });
+  });
+
+});
+
+add_test(function test_getSignedInUserProfile_unverified_account() {
+  let fxa = new MockFxAccounts();
+  let alice = getTestUser("alice");
+
+  fxa.setSignedInUser(alice).then(() => {
+    let accountState = fxa.internal.currentAccountState;
+
+    fxa.getSignedInUserProfile()
+      .catch(error => {
+         do_check_eq(error.message, "UNVERIFIED_ACCOUNT");
          run_next_test();
       });
   });
 
 });
 
 add_test(function test_getSignedInUserProfile_no_account_data() {
   let fxa = new MockFxAccounts();
 
   fxa.internal.getSignedInUser = function () {
-    return Promise.resolve({ email: "foo@bar.com" });
-  };
-
-  let accountState = fxa.internal.currentAccountState;
-  accountState.getProfile = function () {
-    return Promise.reject("boom");
-  };
-
-  fxa.internal.getSignedInUser = function () {
     return Promise.resolve(null);
   };
 
   fxa.getSignedInUserProfile()
-    .then(result => {
-       do_check_eq(result, null);
+    .catch(error => {
+       do_check_eq(error.message, "NO_ACCOUNT");
        run_next_test();
     });
 
 });
 
 /*
  * End of tests.
  * Utility functions follow.