Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
127
assets/webapps/mobile-wallet.gaiamobile.org/js/app.js
Normal file
127
assets/webapps/mobile-wallet.gaiamobile.org/js/app.js
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
/* globals AppView, ERRORS, SIMAccess, SettingsController, PinController,
|
||||
AppletListView, AppletListController, HCIView, SEUtils, PAYMENT_IMG_SRC,
|
||||
NONPAYMENT_IMG_SRC, PAYMENT_APPLET_FILTER, NONPAYMENT_APPLET_FILTER,
|
||||
SIM_UPDATE, SIM_REFRESH */
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
'use strict';
|
||||
|
||||
var log = (msg) => console.log('[main-ctrl] ' + msg);
|
||||
var appState = { initialised: false, sentToBackground: false };
|
||||
var appView = new AppView('main', 'global-error');
|
||||
|
||||
if(!window.navigator.seManager) {
|
||||
log('Secure Element API not available. Exiting.');
|
||||
appView.showErrorView(ERRORS.GLOBAL.NO_API);
|
||||
return;
|
||||
}
|
||||
|
||||
// components and views instantiation
|
||||
var pin = new PinController(SIMAccess, 'pin-ctrl', 'pin');
|
||||
|
||||
var settingsPin = new PinController(SIMAccess, 'settings-pin-ctrl',
|
||||
'settings-pin-view', 'pin-template');
|
||||
var settings = new SettingsController('settings-ctrl', 'settings',
|
||||
settingsPin);
|
||||
|
||||
var paymentsView = new AppletListView('payments-view', PAYMENT_IMG_SRC);
|
||||
var hciView = new HCIView('hci-notification', PAYMENT_IMG_SRC);
|
||||
var payments = new AppletListController(SIMAccess,'payments-ctrl',
|
||||
paymentsView, PAYMENT_APPLET_FILTER);
|
||||
|
||||
var nonPaymentsView = new AppletListView('nonpayments-view',
|
||||
NONPAYMENT_IMG_SRC);
|
||||
var nonPayments =
|
||||
new AppletListController(SIMAccess, 'nonpayments-ctrl', nonPaymentsView,
|
||||
NONPAYMENT_APPLET_FILTER, true);
|
||||
|
||||
// updating SIMAccess with settings read from localStorage
|
||||
SIMAccess.crsAid = settings.crsAid;
|
||||
SIMAccess.uiccAid = settings.uiccAid;
|
||||
SIMAccess.pinP2 = settings.pinP2;
|
||||
|
||||
// pin verification
|
||||
var performPinCheck = function performPinCheck() {
|
||||
if(settings.pinEnabled && !pin.pinCheckInProgress) {
|
||||
appView.hideMainView();
|
||||
return pin.verifyPin().then((verified) => {
|
||||
if(!verified) {
|
||||
return Promise.reject('Pin verification failed');
|
||||
}
|
||||
|
||||
appView.showMainView();
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
// app starting point
|
||||
var initApp = function intitApp() {
|
||||
appView.showMainView();
|
||||
SIMAccess.start()
|
||||
.then(performPinCheck)
|
||||
.then(() => payments.init())
|
||||
.then(() => nonPayments.init())
|
||||
.then(() => { appState.initialised = true; })
|
||||
.catch((error) => {
|
||||
log('Init promise chain broken. Reason: ' + error);
|
||||
appView.showErrorView(error);
|
||||
SIMAccess.stop();
|
||||
});
|
||||
};
|
||||
|
||||
// global events, system-messages handlers
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
log('Visibility change');
|
||||
if(!document.hidden && appState.initialised) {
|
||||
performPinCheck().catch((e) => appView.showErrorView(e));
|
||||
}
|
||||
|
||||
appState.sentToBackground = document.hidden;
|
||||
});
|
||||
|
||||
window.navigator.mozSetMessageHandler('nfc-hci-event-transaction', (msg) => {
|
||||
var shouldClose = !appState.initialised || appState.sentToBackground;
|
||||
var aid = SEUtils.byteToHexString(msg.aid);
|
||||
log('Got HCI EVT_TRANSACTION from AID ' + aid);
|
||||
|
||||
// SIM card is not accessible for the first 1.5s after HCI
|
||||
setTimeout(() => payments.handleHCIEvt(msg, settings.fastpayEnabled), 2000);
|
||||
hciView.show(aid, shouldClose);
|
||||
});
|
||||
|
||||
// coponent specific event handlers
|
||||
appView.addListener({ onEvent: (id, data) => {
|
||||
if(data.action === 'open-settings') {
|
||||
settings.show();
|
||||
appView.hideErrorView();
|
||||
}
|
||||
}});
|
||||
|
||||
settings.addListener({ onEvent: (id, data) => {
|
||||
log('Settings event: ' + JSON.stringify(data));
|
||||
if(data.error) {
|
||||
appView.showErrorView(data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if(data.action === 'settings-updated') {
|
||||
var refreshNeeded = false;
|
||||
Object.keys(data.changes).forEach(key => {
|
||||
refreshNeeded = refreshNeeded || SIM_REFRESH.indexOf(key) !== -1;
|
||||
|
||||
if(SIM_UPDATE.indexOf(key) !== -1) {
|
||||
SIMAccess[key] = data.changes[key];
|
||||
}
|
||||
});
|
||||
|
||||
if(refreshNeeded) {
|
||||
initApp();
|
||||
}
|
||||
}
|
||||
}});
|
||||
|
||||
log('Init access, getting applets from CRS');
|
||||
initApp();
|
||||
});
|
||||
53
assets/webapps/mobile-wallet.gaiamobile.org/js/app_view.js
Normal file
53
assets/webapps/mobile-wallet.gaiamobile.org/js/app_view.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
'use strict';
|
||||
|
||||
/* globals ErrorView, addMixin, ObserverSubjectMixin */
|
||||
/* exported AppView */
|
||||
|
||||
(function(exports) {
|
||||
var AppView = function(mainViewId, errorViewId) {
|
||||
this._mainEl = document.querySelector('#' + mainViewId);
|
||||
|
||||
this._errorView = new ErrorView(errorViewId);
|
||||
this._errorView.addListener(this);
|
||||
|
||||
var settingsBtn = this._mainEl.querySelector('#settings-edit');
|
||||
settingsBtn.addEventListener('click', () => this._settingsClicked());
|
||||
|
||||
addMixin(this, ObserverSubjectMixin);
|
||||
};
|
||||
|
||||
AppView.prototype = {
|
||||
_mainEl: null,
|
||||
_errorView: null,
|
||||
|
||||
showMainView: function showMainView() {
|
||||
this._mainEl.classList.remove('hide');
|
||||
this._errorView.hide();
|
||||
},
|
||||
|
||||
hideMainView: function hideMainView() {
|
||||
this._mainEl.classList.add('hide');
|
||||
},
|
||||
|
||||
showErrorView: function showErrorView(error) {
|
||||
this._errorView.handleError(error);
|
||||
this.hideMainView();
|
||||
},
|
||||
|
||||
hideErrorView: function hideErrorView() {
|
||||
this._errorView.hide();
|
||||
},
|
||||
|
||||
onEvent: function onEvent(id, data) {
|
||||
if(data.action === 'open-settings') {
|
||||
this._settingsClicked();
|
||||
}
|
||||
},
|
||||
|
||||
_settingsClicked: function _settingsClicked() {
|
||||
this._notify({ action: 'open-settings' });
|
||||
},
|
||||
},
|
||||
|
||||
exports.AppView = AppView;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
'use strict';
|
||||
|
||||
/* globals addMixin, DetailsViewMixin, DebugMixin */
|
||||
/* exported AppletDetailsView */
|
||||
|
||||
(function(exports) {
|
||||
var AppletDetailsView = function(id, applet) {
|
||||
addMixin(this, DetailsViewMixin);
|
||||
addMixin(this, DebugMixin);
|
||||
|
||||
this.initDetailsView(id, applet);
|
||||
};
|
||||
|
||||
AppletDetailsView.prototype = {
|
||||
_template:
|
||||
'<ul class="details">' +
|
||||
'<li id="aid"><p class="label">AID</p><p class="value"></p></li>' +
|
||||
'<li class="details-more">' +
|
||||
'<p class="details-note">' +
|
||||
'More details read from the applet can be presented here' +
|
||||
'</p>' +
|
||||
'</li>' +
|
||||
'</ul>',
|
||||
|
||||
_render: function(applet) {
|
||||
this._el.querySelector('#aid .value')
|
||||
.textContent = applet.aid;
|
||||
},
|
||||
};
|
||||
|
||||
exports.AppletDetailsView = AppletDetailsView;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
'use strict';
|
||||
|
||||
/* globals AppletListModel, AppletListView, Timer, SEUtils,
|
||||
addMixin, DebugMixin */
|
||||
/* exported AppletListController */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
var AppletListController = function(simDataSource, id, view, filter, multipleAppletsActive){
|
||||
this._simDataSource = simDataSource;
|
||||
this._id = id;
|
||||
this._filter = filter;
|
||||
this._multipleAppletsActive = multipleAppletsActive;
|
||||
|
||||
this._view = view;
|
||||
this._view.addListener(this);
|
||||
|
||||
addMixin(this, DebugMixin);
|
||||
};
|
||||
|
||||
AppletListController.prototype = {
|
||||
_id: null,
|
||||
_viewId: null,
|
||||
_multipleAppletsActive: false,
|
||||
|
||||
_simDataSource: null,
|
||||
|
||||
_model: null,
|
||||
_view: null,
|
||||
_filter: null,
|
||||
|
||||
init: function alc_init() {
|
||||
return this.refreshModelView();
|
||||
},
|
||||
|
||||
refreshModelView: function alc_refreshModelView() {
|
||||
return this._refreshModel()
|
||||
.then((simAccessTime) => {
|
||||
var viewUpdateTime = this._refreshView();
|
||||
this.debug('Applets read in ' + simAccessTime + ' ms. ' +
|
||||
'View refreshed in ' + viewUpdateTime + ' ms.');
|
||||
|
||||
return simAccessTime;
|
||||
});
|
||||
},
|
||||
|
||||
// TODO unify AID arguments
|
||||
handleHCIEvt: function alc_handleHCIEvt(hci, fastPayOn) {
|
||||
var aid = SEUtils.byteToHexString(hci.aid);
|
||||
var applet = this._model.getAppletByHexAID(aid);
|
||||
|
||||
if(!applet) {
|
||||
this.debug('Got HCI from uknown aid '+ aid);
|
||||
return;
|
||||
}
|
||||
|
||||
this.debug('HCI EVT_TRANSACTION from ' + aid);
|
||||
if(!fastPayOn) {
|
||||
this._toggleAppletCLF(aid, false)
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
},
|
||||
|
||||
onEvent: function alc_onEvent(source, data) {
|
||||
if(data.action === 'details-hide') {
|
||||
this._view.hideDetails();
|
||||
return;
|
||||
}
|
||||
|
||||
if(data.action === 'details-show') {
|
||||
this._view.showDetails(this._model.getAppletByID(data.id));
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO add explicit event
|
||||
var applet = this._model.getAppletByID(data.id);
|
||||
var select = data.action === 'card-selection';
|
||||
|
||||
this._toggleAppletCLF(applet.aid, select);
|
||||
},
|
||||
|
||||
showView: function alc_showView() {
|
||||
this._view.visible = true;
|
||||
},
|
||||
|
||||
hideView: function alc_hideView() {
|
||||
this._view.visible = false;
|
||||
},
|
||||
|
||||
_toggleAppletCLF(aid, toggle) {
|
||||
this._view.disableCardSelection();
|
||||
|
||||
return this._simDataSource.toggleCLF(toggle, aid)
|
||||
// TODO add comment about CREL and auto off
|
||||
//.then(() => this._refreshModel())
|
||||
.catch((e) => this.debug('Toggle fast pay failed', e))
|
||||
.then((success) => {
|
||||
if(success) {
|
||||
this._model.applets.forEach((a) => {
|
||||
if(a.aid === aid) {
|
||||
a.contactlessState = toggle ? 1 : 0;
|
||||
} else if (toggle && !this._multipleAppletsActive) {
|
||||
a.contactlessState = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
this._view.updateSelection(this._model.applets);
|
||||
this._view.enableCardSelection();
|
||||
});
|
||||
},
|
||||
|
||||
_refreshModel: function alc_refreshMode() {
|
||||
var timer = new Timer();
|
||||
return this._simDataSource.getAppletsData()
|
||||
.then((applets) => {
|
||||
var simAccTime = timer.getElapsedTime();
|
||||
|
||||
if(!this._model) {
|
||||
this._model = new AppletListModel();
|
||||
}
|
||||
|
||||
this._model.updateList(applets, this._filter);
|
||||
return simAccTime;
|
||||
});
|
||||
},
|
||||
|
||||
_refreshView: function alc_refreshView() {
|
||||
var timer = new Timer();
|
||||
|
||||
this._view.refreshView(this._model.applets);
|
||||
return timer.getElapsedTime();
|
||||
}
|
||||
};
|
||||
|
||||
exports.AppletListController = AppletListController;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
'use strict';
|
||||
|
||||
/* globals addMixin, ObserverSubjectMixin, CardSwipeMixin, getSamplePaymentImg,
|
||||
ListViewMixin, AppletDetailsView, DebugMixin */
|
||||
/* exported AppletListView */
|
||||
|
||||
(function(exports) {
|
||||
var AppletListView = function(id, imgSrc, data) {
|
||||
this._id = id;
|
||||
|
||||
addMixin(this, ObserverSubjectMixin);
|
||||
addMixin(this, ListViewMixin);
|
||||
addMixin(this, CardSwipeMixin);
|
||||
addMixin(this, DebugMixin);
|
||||
|
||||
this.initListView(id, imgSrc, data);
|
||||
this._detailsView = new AppletDetailsView(id + '-details');
|
||||
};
|
||||
|
||||
AppletListView.prototype = {
|
||||
_detailsVisible: false,
|
||||
_detailsView: null,
|
||||
|
||||
refreshView: function(data) {
|
||||
this.updateList(data);
|
||||
|
||||
if(!Array.isArray(data) || !data.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var tapCallback = (id) => {
|
||||
var action = 'details-' + ((this._detailsVisible) ? 'hide' : 'show');
|
||||
this._notify({ id: id, action: action});
|
||||
};
|
||||
|
||||
var swipeCallback = (activeId) => {
|
||||
if(this._detailsVisible) {
|
||||
this.hideDetails();
|
||||
}
|
||||
};
|
||||
|
||||
this.initCardSwipe('li', tapCallback, swipeCallback);
|
||||
this.updateSelection(data);
|
||||
},
|
||||
|
||||
showDetails: function alv_showDetails(applet) {
|
||||
this._el.querySelector('.slider').scrollTop = 0;
|
||||
this._detailsView.refreshView(applet);
|
||||
this._el.classList.add('details-visible');
|
||||
this._detailsVisible = true;
|
||||
},
|
||||
|
||||
hideDetails: function alv_hideDetails() {
|
||||
this._el.classList.remove('details-visible');
|
||||
this._detailsVisible = false;
|
||||
},
|
||||
};
|
||||
|
||||
exports.AppletListView = AppletListView;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
'use strict';
|
||||
|
||||
/* globals SEUtils, TAG */
|
||||
/* exported AppletModel, AppletListModel */
|
||||
|
||||
(function(exports) {
|
||||
// TODO decide how to store data in the app
|
||||
var AppletModel = function(id, aid, state){
|
||||
this.id = id;
|
||||
this.aid = SEUtils.byteToHexString(aid);
|
||||
this.lifecycleState = state[0];
|
||||
this.contactlessState = state[1];
|
||||
};
|
||||
|
||||
AppletModel.prototype = {
|
||||
// id in view
|
||||
id: null,
|
||||
aid: null,
|
||||
lifecycleState: null,
|
||||
contactlessState: null,
|
||||
|
||||
set state(value) {
|
||||
this.lifecycleState = value[0];
|
||||
this.contactlessState = value[1];
|
||||
},
|
||||
|
||||
get selected() {
|
||||
return this.contactlessState === 1;
|
||||
},
|
||||
|
||||
get imgId() {
|
||||
return this.aid;
|
||||
}
|
||||
};
|
||||
|
||||
var AppletListModel = function(appletsData, filter) {
|
||||
if(appletsData) {
|
||||
this.updateList(appletsData, filter);
|
||||
}
|
||||
};
|
||||
|
||||
AppletListModel.prototype = {
|
||||
applets: [],
|
||||
|
||||
updateList: function alm_updateList(appletsData, filter) {
|
||||
var parsedApplets = this._parseAppletList(appletsData);
|
||||
parsedApplets = (!filter) ? parsedApplets : parsedApplets.filter(filter);
|
||||
|
||||
// now we create AppletModel objects and store them in the list
|
||||
var refreshed = [];
|
||||
parsedApplets.forEach((appl, idx) => {
|
||||
var aidHex = SEUtils.byteToHexString(appl.aid);
|
||||
var nAppl = this.getAppletByHexAID(aidHex);
|
||||
if(nAppl) {
|
||||
nAppl.state = appl.state;
|
||||
} else {
|
||||
nAppl = new AppletModel('appl'+idx, appl.aid, appl.state);
|
||||
}
|
||||
|
||||
refreshed.push(nAppl);
|
||||
});
|
||||
|
||||
this.applets = refreshed;
|
||||
},
|
||||
|
||||
getAppletByID: function alm_getAppletByID(id) {
|
||||
return this.applets.find((applet) => applet.id === id);
|
||||
},
|
||||
|
||||
getAppletByHexAID: function alm_getAppletByHexAID(aid) {
|
||||
return this.applets.find((applet) => applet.aid === aid);
|
||||
},
|
||||
|
||||
_parseAppletList: function alm_parseAppletList(data) {
|
||||
var list = [];
|
||||
for(var i = 0, len = data.length; i < len;) {
|
||||
if(data[i] === TAG.CRS.APP_TEMPLATE) {
|
||||
var end = i+2 + data[i+1];
|
||||
var appletData = data.subarray(i+2, end);
|
||||
var applet = this._parseApplet(appletData);
|
||||
|
||||
if(applet) {
|
||||
list.push(applet);
|
||||
}
|
||||
i = end;
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
},
|
||||
|
||||
_parseApplet: function alm_parseApplet(data) {
|
||||
if (data[0] === TAG.CRS.AID) {
|
||||
var applet = {
|
||||
aid: data.subarray(2, 2 + data[1]),
|
||||
state: data.subarray(data[1] + 5)
|
||||
};
|
||||
return applet;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
exports.AppletModel = AppletModel;
|
||||
exports.AppletListModel = AppletListModel;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
'use strict';
|
||||
|
||||
/* exported HCIView */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const CLOSE_TIMEOUT = 1000;
|
||||
|
||||
var HCIView = function(id, imgSrc) {
|
||||
this._el = document.querySelector('#'+id);
|
||||
this._imgSrc = imgSrc;
|
||||
};
|
||||
|
||||
HCIView.prototype = {
|
||||
_el: null,
|
||||
_imgSrc: null,
|
||||
_clickHandler: null,
|
||||
|
||||
show: function show(aid, closeApp) {
|
||||
window.navigator.vibrate([50, 50, 50]);
|
||||
|
||||
this._el.querySelector('#hci-image').src = this._imgSrc(aid);
|
||||
this._el.querySelector('#hci-number').textContent = aid;
|
||||
|
||||
this._clickHandler = () => this._dismiss(closeApp);
|
||||
this._el.addEventListener('click', this._clickHandler);
|
||||
this._el.classList.add('show');
|
||||
},
|
||||
|
||||
_dismiss: function _dismiss(closeApp) {
|
||||
if(closeApp) {
|
||||
setTimeout(() => window.close(), CLOSE_TIMEOUT);
|
||||
return;
|
||||
}
|
||||
|
||||
this._el.classList.remove('show');
|
||||
this._el.removeEventListener('click', this._clickHandler);
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
exports.HCIView = HCIView;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
'use strict';
|
||||
|
||||
/* exported CardSwipeMixin */
|
||||
/* globals Swipeable */
|
||||
|
||||
(function(exports) {
|
||||
/**
|
||||
* Needs to be used with ObserverSubjectMixin
|
||||
*/
|
||||
var CardSwipeMixin = {
|
||||
_swipeHandler: null,
|
||||
|
||||
initCardSwipe: function initCardSwipe(selector, tapCb, swipeCb, options) {
|
||||
options = options || {
|
||||
'element-width': 260,
|
||||
'auto-scroll-trigger': 0.25,
|
||||
'min-speed': 0.75
|
||||
};
|
||||
|
||||
if(this._swipeHandler) {
|
||||
this._swipeHandler.disable();
|
||||
this._swipeHandler.onswipe = null;
|
||||
this._swipeHandler.ontap = null;
|
||||
this._swipeHandler.onvertswipe = null;
|
||||
this._swipeHandler = null;
|
||||
}
|
||||
|
||||
this._swipeHandler = new Swipeable(this._el, selector, options);
|
||||
if(tapCb) {
|
||||
this._swipeHandler.ontap = tapCb;
|
||||
}
|
||||
|
||||
if(swipeCb) {
|
||||
this._swipeHandler.onswipe = swipeCb;
|
||||
}
|
||||
|
||||
this._swipeHandler.onvertswipe = (direction, id) => {
|
||||
var itemEl = this._el.querySelector('#' + id);
|
||||
var action = '';
|
||||
if(direction === 'up' && !itemEl.classList.contains('selected')) {
|
||||
action = 'card-selection';
|
||||
this._changeItemSelection(itemEl, true);
|
||||
}
|
||||
|
||||
if(direction === 'down' && itemEl.classList.contains('selected')) {
|
||||
action = 'card-deselection';
|
||||
this._changeItemSelection(itemEl, false);
|
||||
}
|
||||
|
||||
if(action) {
|
||||
this._notify({ id: id, action: action});
|
||||
}
|
||||
};
|
||||
|
||||
this._swipeHandler.enable();
|
||||
},
|
||||
|
||||
updateSelection: function updateSelection(items) {
|
||||
items.forEach((item, idx) => {
|
||||
var itemEl = this._el.querySelector('#'+item.id);
|
||||
|
||||
if(item.selected && !itemEl.classList.contains('selected')) {
|
||||
this._changeItemSelection(itemEl, true);
|
||||
}
|
||||
|
||||
if(!item.selected && itemEl.classList.contains('selected')) {
|
||||
this._changeItemSelection(itemEl, false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
enableCardSelection: function enablCardSelection() {
|
||||
this._swipeHandler.unpauseSwipe();
|
||||
},
|
||||
|
||||
disableCardSelection: function disableCardSelection() {
|
||||
this._swipeHandler.pauseSwipe();
|
||||
},
|
||||
|
||||
_changeItemSelection: function changeItemSelection(itemEl, select) {
|
||||
var elIdx = this._swipeHandler._elts.indexOf(itemEl);
|
||||
if(elIdx === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(select) {
|
||||
itemEl.classList.remove('deselected');
|
||||
itemEl.classList.add('selected');
|
||||
this._swipeHandler.progressSelect(elIdx, true);
|
||||
} else {
|
||||
itemEl.classList.remove('selected');
|
||||
itemEl.classList.add('deselected');
|
||||
this._swipeHandler.progressSelect(elIdx, false);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
exports.CardSwipeMixin = CardSwipeMixin;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
'use strict';
|
||||
|
||||
/* exported DetailsViewMixin */
|
||||
(function(exports) {
|
||||
|
||||
var DetailsViewMixin = {
|
||||
_id: null,
|
||||
_el: null,
|
||||
_visible: false,
|
||||
_template: '',
|
||||
|
||||
initDetailsView: function(id, data) {
|
||||
this._id = id;
|
||||
this._el = document.querySelector('#' + this._id);
|
||||
this._el.innerHTML = this._template;
|
||||
|
||||
if(data) {
|
||||
this._render(data);
|
||||
}
|
||||
},
|
||||
|
||||
isVisible: function() {
|
||||
return this._visible;
|
||||
},
|
||||
|
||||
show: function() {
|
||||
this._visible = true;
|
||||
this._el.classList.add(this._id+'-visible');
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
this._visible = false;
|
||||
this._el.classList.remove(this._id+'-visible');
|
||||
},
|
||||
|
||||
_render: function(data) {},
|
||||
|
||||
refreshView: function(data) {
|
||||
if(data) {
|
||||
this._render(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.DetailsViewMixin = DetailsViewMixin;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
'use strict';
|
||||
|
||||
/* globals CARD_UNKNOWN */
|
||||
/* exported ListViewMixin */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
var ListViewMixin = {
|
||||
_id: null,
|
||||
_el: null,
|
||||
_visible: false,
|
||||
|
||||
_imgMap: null,
|
||||
|
||||
_itemTemplate:
|
||||
'<div class="listview-element-pocket">' +
|
||||
'<div class="listview-element-sew"></div>' +
|
||||
'</div>' +
|
||||
'<img class="listview-element-img" src="img/cards/unknown.png">',
|
||||
|
||||
initListView: function(id, imgMap, data) {
|
||||
this._id = id;
|
||||
this._el = document.querySelector('#' + this._id);
|
||||
|
||||
this._imgMap = imgMap;
|
||||
|
||||
if(data && data.length) {
|
||||
this._render(data);
|
||||
}
|
||||
},
|
||||
|
||||
updateList(listItems) {
|
||||
if(!Array.isArray(listItems) || !listItems.length) {
|
||||
this.debug('list without elements', listItems);
|
||||
this._renderEmpty();
|
||||
return;
|
||||
}
|
||||
|
||||
this._render(listItems);
|
||||
},
|
||||
|
||||
isVisible: function() {
|
||||
return this._visible;
|
||||
},
|
||||
|
||||
show: function() {
|
||||
this._visible = true;
|
||||
this._el.classList.remove('listview-hide');
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
this._visible = false;
|
||||
this._el.classList.add('listview-hide');
|
||||
},
|
||||
|
||||
_render: function(data){
|
||||
var emptyLi = document.createElement('li');
|
||||
emptyLi.innerHTML = this._itemTemplate;
|
||||
var ulFragment = document.createDocumentFragment();
|
||||
data.forEach((item) => {
|
||||
var li = emptyLi.cloneNode(true);
|
||||
li.id = item.id;
|
||||
|
||||
var src = this._imgMap(item.imgId);
|
||||
// TODO remove CARD_UNKNOW dependency here
|
||||
li.querySelector('img').src = src ? src : CARD_UNKNOWN;
|
||||
|
||||
ulFragment.appendChild(li);
|
||||
});
|
||||
|
||||
var ul = this._el.querySelector('ul');
|
||||
ul.innerHTML = '';
|
||||
ul.appendChild(ulFragment);
|
||||
},
|
||||
|
||||
_renderEmpty: function() {
|
||||
var emptyLi = document.createElement('li');
|
||||
emptyLi.innerHTML = this._itemTemplate;
|
||||
|
||||
var ul = this._el.querySelector('ul');
|
||||
ul.innerHTML = '';
|
||||
ul.appendChild(emptyLi);
|
||||
},
|
||||
};
|
||||
|
||||
exports.ListViewMixin = ListViewMixin;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
'use strict';
|
||||
|
||||
/* globals GestureDetector */
|
||||
/* exported Swipeable */
|
||||
|
||||
(function(exports) {
|
||||
var Swipeable = function(container, eltSelector, options) {
|
||||
this._container = container.querySelector('.swipeable-container');
|
||||
this._eltSelector = eltSelector;
|
||||
|
||||
this.updateElements();
|
||||
this._createProgress();
|
||||
|
||||
this._gestureDetector = new GestureDetector(this._container,
|
||||
{ holdEvents: false });
|
||||
this._panHandler = e => this._handlePan(e);
|
||||
this._swipeHandler = e => this._handleSwipe(e);
|
||||
|
||||
this._container.addEventListener('tap', (e) => {
|
||||
if(this._tapCallback) {
|
||||
this._tapCallback(this._elts[this._activeEltIdx].id);
|
||||
}
|
||||
});
|
||||
|
||||
options = options || {};
|
||||
this._elWidth = options['element-width'] || this._elts[0].offsetWidth;
|
||||
this._triggerPoint = options['auto-scroll-trigger'] || this._triggerPoint;
|
||||
this._minSpeed = options['min-speed'] || this._minSpeed;
|
||||
};
|
||||
|
||||
Swipeable.prototype = {
|
||||
_gestureDetector: null,
|
||||
_container: null,
|
||||
_eltSelector: null,
|
||||
_elts: [],
|
||||
_progressElts: null,
|
||||
|
||||
_elWidth: 0,
|
||||
_triggerPoint: 0.25,
|
||||
_minSpeed: 1.0,
|
||||
|
||||
_inTransition: false,
|
||||
_offset: 0,
|
||||
_activeEltIdx: 0,
|
||||
|
||||
_tapCallback: null,
|
||||
_swipeCallback: null,
|
||||
_vertSwipeCallback: null,
|
||||
|
||||
set ontap(callback) {
|
||||
this._tapCallback = callback;
|
||||
},
|
||||
|
||||
set onswipe(callback) {
|
||||
this._swipeCallback = callback;
|
||||
},
|
||||
|
||||
set onvertswipe(callback) {
|
||||
this._vertSwipeCallback = callback;
|
||||
},
|
||||
|
||||
enable: function() {
|
||||
this._gestureDetector.startDetecting();
|
||||
this.unpauseSwipe();
|
||||
},
|
||||
|
||||
disable: function() {
|
||||
this._gestureDetector.stopDetecting();
|
||||
},
|
||||
|
||||
pauseSwipe: function() {
|
||||
this._container.removeEventListener('pan', this._panHandler);
|
||||
this._container.removeEventListener('swipe', this._swipeHandler);
|
||||
},
|
||||
|
||||
unpauseSwipe: function() {
|
||||
this._container.addEventListener('pan', this._panHandler);
|
||||
this._container.addEventListener('swipe', this._swipeHandler);
|
||||
},
|
||||
|
||||
updateElements: function() {
|
||||
var nodeList = this._container.querySelectorAll('.swipeable > ' + this._eltSelector);
|
||||
this._elts = Array.prototype.slice.call(nodeList);
|
||||
},
|
||||
|
||||
_createProgress: function() {
|
||||
if(!this._elts || !this._elts.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var progressDiv = this._container.querySelector('.swipeable-progress');
|
||||
if(!progressDiv) {
|
||||
return;
|
||||
}
|
||||
|
||||
var progressFragment = document.createDocumentFragment();
|
||||
this._progressElts = this._elts.map(() => {
|
||||
var div = document.createElement('div');
|
||||
progressFragment.appendChild(div);
|
||||
return div;
|
||||
});
|
||||
|
||||
this._progressElts[0].classList.add('swipeable-progress-active');
|
||||
progressDiv.innerHTML = '';
|
||||
progressDiv.appendChild(progressFragment);
|
||||
},
|
||||
|
||||
_updateProgress: function() {
|
||||
this._progressElts.forEach((elt, idx) => {
|
||||
if(idx === this._activeEltIdx) {
|
||||
elt.classList.add('swipeable-progress-active');
|
||||
} else if(elt.classList.contains('swipeable-progress-active')) {
|
||||
elt.classList.remove('swipeable-progress-active');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
progressSelect: function progressSelect(idx, select) {
|
||||
var pElt = this._progressElts[idx];
|
||||
if(!pElt) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(select) {
|
||||
pElt.classList.add('swipeable-progress-selected');
|
||||
} else if(pElt.classList.contains('swipeable-progress-selected')) {
|
||||
pElt.classList.remove('swipeable-progress-selected');
|
||||
}
|
||||
},
|
||||
|
||||
_handlePan: function(event) {
|
||||
if(this._inTransition) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Math.abs(event.detail.relative.dx) > Math.abs(event.detail.relative.dy)) {
|
||||
this._offset += event.detail.relative.dx;
|
||||
this._animate();
|
||||
}
|
||||
},
|
||||
|
||||
_handleSwipe: function(event) {
|
||||
if(this._vertSwipeCallback &&
|
||||
['up', 'down'].indexOf(event.detail.direction) !== -1) {
|
||||
var id = this._elts[this._activeEltIdx].id;
|
||||
this._vertSwipeCallback(event.detail.direction, id);
|
||||
}
|
||||
|
||||
var elPos = Math.abs(this._offset)%this._elWidth;
|
||||
var triggerPassed = elPos > this._elWidth * this._triggerPoint;
|
||||
|
||||
var velocity = event.detail.vx;
|
||||
var fastEnough = Math.abs(velocity) > this._minSpeed;
|
||||
|
||||
var direction = (event.detail.direction === 'left') ? 1 : -1;
|
||||
var newIdx = this._computeNewIndex(direction);
|
||||
|
||||
// Make sure that that the speed and pan amount are in the same direction
|
||||
//var samedirection = velocity === 0 || this._offset / velocity >= 0;
|
||||
|
||||
if((triggerPassed || fastEnough) && newIdx !== this._activeEltIdx) {
|
||||
var speed = Math.max(Math.abs(velocity), this._minSpeed);
|
||||
var time = (this._elWidth - Math.abs(this._offset%this._elWidth)) / speed;
|
||||
|
||||
this._activeEltIdx = newIdx;
|
||||
this._performScroll(time);
|
||||
this._updateProgress();
|
||||
if (this._swipeCallback) {
|
||||
this._swipeCallback(this._activeEltIdx);
|
||||
}
|
||||
} else if(this._offset !== 0){
|
||||
var time = Math.abs(this._offset%this._elWidth) / this._minSpeed;
|
||||
this._performScroll(time);
|
||||
if (this._swipeCallback) {
|
||||
this._swipeCallback(this._activeEltIdx);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_animate: function(time) {
|
||||
var handler = (e) => {
|
||||
e.target.style.transition = null;
|
||||
e.target.removeEventListener('transitionend', handler);
|
||||
};
|
||||
this._elts.forEach((el) => {
|
||||
if(time) {
|
||||
el.style.transition = 'transform ' + time + 'ms ease';
|
||||
el.addEventListener('transitionend', handler);
|
||||
}
|
||||
el.style.transform = 'translate3d('+this._offset+'px,0,0)';
|
||||
});
|
||||
},
|
||||
|
||||
_performScroll: function(time) {
|
||||
this._inTransition = true;
|
||||
this._offset = -1 * this._activeEltIdx * this._elWidth;
|
||||
this._animate(time);
|
||||
setTimeout(() => { this._inTransition = false; }, time);
|
||||
},
|
||||
|
||||
_computeNewIndex: function(direction) {
|
||||
var index = this._activeEltIdx + direction;
|
||||
var offBounds = index < 0 || index >= this._elts.length;
|
||||
return offBounds ? this._activeEltIdx : index;
|
||||
}
|
||||
};
|
||||
|
||||
exports.Swipeable = Swipeable;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
'use strict';
|
||||
|
||||
/* exported Timer, addMixin, ObserverSubjectMixin, DebugMixin */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
function addMixin(destination, source) {
|
||||
for (var k in source) {
|
||||
if (source.hasOwnProperty(k) && !(k in destination)) {
|
||||
destination[k] = source[k];
|
||||
}
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
|
||||
var DebugMixin = {
|
||||
DEBUG: true,
|
||||
|
||||
debug: function(msg, obj) {
|
||||
if(!this.DEBUG) {
|
||||
return;
|
||||
}
|
||||
|
||||
var str = (this._id) ? ('[' + this._id + '] ' + msg) : msg;
|
||||
if(obj) {
|
||||
str += '|' + JSON.stringify(obj);
|
||||
}
|
||||
console.log(str);
|
||||
}
|
||||
};
|
||||
|
||||
var ObserverSubjectMixin = {
|
||||
_notify: function mlv_notify(data) {
|
||||
if(this._observers && Array.isArray(this._observers)) {
|
||||
this._observers.forEach((obs) => obs.onEvent(this._id, data));
|
||||
}
|
||||
},
|
||||
|
||||
addListener: function mlv_addListener(observer) {
|
||||
if('onEvent' in observer) {
|
||||
if(!this._observers) {
|
||||
this._observers = [];
|
||||
}
|
||||
this._observers.push(observer);
|
||||
}
|
||||
},
|
||||
|
||||
removeListener: function mlv_removeListener(observer) {
|
||||
if(!this._observers || !Array.isArray(this._observers)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var idx = this._observers.indexOf(observer);
|
||||
if(idx !== -1) {
|
||||
this._observers.splice(idx, 1);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
var Timer = function() {
|
||||
this._startTime = new Date();
|
||||
};
|
||||
|
||||
Timer.prototype = {
|
||||
_startTime: null,
|
||||
|
||||
getElapsedTime: function getElapsedTime() {
|
||||
var endTime = new Date();
|
||||
return endTime.getTime() - this._startTime.getTime();
|
||||
},
|
||||
|
||||
reset: function reset() {
|
||||
this._startTime = new Date();
|
||||
}
|
||||
};
|
||||
|
||||
exports.addMixin = addMixin;
|
||||
exports.DebugMixin = DebugMixin;
|
||||
exports.ObserverSubjectMixin = ObserverSubjectMixin;
|
||||
exports.Timer = Timer;
|
||||
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
'use strict';
|
||||
|
||||
/* exported ERRORS */
|
||||
(function(exports) {
|
||||
|
||||
const ERRORS = {
|
||||
SIM: {
|
||||
NOT_IDLE: 'Wrong sim access state.',
|
||||
ACCESS_FAILED: 'Failed to enable SIM access.',
|
||||
NO_READER: 'No reader with SE present.',
|
||||
APPLETS_FAILURE: 'Failed to get applets data.',
|
||||
PIN_FAILURE: 'Failed to issue PIN command.',
|
||||
},
|
||||
|
||||
GLOBAL: {
|
||||
NO_API: 'No Secure Element API present.',
|
||||
NO_SIM: 'No SIM present.',
|
||||
SIM_FAILURE: 'SIM failure.',
|
||||
UNKNOWN: 'Unknown error.'
|
||||
}
|
||||
};
|
||||
|
||||
exports.ERRORS = ERRORS;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
'use strict';
|
||||
|
||||
/* globals ERRORS, addMixin, ObserverSubjectMixin */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const ERROR_VIEW_ID = {};
|
||||
ERROR_VIEW_ID[ERRORS.GLOBAL.NO_API] = 'no-api';
|
||||
ERROR_VIEW_ID[ERRORS.GLOBAL.NO_SIM] = 'no-sim';
|
||||
ERROR_VIEW_ID[ERRORS.GLOBAL.SIM_FAILURE] = 'sim-error';
|
||||
ERROR_VIEW_ID[ERRORS.GLOBAL.Unknown] = 'unknown-error';
|
||||
|
||||
var ErrorView = function(id) {
|
||||
this._el = document.querySelector('#' + id);
|
||||
|
||||
addMixin(this, ObserverSubjectMixin);
|
||||
|
||||
var settingsBtn = this._el.querySelector('.error-settings-btn');
|
||||
settingsBtn.addEventListener('click',
|
||||
() => this._notify({ action: 'open-settings'}));
|
||||
};
|
||||
|
||||
ErrorView.prototype = {
|
||||
_el: null,
|
||||
|
||||
show: function() {
|
||||
this._el.classList.remove('hide');
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
this._el.classList.add('hide');
|
||||
},
|
||||
|
||||
handleError: function(error) {
|
||||
console.error(error);
|
||||
var globalError = this._mapErrorToGlobal(error);
|
||||
this._showGlobalError(globalError);
|
||||
this.show();
|
||||
},
|
||||
|
||||
_mapErrorToGlobal: function(error) {
|
||||
if(Object.keys(ERRORS.GLOBAL).find(k => ERRORS.GLOBAL[k] === error)) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// mapping no reader to no SIM
|
||||
if(ERRORS.SIM.NO_READER === error) {
|
||||
return ERRORS.GLOBAL.NO_SIM;
|
||||
}
|
||||
|
||||
if(Object.keys(ERRORS.SIM).find(k => ERRORS.SIM[k] === error)) {
|
||||
return ERRORS.GLOBAL.SIM_FAILURE;
|
||||
}
|
||||
|
||||
return ERRORS.GLOBAL.UNKNOWN;
|
||||
},
|
||||
|
||||
_showGlobalError: function(globalError) {
|
||||
var errorViewId = ERROR_VIEW_ID[globalError];
|
||||
var nodesList = this._el.querySelectorAll('article');
|
||||
[].slice.call(nodesList).forEach(e => {
|
||||
if(e.id === errorViewId) {
|
||||
e.classList.remove('hide');
|
||||
} else {
|
||||
e.classList.add('hide');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
exports.ErrorView = ErrorView;
|
||||
}(window));
|
||||
66
assets/webapps/mobile-wallet.gaiamobile.org/js/ext_data.js
Normal file
66
assets/webapps/mobile-wallet.gaiamobile.org/js/ext_data.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
'use strict';
|
||||
|
||||
/* globals SEUtils */
|
||||
/* exported PAYMENT_IMG_SRC, NONPAYMENT_IMG_SRC, PAYMENT_APPLET_FILTER,
|
||||
NONPAYMENT_APPLET_FILTER, CARD_UNKNOWN */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const CARD_UNKNOWN = 'img/cards/unknown.png';
|
||||
|
||||
const SAMPLE_NONPAYMENT_IMG = [
|
||||
'img/cards/other-1.png',
|
||||
'img/cards/other-2.png',
|
||||
'img/cards/other-3.png'
|
||||
];
|
||||
|
||||
const SAMPLE_PAYMENT_IMG = [
|
||||
'img/cards/payment-1.png',
|
||||
'img/cards/payment-2.png',
|
||||
'img/cards/payment-3.png'
|
||||
];
|
||||
|
||||
var _getSampleImg = function(imageArray) {
|
||||
var counter = 0;
|
||||
var imgMap = {};
|
||||
return function(id) {
|
||||
var imgSrc = imgMap[id];
|
||||
if(imgSrc) {
|
||||
return imgSrc;
|
||||
}
|
||||
|
||||
counter = (imageArray.length - 1) === counter ? 0 : ++counter;
|
||||
if(id) {
|
||||
imgMap[id] = imageArray[counter];
|
||||
}
|
||||
|
||||
return imageArray[counter];
|
||||
};
|
||||
};
|
||||
|
||||
var PAYMENT_IMG_SRC = _getSampleImg(SAMPLE_PAYMENT_IMG);
|
||||
var NONPAYMENT_IMG_SRC = _getSampleImg(SAMPLE_NONPAYMENT_IMG);
|
||||
|
||||
var PAYMENT_APPLET_FILTER = function(appletData) {
|
||||
var hexAid = SEUtils.byteToHexString(appletData.aid);
|
||||
var hexState = SEUtils.byteToHexString(appletData.state);
|
||||
// hex states for payment applets
|
||||
// 1F00 - personalised, not active on Contactless interface (CLF)
|
||||
// 1F01 - personalised, active on CLF
|
||||
// 0708 - not personalised, not possible to use in payment
|
||||
return hexAid.startsWith('A000000004') &&
|
||||
(hexState === '1F00' || hexState === '1F01');
|
||||
};
|
||||
|
||||
var NONPAYMENT_APPLET_FILTER = function(appletData) {
|
||||
var hexAid = SEUtils.byteToHexString(appletData.aid);
|
||||
return hexAid.startsWith('D');
|
||||
};
|
||||
|
||||
exports.CARD_UNKNOWN = CARD_UNKNOWN;
|
||||
exports.PAYMENT_IMG_SRC = PAYMENT_IMG_SRC;
|
||||
exports.NONPAYMENT_IMG_SRC = NONPAYMENT_IMG_SRC;
|
||||
exports.PAYMENT_APPLET_FILTER = PAYMENT_APPLET_FILTER;
|
||||
exports.NONPAYMENT_APPLET_FILTER = NONPAYMENT_APPLET_FILTER;
|
||||
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,316 @@
|
|||
'use strict';
|
||||
|
||||
/* globals addMixin, DebugMixin, PinView, DEFAULTS, PIN_DEFAULTS */
|
||||
/* exported PinController */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const PIN_STATE = {
|
||||
UNKNOWN: 'unknown',
|
||||
ENABLED: 'enabled',
|
||||
DISABLED: 'disabled',
|
||||
BLOCKED: 'blocked',
|
||||
};
|
||||
|
||||
var _defaultPin = DEFAULTS.PIN_VALUE;
|
||||
|
||||
var PinController = function(simDataSource, id, viewId, templateId) {
|
||||
this._simDataSource = simDataSource;
|
||||
this._id = id;
|
||||
this._view = new PinView(viewId, PIN_DEFAULTS.PIN_LEN, templateId);
|
||||
|
||||
addMixin(this, DebugMixin);
|
||||
};
|
||||
|
||||
PinController.prototype = {
|
||||
_id: null,
|
||||
_simDataSource: null,
|
||||
_view: null,
|
||||
|
||||
// TODO consider removing
|
||||
_simPinState: PIN_STATE.UNKNOWN,
|
||||
|
||||
_pinCheckInProgress: false,
|
||||
_lastInputedPin: null,
|
||||
|
||||
get pinCheckInProgress() {
|
||||
return this._pinCheckInProgress;
|
||||
},
|
||||
|
||||
set defaultPin(value) {
|
||||
_defaultPin = value;
|
||||
},
|
||||
|
||||
get view() {
|
||||
return this._view._el;
|
||||
},
|
||||
|
||||
hideView: function() {
|
||||
this._view.hide();
|
||||
},
|
||||
|
||||
verifyPin: function() {
|
||||
return this._requestPinVerification().then((result) => {
|
||||
if(result) {
|
||||
this.hideView();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
},
|
||||
|
||||
enablePin: function() {
|
||||
return this._requestPinVerification()
|
||||
.then((verified) => {
|
||||
if(verified) {
|
||||
this.debug('Pin verified');
|
||||
this._simPinState = PIN_STATE.ENABLED;
|
||||
this._lastInputedPin = null;
|
||||
return true;
|
||||
} else {
|
||||
this.debug('Pin verification failed, pin probably disabled');
|
||||
var pin = this._lastInputedPin;
|
||||
this._lastInputedPin = null;
|
||||
return this._enableSimPin(pin);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
disablePin: function() {
|
||||
return this._requestPinVerification()
|
||||
.then((verified) => {
|
||||
if(verified) {
|
||||
this.debug('Pin verified');
|
||||
var pin = this._lastInputedPin;
|
||||
this._lastInputedPin = null;
|
||||
return this._disableSimPin(pin);
|
||||
} else {
|
||||
this.debug('Pin verification failed, pin probably disabled');
|
||||
this._lastInputedPin = null;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
changePin: function() {
|
||||
var pin;
|
||||
return this._requestPinVerification(this._view.REQUEST_BEFORE_CHANGE)
|
||||
.then((verified) => {
|
||||
if(!verified) {
|
||||
this.debug('Pin not verfied, not changing');
|
||||
this._lastInputedPin = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
pin = this._lastInputedPin;
|
||||
this._lastInputedPin = null;
|
||||
return this._getNewPin();
|
||||
})
|
||||
.then((newPin) => {
|
||||
this.debug('New pin value ' + newPin);
|
||||
var pinBytes = this._formatPin(pin);
|
||||
var newPinBytes = this._formatPin(newPin);
|
||||
return this._simDataSource.changePIN(pinBytes, newPinBytes);
|
||||
})
|
||||
.then((result) => this._handlePinChangeResult(result));
|
||||
},
|
||||
|
||||
cancelPinAction: function() {
|
||||
this._view.cancelPinRequest();
|
||||
},
|
||||
|
||||
_handlePinChangeResult: function(result) {
|
||||
this.debug('Pin change result ' + result);
|
||||
if(result === '9000') {
|
||||
return true;
|
||||
} else {
|
||||
this.debug('Unexpected error');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
_getNewPin: function(prevAttempFailed) {
|
||||
var newPin;
|
||||
return this._getUserPin(PIN_DEFAULTS.MAX_PUK_ATTEMPTS, prevAttempFailed,
|
||||
this._view.REQUEST_NEW_PIN)
|
||||
.then((pin1) => {
|
||||
newPin = pin1;
|
||||
return this._getUserPin(PIN_DEFAULTS.MAX_PUK_ATTEMPTS, false,
|
||||
this._view.REQUEST_CONFIRM);
|
||||
})
|
||||
.then((pin2) => {
|
||||
if(newPin === pin2) {
|
||||
return newPin;
|
||||
}
|
||||
|
||||
this.debug('Pins do not match');
|
||||
return this._getNewPin(true);
|
||||
});
|
||||
},
|
||||
|
||||
_requestPinVerification: function(reqType) {
|
||||
this.debug('_requestPinVerification, SIM PIN : ' + this._simPinState);
|
||||
// TODO verify if we need this
|
||||
//if(this._pinCheckInProgress) {
|
||||
// return Promise.resolve(false);
|
||||
//}
|
||||
|
||||
this._pinCheckInProgress = true;
|
||||
this._view.show();
|
||||
return this._performVerification(PIN_DEFAULTS.MAX_PIN_ATTEMPTS, false, reqType)
|
||||
.then((verified) => {
|
||||
if(verified) {
|
||||
//this._view.hide();
|
||||
// TODO think this through
|
||||
this._pinCheckInProgress = false;
|
||||
}
|
||||
|
||||
return verified;
|
||||
});
|
||||
},
|
||||
|
||||
_getUserPin: function(attemptsLeft, lastVerFailed, reqType, pinLen) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var listener = {
|
||||
onEvent: (id, data) => {
|
||||
this._view.removeListener(listener);
|
||||
if (data.pin) {
|
||||
this.debug('got PIN: ' + data.pin);
|
||||
resolve(data.pin);
|
||||
} else {
|
||||
reject('pin-cancelled');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this._view.addListener(listener);
|
||||
if(!this._view.isVisible) {
|
||||
this._view.show();
|
||||
}
|
||||
|
||||
this.debug('waiting for PIN input from user');
|
||||
this._view.requestPin(attemptsLeft, lastVerFailed, reqType, pinLen);
|
||||
});
|
||||
},
|
||||
|
||||
_performVerification: function(attemptsLeft, lastVerFailed, reqType) {
|
||||
return this._getUserPin(attemptsLeft, lastVerFailed, reqType)
|
||||
.then((pin) => {
|
||||
this._lastInputedPin = pin;
|
||||
var pinBytes = this._formatPin(pin);
|
||||
this.debug('verifying PIN ' + pinBytes);
|
||||
return this._simDataSource.verifyPIN(pinBytes);
|
||||
})
|
||||
.then((result) => this._handleVerficationResult(result));
|
||||
//TODO should we catch here?
|
||||
},
|
||||
|
||||
_handleVerficationResult: function(result) {
|
||||
this.debug('Verification finished, status word:' + result);
|
||||
if(result === '9000') {
|
||||
return true;
|
||||
} else if(result === '63C0' || result === '6983') {
|
||||
this.debug('Pin blocked');
|
||||
return this._handlePinBlocked();
|
||||
} else if(result.startsWith('63C')) {
|
||||
var attemptsLeft = Number.parseInt(result[3]);
|
||||
this.debug('Verification failed, tries left: ' + attemptsLeft);
|
||||
return this._performVerification(attemptsLeft, true);
|
||||
} else {
|
||||
this.debug('Unknown status word, PIN probably not enabled');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
_handlePinBlocked: function() {
|
||||
this._simPinState = PIN_STATE.BLOCKED;
|
||||
|
||||
this.debug('PUK unblock flow start.');
|
||||
var unblockRequested = () => this._performPinUnblock(PIN_DEFAULTS.MAX_PUK_ATTEMPTS);
|
||||
return this._view.blockPin().then(unblockRequested);
|
||||
},
|
||||
|
||||
_performPinUnblock: function(attemptsLeft, lastUnblockFailed) {
|
||||
var puk;
|
||||
return this._getUserPin(attemptsLeft, lastUnblockFailed,
|
||||
this._view.REQUEST_PUK, PIN_DEFAULTS.MAX_SIM_PIN_LEN)
|
||||
.then((inputedPuk) => {
|
||||
this.debug('Inputed PUK ' + inputedPuk);
|
||||
puk = inputedPuk;
|
||||
return this._getNewPin(false);
|
||||
})
|
||||
.then((inputedPin) => {
|
||||
this.debug('Inputed PIN ' + inputedPin);
|
||||
var bytePuk = this._formatPin(puk);
|
||||
var bytePin = this._formatPin(inputedPin);
|
||||
// if we're part of disable/enable flow we need to save pin here
|
||||
this._lastInputedPin = inputedPin;
|
||||
return this._simDataSource.unblockPIN(bytePuk, bytePin);
|
||||
})
|
||||
.then((result) => this._handlePinUnblockResult(result));
|
||||
},
|
||||
|
||||
_handlePinUnblockResult: function(result) {
|
||||
this.debug('PIN unblock result ' + result);
|
||||
if(result === '9000') {
|
||||
return true;
|
||||
} else if(result === '63C0' || !result.startsWith('63C')) {
|
||||
this.debug('Complate block');
|
||||
return this._view.blockPuk();
|
||||
} else {
|
||||
var attemptsLeft = Number.parseInt(result[3]);
|
||||
this.debug('PIN unblock failed, tries left: ' + attemptsLeft);
|
||||
return this._performPinUnblock(attemptsLeft, true);
|
||||
}
|
||||
},
|
||||
|
||||
_enableSimPin: function(pin) {
|
||||
var pinBytes = this._formatPin(pin);
|
||||
return this._simDataSource.enablePIN(pinBytes)
|
||||
.then((result) => this._handlePinEnabledResult(result));
|
||||
},
|
||||
|
||||
_handlePinEnabledResult: function(result) {
|
||||
this.debug('PIN enable result ' + result);
|
||||
if(result === '9000') {
|
||||
this._simPinState = PIN_STATE.ENABLED;
|
||||
//this._view.hide();
|
||||
return true;
|
||||
} else if(result === '63C0' || result === '6983') {
|
||||
this.debug('Pin blocked');
|
||||
return this._handlePinBlocked();
|
||||
} else {
|
||||
var tries = result.startsWith('63C') ? Number.parseInt(result[3]) : 0;
|
||||
this._view.showEnableFailed(tries, _defaultPin);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
_disableSimPin: function(pin) {
|
||||
var pinBytes = this._formatPin(pin);
|
||||
return this._simDataSource.disablePIN(pinBytes)
|
||||
.then((result) => this._handlePinDisableResult(result));
|
||||
},
|
||||
|
||||
_handlePinDisableResult: function(result) {
|
||||
this.debug('PIN disable result ' + result);
|
||||
if(result === '9000') {
|
||||
this._simPinState = PIN_STATE.DISABLED;
|
||||
//this._view.hide();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
_formatPin: function(pin) {
|
||||
var pinBytes = pin.split('').map((d) => d.charCodeAt());
|
||||
for(var i = pinBytes.length; i < PIN_DEFAULTS.MAX_SIM_PIN_LEN; i++) {
|
||||
pinBytes.push(PIN_DEFAULTS.PIN_PADDING);
|
||||
}
|
||||
|
||||
return pinBytes;
|
||||
}
|
||||
};
|
||||
|
||||
exports.PinController = PinController;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
274
assets/webapps/mobile-wallet.gaiamobile.org/js/pin/pin_view.js
Normal file
274
assets/webapps/mobile-wallet.gaiamobile.org/js/pin/pin_view.js
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
'use strict';
|
||||
|
||||
/* globals addMixin, ObserverSubjectMixin, DebugMixin */
|
||||
/* exported PinView */
|
||||
|
||||
(function(exports) {
|
||||
// number of pin input attempts which should
|
||||
// trigger the pin warning display
|
||||
const PIN_ATTEMPTS_WARNING = 2;
|
||||
const PIN_INCORRECT_TIMEOUT = 1000; //ms
|
||||
const PIN_DEFAULT_LEN = 4;
|
||||
|
||||
const PIN_REQ_DEF = 'request-default-pin';
|
||||
const PIN_REQ_BEFORE_CHANGE = 'request-pin-before-change';
|
||||
const PIN_REQ_PUK = 'request-puk';
|
||||
const PIN_REQ_NEW_PIN = 'request-new-pin';
|
||||
const PIN_REQ_REPEAT_NEW_PIN = 'request-repeat-new-pin';
|
||||
|
||||
const PIN_ENTRY_TEXT_MAP = {};
|
||||
|
||||
PIN_ENTRY_TEXT_MAP[PIN_REQ_DEF] = {
|
||||
entry: 'Please enter your PIN',
|
||||
error: 'Incorrect PIN'
|
||||
};
|
||||
|
||||
PIN_ENTRY_TEXT_MAP[PIN_REQ_BEFORE_CHANGE] = {
|
||||
entry: 'Please enter current PIN',
|
||||
error: 'Incorrect PIN'
|
||||
},
|
||||
|
||||
PIN_ENTRY_TEXT_MAP[PIN_REQ_PUK] = {
|
||||
entry: 'Please enter your PUK',
|
||||
error: 'Incorrect PUK'
|
||||
};
|
||||
|
||||
PIN_ENTRY_TEXT_MAP[PIN_REQ_NEW_PIN] = {
|
||||
entry: 'Please enter new PIN',
|
||||
error: 'New PIN mismatch',
|
||||
};
|
||||
|
||||
PIN_ENTRY_TEXT_MAP[PIN_REQ_REPEAT_NEW_PIN] = {
|
||||
entry: 'Please confirm new PIN',
|
||||
error: ''
|
||||
};
|
||||
|
||||
var PinView = function(id, defPinLen, templateId) {
|
||||
this._id = id;
|
||||
this._defaultPinLen = defPinLen || PIN_DEFAULT_LEN;
|
||||
this._maxPinLen = this._defaultPinLen;
|
||||
|
||||
addMixin(this, ObserverSubjectMixin);
|
||||
addMixin(this, DebugMixin);
|
||||
|
||||
if(templateId) {
|
||||
this._el = document.querySelector('#' + templateId).cloneNode(true);
|
||||
this._el.id = this._id;
|
||||
} else {
|
||||
this._el = document.querySelector('#' + this._id);
|
||||
}
|
||||
|
||||
this._init();
|
||||
};
|
||||
|
||||
PinView.prototype = {
|
||||
REQUEST_PIN: PIN_REQ_DEF,
|
||||
REQUEST_BEFORE_CHANGE: PIN_REQ_BEFORE_CHANGE,
|
||||
REQUEST_PUK: PIN_REQ_PUK,
|
||||
REQUEST_NEW_PIN: PIN_REQ_NEW_PIN,
|
||||
REQUEST_CONFIRM: PIN_REQ_REPEAT_NEW_PIN,
|
||||
|
||||
_el: null,
|
||||
_id: null,
|
||||
_visible: false,
|
||||
|
||||
_pinInputEl: null,
|
||||
_inputedPin: '',
|
||||
|
||||
// we have a default PIN length but we can do one time request for PINs
|
||||
// of different length (i.e. PUK)
|
||||
_defaultPinLen: 0,
|
||||
_maxPinLen: 0,
|
||||
|
||||
_inputBlocked: false,
|
||||
|
||||
isVisible() {
|
||||
return this._visible;
|
||||
},
|
||||
|
||||
show: function() {
|
||||
this._visible = true;
|
||||
this._el.classList.remove('hide');
|
||||
this._clearPin();
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
this._visible = false;
|
||||
this._el.classList.add('hide');
|
||||
},
|
||||
|
||||
_init: function() {
|
||||
this._pinInputEl = this._el.querySelector('.pin-input');
|
||||
|
||||
var keyHandler = this._keyHandler.bind(this);
|
||||
var keypad = this._el.querySelector('.pin-keypad');
|
||||
keypad.addEventListener('touchstart', keyHandler, true);
|
||||
|
||||
this._updatePinInput();
|
||||
},
|
||||
|
||||
_keyHandler: function(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
var key = event.target.dataset.value;
|
||||
if(!key || this._inputBlocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch(key) {
|
||||
case 'clear': this._clearPin(); break;
|
||||
case 'backspace': this._backspacePin(); break;
|
||||
default: this._appendPin(key);
|
||||
}
|
||||
},
|
||||
|
||||
_clearPin: function() {
|
||||
this._inputedPin = '';
|
||||
this._updatePinInput();
|
||||
},
|
||||
|
||||
_backspacePin: function() {
|
||||
if(!this._inputedPin.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._inputedPin = this._inputedPin.slice(0, -1);
|
||||
this._updatePinInput();
|
||||
},
|
||||
|
||||
_appendPin: function(digit) {
|
||||
if(this._inputedPin.length >= this._maxPinLen) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._inputedPin += digit;
|
||||
this._updatePinInput();
|
||||
|
||||
if(this._inputedPin.length === this._maxPinLen) {
|
||||
if(this._maxPinLen !== this._defaultPinLen) {
|
||||
this._maxPinLen = this._defaultPinLen;
|
||||
}
|
||||
this._notify({ action: 'pin-inputed', pin: this._inputedPin });
|
||||
}
|
||||
},
|
||||
|
||||
_updatePinInput: function() {
|
||||
var inputedDigits = this._inputedPin.length;
|
||||
var fragment = document.createDocumentFragment();
|
||||
for(var i = 0; i < this._maxPinLen; i++) {
|
||||
var pinDigit = document.createElement('div');
|
||||
if(i < inputedDigits) {
|
||||
pinDigit.classList.add('pin-digit-entered');
|
||||
}
|
||||
fragment.appendChild(pinDigit);
|
||||
}
|
||||
this._pinInputEl.innerHTML = '';
|
||||
this._pinInputEl.appendChild(fragment);
|
||||
},
|
||||
|
||||
_updatePinInputTexts: function _updatePinInputTexts(reqType) {
|
||||
reqType = (reqType in PIN_ENTRY_TEXT_MAP) ? reqType : PIN_REQ_DEF;
|
||||
this._el.querySelector('.pin-entry-text')
|
||||
.textContent = PIN_ENTRY_TEXT_MAP[reqType].entry;
|
||||
this._el.querySelector('.pin-entry-error-text')
|
||||
.textContent = PIN_ENTRY_TEXT_MAP[reqType].error;
|
||||
},
|
||||
|
||||
requestPin: function(attemptsLeft, showPinIncorrect, reqType, pinLen) {
|
||||
this.debug('requestPIN, tries: ' + attemptsLeft + ', type: ' + reqType);
|
||||
this._maxPinLen = pinLen || this._maxPinLen;
|
||||
this._clearPin();
|
||||
|
||||
this._updatePinInputTexts(reqType);
|
||||
this._showByClass('pin-unblocked');
|
||||
|
||||
if(showPinIncorrect) {
|
||||
this._showPinIncorrect();
|
||||
if(reqType !== PIN_REQ_NEW_PIN) {
|
||||
this._showWarning(attemptsLeft);
|
||||
}
|
||||
} else if(attemptsLeft <= PIN_ATTEMPTS_WARNING) {
|
||||
this._showWarning(attemptsLeft);
|
||||
} else {
|
||||
this._clearWarning();
|
||||
}
|
||||
},
|
||||
|
||||
cancelPinRequest: function() {
|
||||
this._maxPinLen = this._defaultPinLen;
|
||||
this._notify({});
|
||||
},
|
||||
|
||||
blockPin: function() {
|
||||
window.navigator.vibrate([200, 100, 200, 100, 200]);
|
||||
this._showByClass('pin-blocked');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
var unblockBtn = this._el.querySelector('.pin-unblock-btn');
|
||||
var onclick = () => {
|
||||
unblockBtn.removeEventListener('click', onclick);
|
||||
resolve();
|
||||
};
|
||||
|
||||
unblockBtn.addEventListener('click', onclick);
|
||||
});
|
||||
},
|
||||
|
||||
blockPuk: function() {
|
||||
window.navigator.vibrate([200, 100, 200, 100, 200]);
|
||||
this._showByClass('puk-blocked');
|
||||
return new Promise(()=>{});
|
||||
},
|
||||
|
||||
showEnableFailed: function(tries, defaultPin) {
|
||||
window.navigator.vibrate([200, 100, 200, 100, 200]);
|
||||
this._showByClass('pin-enable-error');
|
||||
this._el.querySelector('.pin-default-val').textContent = defaultPin;
|
||||
if(tries || tries === 0) {
|
||||
this._el.querySelector('.pin-enable-warning').classList.remove('hide');
|
||||
var att = (tries === 1) ? ' entry attempt' : ' entry attempts';
|
||||
this._el.querySelector('.pin-enable-attempt').textContent = tries + att;
|
||||
} else {
|
||||
this._el.querySelector('.pin-enable-warning').classList.add('hide');
|
||||
}
|
||||
},
|
||||
|
||||
_showByClass: function(clss) {
|
||||
var nodesList = this._el.querySelectorAll('article');
|
||||
Array.prototype.slice.call(nodesList).forEach(el => {
|
||||
if(el.classList.contains(clss)) {
|
||||
el.classList.remove('hide');
|
||||
} else {
|
||||
el.classList.add('hide');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_showWarning: function(attemptsLeft) {
|
||||
this._el.querySelector('.pin-warning').classList.remove('hide');
|
||||
var warning = attemptsLeft + ' attempt';
|
||||
warning = (attemptsLeft !== 1) ? (warning + 's') : warning;
|
||||
warning += ' remaining';
|
||||
this._el.querySelector('.pin-attempts').textContent = warning;
|
||||
},
|
||||
|
||||
_togglePinIncorrect: function(show) {
|
||||
this._inputBlocked = show;
|
||||
this._el.querySelector('.pin-entry').classList.toggle('hide');
|
||||
this._el.querySelector('.pin-incorrect').classList.toggle('hide');
|
||||
},
|
||||
|
||||
_showPinIncorrect: function() {
|
||||
window.navigator.vibrate([200, 100, 200]);
|
||||
this._togglePinIncorrect(true);
|
||||
setTimeout(() => this._togglePinIncorrect(false), PIN_INCORRECT_TIMEOUT);
|
||||
},
|
||||
|
||||
_clearWarning: function() {
|
||||
this._el.querySelector('.pin-warning').classList.add('hide');
|
||||
}
|
||||
};
|
||||
|
||||
exports.PinView = PinView;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
CRS AID, TAGS and APDUs based on:
|
||||
GlobalPlatform Card Technology Contactless Services Card Specification v2.2
|
||||
– Amendment C Version 1.1.1
|
||||
|
||||
UICC APDUs based on:
|
||||
ETSI TS 102 221 V8.2.0 (2009-06)
|
||||
Smart Cards; UICC-Terminal interface; Physical and logical characteristics
|
||||
(Release 8)
|
||||
*/
|
||||
|
||||
/* globals SEUtils */
|
||||
/* exported APDU, AID, TAG */
|
||||
|
||||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const AID = {
|
||||
CRS: 'A00000015143525300',
|
||||
UICC: 'A0000000871001FF33FFFF8901010100'
|
||||
};
|
||||
|
||||
const APDU = {
|
||||
// gets the response from SIM, can be used to retireve channel select
|
||||
getResponse: { cla: 0x00, ins: 0xC0, p1: 0x00, p2: 0x00 },
|
||||
readBinary: { cla: 0x00, ins: 0xB0, p1: 0x00, p2: 0x00 },
|
||||
CRS: {
|
||||
// Get applet version number and global update counter
|
||||
getData: { cla: 0x80, ins: 0xCA, p1: 0x00, p2: 0xA5 },
|
||||
// Get status of applets and Security Domains only.
|
||||
// The ISD is ignored. Get first or only occurrence
|
||||
getStatusAll1st: {
|
||||
cla: 0x80, ins: 0xF2, p1: 0x40, p2: 0x00,
|
||||
data: new Uint8Array([
|
||||
0x4F, 0x00, // applet AID, empty here
|
||||
0x5C, 0x03, // tag list to be returned
|
||||
0x4F, // AID
|
||||
0x9F, 0x70 // applet life cycle state - first byte;
|
||||
// contactless activation state - second byte
|
||||
])
|
||||
},
|
||||
// Get status of applets and Security Domains only.
|
||||
// The ISD is ignored. Get next occurrence(s), if SW is 63h10h
|
||||
getStatusAllNext: { cla: 0x80, ins: 0xF2, p1: 0x40, p2: 0x01,
|
||||
data: new Uint8Array([0x4F, 0x00, 0x5C, 0x03, 0x4F, 0x9F, 0x70]) },
|
||||
// Get status of applet identified with AID
|
||||
getStatusAID: (aid) => {
|
||||
return { cla: 0x80, ins: 0xF2, p1: 0x40, p2: 0x00,
|
||||
data: SEUtils.joinUint8Arrays(
|
||||
[0x4F, 0x0F], // applet AID
|
||||
SEUtils.hexStringToByte(aid),
|
||||
[0x5C, 0x04, // tag list
|
||||
0x4F, // AID
|
||||
0x9F, // applet lifecycle state
|
||||
0x70, // contactless activation state
|
||||
0x81 // selection priority
|
||||
]
|
||||
)
|
||||
};
|
||||
},
|
||||
activateCLF: (aid) => {
|
||||
return { cla: 0x80, ins: 0xF0, p1: 0x01, p2: 0x01,
|
||||
data: SEUtils.joinUint8Arrays([0x4F, 0x0F],
|
||||
SEUtils.hexStringToByte(aid))
|
||||
};
|
||||
},
|
||||
deactivateCLF: (aid) => {
|
||||
return { cla: 0x80, ins: 0xF0, p1: 0x01, p2: 0x00,
|
||||
data: SEUtils.joinUint8Arrays([0x4F, 0x0F],
|
||||
SEUtils.hexStringToByte(aid))
|
||||
};
|
||||
},
|
||||
nfcActivate: { cla: 0x80, ins: 0xF0, p1: 0x04, p2: 0x80,
|
||||
data: new Uint8Array([0x80, 0x01, 0x40]) },
|
||||
nfcDeactivate: { cla: 0x80, ins: 0xF0, p1: 0x04, p2: 0x00,
|
||||
data: new Uint8Array([0x80, 0x01, 0x40]) }
|
||||
},
|
||||
// PIN commands
|
||||
UICC: {
|
||||
getAttempts: (p2) => {
|
||||
return { cla: 0x00, ins: 0x20, p1: 0x00, p2: p2 };
|
||||
},
|
||||
verify: (p2, pinBytes) => {
|
||||
return { cla: 0x00, ins: 0x20, p1: 0x00, p2: p2, data: pinBytes };
|
||||
},
|
||||
enable: (p2, defPinBytes) => {
|
||||
return { cla: 0x00, ins: 0x28, p1: 0x00, p2: p2, data: defPinBytes };
|
||||
},
|
||||
disable: (p2, defPinBytes) => {
|
||||
return { cla: 0x00, ins: 0x26, p1: 0x00, p2: p2, data: defPinBytes };
|
||||
},
|
||||
change: (p2, curPinBytes, newPinBytes) => {
|
||||
var data = SEUtils.joinUint8Arrays(curPinBytes, newPinBytes);
|
||||
return { cla: 0x00, ins: 0x24, p1: 0x00, p2: p2, data: data };
|
||||
},
|
||||
unblock: (p2, pukBytes, defPinBytes) => {
|
||||
var data = SEUtils.joinUint8Arrays(pukBytes, defPinBytes);
|
||||
return { cla: 0x00, ins: 0x2C, p1: 0x00, p2: p2, data: data };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const TAG = {
|
||||
CRS: {
|
||||
APP_TEMPLATE: 0x61,
|
||||
AID: 0x4F,
|
||||
STATE: 0x9F70
|
||||
},
|
||||
};
|
||||
|
||||
exports.AID = AID;
|
||||
exports.APDU = APDU;
|
||||
exports.TAG = TAG;
|
||||
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
'use strict';
|
||||
|
||||
/* exported SEUtils */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
var SEUtils = {
|
||||
byteToHexString: function byteToHexString(uint8arr) {
|
||||
if (!uint8arr) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var hexStr = '';
|
||||
for (var i = 0; i < uint8arr.length; i++) {
|
||||
var hex = (uint8arr[i] & 0xff).toString(16);
|
||||
hex = (hex.length === 1) ? '0' + hex : hex;
|
||||
hexStr += hex;
|
||||
}
|
||||
return hexStr.toUpperCase();
|
||||
},
|
||||
|
||||
hexStringToByte: function hexStringToByte(str) {
|
||||
var a = [];
|
||||
for(var i = 0, len = str.length; i < len; i+=2) {
|
||||
a.push(parseInt(str.substr(i,2),16));
|
||||
}
|
||||
return new Uint8Array(a);
|
||||
},
|
||||
|
||||
byteToASCIIString: function byteToASCIIString(arr) {
|
||||
var str = '';
|
||||
|
||||
var len = arr && arr.length ? arr.length : 0;
|
||||
for(var i = 0; i < len; i++) {
|
||||
str += arr[i] < 0x20 || arr > 0x7E ? '' : String.fromCharCode(arr[i]);
|
||||
}
|
||||
|
||||
return str;
|
||||
},
|
||||
|
||||
joinUint8Arrays: function joinUint8Arrays() {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
var length = args.reduce(function(a, b) { return a + b.length; }, 0);
|
||||
var out = new Uint8Array(length);
|
||||
|
||||
args.reduce(function(previousLen, buffer) {
|
||||
out.set(buffer, previousLen);
|
||||
return previousLen + buffer.length;
|
||||
}, 0);
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
// no support for multibyte tag and constructed data object
|
||||
parseSimpleTLV: function parseSimpleTLV(data) {
|
||||
if(!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = {};
|
||||
for(var i = 0, len = data.length; i < len;) {
|
||||
var tag = data[i];
|
||||
var length = data[i+1];
|
||||
var value = data.slice(i+2, i+2 + length);
|
||||
result[tag] = value;
|
||||
i += 2 + length;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
promises: {
|
||||
whilePromise: function(condition, action, init) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var loop = (value) => {
|
||||
if (condition(value)) {
|
||||
return resolve(value);
|
||||
}
|
||||
|
||||
return action(value).then(loop).catch(reject);
|
||||
};
|
||||
loop(init);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
exports.SEUtils = SEUtils;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
'use strict';
|
||||
|
||||
/* globals SEUtils, APDU, ERRORS, DEFAULTS */
|
||||
/* exported simAccessManager */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const SIM_ACCESS_DISABLED = 'disabled';
|
||||
const SIM_ACCESS_IDLE = 'idle';
|
||||
const SIM_ACCESS_BUSY = 'busy';
|
||||
|
||||
var _simState = SIM_ACCESS_DISABLED;
|
||||
var _seSession = null;
|
||||
|
||||
var _crsAid = DEFAULTS.CRS_AID;
|
||||
var _uiccAid = DEFAULTS.UICC_AID;
|
||||
var _pinP2 = DEFAULTS.PIN_P2;
|
||||
|
||||
var SIMAccessManager = {
|
||||
set crsAid(value) {
|
||||
_crsAid = value;
|
||||
},
|
||||
|
||||
get crsAidBytes() {
|
||||
return SEUtils.hexStringToByte(_crsAid);
|
||||
},
|
||||
|
||||
set uiccAid(value) {
|
||||
_uiccAid = value;
|
||||
},
|
||||
|
||||
get uiccAidBytes() {
|
||||
return SEUtils.hexStringToByte(_uiccAid);
|
||||
},
|
||||
|
||||
set pinP2(value) {
|
||||
_pinP2 = value;
|
||||
},
|
||||
|
||||
get pinP2Bytes() {
|
||||
return SEUtils.hexStringToByte(_pinP2)[0];
|
||||
},
|
||||
|
||||
start: function start() {
|
||||
if(!navigator.seManager) {
|
||||
return Promise.reject(ERRORS.GLOBAL.NO_API);
|
||||
}
|
||||
|
||||
return navigator.seManager.getSEReaders()
|
||||
.then((readers) => {
|
||||
var reader = readers.find(r => r.type.toLowerCase() === 'uicc' && r.isSEPresent);
|
||||
if(!reader) {
|
||||
return Promise.reject(ERRORS.SIM.NO_READER);
|
||||
}
|
||||
return reader.openSession();
|
||||
})
|
||||
.then((session) => {
|
||||
_seSession = session;
|
||||
_simState = SIM_ACCESS_IDLE;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
_simState = SIM_ACCESS_DISABLED;
|
||||
|
||||
if(e !== ERRORS.SIM.NO_READER) {
|
||||
e = ERRORS.SIM.ACCESS_FAILED;
|
||||
}
|
||||
return Promise.reject(e);
|
||||
});
|
||||
},
|
||||
|
||||
stop: function stop() {
|
||||
return _seSession.closeAll()
|
||||
.then(() => {
|
||||
_seSession = null;
|
||||
_simState = SIM_ACCESS_DISABLED;
|
||||
})
|
||||
.catch(e => this._cleanup(e, ERRORS.SIM.SIM_FAILURE));
|
||||
},
|
||||
|
||||
_initSIMAccess: function _initSIMAccess() {
|
||||
if(_simState !== SIM_ACCESS_IDLE) {
|
||||
return Promise.reject(ERRORS.SIM.NOT_IDLE);
|
||||
}
|
||||
|
||||
if(!_seSession.reader.isSEPresent) {
|
||||
return Promise.reject(ERRORS.SIM.NO_READER);
|
||||
}
|
||||
|
||||
_simState = SIM_ACCESS_BUSY;
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
_cleanup: function _cleanup(error, message) {
|
||||
console.error(error);
|
||||
_simState = SIM_ACCESS_IDLE;
|
||||
if(error === ERRORS.SIM.NOT_IDLE || error === ERRORS.SIM.NO_READER) {
|
||||
message = error;
|
||||
}
|
||||
return Promise.reject(message);
|
||||
},
|
||||
|
||||
getAppletsData: function getAppletsData() {
|
||||
return this._initSIMAccess()
|
||||
.then(() => _seSession.openLogicalChannel(this.crsAidBytes))
|
||||
.then((channel) => {
|
||||
return channel.transmit(APDU.CRS.getStatusAll1st);
|
||||
})
|
||||
.then((resp) => {
|
||||
var receivedData = new Uint8Array();
|
||||
|
||||
// we can get different responses, also propriatary
|
||||
// using counter to not allow to try to query the SIM indifinately
|
||||
var count = 0;
|
||||
var condition = (resp) => (count++ > 10 || resp.sw1 === 0x90 && resp.sw2 === 0x00);
|
||||
var action = (resp) => {
|
||||
receivedData = SEUtils.joinUint8Arrays(receivedData, resp.data);
|
||||
return resp.channel.transmit(APDU.CRS.getStatusAllNext);
|
||||
};
|
||||
|
||||
return SEUtils.promises.whilePromise(condition, action, resp)
|
||||
.then((lastResp) => {
|
||||
lastResp.channel.close();
|
||||
_simState = SIM_ACCESS_IDLE;
|
||||
return SEUtils.joinUint8Arrays(receivedData, lastResp.data);
|
||||
});
|
||||
})
|
||||
.catch(e => this._cleanup(e, ERRORS.SIM.APPLETS_FAILURE));
|
||||
},
|
||||
|
||||
toggleCLF: function toggleCLF(on, aid) {
|
||||
var apdu = on ? APDU.CRS.activateCLF(aid) : APDU.CRS.deactivateCLF(aid);
|
||||
return this._initSIMAccess()
|
||||
.then(() => _seSession.openLogicalChannel(this.crsAidBytes))
|
||||
.then((channel) => channel.transmit(apdu))
|
||||
.then((resp) => {
|
||||
return resp.channel.close()
|
||||
.then(() => {
|
||||
_simState = SIM_ACCESS_IDLE;
|
||||
return resp.sw1 === 0x90 && resp.sw2 === 0x00;
|
||||
});
|
||||
})
|
||||
.catch(e => this._cleanup(e, ERRORS.SIM.SIM_FAILURE));
|
||||
},
|
||||
|
||||
_execPinCommand: function _execPinCommand(type, pin1, pin2) {
|
||||
return this._initSIMAccess()
|
||||
.then(() => _seSession.openLogicalChannel(this.uiccAidBytes))
|
||||
.then((channel) => {
|
||||
var apdu = APDU.UICC[type](this.pinP2Bytes, pin1, pin2);
|
||||
if(!apdu) {
|
||||
return Promise.reject('PIN APDU type ' + type + ' not found');
|
||||
}
|
||||
return channel.transmit(apdu);
|
||||
})
|
||||
.then((resp) => {
|
||||
return resp.channel.close().then(() => {
|
||||
_simState = SIM_ACCESS_IDLE;
|
||||
return SEUtils.byteToHexString([resp.sw1, resp.sw2]);
|
||||
});
|
||||
})
|
||||
.catch(e => this._cleanup(e, ERRORS.SIM.PIN_FAILURE));
|
||||
},
|
||||
|
||||
verifyPIN: function verifyPIN(pin) {
|
||||
return this._execPinCommand('verify', pin);
|
||||
},
|
||||
|
||||
enablePIN: function enablePIN(pin) {
|
||||
return this._execPinCommand('enable', pin);
|
||||
},
|
||||
|
||||
unblockPIN: function unblockPIN(puk, pin) {
|
||||
return this._execPinCommand('unblock', puk, pin);
|
||||
},
|
||||
|
||||
disablePIN: function disablePIN(pin) {
|
||||
return this._execPinCommand('disable', pin);
|
||||
},
|
||||
|
||||
changePIN: function changePIN(currentPin, newPin) {
|
||||
return this._execPinCommand('change', currentPin, newPin);
|
||||
},
|
||||
};
|
||||
|
||||
exports.SIMAccess = SIMAccessManager;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
'use strict';
|
||||
|
||||
/* exported ConfigSubView */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
var ConfigSubView = function(id, crsAid, uiccAid, pinP2, defaultPin) {
|
||||
this._id = id;
|
||||
this._el = document.querySelector('#'+id);
|
||||
|
||||
this.crsInput = this._el.querySelector('#crs-aid');
|
||||
this.crsInput.value = crsAid;
|
||||
|
||||
this.uiccInput = this._el.querySelector('#uicc-aid');
|
||||
this.uiccInput.value = uiccAid;
|
||||
|
||||
this.pinP2Input = this._el.querySelector('#pinP2');
|
||||
this.pinP2Input.value = pinP2;
|
||||
|
||||
this.defaultPinInput = this._el.querySelector('#default-pin');
|
||||
this.defaultPinInput.value = defaultPin;
|
||||
|
||||
this._el.querySelector('header button.icon-back')
|
||||
.addEventListener('click', () => this._handleConfigFinished());
|
||||
this._el.querySelector('#settings-configuration-done')
|
||||
.addEventListener('click', () => this._handleConfigFinished());
|
||||
};
|
||||
|
||||
ConfigSubView.prototype = {
|
||||
_el: null,
|
||||
_id: null,
|
||||
_visible: false,
|
||||
|
||||
crsInput: null,
|
||||
uiccInput: null,
|
||||
pinP2Input: null,
|
||||
defaultPinInput: null,
|
||||
|
||||
get visible() {
|
||||
return this._visible;
|
||||
},
|
||||
|
||||
set visible(value) {
|
||||
if(value) {
|
||||
this._visible = true;
|
||||
this._el.classList.add('slide-in');
|
||||
} else {
|
||||
this._visible = false;
|
||||
this._el.classList.remove('slide-in');
|
||||
}
|
||||
},
|
||||
|
||||
_handleConfigFinished: function() {
|
||||
if(!this.crsInput.validity.valid || !this.uiccInput.validity.valid ||
|
||||
!this.pinP2Input.validity.valid ||
|
||||
!this.defaultPinInput.validity.valid) {
|
||||
this.debug('Inputs not valid, not closing settings');
|
||||
return;
|
||||
}
|
||||
|
||||
this.visible = false;
|
||||
},
|
||||
};
|
||||
|
||||
exports.ConfigSubView = ConfigSubView;
|
||||
}(window));
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
'use strict';
|
||||
|
||||
/* globals AID */
|
||||
/* exported DEFAULTS, SIM_REFRESH, SIM_UPDATE */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const DEFAULTS = {
|
||||
PIN: false,
|
||||
FASTPAY: true,
|
||||
CRS_AID: AID.CRS,
|
||||
UICC_AID: AID.UICC,
|
||||
PIN_P2: '0E',
|
||||
PIN_VALUE: '0000'
|
||||
};
|
||||
|
||||
// settings properties which trigger sim data refresh on change
|
||||
const SIM_REFRESH = ['crsAid'];
|
||||
// settings properties which trigger SIMAccessManager update
|
||||
const SIM_UPDATE = ['crsAid', 'uiccAid', 'pinP2'];
|
||||
|
||||
const PIN_DEFAULTS = {
|
||||
MAX_PIN_ATTEMPTS: 3,
|
||||
MAX_PUK_ATTEMPTS: 10,
|
||||
PIN_LEN: 4,
|
||||
MAX_SIM_PIN_LEN: 8,
|
||||
PIN_PADDING: 255
|
||||
};
|
||||
|
||||
exports.DEFAULTS = DEFAULTS;
|
||||
exports.SIM_REFRESH = SIM_REFRESH;
|
||||
exports.SIM_UPDATE = SIM_UPDATE;
|
||||
exports.PIN_DEFAULTS = PIN_DEFAULTS;
|
||||
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
'use strict';
|
||||
|
||||
/* globals addMixin, DebugMixin, ObserverSubjectMixin, SettingsView, DEFAULTS */
|
||||
/* exported SettingsController */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
var SettingsController = function(id, viewId, pinCtrl) {
|
||||
this._id = id;
|
||||
this._pinCtrl = pinCtrl;
|
||||
|
||||
addMixin(this, DebugMixin);
|
||||
addMixin(this, ObserverSubjectMixin);
|
||||
|
||||
this._init(viewId);
|
||||
};
|
||||
|
||||
SettingsController.prototype = {
|
||||
_id: null,
|
||||
_view: null,
|
||||
|
||||
_settingsData: {
|
||||
'pin': DEFAULTS.PIN,
|
||||
'fastPay': DEFAULTS.FASTPAY,
|
||||
'crsAid': DEFAULTS.CRS_AID,
|
||||
'uiccAid': DEFAULTS.UICC_AID,
|
||||
'pinP2': DEFAULTS.PIN_P2,
|
||||
'defaultPin': DEFAULTS.PIN_VALUE
|
||||
},
|
||||
|
||||
get pinEnabled() {
|
||||
return this._settingsData.pin;
|
||||
},
|
||||
|
||||
get fastpayEnabled() {
|
||||
return this._settingsData.fastPay;
|
||||
},
|
||||
|
||||
get crsAid() {
|
||||
return this._settingsData.crsAid;
|
||||
},
|
||||
|
||||
get uiccAid() {
|
||||
return this._settingsData.uiccAid;
|
||||
},
|
||||
|
||||
get pinP2() {
|
||||
return this._settingsData.pinP2;
|
||||
},
|
||||
|
||||
get defaultPin() {
|
||||
return this._settingsData.defaultPin;
|
||||
},
|
||||
|
||||
show: function() {
|
||||
this._view.visible = true;
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
this._view.visible = false;
|
||||
},
|
||||
|
||||
_init: function(viewId) {
|
||||
Object.keys(this._settingsData).forEach(s => {
|
||||
this._settingsData[s] = this._readSetting(s, this._settingsData[s]);
|
||||
});
|
||||
|
||||
this._view = new SettingsView(viewId, Object.create(this._settingsData));
|
||||
this._view.addListener(this);
|
||||
this._view.setSubViewContent('settings-pin', this._pinCtrl.view);
|
||||
},
|
||||
|
||||
_readSetting: function(settingName, defValue) {
|
||||
var settingVal = localStorage.getItem(settingName);
|
||||
if(settingVal === 'true' || settingVal === 'false') {
|
||||
settingVal = (settingVal === 'true');
|
||||
}
|
||||
|
||||
if(settingVal === null && defValue !== undefined) {
|
||||
localStorage.setItem(settingName, defValue);
|
||||
settingVal = defValue;
|
||||
}
|
||||
|
||||
return settingVal;
|
||||
},
|
||||
|
||||
onEvent: function(id, event) {
|
||||
this.debug('Event from: ' + id + ', ' + JSON.stringify(event));
|
||||
switch(event.action) {
|
||||
case 'editing-finished': this._handleInput(event.data); break;
|
||||
case 'pin-toggle': this._handlePinToggle(event.data.checked); break;
|
||||
case 'pin-change': this._handlePinChange(); break;
|
||||
case 'pin-cancel': this._handlePinCancel(); break;
|
||||
}
|
||||
},
|
||||
|
||||
addPinComponent: function(pinCtrl) {
|
||||
this._pinCtrl = pinCtrl;
|
||||
this._view.setSubViewContent('settings-pin', this._pinCtrl.view);
|
||||
},
|
||||
|
||||
_handlePinToggle: function(enable) {
|
||||
this._view.showSubview('settings-pin');
|
||||
if(enable) {
|
||||
this._handlePinEnable();
|
||||
} else {
|
||||
this._handlePinDisable();
|
||||
}
|
||||
},
|
||||
|
||||
_handlePinEnable: function() {
|
||||
this._pinCtrl.enablePin()
|
||||
.then((enabled) => {
|
||||
this._view.pinEnabled = enabled;
|
||||
if(enabled) {
|
||||
this._view.hideSubview('settings-pin');
|
||||
this._pin = true;
|
||||
localStorage.setItem('pin', this._pin);
|
||||
this._view.showStatus('Wallet PIN enabled successfully');
|
||||
} else {
|
||||
this.debug('PIN enable failure');
|
||||
this._view.showStatus('Failed to enable Wallet PIN');
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
if(e !== 'pin-cancelled') {
|
||||
this._view.hideSubview('settings-pin');
|
||||
this._notify({ error: e, msg: 'PIN enable error' });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_handlePinDisable: function() {
|
||||
this._pinCtrl.disablePin()
|
||||
.then((disabled) => {
|
||||
this._view.hideSubview('settings-pin');
|
||||
this._view.pinEnabled = !disabled;
|
||||
if(disabled) {
|
||||
this._pin = false;
|
||||
localStorage.setItem('pin', this._pin);
|
||||
this._view.showStatus('Wallet PIN disabled successfully');
|
||||
} else {
|
||||
this.debug('PIN disable failure');
|
||||
this._view.showStatus('Failed to disable Wallet PIN');
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
if(e !== 'pin-cancelled') {
|
||||
this._view.hideSubview('settings-pin');
|
||||
this._notify({ error: e, msg: 'PIN disable error' });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_handlePinChange: function() {
|
||||
this._view.showSubview('settings-pin');
|
||||
this._pinCtrl.changePin()
|
||||
.then((success) => {
|
||||
this._view.hideSubview('settings-pin');
|
||||
if(success) {
|
||||
this._view.showStatus('Wallet PIN changed successfully');
|
||||
} else {
|
||||
this.debug('Change failed');
|
||||
this._view.showStatus('Failed to change Wallet PIN');
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
if(e !== 'pin-cancelled') {
|
||||
this._view.hideSubview('settings-pin');
|
||||
this._notify({ error: e, msg: 'PIN chnage error' });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_handlePinCancel() {
|
||||
this._pinCtrl.cancelPinAction();
|
||||
},
|
||||
|
||||
_handleInput: function(data) {
|
||||
var changes = {};
|
||||
Object.keys(data).forEach(s => {
|
||||
if(this._settingsData[s] !== data[s]) {
|
||||
this._settingsData[s] = data[s];
|
||||
changes[s] = data[s];
|
||||
localStorage.setItem(s, data[s]);
|
||||
}
|
||||
});
|
||||
|
||||
if(Object.keys(changes).length) {
|
||||
this._notify({ action: 'settings-updated', changes: changes });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
exports.SettingsController = SettingsController;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
'use strict';
|
||||
|
||||
/* exported SettingsSubView */
|
||||
|
||||
(function(exports) {
|
||||
var SettingsSubView = function(id, onCloseCb) {
|
||||
this._id = id;
|
||||
this._onCloseCallback = onCloseCb;
|
||||
this._el = document.querySelector('#'+id);
|
||||
this._el.querySelector('header button.icon-back')
|
||||
.addEventListener('click', () => {
|
||||
this.visible = false;
|
||||
if(this._onCloseCallback) {
|
||||
this._onCloseCallback();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
SettingsSubView.prototype = {
|
||||
_el: null,
|
||||
_id: null,
|
||||
_visible: false,
|
||||
|
||||
_onCloseCallback: null,
|
||||
|
||||
get visible() {
|
||||
return this._visible;
|
||||
},
|
||||
|
||||
set visible(value) {
|
||||
if(value) {
|
||||
this._visible = true;
|
||||
this._el.classList.add('slide-in');
|
||||
} else {
|
||||
this._visible = false;
|
||||
this._el.classList.remove('slide-in');
|
||||
}
|
||||
},
|
||||
|
||||
set content(el) {
|
||||
var contentEl = this._el.querySelector('.subview-content');
|
||||
this._el.replaceChild(el, contentEl);
|
||||
el.classList.add('subview-content');
|
||||
},
|
||||
};
|
||||
|
||||
exports.SettingsSubView = SettingsSubView;
|
||||
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
'use strict';
|
||||
|
||||
/* globals addMixin, ObserverSubjectMixin, DebugMixin, SettingsSubView,
|
||||
ConfigSubView */
|
||||
/* exported SettingsView */
|
||||
|
||||
(function(exports) {
|
||||
const STATUS_TIMEOUT = 4000; // ms
|
||||
|
||||
var SettingsView = function(id, options) {
|
||||
this._id = id;
|
||||
this._el = document.querySelector('#' + id);
|
||||
|
||||
this._initMainView(options.pin, options.fastPay);
|
||||
this._initSubViews(options);
|
||||
|
||||
addMixin(this, ObserverSubjectMixin);
|
||||
addMixin(this, DebugMixin);
|
||||
};
|
||||
|
||||
SettingsView.prototype = {
|
||||
_el: null,
|
||||
_id: null,
|
||||
_visible: false,
|
||||
_subviews: [],
|
||||
|
||||
_pinCheckbox: null,
|
||||
_fastPayCheckbox: null,
|
||||
|
||||
_pinChangeBtn: null,
|
||||
_pinEnabled: false,
|
||||
|
||||
get visible() {
|
||||
return this._visible;
|
||||
},
|
||||
|
||||
set visible(value) {
|
||||
if(value) {
|
||||
this._visible = true;
|
||||
this._el.classList.remove('hide');
|
||||
this._el.classList.add('edit');
|
||||
} else {
|
||||
this._subviews.forEach((v) => { v.visible = false; });
|
||||
this._visible = false;
|
||||
this._el.classList.remove('edit');
|
||||
}
|
||||
},
|
||||
|
||||
set pinEnabled(value) {
|
||||
this._pinCheckbox.checked = value;
|
||||
if(value) {
|
||||
this._pinChangeBtn.classList.remove('settings-disabled');
|
||||
} else {
|
||||
this._pinChangeBtn.classList.add('settings-disabled');
|
||||
}
|
||||
},
|
||||
|
||||
_initMainView: function(pin, fastPay) {
|
||||
this._pinCheckbox = this._el.querySelector('#pin-checkbox');
|
||||
this._pinCheckbox.addEventListener('change', () => {
|
||||
this._notify({
|
||||
action: 'pin-toggle',
|
||||
data: { checked: this._pinCheckbox.checked }
|
||||
});
|
||||
this.pinEnabled = !this._pinCheckbox.checked;
|
||||
});
|
||||
|
||||
this._pinChangeBtn = this._el.querySelector('#pin-change-btn');
|
||||
this._pinChangeBtn.addEventListener('click', () => {
|
||||
this._notify({ action: 'pin-change' });
|
||||
});
|
||||
|
||||
this.pinEnabled = pin;
|
||||
|
||||
this._fastPayCheckbox = this._el.querySelector('#fast-pay');
|
||||
this._fastPayCheckbox.checked = fastPay;
|
||||
|
||||
this._el.querySelector('#settings-done').addEventListener('click',
|
||||
() => this._handleEditingFinished());
|
||||
},
|
||||
|
||||
_initSubViews: function(cfg){
|
||||
var config = new ConfigSubView('settings-configuration', cfg.crsAid,
|
||||
cfg.uiccAid, cfg.pinP2, cfg.defaultPin);
|
||||
this._el.querySelector('#adv-settings-btn')
|
||||
.addEventListener('click', () => { config.visible = true; });
|
||||
|
||||
var about = new SettingsSubView('settings-about');
|
||||
this._el.querySelector('#about-btn')
|
||||
.addEventListener('click', () => { about.visible = true; });
|
||||
|
||||
var pinCloseHandler = () => this._handlePinViewClosed();
|
||||
var pin = new SettingsSubView('settings-pin', pinCloseHandler);
|
||||
|
||||
this._subviews.push(config);
|
||||
this._subviews.push(about);
|
||||
this._subviews.push(pin);
|
||||
},
|
||||
|
||||
_getSubView: function(id) {
|
||||
return this._subviews.find(v => v._id === id);
|
||||
},
|
||||
|
||||
setSubViewContent: function(id, content) {
|
||||
var subView = this._getSubView(id);
|
||||
if(subView) {
|
||||
subView.content = content;
|
||||
}
|
||||
},
|
||||
|
||||
showSubview: function(id, hide) {
|
||||
var subView = this._getSubView(id);
|
||||
if(subView) {
|
||||
subView.visible = !hide;
|
||||
}
|
||||
},
|
||||
|
||||
hideSubview: function(id) {
|
||||
this.showSubview(id, true);
|
||||
},
|
||||
|
||||
showStatus: function(statusMsg) {
|
||||
this._el.querySelector('#settings-status p').textContent = statusMsg;
|
||||
var status = this._el.querySelector('#settings-status');
|
||||
status.classList.add('visible');
|
||||
setTimeout(() => status.classList.remove('visible'), STATUS_TIMEOUT);
|
||||
},
|
||||
|
||||
_handleEditingFinished: function _handleEditingFinished() {
|
||||
var conf = this._getSubView('settings-configuration');
|
||||
|
||||
this._notify({
|
||||
action: 'editing-finished',
|
||||
data: {
|
||||
pin: this._pinCheckbox.checked,
|
||||
fastPay: this._fastPayCheckbox.checked,
|
||||
crsAid: conf.crsInput.value,
|
||||
uiccAid: conf.uiccInput.value,
|
||||
pinP2: conf.pinP2Input.value,
|
||||
defaultPin: conf.defaultPinInput.value
|
||||
}
|
||||
});
|
||||
this.visible = false;
|
||||
},
|
||||
|
||||
_handlePinViewClosed: function _handlePinViewClosed() {
|
||||
this._notify({ action: 'pin-cancel' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.SettingsView = SettingsView;
|
||||
}((typeof exports === 'undefined') ? window : exports));
|
||||
Loading…
Reference in a new issue