 |
1 # This Source Code Form is subject to the terms of the Mozilla Public
2 # License, v. 2.0. If a copy of the MPL was not distributed with this
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5 let gFxAccounts = {
6
7 PREF_SYNC_START_DOORHANGER: "services.sync.ui.showSyncStartDoorhanger",
8 DOORHANGER_ACTIVATE_DELAY_MS: 5000,
9 SYNC_MIGRATION_NOTIFICATION_TITLE: "fxa-migration",
10
11 _initialized: false,
12 _inCustomizationMode: false,
13 // _expectingNotifyClose is a hack that helps us determine if the
14 // migration notification was closed due to being "dismissed" vs closed
15 // due to one of the migration buttons being clicked. It's ugly and somewhat
16 // fragile, so bug 1119020 exists to help us do this better.
17 _expectingNotifyClose: false,
18
19 get weave() {
20 delete this.weave;
21 return this.weave = Cc["@mozilla.org/weave/service;1"]
22 .getService(Ci.nsISupports)
23 .wrappedJSObject;
24 },
25
26 get topics() {
27 // Do all this dance to lazy-load FxAccountsCommon.
28 delete this.topics;
29 return this.topics = [
30 "weave:service:ready",
31 "weave:service:sync:start",
32 "weave:service:login:error",
33 "weave:service:setup-complete",
34 "fxa-migration:state-changed",
35 this.FxAccountsCommon.ONVERIFIED_NOTIFICATION,
36 this.FxAccountsCommon.ONLOGOUT_NOTIFICATION,
37 "weave:notification:removed",
38 ];
39 },
40
41 get button() {
42 delete this.button;
43 return this.button = document.getElementById("PanelUI-fxa-status");
44 },
45
46 get strings() {
47 delete this.strings;
48 return this.strings = Services.strings.createBundle(
49 "chrome://browser/locale/accounts.properties"
50 );
51 },
52
53 get loginFailed() {
54 // Referencing Weave.Service will implicitly initialize sync, and we don't
55 // want to force that - so first check if it is ready.
56 let service = Cc["@mozilla.org/weave/service;1"]
57 .getService(Components.interfaces.nsISupports)
58 .wrappedJSObject;
59 if (!service.ready) {
60 return false;
61 }
62 // LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in".
63 // All other login failures are assumed to be transient and should go
64 // away by themselves, so aren't reflected here.
65 return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
66 },
67
68 get isActiveWindow() {
69 let fm = Services.focus;
70 return fm.activeWindow == window;
71 },
72
73 init: function () {
74 // Bail out if we're already initialized and for pop-up windows.
75 if (this._initialized || !window.toolbar.visible) {
76 return;
77 }
78
79 for (let topic of this.topics) {
80 Services.obs.addObserver(this, topic, false);
81 }
82
83 addEventListener("activate", this);
84 gNavToolbox.addEventListener("customizationstarting", this);
85 gNavToolbox.addEventListener("customizationending", this);
86
87 // Request the current Legacy-Sync-to-FxA migration status. We'll be
88 // notified of fxa-migration:state-changed in response if necessary.
89 Services.obs.notifyObservers(null, "fxa-migration:state-request", null);
90
91 this._initialized = true;
92
93 this.updateUI();
94 },
95
96 uninit: function () {
97 if (!this._initialized) {
98 return;
99 }
100
101 for (let topic of this.topics) {
102 Services.obs.removeObserver(this, topic);
103 }
104
105 this._initialized = false;
106 },
107
108 observe: function (subject, topic, data) {
109 switch (topic) {
110 case this.FxAccountsCommon.ONVERIFIED_NOTIFICATION:
111 Services.prefs.setBoolPref(this.PREF_SYNC_START_DOORHANGER, true);
112 break;
113 case "weave:service:sync:start":
114 this.onSyncStart();
115 break;
116 case "fxa-migration:state-changed":
117 this.onMigrationStateChanged(data, subject);
118 break;
119 case "weave:notification:removed":
120 // this exists just so we can tell the difference between "box was
121 // closed due to button press" vs "was closed due to click on [x]"
122 let notif = subject.wrappedJSObject.object;
123 if (notif.title == this.SYNC_MIGRATION_NOTIFICATION_TITLE &&
124 !this._expectingNotifyClose) {
125 // it's an [x] on our notification, so record telemetry.
126 this.fxaMigrator.recordTelemetry(this.fxaMigrator.TELEMETRY_DECLINED);
127 }
128 break;
129 default:
130 this.updateUI();
131 break;
132 }
133 },
134
135 onSyncStart: function () {
136 if (!this.isActiveWindow) {
137 return;
138 }
139
140 let showDoorhanger = false;
141
142 try {
143 showDoorhanger = Services.prefs.getBoolPref(this.PREF_SYNC_START_DOORHANGER);
144 } catch (e) { /* The pref might not exist. */ }
145
146 if (showDoorhanger) {
147 Services.prefs.clearUserPref(this.PREF_SYNC_START_DOORHANGER);
148 this.showSyncStartedDoorhanger();
149 }
150 },
151
152 onMigrationStateChanged: function (newState, email) {
153 this._migrationInfo = !newState ? null : {
154 state: newState,
155 email: email ? email.QueryInterface(Ci.nsISupportsString).data : null,
156 };
157 this.updateUI();
158 },
159
160 handleEvent: function (event) {
161 if (event.type == "activate") {
162 // Our window might have been in the background while we received the
163 // sync:start notification. If still needed, show the doorhanger after
164 // a short delay. Without this delay the doorhanger would not show up
165 // or with a too small delay show up while we're still animating the
166 // window.
167 setTimeout(() => this.onSyncStart(), this.DOORHANGER_ACTIVATE_DELAY_MS);
168 } else {
169 this._inCustomizationMode = event.type == "customizationstarting";
170 this.updateAppMenuItem();
171 }
172 },
173
174 showDoorhanger: function (id) {
175 let panel = document.getElementById(id);
176 let anchor = document.getElementById("PanelUI-menu-button");
177
178 let iconAnchor =
179 document.getAnonymousElementByAttribute(anchor, "class",
180 "toolbarbutton-icon");
181
182 panel.hidden = false;
183 panel.openPopup(iconAnchor || anchor, "bottomcenter topright");
184 },
185
186 showSyncStartedDoorhanger: function () {
187 this.showDoorhanger("sync-start-panel");
188 },
189
190 showSyncFailedDoorhanger: function () {
191 this.showDoorhanger("sync-error-panel");
192 },
193
194 updateUI: function () {
195 this.updateAppMenuItem();
196 this.updateMigrationNotification();
197 },
198
199 updateAppMenuItem: function () {
200 if (this._migrationInfo) {
201 this.updateAppMenuItemForMigration();
202 return;
203 }
204
205 // Bail out if FxA is disabled.
206 if (!this.weave.fxAccountsEnabled) {
207 // When migration transitions from needs-verification to the null state,
208 // fxAccountsEnabled is false because migration has not yet finished. In
209 // that case, hide the button. We'll get another notification with a null
210 // state once migration is complete.
211 this.button.hidden = true;
212 this.button.removeAttribute("fxastatus");
213 return;
214 }
215
216 // FxA is enabled, show the widget.
217 this.button.hidden = false;
218
219 // Make sure the button is disabled in customization mode.
220 if (this._inCustomizationMode) {
221 this.button.setAttribute("disabled", "true");
222 } else {
223 this.button.removeAttribute("disabled");
224 }
225
226 let defaultLabel = this.button.getAttribute("defaultlabel");
227 let errorLabel = this.button.getAttribute("errorlabel");
228
229 // If the user is signed into their Firefox account and we are not
230 // currently in customization mode, show their email address.
231 let doUpdate = userData => {
232 // Reset the button to its original state.
233 this.button.setAttribute("label", defaultLabel);
234 this.button.removeAttribute("tooltiptext");
235 this.button.removeAttribute("fxastatus");
236
237 if (!this._inCustomizationMode) {
238 if (this.loginFailed) {
239 this.button.setAttribute("fxastatus", "error");
240 this.button.setAttribute("label", errorLabel);
241 } else if (userData) {
242 this.button.setAttribute("fxastatus", "signedin");
243 this.button.setAttribute("label", userData.email);
244 this.button.setAttribute("tooltiptext", userData.email);
245 }
246 }
247 }
248 fxAccounts.getSignedInUser().then(userData => {
249 doUpdate(userData);
250 }).then(null, error => {
251 // This is most likely in tests, were we quickly log users in and out.
252 // The most likely scenario is a user logged out, so reflect that.
253 // Bug 995134 calls for better errors so we could retry if we were
254 // sure this was the failure reason.
255 doUpdate(null);
256 });
257 },
258
259 updateAppMenuItemForMigration: Task.async(function* () {
260 let status = null;
261 let label = null;
262 switch (this._migrationInfo.state) {
263 case this.fxaMigrator.STATE_USER_FXA:
264 status = "migrate-signup";
265 label = this.strings.formatStringFromName("needUserShort",
266 [this.button.getAttribute("fxabrandname")], 1);
267 break;
268 case this.fxaMigrator.STATE_USER_FXA_VERIFIED:
269 status = "migrate-verify";
270 label = this.strings.formatStringFromName("needVerifiedUserShort",
271 [this._migrationInfo.email],
272 1);
273 break;
274 }
275 this.button.label = label;
276 this.button.hidden = false;
277 this.button.setAttribute("fxastatus", status);
278 }),
279
280 updateMigrationNotification: Task.async(function* () {
281 if (!this._migrationInfo) {
282 this._expectingNotifyClose = true;
283 Weave.Notifications.removeAll(this.SYNC_MIGRATION_NOTIFICATION_TITLE);
284 // because this is called even when there is no such notification, we
285 // set _expectingNotifyClose back to false as we may yet create a new
286 // notification (but in general, once we've created a migration
287 // notification once in a session, we don't create one again)
288 this._expectingNotifyClose = false;
289 return;
290 }
291 let note = null;
292 switch (this._migrationInfo.state) {
293 case this.fxaMigrator.STATE_USER_FXA: {
294 // There are 2 cases here - no email address means it is an offer on
295 // the first device (so the user is prompted to create an account).
296 // If there is an email address it is the "join the party" flow, so the
297 // user is prompted to sign in with the address they previously used.
298 let msg, upgradeLabel, upgradeAccessKey, learnMoreLink;
299 if (this._migrationInfo.email) {
300 msg = this.strings.formatStringFromName("signInAfterUpgradeOnOtherDevice.description",
301 [this._migrationInfo.email],
302 1);
303 upgradeLabel = this.strings.GetStringFromName("signInAfterUpgradeOnOtherDevice.label");
304 upgradeAccessKey = this.strings.GetStringFromName("signInAfterUpgradeOnOtherDevice.accessKey");
305 } else {
306 msg = this.strings.GetStringFromName("needUserLong");
307 upgradeLabel = this.strings.GetStringFromName("upgradeToFxA.label");
308 upgradeAccessKey = this.strings.GetStringFromName("upgradeToFxA.accessKey");
309 learnMoreLink = this.fxaMigrator.learnMoreLink;
310 }
311 note = new Weave.Notification(
312 undefined, msg, undefined, Weave.Notifications.PRIORITY_WARNING, [
313 new Weave.NotificationButton(upgradeLabel, upgradeAccessKey, () => {
314 this._expectingNotifyClose = true;
315 this.fxaMigrator.createFxAccount(window);
316 }),
317 ], learnMoreLink
318 );
319 break;
320 }
321 case this.fxaMigrator.STATE_USER_FXA_VERIFIED: {
322 let msg =
323 this.strings.formatStringFromName("needVerifiedUserLong",
324 [this._migrationInfo.email], 1);
325 let resendLabel =
326 this.strings.GetStringFromName("resendVerificationEmail.label");
327 let resendAccessKey =
328 this.strings.GetStringFromName("resendVerificationEmail.accessKey");
329 note = new Weave.Notification(
330 undefined, msg, undefined, Weave.Notifications.PRIORITY_INFO, [
331 new Weave.NotificationButton(resendLabel, resendAccessKey, () => {
332 this._expectingNotifyClose = true;
333 this.fxaMigrator.resendVerificationMail();
334 }),
335 ]
336 );
337 break;
338 }
339 }
340 note.title = this.SYNC_MIGRATION_NOTIFICATION_TITLE;
341 Weave.Notifications.replaceTitle(note);
342 }),
343
344 onMenuPanelCommand: function (event) {
345 let button = event.originalTarget;
346
347 switch (button.getAttribute("fxastatus")) {
348 case "signedin":
349 this.openPreferences();
350 break;
351 case "error":
352 this.openSignInAgainPage("menupanel");
353 break;
354 case "migrate-signup":
355 case "migrate-verify":
356 // The migration flow calls for the menu item to open sync prefs rather
357 // than requesting migration start immediately.
358 this.openPreferences();
359 break;
360 default:
361 this.openAccountsPage(null, { entryPoint: "menupanel" });
362 break;
363 }
364
365 PanelUI.hide();
366 },
367
368 openPreferences: function () {
369 openPreferences("paneSync");
370 },
371
372 openAccountsPage: function (action, urlParams={}) {
373 // An entryPoint param is used for server-side metrics. If the current tab
374 // is UITour, assume that it initiated the call to this method and override
375 // the entryPoint accordingly.
376 if (UITour.tourBrowsersByWindow.get(window) &&
377 UITour.tourBrowsersByWindow.get(window).has(gBrowser.selectedBrowser)) {
378 urlParams.entryPoint = "uitour";
379 }
380 let params = new URLSearchParams();
381 if (action) {
382 params.set("action", action);
383 }
384 for (let name in urlParams) {
385 if (urlParams[name] !== undefined) {
386 params.set(name, urlParams[name]);
387 }
388 }
389 let url = "about:accounts?" + params;
390 switchToTabHavingURI(url, true, {
391 replaceQueryString: true
392 });
393 },
394
395 openSignInAgainPage: function (entryPoint) {
396 this.openAccountsPage("reauth", { entryPoint: entryPoint });
397 },
398 };
399
400 XPCOMUtils.defineLazyGetter(gFxAccounts, "FxAccountsCommon", function () {
401 return Cu.import("resource://gre/modules/FxAccountsCommon.js", {});
402 });
403
404 XPCOMUtils.defineLazyModuleGetter(gFxAccounts, "fxaMigrator",
405 "resource://services-sync/FxaMigrator.jsm");
406