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:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2015 Deutsche Telekom, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -0,0 +1,364 @@
html {
font-size: 10px;
background-color: #F4F4F4;
}
[role="toolbar"] {
position: static;
}
ul {
font-size: 0.9em;
}
.gaia-icon::before {
font-style: normal;
position: relative;
}
.icon-settings::before {
color: white;
top: 2px;
left: 1px;
}
.note {
font-size: 0.7em;
padding-left: 15px;
padding-bottom: 5px;
background-color: rgb(248, 246, 246);
display: none;
}
#main article > header {
background-color: rgb(248, 246, 246);
color: #F97C17;
height: 30px;
line-height: 30px;
text-align: center;
text-transform: uppercase;
border-bottom: 1px white solid;
border-top: 1px white solid;
/* box-shadow: 0px 1px 5px 0px rgba(50, 50, 50, 0.1);*/
font-size: 12px;
box-shadow: 0px 1px 1px 0px rgba(33, 33, 33, 0.1);
}
.bottom-article > header {
z-index: 3;
position: relative;
}
.hide, .listview-hide {
display: none;
}
#settings p {
margin: 10px;
}
.error {
color: red;
}
section[role="region"] > header:first-child h1:nth-child(2) {
margin-left: 50px;
}
#hci-notification {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 300;
transition: all 0.6s ease;
transform: translateY(100%);
}
#hci-notification img {
width: 240px;
box-shadow: 0px 1px 4px 1px #7B7B7B;
border-radius: 8px;
}
#hci-notification.show {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 300;
transition: all 0.6s ease;
transform: translateY(0);
}
article.notification {
color: #9C9C9C;
font-size: 0.7em;
font-style: italic;
opacity: 0.95;
background: black;
padding-bottom: 10px;
}
.hci-card {
position: absolute;
width: 100%;
height: 100%;
color: white;
z-index: 1;
}
.hci-container {
text-align: center;
border-radius: 10px;
padding: 0px 20px;
color: rgb(77, 78, 83);
background: transparent linear-gradient(to bottom, rgb(234, 239, 242), rgb(212, 221, 228));
}
.hci-container p {
line-height: 3em;
font-weight: 300;
font-size: 0.8em;
}
@keyframes show-card {
0% { z-index: 2; transform: rotate(0deg); top: 0px; left: 0px; width: 240px;}
50% { z-index: 10; transform: rotate(0deg); top: -30px; left: -2px; width: 244px;}
100% { z-index: 10; transform: rotate(0deg); top: -8px; left: 0px; width: 240px;}
}
@keyframes hide-card {
0% { z-index: 10; transform: rotate(0deg); top: -8px; left: 0px; width: 240px;}
50% { z-index: 10; transform: rotate(0deg); top: -30px; left: -2px; width: 244px;}
100% { z-index: 2; transform: rotate(0deg); top: 0px; left: 0px; width: 240px;}
}
ul.swipeable > li.deselected > img {
animation-name: hide-card;
animation-duration: 0.5s;
z-index: 2;
position: relative;
top: 0px;
}
ul.swipeable > li.selected > img {
z-index: 10;
transform: rotate(0deg);
position: relative;
top: -8px;
animation-name: show-card;
animation-duration: 0.5s;
}
ul.details > li {
display: block;
padding: 15px 10px;
}
ul.details > li > p.label {
font-size: 1em;
}
.details span.date {
display: inline-block;
width: 67%;
}
li.details-more {
text-align: center;
vertical-align: bottom;
margin-right: 15px;
}
li.details-more > .details-note {
font-style: italic;
font-size: 0.8em;
color: rgb(133, 133, 133);
vertical-align: bottom;
}
/*.details-oneline p {
display: inline-block;
}*/
.active > header {
z-index: 3;
}
.slider {
background-color: rgb(248, 246, 246);
/* transform: translate3d(0, -131px, 0);
box-shadow: inset 0px 1px 5px 0px rgba(50, 50, 50, 0.1);
box-shadow: 0px 5px 5px 0px rgba(33, 33, 33, 0.1) inset; */
padding-top: 1rem;
height: 25rem;
overflow: hidden;
}
.bottom-slide .slider {
box-shadow: 0px 2px 2px 0px rgba(33, 33, 33, 0.1) inset;
}
.bottom-slide > header {
position: relative;
}
.top-slide {
margin-top: -26rem;
transition: transform 0.75s, margin-top 0.75s ease;
}
.details-visible .top-slide {
margin-top: 0px;
overflow: auto;
transform: translate3d(0,0,0)
}
.bottom-slide {
z-index: 3;
position: relative;
top: -10px;
transition: transform 0.75s, top 0.75s ease;
}
.bottom-slide.details-visible {
top: -253px;
transform: translate3d(0,0,0)
}
section[role="region"] > header:first-child .t-logo {
background: url(../img/icons/t_logo_white.png) center center / 5rem auto no-repeat;
width: 5rem;
}
.pocket, .listview-element-pocket {
width: 260px;
height: 40px;
border-radius: 12px;
position: relative;
z-index: 5;
top: 150px;
left: 0px;
background: linear-gradient(to bottom, rgb(200, 200, 200) 0%, #F4F4F4 77%, #F4F4F4 100%) repeat scroll 0% 0% transparent;
border-top: 1px solid #808080;
opacity: 0.9;
}
/*.nonpayment*/ .details li {
min-height: 0px
}
/*.nonpayment*/ .details p {
display: inline;
}
/*.nonpayment*/ .details p.val {
float: right;
margin-right: 15px;
}
.details .price {
font-family: "Fira Mono";
font-size: 13px;
width: 21%;
display: inline-block;
text-align: right;
}
.details .currency {
font-family: "Fira Mono";
font-size: 13px;
}
.sew, .listview-element-sew {
width: 92%;
height: 10px;
margin: 2px 0px 0px 5px;
border-radius: 12px;
padding-right: 10px;
border-top: 1px dashed #8C8C8C;
}
ul.transaction-list > li.transaction {
padding: 4px 0px 1px 10px;
color: rgb(133, 133, 133);
}
ul.transaction-list > li.transaction:after {
border: 0px none;
}
ul.details > li p {
font-size: 1em;
margin-left: 10px;
line-height: 1em;
}
ul.details > li p.value {
float: right;
margin-right: 10px;
font-size: 0.9em;
}
.error-view {
text-align: center;
padding: 10px;
margin-top: -2em;
}
.error-view h2.error {
font-weight: normal;
font-size: 1.2em;
}
.error-view button {
margin-bottom: 1em;
}
.error-img {
width: 5em;
height: 5em;
background-image: url(../img/errors/error.png);
background-size: contain;
background-repeat: no-repeat;
background-position: center center;
display: inline-block;
}
.error-api {
background-image: url(../img/errors/api-error.png);
}
.error-no-sim {
background-image: url(../img/errors/no-sim.png);
}
.error-sim {
background-image: url(../img/errors/sim-error.png);
}
.footer {
position: absolute;
left: 0px;
right: 0px;
bottom: 0px;
margin: 10px 10px 0px 10px;
}
@media (min-width: 360px) {
.bottom-slide {
top: -1px;
}
.bottom-slide.details-visible {
top: -26rem;
}
ul.details > li.details-more {
padding-right: 20px;
padding-left: 20px;
}
}

View file

@ -0,0 +1,131 @@
section#pin {
background-color: #F4F4F4;
}
.pin-unblocked {
font-weight: 300;
color: #333;
}
.pin-view h2 {
font-size: 1.2em;
font-weight: 300;
text-align: center;
}
.pin-view:not(hide) {
z-index: 200;
}
.pin-top {
/*font-style: italic;*/
height: 7.3em;
}
.pin-input {
text-align: center;
margin: 35px 0px 30px;
}
.pin-input > div {
border-radius: 50%;
display: inline-block;
width: 14px;
height: 14px;
border: 1.5px solid #ccc;
vertical-align: baseline;
margin: 0px 5px;
}
.pin-input > div.pin-digit-entered {
background: linear-gradient(to bottom, #f4f4f4 0%,#333333 100%);
border: 1.5px solid #333;
}
.pin-keypad {
display: table;
text-align: center;
border-collapse: collapse;
margin: 0px 0px 25px 0px;
}
.pin-keypad-row {
border-width: 0px 0px 1px 0px;
border-style: solid;
border-color: transparent;
/*border-image: linear-gradient(45deg, #f4f4f4, #ccc, #f4f4f4) 1;*/
border-image: linear-gradient(to right, #f4f4f4 0%,#cccccc 30%,#cccccc 70%,#f4f4f4 100%) 1;
}
.pin-keypad-row:last-child {
border: none;
}
.pin-key {
display: table-cell;
width: 2.7em;
height: 2.7em;
vertical-align: middle;
border-right: 1px solid #ccc;
font-size: 1.7em;
cursor: pointer;
}
.pin-key.clear-key::after {
content: "clear";
font-size: 0.5em;
vertical-align: 35%;
}
.pin-key:active {
color: #F97C17;
/*background-color: #ccc;*/
}
.pin-key.gaia-icon::before {
position: relative;
font-size: 1em;
}
.pin-key.icon-clearcancel-cross::before {
font-size: 1.7em;
}
.pin-keypad-row:first-child .pin-key {
background: linear-gradient(to bottom, #f4f4f4, #ccc);
background-origin: border-box;
border-right: 1px solid transparent;
box-shadow: inset -999px 0 0 #f4f4f4;
}
.pin-keypad-row:last-child .pin-key {
background: linear-gradient(to top, #f4f4f4, #ccc);
background-origin: border-box;
border-right: 1px solid transparent;
box-shadow: inset -999px 0 0 #f4f4f4;
}
.pin-keypad-row .pin-key:last-child {
border: none;
}
.pin-warning {
margin-bottom: -39px;
position: relative;
top: -13px;
font-size: 0.85em;
text-align: center;
}
.pin-enable-warning {
margin-top: 1em;
}
.error-pin-enable {
background-image: url(../img/errors/pin-enable-fail.png);
}
.error-locked {
background-image: url(../img/errors/locked.png);
}

View file

@ -0,0 +1,172 @@
#settings {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
opacity: 1;
transition: all 0.6s ease;
background-color: #F4F4F4;
transform: translateY(100%);
}
#settings.edit {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 100;
opacity: 1;
transition: all 0.6s ease;
transform: translateY(0);
}
.settings-subpage {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 101;
opacity: 1;
background-color: #F4F4F4;
transition: transform 0.6s ease;
transform: translateX(100%);
}
.settings-subpage.slide-in {
transition: transform 0.6s ease;
transform: translateX(0);
}
.settings-disabled {
pointer-events: none;
color: #ccc;
}
.settings-disabled .gaia-icon::before {
color: #ccc;
}
.settings-disclaimer {
font-size: 0.6em;
padding: 0px 15px 10px 15px;
line-height: 1.5em;
}
.settings-forward div {
width: 100%;
margin-left: 15px;
}
.settings-forward .gaia-icon {
text-align: right;
color: #868692;
}
.settings-forward .gaia-icon::before {
position: relative;
font-size: 1.5rem;
}
#settings-status {
opacity: 0;
transition: opacity 600ms ease 0s;
}
#settings-status.visible {
opacity: 1;
transition: opacity 1000ms ease 0s;
}
#settings-status p{
text-align: center;
}
#settings-about section > div {
text-align: center;
border-bottom: 1px solid #E7E7E7;
padding: 10px;
margin: 0px 15px;
}
#settings-about div:last-child {
border: none;
}
#settings-about div:first-child {
padding-top: 0px;
}
.about-logo img {
max-width: 9em;
}
.about-logo p {
line-height: 1.3em;
}
.about-logo p:first-child {
font-size: 1.1em;
}
.about-logo p:nth-child(2) {
font-size: 1.5em;
}
.about-logo p:nth-child(4) {
font-size: 1em;
}
.about-authors p, .about-contributors p {
font-size: 0.8em;
color: #a6a6a6;
line-height: 1.5em;
}
.about-authors strong, .about-contributors strong {
color: #333;
font-weight: normal;
}
.about-authors p {
text-align: left;
}
.about-authors strong {
float: right;
}
.about-contributors div {
margin-bottom: 0.5em;
}
.about-contributors h3 {
font-size: 0.8em;
margin-bottom: 2px;
margin-top: 1px;
font-weight: 500;
}
.about-contributors div p:nth-child(2) {
line-height: 1em;
margin: 5px 0px;
}
.about-contributors div p:nth-child(3) {
line-height: 1em;
font-size: 0.6em;
color: #333;
}
/* ugly hacks to override header.css, TODO migrate to proper gaia header */
section[role="region"].settings-subpage > header:first-child h1:nth-child(2) {
margin: -1px;
}
section.skin-organic.settings-subpage[role="region"] > header:first-child .gaia-icon {
color: #868692;
background: none;
}

View file

@ -0,0 +1,70 @@
ul.swipeable {
white-space: nowrap;
overflow: hidden;
font-size: 0px;
}
ul.swipeable > li {
display: inline-block;
width: 260px;
height: 206px;
overflow: hidden;
}
ul.swipeable > li > img, .listview-element-img {
width: 240px;
padding: 20px 10px 25px;
margin-top: -35px;
position: relative;
}
ul.swipeable > li:first-child {
margin-left: 3rem;
}
.swipeable-container {
z-index: 2;
background-color: #f4f4f4;
border-bottom: 1px solid #FBFBFB;
box-shadow: 0px 2px 2px 0px rgba(33, 33, 33, 0.1);
}
.swipeable-progress {
text-align: center;
height: 1.6rem;
position: relative;
z-index: 1;
}
.swipeable-progress div{
border-radius: 50%;
display: inline-block;
width: 6px;
height: 6px;
border: 2px solid #aeaeae;
vertical-align: baseline;
margin: 0px 2px;
}
.swipeable-progress div.swipeable-progress-selected {
background: #aeaeae;
}
.swipeable-progress div.swipeable-progress-active {
/* background-color: #e20074; */
border: 2px solid #F97C17;
}
.swipeable-progress div.swipeable-progress-active.swipeable-progress-selected {
background: #F97C17;
}
@media (min-width: 360px) {
ul.swipeable > li:first-child {
margin-left: 5rem;
}
.swipeable-container {
padding-top: 0.5rem;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View file

@ -0,0 +1,361 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Mobile Wallet</title>
<meta name="theme-color" content="#ec7616">
<meta name="description" content="New wallet version targeted for FFOS 2.2">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<link href="shared/style/layout.css" rel="stylesheet" type="text/css">
<link href="shared/style/headers.css" rel="stylesheet" type="text/css">
<link href="shared/style/buttons.css" rel="stylesheet" type="text/css">
<link href="shared/style/input_areas.css" rel="stylesheet" type="text/css">
<link href="shared/style/status.css" rel="stylesheet" type="text/css">
<link href="shared/style/icons.css" rel="stylesheet" type="text/css">
<link href="css/app.css" rel="stylesheet" type="text/css">
<link href="css/swipeable.css" rel="stylesheet" type="text/css">
<link href="css/settings.css" rel="stylesheet" type="text/css">
<link href="css/pin.css" rel="stylesheet" type="text/css">
<script src="/shared/js/component_utils.js" defer></script>
<script src="/shared/elements/gaia_switch/script.js" defer></script>
<script type="text/javascript" src="shared/js/gesture_detector.js" defer></script>
<script type="text/javascript" src="js/error/error_consts.js" defer></script>
<script type="text/javascript" src="js/error/error_view.js" defer></script>
<script type="text/javascript" src="js/common/utils.js" defer></script>
<script type="text/javascript" src="js/common/details_view_mixin.js" defer></script>
<script type="text/javascript" src="js/common/list_view_mixin.js" defer></script>
<script type="text/javascript" src="js/common/card_swipe_mixin.js" defer></script>
<script type="text/javascript" src="js/common/swipeable.js" defer></script>
<script type="text/javascript" src="js/secure-element/se_utils.js" defer></script>
<script type="text/javascript" src="js/secure-element/apdu_helpers.js" defer></script>
<script type="text/javascript" src="js/settings/default_settings.js" defer></script>
<script type="text/javascript" src="js/secure-element/sim_access_manager.js" defer></script>
<script type="text/javascript" src="js/settings/settings_sub_view.js" defer></script>
<script type="text/javascript" src="js/settings/config_sub_view.js" defer></script>
<script type="text/javascript" src="js/settings/settings_view.js" defer></script>
<script type="text/javascript" src="js/settings/settings_controller.js" defer></script>
<script type="text/javascript" src="js/pin/pin_controller.js" defer></script>
<script type="text/javascript" src="js/pin/pin_view.js" defer></script>
<script type="text/javascript" src="js/applets/applet_model.js" defer></script>
<script type="text/javascript" src="js/applets/applet_details_view.js" defer></script>
<script type="text/javascript" src="js/applets/applet_list_view.js" defer></script>
<script type="text/javascript" src="js/applets/applet_list_controller.js" defer></script>
<script type="text/javascript" src="js/applets/hci_view.js" defer></script>
<script type="text/javascript" src="js/ext_data.js" defer></script>
<script type="text/javascript" src="js/app_view.js" defer></script>
<script type="text/javascript" src="js/app.js" defer></script>
</head>
<body>
<section role="region" id="global-error" class="vbox fit hide">
<header>
<h1 class="title">Mobile Wallet</h1>
</header>
<section class="vbox fit hbox middle center">
<article id="unknown-error" class="error-view">
<div class="error-img"></div>
<h2 class="error">Unknown problem occured</h2>
<p>
There's something wrong, but we're not sure what that is. Please contact your wallet provider for assistance.
</p>
</article>
<article id="no-sim" class="error-view hide">
<div class="error-img error-no-sim"></div>
<h2 class="error">Your SIM card is missing</h2>
<p>
We're not able to detect SIM card in your phone.
Please make sure it's inserted properly.
If the problem persists, please contact wallet provider for assistance.
</p>
</article>
<article id="no-api" class="error-view hide">
<div class="error-img error-api"></div>
<h2 class="error">SE API not detected</h2>
<p>
Most probably your phone has some compatibility issues or not up-to-date software.
Please make sure you're running the latest available version of Firefox OS.
If the problem persists, please contact your wallet provider for assistance.
</p>
</article>
<article id="sim-error" class="error-view hide">
<div class="error-img error-sim"></div>
<h2 class="error">SIM problem detected</h2>
<p>
It seems your SIM card has some technical issues. Please confirm that applet AID values in Settings are vaild and application is authorized to access them.
If the problem persists please contact your wallet provider for assistance.
</p>
<section class="footer">
<button class="error-settings-btn">Settings</button>
</section>
</article>
</section>
</section>
<section role="region" id="pin" class="vbox fit pin-view hide">
<header>
<h1 class="title">Mobile Wallet</h1>
</header>
<section id="pin-template" class="vbox fit hbox middle center">
<article class="pin-unblocked vbox middle center">
<div class="pin-top pin-entry">
<h2 class="pin-entry-text">Please enter your PIN</h2>
<div class="pin-warning error hide">
<p><span class="pin-attempts"></span></p>
</div>
<div class="pin-input">
</div>
</div>
<div class="pin-top hide vbox middle pin-incorrect">
<h2 class="pin-entry-error-text error">Incorrect PIN</h2>
</div>
<div class="pin-keypad">
<div class="pin-keypad-row">
<div class="pin-key" data-value="1">1</div>
<div class="pin-key" data-value="2">2</div>
<div class="pin-key" data-value="3">3</div>
</div>
<div class="pin-keypad-row">
<div class="pin-key" data-value="4">4</div>
<div class="pin-key" data-value="5">5</div>
<div class="pin-key" data-value="6">6</div>
</div>
<div class="pin-keypad-row">
<div class="pin-key" data-value="7">7</div>
<div class="pin-key" data-value="8">8</div>
<div class="pin-key" data-value="9">9</div>
</div>
<div class="pin-keypad-row">
<div class="pin-key clear-key" data-value="clear"></div>
<div class="pin-key" data-value="0">0</div>
<div class="pin-key gaia-icon icon-dialer-delete" data-value="backspace"></div>
</div>
</div>
</article>
<article class="pin-blocked error-view hide">
<div class="error-img error-locked"></div>
<h2 class="error">Your wallet was locked</h2>
<p>
We're sorry, but it looks like you entered incorrect PIN too many times.
Please unlock your wallet by entering PUK or contact your wallet provider for assistance.
</p>
<section class="footer">
<button class="pin-unblock-btn">Enter PUK</button>
</section>
</article>
<article class="puk-blocked error-view hide">
<div class="error-img error-locked"></div>
<h2 class="error">Your wallet was locked</h2>
<p>
We're sorry, but it looks like you entered incorrect PUK too many times.
Please contact your wallet provider for assistance.
</p>
</article>
<article class="pin-enable-error error-view hide">
<div class="error-img error-pin-enable"></div>
<h2 class="error">Enabling PIN lock failed</h2>
<p>
We're not able to enable PIN lock in your wallet. Most probably you're
entering incorrect PIN. If you haven't used wallet PIN lock before,
you might want to try the default value, which is
<span class="pin-default-val">0000</span>. If the problem persists, please
contact your wallet service provider.
<div class="pin-enable-warning error hide">
<p><span class="pin-enable-attempt">0 attempts</span> remaining</p>
</div>
</p>
</article>
</section>
</section>
<section role="region" id="main" class="vbox fit hide">
<header>
<menu type="toolbar">
<button class="gaia-icon icon-settings" id="settings-edit"></button>
</menu>
<h1 class="title">Mobile Wallet</h1>
</header>
<section id="cards-view" class="fit">
<article id="payments-view" class="vbox middle active">
<header>Your Payment cards</header>
<div class="swipeable-container">
<div class="swipeable-progress"></div>
<ul class="swipeable">
<li>
<div class="pocket">
<div class="sew"></div>
</div>
<img src="img/cards/no-cards-payment.png">
</li>
</ul>
</div>
<div id="payments-view-details" class="slider top-slide gaia-list">
<ul class="details">
<li><p class="aid">Card number</p><p class="val number"></p></li>
<li><p class="date">Valid thru</p><p class="val issued"></p></li>
<li class="transactions">
<p class="transaction"><span class="date"></span> <span class="price"></span><span class="currency"></span></p>
</li>
</ul>
</div>
</article>
<article id="nonpayments-view" class="bottom-slide">
<header>Your Other Cards</header>
<div class="swipeable-container" id="s2">
<div id="s2-progress" class="swipeable-progress"></div>
<ul class="swipeable">
<li>
<div class="pocket">
<div class="sew"></div>
</div>
<img src="img/cards/no-cards-other.png">
</li>
</ul>
</div>
<div id="nonpayments-view-details" class="slider gaia-list">
<ul class="nonpayment details"></ul>
</div>
</article>
</section>
</section>
<section role="region" id="settings" class="vbox fit scroll skin-organic">
<header>
<menu type="toolbar">
<button id="settings-done">Done</button>
</menu>
<h1 class="title ">Settings</h1>
</header>
<section class="gaia-list fit scroll">
<article>
<section>
<ul>
<li>
<gaia-switch id="fast-pay">
<label>Fast Pay Mode</label>
</label>
</li>
<li>
<gaia-switch id="pin-checkbox">
<label>Enable PIN</label>
</gaia-switch>
</li>
<li id="pin-change-btn" class="settings-disabled settings-forward">
<div>Change PIN</div>
<aside class="gaia-icon icon-forward"></aside>
</li>
<li id="about-btn" class="settings-forward">
<div>About</div>
<aside class="gaia-icon icon-forward"></aside>
</li>
<li id="adv-settings-btn" class="settings-forward">
<div>Advanced Settings</div>
<aside class="gaia-icon icon-forward"></aside>
</li>
</ul>
</section>
</article>
</section>
<section role="status" id="settings-status">
<p></p>
</section>
</section>
<section role="region" id="settings-about" class="settings-subpage vbox fit skin-organic">
<header>
<button class="gaia-icon icon-back"></button>
<h1 class="title ">About</h1>
</header>
<section class="gaia-list fit subview-content">
<div class="about-logo">
<p>Open Source</p>
<p>Mobile Wallet</p>
<img src="img/t-logo.png">
<p>by Deutsche Telekom</p>
</div>
<div class="about-authors">
<p>Code <strong>Krzysztof Mioduszewski</strong></p>
<p>Design <strong>Bartosz Ziółkowski</strong></p>
</div>
<div class="about-contributors">
<h3>Contributors</h3>
<div>
<p>Secure Element API Development</p>
<p>
<strong>
Garner Lee, Krzysztof Mioduszewski, Siddartha Pothapragada, Arno Puder
</strong>
</p>
<p>(Deutsche Telekom Silicon Valley Innovation Center)</p>
</div>
<div>
<p>SIM Integration Support</p>
<p>
<strong>
Andrzej Baraniak, Łukasz Leszek
</strong>
</p>
<p>(T-Mobile Polska)</p>
</div>
<div>
<p>Mobile App Consultancy</p>
<p>
<strong>
Adam Paziewski, Radosław Rzepkowski
</strong>
</p>
<p>(T-Mobile Polska)</p>
</div>
</div>
</section>
</section>
<section role="region" id="settings-pin" class="settings-subpage vbox fit scroll skin-organic pin-view">
<header>
<button class="gaia-icon icon-back"></button>
<h1 class="title ">PIN Toggle</h1>
</header>
<section class="gaia-list fit scroll subview-content">
</section>
</section>
<section role="region" id="settings-configuration" class="settings-subpage vbox fit scroll skin-organic">
<header>
<button class="gaia-icon icon-back"></button>
<menu type="toolbar">
<button id="settings-configuration-done">Done</button>
</menu>
<h1 class="title ">Advanced Settings</h1>
</header>
<section class="gaia-list fit scroll subview-content">
<section>
<p class="settings-disclaimer">
This menu lets you configure SE-related parameters.
Changing these settings can cause the app to stop working.
To restore original settings, please reinstall the app.
</p>
<header>Contactless Registry Service AID</header>
<p>
<input id="crs-aid" placeholder="CRS AID" required="true" type="text" pattern="[a-fA-F0-9]{10,32}">
</p>
<header>Universal Integrated Circuit Card AID</header>
<p>
<input id="uicc-aid" placeholder="UICC AID" required="true" type="text" pattern="[a-fA-F0-9]{10,32}">
</p>
<header>PIN P2</header>
<p>
<input id="pinP2" placeholder="PIN P2" required="true" type="text" pattern="[a-fA-F0-9]{2}">
</p>
<header>Default PIN</header>
<p>
<input id="default-pin" placeholder="Default PIN" required="true" type="text" pattern="[0-9]{4}">
</p>
</section>
</section>
</section>
<section role="region" id="hci-notification" class="vbox fit">
<div class="hci-card vbox hbox middle center">
<div class="hci-container">
<p>You paid using:</p>
<img id="hci-image" src="img/cards/payment_blue.png">
<p id="hci-number"></p>
</div>
</div>
<article class="notification vbox fit hbox bottom center">
<p class="bottom-note">Tap to dismiss</p>
</article>
</section>
</body>
</html>

View 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();
});

View 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));

View file

@ -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));

View file

@ -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));

View file

@ -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));

View file

@ -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));

View file

@ -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));

View file

@ -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));

View file

@ -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));

View file

@ -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));

View file

@ -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));

View file

@ -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));

View file

@ -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));

View file

@ -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));

View 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));

View file

@ -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));

View 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));

View file

@ -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));

View file

@ -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));

View file

@ -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));

View file

@ -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));

View file

@ -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));

View file

@ -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));

View file

@ -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));

View file

@ -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));

View file

@ -0,0 +1,24 @@
{
"version": "1.0.0",
"name": "Mobile Wallet",
"description": "New wallet version for FFOS 2.2",
"launch_path": "/index.html",
"icons": {
"84": "/img/icons/default_84.png",
"126": "/img/icons/default_126.png",
"142": "/img/icons/default_142.png",
"189": "/img/icons/default_189.png",
"284": "/img/icons/default_284.png"
},
"type": "certified",
"permissions": {
"secureelement-manage": {},
"nfc-hci-events":{}
},
"secure_element_access": ["*/A0*"],
"messages": [
{ "nfc-hci-event-transaction": "/index.html" }
],
"orientation": "portrait",
"default_locale": "en"
}