[WO-233] Implement opt-in for terms of service

This commit is contained in:
Tankred Hase 2014-04-15 17:43:33 +02:00
parent 680ed6e0c3
commit becff37b4b
18 changed files with 161 additions and 26 deletions

View File

@ -59,8 +59,8 @@ Then visit [http://localhost:8580/dist/chrome.html#/desktop](http://localhost:85
We work together with existing open source projects wherever possible and contribute any changes we make back upstream. Many of theses libraries are licensed under an open source license. Here are some of them: We work together with existing open source projects wherever possible and contribute any changes we make back upstream. Many of theses libraries are licensed under an open source license. Here are some of them:
* [OpenPGP.js](http://openpgpjs.org): An implementation of OpenPGP in Javascript * [OpenPGP.js](http://openpgpjs.org) (LGPL license): An implementation of OpenPGP in Javascript
* [Inbox](https://github.com/andris9/inbox): Simple IMAP client for node.js * [Inbox](https://github.com/andris9/inbox) (MIT license): Simple IMAP client for node.js
* [Nodemailer](http://www.nodemailer.com): SMTP client for node.js * [Nodemailer](http://www.nodemailer.com) (MIT license): SMTP client for node.js
* [Forge](https://github.com/digitalbazaar/forge): An implementation of TLS in Javascript * [Forge](https://github.com/digitalbazaar/forge) (BSD license): An implementation of TLS in Javascript
* [node-shims](https://github.com/whiteout-io/node-shims): Shims for wrapping node's net/tls (TCP socket) APIs around chrome.socket * [node-shims](https://github.com/whiteout-io/node-shims) (MIT license): Shims for wrapping node's net/tls (TCP socket) APIs around chrome.socket

View File

@ -3,7 +3,7 @@ define(function(require) {
var _ = require('underscore'), var _ = require('underscore'),
app = {}, app = {},
cloudUrl, clientId; appVersion, cloudUrl, clientId;
// parse manifest to get configurations for current runtime // parse manifest to get configurations for current runtime
try { try {
@ -16,6 +16,8 @@ define(function(require) {
cloudUrl = cloudUrl.substring(0, cloudUrl.length - 1); cloudUrl = cloudUrl.substring(0, cloudUrl.length - 1);
// get client ID for OAuth requests // get client ID for OAuth requests
clientId = manifest.oauth2.client_id; clientId = manifest.oauth2.client_id;
// get the app version
appVersion = manifest.version;
} catch (e) {} } catch (e) {}
/** /**
@ -44,7 +46,8 @@ define(function(require) {
iconPath: '/img/icon.png', iconPath: '/img/icon.png',
verificationUrl: '/verify/', verificationUrl: '/verify/',
verificationUuidLength: 36, verificationUuidLength: 36,
dbVersion: 1 dbVersion: 1,
appVersion: appVersion
}; };
/** /**

View File

@ -9,6 +9,7 @@ requirejs([
'js/controller/account', 'js/controller/account',
'js/controller/set-passphrase', 'js/controller/set-passphrase',
'js/controller/contacts', 'js/controller/contacts',
'js/controller/about',
'js/controller/login', 'js/controller/login',
'js/controller/login-initial', 'js/controller/login-initial',
'js/controller/login-new-device', 'js/controller/login-new-device',
@ -29,6 +30,7 @@ requirejs([
AccountCtrl, AccountCtrl,
SetPassphraseCtrl, SetPassphraseCtrl,
ContactsCtrl, ContactsCtrl,
AboutCtrl,
LoginCtrl, LoginCtrl,
LoginInitialCtrl, LoginInitialCtrl,
LoginNewDeviceCtrl, LoginNewDeviceCtrl,
@ -96,6 +98,7 @@ requirejs([
app.controller('AccountCtrl', AccountCtrl); app.controller('AccountCtrl', AccountCtrl);
app.controller('SetPassphraseCtrl', SetPassphraseCtrl); app.controller('SetPassphraseCtrl', SetPassphraseCtrl);
app.controller('ContactsCtrl', ContactsCtrl); app.controller('ContactsCtrl', ContactsCtrl);
app.controller('AboutCtrl', AboutCtrl);
app.controller('DialogCtrl', DialogCtrl); app.controller('DialogCtrl', DialogCtrl);
app.controller('PopoverCtrl', PopoverCtrl); app.controller('PopoverCtrl', PopoverCtrl);

View File

@ -0,0 +1,32 @@
define(function(require) {
'use strict';
var cfg = require('js/app-config').config;
//
// Controller
//
var AboutCtrl = function($scope) {
$scope.state.about = {
open: false,
toggle: function(to) {
this.open = to;
}
};
//
// scope variables
//
$scope.version = cfg.appVersion;
$scope.date = new Date();
//
// scope functions
//
};
return AboutCtrl;
});

View File

@ -6,7 +6,7 @@ define(function(require) {
var LoginInitialCtrl = function($scope, $location) { var LoginInitialCtrl = function($scope, $location) {
var emailDao = appController._emailDao, var emailDao = appController._emailDao,
states; states, termsMsg = 'You must accept the Terms of Service to continue.';
// global state... inherited to all child scopes // global state... inherited to all child scopes
$scope.$root.state = {}; $scope.$root.state = {};
@ -25,6 +25,13 @@ define(function(require) {
// //
$scope.importKey = function() { $scope.importKey = function() {
if (!$scope.state.agree) {
$scope.onError({
message: termsMsg
});
return;
}
$location.path('/login-new-device'); $location.path('/login-new-device');
}; };
@ -89,6 +96,13 @@ define(function(require) {
return; return;
} }
if (!$scope.state.agree) {
$scope.onError({
message: termsMsg
});
return;
}
$scope.setState(states.PROCESSING); $scope.setState(states.PROCESSING);
setTimeout(function() { setTimeout(function() {
emailDao.unlock({ emailDao.unlock({

View File

@ -27,6 +27,7 @@
@import "views/account"; @import "views/account";
@import "views/set-passphrase"; @import "views/set-passphrase";
@import "views/contacts"; @import "views/contacts";
@import "views/about";
@import "views/dialog"; @import "views/dialog";
@import "views/navigation"; @import "views/navigation";
@import "views/mail-list"; @import "views/mail-list";

View File

@ -2,11 +2,6 @@
padding: 0px; padding: 0px;
color: $color-grey-dark; color: $color-grey-dark;
@include respond-to(mobile) {
top: 0;
max-width: 100%;
}
.control { .control {
float: right; float: right;

View File

@ -0,0 +1,15 @@
.view-about {
@include respond-to(desktop) {
max-width: 550px;
}
.content {
margin: 30px auto;
text-align: center;
a {
color: $color-blue;
}
}
}

View File

@ -1,7 +1,10 @@
.view-dialog { .view-dialog {
max-width: 350px;
height: auto; height: auto;
top: 30%;
@include respond-to(desktop) {
max-width: 350px;
top: 30%;
}
p { p {
text-align: center; text-align: center;

View File

@ -24,12 +24,12 @@
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
b, a {
color: $color-blue;
}
p { p {
line-height: 150%; line-height: 150%;
b, a {
color: $color-blue;
}
} }
div { div {
@ -69,6 +69,13 @@
.passphrase-label-ok { .passphrase-label-ok {
color: green; color: green;
} }
.opt-in-terms {
.checkbox-wrapper {
margin-top: 0;
float: left;
}
}
} }
} }
@ -76,7 +83,9 @@
} }
.view-login-initial { .view-login-initial {
button { .content {
margin-right: 10px; button {
margin-right: 10px;
}
} }
} }

15
src/tpl/about.html Normal file
View File

@ -0,0 +1,15 @@
<div class="lightbox-body" ng-controller="AboutCtrl">
<header>
<button class="close" ng-click="state.about.toggle(false)" data-action="lightbox-close">&#xe007;</button>
</header>
<div class="content">
<img src="img/icon.png">
<h2>Whiteout Mail</h2>
<p>Version {{version}}</p>
<p>&copy; {{ date | date:'yyyy'}} Whiteout Networks GmbH</p>
<p><a href="https://whiteout.io/imprint.html" target="_blank">Imprint</a> &middot; <a href="https://whiteout.io/privacy.html" target="_blank">Privacy</a> &middot; <a href="https://whiteout.io/terms.html" target="_blank">Terms</a> &middot; <a href="https://github.com/whiteout-io/mail-html5#license" target="_blank">Licenses</a></p>
</div><!-- /.content -->
</div><!-- /.lightbox-body -->

View File

@ -31,6 +31,9 @@
<div class="lightbox-overlay" ng-class="{'show': state.contacts.open}"> <div class="lightbox-overlay" ng-class="{'show': state.contacts.open}">
<div class="lightbox lightbox-effect" ng-include="'tpl/contacts.html'"></div> <div class="lightbox lightbox-effect" ng-include="'tpl/contacts.html'"></div>
</div> </div>
<div class="lightbox-overlay" ng-class="{'show': state.about.open}">
<div class="lightbox lightbox-effect view-about" ng-include="'tpl/about.html'"></div>
</div>
<div class="lightbox-overlay" ng-class="{'show': state.dialog.open}"> <div class="lightbox-overlay" ng-class="{'show': state.dialog.open}">
<div class="lightbox lightbox-effect dialog view-dialog" ng-include="'tpl/dialog.html'"></div> <div class="lightbox lightbox-effect dialog view-dialog" ng-include="'tpl/dialog.html'"></div>
</div> </div>

View File

@ -20,7 +20,7 @@
<!-- lightbox --> <!-- lightbox -->
<div class="lightbox-overlay" ng-class="{'show': state.dialog.open}"> <div class="lightbox-overlay" ng-class="{'show': state.dialog.open}">
<div class="lightbox lightbox-effect view-dialog" ng-include="'tpl/dialog.html'"></div> <div class="lightbox lightbox-effect dialog view-dialog" ng-include="'tpl/dialog.html'"></div>
</div> </div>
<!-- popovers --> <!-- popovers -->

View File

@ -14,6 +14,12 @@
<input class="input-text" type="password" ng-model="state.confirmation" ng-class="{'input-text-error': (state.confirmation || state.passphrase) && state.confirmation !== state.passphrase}" placeholder="Confirm passphrase" tabindex="2"> <input class="input-text" type="password" ng-model="state.confirmation" ng-class="{'input-text-error': (state.confirmation || state.passphrase) && state.confirmation !== state.passphrase}" placeholder="Confirm passphrase" tabindex="2">
<span class="popover-info" data-icon-append="&#xe010;" popover="#passphrase-info"></span> <span class="popover-info" data-icon-append="&#xe010;" popover="#passphrase-info"></span>
</div> </div>
<div class="opt-in-terms">
<div class="checkbox-wrapper"><input type="checkbox" ng-model="state.agree"></div>
<div>I agree to the Whiteout Networks <a href="https://whiteout.io/terms.html" target="_blank">Terms of Service</a> and have read the <a href="https://whiteout.io/privacy.html" target="_blank">Privacy Policy</a>.</div>
</div>
<div> <div>
<button type="submit" ng-click="confirmPassphrase()" class="btn" ng-disabled="(state.confirmation || state.passphrase) && state.confirmation !== state.passphrase" tabindex="3">Continue</button> <button type="submit" ng-click="confirmPassphrase()" class="btn" ng-disabled="(state.confirmation || state.passphrase) && state.confirmation !== state.passphrase" tabindex="3">Continue</button>
</div> </div>
@ -29,7 +35,7 @@
<!-- lightbox --> <!-- lightbox -->
<div class="lightbox-overlay" ng-class="{'show': state.dialog.open}"> <div class="lightbox-overlay" ng-class="{'show': state.dialog.open}">
<div class="lightbox lightbox-effect view-dialog" ng-include="'tpl/dialog.html'"></div> <div class="lightbox lightbox-effect dialog view-dialog" ng-include="'tpl/dialog.html'"></div>
</div> </div>
<!-- popovers --> <!-- popovers -->

View File

@ -22,7 +22,7 @@
<!-- lightbox --> <!-- lightbox -->
<div class="lightbox-overlay" ng-class="{'show': state.dialog.open}"> <div class="lightbox-overlay" ng-class="{'show': state.dialog.open}">
<div class="lightbox lightbox-effect view-dialog" ng-include="'tpl/dialog.html'"></div> <div class="lightbox lightbox-effect dialog view-dialog" ng-include="'tpl/dialog.html'"></div>
</div> </div>
<!-- popovers --> <!-- popovers -->

View File

@ -12,5 +12,5 @@
<!-- lightbox --> <!-- lightbox -->
<div class="lightbox-overlay" ng-class="{'show': state.dialog.open}"> <div class="lightbox-overlay" ng-class="{'show': state.dialog.open}">
<div class="lightbox lightbox-effect view-dialog" ng-include="'tpl/dialog.html'"></div> <div class="lightbox lightbox-effect dialog view-dialog" ng-include="'tpl/dialog.html'"></div>
</div> </div>

View File

@ -17,7 +17,7 @@
<ul class="nav-secondary"> <ul class="nav-secondary">
<li><a href="#" ng-click="state.account.toggle(true); $event.preventDefault()">Account</a></li> <li><a href="#" ng-click="state.account.toggle(true); $event.preventDefault()">Account</a></li>
<li><a href="#" ng-click="state.contacts.toggle(true); $event.preventDefault()">Contacts</a></li> <li><a href="#" ng-click="state.contacts.toggle(true); $event.preventDefault()">Contacts</a></li>
<li><a href="http://whiteout.io" target="_blank">About</a></li> <li><a href="#" ng-click="state.about.toggle(true); $event.preventDefault()">About</a></li>
</ul> </ul>
<footer> <footer>

View File

@ -58,6 +58,25 @@ define(function(require) {
}); });
}); });
describe('go to import key', function() {
it('should not continue if terms are not accepted', function(done) {
scope.state.agree = undefined;
scope.onError = function(err) {
expect(err.message).to.contain('Terms');
done();
};
scope.importKey();
});
it('should work', function() {
scope.state.agree = true;
scope.importKey();
expect(location.$$path).to.equal('/login-new-device');
});
});
describe('check passphrase quality', function() { describe('check passphrase quality', function() {
it('should be too short', function() { it('should be too short', function() {
scope.state.passphrase = '&§DG36'; scope.state.passphrase = '&§DG36';
@ -103,9 +122,24 @@ define(function(require) {
describe('confirm passphrase', function() { describe('confirm passphrase', function() {
var setStateStub; var setStateStub;
it('should not continue if terms are not accepted', function(done) {
scope.state.passphrase = passphrase;
scope.state.confirmation = passphrase;
scope.state.agree = undefined;
scope.onError = function(err) {
expect(err.message).to.contain('Terms');
done();
};
scope.confirmPassphrase();
});
it('should unlock crypto', function(done) { it('should unlock crypto', function(done) {
scope.state.passphrase = passphrase; scope.state.passphrase = passphrase;
scope.state.confirmation = passphrase; scope.state.confirmation = passphrase;
scope.state.agree = true;
emailDaoMock.unlock.withArgs({ emailDaoMock.unlock.withArgs({
passphrase: passphrase passphrase: passphrase
}).yields(); }).yields();
@ -129,6 +163,8 @@ define(function(require) {
it('should not work when keypair generation fails', function(done) { it('should not work when keypair generation fails', function(done) {
scope.state.passphrase = passphrase; scope.state.passphrase = passphrase;
scope.state.confirmation = passphrase; scope.state.confirmation = passphrase;
scope.state.agree = true;
emailDaoMock.unlock.withArgs({ emailDaoMock.unlock.withArgs({
passphrase: passphrase passphrase: passphrase
}).yields(new Error('asd')); }).yields(new Error('asd'));