|
1 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- / |
|
2 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 // This JS shim contains the callbacks to fire DOMRequest events for |
|
8 // navigator.pay API within the payment processor's scope. |
|
9 |
|
10 "use strict"; |
|
11 |
|
12 let { classes: Cc, interfaces: Ci, utils: Cu } = Components; |
|
13 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
14 Cu.import("resource://gre/modules/Services.jsm"); |
|
15 |
|
16 const PREF_DEBUG = "dom.payment.debug"; |
|
17 |
|
18 let _debug; |
|
19 try { |
|
20 _debug = Services.prefs.getPrefType(PREF_DEBUG) == Ci.nsIPrefBranch.PREF_BOOL |
|
21 && Services.prefs.getBoolPref(PREF_DEBUG); |
|
22 } catch(e){ |
|
23 _debug = false; |
|
24 } |
|
25 |
|
26 function LOG(s) { |
|
27 if (!_debug) { |
|
28 return; |
|
29 } |
|
30 dump("== Payment flow == " + s + "\n"); |
|
31 } |
|
32 |
|
33 function LOGE(s) { |
|
34 dump("== Payment flow ERROR == " + s + "\n"); |
|
35 } |
|
36 |
|
37 if (_debug) { |
|
38 LOG("Frame script injected"); |
|
39 } |
|
40 |
|
41 XPCOMUtils.defineLazyServiceGetter(this, "cpmm", |
|
42 "@mozilla.org/childprocessmessagemanager;1", |
|
43 "nsIMessageSender"); |
|
44 |
|
45 XPCOMUtils.defineLazyServiceGetter(this, "uuidgen", |
|
46 "@mozilla.org/uuid-generator;1", |
|
47 "nsIUUIDGenerator"); |
|
48 |
|
49 XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy", |
|
50 "resource://gre/modules/SystemAppProxy.jsm"); |
|
51 |
|
52 #ifdef MOZ_B2G_RIL |
|
53 XPCOMUtils.defineLazyServiceGetter(this, "gRil", |
|
54 "@mozilla.org/ril;1", |
|
55 "nsIRadioInterfaceLayer"); |
|
56 |
|
57 XPCOMUtils.defineLazyServiceGetter(this, "iccProvider", |
|
58 "@mozilla.org/ril/content-helper;1", |
|
59 "nsIIccProvider"); |
|
60 |
|
61 XPCOMUtils.defineLazyServiceGetter(this, "smsService", |
|
62 "@mozilla.org/sms/smsservice;1", |
|
63 "nsISmsService"); |
|
64 |
|
65 XPCOMUtils.defineLazyServiceGetter(this, "gSettingsService", |
|
66 "@mozilla.org/settingsService;1", |
|
67 "nsISettingsService"); |
|
68 |
|
69 const kSilentSmsReceivedTopic = "silent-sms-received"; |
|
70 const kMozSettingsChangedObserverTopic = "mozsettings-changed"; |
|
71 |
|
72 const kRilDefaultDataServiceId = "ril.data.defaultServiceId"; |
|
73 const kRilDefaultPaymentServiceId = "ril.payment.defaultServiceId"; |
|
74 |
|
75 const MOBILEMESSAGECALLBACK_CID = |
|
76 Components.ID("{b484d8c9-6be4-4f94-ab60-c9c7ebcc853d}"); |
|
77 |
|
78 // In order to send messages through nsISmsService, we need to implement |
|
79 // nsIMobileMessageCallback, as the WebSMS API implementation is not usable |
|
80 // from JS. |
|
81 function SilentSmsRequest() { |
|
82 } |
|
83 |
|
84 SilentSmsRequest.prototype = { |
|
85 __exposedProps__: { |
|
86 onsuccess: "rw", |
|
87 onerror: "rw" |
|
88 }, |
|
89 |
|
90 QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileMessageCallback]), |
|
91 |
|
92 classID: MOBILEMESSAGECALLBACK_CID, |
|
93 |
|
94 set onsuccess(aSuccessCallback) { |
|
95 this._onsuccess = aSuccessCallback; |
|
96 }, |
|
97 |
|
98 set onerror(aErrorCallback) { |
|
99 this._onerror = aErrorCallback; |
|
100 }, |
|
101 |
|
102 notifyMessageSent: function notifyMessageSent(aMessage) { |
|
103 if (_debug) { |
|
104 LOG("Silent message successfully sent"); |
|
105 } |
|
106 this._onsuccess(aMessage); |
|
107 }, |
|
108 |
|
109 notifySendMessageFailed: function notifySendMessageFailed(aError) { |
|
110 LOGE("Error sending silent message " + aError); |
|
111 this._onerror(aError); |
|
112 } |
|
113 }; |
|
114 |
|
115 function PaymentSettings() { |
|
116 Services.obs.addObserver(this, kMozSettingsChangedObserverTopic, false); |
|
117 |
|
118 [kRilDefaultDataServiceId, kRilDefaultPaymentServiceId].forEach(setting => { |
|
119 gSettingsService.createLock().get(setting, this); |
|
120 }); |
|
121 } |
|
122 |
|
123 PaymentSettings.prototype = { |
|
124 QueryInterface: XPCOMUtils.generateQI([Ci.nsISettingsServiceCallback, |
|
125 Ci.nsIObserver]), |
|
126 |
|
127 dataServiceId: 0, |
|
128 _paymentServiceId: 0, |
|
129 |
|
130 get paymentServiceId() { |
|
131 return this._paymentServiceId; |
|
132 }, |
|
133 |
|
134 set paymentServiceId(serviceId) { |
|
135 // We allow the payment provider to set the service ID that will be used |
|
136 // for the payment process. |
|
137 // This service ID will be the one used by the silent SMS flow. |
|
138 // If the payment is done with an external SIM, the service ID must be set |
|
139 // to null. |
|
140 if (serviceId != null && serviceId >= gRil.numRadioInterfaces) { |
|
141 LOGE("Invalid service ID " + serviceId); |
|
142 return; |
|
143 } |
|
144 |
|
145 gSettingsService.createLock().set(kRilDefaultPaymentServiceId, |
|
146 serviceId, null); |
|
147 this._paymentServiceId = serviceId; |
|
148 }, |
|
149 |
|
150 setServiceId: function(aName, aValue) { |
|
151 switch (aName) { |
|
152 case kRilDefaultDataServiceId: |
|
153 this.dataServiceId = aValue; |
|
154 if (_debug) { |
|
155 LOG("dataServiceId " + this.dataServiceId); |
|
156 } |
|
157 break; |
|
158 case kRilDefaultPaymentServiceId: |
|
159 this._paymentServiceId = aValue; |
|
160 if (_debug) { |
|
161 LOG("paymentServiceId " + this._paymentServiceId); |
|
162 } |
|
163 break; |
|
164 } |
|
165 }, |
|
166 |
|
167 handle: function(aName, aValue) { |
|
168 if (aName != kRilDefaultDataServiceId) { |
|
169 return; |
|
170 } |
|
171 |
|
172 this.setServiceId(aName, aValue); |
|
173 }, |
|
174 |
|
175 observe: function(aSubject, aTopic, aData) { |
|
176 if (aTopic != kMozSettingsChangedObserverTopic) { |
|
177 return; |
|
178 } |
|
179 |
|
180 try { |
|
181 let setting = JSON.parse(aData); |
|
182 if (!setting.key || |
|
183 (setting.key !== kRilDefaultDataServiceId && |
|
184 setting.key !== kRilDefaultPaymentServiceId)) { |
|
185 return; |
|
186 } |
|
187 this.setServiceId(setting.key, setting.value); |
|
188 } catch (e) { |
|
189 LOGE(e); |
|
190 } |
|
191 }, |
|
192 |
|
193 cleanup: function() { |
|
194 Services.obs.removeObserver(this, kMozSettingsChangedObserverTopic); |
|
195 } |
|
196 }; |
|
197 #endif |
|
198 |
|
199 const kClosePaymentFlowEvent = "close-payment-flow-dialog"; |
|
200 |
|
201 let gRequestId; |
|
202 |
|
203 let PaymentProvider = { |
|
204 #ifdef MOZ_B2G_RIL |
|
205 __exposedProps__: { |
|
206 paymentSuccess: "r", |
|
207 paymentFailed: "r", |
|
208 paymentServiceId: "rw", |
|
209 iccInfo: "r", |
|
210 sendSilentSms: "r", |
|
211 observeSilentSms: "r", |
|
212 removeSilentSmsObserver: "r" |
|
213 }, |
|
214 #else |
|
215 __exposedProps__: { |
|
216 paymentSuccess: "r", |
|
217 paymentFailed: "r" |
|
218 }, |
|
219 #endif |
|
220 |
|
221 _init: function _init() { |
|
222 #ifdef MOZ_B2G_RIL |
|
223 this._settings = new PaymentSettings(); |
|
224 #endif |
|
225 }, |
|
226 |
|
227 _closePaymentFlowDialog: function _closePaymentFlowDialog(aCallback) { |
|
228 // After receiving the payment provider confirmation about the |
|
229 // successful or failed payment flow, we notify the UI to close the |
|
230 // payment flow dialog and return to the caller application. |
|
231 let id = kClosePaymentFlowEvent + "-" + uuidgen.generateUUID().toString(); |
|
232 |
|
233 let detail = { |
|
234 type: kClosePaymentFlowEvent, |
|
235 id: id, |
|
236 requestId: gRequestId |
|
237 }; |
|
238 |
|
239 // In order to avoid race conditions, we wait for the UI to notify that |
|
240 // it has successfully closed the payment flow and has recovered the |
|
241 // caller app, before notifying the parent process to fire the success |
|
242 // or error event over the DOMRequest. |
|
243 SystemAppProxy.addEventListener("mozContentEvent", |
|
244 function closePaymentFlowReturn(evt) { |
|
245 if (evt.detail.id == id && aCallback) { |
|
246 aCallback(); |
|
247 } |
|
248 |
|
249 SystemAppProxy.removeEventListener("mozContentEvent", |
|
250 closePaymentFlowReturn); |
|
251 |
|
252 let glue = Cc["@mozilla.org/payment/ui-glue;1"] |
|
253 .createInstance(Ci.nsIPaymentUIGlue); |
|
254 glue.cleanup(); |
|
255 }); |
|
256 |
|
257 SystemAppProxy.dispatchEvent(detail); |
|
258 |
|
259 #ifdef MOZ_B2G_RIL |
|
260 this._cleanUp(); |
|
261 #endif |
|
262 }, |
|
263 |
|
264 paymentSuccess: function paymentSuccess(aResult) { |
|
265 if (_debug) { |
|
266 LOG("paymentSuccess " + aResult); |
|
267 } |
|
268 |
|
269 PaymentProvider._closePaymentFlowDialog(function notifySuccess() { |
|
270 if (!gRequestId) { |
|
271 return; |
|
272 } |
|
273 cpmm.sendAsyncMessage("Payment:Success", { result: aResult, |
|
274 requestId: gRequestId }); |
|
275 }); |
|
276 }, |
|
277 |
|
278 paymentFailed: function paymentFailed(aErrorMsg) { |
|
279 LOGE("paymentFailed " + aErrorMsg); |
|
280 |
|
281 PaymentProvider._closePaymentFlowDialog(function notifyError() { |
|
282 if (!gRequestId) { |
|
283 return; |
|
284 } |
|
285 cpmm.sendAsyncMessage("Payment:Failed", { errorMsg: aErrorMsg, |
|
286 requestId: gRequestId }); |
|
287 }); |
|
288 }, |
|
289 |
|
290 #ifdef MOZ_B2G_RIL |
|
291 get paymentServiceId() { |
|
292 return this._settings.paymentServiceId; |
|
293 }, |
|
294 |
|
295 set paymentServiceId(serviceId) { |
|
296 this._settings.paymentServiceId = serviceId; |
|
297 }, |
|
298 |
|
299 // We expose to the payment provider the information of all the SIMs |
|
300 // available in the device. iccInfo is an object of this form: |
|
301 // { |
|
302 // "serviceId1": { |
|
303 // mcc: <string>, |
|
304 // mnc: <string>, |
|
305 // iccId: <string>, |
|
306 // dataPrimary: <boolean> |
|
307 // }, |
|
308 // "serviceIdN": {...} |
|
309 // } |
|
310 get iccInfo() { |
|
311 if (!this._iccInfo) { |
|
312 this._iccInfo = {}; |
|
313 for (let i = 0; i < gRil.numRadioInterfaces; i++) { |
|
314 let info = iccProvider.getIccInfo(i); |
|
315 if (!info) { |
|
316 LOGE("Tried to get the ICC info for an invalid service ID " + i); |
|
317 continue; |
|
318 } |
|
319 |
|
320 this._iccInfo[i] = { |
|
321 iccId: info.iccid, |
|
322 mcc: info.mcc, |
|
323 mnc: info.mnc, |
|
324 dataPrimary: i == this._settings.dataServiceId |
|
325 }; |
|
326 } |
|
327 } |
|
328 |
|
329 return Cu.cloneInto(this._iccInfo, content); |
|
330 }, |
|
331 |
|
332 _silentNumbers: null, |
|
333 _silentSmsObservers: null, |
|
334 |
|
335 sendSilentSms: function sendSilentSms(aNumber, aMessage) { |
|
336 if (_debug) { |
|
337 LOG("Sending silent message " + aNumber + " - " + aMessage); |
|
338 } |
|
339 |
|
340 let request = new SilentSmsRequest(); |
|
341 |
|
342 if (this._settings.paymentServiceId === null) { |
|
343 LOGE("No payment service ID set. Cannot send silent SMS"); |
|
344 let runnable = { |
|
345 run: function run() { |
|
346 request.notifySendMessageFailed("NO_PAYMENT_SERVICE_ID"); |
|
347 } |
|
348 }; |
|
349 Services.tm.currentThread.dispatch(runnable, |
|
350 Ci.nsIThread.DISPATCH_NORMAL); |
|
351 return request; |
|
352 } |
|
353 |
|
354 smsService.send(this._settings.paymentServiceId, aNumber, aMessage, true, |
|
355 request); |
|
356 return request; |
|
357 }, |
|
358 |
|
359 observeSilentSms: function observeSilentSms(aNumber, aCallback) { |
|
360 if (_debug) { |
|
361 LOG("observeSilentSms " + aNumber); |
|
362 } |
|
363 |
|
364 if (!this._silentSmsObservers) { |
|
365 this._silentSmsObservers = {}; |
|
366 this._silentNumbers = []; |
|
367 Services.obs.addObserver(this._onSilentSms.bind(this), |
|
368 kSilentSmsReceivedTopic, |
|
369 false); |
|
370 } |
|
371 |
|
372 if (!this._silentSmsObservers[aNumber]) { |
|
373 this._silentSmsObservers[aNumber] = []; |
|
374 this._silentNumbers.push(aNumber); |
|
375 smsService.addSilentNumber(aNumber); |
|
376 } |
|
377 |
|
378 if (this._silentSmsObservers[aNumber].indexOf(aCallback) == -1) { |
|
379 this._silentSmsObservers[aNumber].push(aCallback); |
|
380 } |
|
381 }, |
|
382 |
|
383 removeSilentSmsObserver: function removeSilentSmsObserver(aNumber, aCallback) { |
|
384 if (_debug) { |
|
385 LOG("removeSilentSmsObserver " + aNumber); |
|
386 } |
|
387 |
|
388 if (!this._silentSmsObservers || !this._silentSmsObservers[aNumber]) { |
|
389 if (_debug) { |
|
390 LOG("No observers for " + aNumber); |
|
391 } |
|
392 return; |
|
393 } |
|
394 |
|
395 let index = this._silentSmsObservers[aNumber].indexOf(aCallback); |
|
396 if (index != -1) { |
|
397 this._silentSmsObservers[aNumber].splice(index, 1); |
|
398 if (this._silentSmsObservers[aNumber].length == 0) { |
|
399 this._silentSmsObservers[aNumber] = null; |
|
400 this._silentNumbers.splice(this._silentNumbers.indexOf(aNumber), 1); |
|
401 smsService.removeSilentNumber(aNumber); |
|
402 } |
|
403 } else if (_debug) { |
|
404 LOG("No callback found for " + aNumber); |
|
405 } |
|
406 }, |
|
407 |
|
408 _onSilentSms: function _onSilentSms(aSubject, aTopic, aData) { |
|
409 if (_debug) { |
|
410 LOG("Got silent message! " + aSubject.sender + " - " + aSubject.body); |
|
411 } |
|
412 |
|
413 let number = aSubject.sender; |
|
414 if (!number || this._silentNumbers.indexOf(number) == -1) { |
|
415 if (_debug) { |
|
416 LOG("No observers for " + number); |
|
417 } |
|
418 return; |
|
419 } |
|
420 |
|
421 // If the service ID is null it means that the payment provider asked the |
|
422 // user for her MSISDN, so we are in a MT only SMS auth flow. In this case |
|
423 // we manually set the service ID to the one corresponding with the SIM |
|
424 // that received the SMS. |
|
425 if (this._settings.paymentServiceId === null) { |
|
426 let i = 0; |
|
427 while(i < gRil.numRadioInterfaces) { |
|
428 if (this.iccInfo[i].iccId === aSubject.iccId) { |
|
429 this._settings.paymentServiceId = i; |
|
430 break; |
|
431 } |
|
432 i++; |
|
433 } |
|
434 } |
|
435 |
|
436 this._silentSmsObservers[number].forEach(function(callback) { |
|
437 callback(aSubject); |
|
438 }); |
|
439 }, |
|
440 |
|
441 _cleanUp: function _cleanUp() { |
|
442 if (_debug) { |
|
443 LOG("Cleaning up!"); |
|
444 } |
|
445 |
|
446 if (!this._silentNumbers) { |
|
447 return; |
|
448 } |
|
449 |
|
450 while (this._silentNumbers.length) { |
|
451 let number = this._silentNumbers.pop(); |
|
452 smsService.removeSilentNumber(number); |
|
453 } |
|
454 this._silentNumbers = null; |
|
455 this._silentSmsObservers = null; |
|
456 this._settings.cleanup(); |
|
457 Services.obs.removeObserver(this._onSilentSms, kSilentSmsReceivedTopic); |
|
458 } |
|
459 #endif |
|
460 }; |
|
461 |
|
462 // We save the identifier of the DOM request, so we can dispatch the results |
|
463 // of the payment flow to the appropriate content process. |
|
464 addMessageListener("Payment:LoadShim", function receiveMessage(aMessage) { |
|
465 gRequestId = aMessage.json.requestId; |
|
466 PaymentProvider._init(); |
|
467 }); |
|
468 |
|
469 addEventListener("DOMWindowCreated", function(e) { |
|
470 content.wrappedJSObject.mozPaymentProvider = PaymentProvider; |
|
471 }); |
|
472 |
|
473 #ifdef MOZ_B2G_RIL |
|
474 // If the trusted dialog is not closed via paymentSuccess or paymentFailed |
|
475 // a mozContentEvent with type 'cancel' is sent from the UI. We need to listen |
|
476 // for this event to clean up the silent sms observers if any exists. |
|
477 SystemAppProxy.addEventListener("mozContentEvent", function(e) { |
|
478 if (e.detail.type === "cancel") { |
|
479 PaymentProvider._cleanUp(); |
|
480 } |
|
481 }); |
|
482 #endif |