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.
202
gaia/dev_apps/mobile-wallet/LICENSE
Executable 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.
|
||||
|
||||
364
gaia/dev_apps/mobile-wallet/css/app.css
Executable 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;
|
||||
}
|
||||
}
|
||||
131
gaia/dev_apps/mobile-wallet/css/pin.css
Executable 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);
|
||||
}
|
||||
|
||||
172
gaia/dev_apps/mobile-wallet/css/settings.css
Executable 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;
|
||||
}
|
||||
70
gaia/dev_apps/mobile-wallet/css/swipeable.css
Executable 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;
|
||||
}
|
||||
}
|
||||
BIN
gaia/dev_apps/mobile-wallet/img/cards/no-cards-other.png
Executable file
|
After Width: | Height: | Size: 11 KiB |
BIN
gaia/dev_apps/mobile-wallet/img/cards/no-cards-payment.png
Executable file
|
After Width: | Height: | Size: 12 KiB |
BIN
gaia/dev_apps/mobile-wallet/img/cards/other-1.png
Executable file
|
After Width: | Height: | Size: 25 KiB |
BIN
gaia/dev_apps/mobile-wallet/img/cards/other-2.png
Executable file
|
After Width: | Height: | Size: 24 KiB |
BIN
gaia/dev_apps/mobile-wallet/img/cards/other-3.png
Executable file
|
After Width: | Height: | Size: 23 KiB |
BIN
gaia/dev_apps/mobile-wallet/img/cards/payment-1.png
Executable file
|
After Width: | Height: | Size: 21 KiB |
BIN
gaia/dev_apps/mobile-wallet/img/cards/payment-2.png
Executable file
|
After Width: | Height: | Size: 22 KiB |
BIN
gaia/dev_apps/mobile-wallet/img/cards/payment-3.png
Executable file
|
After Width: | Height: | Size: 22 KiB |
BIN
gaia/dev_apps/mobile-wallet/img/cards/unknown.png
Executable file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
gaia/dev_apps/mobile-wallet/img/errors/api-error.png
Executable file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
gaia/dev_apps/mobile-wallet/img/errors/error.png
Executable file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
gaia/dev_apps/mobile-wallet/img/errors/locked.png
Executable file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
gaia/dev_apps/mobile-wallet/img/errors/no-sim.png
Executable file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
gaia/dev_apps/mobile-wallet/img/errors/pin-enable-fail.png
Executable file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
gaia/dev_apps/mobile-wallet/img/errors/sim-error.png
Executable file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
gaia/dev_apps/mobile-wallet/img/icons/default_126.png
Executable file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
gaia/dev_apps/mobile-wallet/img/icons/default_142.png
Executable file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
gaia/dev_apps/mobile-wallet/img/icons/default_189.png
Executable file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
gaia/dev_apps/mobile-wallet/img/icons/default_284.png
Executable file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
gaia/dev_apps/mobile-wallet/img/icons/default_84.png
Executable file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
gaia/dev_apps/mobile-wallet/img/t-logo.png
Executable file
|
After Width: | Height: | Size: 7.4 KiB |
361
gaia/dev_apps/mobile-wallet/index.html
Executable 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>
|
||||
127
gaia/dev_apps/mobile-wallet/js/app.js
Executable file
|
|
@ -0,0 +1,127 @@
|
|||
/* globals AppView, ERRORS, SIMAccess, SettingsController, PinController,
|
||||
AppletListView, AppletListController, HCIView, SEUtils, PAYMENT_IMG_SRC,
|
||||
NONPAYMENT_IMG_SRC, PAYMENT_APPLET_FILTER, NONPAYMENT_APPLET_FILTER,
|
||||
SIM_UPDATE, SIM_REFRESH */
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
'use strict';
|
||||
|
||||
var log = (msg) => console.log('[main-ctrl] ' + msg);
|
||||
var appState = { initialised: false, sentToBackground: false };
|
||||
var appView = new AppView('main', 'global-error');
|
||||
|
||||
if(!window.navigator.seManager) {
|
||||
log('Secure Element API not available. Exiting.');
|
||||
appView.showErrorView(ERRORS.GLOBAL.NO_API);
|
||||
return;
|
||||
}
|
||||
|
||||
// components and views instantiation
|
||||
var pin = new PinController(SIMAccess, 'pin-ctrl', 'pin');
|
||||
|
||||
var settingsPin = new PinController(SIMAccess, 'settings-pin-ctrl',
|
||||
'settings-pin-view', 'pin-template');
|
||||
var settings = new SettingsController('settings-ctrl', 'settings',
|
||||
settingsPin);
|
||||
|
||||
var paymentsView = new AppletListView('payments-view', PAYMENT_IMG_SRC);
|
||||
var hciView = new HCIView('hci-notification', PAYMENT_IMG_SRC);
|
||||
var payments = new AppletListController(SIMAccess,'payments-ctrl',
|
||||
paymentsView, PAYMENT_APPLET_FILTER);
|
||||
|
||||
var nonPaymentsView = new AppletListView('nonpayments-view',
|
||||
NONPAYMENT_IMG_SRC);
|
||||
var nonPayments =
|
||||
new AppletListController(SIMAccess, 'nonpayments-ctrl', nonPaymentsView,
|
||||
NONPAYMENT_APPLET_FILTER, true);
|
||||
|
||||
// updating SIMAccess with settings read from localStorage
|
||||
SIMAccess.crsAid = settings.crsAid;
|
||||
SIMAccess.uiccAid = settings.uiccAid;
|
||||
SIMAccess.pinP2 = settings.pinP2;
|
||||
|
||||
// pin verification
|
||||
var performPinCheck = function performPinCheck() {
|
||||
if(settings.pinEnabled && !pin.pinCheckInProgress) {
|
||||
appView.hideMainView();
|
||||
return pin.verifyPin().then((verified) => {
|
||||
if(!verified) {
|
||||
return Promise.reject('Pin verification failed');
|
||||
}
|
||||
|
||||
appView.showMainView();
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
// app starting point
|
||||
var initApp = function intitApp() {
|
||||
appView.showMainView();
|
||||
SIMAccess.start()
|
||||
.then(performPinCheck)
|
||||
.then(() => payments.init())
|
||||
.then(() => nonPayments.init())
|
||||
.then(() => { appState.initialised = true; })
|
||||
.catch((error) => {
|
||||
log('Init promise chain broken. Reason: ' + error);
|
||||
appView.showErrorView(error);
|
||||
SIMAccess.stop();
|
||||
});
|
||||
};
|
||||
|
||||
// global events, system-messages handlers
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
log('Visibility change');
|
||||
if(!document.hidden && appState.initialised) {
|
||||
performPinCheck().catch((e) => appView.showErrorView(e));
|
||||
}
|
||||
|
||||
appState.sentToBackground = document.hidden;
|
||||
});
|
||||
|
||||
window.navigator.mozSetMessageHandler('nfc-hci-event-transaction', (msg) => {
|
||||
var shouldClose = !appState.initialised || appState.sentToBackground;
|
||||
var aid = SEUtils.byteToHexString(msg.aid);
|
||||
log('Got HCI EVT_TRANSACTION from AID ' + aid);
|
||||
|
||||
// SIM card is not accessible for the first 1.5s after HCI
|
||||
setTimeout(() => payments.handleHCIEvt(msg, settings.fastpayEnabled), 2000);
|
||||
hciView.show(aid, shouldClose);
|
||||
});
|
||||
|
||||
// coponent specific event handlers
|
||||
appView.addListener({ onEvent: (id, data) => {
|
||||
if(data.action === 'open-settings') {
|
||||
settings.show();
|
||||
appView.hideErrorView();
|
||||
}
|
||||
}});
|
||||
|
||||
settings.addListener({ onEvent: (id, data) => {
|
||||
log('Settings event: ' + JSON.stringify(data));
|
||||
if(data.error) {
|
||||
appView.showErrorView(data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if(data.action === 'settings-updated') {
|
||||
var refreshNeeded = false;
|
||||
Object.keys(data.changes).forEach(key => {
|
||||
refreshNeeded = refreshNeeded || SIM_REFRESH.indexOf(key) !== -1;
|
||||
|
||||
if(SIM_UPDATE.indexOf(key) !== -1) {
|
||||
SIMAccess[key] = data.changes[key];
|
||||
}
|
||||
});
|
||||
|
||||
if(refreshNeeded) {
|
||||
initApp();
|
||||
}
|
||||
}
|
||||
}});
|
||||
|
||||
log('Init access, getting applets from CRS');
|
||||
initApp();
|
||||
});
|
||||
53
gaia/dev_apps/mobile-wallet/js/app_view.js
Executable 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));
|
||||
32
gaia/dev_apps/mobile-wallet/js/applets/applet_details_view.js
Executable 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));
|
||||
136
gaia/dev_apps/mobile-wallet/js/applets/applet_list_controller.js
Executable 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));
|
||||
60
gaia/dev_apps/mobile-wallet/js/applets/applet_list_view.js
Executable 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));
|
||||
105
gaia/dev_apps/mobile-wallet/js/applets/applet_model.js
Executable 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));
|
||||
43
gaia/dev_apps/mobile-wallet/js/applets/hci_view.js
Executable 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));
|
||||
99
gaia/dev_apps/mobile-wallet/js/common/card_swipe_mixin.js
Executable 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));
|
||||
46
gaia/dev_apps/mobile-wallet/js/common/details_view_mixin.js
Executable 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));
|
||||
87
gaia/dev_apps/mobile-wallet/js/common/list_view_mixin.js
Executable 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));
|
||||
209
gaia/dev_apps/mobile-wallet/js/common/swipeable.js
Executable 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));
|
||||
82
gaia/dev_apps/mobile-wallet/js/common/utils.js
Executable 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));
|
||||
24
gaia/dev_apps/mobile-wallet/js/error/error_consts.js
Executable 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));
|
||||
73
gaia/dev_apps/mobile-wallet/js/error/error_view.js
Executable 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));
|
||||
66
gaia/dev_apps/mobile-wallet/js/ext_data.js
Executable 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));
|
||||
316
gaia/dev_apps/mobile-wallet/js/pin/pin_controller.js
Executable 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));
|
||||
274
gaia/dev_apps/mobile-wallet/js/pin/pin_view.js
Executable 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));
|
||||
116
gaia/dev_apps/mobile-wallet/js/secure-element/apdu_helpers.js
Executable 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));
|
||||
90
gaia/dev_apps/mobile-wallet/js/secure-element/se_utils.js
Executable 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));
|
||||
|
||||
187
gaia/dev_apps/mobile-wallet/js/secure-element/sim_access_manager.js
Executable 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));
|
||||
66
gaia/dev_apps/mobile-wallet/js/settings/config_sub_view.js
Executable 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));
|
||||
35
gaia/dev_apps/mobile-wallet/js/settings/default_settings.js
Executable 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));
|
||||
196
gaia/dev_apps/mobile-wallet/js/settings/settings_controller.js
Executable 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));
|
||||
49
gaia/dev_apps/mobile-wallet/js/settings/settings_sub_view.js
Executable 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));
|
||||
152
gaia/dev_apps/mobile-wallet/js/settings/settings_view.js
Executable 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));
|
||||
24
gaia/dev_apps/mobile-wallet/manifest.webapp
Executable 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"
|
||||
}
|
||||