michael@0: /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- / michael@0: /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: // This JS shim contains the callbacks to fire DOMRequest events for michael@0: // navigator.pay API within the payment processor's scope. michael@0: michael@0: "use strict"; michael@0: michael@0: let { classes: Cc, interfaces: Ci, utils: Cu } = Components; michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: const PREF_DEBUG = "dom.payment.debug"; michael@0: michael@0: let _debug; michael@0: try { michael@0: _debug = Services.prefs.getPrefType(PREF_DEBUG) == Ci.nsIPrefBranch.PREF_BOOL michael@0: && Services.prefs.getBoolPref(PREF_DEBUG); michael@0: } catch(e){ michael@0: _debug = false; michael@0: } michael@0: michael@0: function LOG(s) { michael@0: if (!_debug) { michael@0: return; michael@0: } michael@0: dump("== Payment flow == " + s + "\n"); michael@0: } michael@0: michael@0: function LOGE(s) { michael@0: dump("== Payment flow ERROR == " + s + "\n"); michael@0: } michael@0: michael@0: if (_debug) { michael@0: LOG("Frame script injected"); michael@0: } michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "cpmm", michael@0: "@mozilla.org/childprocessmessagemanager;1", michael@0: "nsIMessageSender"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "uuidgen", michael@0: "@mozilla.org/uuid-generator;1", michael@0: "nsIUUIDGenerator"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy", michael@0: "resource://gre/modules/SystemAppProxy.jsm"); michael@0: michael@0: #ifdef MOZ_B2G_RIL michael@0: XPCOMUtils.defineLazyServiceGetter(this, "gRil", michael@0: "@mozilla.org/ril;1", michael@0: "nsIRadioInterfaceLayer"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "iccProvider", michael@0: "@mozilla.org/ril/content-helper;1", michael@0: "nsIIccProvider"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "smsService", michael@0: "@mozilla.org/sms/smsservice;1", michael@0: "nsISmsService"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "gSettingsService", michael@0: "@mozilla.org/settingsService;1", michael@0: "nsISettingsService"); michael@0: michael@0: const kSilentSmsReceivedTopic = "silent-sms-received"; michael@0: const kMozSettingsChangedObserverTopic = "mozsettings-changed"; michael@0: michael@0: const kRilDefaultDataServiceId = "ril.data.defaultServiceId"; michael@0: const kRilDefaultPaymentServiceId = "ril.payment.defaultServiceId"; michael@0: michael@0: const MOBILEMESSAGECALLBACK_CID = michael@0: Components.ID("{b484d8c9-6be4-4f94-ab60-c9c7ebcc853d}"); michael@0: michael@0: // In order to send messages through nsISmsService, we need to implement michael@0: // nsIMobileMessageCallback, as the WebSMS API implementation is not usable michael@0: // from JS. michael@0: function SilentSmsRequest() { michael@0: } michael@0: michael@0: SilentSmsRequest.prototype = { michael@0: __exposedProps__: { michael@0: onsuccess: "rw", michael@0: onerror: "rw" michael@0: }, michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileMessageCallback]), michael@0: michael@0: classID: MOBILEMESSAGECALLBACK_CID, michael@0: michael@0: set onsuccess(aSuccessCallback) { michael@0: this._onsuccess = aSuccessCallback; michael@0: }, michael@0: michael@0: set onerror(aErrorCallback) { michael@0: this._onerror = aErrorCallback; michael@0: }, michael@0: michael@0: notifyMessageSent: function notifyMessageSent(aMessage) { michael@0: if (_debug) { michael@0: LOG("Silent message successfully sent"); michael@0: } michael@0: this._onsuccess(aMessage); michael@0: }, michael@0: michael@0: notifySendMessageFailed: function notifySendMessageFailed(aError) { michael@0: LOGE("Error sending silent message " + aError); michael@0: this._onerror(aError); michael@0: } michael@0: }; michael@0: michael@0: function PaymentSettings() { michael@0: Services.obs.addObserver(this, kMozSettingsChangedObserverTopic, false); michael@0: michael@0: [kRilDefaultDataServiceId, kRilDefaultPaymentServiceId].forEach(setting => { michael@0: gSettingsService.createLock().get(setting, this); michael@0: }); michael@0: } michael@0: michael@0: PaymentSettings.prototype = { michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsISettingsServiceCallback, michael@0: Ci.nsIObserver]), michael@0: michael@0: dataServiceId: 0, michael@0: _paymentServiceId: 0, michael@0: michael@0: get paymentServiceId() { michael@0: return this._paymentServiceId; michael@0: }, michael@0: michael@0: set paymentServiceId(serviceId) { michael@0: // We allow the payment provider to set the service ID that will be used michael@0: // for the payment process. michael@0: // This service ID will be the one used by the silent SMS flow. michael@0: // If the payment is done with an external SIM, the service ID must be set michael@0: // to null. michael@0: if (serviceId != null && serviceId >= gRil.numRadioInterfaces) { michael@0: LOGE("Invalid service ID " + serviceId); michael@0: return; michael@0: } michael@0: michael@0: gSettingsService.createLock().set(kRilDefaultPaymentServiceId, michael@0: serviceId, null); michael@0: this._paymentServiceId = serviceId; michael@0: }, michael@0: michael@0: setServiceId: function(aName, aValue) { michael@0: switch (aName) { michael@0: case kRilDefaultDataServiceId: michael@0: this.dataServiceId = aValue; michael@0: if (_debug) { michael@0: LOG("dataServiceId " + this.dataServiceId); michael@0: } michael@0: break; michael@0: case kRilDefaultPaymentServiceId: michael@0: this._paymentServiceId = aValue; michael@0: if (_debug) { michael@0: LOG("paymentServiceId " + this._paymentServiceId); michael@0: } michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: handle: function(aName, aValue) { michael@0: if (aName != kRilDefaultDataServiceId) { michael@0: return; michael@0: } michael@0: michael@0: this.setServiceId(aName, aValue); michael@0: }, michael@0: michael@0: observe: function(aSubject, aTopic, aData) { michael@0: if (aTopic != kMozSettingsChangedObserverTopic) { michael@0: return; michael@0: } michael@0: michael@0: try { michael@0: let setting = JSON.parse(aData); michael@0: if (!setting.key || michael@0: (setting.key !== kRilDefaultDataServiceId && michael@0: setting.key !== kRilDefaultPaymentServiceId)) { michael@0: return; michael@0: } michael@0: this.setServiceId(setting.key, setting.value); michael@0: } catch (e) { michael@0: LOGE(e); michael@0: } michael@0: }, michael@0: michael@0: cleanup: function() { michael@0: Services.obs.removeObserver(this, kMozSettingsChangedObserverTopic); michael@0: } michael@0: }; michael@0: #endif michael@0: michael@0: const kClosePaymentFlowEvent = "close-payment-flow-dialog"; michael@0: michael@0: let gRequestId; michael@0: michael@0: let PaymentProvider = { michael@0: #ifdef MOZ_B2G_RIL michael@0: __exposedProps__: { michael@0: paymentSuccess: "r", michael@0: paymentFailed: "r", michael@0: paymentServiceId: "rw", michael@0: iccInfo: "r", michael@0: sendSilentSms: "r", michael@0: observeSilentSms: "r", michael@0: removeSilentSmsObserver: "r" michael@0: }, michael@0: #else michael@0: __exposedProps__: { michael@0: paymentSuccess: "r", michael@0: paymentFailed: "r" michael@0: }, michael@0: #endif michael@0: michael@0: _init: function _init() { michael@0: #ifdef MOZ_B2G_RIL michael@0: this._settings = new PaymentSettings(); michael@0: #endif michael@0: }, michael@0: michael@0: _closePaymentFlowDialog: function _closePaymentFlowDialog(aCallback) { michael@0: // After receiving the payment provider confirmation about the michael@0: // successful or failed payment flow, we notify the UI to close the michael@0: // payment flow dialog and return to the caller application. michael@0: let id = kClosePaymentFlowEvent + "-" + uuidgen.generateUUID().toString(); michael@0: michael@0: let detail = { michael@0: type: kClosePaymentFlowEvent, michael@0: id: id, michael@0: requestId: gRequestId michael@0: }; michael@0: michael@0: // In order to avoid race conditions, we wait for the UI to notify that michael@0: // it has successfully closed the payment flow and has recovered the michael@0: // caller app, before notifying the parent process to fire the success michael@0: // or error event over the DOMRequest. michael@0: SystemAppProxy.addEventListener("mozContentEvent", michael@0: function closePaymentFlowReturn(evt) { michael@0: if (evt.detail.id == id && aCallback) { michael@0: aCallback(); michael@0: } michael@0: michael@0: SystemAppProxy.removeEventListener("mozContentEvent", michael@0: closePaymentFlowReturn); michael@0: michael@0: let glue = Cc["@mozilla.org/payment/ui-glue;1"] michael@0: .createInstance(Ci.nsIPaymentUIGlue); michael@0: glue.cleanup(); michael@0: }); michael@0: michael@0: SystemAppProxy.dispatchEvent(detail); michael@0: michael@0: #ifdef MOZ_B2G_RIL michael@0: this._cleanUp(); michael@0: #endif michael@0: }, michael@0: michael@0: paymentSuccess: function paymentSuccess(aResult) { michael@0: if (_debug) { michael@0: LOG("paymentSuccess " + aResult); michael@0: } michael@0: michael@0: PaymentProvider._closePaymentFlowDialog(function notifySuccess() { michael@0: if (!gRequestId) { michael@0: return; michael@0: } michael@0: cpmm.sendAsyncMessage("Payment:Success", { result: aResult, michael@0: requestId: gRequestId }); michael@0: }); michael@0: }, michael@0: michael@0: paymentFailed: function paymentFailed(aErrorMsg) { michael@0: LOGE("paymentFailed " + aErrorMsg); michael@0: michael@0: PaymentProvider._closePaymentFlowDialog(function notifyError() { michael@0: if (!gRequestId) { michael@0: return; michael@0: } michael@0: cpmm.sendAsyncMessage("Payment:Failed", { errorMsg: aErrorMsg, michael@0: requestId: gRequestId }); michael@0: }); michael@0: }, michael@0: michael@0: #ifdef MOZ_B2G_RIL michael@0: get paymentServiceId() { michael@0: return this._settings.paymentServiceId; michael@0: }, michael@0: michael@0: set paymentServiceId(serviceId) { michael@0: this._settings.paymentServiceId = serviceId; michael@0: }, michael@0: michael@0: // We expose to the payment provider the information of all the SIMs michael@0: // available in the device. iccInfo is an object of this form: michael@0: // { michael@0: // "serviceId1": { michael@0: // mcc: , michael@0: // mnc: , michael@0: // iccId: , michael@0: // dataPrimary: michael@0: // }, michael@0: // "serviceIdN": {...} michael@0: // } michael@0: get iccInfo() { michael@0: if (!this._iccInfo) { michael@0: this._iccInfo = {}; michael@0: for (let i = 0; i < gRil.numRadioInterfaces; i++) { michael@0: let info = iccProvider.getIccInfo(i); michael@0: if (!info) { michael@0: LOGE("Tried to get the ICC info for an invalid service ID " + i); michael@0: continue; michael@0: } michael@0: michael@0: this._iccInfo[i] = { michael@0: iccId: info.iccid, michael@0: mcc: info.mcc, michael@0: mnc: info.mnc, michael@0: dataPrimary: i == this._settings.dataServiceId michael@0: }; michael@0: } michael@0: } michael@0: michael@0: return Cu.cloneInto(this._iccInfo, content); michael@0: }, michael@0: michael@0: _silentNumbers: null, michael@0: _silentSmsObservers: null, michael@0: michael@0: sendSilentSms: function sendSilentSms(aNumber, aMessage) { michael@0: if (_debug) { michael@0: LOG("Sending silent message " + aNumber + " - " + aMessage); michael@0: } michael@0: michael@0: let request = new SilentSmsRequest(); michael@0: michael@0: if (this._settings.paymentServiceId === null) { michael@0: LOGE("No payment service ID set. Cannot send silent SMS"); michael@0: let runnable = { michael@0: run: function run() { michael@0: request.notifySendMessageFailed("NO_PAYMENT_SERVICE_ID"); michael@0: } michael@0: }; michael@0: Services.tm.currentThread.dispatch(runnable, michael@0: Ci.nsIThread.DISPATCH_NORMAL); michael@0: return request; michael@0: } michael@0: michael@0: smsService.send(this._settings.paymentServiceId, aNumber, aMessage, true, michael@0: request); michael@0: return request; michael@0: }, michael@0: michael@0: observeSilentSms: function observeSilentSms(aNumber, aCallback) { michael@0: if (_debug) { michael@0: LOG("observeSilentSms " + aNumber); michael@0: } michael@0: michael@0: if (!this._silentSmsObservers) { michael@0: this._silentSmsObservers = {}; michael@0: this._silentNumbers = []; michael@0: Services.obs.addObserver(this._onSilentSms.bind(this), michael@0: kSilentSmsReceivedTopic, michael@0: false); michael@0: } michael@0: michael@0: if (!this._silentSmsObservers[aNumber]) { michael@0: this._silentSmsObservers[aNumber] = []; michael@0: this._silentNumbers.push(aNumber); michael@0: smsService.addSilentNumber(aNumber); michael@0: } michael@0: michael@0: if (this._silentSmsObservers[aNumber].indexOf(aCallback) == -1) { michael@0: this._silentSmsObservers[aNumber].push(aCallback); michael@0: } michael@0: }, michael@0: michael@0: removeSilentSmsObserver: function removeSilentSmsObserver(aNumber, aCallback) { michael@0: if (_debug) { michael@0: LOG("removeSilentSmsObserver " + aNumber); michael@0: } michael@0: michael@0: if (!this._silentSmsObservers || !this._silentSmsObservers[aNumber]) { michael@0: if (_debug) { michael@0: LOG("No observers for " + aNumber); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: let index = this._silentSmsObservers[aNumber].indexOf(aCallback); michael@0: if (index != -1) { michael@0: this._silentSmsObservers[aNumber].splice(index, 1); michael@0: if (this._silentSmsObservers[aNumber].length == 0) { michael@0: this._silentSmsObservers[aNumber] = null; michael@0: this._silentNumbers.splice(this._silentNumbers.indexOf(aNumber), 1); michael@0: smsService.removeSilentNumber(aNumber); michael@0: } michael@0: } else if (_debug) { michael@0: LOG("No callback found for " + aNumber); michael@0: } michael@0: }, michael@0: michael@0: _onSilentSms: function _onSilentSms(aSubject, aTopic, aData) { michael@0: if (_debug) { michael@0: LOG("Got silent message! " + aSubject.sender + " - " + aSubject.body); michael@0: } michael@0: michael@0: let number = aSubject.sender; michael@0: if (!number || this._silentNumbers.indexOf(number) == -1) { michael@0: if (_debug) { michael@0: LOG("No observers for " + number); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: // If the service ID is null it means that the payment provider asked the michael@0: // user for her MSISDN, so we are in a MT only SMS auth flow. In this case michael@0: // we manually set the service ID to the one corresponding with the SIM michael@0: // that received the SMS. michael@0: if (this._settings.paymentServiceId === null) { michael@0: let i = 0; michael@0: while(i < gRil.numRadioInterfaces) { michael@0: if (this.iccInfo[i].iccId === aSubject.iccId) { michael@0: this._settings.paymentServiceId = i; michael@0: break; michael@0: } michael@0: i++; michael@0: } michael@0: } michael@0: michael@0: this._silentSmsObservers[number].forEach(function(callback) { michael@0: callback(aSubject); michael@0: }); michael@0: }, michael@0: michael@0: _cleanUp: function _cleanUp() { michael@0: if (_debug) { michael@0: LOG("Cleaning up!"); michael@0: } michael@0: michael@0: if (!this._silentNumbers) { michael@0: return; michael@0: } michael@0: michael@0: while (this._silentNumbers.length) { michael@0: let number = this._silentNumbers.pop(); michael@0: smsService.removeSilentNumber(number); michael@0: } michael@0: this._silentNumbers = null; michael@0: this._silentSmsObservers = null; michael@0: this._settings.cleanup(); michael@0: Services.obs.removeObserver(this._onSilentSms, kSilentSmsReceivedTopic); michael@0: } michael@0: #endif michael@0: }; michael@0: michael@0: // We save the identifier of the DOM request, so we can dispatch the results michael@0: // of the payment flow to the appropriate content process. michael@0: addMessageListener("Payment:LoadShim", function receiveMessage(aMessage) { michael@0: gRequestId = aMessage.json.requestId; michael@0: PaymentProvider._init(); michael@0: }); michael@0: michael@0: addEventListener("DOMWindowCreated", function(e) { michael@0: content.wrappedJSObject.mozPaymentProvider = PaymentProvider; michael@0: }); michael@0: michael@0: #ifdef MOZ_B2G_RIL michael@0: // If the trusted dialog is not closed via paymentSuccess or paymentFailed michael@0: // a mozContentEvent with type 'cancel' is sent from the UI. We need to listen michael@0: // for this event to clean up the silent sms observers if any exists. michael@0: SystemAppProxy.addEventListener("mozContentEvent", function(e) { michael@0: if (e.detail.type === "cancel") { michael@0: PaymentProvider._cleanUp(); michael@0: } michael@0: }); michael@0: #endif