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