1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/b2g/chrome/content/payment.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,482 @@ 1.4 +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- / 1.5 +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.8 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +// This JS shim contains the callbacks to fire DOMRequest events for 1.11 +// navigator.pay API within the payment processor's scope. 1.12 + 1.13 +"use strict"; 1.14 + 1.15 +let { classes: Cc, interfaces: Ci, utils: Cu } = Components; 1.16 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.17 +Cu.import("resource://gre/modules/Services.jsm"); 1.18 + 1.19 +const PREF_DEBUG = "dom.payment.debug"; 1.20 + 1.21 +let _debug; 1.22 +try { 1.23 + _debug = Services.prefs.getPrefType(PREF_DEBUG) == Ci.nsIPrefBranch.PREF_BOOL 1.24 + && Services.prefs.getBoolPref(PREF_DEBUG); 1.25 +} catch(e){ 1.26 + _debug = false; 1.27 +} 1.28 + 1.29 +function LOG(s) { 1.30 + if (!_debug) { 1.31 + return; 1.32 + } 1.33 + dump("== Payment flow == " + s + "\n"); 1.34 +} 1.35 + 1.36 +function LOGE(s) { 1.37 + dump("== Payment flow ERROR == " + s + "\n"); 1.38 +} 1.39 + 1.40 +if (_debug) { 1.41 + LOG("Frame script injected"); 1.42 +} 1.43 + 1.44 +XPCOMUtils.defineLazyServiceGetter(this, "cpmm", 1.45 + "@mozilla.org/childprocessmessagemanager;1", 1.46 + "nsIMessageSender"); 1.47 + 1.48 +XPCOMUtils.defineLazyServiceGetter(this, "uuidgen", 1.49 + "@mozilla.org/uuid-generator;1", 1.50 + "nsIUUIDGenerator"); 1.51 + 1.52 +XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy", 1.53 + "resource://gre/modules/SystemAppProxy.jsm"); 1.54 + 1.55 +#ifdef MOZ_B2G_RIL 1.56 +XPCOMUtils.defineLazyServiceGetter(this, "gRil", 1.57 + "@mozilla.org/ril;1", 1.58 + "nsIRadioInterfaceLayer"); 1.59 + 1.60 +XPCOMUtils.defineLazyServiceGetter(this, "iccProvider", 1.61 + "@mozilla.org/ril/content-helper;1", 1.62 + "nsIIccProvider"); 1.63 + 1.64 +XPCOMUtils.defineLazyServiceGetter(this, "smsService", 1.65 + "@mozilla.org/sms/smsservice;1", 1.66 + "nsISmsService"); 1.67 + 1.68 +XPCOMUtils.defineLazyServiceGetter(this, "gSettingsService", 1.69 + "@mozilla.org/settingsService;1", 1.70 + "nsISettingsService"); 1.71 + 1.72 +const kSilentSmsReceivedTopic = "silent-sms-received"; 1.73 +const kMozSettingsChangedObserverTopic = "mozsettings-changed"; 1.74 + 1.75 +const kRilDefaultDataServiceId = "ril.data.defaultServiceId"; 1.76 +const kRilDefaultPaymentServiceId = "ril.payment.defaultServiceId"; 1.77 + 1.78 +const MOBILEMESSAGECALLBACK_CID = 1.79 + Components.ID("{b484d8c9-6be4-4f94-ab60-c9c7ebcc853d}"); 1.80 + 1.81 +// In order to send messages through nsISmsService, we need to implement 1.82 +// nsIMobileMessageCallback, as the WebSMS API implementation is not usable 1.83 +// from JS. 1.84 +function SilentSmsRequest() { 1.85 +} 1.86 + 1.87 +SilentSmsRequest.prototype = { 1.88 + __exposedProps__: { 1.89 + onsuccess: "rw", 1.90 + onerror: "rw" 1.91 + }, 1.92 + 1.93 + QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileMessageCallback]), 1.94 + 1.95 + classID: MOBILEMESSAGECALLBACK_CID, 1.96 + 1.97 + set onsuccess(aSuccessCallback) { 1.98 + this._onsuccess = aSuccessCallback; 1.99 + }, 1.100 + 1.101 + set onerror(aErrorCallback) { 1.102 + this._onerror = aErrorCallback; 1.103 + }, 1.104 + 1.105 + notifyMessageSent: function notifyMessageSent(aMessage) { 1.106 + if (_debug) { 1.107 + LOG("Silent message successfully sent"); 1.108 + } 1.109 + this._onsuccess(aMessage); 1.110 + }, 1.111 + 1.112 + notifySendMessageFailed: function notifySendMessageFailed(aError) { 1.113 + LOGE("Error sending silent message " + aError); 1.114 + this._onerror(aError); 1.115 + } 1.116 +}; 1.117 + 1.118 +function PaymentSettings() { 1.119 + Services.obs.addObserver(this, kMozSettingsChangedObserverTopic, false); 1.120 + 1.121 + [kRilDefaultDataServiceId, kRilDefaultPaymentServiceId].forEach(setting => { 1.122 + gSettingsService.createLock().get(setting, this); 1.123 + }); 1.124 +} 1.125 + 1.126 +PaymentSettings.prototype = { 1.127 + QueryInterface: XPCOMUtils.generateQI([Ci.nsISettingsServiceCallback, 1.128 + Ci.nsIObserver]), 1.129 + 1.130 + dataServiceId: 0, 1.131 + _paymentServiceId: 0, 1.132 + 1.133 + get paymentServiceId() { 1.134 + return this._paymentServiceId; 1.135 + }, 1.136 + 1.137 + set paymentServiceId(serviceId) { 1.138 + // We allow the payment provider to set the service ID that will be used 1.139 + // for the payment process. 1.140 + // This service ID will be the one used by the silent SMS flow. 1.141 + // If the payment is done with an external SIM, the service ID must be set 1.142 + // to null. 1.143 + if (serviceId != null && serviceId >= gRil.numRadioInterfaces) { 1.144 + LOGE("Invalid service ID " + serviceId); 1.145 + return; 1.146 + } 1.147 + 1.148 + gSettingsService.createLock().set(kRilDefaultPaymentServiceId, 1.149 + serviceId, null); 1.150 + this._paymentServiceId = serviceId; 1.151 + }, 1.152 + 1.153 + setServiceId: function(aName, aValue) { 1.154 + switch (aName) { 1.155 + case kRilDefaultDataServiceId: 1.156 + this.dataServiceId = aValue; 1.157 + if (_debug) { 1.158 + LOG("dataServiceId " + this.dataServiceId); 1.159 + } 1.160 + break; 1.161 + case kRilDefaultPaymentServiceId: 1.162 + this._paymentServiceId = aValue; 1.163 + if (_debug) { 1.164 + LOG("paymentServiceId " + this._paymentServiceId); 1.165 + } 1.166 + break; 1.167 + } 1.168 + }, 1.169 + 1.170 + handle: function(aName, aValue) { 1.171 + if (aName != kRilDefaultDataServiceId) { 1.172 + return; 1.173 + } 1.174 + 1.175 + this.setServiceId(aName, aValue); 1.176 + }, 1.177 + 1.178 + observe: function(aSubject, aTopic, aData) { 1.179 + if (aTopic != kMozSettingsChangedObserverTopic) { 1.180 + return; 1.181 + } 1.182 + 1.183 + try { 1.184 + let setting = JSON.parse(aData); 1.185 + if (!setting.key || 1.186 + (setting.key !== kRilDefaultDataServiceId && 1.187 + setting.key !== kRilDefaultPaymentServiceId)) { 1.188 + return; 1.189 + } 1.190 + this.setServiceId(setting.key, setting.value); 1.191 + } catch (e) { 1.192 + LOGE(e); 1.193 + } 1.194 + }, 1.195 + 1.196 + cleanup: function() { 1.197 + Services.obs.removeObserver(this, kMozSettingsChangedObserverTopic); 1.198 + } 1.199 +}; 1.200 +#endif 1.201 + 1.202 +const kClosePaymentFlowEvent = "close-payment-flow-dialog"; 1.203 + 1.204 +let gRequestId; 1.205 + 1.206 +let PaymentProvider = { 1.207 +#ifdef MOZ_B2G_RIL 1.208 + __exposedProps__: { 1.209 + paymentSuccess: "r", 1.210 + paymentFailed: "r", 1.211 + paymentServiceId: "rw", 1.212 + iccInfo: "r", 1.213 + sendSilentSms: "r", 1.214 + observeSilentSms: "r", 1.215 + removeSilentSmsObserver: "r" 1.216 + }, 1.217 +#else 1.218 + __exposedProps__: { 1.219 + paymentSuccess: "r", 1.220 + paymentFailed: "r" 1.221 + }, 1.222 +#endif 1.223 + 1.224 + _init: function _init() { 1.225 +#ifdef MOZ_B2G_RIL 1.226 + this._settings = new PaymentSettings(); 1.227 +#endif 1.228 + }, 1.229 + 1.230 + _closePaymentFlowDialog: function _closePaymentFlowDialog(aCallback) { 1.231 + // After receiving the payment provider confirmation about the 1.232 + // successful or failed payment flow, we notify the UI to close the 1.233 + // payment flow dialog and return to the caller application. 1.234 + let id = kClosePaymentFlowEvent + "-" + uuidgen.generateUUID().toString(); 1.235 + 1.236 + let detail = { 1.237 + type: kClosePaymentFlowEvent, 1.238 + id: id, 1.239 + requestId: gRequestId 1.240 + }; 1.241 + 1.242 + // In order to avoid race conditions, we wait for the UI to notify that 1.243 + // it has successfully closed the payment flow and has recovered the 1.244 + // caller app, before notifying the parent process to fire the success 1.245 + // or error event over the DOMRequest. 1.246 + SystemAppProxy.addEventListener("mozContentEvent", 1.247 + function closePaymentFlowReturn(evt) { 1.248 + if (evt.detail.id == id && aCallback) { 1.249 + aCallback(); 1.250 + } 1.251 + 1.252 + SystemAppProxy.removeEventListener("mozContentEvent", 1.253 + closePaymentFlowReturn); 1.254 + 1.255 + let glue = Cc["@mozilla.org/payment/ui-glue;1"] 1.256 + .createInstance(Ci.nsIPaymentUIGlue); 1.257 + glue.cleanup(); 1.258 + }); 1.259 + 1.260 + SystemAppProxy.dispatchEvent(detail); 1.261 + 1.262 +#ifdef MOZ_B2G_RIL 1.263 + this._cleanUp(); 1.264 +#endif 1.265 + }, 1.266 + 1.267 + paymentSuccess: function paymentSuccess(aResult) { 1.268 + if (_debug) { 1.269 + LOG("paymentSuccess " + aResult); 1.270 + } 1.271 + 1.272 + PaymentProvider._closePaymentFlowDialog(function notifySuccess() { 1.273 + if (!gRequestId) { 1.274 + return; 1.275 + } 1.276 + cpmm.sendAsyncMessage("Payment:Success", { result: aResult, 1.277 + requestId: gRequestId }); 1.278 + }); 1.279 + }, 1.280 + 1.281 + paymentFailed: function paymentFailed(aErrorMsg) { 1.282 + LOGE("paymentFailed " + aErrorMsg); 1.283 + 1.284 + PaymentProvider._closePaymentFlowDialog(function notifyError() { 1.285 + if (!gRequestId) { 1.286 + return; 1.287 + } 1.288 + cpmm.sendAsyncMessage("Payment:Failed", { errorMsg: aErrorMsg, 1.289 + requestId: gRequestId }); 1.290 + }); 1.291 + }, 1.292 + 1.293 +#ifdef MOZ_B2G_RIL 1.294 + get paymentServiceId() { 1.295 + return this._settings.paymentServiceId; 1.296 + }, 1.297 + 1.298 + set paymentServiceId(serviceId) { 1.299 + this._settings.paymentServiceId = serviceId; 1.300 + }, 1.301 + 1.302 + // We expose to the payment provider the information of all the SIMs 1.303 + // available in the device. iccInfo is an object of this form: 1.304 + // { 1.305 + // "serviceId1": { 1.306 + // mcc: <string>, 1.307 + // mnc: <string>, 1.308 + // iccId: <string>, 1.309 + // dataPrimary: <boolean> 1.310 + // }, 1.311 + // "serviceIdN": {...} 1.312 + // } 1.313 + get iccInfo() { 1.314 + if (!this._iccInfo) { 1.315 + this._iccInfo = {}; 1.316 + for (let i = 0; i < gRil.numRadioInterfaces; i++) { 1.317 + let info = iccProvider.getIccInfo(i); 1.318 + if (!info) { 1.319 + LOGE("Tried to get the ICC info for an invalid service ID " + i); 1.320 + continue; 1.321 + } 1.322 + 1.323 + this._iccInfo[i] = { 1.324 + iccId: info.iccid, 1.325 + mcc: info.mcc, 1.326 + mnc: info.mnc, 1.327 + dataPrimary: i == this._settings.dataServiceId 1.328 + }; 1.329 + } 1.330 + } 1.331 + 1.332 + return Cu.cloneInto(this._iccInfo, content); 1.333 + }, 1.334 + 1.335 + _silentNumbers: null, 1.336 + _silentSmsObservers: null, 1.337 + 1.338 + sendSilentSms: function sendSilentSms(aNumber, aMessage) { 1.339 + if (_debug) { 1.340 + LOG("Sending silent message " + aNumber + " - " + aMessage); 1.341 + } 1.342 + 1.343 + let request = new SilentSmsRequest(); 1.344 + 1.345 + if (this._settings.paymentServiceId === null) { 1.346 + LOGE("No payment service ID set. Cannot send silent SMS"); 1.347 + let runnable = { 1.348 + run: function run() { 1.349 + request.notifySendMessageFailed("NO_PAYMENT_SERVICE_ID"); 1.350 + } 1.351 + }; 1.352 + Services.tm.currentThread.dispatch(runnable, 1.353 + Ci.nsIThread.DISPATCH_NORMAL); 1.354 + return request; 1.355 + } 1.356 + 1.357 + smsService.send(this._settings.paymentServiceId, aNumber, aMessage, true, 1.358 + request); 1.359 + return request; 1.360 + }, 1.361 + 1.362 + observeSilentSms: function observeSilentSms(aNumber, aCallback) { 1.363 + if (_debug) { 1.364 + LOG("observeSilentSms " + aNumber); 1.365 + } 1.366 + 1.367 + if (!this._silentSmsObservers) { 1.368 + this._silentSmsObservers = {}; 1.369 + this._silentNumbers = []; 1.370 + Services.obs.addObserver(this._onSilentSms.bind(this), 1.371 + kSilentSmsReceivedTopic, 1.372 + false); 1.373 + } 1.374 + 1.375 + if (!this._silentSmsObservers[aNumber]) { 1.376 + this._silentSmsObservers[aNumber] = []; 1.377 + this._silentNumbers.push(aNumber); 1.378 + smsService.addSilentNumber(aNumber); 1.379 + } 1.380 + 1.381 + if (this._silentSmsObservers[aNumber].indexOf(aCallback) == -1) { 1.382 + this._silentSmsObservers[aNumber].push(aCallback); 1.383 + } 1.384 + }, 1.385 + 1.386 + removeSilentSmsObserver: function removeSilentSmsObserver(aNumber, aCallback) { 1.387 + if (_debug) { 1.388 + LOG("removeSilentSmsObserver " + aNumber); 1.389 + } 1.390 + 1.391 + if (!this._silentSmsObservers || !this._silentSmsObservers[aNumber]) { 1.392 + if (_debug) { 1.393 + LOG("No observers for " + aNumber); 1.394 + } 1.395 + return; 1.396 + } 1.397 + 1.398 + let index = this._silentSmsObservers[aNumber].indexOf(aCallback); 1.399 + if (index != -1) { 1.400 + this._silentSmsObservers[aNumber].splice(index, 1); 1.401 + if (this._silentSmsObservers[aNumber].length == 0) { 1.402 + this._silentSmsObservers[aNumber] = null; 1.403 + this._silentNumbers.splice(this._silentNumbers.indexOf(aNumber), 1); 1.404 + smsService.removeSilentNumber(aNumber); 1.405 + } 1.406 + } else if (_debug) { 1.407 + LOG("No callback found for " + aNumber); 1.408 + } 1.409 + }, 1.410 + 1.411 + _onSilentSms: function _onSilentSms(aSubject, aTopic, aData) { 1.412 + if (_debug) { 1.413 + LOG("Got silent message! " + aSubject.sender + " - " + aSubject.body); 1.414 + } 1.415 + 1.416 + let number = aSubject.sender; 1.417 + if (!number || this._silentNumbers.indexOf(number) == -1) { 1.418 + if (_debug) { 1.419 + LOG("No observers for " + number); 1.420 + } 1.421 + return; 1.422 + } 1.423 + 1.424 + // If the service ID is null it means that the payment provider asked the 1.425 + // user for her MSISDN, so we are in a MT only SMS auth flow. In this case 1.426 + // we manually set the service ID to the one corresponding with the SIM 1.427 + // that received the SMS. 1.428 + if (this._settings.paymentServiceId === null) { 1.429 + let i = 0; 1.430 + while(i < gRil.numRadioInterfaces) { 1.431 + if (this.iccInfo[i].iccId === aSubject.iccId) { 1.432 + this._settings.paymentServiceId = i; 1.433 + break; 1.434 + } 1.435 + i++; 1.436 + } 1.437 + } 1.438 + 1.439 + this._silentSmsObservers[number].forEach(function(callback) { 1.440 + callback(aSubject); 1.441 + }); 1.442 + }, 1.443 + 1.444 + _cleanUp: function _cleanUp() { 1.445 + if (_debug) { 1.446 + LOG("Cleaning up!"); 1.447 + } 1.448 + 1.449 + if (!this._silentNumbers) { 1.450 + return; 1.451 + } 1.452 + 1.453 + while (this._silentNumbers.length) { 1.454 + let number = this._silentNumbers.pop(); 1.455 + smsService.removeSilentNumber(number); 1.456 + } 1.457 + this._silentNumbers = null; 1.458 + this._silentSmsObservers = null; 1.459 + this._settings.cleanup(); 1.460 + Services.obs.removeObserver(this._onSilentSms, kSilentSmsReceivedTopic); 1.461 + } 1.462 +#endif 1.463 +}; 1.464 + 1.465 +// We save the identifier of the DOM request, so we can dispatch the results 1.466 +// of the payment flow to the appropriate content process. 1.467 +addMessageListener("Payment:LoadShim", function receiveMessage(aMessage) { 1.468 + gRequestId = aMessage.json.requestId; 1.469 + PaymentProvider._init(); 1.470 +}); 1.471 + 1.472 +addEventListener("DOMWindowCreated", function(e) { 1.473 + content.wrappedJSObject.mozPaymentProvider = PaymentProvider; 1.474 +}); 1.475 + 1.476 +#ifdef MOZ_B2G_RIL 1.477 +// If the trusted dialog is not closed via paymentSuccess or paymentFailed 1.478 +// a mozContentEvent with type 'cancel' is sent from the UI. We need to listen 1.479 +// for this event to clean up the silent sms observers if any exists. 1.480 +SystemAppProxy.addEventListener("mozContentEvent", function(e) { 1.481 + if (e.detail.type === "cancel") { 1.482 + PaymentProvider._cleanUp(); 1.483 + } 1.484 +}); 1.485 +#endif