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/. */
5 import { Async } from "resource://services-common/async.sys.mjs";
8 FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
10 ON_ACCOUNT_DESTROYED_NOTIFICATION,
11 ON_COLLECTION_CHANGED_NOTIFICATION,
12 ON_COMMAND_RECEIVED_NOTIFICATION,
13 ON_DEVICE_CONNECTED_NOTIFICATION,
14 ON_DEVICE_DISCONNECTED_NOTIFICATION,
15 ON_PASSWORD_CHANGED_NOTIFICATION,
16 ON_PASSWORD_RESET_NOTIFICATION,
17 ON_PROFILE_CHANGE_NOTIFICATION,
18 ON_PROFILE_UPDATED_NOTIFICATION,
19 ON_VERIFY_LOGIN_NOTIFICATION,
21 } = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
24 * FxAccountsPushService manages Push notifications for Firefox Accounts in the browser
27 * Object, custom options that used for testing
30 export function FxAccountsPushService(options = {}) {
34 // allow custom log for testing purposes
35 this.log = options.log;
38 this.log.debug("FxAccountsPush loading service");
39 this.wrappedJSObject = this;
40 this.initialize(options);
43 FxAccountsPushService.prototype = {
45 * Helps only initialize observers once.
49 * Instance of the nsIPushService or a mocked object.
53 * Instance of FxAccountsInternal or a mocked object.
57 * Component ID of this service, helps register this component.
59 classID: Components.ID("{1b7db999-2ecd-4abf-bb95-a726896798ca}"),
61 * Register used interfaces in this service
63 QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
65 * Initialize the service and register all the required observers.
70 if (this._initialized) {
74 this._initialized = true;
76 if (options.pushService) {
77 this.pushService = options.pushService;
79 this.pushService = Cc["@mozilla.org/push/Service;1"].getService(
85 this.fxai = options.fxai;
87 const { getFxAccountsSingleton } = ChromeUtils.importESModule(
88 "resource://gre/modules/FxAccounts.sys.mjs"
90 const fxAccounts = getFxAccountsSingleton();
91 this.fxai = fxAccounts._internal;
94 this.asyncObserver = Async.asyncObserver(this, this.log);
95 // We use an async observer because a device waking up can
96 // observe multiple "Send Tab received" push notifications at the same time.
97 // The way these notifications are handled is as follows:
98 // Read index from storage, make network request, update the index.
99 // You can imagine what happens when multiple calls race: we load
100 // the same index multiple times and receive the same exact tabs, multiple times.
101 // The async observer will ensure we make these network requests serially.
102 Services.obs.addObserver(this.asyncObserver, this.pushService.pushTopic);
103 Services.obs.addObserver(
105 this.pushService.subscriptionChangeTopic
107 Services.obs.addObserver(this.asyncObserver, ONLOGOUT_NOTIFICATION);
109 this.log.debug("FxAccountsPush initialized");
113 * Registers a new endpoint with the Push Server
116 * Promise always resolves with a subscription or a null if failed to subscribe.
118 registerPushEndpoint() {
119 this.log.trace("FxAccountsPush registerPushEndpoint");
121 return new Promise(resolve => {
122 this.pushService.subscribe(
123 FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
124 Services.scriptSecurityManager.getSystemPrincipal(),
125 (result, subscription) => {
126 if (Components.isSuccessCode(result)) {
127 this.log.debug("FxAccountsPush got subscription");
128 resolve(subscription);
130 this.log.warn("FxAccountsPush failed to subscribe", result);
138 * Async observer interface to listen to push messages, changes and logout.
145 async observe(subject, topic, data) {
148 `observed topic=${topic}, data=${data}, subject=${subject}`
151 case this.pushService.pushTopic:
152 if (data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) {
153 let message = subject.QueryInterface(Ci.nsIPushMessage);
154 await this._onPushMessage(message);
157 case this.pushService.subscriptionChangeTopic:
158 if (data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) {
159 await this._onPushSubscriptionChange();
162 case ONLOGOUT_NOTIFICATION:
163 // user signed out, we need to stop polling the Push Server
164 await this.unsubscribe();
173 * Fired when the Push server sends a notification.
178 async _onPushMessage(message) {
179 this.log.trace("FxAccountsPushService _onPushMessage");
181 // Use the empty signal to check the verification state of the account right away
182 this.log.debug("empty push message - checking account status");
183 this.fxai.checkVerificationStatus();
186 let payload = message.data.json();
187 this.log.debug(`push command: ${payload.command}`);
188 switch (payload.command) {
189 case ON_COMMAND_RECEIVED_NOTIFICATION:
190 await this.fxai.commands.pollDeviceCommands(payload.data.index);
192 case ON_DEVICE_CONNECTED_NOTIFICATION:
193 Services.obs.notifyObservers(
195 ON_DEVICE_CONNECTED_NOTIFICATION,
196 payload.data.deviceName
199 case ON_DEVICE_DISCONNECTED_NOTIFICATION:
200 this.fxai._handleDeviceDisconnection(payload.data.id);
202 case ON_PROFILE_UPDATED_NOTIFICATION:
203 // We already have a "profile updated" notification sent via WebChannel,
204 // let's just re-use that.
205 Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION);
207 case ON_PASSWORD_CHANGED_NOTIFICATION:
208 case ON_PASSWORD_RESET_NOTIFICATION:
209 this._onPasswordChanged();
211 case ON_ACCOUNT_DESTROYED_NOTIFICATION:
212 this.fxai._handleAccountDestroyed(payload.data.uid);
214 case ON_COLLECTION_CHANGED_NOTIFICATION:
215 Services.obs.notifyObservers(
217 ON_COLLECTION_CHANGED_NOTIFICATION,
218 payload.data.collections
221 case ON_VERIFY_LOGIN_NOTIFICATION:
222 Services.obs.notifyObservers(
224 ON_VERIFY_LOGIN_NOTIFICATION,
225 JSON.stringify(payload.data)
229 this.log.warn("FxA Push command unrecognized: " + payload.command);
233 * Check the FxA session status after a password change/reset event.
234 * If the session is invalid, reset credentials and notify listeners of
235 * ON_ACCOUNT_STATE_CHANGE_NOTIFICATION that the account may have changed
240 _onPasswordChanged() {
241 return this.fxai.withCurrentAccountState(async state => {
242 return this.fxai.checkAccountStatus(state);
246 * Fired when the Push server drops a subscription, or the subscription identifier changes.
248 * https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#Receiving_Push_Messages
253 _onPushSubscriptionChange() {
254 this.log.trace("FxAccountsPushService _onPushSubscriptionChange");
255 return this.fxai.updateDeviceRegistration();
258 * Unsubscribe from the Push server
260 * Ref: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#unsubscribe()
262 * @returns {Promise} - The promise resolves with a bool to indicate if we successfully unsubscribed.
263 * The promise never rejects.
267 this.log.trace("FxAccountsPushService unsubscribe");
268 return new Promise(resolve => {
269 this.pushService.unsubscribe(
270 FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
271 Services.scriptSecurityManager.getSystemPrincipal(),
273 if (Components.isSuccessCode(result)) {
275 this.log.debug("FxAccountsPushService unsubscribed");
278 "FxAccountsPushService had no subscription to unsubscribe"
283 "FxAccountsPushService failed to unsubscribe",
294 * Get our Push server subscription.
296 * Ref: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#getSubscription()
298 * @returns {Promise} - resolves with the subscription or null. Never rejects.
301 return new Promise(resolve => {
302 this.pushService.getSubscription(
303 FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
304 Services.scriptSecurityManager.getSystemPrincipal(),
305 (result, subscription) => {
307 this.log.info("FxAccountsPushService no subscription found");
308 return resolve(null);
310 return resolve(subscription);