summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.editorconfig6
-rw-r--r--Makefile15
-rw-r--r--README.md8
-rw-r--r--circle.yml32
-rw-r--r--doc/images/account-recovery.gifbin0 -> 10166747 bytes
-rw-r--r--service/pixelated/account_recovery.py56
-rw-r--r--service/pixelated/assets/recovery.mail.en-US28
-rw-r--r--service/pixelated/assets/recovery.mail.pt-BR26
-rw-r--r--service/pixelated/resources/account_recovery_resource.py87
-rw-r--r--service/pixelated/resources/backup_account_resource.py25
-rw-r--r--service/pixelated/resources/login_resource.py14
-rw-r--r--service/pixelated/resources/root_resource.py4
-rw-r--r--service/pixelated/support/language.py24
-rw-r--r--service/requirements.txt2
-rw-r--r--service/test/functional/features/account_recovery.feature43
-rw-r--r--service/test/functional/features/environment.py41
-rw-r--r--service/test/functional/features/page_objects/__init__.py20
-rw-r--r--service/test/functional/features/page_objects/account_recovery_page.py57
-rw-r--r--service/test/functional/features/page_objects/backup_account_page.py30
-rw-r--r--service/test/functional/features/page_objects/base_page.py39
-rw-r--r--service/test/functional/features/page_objects/inbox_page.py52
-rw-r--r--service/test/functional/features/smoke.feature8
-rw-r--r--service/test/functional/features/steps/__init__.py2
-rw-r--r--service/test/functional/features/steps/account_recovery.py49
-rw-r--r--service/test/functional/features/steps/backup_account.py20
-rw-r--r--service/test/functional/features/steps/common.py4
-rw-r--r--service/test/functional/features/steps/login.py30
-rw-r--r--service/test/functional/features/steps/mail_list.py18
-rw-r--r--service/test/functional/features/steps/mail_view.py23
-rw-r--r--service/test/functional/features/steps/signup.py21
-rw-r--r--service/test/functional/features/steps/utils.py51
-rw-r--r--service/test/unit/config/test_site.py4
-rw-r--r--service/test/unit/resources/test_account_recovery_resource.py108
-rw-r--r--service/test/unit/resources/test_backup_account_resource.py29
-rw-r--r--service/test/unit/resources/test_login_resource.py33
-rw-r--r--service/test/unit/support/test_language.py40
-rw-r--r--service/test/unit/test_account_recovery.py93
-rw-r--r--web-ui/app/images/account-recovery/admins_contact.svg39
-rw-r--r--web-ui/app/images/account-recovery/codes.svg37
-rw-r--r--web-ui/app/images/account-recovery/step_1.svg52
-rw-r--r--web-ui/app/images/account-recovery/step_2.svg53
-rw-r--r--web-ui/app/images/account-recovery/step_3.svg53
-rw-r--r--web-ui/app/images/account-recovery/step_4.svg52
-rw-r--r--web-ui/app/locales/en_US/translation.json37
-rw-r--r--web-ui/app/locales/pt_BR/translation.json37
-rw-r--r--web-ui/config/protected-assets-webpack.js1
-rw-r--r--web-ui/config/public-assets-webpack.js1
-rw-r--r--web-ui/package.json2
-rw-r--r--web-ui/src/account_recovery/account_recovery.html14
-rw-r--r--web-ui/src/account_recovery/account_recovery.js32
-rw-r--r--web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.js54
-rw-r--r--web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.scss22
-rw-r--r--web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.spec.js38
-rw-r--r--web-ui/src/account_recovery/backup_account_step/backup_account_step.js40
-rw-r--r--web-ui/src/account_recovery/backup_account_step/backup_account_step.spec.js27
-rw-r--r--web-ui/src/account_recovery/new_password_form/new_password_form.js111
-rw-r--r--web-ui/src/account_recovery/new_password_form/new_password_form.scss22
-rw-r--r--web-ui/src/account_recovery/new_password_form/new_password_form.spec.js161
-rw-r--r--web-ui/src/account_recovery/page.js95
-rw-r--r--web-ui/src/account_recovery/page.scss121
-rw-r--r--web-ui/src/account_recovery/page.spec.js91
-rw-r--r--web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.js59
-rw-r--r--web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.scss39
-rw-r--r--web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.spec.js55
-rw-r--r--web-ui/src/backup_account/backup_email/backup_email.js43
-rw-r--r--web-ui/src/backup_account/backup_email/backup_email.spec.js72
-rw-r--r--web-ui/src/backup_account/confirmation/confirmation.js11
-rw-r--r--web-ui/src/backup_account/confirmation/confirmation.spec.js4
-rw-r--r--web-ui/src/backup_account/page.js13
-rw-r--r--web-ui/src/backup_account/page.scss14
-rw-r--r--web-ui/src/backup_account/page.spec.js30
-rw-r--r--web-ui/src/common/back_link/back_link.js42
-rw-r--r--web-ui/src/common/back_link/back_link.scss35
-rw-r--r--web-ui/src/common/back_link/back_link.spec.js41
-rw-r--r--web-ui/src/common/header/header.js12
-rw-r--r--web-ui/src/common/header/header.spec.js13
-rw-r--r--web-ui/src/common/link_button/link_button.js58
-rw-r--r--web-ui/src/common/link_button/link_button.scss49
-rw-r--r--web-ui/src/common/link_button/link_button.spec.js20
-rw-r--r--web-ui/src/common/snackbar_notification/snackbar_notification.js65
-rw-r--r--web-ui/src/common/snackbar_notification/snackbar_notification.scss22
-rw-r--r--web-ui/src/common/snackbar_notification/snackbar_notification.spec.js31
-rw-r--r--web-ui/src/common/submit_button/submit_button.js3
-rw-r--r--web-ui/src/common/util.js21
-rw-r--r--web-ui/src/common/util.spec.js35
-rw-r--r--web-ui/src/login/opensans.css20
-rw-r--r--web-ui/test/integration/account_recovery.spec.js59
-rw-r--r--web-ui/test/integration/translations.spec.js34
-rw-r--r--web-ui/webpack.config.js3
-rw-r--r--web-ui/webpack.production.config.js11
90 files changed, 3045 insertions, 203 deletions
diff --git a/.editorconfig b/.editorconfig
index 33d6e755..adfa8881 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -10,5 +10,11 @@ trim_trailing_whitespace = true
[*.js]
indent_size = 2
+[*.feature]
+indent_size = 2
+
[*.py]
indent_size = 4
+
+[Makefile]
+indent_style = tab
diff --git a/Makefile b/Makefile
index df0902c3..ed4ab18b 100644
--- a/Makefile
+++ b/Makefile
@@ -54,6 +54,7 @@ linters_js:
coverage:
@. $(VIRTUALENV)/bin/activate;\
cd service;\
+ export PYTHONPATH=$(PYTHONPATH):`pwd`;\
coverage run -p --source=pixelated `which trial` test.unit;\
coverage run -p --source=pixelated `which trial` test.integration;\
coverage combine;\
@@ -62,6 +63,7 @@ coverage:
unit_tests_py:
@. $(VIRTUALENV)/bin/activate;\
cd service;\
+ export PYTHONPATH=$(PYTHONPATH):`pwd`;\
trial --reporter=text test.unit
unit_tests_js:
@@ -71,6 +73,7 @@ unit_tests_js:
integration_tests_py:
@. $(VIRTUALENV)/bin/activate;\
cd service;\
+ export PYTHONPATH=$(PYTHONPATH):`pwd`;\
trial --reporter=text test.integration
functional_tests: clean requirements install
@@ -79,11 +82,23 @@ functional_tests: clean requirements install
cd service;\
xvfb-run --server-args="-screen 0 1280x1024x24" behave --tags ~@wip --tags ~@smoke test/functional/features
+smoke_tests: clean install
+ @. $(VIRTUALENV)/bin/activate;\
+ export PATH=$(PATH):/usr/lib/chromium/;\
+ cd service;\
+ xvfb-run --server-args="-screen 0 1280x1024x24" behave --tags ~@wip --tags @smoke test/functional/features -k -D host=$(provider)
+
functional_tests_ci: clean requirements install
@. $(VIRTUALENV)/bin/activate;\
cd service;\
behave --tags ~@wip --tags ~@smoke test/functional/features
+functional_tests_wip:
+ @. $(VIRTUALENV)/bin/activate;\
+ export PATH=$(PATH):/usr/lib/chromium/;\
+ cd service;\
+ xvfb-run --server-args="-screen 0 1280x1024x24" behave --tags @wip test/functional/features
+
ensure_virtualenv_installed:
@if [ ! `which virtualenv` ]; then\
echo "Virtualenv must be installed";\
diff --git a/README.md b/README.md
index c03cf95d..a2083f5d 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
Pixelated User Agent
====================
-[![Build Status](https://snap-ci.com/pixelated/pixelated-user-agent/branch/master/build_image)](https://snap-ci.com/pixelated/pixelated-user-agent/branch/master) [
-![Coverage Status](https://coveralls.io/repos/pixelated/pixelated-user-agent/badge.svg?branch=master)](https://coveralls.io/r/pixelated/pixelated-user-agent?branch=master)
+[![CircleCI](https://circleci.com/gh/pixelated/pixelated-user-agent.svg?style=svg)](https://circleci.com/gh/pixelated/pixelated-user-agent)
+[![Coverage Status](https://coveralls.io/repos/pixelated/pixelated-user-agent/badge.svg?branch=master)](https://coveralls.io/r/pixelated/pixelated-user-agent?branch=master)
[![Stories in Doing](https://badge.waffle.io/pixelated/pixelated-user-agent.svg?label=doing&title=Doing)](http://waffle.io/pixelated/pixelated-user-agent)
The Pixelated User Agent is the mail client of the Pixelated ecosystem. It is composed of two parts, a web interface written in JavaScript ([FlightJS](https://flightjs.github.io/)) and a Python API that interacts with a LEAP Provider, the e-mail platform that Pixelated is built on.
@@ -23,6 +23,10 @@ You are most welcome to contribute to the pixelated user agent code base. Please
## Installing Pixelated
+To run your own instance of Pixelated, follow these instructions: https://github.com/pixelated/puppet-pixelated#manual-installation
+
+## Development
+
You like the idea and you want to run it locally, then before you have to install the following packages:
* [Vagrant](https://www.vagrantup.com/downloads.html), Vagrant is a tool that automates the setup of a virtual machine with the development environment
diff --git a/circle.yml b/circle.yml
new file mode 100644
index 00000000..3a2c4773
--- /dev/null
+++ b/circle.yml
@@ -0,0 +1,32 @@
+machine:
+ python:
+ version:
+ 2.7.11
+ node:
+ version:
+ 6.1.0
+ environment:
+ LC_ALL: 'en_US.UTF-8'
+
+general:
+ artifacts:
+ - "service/screenshots"
+
+dependencies:
+ override:
+ - make clean_all
+ - pip install coveralls
+ - gem install compass && rbenv rehash
+
+test:
+ override:
+ - make linters
+ - make test
+ - make functional_tests_ci
+ - cd service && coveralls || true
+
+deployment:
+ packaging:
+ branch: master
+ commands:
+ - "curl -X POST --insecure -H 'Confirm: true' --fail --data \"materials[pixelated-user-agent]=$CIRCLE_SHA1\" https://$USERPASS@go.pixelated-project.org/go/api/pipelines/jessie-pixelated-user-agent-packaging/schedule"
diff --git a/doc/images/account-recovery.gif b/doc/images/account-recovery.gif
new file mode 100644
index 00000000..643e2f38
--- /dev/null
+++ b/doc/images/account-recovery.gif
Binary files differ
diff --git a/service/pixelated/account_recovery.py b/service/pixelated/account_recovery.py
index 234bb1fe..58242a7d 100644
--- a/service/pixelated/account_recovery.py
+++ b/service/pixelated/account_recovery.py
@@ -13,23 +13,73 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+import pkg_resources
+import binascii
+from email import message_from_string
+
from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.logger import Logger
+from twisted.mail import smtp
+
+from pixelated.support import date
log = Logger()
class AccountRecovery(object):
- def __init__(self, session, soledad):
+ def __init__(self, session, soledad, smtp_config, backup_email, domain, language='en-US'):
self._bonafide_session = session
self._soledad = soledad
+ self._smtp_config = smtp_config
+ self._backup_email = backup_email
+ self._domain = domain
+ self._language = language
@inlineCallbacks
def update_recovery_code(self):
+ log.info('Updating user\'s recovery code')
+
try:
code = self._soledad.create_recovery_code()
response = yield self._bonafide_session.update_recovery_code(code)
+ yield self._send_mail(code, self._backup_email)
+
returnValue(response)
+
+ except Exception as e:
+ log.error('Something went wrong when trying to save the recovery code')
+ log.error(e)
+ raise e
+
+ @inlineCallbacks
+ def _send_mail(self, code, backup_email):
+ log.info('Sending mail containing the user\'s recovery code')
+
+ sender = 'team@{}'.format(self._domain)
+ msg = self._get_recovery_mail(code, sender, backup_email)
+
+ try:
+ send_mail_result = yield smtp.sendmail(
+ str(self._smtp_config.remote_smtp_host),
+ sender,
+ [backup_email],
+ msg.as_string())
+ returnValue(send_mail_result)
except Exception as e:
- log.warn('Something went wrong when trying to save the recovery code')
- raise
+ log.error('Failed trying to send the email with the recovery code')
+ raise e
+
+ def _get_recovery_mail(self, code, sender, backup_email):
+ email_date = date.mail_date_now()
+ recovery_mail = pkg_resources.resource_filename(
+ 'pixelated.assets',
+ 'recovery.mail.%s' % (self._language))
+
+ with open(recovery_mail) as mail_template_file:
+ return message_from_string(mail_template_file.read().format(
+ domain=self._domain,
+ recovery_code=binascii.hexlify(code),
+ backup_email=backup_email,
+ sender=sender,
+ date=email_date))
diff --git a/service/pixelated/assets/recovery.mail.en-US b/service/pixelated/assets/recovery.mail.en-US
new file mode 100644
index 00000000..e7a09f1b
--- /dev/null
+++ b/service/pixelated/assets/recovery.mail.en-US
@@ -0,0 +1,28 @@
+From: {sender}
+Date: {date}
+Subject: Recovery Code for {domain}
+To: {backup_email}
+Content-Type: text/plain; charset=UTF-8
+
+Hello,
+
+You are receiving this message because you registered an email account at https://{domain}.
+If you ever forget your password, you'll need the code below to recover it. Save it! It is the only way to access to your account again.
+
+{recovery_code}
+
+Save this message or write this code in a safe place.
+
+--
+Why is this so important?
+
+Pixelated is an email client that respects your privacy and uses PGP Encryption to do so. Your password also gives you access to your keys, so if you forget it you will lose access to your account and the ability to read your messages.
+Forgetting passwords is a common thing, so we developed a more secure way to recover access to your account.
+
+1) This code is half of a big code to recover your account.
+2) The other half is with the account administrator.
+3) In case you forget your password, use this code and your administrator code to recover access to your account. It's like those locks with two keys :)
+
+
+
+PS: If you didn't create an account at https://{domain}, please ignore this email.
diff --git a/service/pixelated/assets/recovery.mail.pt-BR b/service/pixelated/assets/recovery.mail.pt-BR
new file mode 100644
index 00000000..558c6905
--- /dev/null
+++ b/service/pixelated/assets/recovery.mail.pt-BR
@@ -0,0 +1,26 @@
+From: {sender}
+Date: {date}
+Subject: Codigo de Recuperacao de {domain}
+To: {backup_email}
+Content-Type: text/plain; charset=UTF-8
+
+Olá,
+
+Você está recebendo isso porque você registrou um email no https://{domain}.
+Guarde o código abaixo para se um dia esquecer sua senha. Ele é a única forma de recuperar uma senha para acessar sua conta outra vez:
+
+{recovery_code}
+
+Salve essa mensagem ou anote o código em um lugar seguro.
+
+--
+Por que isso é importante?
+
+O Pixelated é um cliente de email que respeita sua privacidade e usa criptografia PGP para isso. Sua senha também dá acesso às suas chaves, então se você esquecê-la você perderá acesso a sua conta e a habilidade de ler suas mensagens. Esquecer a senha é algo comum, por isso desenvolvemos uma forma mais segura de recuperar sua conta.
+
+1) Esse código é uma metade de um código necessário para recuperar a conta.
+2) A outra metade está com o administrador da conta.
+3) Se você esquecer a senha, use esse código e o do administrador para recuperar acesso a conta. É como se fosse um cadeado com duas chaves :)
+
+
+PS: Se você não criou uma conta no site https://{domain}, por favor ignore esse email.
diff --git a/service/pixelated/resources/account_recovery_resource.py b/service/pixelated/resources/account_recovery_resource.py
new file mode 100644
index 00000000..209a7693
--- /dev/null
+++ b/service/pixelated/resources/account_recovery_resource.py
@@ -0,0 +1,87 @@
+#
+# Copyright (c) 2017 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import json
+
+from twisted.python.filepath import FilePath
+from twisted.web.http import OK, INTERNAL_SERVER_ERROR
+from twisted.web.template import Element, XMLFile, renderElement
+from twisted.web.server import NOT_DONE_YET
+from twisted.internet import defer
+from twisted.logger import Logger
+
+from pixelated.resources import BaseResource
+from pixelated.resources import get_public_static_folder
+
+log = Logger()
+
+
+class InvalidPasswordError(Exception):
+ pass
+
+
+class AccountRecoveryPage(Element):
+ loader = XMLFile(FilePath(os.path.join(get_public_static_folder(), 'account_recovery.html')))
+
+ def __init__(self):
+ super(AccountRecoveryPage, self).__init__()
+
+
+class AccountRecoveryResource(BaseResource):
+ BASE_URL = 'account-recovery'
+ isLeaf = True
+
+ def __init__(self, services_factory):
+ BaseResource.__init__(self, services_factory)
+
+ def render_GET(self, request):
+ request.setResponseCode(OK)
+ return self._render_template(request)
+
+ def _render_template(self, request):
+ site = AccountRecoveryPage()
+ return renderElement(request, site)
+
+ def render_POST(self, request):
+ def success_response(response):
+ request.setResponseCode(OK)
+ request.finish()
+
+ def error_response(failure):
+ log.warn(failure)
+ request.setResponseCode(INTERNAL_SERVER_ERROR)
+ request.finish()
+
+ d = self._handle_post(request)
+ d.addCallbacks(success_response, error_response)
+ return NOT_DONE_YET
+
+ def _get_post_form(self, request):
+ return json.loads(request.content.getvalue())
+
+ def _validate_password(self, password, confirm_password):
+ return password == confirm_password and len(password) >= 8 and len(password) <= 9999
+
+ def _handle_post(self, request):
+ form = self._get_post_form(request)
+ password = form.get('password')
+ confirm_password = form.get('confirmPassword')
+
+ if not self._validate_password(password, confirm_password):
+ return defer.fail(InvalidPasswordError('The user entered an invalid password or confirmation'))
+
+ return defer.succeed('Done!')
diff --git a/service/pixelated/resources/backup_account_resource.py b/service/pixelated/resources/backup_account_resource.py
index b752b4c7..94129122 100644
--- a/service/pixelated/resources/backup_account_resource.py
+++ b/service/pixelated/resources/backup_account_resource.py
@@ -15,16 +15,18 @@
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
import os
-from xml.sax import SAXParseException
+import json
-from pixelated.resources import BaseResource
from twisted.python.filepath import FilePath
-from pixelated.resources import get_protected_static_folder
-from pixelated.account_recovery import AccountRecovery
from twisted.web.http import OK, NO_CONTENT, INTERNAL_SERVER_ERROR
from twisted.web.server import NOT_DONE_YET
from twisted.web.template import Element, XMLFile, renderElement
+from pixelated.resources import BaseResource
+from pixelated.resources import get_protected_static_folder
+from pixelated.account_recovery import AccountRecovery
+from pixelated.support.language import parse_accept_language
+
class BackupAccountPage(Element):
loader = XMLFile(FilePath(os.path.join(get_protected_static_folder(), 'backup_account.html')))
@@ -36,9 +38,10 @@ class BackupAccountPage(Element):
class BackupAccountResource(BaseResource):
isLeaf = True
- def __init__(self, services_factory, authenticator):
+ def __init__(self, services_factory, authenticator, leap_provider):
BaseResource.__init__(self, services_factory)
self._authenticator = authenticator
+ self._leap_provider = leap_provider
def render_GET(self, request):
request.setResponseCode(OK)
@@ -51,7 +54,11 @@ class BackupAccountResource(BaseResource):
def render_POST(self, request):
account_recovery = AccountRecovery(
self._authenticator.bonafide_session,
- self.soledad(request))
+ self.soledad(request),
+ self._service(request, '_leap_session').smtp_config,
+ self._get_backup_email(request),
+ self._leap_provider.server_name,
+ language=self._get_language(request))
def update_response(response):
request.setResponseCode(NO_CONTENT)
@@ -64,3 +71,9 @@ class BackupAccountResource(BaseResource):
d = account_recovery.update_recovery_code()
d.addCallbacks(update_response, error_response)
return NOT_DONE_YET
+
+ def _get_backup_email(self, request):
+ return json.loads(request.content.getvalue()).get('backupEmail')
+
+ def _get_language(self, request):
+ return parse_accept_language(request.getAllHeaders())
diff --git a/service/pixelated/resources/login_resource.py b/service/pixelated/resources/login_resource.py
index 3e1200d7..5b0b70d0 100644
--- a/service/pixelated/resources/login_resource.py
+++ b/service/pixelated/resources/login_resource.py
@@ -20,7 +20,10 @@ from xml.sax import SAXParseException
from pixelated.authentication import Authenticator
from pixelated.config.leap import BootstrapUserServices
from pixelated.resources import BaseResource, UnAuthorizedResource, IPixelatedSession
+from pixelated.resources.account_recovery_resource import AccountRecoveryResource
from pixelated.resources import get_public_static_folder, respond_json
+from pixelated.support.language import parse_accept_language
+
from twisted.cred.error import UnauthorizedLogin
from twisted.internet import defer
from twisted.logger import Logger
@@ -35,15 +38,6 @@ from twisted.web.template import Element, XMLFile, renderElement, renderer
log = Logger()
-def parse_accept_language(all_headers):
- accepted_languages = ['pt-BR', 'en-US']
- languages = all_headers.get('accept-language', '').split(';')[0]
- for language in accepted_languages:
- if language in languages:
- return language
- return 'pt-BR'
-
-
class DisclaimerElement(Element):
loader = XMLFile(FilePath(os.path.join(get_public_static_folder(), '_login_disclaimer_banner.html')))
@@ -101,6 +95,8 @@ class LoginResource(BaseResource):
return self
if path == 'status':
return LoginStatusResource(self._services_factory)
+ if path == AccountRecoveryResource.BASE_URL:
+ return AccountRecoveryResource(self._services_factory)
if not self.is_logged_in(request):
return UnAuthorizedResource()
return NoResource()
diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py
index 10d57c6f..896bc24b 100644
--- a/service/pixelated/resources/root_resource.py
+++ b/service/pixelated/resources/root_resource.py
@@ -23,6 +23,7 @@ from pixelated.resources import BaseResource, UnAuthorizedResource, UnavailableR
from pixelated.resources import get_public_static_folder, get_protected_static_folder
from pixelated.resources.attachments_resource import AttachmentsResource
from pixelated.resources.sandbox_resource import SandboxResource
+from pixelated.resources.account_recovery_resource import AccountRecoveryResource
from pixelated.resources.backup_account_resource import BackupAccountResource
from pixelated.resources.contacts_resource import ContactsResource
from pixelated.resources.features_resource import FeaturesResource
@@ -91,7 +92,8 @@ class RootResource(BaseResource):
def initialize(self, provider=None, disclaimer_banner=None, authenticator=None):
self._child_resources.add('assets', File(self._protected_static_folder))
- self._child_resources.add('backup-account', BackupAccountResource(self._services_factory, authenticator))
+ self._child_resources.add(AccountRecoveryResource.BASE_URL, AccountRecoveryResource(self._services_factory))
+ self._child_resources.add('backup-account', BackupAccountResource(self._services_factory, authenticator, provider))
self._child_resources.add('sandbox', SandboxResource(self._protected_static_folder))
self._child_resources.add('keys', KeysResource(self._services_factory))
self._child_resources.add(AttachmentsResource.BASE_URL, AttachmentsResource(self._services_factory))
diff --git a/service/pixelated/support/language.py b/service/pixelated/support/language.py
new file mode 100644
index 00000000..cd455f89
--- /dev/null
+++ b/service/pixelated/support/language.py
@@ -0,0 +1,24 @@
+#
+# Copyright (c) 2017 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+
+def parse_accept_language(all_headers):
+ accepted_languages = ['pt-BR', 'en-US']
+ languages = all_headers.get('accept-language', '').split(';')[0]
+ for language in accepted_languages:
+ if language in languages:
+ return language
+ return 'en-US'
diff --git a/service/requirements.txt b/service/requirements.txt
index 3ddb02df..01a2df5b 100644
--- a/service/requirements.txt
+++ b/service/requirements.txt
@@ -1,6 +1,6 @@
--index-url https://pypi.python.org/simple/
--e 'git+https://github.com/pixelated/python-gnupg.git@key_extension_and_sign#egg=gnupg'
+-e 'git+https://github.com/pixelated/python-gnupg.git@develop#egg=gnupg'
pyasn1==0.1.9
requests==2.11.1
srp==1.0.6
diff --git a/service/test/functional/features/account_recovery.feature b/service/test/functional/features/account_recovery.feature
new file mode 100644
index 00000000..da167d31
--- /dev/null
+++ b/service/test/functional/features/account_recovery.feature
@@ -0,0 +1,43 @@
+#
+# Copyright (c) 2017 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+@smoke @require_user
+Feature: Account Recovery
+ As a user of Pixelated
+ I want to recover my account
+ So that I can see my emails if I lose my password
+
+ Scenario: Sending recovery code
+ Given I am logged in Pixelated
+ When I go to the backup account page
+ And I submit my backup account
+ Then I see the confirmation of this submission
+ And I logout from the header
+ And I should see the login page
+
+ Scenario: Confirming I received the recovery code at my backup email
+ Given I am logged in Pixelated
+ When I open the mail with the recovery code
+ Then I see the mail has the recovery code
+ Then I logout
+
+ Scenario: Recovering an account
+ Given I am on the account recovery page
+ When I submit admin recovery code
+ And I submit user recovery code
+ And I submit new password
+ And I click on the backup account link
+ Then I see the backup account page
diff --git a/service/test/functional/features/environment.py b/service/test/functional/features/environment.py
index 9f8507b2..98c7fa99 100644
--- a/service/test/functional/features/environment.py
+++ b/service/test/functional/features/environment.py
@@ -29,6 +29,8 @@ from pixelated.config.site import PixelatedSite
from pixelated.resources.features_resource import FeaturesResource
from test.support.integration import AppTestClient
from steps.common import DEFAULT_IMPLICIT_WAIT_TIMEOUT_IN_S
+from steps import utils
+from ..page_objects import BackupAccountPage
class UnsuportedWebDriverError(Exception):
@@ -51,19 +53,30 @@ def before_all(context):
if not context.host.startswith('http'):
context.host = 'https://{}'.format(context.host)
- hostname = urlparse(context.host).hostname
- context.signup_url = 'https://{}/signup'.format(hostname)
- context.login_url = 'https://mail.{}/login'.format(hostname)
- context.backup_account_url = 'https://mail.{}/backup-account'.format(hostname)
+ context.hostname = urlparse(context.host).hostname
+ context.signup_url = 'https://{}/signup'.format(context.hostname)
+ context.inbox_url = 'https://mail.{}'.format(context.hostname)
+ context.login_url = 'https://mail.{}/login'.format(context.hostname)
+ context.backup_account_url = 'https://mail.{}/backup-account'.format(context.hostname)
+ context.account_recovery_url = 'https://mail.{}/account-recovery'.format(context.hostname)
context.username = 'testuser_{}'.format(uuid.uuid4())
+ context.user_email = '{}@{}'.format(context.username, context.hostname)
if 'localhost' in context.host:
_mock_user_agent(context)
context.login_url = context.multi_user_url + '/login'
context.backup_account_url = context.single_user_url + '/backup-account'
+ context.account_recovery_url = context.single_user_url + '/account-recovery'
context.username = 'username'
+def before_tag(context, tag):
+ if tag == "require_user":
+ context.username = 'testuser_{}'.format(uuid.uuid4())
+ context.user_email = '{}@{}'.format(context.username, context.hostname)
+ utils.create_user(context)
+
+
def _setup_webdriver(context):
browser = context.config.userdata.get('webdriver', 'chrome')
supported_webdrivers = {
@@ -122,13 +135,24 @@ def after_feature(context, feature):
context.last_mail = None
+def after_scenario(context, scenario):
+ _logout(context)
+ context.browser.refresh()
+
+
def after_step(context, step):
- _debug_on_error(context, step)
- _save_screenshot(context, step)
+ if step.status == 'failed':
+ _debug_on_error(context, step)
+ _save_screenshot(context, step)
+ _logout(context)
+
+
+def _logout(context):
+ context.browser.delete_all_cookies()
def _debug_on_error(context, step):
- if step.status == 'failed' and context.config.userdata.getbool("debug"):
+ if context.config.userdata.getbool("debug"):
try:
import ipdb
ipdb.post_mortem(step.exc_traceback)
@@ -138,8 +162,7 @@ def _debug_on_error(context, step):
def _save_screenshot(context, step):
- if (step.status == 'failed' and
- context.config.userdata.getbool("screenshots", True)):
+ if context.config.userdata.getbool("screenshots", True):
timestamp = time.strftime("%Y-%m-%d-%H-%M-%S")
filename = _slugify('{} failed {}'.format(timestamp, str(step.name)))
filepath = os.path.join('screenshots', filename + '.png')
diff --git a/service/test/functional/features/page_objects/__init__.py b/service/test/functional/features/page_objects/__init__.py
new file mode 100644
index 00000000..af50948c
--- /dev/null
+++ b/service/test/functional/features/page_objects/__init__.py
@@ -0,0 +1,20 @@
+#
+# Copyright (c) 2017 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+from account_recovery_page import AccountRecoveryPage
+from base_page import BasePage
+from inbox_page import InboxPage
+from backup_account_page import BackupAccountPage
diff --git a/service/test/functional/features/page_objects/account_recovery_page.py b/service/test/functional/features/page_objects/account_recovery_page.py
new file mode 100644
index 00000000..8a4e05cd
--- /dev/null
+++ b/service/test/functional/features/page_objects/account_recovery_page.py
@@ -0,0 +1,57 @@
+#
+# Copyright (c) 2017 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+from base_page import BasePage
+
+
+class AccountRecoveryPage(BasePage):
+ def __init__(self, context):
+ super(AccountRecoveryPage, self).__init__(context, context.account_recovery_url)
+
+ self._locators = {
+ 'admin_form': '.account-recovery-form.admin-code',
+ 'admin_code': 'input[name="admin-code"]',
+ 'user_form': '.account-recovery-form.user-code',
+ 'user_code': 'input[name="user-code"]',
+ 'new_password_form': '.account-recovery-form.new-password',
+ 'new_password': 'input[name="new-password"]',
+ 'confirm_password': 'input[name="confirm-password"]',
+ 'submit_button': '.submit-button button[type="submit"]',
+ 'backup_account_link': 'a[href="/backup-account"]'
+ }
+
+ def submit_admin_recovery_code(self, admin_code):
+ self.find_element_by_css_selector(self._locators['admin_form'])
+ self.fill_by_css_selector(self._locators['admin_code'], admin_code)
+ self.click_submit()
+
+ def submit_user_recovery_code(self, user_code):
+ self.find_element_by_css_selector(self._locators['user_form'])
+ self.fill_by_css_selector(self._locators['user_code'], user_code)
+ self.click_submit()
+
+ def submit_new_password(self, new_password, confirm_password):
+ self.find_element_by_css_selector(self._locators['new_password_form'])
+ self.fill_by_css_selector(self._locators['new_password'], new_password)
+ self.fill_by_css_selector(self._locators['confirm_password'], confirm_password)
+ self.click_submit()
+
+ def go_to_backup_account(self):
+ self.find_element_by_css_selector(self._locators['backup_account_link']).click()
+
+ def click_submit(self):
+ submit_button = self.find_element_by_css_selector(self._locators['submit_button'])
+ submit_button.click()
diff --git a/service/test/functional/features/page_objects/backup_account_page.py b/service/test/functional/features/page_objects/backup_account_page.py
new file mode 100644
index 00000000..d5f84b40
--- /dev/null
+++ b/service/test/functional/features/page_objects/backup_account_page.py
@@ -0,0 +1,30 @@
+#
+# Copyright (c) 2017 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+from base_page import BasePage
+
+
+class BackupAccountPage(BasePage):
+ def __init__(self, context):
+ super(BackupAccountPage, self).__init__(context, context.backup_account_url)
+
+ self._locators = {
+ 'logout_button': 'button[name="logout"]'
+ }
+
+ def logout(self):
+ logout_button = self.find_element_by_css_selector(self._locators['logout_button'])
+ logout_button.click()
diff --git a/service/test/functional/features/page_objects/base_page.py b/service/test/functional/features/page_objects/base_page.py
new file mode 100644
index 00000000..4756d930
--- /dev/null
+++ b/service/test/functional/features/page_objects/base_page.py
@@ -0,0 +1,39 @@
+#
+# Copyright (c) 2017 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+from steps.common import (
+ fill_by_css_selector,
+ find_element_by_css_selector,
+ find_element_by_xpath)
+
+
+class BasePage(object):
+ def __init__(self, context, base_url):
+ self.context = context
+ self.timeout = 30
+ self.base_url = base_url
+
+ def find_element_by_css_selector(self, loc):
+ return find_element_by_css_selector(self.context, loc)
+
+ def fill_by_css_selector(self, loc, text):
+ fill_by_css_selector(self.context, loc, text)
+
+ def find_element_by_xpath(self, xpath):
+ return find_element_by_xpath(self.context, xpath)
+
+ def visit(self):
+ self.context.browser.get(self.base_url)
diff --git a/service/test/functional/features/page_objects/inbox_page.py b/service/test/functional/features/page_objects/inbox_page.py
new file mode 100644
index 00000000..a6b5fef7
--- /dev/null
+++ b/service/test/functional/features/page_objects/inbox_page.py
@@ -0,0 +1,52 @@
+#
+# Copyright (c) 2017 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+from base_page import BasePage
+from steps.common import execute_ignoring_staleness
+
+
+class InboxPage(BasePage):
+ def __init__(self, context):
+ super(InboxPage, self).__init__(context, context.inbox_url)
+
+ self._locators = {
+ 'first_email': '.mail-list-entry__item',
+ 'read_sandbox': '#read-sandbox',
+ 'iframe_body': 'body',
+ }
+
+ def _get_first_mail(self):
+ return self.find_element_by_css_selector(self._locators['first_email'])
+
+ def get_mail_with_subject(self, subject):
+ return self.find_element_by_xpath("//*[@class='mail-list-entry__item-subject' and contains(.,'%s')]" % subject)
+
+ def open_first_mail_in_the_mail_list(self):
+ # it seems page is often still loading so staleness exceptions happen often
+ self.context.current_mail_id = 'mail-' + execute_ignoring_staleness(
+ lambda: self._get_first_mail().get_attribute('href').split('/')[-1])
+ execute_ignoring_staleness(lambda: self._get_first_mail().click())
+
+ def open_mail_with_the_recovery_code(self):
+ self.get_mail_with_subject('Recovery Code').click()
+
+ def get_body_message(self):
+ self.find_element_by_css_selector(self._locators['read_sandbox'])
+ self.context.browser.switch_to_frame('read-sandbox')
+ body_message = self.find_element_by_css_selector(self._locators['iframe_body']).text
+ self.context.browser.switch_to_default_content()
+
+ return body_message
diff --git a/service/test/functional/features/smoke.feature b/service/test/functional/features/smoke.feature
index 1467baf9..d4149334 100644
--- a/service/test/functional/features/smoke.feature
+++ b/service/test/functional/features/smoke.feature
@@ -14,7 +14,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
-@smoke
+@smoke @require_user
Feature: sign up, login and logout
As a visitor of Pixelated
I want to sign up
@@ -31,7 +31,8 @@ Feature: sign up, login and logout
When I enter username and password as credentials
And I click on the login button
Then I should see the fancy interstitial
- Then I have mails
+ And I should see the inbox
+ And I have mails
When I logout
Then I should see the login page
@@ -40,6 +41,7 @@ Feature: sign up, login and logout
When I enter username and password as credentials
And I click on the login button
Then I should see the fancy interstitial
- Given I am on the backup account page
+ And I should see the inbox
+ Given I go to the backup account page
When I logout from the header
Then I should see the login page
diff --git a/service/test/functional/features/steps/__init__.py b/service/test/functional/features/steps/__init__.py
index 2756a319..5750dc53 100644
--- a/service/test/functional/features/steps/__init__.py
+++ b/service/test/functional/features/steps/__init__.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2014 ThoughtWorks, Inc.
+# Copyright (c) 2017 ThoughtWorks, Inc.
#
# Pixelated is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
diff --git a/service/test/functional/features/steps/account_recovery.py b/service/test/functional/features/steps/account_recovery.py
new file mode 100644
index 00000000..ac66cf76
--- /dev/null
+++ b/service/test/functional/features/steps/account_recovery.py
@@ -0,0 +1,49 @@
+#
+# Copyright (c) 2017 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+from behave import given, when, then
+
+from ..page_objects import AccountRecoveryPage
+
+
+@given(u'I am on the account recovery page')
+def account_recovery_page(context):
+ AccountRecoveryPage(context).visit()
+
+
+@when(u'I submit admin recovery code')
+def submit_admin_recovery_code(context):
+ AccountRecoveryPage(context).submit_admin_recovery_code('1234')
+
+
+@when(u'I submit user recovery code')
+def submit_user_recovery_code(context):
+ AccountRecoveryPage(context).submit_user_recovery_code('5678')
+
+
+@when(u'I submit new password')
+def submit_new_password(context):
+ AccountRecoveryPage(context).submit_new_password('new test password', 'new test password')
+
+
+@when(u'I click on the backup account link')
+def go_to_backup_account(context):
+ AccountRecoveryPage(context).go_to_backup_account()
+
+
+@then(u'I see the backup account page')
+def verify_backup_account_page(context):
+ assert('/backup-account' in context.browser.current_url)
diff --git a/service/test/functional/features/steps/backup_account.py b/service/test/functional/features/steps/backup_account.py
index 914309f2..5a1052a8 100644
--- a/service/test/functional/features/steps/backup_account.py
+++ b/service/test/functional/features/steps/backup_account.py
@@ -14,9 +14,25 @@
# You should have received a copy of the GNU Affero General Public License
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
-from behave import given
+from behave import given, when, then
+from common import (
+ fill_by_css_selector,
+ find_element_by_css_selector)
-@given(u'I am on the backup account page')
+
+@when(u'I go to the backup account page')
+@given(u'I go to the backup account page')
def backup_account_page(context):
context.browser.get(context.backup_account_url)
+
+
+@when(u'I submit my backup account')
+def submit_backup_email(context):
+ fill_by_css_selector(context, 'input[name="email"]', context.user_email)
+ find_element_by_css_selector(context, '.submit-button button[type="submit"]').click()
+
+
+@then(u'I see the confirmation of this submission')
+def confirmation_page(context):
+ find_element_by_css_selector(context, '.confirmation-container', timeout=50)
diff --git a/service/test/functional/features/steps/common.py b/service/test/functional/features/steps/common.py
index 3e1e995e..ff6e6166 100644
--- a/service/test/functional/features/steps/common.py
+++ b/service/test/functional/features/steps/common.py
@@ -146,10 +146,6 @@ def click_button(context, title, element='button'):
button.click()
-def mail_list_with_subject_exists(context, subject):
- return find_element_by_xpath(context, "//*[@class='mail-list-entry__item-subject' and contains(.,'%s')]" % subject)
-
-
def reply_subject(context):
e = find_element_by_css_selector(context, '#reply-subject')
return e.text
diff --git a/service/test/functional/features/steps/login.py b/service/test/functional/features/steps/login.py
index 2d7be259..0ac27c46 100644
--- a/service/test/functional/features/steps/login.py
+++ b/service/test/functional/features/steps/login.py
@@ -21,16 +21,29 @@ from common import (
find_element_by_css_selector)
+@given('I am logged in Pixelated')
+def login_user(context):
+ login_page(context)
+ enter_credentials(context)
+ click_login(context)
+ see_interstitial(context)
+ _see_inbox(context)
+
+
@given(u'a user is accessing the login page')
@when(u'I open the login page')
def login_page(context):
context.browser.get(context.login_url)
-@when(u'I enter {username} and {password} as credentials')
-def enter_credentials(context, username, password):
+def _see_inbox(context):
+ find_element_by_css_selector(context, '#compose', timeout=40)
+
+
+@when(u'I enter username and password as credentials')
+def enter_credentials(context):
fill_by_css_selector(context, 'input[name="username"]', context.username)
- fill_by_css_selector(context, 'input[name="password"]', password)
+ fill_by_css_selector(context, 'input[name="password"]', 'password')
@when(u'I click on the login button')
@@ -39,20 +52,29 @@ def click_login(context):
@then(u'I should see the fancy interstitial')
-def step_impl(context):
+def see_interstitial(context):
find_element_by_css_selector(context, 'section#hive-section')
+@then(u'I should see the inbox')
+def see_inbox(context):
+ _see_inbox(context)
+
+
+@then(u'I logout')
@when(u'I logout')
def click_logout(context):
+ context.browser.refresh()
find_element_by_css_selector(context, '#logout-form div').click()
+@then(u'I logout from the header') # noqa
@when(u'I logout from the header')
def click_logout(context):
find_element_by_css_selector(context, 'button[name="logout"]').click()
+@when(u'I should see the login page')
@then(u'I should see the login page')
def see_login_page(context):
find_element_by_css_selector(context, 'form#login_form')
diff --git a/service/test/functional/features/steps/mail_list.py b/service/test/functional/features/steps/mail_list.py
index 227aa9ed..21694153 100644
--- a/service/test/functional/features/steps/mail_list.py
+++ b/service/test/functional/features/steps/mail_list.py
@@ -17,13 +17,12 @@
from behave import when, then, given
from selenium.common.exceptions import TimeoutException
+from ..page_objects import InboxPage
from common import (
ImplicitWait,
- execute_ignoring_staleness,
find_element_by_id,
find_element_by_css_selector,
find_elements_by_css_selector,
- mail_list_with_subject_exists,
wait_for_condition,
wait_for_loading_to_finish)
@@ -42,10 +41,6 @@ def open_current_mail(context):
e.click()
-def get_first_email(context):
- return find_element_by_css_selector(context, '.mail-list-entry__item')
-
-
@then('I see that mail under the \'{tag}\' tag')
def impl(context, tag):
context.execute_steps("when I select the tag '%s'" % tag)
@@ -59,9 +54,12 @@ def impl(context):
@when('I open the first mail in the mail list')
def impl(context):
- # it seems page is often still loading so staleness exceptions happen often
- context.current_mail_id = 'mail-' + execute_ignoring_staleness(lambda: get_first_email(context).get_attribute('href').split('/')[-1])
- execute_ignoring_staleness(lambda: get_first_email(context).click())
+ InboxPage(context).open_first_mail_in_the_mail_list()
+
+
+@when('I open the mail with the recovery code')
+def impl(context):
+ InboxPage(context).open_mail_with_the_recovery_code()
@when('I open the first mail in the \'{tag}\'')
@@ -83,7 +81,7 @@ def impl(context):
@then('the deleted mail is there')
def impl(context):
- mail_list_with_subject_exists(context, context.last_subject)
+ InboxPage(context).get_mail_with_subject(context.last_subject)
@given('I have mails')
diff --git a/service/test/functional/features/steps/mail_view.py b/service/test/functional/features/steps/mail_view.py
index 65959b70..26368aaa 100644
--- a/service/test/functional/features/steps/mail_view.py
+++ b/service/test/functional/features/steps/mail_view.py
@@ -17,6 +17,7 @@
from behave import then, when
from selenium.webdriver.common.keys import Keys
+from ..page_objects import InboxPage
from common import (
click_button,
find_element_by_css_selector,
@@ -25,19 +26,17 @@ from common import (
wait_until_button_is_visible)
-@then('I see that the subject reads \'{subject}\'')
-def impl(context, subject):
- e = find_element_by_css_selector(context, '#mail-view .mail-read-view__header-subject')
- assert e.text == subject
+@then('I see that the subject reads \'{expected_subject}\'')
+def impl(context, expected_subject):
+ actual_subject = find_element_by_css_selector(context, '#mail-view .mail-read-view__header-subject').text
+ assert expected_subject == actual_subject
@then('I see that the body reads \'{expected_body}\'')
+@then('I see that the body has \'{expected_body}\'')
def impl(context, expected_body):
- find_element_by_css_selector(context, '#read-sandbox')
- context.browser.switch_to_frame('read-sandbox')
- e = find_element_by_css_selector(context, 'body')
- assert e.text == expected_body
- context.browser.switch_to_default_content()
+ actual_body = InboxPage(context).get_body_message()
+ assert expected_body in actual_body
@then('that email has the \'{tag}\' tag')
@@ -110,3 +109,9 @@ def impl(context):
assert cc is not None
assert bcc is not None
+
+
+@then(u'I see the mail has the recovery code')
+def step_impl(context):
+ expected_body = 'You are receiving this message because you registered an email account at'
+ context.execute_steps(u"Then I see that the body has '%s'" % expected_body)
diff --git a/service/test/functional/features/steps/signup.py b/service/test/functional/features/steps/signup.py
index 43480666..71df1868 100644
--- a/service/test/functional/features/steps/signup.py
+++ b/service/test/functional/features/steps/signup.py
@@ -14,6 +14,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+import uuid
+
from behave import given, then, when
from common import (
@@ -23,22 +25,31 @@ from common import (
@given(u'a user is accessing the signup page') # noqa
-def step_impl(context):
+def access_signup_page(context):
context.browser.get(context.signup_url)
+@given(u'I am an existent Pixelated user')
+def setup_user(context):
+ access_signup_page(context)
+ enter_user_information(context)
+ click_signup_button(context)
+ see_user_control_panel(context)
+
+
@when(u'I enter username, password and password confirmation') # noqa
-def step_impl(context):
- fill_by_css_selector(context, '#srp_username', context.username)
+def enter_user_information(context):
+ username = 'testuser_{}'.format(uuid.uuid4())
+ fill_by_css_selector(context, '#srp_username', username)
fill_by_css_selector(context, '#srp_password', 'password')
fill_by_css_selector(context, '#srp_password_confirmation', 'password')
@when(u'I click on the signup button') # noqa
-def step_impl(context):
+def click_signup_button(context):
find_element_by_css_selector(context, 'button[type=submit]').click()
@then(u'I should see the user control panel') # noqa
-def step_impl(context):
+def see_user_control_panel(context):
element_should_have_content(context, 'h1', 'user control panel')
diff --git a/service/test/functional/features/steps/utils.py b/service/test/functional/features/steps/utils.py
new file mode 100644
index 00000000..9ac05928
--- /dev/null
+++ b/service/test/functional/features/steps/utils.py
@@ -0,0 +1,51 @@
+#
+# Copyright (c) 2016 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+from common import (
+ element_should_have_content,
+ fill_by_css_selector,
+ find_element_by_css_selector)
+
+
+def access_signup_page(context):
+ context.browser.get(context.signup_url)
+
+
+def create_user(context):
+ context.browser.get(context.login_url)
+ access_signup_page(context)
+ enter_user_information(context)
+ click_signup_button(context)
+ see_user_control_panel(context)
+ log_out(context)
+
+
+def log_out(context):
+ find_element_by_css_selector(context, 'a[href="/logout"]').click()
+
+
+def enter_user_information(context):
+ fill_by_css_selector(context, '#srp_username', context.username)
+ fill_by_css_selector(context, '#srp_password', 'password')
+ fill_by_css_selector(context, '#srp_password_confirmation', 'password')
+
+
+def click_signup_button(context):
+ find_element_by_css_selector(context, 'button[type=submit]').click()
+
+
+def see_user_control_panel(context):
+ element_should_have_content(context, 'h1', 'user control panel')
diff --git a/service/test/unit/config/test_site.py b/service/test/unit/config/test_site.py
index 896126ec..64a8b68a 100644
--- a/service/test/unit/config/test_site.py
+++ b/service/test/unit/config/test_site.py
@@ -17,7 +17,7 @@
from twisted.trial import unittest
from mock import MagicMock
from pixelated.config.site import PixelatedSite
-from twisted.protocols.basic import LineReceiver
+from twisted.web.test.requesthelper import DummyChannel
class TestPixelatedSite(unittest.TestCase):
@@ -52,7 +52,7 @@ class TestPixelatedSite(unittest.TestCase):
self.assertFalse(request.responseHeaders.hasHeader('Strict-Transport-Security'.lower()))
def create_request(self):
- channel = LineReceiver()
+ channel = DummyChannel()
channel.site = PixelatedSite(MagicMock())
request = PixelatedSite.requestFactory(channel=channel, queued=True)
request.method = "GET"
diff --git a/service/test/unit/resources/test_account_recovery_resource.py b/service/test/unit/resources/test_account_recovery_resource.py
new file mode 100644
index 00000000..4e26fc5b
--- /dev/null
+++ b/service/test/unit/resources/test_account_recovery_resource.py
@@ -0,0 +1,108 @@
+#
+# Copyright (c) 2017 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+from mock import MagicMock
+from twisted.trial import unittest
+from twisted.web.test.requesthelper import DummyRequest
+from twisted.internet import defer
+
+from pixelated.resources.account_recovery_resource import AccountRecoveryResource, InvalidPasswordError
+from test.unit.resources import DummySite
+
+
+class TestAccountRecoveryResource(unittest.TestCase):
+ def setUp(self):
+ self.services_factory = MagicMock()
+ self.resource = AccountRecoveryResource(self.services_factory)
+ self.web = DummySite(self.resource)
+
+ def test_get(self):
+ request = DummyRequest(['/account-recovery'])
+ request.method = 'GET'
+ d = self.web.get(request)
+
+ def assert_200_when_user_logged_in(_):
+ self.assertEqual(200, request.responseCode)
+ self.assertIn("DOCTYPE html", request.written[0])
+
+ d.addCallback(assert_200_when_user_logged_in)
+ return d
+
+ def test_post_returns_successfully(self):
+ request = DummyRequest(['/account-recovery'])
+ request.method = 'POST'
+ self.resource._handle_post = MagicMock(return_value=defer.succeed(None))
+
+ d = self.web.get(request)
+
+ def assert_successful_response(_):
+ self.assertEqual(200, request.responseCode)
+
+ d.addCallback(assert_successful_response)
+ return d
+
+ def test_post_returns_failure(self):
+ request = DummyRequest(['/account-recovery'])
+ request.method = 'POST'
+ self.resource._handle_post = MagicMock(return_value=defer.fail(InvalidPasswordError))
+
+ d = self.web.get(request)
+
+ def assert_error_response(_):
+ self.assertEqual(500, request.responseCode)
+
+ d.addCallback(assert_error_response)
+ return d
+
+ def test_handle_post_successfully(self):
+ request = MagicMock()
+ self.resource._get_post_form = MagicMock()
+ self.resource._validate_password = MagicMock(return_value=True)
+
+ d = self.resource._handle_post(request)
+
+ def assert_successful(success):
+ self.assertEqual(success, 'Done!')
+
+ d.addCallback(assert_successful)
+ return d
+
+ @defer.inlineCallbacks
+ def test_handle_post_failed(self):
+ request = MagicMock()
+ self.resource._get_post_form = MagicMock()
+ self.resource._validate_password = MagicMock(return_value=False)
+
+ with self.assertRaises(InvalidPasswordError):
+ yield self.resource._handle_post(request)
+
+ def test_get_post_form(self):
+ request = MagicMock()
+ request.content.getvalue.return_value = '{"userCode": "abc", "password": "123", "confirmPassword": "456"}'
+ form = self.resource._get_post_form(request)
+
+ self.assertEqual(form.get('userCode'), 'abc')
+ self.assertEqual(form.get('password'), '123')
+ self.assertEqual(form.get('confirmPassword'), '456')
+
+ def test_validate_password_successfully(self):
+ self.assertTrue(self.resource._validate_password('12345678', '12345678'))
+
+ def test_validate_password_failed_by_confirmation(self):
+ self.assertFalse(self.resource._validate_password('12345678', '1234'))
+
+ def test_validate_password_failed_by_length(self):
+ self.assertFalse(self.resource._validate_password('1234', '1234'))
diff --git a/service/test/unit/resources/test_backup_account_resource.py b/service/test/unit/resources/test_backup_account_resource.py
index 2b68dd1b..e16fa0e1 100644
--- a/service/test/unit/resources/test_backup_account_resource.py
+++ b/service/test/unit/resources/test_backup_account_resource.py
@@ -14,8 +14,6 @@
# You should have received a copy of the GNU Affero General Public License
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
-import os
-
from mock import MagicMock, patch
from twisted.trial import unittest
from twisted.web.test.requesthelper import DummyRequest
@@ -28,7 +26,10 @@ from test.unit.resources import DummySite
class TestBackupAccountResource(unittest.TestCase):
def setUp(self):
self.services_factory = MagicMock()
- self.resource = BackupAccountResource(self.services_factory, MagicMock())
+ self.authenticator = MagicMock()
+ self.leap_provider = MagicMock()
+ self.leap_provider.server_name = 'test.com'
+ self.resource = BackupAccountResource(self.services_factory, self.authenticator, self.leap_provider)
self.web = DummySite(self.resource)
def test_get(self):
@@ -43,19 +44,27 @@ class TestBackupAccountResource(unittest.TestCase):
d.addCallback(assert_200_when_user_logged_in)
return d
+ @patch('pixelated.resources.backup_account_resource.parse_accept_language')
@patch('pixelated.resources.backup_account_resource.AccountRecovery')
- def test_post_updates_recovery_code(self, mock_account_recovery_init):
+ def test_post_updates_recovery_code(self, mock_account_recovery_init, mock_language):
+ mock_language.return_value = 'pt-BR'
mock_account_recovery = MagicMock()
mock_account_recovery_init.return_value = mock_account_recovery
mock_account_recovery.update_recovery_code.return_value = defer.succeed("Success")
request = DummyRequest(['/backup-account'])
request.method = 'POST'
+ request.content = MagicMock()
+ request.content.getvalue.return_value = '{"email": "test@test.com"}'
d = self.web.get(request)
def assert_update_recovery_code_called(_):
mock_account_recovery_init.assert_called_with(
self.resource._authenticator.bonafide_session,
- self.resource.soledad(request))
+ self.resource.soledad(request),
+ self.resource._service(request, '_leap_session').smtp_config,
+ self.resource._get_backup_email(request),
+ self.leap_provider.server_name,
+ language='pt-BR')
mock_account_recovery.update_recovery_code.assert_called()
d.addCallback(assert_update_recovery_code_called)
@@ -66,6 +75,8 @@ class TestBackupAccountResource(unittest.TestCase):
mock_update_recovery_code.return_value = defer.succeed("Success")
request = DummyRequest(['/backup-account'])
request.method = 'POST'
+ request.content = MagicMock()
+ request.content.getvalue.return_value = '{"email": "test@test.com"}'
d = self.web.get(request)
def assert_successful_response(_):
@@ -79,6 +90,8 @@ class TestBackupAccountResource(unittest.TestCase):
mock_update_recovery_code.return_value = defer.fail(Exception)
request = DummyRequest(['/backup-account'])
request.method = 'POST'
+ request.content = MagicMock()
+ request.content.getvalue.return_value = '{"email": "test@test.com"}'
d = self.web.get(request)
def assert_successful_response(_):
@@ -86,3 +99,9 @@ class TestBackupAccountResource(unittest.TestCase):
d.addCallback(assert_successful_response)
return d
+
+ def test_get_backup_email_from_request(self):
+ request = MagicMock()
+ request.content.getvalue.return_value = '{"backupEmail": "test@test.com"}'
+
+ self.assertEqual(self.resource._get_backup_email(request), 'test@test.com')
diff --git a/service/test/unit/resources/test_login_resource.py b/service/test/unit/resources/test_login_resource.py
index 9f940bc6..03d61758 100644
--- a/service/test/unit/resources/test_login_resource.py
+++ b/service/test/unit/resources/test_login_resource.py
@@ -24,30 +24,9 @@ from twisted.trial import unittest
from twisted.web.test.requesthelper import DummyRequest
from pixelated.resources.login_resource import LoginResource, LoginStatusResource
-from pixelated.resources.login_resource import parse_accept_language
from test.unit.resources import DummySite
-class TestParseAcceptLanguage(unittest.TestCase):
- def test_parse_pt_br_simple(self):
- all_headers = {
- 'accept-language': 'pt-BR,pt;q=0.8,en-US;q=0.5,en;q=0.3'}
- parsed_language = parse_accept_language(all_headers)
- self.assertEqual('pt-BR', parsed_language)
-
- def test_parse_en_us_simple(self):
- all_headers = {
- 'accept-language': 'en-US,en;q=0.8,en-US;q=0.5,en;q=0.3'}
- parsed_language = parse_accept_language(all_headers)
- self.assertEqual('en-US', parsed_language)
-
- def test_parse_pt_br_as_default(self):
- all_headers = {
- 'accept-language': 'de-DE,de;q=0.8,en-US;q=0.5,en;q=0.3'}
- parsed_language = parse_accept_language(all_headers)
- self.assertEqual('pt-BR', parsed_language)
-
-
class TestLoginResource(unittest.TestCase):
def setUp(self):
self.services_factory = mock()
@@ -67,6 +46,16 @@ class TestLoginResource(unittest.TestCase):
d.addCallback(assert_unauthorized_resources)
return d
+ def test_account_recovery_resource_does_not_require_login(self):
+ request = DummyRequest(['account-recovery'])
+ d = self.web.get(request)
+
+ def assert_successful(_):
+ self.assertEqual(200, request.responseCode)
+
+ d.addCallback(assert_successful)
+ return d
+
@patch('pixelated.resources.session.PixelatedSession.is_logged_in')
def test_there_are_no_grand_children_resources_when_logged_in(self, mock_is_logged_in):
request = DummyRequest(['/login/grand_children'])
@@ -242,7 +231,7 @@ class TestLoginPOST(unittest.TestCase):
d = self.web.get(self.request)
def assert_login_setup_service_for_user(_):
- mock_user_bootstrap_setup.assert_called_once_with(self.user_auth, self.password, 'pt-BR')
+ mock_user_bootstrap_setup.assert_called_once_with(self.user_auth, self.password, 'en-US')
d.addCallback(assert_login_setup_service_for_user)
return d
diff --git a/service/test/unit/support/test_language.py b/service/test/unit/support/test_language.py
new file mode 100644
index 00000000..b84f3a23
--- /dev/null
+++ b/service/test/unit/support/test_language.py
@@ -0,0 +1,40 @@
+#
+# Copyright (c) 2017 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+
+from twisted.trial import unittest
+
+from pixelated.support.language import parse_accept_language
+
+
+class TestParseAcceptLanguage(unittest.TestCase):
+ def test_parse_pt_br_simple(self):
+ all_headers = {
+ 'accept-language': 'pt-BR,pt;q=0.8,en-US;q=0.5,en;q=0.3'}
+ parsed_language = parse_accept_language(all_headers)
+ self.assertEqual('pt-BR', parsed_language)
+
+ def test_parse_en_us_simple(self):
+ all_headers = {
+ 'accept-language': 'en-US,en;q=0.8,en-US;q=0.5,en;q=0.3'}
+ parsed_language = parse_accept_language(all_headers)
+ self.assertEqual('en-US', parsed_language)
+
+ def test_parse_en_us_as_default(self):
+ all_headers = {
+ 'accept-language': 'de-DE,de;q=0.8,en-US;q=0.5,en;q=0.3'}
+ parsed_language = parse_accept_language(all_headers)
+ self.assertEqual('en-US', parsed_language)
diff --git a/service/test/unit/test_account_recovery.py b/service/test/unit/test_account_recovery.py
index af14814a..2a185347 100644
--- a/service/test/unit/test_account_recovery.py
+++ b/service/test/unit/test_account_recovery.py
@@ -13,24 +13,95 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+from email.mime.text import MIMEText
-from twisted.internet.defer import inlineCallbacks
+from twisted.internet import defer
from twisted.trial import unittest
+from twisted.mail import smtp
-from mock import patch, Mock
+from mock import patch, Mock, mock_open
+from mockito import when, any as ANY
from pixelated.account_recovery import AccountRecovery
class AccountRecoveryTest(unittest.TestCase):
+ def setUp(self):
+ self.generated_code = '4645a2f8997e5d0d'
+ self.mock_bonafide_session = Mock()
+ self.mock_soledad = Mock()
+ self.mock_smtp_config = Mock()
+ self.keymanager = Mock()
+ self.mock_smtp_config.remote_smtp_host = 'localhost'
+ self.mock_soledad.create_recovery_code.return_value = self.generated_code
+ self.backup_email = 'test@test.com'
+ self.domain = 'test.com'
+ self.account_recovery = AccountRecovery(
+ self.mock_bonafide_session,
+ self.mock_soledad,
+ self.mock_smtp_config,
+ self.backup_email,
+ self.domain)
+ self.mock_smtp = Mock()
- @inlineCallbacks
+ @defer.inlineCallbacks
def test_update_recovery_code(self):
- generated_code = '4645a2f8997e5d0d'
- mock_bonafide_session = Mock()
- mock_soledad = Mock()
- mock_soledad.create_recovery_code.return_value = generated_code
- account_recovery = AccountRecovery(mock_bonafide_session, mock_soledad)
-
- yield account_recovery.update_recovery_code()
- mock_bonafide_session.update_recovery_code.assert_called_once_with(generated_code)
+ when(self.account_recovery)._send_mail(ANY).thenReturn(defer.succeed(None))
+ yield self.account_recovery.update_recovery_code()
+ self.mock_bonafide_session.update_recovery_code.assert_called_once_with(self.generated_code)
+
+ @defer.inlineCallbacks
+ def test_creates_recovery_code(self):
+ when(self.account_recovery)._send_mail(ANY).thenReturn(defer.succeed(None))
+ yield self.account_recovery.update_recovery_code()
+ self.mock_soledad.create_recovery_code.assert_called_once()
+
+ @patch('pixelated.account_recovery.smtp.sendmail')
+ @patch('pixelated.account_recovery.pkg_resources.resource_filename')
+ @defer.inlineCallbacks
+ def test_default_email_template(self, mock_resource, mock_sendmail):
+ mock_sendmail.return_value = defer.succeed(None)
+
+ with patch('pixelated.account_recovery.open', mock_open(read_data=''), create=True):
+ yield self.account_recovery.update_recovery_code()
+ mock_resource.assert_called_once_with('pixelated.assets',
+ 'recovery.mail.en-US')
+
+ @patch('pixelated.account_recovery.smtp.sendmail')
+ @patch('pixelated.account_recovery.pkg_resources.resource_filename')
+ @defer.inlineCallbacks
+ def test_portuguese_email_template(self, mock_resource, mock_sendmail):
+ self.account_recovery = AccountRecovery(
+ self.mock_bonafide_session,
+ self.mock_soledad,
+ self.mock_smtp_config,
+ self.backup_email,
+ self.domain,
+ language='pt-BR')
+ mock_sendmail.return_value = defer.succeed(None)
+
+ with patch('pixelated.account_recovery.open', mock_open(read_data=''), create=True):
+ yield self.account_recovery.update_recovery_code()
+ mock_resource.assert_called_once_with('pixelated.assets',
+ 'recovery.mail.pt-BR')
+
+ @patch('pixelated.account_recovery.date.mail_date_now')
+ @patch('pixelated.account_recovery.smtp.sendmail')
+ @patch('pixelated.account_recovery.pkg_resources.resource_filename')
+ @defer.inlineCallbacks
+ def test_send_recovery_code_by_email(self, mock_resource, mock_sendmail, mock_date):
+ mock_sendmail.return_value = defer.succeed(None)
+ mock_date.return_value = 'Sat, 21 Mar 2015 19:30:09 -0300'
+
+ sender = 'team@{}'.format(self.domain)
+ mock_file_content = '{backup_email}, {sender}, {date}, {domain}, {recovery_code}'
+ recovery_code_email = '\ntest@test.com, team@test.com, Sat, 21 Mar 2015 19:30:09 -0300, test.com, 34363435613266383939376535643064'
+
+ with patch('pixelated.account_recovery.open', mock_open(read_data=mock_file_content), create=True):
+ yield self.account_recovery.update_recovery_code()
+
+ mock_sendmail.assert_called_with(
+ self.mock_smtp_config.remote_smtp_host,
+ sender,
+ [self.backup_email],
+ recovery_code_email)
diff --git a/web-ui/app/images/account-recovery/admins_contact.svg b/web-ui/app/images/account-recovery/admins_contact.svg
new file mode 100644
index 00000000..ae94f307
--- /dev/null
+++ b/web-ui/app/images/account-recovery/admins_contact.svg
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="191px" height="42px" viewBox="0 0 191 42" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
+ <title>Group 2 Copy</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Account-Recovery-MVP" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="01-m_Forgot_Password" transform="translate(-92.000000, -261.000000)">
+ <g id="Group-2-Copy" transform="translate(92.000000, 261.000000)">
+ <g id="Group-3-Copy" transform="translate(80.000000, 9.000000)">
+ <g id="Group-24" transform="translate(0.000000, 0.344444)">
+ <g id="phone">
+ <path d="M16.8073019,0 C17.6632335,0 18.4024362,0.302394979 19.024932,0.90719401 C19.6474277,1.51199304 19.958671,2.2435938 19.958671,3.10201823 L19.958671,26.8646484 C19.958671,27.7230729 19.6474277,28.4546736 19.024932,29.0594727 C18.4024362,29.6642717 17.6632335,29.9666667 16.8073019,29.9666667 L3.1513691,29.9666667 C2.29543741,29.9666667 1.55623477,29.6642717 0.933738992,29.0594727 C0.311243218,28.4546736 0,27.7230729 0,26.8646484 L0,3.10201823 C0,2.2435938 0.311243218,1.51199304 0.933738992,0.90719401 C1.55623477,0.302394979 2.29543741,0 3.1513691,0 L16.8073019,0 Z" id="Path" fill="#DDD9ED"></path>
+ <rect id="Rectangle-4" fill="#FFFFFF" x="1.34642857" y="6.26169154" width="17.5035714" height="17.4432836"></rect>
+ <path d="M16.8073019,0 C17.6632335,0 18.4024362,0.302394979 19.024932,0.90719401 C19.6474277,1.51199304 19.958671,2.2435938 19.958671,3.10201823 L19.958671,26.8646484 C19.958671,27.7230729 19.6474277,28.4546736 19.024932,29.0594727 C18.4024362,29.6642717 17.6632335,29.9666667 16.8073019,29.9666667 L3.1513691,29.9666667 C2.29543741,29.9666667 1.55623477,29.6642717 0.933738992,29.0594727 C0.311243218,28.4546736 0,27.7230729 0,26.8646484 L0,3.10201823 C0,2.2435938 0.311243218,1.51199304 0.933738992,0.90719401 C1.55623477,0.302394979 2.29543741,0 3.1513691,0 L16.8073019,0 Z M1.28389111,6.2625651 L1.28389111,23.7041016 L18.6747798,23.7041016 L18.6747798,6.2625651 L1.28389111,6.2625651 Z M3.1513691,1.22910156 C2.64559128,1.22910156 2.20790551,1.41444042 1.83829864,1.7851237 C1.46869178,2.15580697 1.28389111,2.59476743 1.28389111,3.10201823 L1.28389111,4.9749349 L18.6747798,4.9749349 L18.6747798,3.10201823 C18.6747798,2.59476743 18.4899792,2.15580697 18.1203723,1.7851237 C17.7507655,1.41444042 17.3130797,1.22910156 16.8073019,1.22910156 L3.1513691,1.22910156 Z M16.8073019,28.7375651 C17.3130797,28.7375651 17.7507655,28.5522262 18.1203723,28.181543 C18.4899792,27.8108597 18.6747798,27.3718992 18.6747798,26.8646484 L18.6747798,24.9917318 L1.28389111,24.9917318 L1.28389111,26.8646484 C1.28389111,27.3718992 1.46869178,27.8108597 1.83829864,28.181543 C2.20790551,28.5522262 2.64559128,28.7375651 3.1513691,28.7375651 L16.8073019,28.7375651 Z M11.2048679,26.8646484 C11.2048679,27.1768028 11.0881517,27.4596884 10.8547158,27.7133138 C10.6212799,27.9669392 10.3294894,28.09375 9.97933548,28.09375 C9.62918161,28.09375 9.33739109,27.9669392 9.10395517,27.7133138 C8.87051926,27.4596884 8.75380305,27.1768028 8.75380305,26.8646484 C8.75380305,26.5134748 8.87051926,26.2110798 9.10395517,25.9574544 C9.33739109,25.703829 9.62918161,25.5770182 9.97933548,25.5770182 C10.3294894,25.5770182 10.6212799,25.703829 10.8547158,25.9574544 C11.0881517,26.2110798 11.2048679,26.5134748 11.2048679,26.8646484 Z M6.88632507,3.74583333 C6.45835922,3.74583333 6.24437951,3.53123044 6.24437951,3.10201823 C6.24437951,2.71182531 6.45835922,2.51673177 6.88632507,2.51673177 L13.0723459,2.51673177 C13.5003117,2.51673177 13.7142914,2.71182531 13.7142914,3.10201823 C13.7142914,3.53123044 13.5003117,3.74583333 13.0723459,3.74583333 L6.88632507,3.74583333 Z" id="e" fill="#4A4745"></path>
+ </g>
+ </g>
+ <path d="M38.5929963,20.6990762 C38.5929963,20.9403506 38.5073834,21.1454307 38.3361551,21.3143227 L30.4474604,29.0953812 C30.276232,29.2642732 30.068315,29.3487179 29.8237031,29.3487179 C29.5790912,29.3487179 29.3711742,29.2642732 29.1999458,29.0953812 L24.6318412,24.5896055 C24.4606129,24.4207134 24.375,24.2156333 24.375,23.974359 C24.375,23.7330846 24.4606129,23.5280045 24.6318412,23.3591125 L25.8793557,22.1286195 C26.0505841,21.9597275 26.2585011,21.8752827 26.503113,21.8752827 C26.7477249,21.8752827 26.9556419,21.9597275 27.1268702,22.1286195 L29.8237031,24.7977035 L35.841126,18.8533368 C36.0123544,18.6844448 36.2202714,18.6 36.4648833,18.6 C36.7094952,18.6 36.9174122,18.6844448 37.0886406,18.8533368 L38.3361551,20.0838298 C38.5073834,20.2527218 38.5929963,20.4578019 38.5929963,20.6990762 Z" id="-copy-5" stroke="#4A4A4A" stroke-width="1.4" fill="#B8E986"></path>
+ </g>
+ <g id="Group-2-Copy-2" transform="translate(139.000000, 14.000000)">
+ <g id="Group-17-Copy-3">
+ <path d="M33.6594772,3.45745435 C33.6594772,3.03338815 33.5120634,2.68160596 33.214896,2.39969831 C32.9153887,2.11779067 32.5620636,1.97563211 32.150241,1.97563211 L2.94827538,1.97563211 C2.53645278,1.97563211 2.18312772,2.11779067 1.88596028,2.39969831 C1.58645294,2.68160596 1.43903917,3.03338815 1.43903917,3.45745435 L1.43903917,21.5284572 C1.43903917,21.9525234 1.58645294,22.3043056 1.88596028,22.5886227 C2.18312772,22.8705304 2.53645278,23.0102795 2.94827538,23.0102795 L32.150241,23.0102795 C32.5620636,23.0102795 32.9153887,22.8705304 33.214896,22.5886227 C33.5120634,22.3043056 33.6594772,21.9525234 33.6594772,21.5284572 L33.6594772,3.45745435 Z" id="Path" fill="#E9E7F3"></path>
+ <path d="M33.6594772,3.45745435 C33.6594772,3.03338815 33.6416783,2.68160596 33.6057981,2.39969831 C33.5696353,2.11779067 33.5269745,1.97563211 33.4772506,1.97563211 L29.951378,1.97563211 C29.9016541,1.97563211 29.8589933,2.11779067 29.8231131,2.39969831 C29.7869503,2.68160596 29.7691514,3.03338815 29.7691514,3.45745435 L29.7691514,21.5284572 C29.7691514,21.9525234 29.7869503,22.3043056 29.8231131,22.5886227 C29.8589933,22.8705304 29.9016541,23.0102795 29.951378,23.0102795 L33.4772506,23.0102795 C33.5269745,23.0102795 33.5696353,22.8705304 33.6057981,22.5886227 C33.6416783,22.3043056 33.6594772,21.9525234 33.6594772,21.5284572 L33.6594772,3.45745435 Z" id="Path" fill="#DDD9ED"></path>
+ <path d="M31.1230244,18.6346873 C31.534847,18.8708151 31.6027041,19.2225973 31.3289357,19.6924433 C31.1908816,19.9285711 30.9849703,20.046635 30.7112018,20.046635 C30.6199457,20.046635 30.4818915,19.9984457 30.2993792,19.9044764 L23.7195774,15.3867257 C23.3077548,15.1530074 23.2398976,14.7988158 23.5136661,14.3289697 C23.7429764,13.9049035 24.0846019,13.8350289 24.5408827,14.1169366 L31.1230244,18.6346873 Z M17.5492582,16.3770167 L4.73128001,7.83545598 C4.31945742,7.55113887 4.25160028,7.22345135 4.52536871,6.84516502 C4.79913714,6.37531895 5.11736369,6.28134973 5.48472816,6.56325738 L17.5492582,14.6108773 L29.6137882,6.56325738 C29.9811527,6.28134973 30.2993792,6.37531895 30.5754876,6.84516502 C30.849256,7.22345135 30.7813989,7.55113887 30.3695763,7.83545598 L17.5492582,16.3770167 Z M11.5848503,14.3289697 C11.8609586,14.7988158 11.7907616,15.1530074 11.378939,15.3867257 L4.79913714,19.9044764 C4.61662485,19.9984457 4.47857069,20.046635 4.38731455,20.046635 C4.11354612,20.046635 3.90763482,19.9285711 3.77192056,19.6924433 C3.49581223,19.2225973 3.56600926,18.8708151 3.97549196,18.6346873 L10.5576337,14.1169366 C11.0139144,13.8350289 11.35554,13.9049035 11.5848503,14.3289697 L11.5848503,14.3289697 Z M33.6594772,3.45745435 C33.6594772,3.03338815 33.5120634,2.68160596 33.214896,2.39969831 C32.9153887,2.11779067 32.5620636,1.97563211 32.150241,1.97563211 L2.94827538,1.97563211 C2.53645278,1.97563211 2.18312772,2.11779067 1.88596028,2.39969831 C1.58645294,2.68160596 1.43903917,3.03338815 1.43903917,3.45745435 L1.43903917,21.5284572 C1.43903917,21.9525234 1.58645294,22.3043056 1.88596028,22.5886227 C2.18312772,22.8705304 2.53645278,23.0102795 2.94827538,23.0102795 L32.150241,23.0102795 C32.5620636,23.0102795 32.9153887,22.8705304 33.214896,22.5886227 C33.5120634,22.3043056 33.6594772,21.9525234 33.6594772,21.5284572 L33.6594772,3.45745435 Z M34.2421126,1.30580027 C34.8130484,1.89371023 35.0985164,2.61173141 35.0985164,3.45745435 L35.0985164,21.5284572 C35.0985164,22.3765896 34.8130484,23.0946108 34.2421126,23.6825208 C33.6688368,24.2704307 32.9738862,24.5643857 32.150241,24.5643857 L2.94827538,24.5643857 C2.12463019,24.5643857 1.42967957,24.2704307 0.8564038,23.6825208 C0.285467933,23.0946108 0,22.3765896 0,21.5284572 L0,3.45745435 C0,2.61173141 0.285467933,1.89371023 0.8564038,1.30580027 C1.42967957,0.715480845 2.12463019,0.421525865 2.94827538,0.421525865 L32.150241,0.421525865 C32.9738862,0.421525865 33.6688368,0.715480845 34.2421126,1.30580027 L34.2421126,1.30580027 Z" id="Fill-1" fill="#4A4A4A"></path>
+ <path d="M17.5492582,16.3770167 L4.73128001,7.83545598 C4.31945742,7.55113887 4.25160028,7.22345135 4.52536871,6.84516502 C4.79913714,6.37531895 5.11736369,6.28134973 5.48472816,6.56325738 L17.5492582,14.6108773 L17.5492582,16.3770167 Z" id="Path" fill="#4A4A4A"></path>
+ </g>
+ <path d="M51.0829388,21.9627185 L49.8523364,23.2093771 C49.6834293,23.380488 49.4783309,23.4660422 49.2370351,23.4660422 C48.9957393,23.4660422 48.790641,23.380488 48.6217339,23.2093771 L45.9614609,20.5143945 L43.3011879,23.2093771 C43.1322808,23.380488 42.9271825,23.4660422 42.6858867,23.4660422 C42.4445909,23.4660422 42.2394925,23.380488 42.0705854,23.2093771 L40.839983,21.9627185 C40.6710759,21.7916077 40.5866236,21.5838333 40.5866236,21.3393892 C40.5866236,21.0949451 40.6710759,20.8871708 40.839983,20.7160599 L43.500256,18.0210773 L40.839983,15.3260947 C40.6710759,15.1549838 40.5866236,14.9472094 40.5866236,14.7027654 C40.5866236,14.4583213 40.6710759,14.2505469 40.839983,14.079436 L42.0705854,12.8327774 C42.2394925,12.6616666 42.4445909,12.5761124 42.6858867,12.5761124 C42.9271825,12.5761124 43.1322808,12.6616666 43.3011879,12.8327774 L45.9614609,15.52776 L48.6217339,12.8327774 C48.790641,12.6616666 48.9957393,12.5761124 49.2370351,12.5761124 C49.4783309,12.5761124 49.6834293,12.6616666 49.8523364,12.8327774 L51.0829388,14.079436 C51.2518459,14.2505469 51.3362982,14.4583213 51.3362982,14.7027654 C51.3362982,14.9472094 51.2518459,15.1549838 51.0829388,15.3260947 L48.4226658,18.0210773 L51.0829388,20.7160599 C51.2518459,20.8871708 51.3362982,21.0949451 51.3362982,21.3393892 C51.3362982,21.5838333 51.2518459,21.7916077 51.0829388,21.9627185 Z" id="x" stroke="#4A4A4A" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" fill="#F2565B"></path>
+ </g>
+ <g id="Group-2-Copy-3">
+ <path d="M8.13252691,23.9271619 C6.95549844,22.9955915 6.2,21.5521646 6.2,19.9317865 C6.2,17.1223367 8.47113736,14.8448276 11.2727273,14.8448276 C14.0743172,14.8448276 16.3454545,17.1223367 16.3454545,19.9317865 C16.3454545,21.5521646 15.5899561,22.9955915 14.4129276,23.9271619 C17.527356,25.1772932 19.7272727,28.2316954 19.7272727,31.8013572 C19.7272727,36.4837736 15.9420438,36.0404896 11.2727273,36.0404896 C6.60341075,36.0404896 2.81818182,36.4837736 2.81818182,31.8013572 C2.81818182,28.2316954 5.01809856,25.1772932 8.13252691,23.9271619 Z" id="Combined-Shape-Copy-2" stroke="#4A4745" stroke-width="1.68" fill="#DDD9ED"></path>
+ <path d="M28.5643451,28.8754378 C27.3873166,27.9438674 26.6318182,26.5004404 26.6318182,24.8800623 C26.6318182,22.0706125 28.9029555,19.7931034 31.7045455,19.7931034 C34.5061354,19.7931034 36.7772727,22.0706125 36.7772727,24.8800623 C36.7772727,26.5004404 36.0217743,27.9438674 34.8447458,28.8754378 C37.9591742,30.125569 40.1590909,33.1799713 40.1590909,36.7496331 C40.1590909,41.4320494 36.373862,40.9887655 31.7045455,40.9887655 C27.0352289,40.9887655 23.25,41.4320494 23.25,36.7496331 C23.25,33.1799713 25.4499167,30.125569 28.5643451,28.8754378 Z" id="Combined-Shape-Copy-3" stroke="#4A4745" stroke-width="1.68" fill="#DDD9ED"></path>
+ <path d="M61.5882527,31.8436016 C61.5882527,32.0911829 61.501642,32.3016238 61.3284179,32.4749307 L53.3477804,40.4593872 C53.1745564,40.6326941 52.9642161,40.7193463 52.7167532,40.7193463 C52.4692904,40.7193463 52.2589501,40.6326941 52.0857261,40.4593872 L47.4643802,35.8358298 C47.2911562,35.6625229 47.2045455,35.452082 47.2045455,35.2045007 C47.2045455,34.9569194 47.2911562,34.7464785 47.4643802,34.5731716 L48.7264345,33.3105134 C48.8996585,33.1372065 49.1099988,33.0505543 49.3574616,33.0505543 C49.6049245,33.0505543 49.8152648,33.1372065 49.9884888,33.3105134 L52.7167532,36.0493676 L58.8043093,29.9496142 C58.9775333,29.7763073 59.1878736,29.6896552 59.4353365,29.6896552 C59.6827993,29.6896552 59.8931396,29.7763073 60.0663636,29.9496142 L61.3284179,31.2122725 C61.501642,31.3855794 61.5882527,31.5960203 61.5882527,31.8436016 Z" id="-copy-6" stroke="#4A4A4A" stroke-width="1.4" fill="#B8E986"></path>
+ <path d="M47.240592,9.99879743 C47.240592,12.0978951 46.5129103,13.9231701 45.0575251,15.474677 C43.8447041,16.8740755 42.7228615,18.6537186 41.6919637,20.8136597 C41.4493995,21.3004069 41.252319,21.5437769 41.1007164,21.5437769 C41.0703959,21.5437769 41.0400758,21.5285663 41.0097553,21.4981446 L40.9642747,21.4981446 C40.7823516,21.4373012 40.6838114,21.3232215 40.6686511,21.1559021 C40.6534908,20.9885828 40.6913909,20.6919756 40.7823525,20.2660717 C40.8733141,19.8097461 40.7823539,19.3990593 40.5094691,19.0339988 C40.0243408,18.3647213 39.251179,18.0300875 38.1899606,18.0300875 L31.9591239,18.0300875 C29.7457256,18.0300875 27.8583013,17.2467404 26.2967943,15.6800225 C24.7352873,14.1133047 23.9545455,12.2195819 23.9545455,9.99879743 C23.9545455,7.71716951 24.7428673,5.81584144 26.3195346,4.29475616 C27.8962018,2.71282748 29.7760462,1.921875 31.9591239,1.921875 L39.2360135,1.921875 C41.4494118,1.921875 43.3368361,2.71282748 44.8983431,4.29475616 C46.4598502,5.87668485 47.240592,7.77801292 47.240592,9.99879743 Z M41.555522,19.2621604 C42.2832146,17.8323403 43.2079767,16.4481734 44.3298362,15.1096184 L44.2843556,15.1096184 C45.7094203,13.6797982 46.4219419,11.9762083 46.4219419,9.99879743 C46.4219419,7.99096486 45.7170003,6.2797696 44.3070959,4.86516029 C42.8971915,3.45055099 41.2068476,2.74325694 39.2360135,2.74325694 L31.9591239,2.74325694 C29.9882898,2.74325694 28.2903659,3.4581563 26.8653013,4.88797646 C25.4705571,6.34821832 24.7731955,8.05180827 24.7731955,9.99879743 C24.7731955,11.9762083 25.4781372,13.6721929 26.8880415,15.0868022 C28.2979459,16.5014115 29.9882898,17.2087056 31.9591239,17.2087056 L38.2809217,17.2087056 C38.584127,17.2087056 39.0540881,17.31518 39.6908191,17.5281319 C40.3275501,17.7410838 40.8278312,18.0757176 41.1916775,18.5320432 C41.3736007,18.8362602 41.494881,19.0796302 41.555522,19.2621604 Z M30.0489404,8.12787189 C30.5947099,8.12787189 31.064671,8.32561001 31.4588378,8.72109218 C31.8530046,9.11657436 32.0500851,9.58810372 32.0500851,10.1356944 C32.0500851,10.6832851 31.8530046,11.1472092 31.4588378,11.5274805 C31.064671,11.9077518 30.5947099,12.0978846 30.0489404,12.0978846 C29.503171,12.0978846 29.0332099,11.9077518 28.6390431,11.5274805 C28.2448763,11.1472092 28.0477958,10.6832851 28.0477958,10.1356944 C28.0477958,9.58810372 28.2448763,9.11657436 28.6390431,8.72109218 C29.0332099,8.32561001 29.503171,8.12787189 30.0489404,8.12787189 Z M30.0489404,11.2765027 C30.3824662,11.2765027 30.6629269,11.162423 30.8903308,10.9342602 C31.1177347,10.7060974 31.231435,10.4399115 31.231435,10.1356944 C31.231435,9.80105566 31.1101547,9.5196591 30.8675905,9.29149631 C30.6250263,9.06333352 30.3521457,8.94925383 30.0489404,8.94925383 C29.7457352,8.94925383 29.4728546,9.06333352 29.2302904,9.29149631 C28.9877262,9.5196591 28.8664459,9.80105566 28.8664459,10.1356944 C28.8664459,10.4399115 28.9801462,10.7060974 29.2075501,10.9342602 C29.434954,11.162423 29.7154147,11.2765027 30.0489404,11.2765027 Z M35.1882437,8.03660723 C35.7340131,8.03660723 36.2039742,8.23434535 36.598141,8.62982752 C36.9923078,9.0253097 37.1893883,9.49683906 37.1893883,10.0444298 C37.1893883,10.5920205 36.9923078,11.0635498 36.598141,11.459032 C36.2039742,11.8545142 35.7340131,12.0522523 35.1882437,12.0522523 C34.6424742,12.0522523 34.1725131,11.8545142 33.7783463,11.459032 C33.3841795,11.0635498 33.1870991,10.5920205 33.1870991,10.0444298 C33.1870991,9.49683906 33.3841795,9.0253097 33.7783463,8.62982752 C34.1725131,8.23434535 34.6424742,8.03660723 35.1882437,8.03660723 Z M35.1882437,11.2308703 C35.5217694,11.2308703 35.8022301,11.1167907 36.029634,10.8886279 C36.257038,10.6604651 36.3707382,10.3790685 36.3707382,10.0444298 C36.3707382,9.7402127 36.257038,9.47402677 36.029634,9.24586398 C35.8022301,9.01770119 35.5217694,8.9036215 35.1882437,8.9036215 C34.8547179,8.9036215 34.5742573,9.01770119 34.3468533,9.24586398 C34.1194494,9.47402677 34.0057491,9.7402127 34.0057491,10.0444298 C34.0057491,10.3790685 34.1194494,10.6604651 34.3468533,10.8886279 C34.5742573,11.1167907 34.8547179,11.2308703 35.1882437,11.2308703 Z M40.3730275,8.12787189 C40.9187969,8.12787189 41.388758,8.32561001 41.7829248,8.72109218 C42.1770916,9.11657436 42.3741721,9.58810372 42.3741721,10.1356944 C42.3741721,10.6832851 42.1770916,11.1472092 41.7829248,11.5274805 C41.388758,11.9077518 40.9187969,12.0978846 40.3730275,12.0978846 C39.827258,12.0978846 39.364877,11.9077518 38.9858704,11.5274805 C38.6068638,11.1472092 38.4173634,10.6832851 38.4173634,10.1356944 C38.4173634,9.58810372 38.6068638,9.11657436 38.9858704,8.72109218 C39.364877,8.32561001 39.827258,8.12787189 40.3730275,8.12787189 Z M40.3730275,11.2765027 C40.7065532,11.2765027 40.9870139,11.162423 41.2144178,10.9342602 C41.4418218,10.7060974 41.555522,10.4399115 41.555522,10.1356944 C41.555522,9.80105566 41.4418218,9.5196591 41.2144178,9.29149631 C40.9870139,9.06333352 40.7065532,8.94925383 40.3730275,8.94925383 C40.0698222,8.94925383 39.8045216,9.06333352 39.5771177,9.29149631 C39.3497137,9.5196591 39.2360135,9.80105566 39.2360135,10.1356944 C39.2360135,10.4703332 39.3421337,10.7441244 39.5543774,10.9570764 C39.7666211,11.1700283 40.0395017,11.2765027 40.3730275,11.2765027 Z" id="6-copy-2" fill="#4A4A4A"></path>
+ <path d="M16.9090909,6.29027478 C16.9090909,7.8145281 16.3806871,9.13994588 15.3238636,10.2665679 C14.4431774,11.2827368 13.6285549,12.5750191 12.8799716,14.1434537 C12.7038343,14.4969037 12.560725,14.6736261 12.4506392,14.6736261 C12.428622,14.6736261 12.4066052,14.6625809 12.3845881,14.6404903 L12.3515625,14.6404903 C12.2194596,14.596309 12.1479049,14.5134704 12.1368963,14.391972 C12.1258877,14.2704735 12.1534088,14.0550931 12.2194602,13.7458244 C12.2855117,13.4144649 12.2194612,13.1162459 12.0213068,12.8511584 C11.6690323,12.3651646 11.1076033,12.1221713 10.3370028,12.1221713 L5.8125,12.1221713 C4.20524765,12.1221713 2.83470027,11.5533462 1.70081676,10.4156789 C0.566933251,9.27801155 2.0783375e-13,7.90289061 2.0783375e-13,6.29027478 C2.0783375e-13,4.63347771 0.572437457,3.25283419 1.71732955,2.1483028 C2.86222163,0.999590162 4.2272648,0.425242457 5.8125,0.425242457 L11.0965909,0.425242457 C12.7038433,0.425242457 14.0743906,0.999590162 15.2082741,2.1483028 C16.3421577,3.29701544 16.9090909,4.67765896 16.9090909,6.29027478 Z M12.7808949,13.0168373 C13.3093066,11.9785778 13.9808198,10.9734693 14.7954545,10.0014817 L14.762429,10.0014817 C15.7972353,8.96322218 16.3146307,7.72616558 16.3146307,6.29027478 C16.3146307,4.83229336 15.8027395,3.58971419 14.7789418,2.5625 C13.755144,1.53528581 12.527706,1.02168642 11.0965909,1.02168642 L5.8125,1.02168642 C4.38138489,1.02168642 3.14844267,1.54080839 2.11363636,2.57906789 C1.10084721,3.63941802 0.594460227,4.87647461 0.594460227,6.29027478 C0.594460227,7.72616558 1.10635142,8.95769961 2.13014915,9.98491379 C3.15394688,11.012128 4.38138489,11.5257274 5.8125,11.5257274 L10.403054,11.5257274 C10.6232255,11.5257274 10.9644863,11.6030434 11.4268466,11.7576778 C11.8892069,11.9123122 12.2524845,12.1553055 12.5166903,12.4866649 C12.6487933,12.7075711 12.7368606,12.8842935 12.7808949,13.0168373 Z M4.42542614,4.93170797 C4.82173494,4.93170797 5.16299573,5.0752949 5.44921875,5.36247306 C5.73544177,5.64965122 5.87855114,5.99205081 5.87855114,6.38968211 C5.87855114,6.78731341 5.73544177,7.12419043 5.44921875,7.40032328 C5.16299573,7.67645612 4.82173494,7.81452047 4.42542614,7.81452047 C4.02911734,7.81452047 3.68785654,7.67645612 3.40163352,7.40032328 C3.1154105,7.12419043 2.97230114,6.78731341 2.97230114,6.38968211 C2.97230114,5.99205081 3.1154105,5.64965122 3.40163352,5.36247306 C3.68785654,5.0752949 4.02911734,4.93170797 4.42542614,4.93170797 Z M4.42542614,7.21807651 C4.66761485,7.21807651 4.87127048,7.1352379 5.03639915,6.96955819 C5.20152781,6.80387848 5.28409091,6.61058839 5.28409091,6.38968211 C5.28409091,6.14668521 5.19602361,5.94234997 5.01988636,5.77667026 C4.84374912,5.61099055 4.64559769,5.52815194 4.42542614,5.52815194 C4.20525458,5.52815194 4.00710315,5.61099055 3.83096591,5.77667026 C3.65482866,5.94234997 3.56676136,6.14668521 3.56676136,6.38968211 C3.56676136,6.61058839 3.64932446,6.80387848 3.81445313,6.96955819 C3.97958179,7.1352379 4.18323743,7.21807651 4.42542614,7.21807651 Z M8.15731534,4.86543642 C8.55362414,4.86543642 8.89488493,5.00902335 9.18110795,5.29620151 C9.46733098,5.58337967 9.61044034,5.92577926 9.61044034,6.32341056 C9.61044034,6.72104186 9.46733098,7.06344145 9.18110795,7.35061961 C8.89488493,7.63779777 8.55362414,7.7813847 8.15731534,7.7813847 C7.76100654,7.7813847 7.41974575,7.63779777 7.13352273,7.35061961 C6.84729971,7.06344145 6.70419034,6.72104186 6.70419034,6.32341056 C6.70419034,5.92577926 6.84729971,5.58337967 7.13352273,5.29620151 C7.41974575,5.00902335 7.76100654,4.86543642 8.15731534,4.86543642 Z M8.15731534,7.18494073 C8.39950405,7.18494073 8.60315969,7.10210212 8.76828835,6.93642241 C8.93341702,6.77074271 9.01598011,6.56640746 9.01598011,6.32341056 C9.01598011,6.10250428 8.93341702,5.90921419 8.76828835,5.74353448 C8.60315969,5.57785478 8.39950405,5.49501616 8.15731534,5.49501616 C7.91512663,5.49501616 7.711471,5.57785478 7.54634233,5.74353448 C7.38121366,5.90921419 7.29865057,6.10250428 7.29865057,6.32341056 C7.29865057,6.56640746 7.38121366,6.77074271 7.54634233,6.93642241 C7.711471,7.10210212 7.91512663,7.18494073 8.15731534,7.18494073 Z M11.9222301,4.93170797 C12.3185389,4.93170797 12.6597997,5.0752949 12.9460227,5.36247306 C13.2322457,5.64965122 13.3753551,5.99205081 13.3753551,6.38968211 C13.3753551,6.78731341 13.2322457,7.12419043 12.9460227,7.40032328 C12.6597997,7.67645612 12.3185389,7.81452047 11.9222301,7.81452047 C11.5259213,7.81452047 11.1901647,7.67645612 10.9149503,7.40032328 C10.6397358,7.12419043 10.5021307,6.78731341 10.5021307,6.38968211 C10.5021307,5.99205081 10.6397358,5.64965122 10.9149503,5.36247306 C11.1901647,5.0752949 11.5259213,4.93170797 11.9222301,4.93170797 Z M11.9222301,7.21807651 C12.1644188,7.21807651 12.3680745,7.1352379 12.5332031,6.96955819 C12.6983318,6.80387848 12.7808949,6.61058839 12.7808949,6.38968211 C12.7808949,6.14668521 12.6983318,5.94234997 12.5332031,5.77667026 C12.3680745,5.61099055 12.1644188,5.52815194 11.9222301,5.52815194 C11.7020586,5.52815194 11.5094113,5.61099055 11.3442827,5.77667026 C11.179154,5.94234997 11.0965909,6.14668521 11.0965909,6.38968211 C11.0965909,6.63267902 11.1736498,6.83149168 11.3277699,6.98612608 C11.48189,7.14076047 11.6800414,7.21807651 11.9222301,7.21807651 Z" id="6-copy-3" fill="#4A4A4A" transform="translate(8.454545, 7.549434) scale(-1, 1) translate(-8.454545, -7.549434) "></path>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/web-ui/app/images/account-recovery/codes.svg b/web-ui/app/images/account-recovery/codes.svg
new file mode 100644
index 00000000..553064b4
--- /dev/null
+++ b/web-ui/app/images/account-recovery/codes.svg
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="42px" height="65px" viewBox="0 0 42 65" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
+ <title>Group Copy 3</title>
+ <desc>Created with Sketch.</desc>
+ <defs>
+ <path d="M1.16084559,3.94126428 C1.8019629,3.21411956 2.89307448,2.09351474 3.60127528,1.43519653 L5.14522059,2.64517297e-13 L28.2522602,2.64517297e-13 C29.2175102,2.64517297e-13 30,0.778887644 30,1.75376832 L30,39.0612034 C30,40.0297829 29.2205091,40.7957878 28.2577703,40.7720939 L14.2813382,40.4281215 L8.02105049e-13,37.0941474 L8.02105049e-13,5.25787628 L1.16084559,3.94126428 Z" id="path-1"></path>
+ <mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="30" height="40.7722731" fill="white">
+ <use xlink:href="#path-1"></use>
+ </mask>
+ </defs>
+ <g id="Account-Recovery-MVP" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="02-m_Forgot_Password-" transform="translate(-167.000000, -213.000000)">
+ <g id="Group-Copy-3" transform="translate(167.000000, 213.000000)">
+ <g id="Group-28" transform="translate(0.000000, 30.335452)">
+ <path d="M32.1836735,10.5981349 L16.7446084,1.01926433 L1.66630672,10.9498508 L1.63636364,17.1116022 C1.63636364,17.5098564 1.77460901,17.8402262 2.05548851,18.1072375 C2.33417364,18.371986 2.66552367,18.5032288 3.05173297,18.5032288 L30.4374839,18.5032288 C30.8236932,18.5032288 31.1550432,18.371986 31.4359227,18.1072375 C31.7146078,17.8402262 31.8528532,17.5098564 31.8528532,17.1116022 L32.1836735,10.5981349 Z" id="Path-Copy-2" fill="#E9E7F3"></path>
+ <path d="M31.3073987,15.0966254 C31.3073987,14.6983713 31.1691533,14.3680014 30.8904682,14.1032529 C30.6095887,13.8385044 30.2782386,13.7049988 29.8920293,13.7049988 L2.50627843,13.7049988 C2.12006912,13.7049988 1.78871909,13.8385044 1.51003396,14.1032529 C1.22915447,14.3680014 1.09090909,14.6983713 1.09090909,15.0966254 L1.09090909,32.0676812 C1.09090909,32.4659353 1.22915447,32.7963052 1.51003396,33.0633164 C1.78871909,33.3280649 2.12006912,33.4593077 2.50627843,33.4593077 L29.8920293,33.4593077 C30.2782386,33.4593077 30.6095887,33.3280649 30.8904682,33.0633164 C31.1691533,32.7963052 31.3073987,32.4659353 31.3073987,32.0676812 L31.3073987,15.0966254 Z" id="Path-Copy-3" fill="#E9E7F3"></path>
+ <path d="M16.4575118,0.467710658 L1.46537043,10.4722111 C0.983695211,10.8052241 0.904328271,11.1890357 1.22453282,11.6321123 C1.54473738,12.1824304 1.91694096,12.292494 2.34661715,11.9623032 L16.4575118,2.53634221 L30.5684064,11.9623032 C30.9980826,12.292494 31.3702862,12.1824304 31.6932275,11.6321123 C32.0134321,11.1890357 31.9340651,10.8052241 31.4523899,10.4722111 L16.4575118,0.467710658 Z M29.1873295,29.0760343 C29.5735388,29.2977894 29.6371756,29.6281593 29.3804342,30.0694068 C29.2509663,30.2911619 29.0578616,30.4020395 28.8011202,30.4020395 C28.7155398,30.4020395 28.5860719,30.3567833 28.4149109,30.2685338 L22.2443395,26.0257699 C21.8581302,25.8062776 21.7944934,25.4736449 22.0512348,25.0323974 C22.2662832,24.6341433 22.5866614,24.5685219 23.0145637,24.8332704 L29.1873295,29.0760343 Z M10.6712265,26.0257699 L4.50065506,30.2685338 C4.32949411,30.3567833 4.20002622,30.4020395 4.11444575,30.4020395 C3.85770433,30.4020395 3.66459968,30.2911619 3.53732616,30.0694068 C3.27839037,29.6281593 3.3442215,29.2977894 3.72823644,29.0760343 L9.90100225,24.8332704 C10.3289046,24.5685219 10.6492828,24.6341433 10.8643311,25.0323974 C11.1232669,25.4736449 11.0574358,25.8062776 10.6712265,26.0257699 Z M31.5660278,14.8226102 C31.5660278,14.4243561 31.4277824,14.0939862 31.1490973,13.8292378 C30.8682178,13.5644893 30.5368677,13.4309837 30.1506584,13.4309837 L2.76490754,13.4309837 C2.37869823,13.4309837 2.0473482,13.5644893 1.76866308,13.8292378 C1.48778358,14.0939862 1.34953821,14.4243561 1.34953821,14.8226102 L1.34953821,31.793666 C1.34953821,32.1919201 1.48778358,32.52229 1.76866308,32.7893013 C2.0473482,33.0540498 2.37869823,33.1852926 2.76490754,33.1852926 L30.1506584,33.1852926 C30.5368677,33.1852926 30.8682178,33.0540498 31.1490973,32.7893013 C31.4277824,32.52229 31.5660278,32.1919201 31.5660278,31.793666 L31.5660278,14.8226102 Z M32.915566,14.8226102 L32.915566,31.793666 C32.915566,32.5901743 32.6478527,33.2644909 32.1124262,33.8166159 C31.5748053,34.3687409 30.9230771,34.6448034 30.1506584,34.6448034 L2.76490754,34.6448034 C1.99248893,34.6448034 1.34076072,34.3687409 0.80313981,33.8166159 C0.26771327,33.2644909 0,32.5901743 0,31.793666 L0,14.8226102 C0,14.0283648 0.26771327,13.3540482 0.80313981,12.8019232 C1.34076072,12.2475354 1.99248893,11.9714729 2.76490754,11.9714729 L30.1506584,11.9714729 C30.9230771,11.9714729 31.5748053,12.2475354 32.1124262,12.8019232 C32.6478527,13.3540482 32.915566,14.0283648 32.915566,14.8226102 Z" id="Fill-1-Copy" fill="#4A4A4A"></path>
+ </g>
+ <g id="Group-9" transform="translate(22.540482, 23.914018) rotate(-13.000000) translate(-22.540482, -23.914018) translate(7.475265, 3.289018)">
+ <g id="Group-25" transform="translate(0.000000, 0.000000)">
+ <g id="Group-15-Copy" transform="translate(-0.000000, 0.000000)">
+ <use id="Rectangle-10" stroke="#4A4A4A" mask="url(#mask-2)" stroke-width="2.6208" stroke-linejoin="round" fill="#E9E7F3" xlink:href="#path-1"></use>
+ <path d="M29.0470588,3.90529677 C29.0470588,3.13553874 29.0397386,2.49698946 29.0249818,1.98527532 C29.0101089,1.47356117 28.9925635,1.21551728 28.9721133,1.21551728 L27.5220044,1.21551728 C27.5015541,1.21551728 27.4840087,1.47356117 27.469252,1.98527532 C27.4543791,2.49698946 27.4470588,3.13553874 27.4470588,3.90529677 L27.4470588,36.7074856 C27.4470588,37.4772436 27.4543791,38.1157929 27.469252,38.6318806 C27.4840087,39.1435948 27.5015541,39.3972651 27.5220044,39.3972651 L28.9721133,39.3972651 C28.9925635,39.3972651 29.0101089,39.1435948 29.0249818,38.6318806 C29.0397386,38.1157929 29.0470588,37.4772436 29.0470588,36.7074856 L29.0470588,3.90529677 Z" id="Path" fill="#DDD9ED"></path>
+ </g>
+ <path d="M0.575597426,5.18373113 L5.07111673,0.342010537 C5.07111673,0.342010537 5.85144761,5.94661178 5.10834099,6.44591288 C4.36523438,6.94521397 0.575597426,5.18373113 0.575597426,5.18373113 Z" id="Path-2" stroke="#4A4A4A" stroke-width="0.8736" stroke-linecap="round" stroke-linejoin="round" fill="#918DA9"></path>
+ </g>
+ <text id="xyz-42-abc" font-family="OpenSans-BoldItalic, Open Sans" font-size="7.4256" font-style="italic" font-weight="bold" line-spacing="10.4832" fill="#4A4A4A">
+ <tspan x="9.41267109" y="11.309322">xyz</tspan>
+ <tspan x="10.9101188" y="21.792522">42</tspan>
+ <tspan x="8.79084961" y="32.275722">abc</tspan>
+ </text>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/web-ui/app/images/account-recovery/step_1.svg b/web-ui/app/images/account-recovery/step_1.svg
new file mode 100644
index 00000000..a3e73b94
--- /dev/null
+++ b/web-ui/app/images/account-recovery/step_1.svg
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="295px" height="38px" viewBox="0 0 295 38" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
+ <title>Group 5</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Account-Recovery-MVP" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="01-m_Forgot_Password" transform="translate(-40.000000, -96.000000)">
+ <g id="Group-5" transform="translate(40.000000, 96.000000)">
+ <rect id="Rectangle-15-Copy-7" fill="#4EADC3" x="19" y="18" width="100" height="2"></rect>
+ <rect id="Rectangle-15-Copy-4" fill="#D4D4D4" x="111" y="18" width="180" height="2"></rect>
+ <g id="Group-3-Copy-2" transform="translate(97.666667, 6.000000)">
+ <ellipse id="Oval-6-Copy-2" fill="#D4D4D4" transform="translate(13.000000, 13.000000) rotate(-270.000000) translate(-13.000000, -13.000000) " cx="13" cy="13" rx="13" ry="13"></ellipse>
+ <g id="Group" transform="translate(5.958333, 8.125000)" fill="#FFFFFF">
+ <path d="M12.4751456,7.26752805 C12.6402178,7.35961788 12.6674172,7.49681293 12.5576817,7.6800529 C12.502345,7.77214273 12.4198089,7.81818765 12.3100734,7.81818765 C12.2734949,7.81818765 12.2181582,7.79939381 12.1450012,7.76274581 L9.50759726,6.00082303 C9.34252504,5.90967289 9.31532564,5.77153815 9.42506115,5.58829818 C9.51697637,5.42291236 9.65391128,5.39566129 9.8368038,5.50560527 L12.4751456,7.26752805 Z M7.03432766,6.3870365 L1.89645474,3.05582783 C1.73138251,2.94494416 1.70418311,2.81714603 1.81391863,2.66961436 C1.92365414,2.48637439 2.05120994,2.4497264 2.19846187,2.55967038 L7.03432766,5.69824216 L11.8701934,2.55967038 C12.0174454,2.4497264 12.1450012,2.48637439 12.2556746,2.66961436 C12.3654101,2.81714603 12.3382107,2.94494416 12.1731385,3.05582783 L7.03432766,6.3870365 Z M4.64359416,5.58829818 C4.75426759,5.77153815 4.72613028,5.90967289 4.56105805,6.00082303 L1.92365414,7.76274581 C1.85049713,7.79939381 1.79516042,7.81818765 1.75858191,7.81818765 C1.6488464,7.81818765 1.56631029,7.77214273 1.51191149,7.6800529 C1.40123807,7.49681293 1.42937538,7.35961788 1.59350969,7.26752805 L4.23185152,5.50560527 C4.41474404,5.39566129 4.55167895,5.42291236 4.64359416,5.58829818 L4.64359416,5.58829818 Z M13.4918404,1.3484072 C13.4918404,1.18302138 13.4327521,1.04582632 13.3136375,0.935882342 C13.193585,0.825938361 13.0519605,0.770496524 12.8868883,0.770496524 L1.18176705,0.770496524 C1.01669482,0.770496524 0.875070361,0.825938361 0.755955746,0.935882342 C0.63590322,1.04582632 0.576814868,1.18302138 0.576814868,1.3484072 L0.576814868,8.39609832 C0.576814868,8.56148414 0.63590322,8.69867919 0.755955746,8.80956287 C0.875070361,8.91950685 1.01669482,8.97400899 1.18176705,8.97400899 L12.8868883,8.97400899 C13.0519605,8.97400899 13.193585,8.91950685 13.3136375,8.80956287 C13.4327521,8.69867919 13.4918404,8.56148414 13.4918404,8.39609832 L13.4918404,1.3484072 Z M13.7253801,0.509262106 C13.9542303,0.738546991 14.0686553,1.01857525 14.0686553,1.3484072 L14.0686553,8.39609832 C14.0686553,8.72686996 13.9542303,9.00689822 13.7253801,9.2361831 C13.4955921,9.46546799 13.2170327,9.58011043 12.8868883,9.58011043 L1.18176705,9.58011043 C0.851622602,9.58011043 0.573063227,9.46546799 0.34327519,9.2361831 C0.114425063,9.00689822 0,8.72686996 0,8.39609832 L0,1.3484072 C0,1.01857525 0.114425063,0.738546991 0.34327519,0.509262106 C0.573063227,0.27903753 0.851622602,0.164395087 1.18176705,0.164395087 L12.8868883,0.164395087 C13.2170327,0.164395087 13.4955921,0.27903753 13.7253801,0.509262106 L13.7253801,0.509262106 Z" id="Fill-1"></path>
+ </g>
+ </g>
+ <g id="Group-6-Copy" transform="translate(183.333333, 6.000000)">
+ <g id="Group-4" fill="#D4D4D4">
+ <ellipse id="Oval-6-Copy-3" transform="translate(13.000000, 13.000000) rotate(-270.000000) translate(-13.000000, -13.000000) " cx="13" cy="13" rx="13" ry="13"></ellipse>
+ </g>
+ <g id="Group-5-Copy-3" transform="translate(3.791667, 9.208333)" fill="#FFFFFF">
+ <g id="Group-15" transform="translate(0.000000, 0.044106)" fill-rule="nonzero">
+ <path d="M0.541666667,0.908390422 L0.541666667,7.21660958 C0.541666667,7.42046561 0.706084452,7.58333333 0.914399823,7.58333333 L18.0439335,7.58333333 C18.2532036,7.58333333 18.4166667,7.42037231 18.4166667,7.21660958 L18.4166667,0.908390422 C18.4166667,0.704534391 18.2522489,0.541666667 18.0439335,0.541666667 L0.914399823,0.541666667 C0.705129753,0.541666667 0.541666667,0.704627685 0.541666667,0.908390422 Z M0,0.908390422 C0,0.406700245 0.404742514,0 0.914399823,0 L18.0439335,0 C18.5489426,0 18.9583333,0.402920013 18.9583333,0.908390422 L18.9583333,7.21660958 C18.9583333,7.71829975 18.5535908,8.125 18.0439335,8.125 L0.914399823,8.125 C0.409390745,8.125 0,7.72207999 0,7.21660958 L0,0.908390422 Z" id="Rectangle-10"></path>
+ </g>
+ <ellipse id="Oval-5" cx="4.17600458" cy="4.03384483" rx="1.12325506" ry="1.10284184"></ellipse>
+ <ellipse id="Oval-5-Copy" cx="7.77042077" cy="4.03384483" rx="1.12325506" ry="1.10284184"></ellipse>
+ <ellipse id="Oval-5-Copy-2" cx="11.364837" cy="4.03384483" rx="1.12325506" ry="1.10284184"></ellipse>
+ <ellipse id="Oval-5-Copy-3" cx="14.9592531" cy="4.03384483" rx="1.12325506" ry="1.10284184"></ellipse>
+ </g>
+ </g>
+ <g id="Group-7-Copy-2" transform="translate(269.000000, 6.000000)">
+ <ellipse id="Oval-6-Copy-5" fill="#D4D4D4" transform="translate(13.000000, 13.000000) rotate(-270.000000) translate(-13.000000, -13.000000) " cx="13" cy="13" rx="13" ry="13"></ellipse>
+ <g id="Group-29" transform="translate(2.166667, 8.666667)" fill="#FFFFFF">
+ <g id="Group" transform="translate(3.655371, 4.373733) rotate(-270.000000) translate(-3.655371, -4.373733) translate(0.134537, 0.852900)" fill-rule="nonzero">
+ <path d="M3.55327323,6.86597405 L3.55327323,1.1921229 C3.55327323,1.04254578 3.43201702,0.921289568 3.2824399,0.921289568 C3.13286278,0.921289568 3.01160657,1.04254578 3.01160657,1.1921229 L3.01160657,6.86597405 C3.01160657,7.01555117 3.13286278,7.13680739 3.2824399,7.13680739 C3.43201702,7.13680739 3.55327323,7.01555117 3.55327323,6.86597405 Z" id="Line-Copy-13"></path>
+ <path d="M0.637125643,3.53911437 L0.637125643,0.256948898 C0.637125643,0.107371779 0.515869429,-0.0138844349 0.36629231,-0.0138844349 C0.21671519,-0.0138844349 0.0954589763,0.107371779 0.0954589763,0.256948898 L0.0954589763,3.53911437 C0.0954589763,3.68869149 0.21671519,3.8099477 0.36629231,3.8099477 C0.515869429,3.8099477 0.637125643,3.68869149 0.637125643,3.53911437 Z" id="Line-Copy-12"></path>
+ <path d="M6.59620985,3.53911437 L6.59620985,0.256948898 C6.59620985,0.107371779 6.47495364,-0.0138844349 6.32537652,-0.0138844349 C6.1757994,-0.0138844349 6.05454319,0.107371779 6.05454319,0.256948898 L6.05454319,3.53911437 C6.05454319,3.68869149 6.1757994,3.8099477 6.32537652,3.8099477 C6.47495364,3.8099477 6.59620985,3.68869149 6.59620985,3.53911437 Z" id="Line-Copy-14"></path>
+ </g>
+ <g id="Group-17" transform="translate(7.286933, 0.155729)">
+ <path d="M10.8195499,6.03227517 C10.9593721,6.11020439 10.982411,6.22630303 10.889461,6.38136628 C10.8425888,6.4592955 10.7726777,6.49826011 10.6797277,6.49826011 C10.6487444,6.49826011 10.6018721,6.48235619 10.5399055,6.45134354 L8.3059277,4.96035076 C8.16610547,4.88321673 8.14306658,4.7663229 8.23601658,4.61125965 C8.31387214,4.47130513 8.42986103,4.44824444 8.5847777,4.54128239 L10.8195499,6.03227517 Z M6.2109777,5.28717638 L1.85901103,2.46820603 C1.71918881,2.37437288 1.69614992,2.26622621 1.78909992,2.14138041 C1.88204992,1.98631716 1.99009436,1.95530451 2.11482214,2.04834246 L6.2109777,4.7042976 L10.3071333,2.04834246 C10.431861,1.95530451 10.5399055,1.98631716 10.6336499,2.14138041 C10.7265999,2.26622621 10.703561,2.37437288 10.5637388,2.46820603 M4.18593881,4.61125965 C4.27968325,4.7663229 4.25584992,4.88321673 4.1160277,4.96035076 L1.88204992,6.45134354 C1.82008325,6.48235619 1.77321103,6.49826011 1.7422277,6.49826011 C1.6492777,6.49826011 1.57936658,6.4592955 1.53328881,6.38136628 C1.43954436,6.22630303 1.4633777,6.11020439 1.60240547,6.03227517 L3.8371777,4.54128239 C3.99209436,4.44824444 4.10808325,4.47130513 4.18593881,4.61125965 L4.18593881,4.61125965 Z M11.6807277,1.02333463 C11.6807277,0.883380105 11.6306777,0.767281468 11.5297833,0.674243518 C11.4280944,0.581205569 11.3081333,0.534288996 11.168311,0.534288996 L1.25364436,0.534288996 C1.11382214,0.534288996 0.993861029,0.581205569 0.892966585,0.674243518 C0.791277696,0.767281468 0.741227696,0.883380105 0.741227696,1.02333463 L0.741227696,6.98730574 C0.741227696,7.12726026 0.791277696,7.2433589 0.892966585,7.33719205 C0.993861029,7.43023 1.11382214,7.47635137 1.25364436,7.47635137 L11.168311,7.47635137 C11.3081333,7.47635137 11.4280944,7.43023 11.5297833,7.33719205 C11.6306777,7.2433589 11.6807277,7.12726026 11.6807277,6.98730574 L11.6807277,1.02333463 Z M11.8785444,0.313224467 C12.0723888,0.507252327 12.169311,0.744220779 12.169311,1.02333463 L12.169311,6.98730574 C12.169311,7.26721479 12.0723888,7.50418324 11.8785444,7.6982111 C11.6839055,7.89223896 11.4479555,7.98925289 11.168311,7.98925289 L1.25364436,7.98925289 C0.973999918,7.98925289 0.738049918,7.89223896 0.543411029,7.6982111 C0.349566585,7.50418324 0.252644362,7.26721479 0.252644362,6.98730574 L0.252644362,1.02333463 C0.252644362,0.744220779 0.349566585,0.507252327 0.543411029,0.313224467 C0.738049918,0.118401411 0.973999918,0.0213874804 1.25364436,0.0213874804 L11.168311,0.0213874804 C11.4479555,0.0213874804 11.6839055,0.118401411 11.8785444,0.313224467" id="Fill-1"></path>
+ </g>
+ </g>
+ </g>
+ <g id="Group-2">
+ <circle id="Oval-6-Copy-4" fill="#3CAAC3" transform="translate(19.000000, 19.000000) rotate(-270.000000) translate(-19.000000, -19.000000) " cx="19" cy="19" r="19"></circle>
+ <path d="M29,17.5860465 C29,19.297683 28.4062559,20.7860402 27.21875,22.0511628 C26.2291617,23.1922538 25.3138063,24.643402 24.4726563,26.4046512 C24.2747386,26.8015524 24.1139329,27 23.9902344,27 C23.9654947,27 23.9407553,26.987597 23.9160156,26.9627907 L23.8789063,26.9627907 C23.730468,26.913178 23.6500652,26.8201557 23.6376953,26.6837209 C23.6253255,26.5472861 23.6562496,26.3054281 23.7304688,25.9581395 C23.8046879,25.5860447 23.7304699,25.2511643 23.5078125,24.9534884 C23.1119772,24.4077492 22.4811241,24.1348837 21.6152344,24.1348837 L16.53125,24.1348837 C14.7252514,24.1348837 13.1852277,23.4961304 11.9111328,22.2186047 C10.6370379,20.9410789 10,19.3969083 10,17.5860465 C10,15.7255721 10.6432227,14.1752 11.9296875,12.9348837 C13.2161523,11.6449548 14.7499911,11 16.53125,11 L22.46875,11 C24.2747486,11 25.8147723,11.6449548 27.0888672,12.9348837 C28.3629621,14.2248127 29,15.7751847 29,17.5860465 Z M24.3613281,25.1395349 C24.9550811,23.9736376 25.7096308,22.8449667 26.625,21.7534884 L26.5878906,21.7534884 C27.7506569,20.5875911 28.3320313,19.1984577 28.3320313,17.5860465 C28.3320313,15.948829 27.7568417,14.5534941 26.6064453,13.4 C25.4560489,12.2465059 24.076831,11.6697674 22.46875,11.6697674 L16.53125,11.6697674 C14.923169,11.6697674 13.5377662,12.2527073 12.375,13.4186047 C11.2369735,14.6093083 10.6679688,15.9984417 10.6679688,17.5860465 C10.6679688,19.1984577 11.2431583,20.5813896 12.3935547,21.7348837 C13.5439511,22.8883779 14.923169,23.4651163 16.53125,23.4651163 L21.6894531,23.4651163 C21.9368502,23.4651163 22.3203099,23.5519371 22.8398438,23.7255814 C23.3593776,23.8992257 23.7675766,24.1720912 24.0644531,24.544186 C24.2128914,24.7922493 24.3118487,24.9906969 24.3613281,25.1395349 Z M14.9726563,16.0604651 C15.417971,16.0604651 15.8014307,16.2217038 16.1230469,16.544186 C16.4446631,16.8666683 16.6054688,17.2511606 16.6054688,17.6976744 C16.6054688,18.1441883 16.4446631,18.5224791 16.1230469,18.8325581 C15.8014307,19.1426372 15.417971,19.2976744 14.9726563,19.2976744 C14.5273415,19.2976744 14.1438818,19.1426372 13.8222656,18.8325581 C13.5006494,18.5224791 13.3398438,18.1441883 13.3398438,17.6976744 C13.3398438,17.2511606 13.5006494,16.8666683 13.8222656,16.544186 C14.1438818,16.2217038 14.5273415,16.0604651 14.9726563,16.0604651 Z M14.9726563,18.627907 C15.244793,18.627907 15.4736319,18.5348847 15.6591797,18.3488372 C15.8447275,18.1627898 15.9375,17.9457377 15.9375,17.6976744 C15.9375,17.4248048 15.8385427,17.1953498 15.640625,17.0093023 C15.4427073,16.8232549 15.2200533,16.7302326 14.9726563,16.7302326 C14.7252592,16.7302326 14.5026052,16.8232549 14.3046875,17.0093023 C14.1067698,17.1953498 14.0078125,17.4248048 14.0078125,17.6976744 C14.0078125,17.9457377 14.100585,18.1627898 14.2861328,18.3488372 C14.4716806,18.5348847 14.7005195,18.627907 14.9726563,18.627907 Z M19.1660156,15.9860465 C19.6113304,15.9860465 19.9947901,16.1472852 20.3164062,16.4697674 C20.6380224,16.7922497 20.7988281,17.176742 20.7988281,17.6232558 C20.7988281,18.0697697 20.6380224,18.454262 20.3164062,18.7767442 C19.9947901,19.0992264 19.6113304,19.2604651 19.1660156,19.2604651 C18.7207009,19.2604651 18.3372412,19.0992264 18.015625,18.7767442 C17.6940088,18.454262 17.5332031,18.0697697 17.5332031,17.6232558 C17.5332031,17.176742 17.6940088,16.7922497 18.015625,16.4697674 C18.3372412,16.1472852 18.7207009,15.9860465 19.1660156,15.9860465 Z M19.1660156,18.5906977 C19.4381524,18.5906977 19.6669913,18.4976753 19.8525391,18.3116279 C20.0380869,18.1255805 20.1308594,17.8961254 20.1308594,17.6232558 C20.1308594,17.3751926 20.0380869,17.1581405 19.8525391,16.972093 C19.6669913,16.7860456 19.4381524,16.6930233 19.1660156,16.6930233 C18.8938788,16.6930233 18.66504,16.7860456 18.4794922,16.972093 C18.2939444,17.1581405 18.2011719,17.3751926 18.2011719,17.6232558 C18.2011719,17.8961254 18.2939444,18.1255805 18.4794922,18.3116279 C18.66504,18.4976753 18.8938788,18.5906977 19.1660156,18.5906977 Z M23.3964844,16.0604651 C23.8417991,16.0604651 24.2252588,16.2217038 24.546875,16.544186 C24.8684912,16.8666683 25.0292969,17.2511606 25.0292969,17.6976744 C25.0292969,18.1441883 24.8684912,18.5224791 24.546875,18.8325581 C24.2252588,19.1426372 23.8417991,19.2976744 23.3964844,19.2976744 C22.9511696,19.2976744 22.5738948,19.1426372 22.2646484,18.8325581 C21.9554021,18.5224791 21.8007813,18.1441883 21.8007813,17.6976744 C21.8007813,17.2511606 21.9554021,16.8666683 22.2646484,16.544186 C22.5738948,16.2217038 22.9511696,16.0604651 23.3964844,16.0604651 Z M23.3964844,18.627907 C23.6686212,18.627907 23.89746,18.5348847 24.0830078,18.3488372 C24.2685556,18.1627898 24.3613281,17.9457377 24.3613281,17.6976744 C24.3613281,17.4248048 24.2685556,17.1953498 24.0830078,17.0093023 C23.89746,16.8232549 23.6686212,16.7302326 23.3964844,16.7302326 C23.1490873,16.7302326 22.9326181,16.8232549 22.7470703,17.0093023 C22.5615225,17.1953498 22.46875,17.4248048 22.46875,17.6976744 C22.46875,17.970544 22.5553377,18.1937976 22.7285156,18.3674419 C22.9016936,18.5410861 23.1243476,18.627907 23.3964844,18.627907 Z" id="6-copy-4" fill="#FFFFFF" transform="translate(19.500000, 19.000000) scale(-1, 1) translate(-19.500000, -19.000000) "></path>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/web-ui/app/images/account-recovery/step_2.svg b/web-ui/app/images/account-recovery/step_2.svg
new file mode 100644
index 00000000..c977aa66
--- /dev/null
+++ b/web-ui/app/images/account-recovery/step_2.svg
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="297px" height="38px" viewBox="0 0 297 38" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
+ <title>Group 5</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Account-Recovery-MVP" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="02-m_Forgot_Password-" transform="translate(-40.000000, -98.000000)">
+ <g id="Group-5" transform="translate(40.000000, 98.000000)">
+ <path d="M25,17.296875 C25,19.4531358 24.2500075,21.328117 22.75,22.921875 C21.4999938,24.3593822 20.3437553,26.1874889 19.28125,28.40625 C19.0312488,28.9062525 18.8281258,29.15625 18.671875,29.15625 C18.6406248,29.15625 18.6093752,29.1406252 18.578125,29.109375 L18.53125,29.109375 C18.3437491,29.0468747 18.2421876,28.9296884 18.2265625,28.7578125 C18.2109374,28.5859366 18.2499995,28.2812522 18.34375,27.84375 C18.4375005,27.3749977 18.3437514,26.9531269 18.0625,26.578125 C17.5624975,25.8906216 16.7656305,25.546875 15.671875,25.546875 L9.25,25.546875 C6.96873859,25.546875 5.02344555,24.7421955 3.4140625,23.1328125 C1.80467945,21.5234295 1,19.5781364 1,17.296875 C1,14.9531133 1.81249188,13.0000078 3.4375,11.4375 C5.06250813,9.81249187 6.99998875,9 9.25,9 L16.75,9 C19.0312614,9 20.9765545,9.81249187 22.5859375,11.4375 C24.1953205,13.0625081 25,15.0156136 25,17.296875 Z M19.140625,26.8125 C19.8906288,25.3437427 20.8437442,23.9218819 22,22.546875 L21.953125,22.546875 C23.4218823,21.0781177 24.15625,19.3281352 24.15625,17.296875 C24.15625,15.2343647 23.4296948,13.4765698 21.9765625,12.0234375 C20.5234302,10.5703052 18.7812602,9.84375 16.75,9.84375 L9.25,9.84375 C7.21873984,9.84375 5.46875734,10.5781177 4,12.046875 C2.56249281,13.5468825 1.84375,15.296865 1.84375,17.296875 C1.84375,19.3281352 2.57030523,21.0703052 4.0234375,22.5234375 C5.47656977,23.9765698 7.21873984,24.703125 9.25,24.703125 L15.765625,24.703125 C16.0781266,24.703125 16.5624967,24.8124989 17.21875,25.03125 C17.8750033,25.2500011 18.3906231,25.5937477 18.765625,26.0625 C18.9531259,26.3750016 19.0781247,26.6249991 19.140625,26.8125 Z M7.28125,15.375 C7.84375281,15.375 8.32812297,15.578123 8.734375,15.984375 C9.14062703,16.390627 9.34375,16.8749972 9.34375,17.4375 C9.34375,18.0000028 9.14062703,18.4765605 8.734375,18.8671875 C8.32812297,19.2578145 7.84375281,19.453125 7.28125,19.453125 C6.71874719,19.453125 6.23437703,19.2578145 5.828125,18.8671875 C5.42187297,18.4765605 5.21875,18.0000028 5.21875,17.4375 C5.21875,16.8749972 5.42187297,16.390627 5.828125,15.984375 C6.23437703,15.578123 6.71874719,15.375 7.28125,15.375 Z M7.28125,18.609375 C7.62500172,18.609375 7.91406133,18.4921887 8.1484375,18.2578125 C8.38281367,18.0234363 8.5,17.7500016 8.5,17.4375 C8.5,17.0937483 8.37500125,16.8046887 8.125,16.5703125 C7.87499875,16.3359363 7.59375156,16.21875 7.28125,16.21875 C6.96874844,16.21875 6.68750125,16.3359363 6.4375,16.5703125 C6.18749875,16.8046887 6.0625,17.0937483 6.0625,17.4375 C6.0625,17.7500016 6.17968633,18.0234363 6.4140625,18.2578125 C6.64843867,18.4921887 6.93749828,18.609375 7.28125,18.609375 Z M12.578125,15.28125 C13.1406278,15.28125 13.624998,15.484373 14.03125,15.890625 C14.437502,16.296877 14.640625,16.7812472 14.640625,17.34375 C14.640625,17.9062528 14.437502,18.390623 14.03125,18.796875 C13.624998,19.203127 13.1406278,19.40625 12.578125,19.40625 C12.0156222,19.40625 11.531252,19.203127 11.125,18.796875 C10.718748,18.390623 10.515625,17.9062528 10.515625,17.34375 C10.515625,16.7812472 10.718748,16.296877 11.125,15.890625 C11.531252,15.484373 12.0156222,15.28125 12.578125,15.28125 Z M12.578125,18.5625 C12.9218767,18.5625 13.2109363,18.4453137 13.4453125,18.2109375 C13.6796887,17.9765613 13.796875,17.6875017 13.796875,17.34375 C13.796875,17.0312484 13.6796887,16.7578137 13.4453125,16.5234375 C13.2109363,16.2890613 12.9218767,16.171875 12.578125,16.171875 C12.2343733,16.171875 11.9453137,16.2890613 11.7109375,16.5234375 C11.4765613,16.7578137 11.359375,17.0312484 11.359375,17.34375 C11.359375,17.6875017 11.4765613,17.9765613 11.7109375,18.2109375 C11.9453137,18.4453137 12.2343733,18.5625 12.578125,18.5625 Z M17.921875,15.375 C18.4843778,15.375 18.968748,15.578123 19.375,15.984375 C19.781252,16.390627 19.984375,16.8749972 19.984375,17.4375 C19.984375,18.0000028 19.781252,18.4765605 19.375,18.8671875 C18.968748,19.2578145 18.4843778,19.453125 17.921875,19.453125 C17.3593722,19.453125 16.8828145,19.2578145 16.4921875,18.8671875 C16.1015605,18.4765605 15.90625,18.0000028 15.90625,17.4375 C15.90625,16.8749972 16.1015605,16.390627 16.4921875,15.984375 C16.8828145,15.578123 17.3593722,15.375 17.921875,15.375 Z M17.921875,18.609375 C18.2656267,18.609375 18.5546863,18.4921887 18.7890625,18.2578125 C19.0234387,18.0234363 19.140625,17.7500016 19.140625,17.4375 C19.140625,17.0937483 19.0234387,16.8046887 18.7890625,16.5703125 C18.5546863,16.3359363 18.2656267,16.21875 17.921875,16.21875 C17.6093734,16.21875 17.3359387,16.3359363 17.1015625,16.5703125 C16.8671863,16.8046887 16.75,17.0937483 16.75,17.4375 C16.75,17.7812517 16.8593739,18.0624989 17.078125,18.28125 C17.2968761,18.5000011 17.5781233,18.609375 17.921875,18.609375 Z" id="6-copy-4" fill="#FFFFFF" transform="translate(13.000000, 19.078125) scale(-1, 1) translate(-13.000000, -19.078125) "></path>
+ <rect id="Rectangle-15-Copy-8" fill="#4EADC3" x="13" y="18" width="100" height="2"></rect>
+ <rect id="Rectangle-15-Copy-5" fill="#D4D4D4" x="114" y="18" width="170" height="2"></rect>
+ <g id="Group-2-Copy" transform="translate(0.000000, 6.000000)">
+ <circle id="Oval-6-Copy-2" fill="#3DABC4" transform="translate(13.000000, 13.000000) rotate(-270.000000) translate(-13.000000, -13.000000) " cx="13" cy="13" r="13"></circle>
+ <path d="M19.5,12.077474 C19.5,13.2454485 19.0937541,14.2610634 18.28125,15.124349 C17.6041633,15.9029987 16.9778675,16.8932232 16.4023437,18.0950521 C16.2669264,18.3658868 16.1569015,18.5013021 16.0722656,18.5013021 C16.0553385,18.5013021 16.0384115,18.4928386 16.0214844,18.4759115 L15.9960937,18.4759115 C15.8945307,18.4420571 15.8395183,18.3785812 15.8310547,18.2854818 C15.8225911,18.1923823 15.8437497,18.0273449 15.8945312,17.7903646 C15.9453128,17.5364571 15.894532,17.3079437 15.7421875,17.1048177 C15.4713528,16.73242 15.0397165,16.546224 14.4472656,16.546224 L10.96875,16.546224 C9.73306674,16.546224 8.67936634,16.1103559 7.80761719,15.2386068 C6.93586804,14.3668576 6.5,13.3131572 6.5,12.077474 C6.5,10.8079364 6.94009977,9.75000423 7.8203125,8.90364583 C8.70052523,8.0234331 9.74999391,7.58333333 10.96875,7.58333333 L15.03125,7.58333333 C16.2669333,7.58333333 17.3206337,8.0234331 18.1923828,8.90364583 C19.064132,9.78385857 19.5,10.8417907 19.5,12.077474 Z M16.3261719,17.2317708 C16.7324239,16.4361939 17.2486948,15.6660193 17.875,14.921224 L17.8496094,14.921224 C18.6451863,14.1256471 19.0429687,13.1777399 19.0429687,12.077474 C19.0429687,10.9602809 18.649418,10.008142 17.8623047,9.22102865 C17.0751914,8.43391534 16.1315159,8.04036458 15.03125,8.04036458 L10.96875,8.04036458 C9.86848408,8.04036458 8.92057689,8.43814706 8.125,9.23372396 C7.34635027,10.046228 6.95703125,10.9941352 6.95703125,12.077474 C6.95703125,13.1777399 7.350582,14.1214153 8.13769531,14.9085286 C8.92480862,15.695642 9.86848408,16.0891927 10.96875,16.0891927 L14.4980469,16.0891927 C14.6673186,16.0891927 14.9296857,16.1484369 15.2851562,16.2669271 C15.6406268,16.3854173 15.9199209,16.5716133 16.1230469,16.8255208 C16.2246099,16.9947925 16.2923175,17.1302078 16.3261719,17.2317708 Z M9.90234375,11.0364583 C10.2070328,11.0364583 10.4693999,11.1464833 10.6894531,11.3665365 C10.9095063,11.5865896 11.0195312,11.8489568 11.0195312,12.1536458 C11.0195312,12.4583349 10.9095063,12.7164703 10.6894531,12.9280599 C10.4693999,13.1396495 10.2070328,13.2454427 9.90234375,13.2454427 C9.59765473,13.2454427 9.33528756,13.1396495 9.11523437,12.9280599 C8.89518119,12.7164703 8.78515625,12.4583349 8.78515625,12.1536458 C8.78515625,11.8489568 8.89518119,11.5865896 9.11523437,11.3665365 C9.33528756,11.1464833 9.59765473,11.0364583 9.90234375,11.0364583 Z M9.90234375,12.7884115 C10.0885426,12.7884115 10.2451166,12.7249355 10.3720703,12.5979818 C10.4990241,12.471028 10.5625,12.3229175 10.5625,12.1536458 C10.5625,11.967447 10.4947923,11.810873 10.359375,11.6839193 C10.2239577,11.5569655 10.0716154,11.4934896 9.90234375,11.4934896 C9.73307207,11.4934896 9.58072984,11.5569655 9.4453125,11.6839193 C9.30989516,11.810873 9.2421875,11.967447 9.2421875,12.1536458 C9.2421875,12.3229175 9.30566343,12.471028 9.43261719,12.5979818 C9.55957095,12.7249355 9.7161449,12.7884115 9.90234375,12.7884115 Z M12.7714844,10.9856771 C13.0761734,10.9856771 13.3385406,11.095702 13.5585937,11.3157552 C13.7786469,11.5358084 13.8886719,11.7981756 13.8886719,12.1028646 C13.8886719,12.4075536 13.7786469,12.6699208 13.5585937,12.889974 C13.3385406,13.1100271 13.0761734,13.2200521 12.7714844,13.2200521 C12.4667954,13.2200521 12.2044282,13.1100271 11.984375,12.889974 C11.7643218,12.6699208 11.6542969,12.4075536 11.6542969,12.1028646 C11.6542969,11.7981756 11.7643218,11.5358084 11.984375,11.3157552 C12.2044282,11.095702 12.4667954,10.9856771 12.7714844,10.9856771 Z M12.7714844,12.7630208 C12.9576832,12.7630208 13.1142572,12.6995449 13.2412109,12.5725911 C13.3681647,12.4456374 13.4316406,12.2890634 13.4316406,12.1028646 C13.4316406,11.9335929 13.3681647,11.7854824 13.2412109,11.6585286 C13.1142572,11.5315749 12.9576832,11.468099 12.7714844,11.468099 C12.5852855,11.468099 12.4287116,11.5315749 12.3017578,11.6585286 C12.1748041,11.7854824 12.1113281,11.9335929 12.1113281,12.1028646 C12.1113281,12.2890634 12.1748041,12.4456374 12.3017578,12.5725911 C12.4287116,12.6995449 12.5852855,12.7630208 12.7714844,12.7630208 Z M15.6660156,11.0364583 C15.9707046,11.0364583 16.2330718,11.1464833 16.453125,11.3665365 C16.6731782,11.5865896 16.7832031,11.8489568 16.7832031,12.1536458 C16.7832031,12.4583349 16.6731782,12.7164703 16.453125,12.9280599 C16.2330718,13.1396495 15.9707046,13.2454427 15.6660156,13.2454427 C15.3613266,13.2454427 15.1031912,13.1396495 14.8916016,12.9280599 C14.680012,12.7164703 14.5742187,12.4583349 14.5742187,12.1536458 C14.5742187,11.8489568 14.680012,11.5865896 14.8916016,11.3665365 C15.1031912,11.1464833 15.3613266,11.0364583 15.6660156,11.0364583 Z M15.6660156,12.7884115 C15.8522145,12.7884115 16.0087884,12.7249355 16.1357422,12.5979818 C16.2626959,12.471028 16.3261719,12.3229175 16.3261719,12.1536458 C16.3261719,11.967447 16.2626959,11.810873 16.1357422,11.6839193 C16.0087884,11.5569655 15.8522145,11.4934896 15.6660156,11.4934896 C15.4967439,11.4934896 15.3486334,11.5569655 15.2216797,11.6839193 C15.0947259,11.810873 15.03125,11.967447 15.03125,12.1536458 C15.03125,12.3398447 15.0904942,12.4921869 15.2089844,12.6106771 C15.3274746,12.7291673 15.4798168,12.7884115 15.6660156,12.7884115 Z" id="6-copy-3" fill="#FFFFFF" transform="translate(13.000000, 13.042318) scale(-1, 1) translate(-13.000000, -13.042318) "></path>
+ </g>
+ <g id="Group-2" transform="translate(86.333333, 0.000000)">
+ <ellipse id="Oval-6-Copy-4" fill="#4EADC3" transform="translate(19.000000, 19.000000) rotate(-270.000000) translate(-19.000000, -19.000000) " cx="19" cy="19" rx="19" ry="19"></ellipse>
+ <g id="Group-Copy-2" transform="translate(8.708333, 11.875000)" fill="#FFFFFF">
+ <path d="M18.2329051,10.6217718 C18.4741645,10.7563646 18.5139175,10.9568804 18.3535348,11.2246927 C18.2726581,11.3592855 18.1520284,11.4265819 17.9916457,11.4265819 C17.9381848,11.4265819 17.8573081,11.399114 17.7503863,11.3455516 L13.8957191,8.77043366 C13.6544597,8.63721423 13.6147067,8.43532498 13.7750894,8.16751272 C13.909427,7.92579499 14.1095626,7.88596649 14.3768671,8.04665385 L18.2329051,10.6217718 Z M10.2809404,9.33489951 L2.77174154,4.46620991 C2.53048214,4.30414916 2.49072917,4.11736727 2.65111184,3.90174406 C2.81149451,3.6339318 2.99792223,3.58036935 3.21313658,3.74105671 L10.2809404,8.32820008 L17.3487443,3.74105671 C17.5639586,3.58036935 17.7503863,3.6339318 17.9121398,3.90174406 C18.0725225,4.11736727 18.0327695,4.30414916 17.7915101,4.46620991 L10.2809404,9.33489951 Z M6.78679147,8.16751272 C6.94854493,8.43532498 6.90742117,8.63721423 6.66616177,8.77043366 L2.81149451,11.3455516 C2.70457273,11.399114 2.623696,11.4265819 2.57023511,11.4265819 C2.40985244,11.4265819 2.28922273,11.3592855 2.20971679,11.2246927 C2.04796333,10.9568804 2.08908709,10.7563646 2.3289757,10.6217718 L6.18501376,8.04665385 C6.45231821,7.88596649 6.65245385,7.92579499 6.78679147,8.16751272 L6.78679147,8.16751272 Z M19.7188437,1.97074898 C19.7188437,1.72903124 19.6324838,1.5285154 19.4583932,1.36782804 C19.2829319,1.20714068 19.0759423,1.1261103 18.8346829,1.1261103 L1.72719799,1.1261103 C1.48593859,1.1261103 1.27894899,1.20714068 1.1048584,1.36782804 C0.929397014,1.5285154 0.843037115,1.72903124 0.843037115,1.97074898 L0.843037115,12.2712206 C0.843037115,12.5129384 0.929397014,12.7134542 1.1048584,12.875515 C1.27894899,13.0362023 1.48593859,13.1158593 1.72719799,13.1158593 L18.8346829,13.1158593 C19.0759423,13.1158593 19.2829319,13.0362023 19.4583932,12.875515 C19.6324838,12.7134542 19.7188437,12.5129384 19.7188437,12.2712206 L19.7188437,1.97074898 Z M20.060171,0.744306156 C20.3946442,1.07941483 20.5618808,1.48868691 20.5618808,1.97074898 L20.5618808,12.2712206 C20.5618808,12.7546561 20.3946442,13.1639282 20.060171,13.4990368 C19.7243269,13.8341455 19.3172017,14.0016999 18.8346829,14.0016999 L1.72719799,14.0016999 C1.24467919,14.0016999 0.837553946,13.8341455 0.501709893,13.4990368 C0.167236631,13.1639282 0,12.7546561 0,12.2712206 L0,1.97074898 C0,1.48868691 0.167236631,1.07941483 0.501709893,0.744306156 C0.837553946,0.407824082 1.24467919,0.240269743 1.72719799,0.240269743 L18.8346829,0.240269743 C19.3172017,0.240269743 19.7243269,0.407824082 20.060171,0.744306156 L20.060171,0.744306156 Z" id="Fill-1"></path>
+ </g>
+ </g>
+ <g id="Group-6-Copy-2" transform="translate(184.666667, 6.000000)">
+ <g id="Group-4" fill="#D4D4D4">
+ <ellipse id="Oval-6-Copy-3" transform="translate(13.000000, 13.000000) rotate(-270.000000) translate(-13.000000, -13.000000) " cx="13" cy="13" rx="13" ry="13"></ellipse>
+ </g>
+ <g id="Group-5-Copy-3" transform="translate(3.791667, 9.208333)" fill="#FFFFFF">
+ <g id="Group-15" transform="translate(0.000000, 0.044106)" fill-rule="nonzero">
+ <path d="M0.541666667,0.908390422 L0.541666667,7.21660958 C0.541666667,7.42046561 0.706084452,7.58333333 0.914399823,7.58333333 L18.0439335,7.58333333 C18.2532036,7.58333333 18.4166667,7.42037231 18.4166667,7.21660958 L18.4166667,0.908390422 C18.4166667,0.704534391 18.2522489,0.541666667 18.0439335,0.541666667 L0.914399823,0.541666667 C0.705129753,0.541666667 0.541666667,0.704627685 0.541666667,0.908390422 Z M0,0.908390422 C0,0.406700245 0.404742514,0 0.914399823,0 L18.0439335,0 C18.5489426,0 18.9583333,0.402920013 18.9583333,0.908390422 L18.9583333,7.21660958 C18.9583333,7.71829975 18.5535908,8.125 18.0439335,8.125 L0.914399823,8.125 C0.409390745,8.125 0,7.72207999 0,7.21660958 L0,0.908390422 Z" id="Rectangle-10"></path>
+ </g>
+ <ellipse id="Oval-5" cx="4.17600458" cy="4.03384483" rx="1.12325506" ry="1.10284184"></ellipse>
+ <ellipse id="Oval-5-Copy" cx="7.77042077" cy="4.03384483" rx="1.12325506" ry="1.10284184"></ellipse>
+ <ellipse id="Oval-5-Copy-2" cx="11.364837" cy="4.03384483" rx="1.12325506" ry="1.10284184"></ellipse>
+ <ellipse id="Oval-5-Copy-3" cx="14.9592531" cy="4.03384483" rx="1.12325506" ry="1.10284184"></ellipse>
+ </g>
+ </g>
+ <g id="Group-7-Copy-3" transform="translate(271.000000, 6.000000)">
+ <ellipse id="Oval-6-Copy-5" fill="#D4D4D4" transform="translate(13.000000, 13.000000) rotate(-270.000000) translate(-13.000000, -13.000000) " cx="13" cy="13" rx="13" ry="13"></ellipse>
+ <g id="Group-29" transform="translate(2.166667, 8.666667)" fill="#FFFFFF">
+ <g id="Group" transform="translate(3.655371, 4.373733) rotate(-270.000000) translate(-3.655371, -4.373733) translate(0.134537, 0.852900)" fill-rule="nonzero">
+ <path d="M3.55327323,6.86597405 L3.55327323,1.1921229 C3.55327323,1.04254578 3.43201702,0.921289568 3.2824399,0.921289568 C3.13286278,0.921289568 3.01160657,1.04254578 3.01160657,1.1921229 L3.01160657,6.86597405 C3.01160657,7.01555117 3.13286278,7.13680739 3.2824399,7.13680739 C3.43201702,7.13680739 3.55327323,7.01555117 3.55327323,6.86597405 Z" id="Line-Copy-13"></path>
+ <path d="M0.637125643,3.53911437 L0.637125643,0.256948898 C0.637125643,0.107371779 0.515869429,-0.0138844349 0.36629231,-0.0138844349 C0.21671519,-0.0138844349 0.0954589763,0.107371779 0.0954589763,0.256948898 L0.0954589763,3.53911437 C0.0954589763,3.68869149 0.21671519,3.8099477 0.36629231,3.8099477 C0.515869429,3.8099477 0.637125643,3.68869149 0.637125643,3.53911437 Z" id="Line-Copy-12"></path>
+ <path d="M6.59620985,3.53911437 L6.59620985,0.256948898 C6.59620985,0.107371779 6.47495364,-0.0138844349 6.32537652,-0.0138844349 C6.1757994,-0.0138844349 6.05454319,0.107371779 6.05454319,0.256948898 L6.05454319,3.53911437 C6.05454319,3.68869149 6.1757994,3.8099477 6.32537652,3.8099477 C6.47495364,3.8099477 6.59620985,3.68869149 6.59620985,3.53911437 Z" id="Line-Copy-14"></path>
+ </g>
+ <g id="Group-17" transform="translate(7.286933, 0.155729)">
+ <path d="M10.8195499,6.03227517 C10.9593721,6.11020439 10.982411,6.22630303 10.889461,6.38136628 C10.8425888,6.4592955 10.7726777,6.49826011 10.6797277,6.49826011 C10.6487444,6.49826011 10.6018721,6.48235619 10.5399055,6.45134354 L8.3059277,4.96035076 C8.16610547,4.88321673 8.14306658,4.7663229 8.23601658,4.61125965 C8.31387214,4.47130513 8.42986103,4.44824444 8.5847777,4.54128239 L10.8195499,6.03227517 Z M6.2109777,5.28717638 L1.85901103,2.46820603 C1.71918881,2.37437288 1.69614992,2.26622621 1.78909992,2.14138041 C1.88204992,1.98631716 1.99009436,1.95530451 2.11482214,2.04834246 L6.2109777,4.7042976 L10.3071333,2.04834246 C10.431861,1.95530451 10.5399055,1.98631716 10.6336499,2.14138041 C10.7265999,2.26622621 10.703561,2.37437288 10.5637388,2.46820603 M4.18593881,4.61125965 C4.27968325,4.7663229 4.25584992,4.88321673 4.1160277,4.96035076 L1.88204992,6.45134354 C1.82008325,6.48235619 1.77321103,6.49826011 1.7422277,6.49826011 C1.6492777,6.49826011 1.57936658,6.4592955 1.53328881,6.38136628 C1.43954436,6.22630303 1.4633777,6.11020439 1.60240547,6.03227517 L3.8371777,4.54128239 C3.99209436,4.44824444 4.10808325,4.47130513 4.18593881,4.61125965 L4.18593881,4.61125965 Z M11.6807277,1.02333463 C11.6807277,0.883380105 11.6306777,0.767281468 11.5297833,0.674243518 C11.4280944,0.581205569 11.3081333,0.534288996 11.168311,0.534288996 L1.25364436,0.534288996 C1.11382214,0.534288996 0.993861029,0.581205569 0.892966585,0.674243518 C0.791277696,0.767281468 0.741227696,0.883380105 0.741227696,1.02333463 L0.741227696,6.98730574 C0.741227696,7.12726026 0.791277696,7.2433589 0.892966585,7.33719205 C0.993861029,7.43023 1.11382214,7.47635137 1.25364436,7.47635137 L11.168311,7.47635137 C11.3081333,7.47635137 11.4280944,7.43023 11.5297833,7.33719205 C11.6306777,7.2433589 11.6807277,7.12726026 11.6807277,6.98730574 L11.6807277,1.02333463 Z M11.8785444,0.313224467 C12.0723888,0.507252327 12.169311,0.744220779 12.169311,1.02333463 L12.169311,6.98730574 C12.169311,7.26721479 12.0723888,7.50418324 11.8785444,7.6982111 C11.6839055,7.89223896 11.4479555,7.98925289 11.168311,7.98925289 L1.25364436,7.98925289 C0.973999918,7.98925289 0.738049918,7.89223896 0.543411029,7.6982111 C0.349566585,7.50418324 0.252644362,7.26721479 0.252644362,6.98730574 L0.252644362,1.02333463 C0.252644362,0.744220779 0.349566585,0.507252327 0.543411029,0.313224467 C0.738049918,0.118401411 0.973999918,0.0213874804 1.25364436,0.0213874804 L11.168311,0.0213874804 C11.4479555,0.0213874804 11.6839055,0.118401411 11.8785444,0.313224467" id="Fill-1"></path>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/web-ui/app/images/account-recovery/step_3.svg b/web-ui/app/images/account-recovery/step_3.svg
new file mode 100644
index 00000000..80bbefdc
--- /dev/null
+++ b/web-ui/app/images/account-recovery/step_3.svg
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="297px" height="38px" viewBox="0 0 297 38" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
+ <title>Group 2</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Account-Recovery-MVP" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="04-m_Forgot_Password-" transform="translate(-37.000000, -101.000000)">
+ <g id="Group-2" transform="translate(37.000000, 101.000000)">
+ <path d="M25,17.296875 C25,19.4531358 24.2500075,21.328117 22.75,22.921875 C21.4999938,24.3593822 20.3437553,26.1874889 19.28125,28.40625 C19.0312488,28.9062525 18.8281258,29.15625 18.671875,29.15625 C18.6406248,29.15625 18.6093752,29.1406252 18.578125,29.109375 L18.53125,29.109375 C18.3437491,29.0468747 18.2421876,28.9296884 18.2265625,28.7578125 C18.2109374,28.5859366 18.2499995,28.2812522 18.34375,27.84375 C18.4375005,27.3749977 18.3437514,26.9531269 18.0625,26.578125 C17.5624975,25.8906216 16.7656305,25.546875 15.671875,25.546875 L9.25,25.546875 C6.96873859,25.546875 5.02344555,24.7421955 3.4140625,23.1328125 C1.80467945,21.5234295 1,19.5781364 1,17.296875 C1,14.9531133 1.81249188,13.0000078 3.4375,11.4375 C5.06250813,9.81249187 6.99998875,9 9.25,9 L16.75,9 C19.0312614,9 20.9765545,9.81249187 22.5859375,11.4375 C24.1953205,13.0625081 25,15.0156136 25,17.296875 Z M19.140625,26.8125 C19.8906288,25.3437427 20.8437442,23.9218819 22,22.546875 L21.953125,22.546875 C23.4218823,21.0781177 24.15625,19.3281352 24.15625,17.296875 C24.15625,15.2343647 23.4296948,13.4765698 21.9765625,12.0234375 C20.5234302,10.5703052 18.7812602,9.84375 16.75,9.84375 L9.25,9.84375 C7.21873984,9.84375 5.46875734,10.5781177 4,12.046875 C2.56249281,13.5468825 1.84375,15.296865 1.84375,17.296875 C1.84375,19.3281352 2.57030523,21.0703052 4.0234375,22.5234375 C5.47656977,23.9765698 7.21873984,24.703125 9.25,24.703125 L15.765625,24.703125 C16.0781266,24.703125 16.5624967,24.8124989 17.21875,25.03125 C17.8750033,25.2500011 18.3906231,25.5937477 18.765625,26.0625 C18.9531259,26.3750016 19.0781247,26.6249991 19.140625,26.8125 Z M7.28125,15.375 C7.84375281,15.375 8.32812297,15.578123 8.734375,15.984375 C9.14062703,16.390627 9.34375,16.8749972 9.34375,17.4375 C9.34375,18.0000028 9.14062703,18.4765605 8.734375,18.8671875 C8.32812297,19.2578145 7.84375281,19.453125 7.28125,19.453125 C6.71874719,19.453125 6.23437703,19.2578145 5.828125,18.8671875 C5.42187297,18.4765605 5.21875,18.0000028 5.21875,17.4375 C5.21875,16.8749972 5.42187297,16.390627 5.828125,15.984375 C6.23437703,15.578123 6.71874719,15.375 7.28125,15.375 Z M7.28125,18.609375 C7.62500172,18.609375 7.91406133,18.4921887 8.1484375,18.2578125 C8.38281367,18.0234363 8.5,17.7500016 8.5,17.4375 C8.5,17.0937483 8.37500125,16.8046887 8.125,16.5703125 C7.87499875,16.3359363 7.59375156,16.21875 7.28125,16.21875 C6.96874844,16.21875 6.68750125,16.3359363 6.4375,16.5703125 C6.18749875,16.8046887 6.0625,17.0937483 6.0625,17.4375 C6.0625,17.7500016 6.17968633,18.0234363 6.4140625,18.2578125 C6.64843867,18.4921887 6.93749828,18.609375 7.28125,18.609375 Z M12.578125,15.28125 C13.1406278,15.28125 13.624998,15.484373 14.03125,15.890625 C14.437502,16.296877 14.640625,16.7812472 14.640625,17.34375 C14.640625,17.9062528 14.437502,18.390623 14.03125,18.796875 C13.624998,19.203127 13.1406278,19.40625 12.578125,19.40625 C12.0156222,19.40625 11.531252,19.203127 11.125,18.796875 C10.718748,18.390623 10.515625,17.9062528 10.515625,17.34375 C10.515625,16.7812472 10.718748,16.296877 11.125,15.890625 C11.531252,15.484373 12.0156222,15.28125 12.578125,15.28125 Z M12.578125,18.5625 C12.9218767,18.5625 13.2109363,18.4453137 13.4453125,18.2109375 C13.6796887,17.9765613 13.796875,17.6875017 13.796875,17.34375 C13.796875,17.0312484 13.6796887,16.7578137 13.4453125,16.5234375 C13.2109363,16.2890613 12.9218767,16.171875 12.578125,16.171875 C12.2343733,16.171875 11.9453137,16.2890613 11.7109375,16.5234375 C11.4765613,16.7578137 11.359375,17.0312484 11.359375,17.34375 C11.359375,17.6875017 11.4765613,17.9765613 11.7109375,18.2109375 C11.9453137,18.4453137 12.2343733,18.5625 12.578125,18.5625 Z M17.921875,15.375 C18.4843778,15.375 18.968748,15.578123 19.375,15.984375 C19.781252,16.390627 19.984375,16.8749972 19.984375,17.4375 C19.984375,18.0000028 19.781252,18.4765605 19.375,18.8671875 C18.968748,19.2578145 18.4843778,19.453125 17.921875,19.453125 C17.3593722,19.453125 16.8828145,19.2578145 16.4921875,18.8671875 C16.1015605,18.4765605 15.90625,18.0000028 15.90625,17.4375 C15.90625,16.8749972 16.1015605,16.390627 16.4921875,15.984375 C16.8828145,15.578123 17.3593722,15.375 17.921875,15.375 Z M17.921875,18.609375 C18.2656267,18.609375 18.5546863,18.4921887 18.7890625,18.2578125 C19.0234387,18.0234363 19.140625,17.7500016 19.140625,17.4375 C19.140625,17.0937483 19.0234387,16.8046887 18.7890625,16.5703125 C18.5546863,16.3359363 18.2656267,16.21875 17.921875,16.21875 C17.6093734,16.21875 17.3359387,16.3359363 17.1015625,16.5703125 C16.8671863,16.8046887 16.75,17.0937483 16.75,17.4375 C16.75,17.7812517 16.8593739,18.0624989 17.078125,18.28125 C17.2968761,18.5000011 17.5781233,18.609375 17.921875,18.609375 Z" id="6-copy-5" fill="#FFFFFF" transform="translate(13.000000, 19.078125) scale(-1, 1) translate(-13.000000, -19.078125) "></path>
+ <rect id="Rectangle-15-Copy-9" fill="#4EADC3" x="3" y="18" width="200" height="2"></rect>
+ <rect id="Rectangle-15-Copy-10" fill="#D4D4D4" x="198" y="18" width="80" height="2"></rect>
+ <g id="Group-2-Copy-2" transform="translate(0.000000, 6.000000)">
+ <circle id="Oval-6-Copy-2" fill="#4EADC3" transform="translate(13.000000, 13.000000) rotate(-270.000000) translate(-13.000000, -13.000000) " cx="13" cy="13" r="13"></circle>
+ <path d="M19.5,12.077474 C19.5,13.2454485 19.0937541,14.2610634 18.28125,15.124349 C17.6041633,15.9029987 16.9778675,16.8932232 16.4023437,18.0950521 C16.2669264,18.3658868 16.1569015,18.5013021 16.0722656,18.5013021 C16.0553385,18.5013021 16.0384115,18.4928386 16.0214844,18.4759115 L15.9960937,18.4759115 C15.8945307,18.4420571 15.8395183,18.3785812 15.8310547,18.2854818 C15.8225911,18.1923823 15.8437497,18.0273449 15.8945312,17.7903646 C15.9453128,17.5364571 15.894532,17.3079437 15.7421875,17.1048177 C15.4713528,16.73242 15.0397165,16.546224 14.4472656,16.546224 L10.96875,16.546224 C9.73306674,16.546224 8.67936634,16.1103559 7.80761719,15.2386068 C6.93586804,14.3668576 6.5,13.3131572 6.5,12.077474 C6.5,10.8079364 6.94009977,9.75000423 7.8203125,8.90364583 C8.70052523,8.0234331 9.74999391,7.58333333 10.96875,7.58333333 L15.03125,7.58333333 C16.2669333,7.58333333 17.3206337,8.0234331 18.1923828,8.90364583 C19.064132,9.78385857 19.5,10.8417907 19.5,12.077474 Z M16.3261719,17.2317708 C16.7324239,16.4361939 17.2486948,15.6660193 17.875,14.921224 L17.8496094,14.921224 C18.6451863,14.1256471 19.0429687,13.1777399 19.0429687,12.077474 C19.0429687,10.9602809 18.649418,10.008142 17.8623047,9.22102865 C17.0751914,8.43391534 16.1315159,8.04036458 15.03125,8.04036458 L10.96875,8.04036458 C9.86848408,8.04036458 8.92057689,8.43814706 8.125,9.23372396 C7.34635027,10.046228 6.95703125,10.9941352 6.95703125,12.077474 C6.95703125,13.1777399 7.350582,14.1214153 8.13769531,14.9085286 C8.92480862,15.695642 9.86848408,16.0891927 10.96875,16.0891927 L14.4980469,16.0891927 C14.6673186,16.0891927 14.9296857,16.1484369 15.2851562,16.2669271 C15.6406268,16.3854173 15.9199209,16.5716133 16.1230469,16.8255208 C16.2246099,16.9947925 16.2923175,17.1302078 16.3261719,17.2317708 Z M9.90234375,11.0364583 C10.2070328,11.0364583 10.4693999,11.1464833 10.6894531,11.3665365 C10.9095063,11.5865896 11.0195312,11.8489568 11.0195312,12.1536458 C11.0195312,12.4583349 10.9095063,12.7164703 10.6894531,12.9280599 C10.4693999,13.1396495 10.2070328,13.2454427 9.90234375,13.2454427 C9.59765473,13.2454427 9.33528756,13.1396495 9.11523437,12.9280599 C8.89518119,12.7164703 8.78515625,12.4583349 8.78515625,12.1536458 C8.78515625,11.8489568 8.89518119,11.5865896 9.11523437,11.3665365 C9.33528756,11.1464833 9.59765473,11.0364583 9.90234375,11.0364583 Z M9.90234375,12.7884115 C10.0885426,12.7884115 10.2451166,12.7249355 10.3720703,12.5979818 C10.4990241,12.471028 10.5625,12.3229175 10.5625,12.1536458 C10.5625,11.967447 10.4947923,11.810873 10.359375,11.6839193 C10.2239577,11.5569655 10.0716154,11.4934896 9.90234375,11.4934896 C9.73307207,11.4934896 9.58072984,11.5569655 9.4453125,11.6839193 C9.30989516,11.810873 9.2421875,11.967447 9.2421875,12.1536458 C9.2421875,12.3229175 9.30566343,12.471028 9.43261719,12.5979818 C9.55957095,12.7249355 9.7161449,12.7884115 9.90234375,12.7884115 Z M12.7714844,10.9856771 C13.0761734,10.9856771 13.3385406,11.095702 13.5585937,11.3157552 C13.7786469,11.5358084 13.8886719,11.7981756 13.8886719,12.1028646 C13.8886719,12.4075536 13.7786469,12.6699208 13.5585937,12.889974 C13.3385406,13.1100271 13.0761734,13.2200521 12.7714844,13.2200521 C12.4667954,13.2200521 12.2044282,13.1100271 11.984375,12.889974 C11.7643218,12.6699208 11.6542969,12.4075536 11.6542969,12.1028646 C11.6542969,11.7981756 11.7643218,11.5358084 11.984375,11.3157552 C12.2044282,11.095702 12.4667954,10.9856771 12.7714844,10.9856771 Z M12.7714844,12.7630208 C12.9576832,12.7630208 13.1142572,12.6995449 13.2412109,12.5725911 C13.3681647,12.4456374 13.4316406,12.2890634 13.4316406,12.1028646 C13.4316406,11.9335929 13.3681647,11.7854824 13.2412109,11.6585286 C13.1142572,11.5315749 12.9576832,11.468099 12.7714844,11.468099 C12.5852855,11.468099 12.4287116,11.5315749 12.3017578,11.6585286 C12.1748041,11.7854824 12.1113281,11.9335929 12.1113281,12.1028646 C12.1113281,12.2890634 12.1748041,12.4456374 12.3017578,12.5725911 C12.4287116,12.6995449 12.5852855,12.7630208 12.7714844,12.7630208 Z M15.6660156,11.0364583 C15.9707046,11.0364583 16.2330718,11.1464833 16.453125,11.3665365 C16.6731782,11.5865896 16.7832031,11.8489568 16.7832031,12.1536458 C16.7832031,12.4583349 16.6731782,12.7164703 16.453125,12.9280599 C16.2330718,13.1396495 15.9707046,13.2454427 15.6660156,13.2454427 C15.3613266,13.2454427 15.1031912,13.1396495 14.8916016,12.9280599 C14.680012,12.7164703 14.5742187,12.4583349 14.5742187,12.1536458 C14.5742187,11.8489568 14.680012,11.5865896 14.8916016,11.3665365 C15.1031912,11.1464833 15.3613266,11.0364583 15.6660156,11.0364583 Z M15.6660156,12.7884115 C15.8522145,12.7884115 16.0087884,12.7249355 16.1357422,12.5979818 C16.2626959,12.471028 16.3261719,12.3229175 16.3261719,12.1536458 C16.3261719,11.967447 16.2626959,11.810873 16.1357422,11.6839193 C16.0087884,11.5569655 15.8522145,11.4934896 15.6660156,11.4934896 C15.4967439,11.4934896 15.3486334,11.5569655 15.2216797,11.6839193 C15.0947259,11.810873 15.03125,11.967447 15.03125,12.1536458 C15.03125,12.3398447 15.0904942,12.4921869 15.2089844,12.6106771 C15.3274746,12.7291673 15.4798168,12.7884115 15.6660156,12.7884115 Z" id="6-copy-3" fill="#FFFFFF" transform="translate(13.000000, 13.042318) scale(-1, 1) translate(-13.000000, -13.042318) "></path>
+ </g>
+ <g id="Group-7-Copy-4" transform="translate(271.000000, 6.000000)">
+ <ellipse id="Oval-6-Copy-5" fill="#D4D4D4" transform="translate(13.000000, 13.000000) rotate(-270.000000) translate(-13.000000, -13.000000) " cx="13" cy="13" rx="13" ry="13"></ellipse>
+ <g id="Group-29" transform="translate(2.166667, 8.666667)" fill="#FFFFFF">
+ <g id="Group" transform="translate(3.655371, 4.373733) rotate(-270.000000) translate(-3.655371, -4.373733) translate(0.134537, 0.852900)" fill-rule="nonzero">
+ <path d="M3.55327323,6.86597405 L3.55327323,1.1921229 C3.55327323,1.04254578 3.43201702,0.921289568 3.2824399,0.921289568 C3.13286278,0.921289568 3.01160657,1.04254578 3.01160657,1.1921229 L3.01160657,6.86597405 C3.01160657,7.01555117 3.13286278,7.13680739 3.2824399,7.13680739 C3.43201702,7.13680739 3.55327323,7.01555117 3.55327323,6.86597405 Z" id="Line-Copy-13"></path>
+ <path d="M0.637125643,3.53911437 L0.637125643,0.256948898 C0.637125643,0.107371779 0.515869429,-0.0138844349 0.36629231,-0.0138844349 C0.21671519,-0.0138844349 0.0954589763,0.107371779 0.0954589763,0.256948898 L0.0954589763,3.53911437 C0.0954589763,3.68869149 0.21671519,3.8099477 0.36629231,3.8099477 C0.515869429,3.8099477 0.637125643,3.68869149 0.637125643,3.53911437 Z" id="Line-Copy-12"></path>
+ <path d="M6.59620985,3.53911437 L6.59620985,0.256948898 C6.59620985,0.107371779 6.47495364,-0.0138844349 6.32537652,-0.0138844349 C6.1757994,-0.0138844349 6.05454319,0.107371779 6.05454319,0.256948898 L6.05454319,3.53911437 C6.05454319,3.68869149 6.1757994,3.8099477 6.32537652,3.8099477 C6.47495364,3.8099477 6.59620985,3.68869149 6.59620985,3.53911437 Z" id="Line-Copy-14"></path>
+ </g>
+ <g id="Group-17" transform="translate(7.286933, 0.155729)">
+ <path d="M10.8195499,6.03227517 C10.9593721,6.11020439 10.982411,6.22630303 10.889461,6.38136628 C10.8425888,6.4592955 10.7726777,6.49826011 10.6797277,6.49826011 C10.6487444,6.49826011 10.6018721,6.48235619 10.5399055,6.45134354 L8.3059277,4.96035076 C8.16610547,4.88321673 8.14306658,4.7663229 8.23601658,4.61125965 C8.31387214,4.47130513 8.42986103,4.44824444 8.5847777,4.54128239 L10.8195499,6.03227517 Z M6.2109777,5.28717638 L1.85901103,2.46820603 C1.71918881,2.37437288 1.69614992,2.26622621 1.78909992,2.14138041 C1.88204992,1.98631716 1.99009436,1.95530451 2.11482214,2.04834246 L6.2109777,4.7042976 L10.3071333,2.04834246 C10.431861,1.95530451 10.5399055,1.98631716 10.6336499,2.14138041 C10.7265999,2.26622621 10.703561,2.37437288 10.5637388,2.46820603 M4.18593881,4.61125965 C4.27968325,4.7663229 4.25584992,4.88321673 4.1160277,4.96035076 L1.88204992,6.45134354 C1.82008325,6.48235619 1.77321103,6.49826011 1.7422277,6.49826011 C1.6492777,6.49826011 1.57936658,6.4592955 1.53328881,6.38136628 C1.43954436,6.22630303 1.4633777,6.11020439 1.60240547,6.03227517 L3.8371777,4.54128239 C3.99209436,4.44824444 4.10808325,4.47130513 4.18593881,4.61125965 L4.18593881,4.61125965 Z M11.6807277,1.02333463 C11.6807277,0.883380105 11.6306777,0.767281468 11.5297833,0.674243518 C11.4280944,0.581205569 11.3081333,0.534288996 11.168311,0.534288996 L1.25364436,0.534288996 C1.11382214,0.534288996 0.993861029,0.581205569 0.892966585,0.674243518 C0.791277696,0.767281468 0.741227696,0.883380105 0.741227696,1.02333463 L0.741227696,6.98730574 C0.741227696,7.12726026 0.791277696,7.2433589 0.892966585,7.33719205 C0.993861029,7.43023 1.11382214,7.47635137 1.25364436,7.47635137 L11.168311,7.47635137 C11.3081333,7.47635137 11.4280944,7.43023 11.5297833,7.33719205 C11.6306777,7.2433589 11.6807277,7.12726026 11.6807277,6.98730574 L11.6807277,1.02333463 Z M11.8785444,0.313224467 C12.0723888,0.507252327 12.169311,0.744220779 12.169311,1.02333463 L12.169311,6.98730574 C12.169311,7.26721479 12.0723888,7.50418324 11.8785444,7.6982111 C11.6839055,7.89223896 11.4479555,7.98925289 11.168311,7.98925289 L1.25364436,7.98925289 C0.973999918,7.98925289 0.738049918,7.89223896 0.543411029,7.6982111 C0.349566585,7.50418324 0.252644362,7.26721479 0.252644362,6.98730574 L0.252644362,1.02333463 C0.252644362,0.744220779 0.349566585,0.507252327 0.543411029,0.313224467 C0.738049918,0.118401411 0.973999918,0.0213874804 1.25364436,0.0213874804 L11.168311,0.0213874804 C11.4479555,0.0213874804 11.6839055,0.118401411 11.8785444,0.313224467" id="Fill-1"></path>
+ </g>
+ </g>
+ </g>
+ <g id="Group-2-Copy-4" transform="translate(86.333333, 7.000000)">
+ <ellipse id="Oval-6-Copy-4" fill="#3DABC4" transform="translate(13.000000, 13.000000) rotate(-270.000000) translate(-13.000000, -13.000000) " cx="13" cy="13" rx="13" ry="13"></ellipse>
+ <g id="Group-Copy-2" transform="translate(5.958333, 8.125000)" fill="#FFFFFF">
+ <path d="M12.4751456,7.26752805 C12.6402178,7.35961788 12.6674172,7.49681293 12.5576817,7.6800529 C12.502345,7.77214273 12.4198089,7.81818765 12.3100734,7.81818765 C12.2734949,7.81818765 12.2181582,7.79939381 12.1450012,7.76274581 L9.50759726,6.00082303 C9.34252504,5.90967289 9.31532564,5.77153815 9.42506115,5.58829818 C9.51697637,5.42291236 9.65391128,5.39566129 9.8368038,5.50560527 L12.4751456,7.26752805 Z M7.03432766,6.3870365 L1.89645474,3.05582783 C1.73138251,2.94494416 1.70418311,2.81714603 1.81391863,2.66961436 C1.92365414,2.48637439 2.05120994,2.4497264 2.19846187,2.55967038 L7.03432766,5.69824216 L11.8701934,2.55967038 C12.0174454,2.4497264 12.1450012,2.48637439 12.2556746,2.66961436 C12.3654101,2.81714603 12.3382107,2.94494416 12.1731385,3.05582783 L7.03432766,6.3870365 Z M4.64359416,5.58829818 C4.75426759,5.77153815 4.72613028,5.90967289 4.56105805,6.00082303 L1.92365414,7.76274581 C1.85049713,7.79939381 1.79516042,7.81818765 1.75858191,7.81818765 C1.6488464,7.81818765 1.56631029,7.77214273 1.51191149,7.6800529 C1.40123807,7.49681293 1.42937538,7.35961788 1.59350969,7.26752805 L4.23185152,5.50560527 C4.41474404,5.39566129 4.55167895,5.42291236 4.64359416,5.58829818 L4.64359416,5.58829818 Z M13.4918404,1.3484072 C13.4918404,1.18302138 13.4327521,1.04582632 13.3136375,0.935882342 C13.193585,0.825938361 13.0519605,0.770496524 12.8868883,0.770496524 L1.18176705,0.770496524 C1.01669482,0.770496524 0.875070361,0.825938361 0.755955746,0.935882342 C0.63590322,1.04582632 0.576814868,1.18302138 0.576814868,1.3484072 L0.576814868,8.39609832 C0.576814868,8.56148414 0.63590322,8.69867919 0.755955746,8.80956287 C0.875070361,8.91950685 1.01669482,8.97400899 1.18176705,8.97400899 L12.8868883,8.97400899 C13.0519605,8.97400899 13.193585,8.91950685 13.3136375,8.80956287 C13.4327521,8.69867919 13.4918404,8.56148414 13.4918404,8.39609832 L13.4918404,1.3484072 Z M13.7253801,0.509262106 C13.9542303,0.738546991 14.0686553,1.01857525 14.0686553,1.3484072 L14.0686553,8.39609832 C14.0686553,8.72686996 13.9542303,9.00689822 13.7253801,9.2361831 C13.4955921,9.46546799 13.2170327,9.58011043 12.8868883,9.58011043 L1.18176705,9.58011043 C0.851622602,9.58011043 0.573063227,9.46546799 0.34327519,9.2361831 C0.114425063,9.00689822 0,8.72686996 0,8.39609832 L0,1.3484072 C0,1.01857525 0.114425063,0.738546991 0.34327519,0.509262106 C0.573063227,0.27903753 0.851622602,0.164395087 1.18176705,0.164395087 L12.8868883,0.164395087 C13.2170327,0.164395087 13.4955921,0.27903753 13.7253801,0.509262106 L13.7253801,0.509262106 Z" id="Fill-1"></path>
+ </g>
+ </g>
+ <g id="Group-6-Copy-3" transform="translate(172.666667, 0.000000)">
+ <g id="Group-4" fill="#4EADC3">
+ <ellipse id="Oval-6-Copy-3" transform="translate(19.000000, 19.000000) rotate(-270.000000) translate(-19.000000, -19.000000) " cx="19" cy="19" rx="19" ry="19"></ellipse>
+ </g>
+ <g id="Group-5-Copy-3" transform="translate(5.541667, 13.458333)" fill="#FFFFFF">
+ <g id="Group-15" transform="translate(0.000000, 0.064462)" fill-rule="nonzero">
+ <path d="M0.791666667,1.32764754 L0.791666667,10.5473525 C0.791666667,10.8452959 1.03196958,11.0833333 1.33643051,11.0833333 L26.3719028,11.0833333 C26.6777591,11.0833333 26.9166667,10.8451595 26.9166667,10.5473525 L26.9166667,1.32764754 C26.9166667,1.02970411 26.6763637,0.791666667 26.3719028,0.791666667 L1.33643051,0.791666667 C1.03057425,0.791666667 0.791666667,1.02984046 0.791666667,1.32764754 Z M0,1.32764754 C0,0.594408051 0.591546751,0 1.33643051,0 L26.3719028,0 C27.109993,0 27.7083333,0.588883096 27.7083333,1.32764754 L27.7083333,10.5473525 C27.7083333,11.2805919 27.1167866,11.875 26.3719028,11.875 L1.33643051,11.875 C0.59834032,11.875 0,11.2861169 0,10.5473525 L0,1.32764754 Z" id="Rectangle-10"></path>
+ </g>
+ <ellipse id="Oval-5" cx="6.10339131" cy="5.89561937" rx="1.64168047" ry="1.61184577"></ellipse>
+ <ellipse id="Oval-5-Copy" cx="11.3567688" cy="5.89561937" rx="1.64168047" ry="1.61184577"></ellipse>
+ <ellipse id="Oval-5-Copy-2" cx="16.6101463" cy="5.89561937" rx="1.64168047" ry="1.61184577"></ellipse>
+ <ellipse id="Oval-5-Copy-3" cx="21.8635238" cy="5.89561937" rx="1.64168047" ry="1.61184577"></ellipse>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/web-ui/app/images/account-recovery/step_4.svg b/web-ui/app/images/account-recovery/step_4.svg
new file mode 100644
index 00000000..b94793e8
--- /dev/null
+++ b/web-ui/app/images/account-recovery/step_4.svg
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="299px" height="38px" viewBox="0 0 299 38" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
+ <title>Group 2</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Account-Recovery-MVP" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="05-m_Forgot_Password-" transform="translate(-36.000000, -101.000000)">
+ <g id="Group-2" transform="translate(36.000000, 101.000000)">
+ <path d="M25,17.296875 C25,19.4531358 24.2500075,21.328117 22.75,22.921875 C21.4999938,24.3593822 20.3437553,26.1874889 19.28125,28.40625 C19.0312488,28.9062525 18.8281258,29.15625 18.671875,29.15625 C18.6406248,29.15625 18.6093752,29.1406252 18.578125,29.109375 L18.53125,29.109375 C18.3437491,29.0468747 18.2421876,28.9296884 18.2265625,28.7578125 C18.2109374,28.5859366 18.2499995,28.2812522 18.34375,27.84375 C18.4375005,27.3749977 18.3437514,26.9531269 18.0625,26.578125 C17.5624975,25.8906216 16.7656305,25.546875 15.671875,25.546875 L9.25,25.546875 C6.96873859,25.546875 5.02344555,24.7421955 3.4140625,23.1328125 C1.80467945,21.5234295 1,19.5781364 1,17.296875 C1,14.9531133 1.81249188,13.0000078 3.4375,11.4375 C5.06250813,9.81249187 6.99998875,9 9.25,9 L16.75,9 C19.0312614,9 20.9765545,9.81249187 22.5859375,11.4375 C24.1953205,13.0625081 25,15.0156136 25,17.296875 Z M19.140625,26.8125 C19.8906288,25.3437427 20.8437442,23.9218819 22,22.546875 L21.953125,22.546875 C23.4218823,21.0781177 24.15625,19.3281352 24.15625,17.296875 C24.15625,15.2343647 23.4296948,13.4765698 21.9765625,12.0234375 C20.5234302,10.5703052 18.7812602,9.84375 16.75,9.84375 L9.25,9.84375 C7.21873984,9.84375 5.46875734,10.5781177 4,12.046875 C2.56249281,13.5468825 1.84375,15.296865 1.84375,17.296875 C1.84375,19.3281352 2.57030523,21.0703052 4.0234375,22.5234375 C5.47656977,23.9765698 7.21873984,24.703125 9.25,24.703125 L15.765625,24.703125 C16.0781266,24.703125 16.5624967,24.8124989 17.21875,25.03125 C17.8750033,25.2500011 18.3906231,25.5937477 18.765625,26.0625 C18.9531259,26.3750016 19.0781247,26.6249991 19.140625,26.8125 Z M7.28125,15.375 C7.84375281,15.375 8.32812297,15.578123 8.734375,15.984375 C9.14062703,16.390627 9.34375,16.8749972 9.34375,17.4375 C9.34375,18.0000028 9.14062703,18.4765605 8.734375,18.8671875 C8.32812297,19.2578145 7.84375281,19.453125 7.28125,19.453125 C6.71874719,19.453125 6.23437703,19.2578145 5.828125,18.8671875 C5.42187297,18.4765605 5.21875,18.0000028 5.21875,17.4375 C5.21875,16.8749972 5.42187297,16.390627 5.828125,15.984375 C6.23437703,15.578123 6.71874719,15.375 7.28125,15.375 Z M7.28125,18.609375 C7.62500172,18.609375 7.91406133,18.4921887 8.1484375,18.2578125 C8.38281367,18.0234363 8.5,17.7500016 8.5,17.4375 C8.5,17.0937483 8.37500125,16.8046887 8.125,16.5703125 C7.87499875,16.3359363 7.59375156,16.21875 7.28125,16.21875 C6.96874844,16.21875 6.68750125,16.3359363 6.4375,16.5703125 C6.18749875,16.8046887 6.0625,17.0937483 6.0625,17.4375 C6.0625,17.7500016 6.17968633,18.0234363 6.4140625,18.2578125 C6.64843867,18.4921887 6.93749828,18.609375 7.28125,18.609375 Z M12.578125,15.28125 C13.1406278,15.28125 13.624998,15.484373 14.03125,15.890625 C14.437502,16.296877 14.640625,16.7812472 14.640625,17.34375 C14.640625,17.9062528 14.437502,18.390623 14.03125,18.796875 C13.624998,19.203127 13.1406278,19.40625 12.578125,19.40625 C12.0156222,19.40625 11.531252,19.203127 11.125,18.796875 C10.718748,18.390623 10.515625,17.9062528 10.515625,17.34375 C10.515625,16.7812472 10.718748,16.296877 11.125,15.890625 C11.531252,15.484373 12.0156222,15.28125 12.578125,15.28125 Z M12.578125,18.5625 C12.9218767,18.5625 13.2109363,18.4453137 13.4453125,18.2109375 C13.6796887,17.9765613 13.796875,17.6875017 13.796875,17.34375 C13.796875,17.0312484 13.6796887,16.7578137 13.4453125,16.5234375 C13.2109363,16.2890613 12.9218767,16.171875 12.578125,16.171875 C12.2343733,16.171875 11.9453137,16.2890613 11.7109375,16.5234375 C11.4765613,16.7578137 11.359375,17.0312484 11.359375,17.34375 C11.359375,17.6875017 11.4765613,17.9765613 11.7109375,18.2109375 C11.9453137,18.4453137 12.2343733,18.5625 12.578125,18.5625 Z M17.921875,15.375 C18.4843778,15.375 18.968748,15.578123 19.375,15.984375 C19.781252,16.390627 19.984375,16.8749972 19.984375,17.4375 C19.984375,18.0000028 19.781252,18.4765605 19.375,18.8671875 C18.968748,19.2578145 18.4843778,19.453125 17.921875,19.453125 C17.3593722,19.453125 16.8828145,19.2578145 16.4921875,18.8671875 C16.1015605,18.4765605 15.90625,18.0000028 15.90625,17.4375 C15.90625,16.8749972 16.1015605,16.390627 16.4921875,15.984375 C16.8828145,15.578123 17.3593722,15.375 17.921875,15.375 Z M17.921875,18.609375 C18.2656267,18.609375 18.5546863,18.4921887 18.7890625,18.2578125 C19.0234387,18.0234363 19.140625,17.7500016 19.140625,17.4375 C19.140625,17.0937483 19.0234387,16.8046887 18.7890625,16.5703125 C18.5546863,16.3359363 18.2656267,16.21875 17.921875,16.21875 C17.6093734,16.21875 17.3359387,16.3359363 17.1015625,16.5703125 C16.8671863,16.8046887 16.75,17.0937483 16.75,17.4375 C16.75,17.7812517 16.8593739,18.0624989 17.078125,18.28125 C17.2968761,18.5000011 17.5781233,18.609375 17.921875,18.609375 Z" id="6-copy-6" fill="#FFFFFF" transform="translate(13.000000, 19.078125) scale(-1, 1) translate(-13.000000, -19.078125) "></path>
+ <rect id="Rectangle-15-Copy-11" fill="#4EADC3" x="22" y="18" width="260" height="2"></rect>
+ <g id="Group-2-Copy-3" transform="translate(0.000000, 6.000000)">
+ <ellipse id="Oval-6-Copy-2" fill="#4EADC3" transform="translate(13.000000, 13.000000) rotate(-270.000000) translate(-13.000000, -13.000000) " cx="13" cy="13" rx="13" ry="13"></ellipse>
+ <path d="M19.5,12.077474 C19.5,13.2454485 19.0937541,14.2610634 18.28125,15.124349 C17.6041633,15.9029987 16.9778675,16.8932232 16.4023438,18.0950521 C16.2669264,18.3658868 16.1569015,18.5013021 16.0722656,18.5013021 C16.0553385,18.5013021 16.0384115,18.4928386 16.0214844,18.4759115 L15.9960938,18.4759115 C15.8945307,18.4420571 15.8395183,18.3785812 15.8310547,18.2854818 C15.8225911,18.1923823 15.8437497,18.0273449 15.8945313,17.7903646 C15.9453128,17.5364571 15.894532,17.3079437 15.7421875,17.1048177 C15.4713528,16.73242 15.0397165,16.546224 14.4472656,16.546224 L10.96875,16.546224 C9.73306674,16.546224 8.67936634,16.1103559 7.80761719,15.2386068 C6.93586804,14.3668576 6.5,13.3131572 6.5,12.077474 C6.5,10.8079364 6.94009977,9.75000423 7.8203125,8.90364583 C8.70052523,8.0234331 9.74999391,7.58333333 10.96875,7.58333333 L15.03125,7.58333333 C16.2669333,7.58333333 17.3206337,8.0234331 18.1923828,8.90364583 C19.064132,9.78385857 19.5,10.8417907 19.5,12.077474 Z M16.3261719,17.2317708 C16.7324239,16.4361939 17.2486948,15.6660193 17.875,14.921224 L17.8496094,14.921224 C18.6451863,14.1256471 19.0429688,13.1777399 19.0429688,12.077474 C19.0429688,10.9602809 18.649418,10.008142 17.8623047,9.22102865 C17.0751914,8.43391534 16.1315159,8.04036458 15.03125,8.04036458 L10.96875,8.04036458 C9.86848408,8.04036458 8.92057689,8.43814706 8.125,9.23372396 C7.34635027,10.046228 6.95703125,10.9941352 6.95703125,12.077474 C6.95703125,13.1777399 7.350582,14.1214153 8.13769531,14.9085286 C8.92480862,15.695642 9.86848408,16.0891927 10.96875,16.0891927 L14.4980469,16.0891927 C14.6673186,16.0891927 14.9296857,16.1484369 15.2851563,16.2669271 C15.6406268,16.3854173 15.9199209,16.5716133 16.1230469,16.8255208 C16.2246099,16.9947925 16.2923175,17.1302078 16.3261719,17.2317708 Z M9.90234375,11.0364583 C10.2070328,11.0364583 10.4693999,11.1464833 10.6894531,11.3665365 C10.9095063,11.5865896 11.0195313,11.8489568 11.0195313,12.1536458 C11.0195313,12.4583349 10.9095063,12.7164703 10.6894531,12.9280599 C10.4693999,13.1396495 10.2070328,13.2454427 9.90234375,13.2454427 C9.59765473,13.2454427 9.33528756,13.1396495 9.11523438,12.9280599 C8.89518119,12.7164703 8.78515625,12.4583349 8.78515625,12.1536458 C8.78515625,11.8489568 8.89518119,11.5865896 9.11523438,11.3665365 C9.33528756,11.1464833 9.59765473,11.0364583 9.90234375,11.0364583 Z M9.90234375,12.7884115 C10.0885426,12.7884115 10.2451166,12.7249355 10.3720703,12.5979818 C10.4990241,12.471028 10.5625,12.3229175 10.5625,12.1536458 C10.5625,11.967447 10.4947923,11.810873 10.359375,11.6839193 C10.2239577,11.5569655 10.0716154,11.4934896 9.90234375,11.4934896 C9.73307207,11.4934896 9.58072984,11.5569655 9.4453125,11.6839193 C9.30989516,11.810873 9.2421875,11.967447 9.2421875,12.1536458 C9.2421875,12.3229175 9.30566343,12.471028 9.43261719,12.5979818 C9.55957095,12.7249355 9.7161449,12.7884115 9.90234375,12.7884115 Z M12.7714844,10.9856771 C13.0761734,10.9856771 13.3385406,11.095702 13.5585937,11.3157552 C13.7786469,11.5358084 13.8886719,11.7981756 13.8886719,12.1028646 C13.8886719,12.4075536 13.7786469,12.6699208 13.5585937,12.889974 C13.3385406,13.1100271 13.0761734,13.2200521 12.7714844,13.2200521 C12.4667954,13.2200521 12.2044282,13.1100271 11.984375,12.889974 C11.7643218,12.6699208 11.6542969,12.4075536 11.6542969,12.1028646 C11.6542969,11.7981756 11.7643218,11.5358084 11.984375,11.3157552 C12.2044282,11.095702 12.4667954,10.9856771 12.7714844,10.9856771 Z M12.7714844,12.7630208 C12.9576832,12.7630208 13.1142572,12.6995449 13.2412109,12.5725911 C13.3681647,12.4456374 13.4316406,12.2890634 13.4316406,12.1028646 C13.4316406,11.9335929 13.3681647,11.7854824 13.2412109,11.6585286 C13.1142572,11.5315749 12.9576832,11.468099 12.7714844,11.468099 C12.5852855,11.468099 12.4287116,11.5315749 12.3017578,11.6585286 C12.1748041,11.7854824 12.1113281,11.9335929 12.1113281,12.1028646 C12.1113281,12.2890634 12.1748041,12.4456374 12.3017578,12.5725911 C12.4287116,12.6995449 12.5852855,12.7630208 12.7714844,12.7630208 Z M15.6660156,11.0364583 C15.9707046,11.0364583 16.2330718,11.1464833 16.453125,11.3665365 C16.6731782,11.5865896 16.7832031,11.8489568 16.7832031,12.1536458 C16.7832031,12.4583349 16.6731782,12.7164703 16.453125,12.9280599 C16.2330718,13.1396495 15.9707046,13.2454427 15.6660156,13.2454427 C15.3613266,13.2454427 15.1031912,13.1396495 14.8916016,12.9280599 C14.680012,12.7164703 14.5742188,12.4583349 14.5742188,12.1536458 C14.5742188,11.8489568 14.680012,11.5865896 14.8916016,11.3665365 C15.1031912,11.1464833 15.3613266,11.0364583 15.6660156,11.0364583 Z M15.6660156,12.7884115 C15.8522145,12.7884115 16.0087884,12.7249355 16.1357422,12.5979818 C16.2626959,12.471028 16.3261719,12.3229175 16.3261719,12.1536458 C16.3261719,11.967447 16.2626959,11.810873 16.1357422,11.6839193 C16.0087884,11.5569655 15.8522145,11.4934896 15.6660156,11.4934896 C15.4967439,11.4934896 15.3486334,11.5569655 15.2216797,11.6839193 C15.0947259,11.810873 15.03125,11.967447 15.03125,12.1536458 C15.03125,12.3398447 15.0904942,12.4921869 15.2089844,12.6106771 C15.3274746,12.7291673 15.4798168,12.7884115 15.6660156,12.7884115 Z" id="6-copy-3" fill="#FFFFFF" transform="translate(13.000000, 13.042318) scale(-1, 1) translate(-13.000000, -13.042318) "></path>
+ </g>
+ <g id="Group-2-Copy-5" transform="translate(87.000000, 7.000000)">
+ <ellipse id="Oval-6-Copy-4" fill="#4EADC3" transform="translate(13.000000, 13.000000) rotate(-270.000000) translate(-13.000000, -13.000000) " cx="13" cy="13" rx="13" ry="13"></ellipse>
+ <g id="Group-Copy-2" transform="translate(5.958333, 8.125000)" fill="#FFFFFF">
+ <path d="M12.4751456,7.26752805 C12.6402178,7.35961788 12.6674172,7.49681293 12.5576817,7.6800529 C12.502345,7.77214273 12.4198089,7.81818765 12.3100734,7.81818765 C12.2734949,7.81818765 12.2181582,7.79939381 12.1450012,7.76274581 L9.50759726,6.00082303 C9.34252504,5.90967289 9.31532564,5.77153815 9.42506115,5.58829818 C9.51697637,5.42291236 9.65391128,5.39566129 9.8368038,5.50560527 L12.4751456,7.26752805 Z M7.03432766,6.3870365 L1.89645474,3.05582783 C1.73138251,2.94494416 1.70418311,2.81714603 1.81391863,2.66961436 C1.92365414,2.48637439 2.05120994,2.4497264 2.19846187,2.55967038 L7.03432766,5.69824216 L11.8701934,2.55967038 C12.0174454,2.4497264 12.1450012,2.48637439 12.2556746,2.66961436 C12.3654101,2.81714603 12.3382107,2.94494416 12.1731385,3.05582783 L7.03432766,6.3870365 Z M4.64359416,5.58829818 C4.75426759,5.77153815 4.72613028,5.90967289 4.56105805,6.00082303 L1.92365414,7.76274581 C1.85049713,7.79939381 1.79516042,7.81818765 1.75858191,7.81818765 C1.6488464,7.81818765 1.56631029,7.77214273 1.51191149,7.6800529 C1.40123807,7.49681293 1.42937538,7.35961788 1.59350969,7.26752805 L4.23185152,5.50560527 C4.41474404,5.39566129 4.55167895,5.42291236 4.64359416,5.58829818 L4.64359416,5.58829818 Z M13.4918404,1.3484072 C13.4918404,1.18302138 13.4327521,1.04582632 13.3136375,0.935882342 C13.193585,0.825938361 13.0519605,0.770496524 12.8868883,0.770496524 L1.18176705,0.770496524 C1.01669482,0.770496524 0.875070361,0.825938361 0.755955746,0.935882342 C0.63590322,1.04582632 0.576814868,1.18302138 0.576814868,1.3484072 L0.576814868,8.39609832 C0.576814868,8.56148414 0.63590322,8.69867919 0.755955746,8.80956287 C0.875070361,8.91950685 1.01669482,8.97400899 1.18176705,8.97400899 L12.8868883,8.97400899 C13.0519605,8.97400899 13.193585,8.91950685 13.3136375,8.80956287 C13.4327521,8.69867919 13.4918404,8.56148414 13.4918404,8.39609832 L13.4918404,1.3484072 Z M13.7253801,0.509262106 C13.9542303,0.738546991 14.0686553,1.01857525 14.0686553,1.3484072 L14.0686553,8.39609832 C14.0686553,8.72686996 13.9542303,9.00689822 13.7253801,9.2361831 C13.4955921,9.46546799 13.2170327,9.58011043 12.8868883,9.58011043 L1.18176705,9.58011043 C0.851622602,9.58011043 0.573063227,9.46546799 0.34327519,9.2361831 C0.114425063,9.00689822 0,8.72686996 0,8.39609832 L0,1.3484072 C0,1.01857525 0.114425063,0.738546991 0.34327519,0.509262106 C0.573063227,0.27903753 0.851622602,0.164395087 1.18176705,0.164395087 L12.8868883,0.164395087 C13.2170327,0.164395087 13.4955921,0.27903753 13.7253801,0.509262106 L13.7253801,0.509262106 Z" id="Fill-1"></path>
+ </g>
+ </g>
+ <g id="Group-6-Copy-3" transform="translate(174.000000, 6.000000)">
+ <g id="Group-4" fill="#4EADC3">
+ <ellipse id="Oval-6-Copy-3" transform="translate(13.000000, 13.000000) rotate(-270.000000) translate(-13.000000, -13.000000) " cx="13" cy="13" rx="13" ry="13"></ellipse>
+ </g>
+ <g id="Group-5-Copy-3" transform="translate(3.791667, 9.208333)" fill="#FFFFFF">
+ <g id="Group-15" transform="translate(0.000000, 0.044106)" fill-rule="nonzero">
+ <path d="M0.541666667,0.908390422 L0.541666667,7.21660958 C0.541666667,7.42046561 0.706084452,7.58333333 0.914399823,7.58333333 L18.0439335,7.58333333 C18.2532036,7.58333333 18.4166667,7.42037231 18.4166667,7.21660958 L18.4166667,0.908390422 C18.4166667,0.704534391 18.2522489,0.541666667 18.0439335,0.541666667 L0.914399823,0.541666667 C0.705129753,0.541666667 0.541666667,0.704627685 0.541666667,0.908390422 Z M0,0.908390422 C0,0.406700245 0.404742514,0 0.914399823,0 L18.0439335,0 C18.5489426,0 18.9583333,0.402920013 18.9583333,0.908390422 L18.9583333,7.21660958 C18.9583333,7.71829975 18.5535908,8.125 18.0439335,8.125 L0.914399823,8.125 C0.409390745,8.125 0,7.72207999 0,7.21660958 L0,0.908390422 Z" id="Rectangle-10"></path>
+ </g>
+ <ellipse id="Oval-5" cx="4.17600458" cy="4.03384483" rx="1.12325506" ry="1.10284184"></ellipse>
+ <ellipse id="Oval-5-Copy" cx="7.77042077" cy="4.03384483" rx="1.12325506" ry="1.10284184"></ellipse>
+ <ellipse id="Oval-5-Copy-2" cx="11.364837" cy="4.03384483" rx="1.12325506" ry="1.10284184"></ellipse>
+ <ellipse id="Oval-5-Copy-3" cx="14.9592531" cy="4.03384483" rx="1.12325506" ry="1.10284184"></ellipse>
+ </g>
+ </g>
+ <g id="Group-7-Copy-2" transform="translate(261.000000, 0.000000)">
+ <circle id="Oval-6-Copy-5" fill="#3DABC4" transform="translate(19.000000, 19.000000) rotate(-270.000000) translate(-19.000000, -19.000000) " cx="19" cy="19" r="19"></circle>
+ <g id="Group-29" transform="translate(1.705128, 12.666667)" fill="#FFFFFF">
+ <g id="Group" transform="translate(6.834452, 7.092700) rotate(-270.000000) translate(-6.834452, -7.092700) translate(0.988298, 0.515777)" fill-rule="nonzero">
+ <path d="M5.1932455,11.4964236 L5.1932455,3.20387193 C5.1932455,2.98525922 5.01602488,2.8080386 4.79741216,2.8080386 C4.57879945,2.8080386 4.40157883,2.98525922 4.40157883,3.20387193 L4.40157883,11.4964236 C4.40157883,11.7150363 4.57879945,11.8922569 4.79741216,11.8922569 C5.01602488,11.8922569 5.1932455,11.7150363 5.1932455,11.4964236 Z" id="Line-Copy-13"></path>
+ <path d="M0.931183632,6.63409023 L0.931183632,1.83707916 C0.931183632,1.61846645 0.753963012,1.44124583 0.535350299,1.44124583 C0.316737585,1.44124583 0.139516965,1.61846645 0.139516965,1.83707916 L0.139516965,6.63409023 C0.139516965,6.85270295 0.316737585,7.02992357 0.535350299,7.02992357 C0.753963012,7.02992357 0.931183632,6.85270295 0.931183632,6.63409023 Z" id="Line-Copy-12"></path>
+ <path d="M9.6406144,6.63409023 L9.6406144,1.83707916 C9.6406144,1.61846645 9.46339378,1.44124583 9.24478107,1.44124583 C9.02616835,1.44124583 8.84894773,1.61846645 8.84894773,1.83707916 L8.84894773,6.63409023 C8.84894773,6.85270295 9.02616835,7.02992357 9.24478107,7.02992357 C9.46339378,7.02992357 9.6406144,6.85270295 9.6406144,6.63409023 Z" id="Line-Copy-14"></path>
+ </g>
+ <g id="Group-17" transform="translate(12.111672, 0.227604)">
+ <path d="M15.8131883,8.81640217 C16.0175439,8.93029872 16.0512161,9.09998135 15.9153661,9.32661225 C15.8468606,9.44050881 15.7446828,9.49745708 15.6088328,9.49745708 C15.5635495,9.49745708 15.4950439,9.47421289 15.4044772,9.42888671 L12.1394328,7.24974342 C11.9350772,7.13700907 11.901405,6.96616424 12.037255,6.73953333 C12.1510439,6.53498442 12.3205661,6.50128033 12.5469828,6.63725888 L15.8131883,8.81640217 Z M9.07758279,7.72741163 L2.71701612,3.60737804 C2.51266056,3.47023729 2.47898834,3.31217677 2.61483834,3.12970983 C2.75068834,2.90307893 2.90859945,2.85775275 3.0908939,2.99373129 L9.07758279,6.87551188 L15.0642717,2.99373129 C15.2465661,2.85775275 15.4044772,2.90307893 15.5414883,3.12970983 C15.6773383,3.31217677 15.6436661,3.47023729 15.4393106,3.60737804 M6.11791056,6.73953333 C6.25492167,6.96616424 6.22008834,7.13700907 6.01573279,7.24974342 L2.75068834,9.42888671 C2.66012167,9.47421289 2.59161612,9.49745708 2.54633279,9.49745708 C2.41048279,9.49745708 2.30830501,9.44050881 2.24096056,9.32661225 C2.10394945,9.09998135 2.13878279,8.93029872 2.34197723,8.81640217 L5.60818279,6.63725888 C5.83459945,6.50128033 6.00412167,6.53498442 6.11791056,6.73953333 L6.11791056,6.73953333 Z M17.0718328,1.49564292 C17.0718328,1.291094 16.9986828,1.12141138 16.8512217,0.985432835 C16.7025995,0.849454293 16.5272717,0.780883918 16.3229161,0.780883918 L1.83224945,0.780883918 C1.6278939,0.780883918 1.45256612,0.849454293 1.30510501,0.985432835 C1.15648279,1.12141138 1.08333279,1.291094 1.08333279,1.49564292 L1.08333279,10.2122161 C1.08333279,10.416765 1.15648279,10.5864476 1.30510501,10.7235884 C1.45256612,10.8595669 1.6278939,10.9269751 1.83224945,10.9269751 L16.3229161,10.9269751 C16.5272717,10.9269751 16.7025995,10.8595669 16.8512217,10.7235884 C16.9986828,10.5864476 17.0718328,10.416765 17.0718328,10.2122161 L17.0718328,1.49564292 Z M17.3609495,0.457789606 C17.6442606,0.741368786 17.7859161,1.08770729 17.7859161,1.49564292 L17.7859161,10.2122161 C17.7859161,10.6213139 17.6442606,10.9676524 17.3609495,11.2512316 C17.0764772,11.5348108 16.7316272,11.6766004 16.3229161,11.6766004 L1.83224945,11.6766004 C1.42353834,11.6766004 1.07868834,11.5348108 0.794216119,11.2512316 C0.510905008,10.9676524 0.369249453,10.6213139 0.369249453,10.2122161 L0.369249453,1.49564292 C0.369249453,1.08770729 0.510905008,0.741368786 0.794216119,0.457789606 C1.07868834,0.173048215 1.42353834,0.0312586252 1.83224945,0.0312586252 L16.3229161,0.0312586252 C16.7316272,0.0312586252 17.0764772,0.173048215 17.3609495,0.457789606" id="Fill-1"></path>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/web-ui/app/locales/en_US/translation.json b/web-ui/app/locales/en_US/translation.json
index 5e23d39f..6ca72283 100644
--- a/web-ui/app/locales/en_US/translation.json
+++ b/web-ui/app/locales/en_US/translation.json
@@ -81,6 +81,40 @@
"all": "All",
"tags": "Tags"
},
+ "account-recovery": {
+ "page-title": "Pixelated Account Recovery",
+ "admin-form": {
+ "image-description": "Admin Recovery Code - Step 1 of 4",
+ "title": "Contact your account administrator and ask for their part of the recovery code",
+ "tip1": "The safest way to do this is in person.",
+ "tip2": "You can call or text if you need to.",
+ "tip3": "Don't ever ask for it via email.",
+ "input-label": "type here admin's backup code"
+ },
+ "user-form": {
+ "image-description": "User Recovery Code - Step 2 of 4",
+ "title": "Remember your backup account?",
+ "description": "When you created your account you received a message - it was sent by team@pixelated-project.org. You'll need the recovery code that is in it.",
+ "input-label": "type here your backup code"
+ },
+ "new-password-form": {
+ "image-description": "New Password - Step 3 of 4",
+ "title": "Now, create a new password",
+ "input-label1": "create new password",
+ "input-label2": "confirm your new password",
+ "error": {
+ "invalid-password": "A better password has at least 8 characters",
+ "invalid-confirm-password": "Password and confirmation don't match"
+ }
+ },
+ "backup-account-step": {
+ "image-description": "Backup Account - Step 4 of 4",
+ "title": "Wait! What if you forget your password again?",
+ "buttonText": "Set-up Backup Account"
+ },
+ "button-next": "Next",
+ "back": "Back to previous step"
+ },
"backup-account": {
"page-title": "Pixelated Backup Account",
"backup-email": {
@@ -100,6 +134,9 @@
"paragraph": "Save this message, it is really important.",
"button": "Got it! I'm ready!",
"retry-button": "Hey, I didn't received it"
+ },
+ "error": {
+ "submit-error": "There was an error. Please try again later."
}
},
"back-to-inbox": "Back to my inbox",
diff --git a/web-ui/app/locales/pt_BR/translation.json b/web-ui/app/locales/pt_BR/translation.json
index dcf551af..1baf9b07 100644
--- a/web-ui/app/locales/pt_BR/translation.json
+++ b/web-ui/app/locales/pt_BR/translation.json
@@ -81,6 +81,40 @@
"all": "Todas",
"tags": "Etiquetas"
},
+ "account-recovery": {
+ "page-title": "Pixelated Recuperação de Conta",
+ "admin-form": {
+ "image-description": "Código de Recuperação do Administrador - Passo 1 de 4",
+ "title": "Contate o administrador da sua conta e peça seu código",
+ "tip1": "A maneira mais segura é fazer isso pessoalmente.",
+ "tip2": "Você pode ligar ou mandar mensagem de texto se precisar.",
+ "tip3": "Nunca peça por e-mail.",
+ "input-label": "digite aqui o código"
+ },
+ "user-form": {
+ "image-description": "Código de Recuperação do Usuário - Passo 2 de 4",
+ "title": "Lembra do seu e-mail de recuperação?",
+ "description": "Quando você criou uma conta você recebeu uma mensagem do team@pixelated-project.org. Você precisará do código que está neste e-mail.",
+ "input-label": "digite aqui o código"
+ },
+ "new-password-form": {
+ "image-description": "Nova Senha - Passo 3 de 4",
+ "title": "Agora, crie uma nova senha",
+ "input-label1": "digite a nova senha",
+ "input-label2": "confirme a nova senha",
+ "error": {
+ "invalid-password": "Uma senha boa tem pelo menos 8 caracteres",
+ "invalid-confirm-password": "Senha e confirmação não são iguais"
+ }
+ },
+ "backup-account-step": {
+ "image-description": "E-mail de Recuperação - Passo 4 de 4",
+ "title": "Opa! E se você esquecer sua senha de novo?",
+ "buttonText": "Configurar E-mail de Recuperação"
+ },
+ "button-next": "Próximo",
+ "back": "Voltar ao passo anterior"
+ },
"backup-account": {
"page-title": "Pixelated E-mail de Recuperação",
"backup-email": {
@@ -100,6 +134,9 @@
"paragraph": "Salve esse e-mail, ele é bem importante.",
"button": "Recebi! Pronto!",
"retry-button": "Ei, eu não recebi"
+ },
+ "error": {
+ "submit-error": "Ocorreu um erro. Por favor tente novamente mais tarde."
}
},
"back-to-inbox": "Voltar",
diff --git a/web-ui/config/protected-assets-webpack.js b/web-ui/config/protected-assets-webpack.js
index 85654cf0..032f6c31 100644
--- a/web-ui/config/protected-assets-webpack.js
+++ b/web-ui/config/protected-assets-webpack.js
@@ -5,6 +5,7 @@ module.exports = new CopyWebpackPlugin([
{ context: 'app/', from: 'index.html' },
{ context: 'app/', from: 'sandbox.html' },
{ context: 'app/', from: 'css/*' },
+ { context: 'src/account_recovery/', from: 'account_recovery.html' },
{ context: 'src/backup_account/', from: 'backup_account.html' },
{ context: 'app/bower_components/font-awesome/', from: 'fonts/*' },
{ context: 'app/bower_components/font-awesome/', from: 'css/font-awesome.min.css', to: 'css' },
diff --git a/web-ui/config/public-assets-webpack.js b/web-ui/config/public-assets-webpack.js
index 28dff566..13364e2c 100644
--- a/web-ui/config/public-assets-webpack.js
+++ b/web-ui/config/public-assets-webpack.js
@@ -3,6 +3,7 @@ var CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = new CopyWebpackPlugin([
{ context: 'src/login/', from: '*.html' },
{ context: 'src/login/', from: '*.css' },
+ { context: 'src/account_recovery/', from: 'account_recovery.html' },
{ context: 'src/interstitial/', from: '*' },
{ context: 'app/', from: 'fonts/*' },
{ context: 'app/', from: 'locales/**/*' },
diff --git a/web-ui/package.json b/web-ui/package.json
index 08b24ceb..2ef337a2 100644
--- a/web-ui/package.json
+++ b/web-ui/package.json
@@ -20,7 +20,7 @@
"dompurify": "0.8.4",
"enzyme": "2.7.1",
"es6-promise": "4.1.0",
- "eslint": "3.17.1",
+ "eslint": "3.19.0",
"eslint-config-airbnb": "14.1.0",
"eslint-plugin-import": "2.2.0",
"eslint-plugin-jsx-a11y": "4.0.0",
diff --git a/web-ui/src/account_recovery/account_recovery.html b/web-ui/src/account_recovery/account_recovery.html
new file mode 100644
index 00000000..35054455
--- /dev/null
+++ b/web-ui/src/account_recovery/account_recovery.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <link rel="icon" type="image/png" href="public/images/favicon.png" />
+ <meta charset="utf-8"/>
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
+ <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
+ <title>Pixelated</title>
+ </head>
+ <body>
+ <div id="root"/>
+ <script type="text/javascript" src="/public/account_recovery.js"></script>
+ </body>
+</html>
diff --git a/web-ui/src/account_recovery/account_recovery.js b/web-ui/src/account_recovery/account_recovery.js
new file mode 100644
index 00000000..19b7c19c
--- /dev/null
+++ b/web-ui/src/account_recovery/account_recovery.js
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from 'react';
+import { render } from 'react-dom';
+import a11y from 'react-a11y';
+
+import App from 'src/common/app';
+import PageWrapper from './page';
+
+require('es6-promise').polyfill();
+
+if (process.env.NODE_ENV === 'development') a11y(React);
+
+render(
+ <App child={<PageWrapper />} />,
+ document.getElementById('root')
+);
diff --git a/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.js b/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.js
new file mode 100644
index 00000000..5b9da350
--- /dev/null
+++ b/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.js
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from 'react';
+import { translate } from 'react-i18next';
+
+import InputField from 'src/common/input_field/input_field';
+import SubmitButton from 'src/common/submit_button/submit_button';
+
+import './admin_recovery_code_form.scss';
+
+export const AdminRecoveryCodeForm = ({ t, next }) => (
+ <form className='account-recovery-form admin-code' onSubmit={next}>
+ <img
+ className='account-recovery-progress'
+ src='/public/images/account-recovery/step_1.svg'
+ alt={t('account-recovery.admin-form.image-description')}
+ />
+ <h1>{t('account-recovery.admin-form.title')}</h1>
+ <img
+ className='admin-code-image'
+ src='/public/images/account-recovery/admins_contact.svg'
+ alt=''
+ />
+ <ul>
+ <li>{t('account-recovery.admin-form.tip1')}</li>
+ <li>{t('account-recovery.admin-form.tip2')}</li>
+ <li>{t('account-recovery.admin-form.tip3')}</li>
+ </ul>
+ <InputField name='admin-code' label={t('account-recovery.admin-form.input-label')} />
+ <SubmitButton buttonText={t('account-recovery.button-next')} />
+ </form>
+);
+
+AdminRecoveryCodeForm.propTypes = {
+ t: React.PropTypes.func.isRequired,
+ next: React.PropTypes.func.isRequired
+};
+
+export default translate('', { wait: true })(AdminRecoveryCodeForm);
diff --git a/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.scss b/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.scss
new file mode 100644
index 00000000..6dcbcc67
--- /dev/null
+++ b/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.scss
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+.admin-code-image {
+ margin: 1em 0;
+ align-self: center;
+ height: 2.7em;
+}
diff --git a/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.spec.js b/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.spec.js
new file mode 100644
index 00000000..73c4c1e0
--- /dev/null
+++ b/web-ui/src/account_recovery/admin_recovery_code_form/admin_recovery_code_form.spec.js
@@ -0,0 +1,38 @@
+import { shallow } from 'enzyme';
+import expect from 'expect';
+import React from 'react';
+import { AdminRecoveryCodeForm } from './admin_recovery_code_form';
+
+describe('AdminRecoveryCodeForm', () => {
+ let adminRecoveryCodeForm;
+ let mockNext;
+
+ beforeEach(() => {
+ const mockTranslations = key => key;
+ mockNext = expect.createSpy();
+ adminRecoveryCodeForm = shallow(
+ <AdminRecoveryCodeForm t={mockTranslations} next={mockNext} />
+ );
+ });
+
+ it('renders title for admin recovery code', () => {
+ expect(adminRecoveryCodeForm.find('h1').text()).toEqual('account-recovery.admin-form.title');
+ });
+
+ it('renders tips for retrieving recovery code', () => {
+ expect(adminRecoveryCodeForm.find('li').length).toEqual(3);
+ });
+
+ it('renders input field for admin code', () => {
+ expect(adminRecoveryCodeForm.find('InputField').props().name).toEqual('admin-code');
+ });
+
+ it('renders button for next step', () => {
+ expect(adminRecoveryCodeForm.find('SubmitButton').props().buttonText).toEqual('account-recovery.button-next');
+ });
+
+ it('submits form to next step', () => {
+ adminRecoveryCodeForm.find('form').simulate('submit');
+ expect(mockNext).toHaveBeenCalled();
+ });
+});
diff --git a/web-ui/src/account_recovery/backup_account_step/backup_account_step.js b/web-ui/src/account_recovery/backup_account_step/backup_account_step.js
new file mode 100644
index 00000000..891b45ec
--- /dev/null
+++ b/web-ui/src/account_recovery/backup_account_step/backup_account_step.js
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+import React from 'react';
+import { translate } from 'react-i18next';
+import LinkButton from 'src/common/link_button/link_button';
+
+export const BackupAccountStep = ({ t }) => (
+ <div className='account-recovery-form backup-account'>
+ <img
+ className='account-recovery-progress'
+ src='/public/images/account-recovery/step_4.svg'
+ alt={t('account-recovery.backup-account-step.image-description')}
+ />
+ <h1>{t('account-recovery.backup-account-step.title')}</h1>
+ <LinkButton
+ buttonText={t('account-recovery.backup-account-step.buttonText')}
+ href='/backup-account'
+ />
+ </div>
+);
+
+BackupAccountStep.propTypes = {
+ t: React.PropTypes.func.isRequired
+};
+
+export default translate('', { wait: true })(BackupAccountStep);
diff --git a/web-ui/src/account_recovery/backup_account_step/backup_account_step.spec.js b/web-ui/src/account_recovery/backup_account_step/backup_account_step.spec.js
new file mode 100644
index 00000000..38a5e560
--- /dev/null
+++ b/web-ui/src/account_recovery/backup_account_step/backup_account_step.spec.js
@@ -0,0 +1,27 @@
+import { shallow } from 'enzyme';
+import expect from 'expect';
+import React from 'react';
+import LinkButton from 'src/common/link_button/link_button';
+import { BackupAccountStep } from './backup_account_step';
+
+describe('BackupAccountStep', () => {
+ let backupAccountStep;
+
+ beforeEach(() => {
+ const mockTranslations = key => key;
+ backupAccountStep = shallow(<BackupAccountStep t={mockTranslations} />);
+ });
+
+ it('renders title for backup account step', () => {
+ expect(backupAccountStep.find('h1').text()).toEqual('account-recovery.backup-account-step.title');
+ });
+
+ it('renders submit button with given href', () => {
+ expect(backupAccountStep.find(LinkButton).props().href).toEqual('/backup-account');
+ });
+
+ it('renders submit button with given button text', () => {
+ expect(backupAccountStep.find(LinkButton).props().buttonText)
+ .toEqual('account-recovery.backup-account-step.buttonText');
+ });
+});
diff --git a/web-ui/src/account_recovery/new_password_form/new_password_form.js b/web-ui/src/account_recovery/new_password_form/new_password_form.js
new file mode 100644
index 00000000..5e1e72c9
--- /dev/null
+++ b/web-ui/src/account_recovery/new_password_form/new_password_form.js
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import 'isomorphic-fetch';
+import React from 'react';
+import { translate } from 'react-i18next';
+import validator from 'validator';
+
+import { submitForm } from 'src/common/util';
+import InputField from 'src/common/input_field/input_field';
+import SubmitButton from 'src/common/submit_button/submit_button';
+import BackLink from 'src/common/back_link/back_link';
+
+import './new_password_form.scss';
+
+export class NewPasswordForm extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ submitButtonDisabled: true,
+ password: '',
+ errorPassword: '',
+ confirmPassword: '',
+ errorConfirmPassword: ''
+ };
+ }
+
+ submitHandler = (event) => {
+ event.preventDefault();
+ submitForm(event, '/account-recovery', {
+ userCode: this.props.userCode,
+ password: this.state.password,
+ confirmPassword: this.state.confirmPassword
+ }).then(() => this.props.next());
+ }
+
+ handleChangePassword = (event) => {
+ this.setState({ password: event.target.value });
+ this.validatePassword(event.target.value, this.state.confirmPassword);
+ };
+
+ handleChangeConfirmPassword = (event) => {
+ this.setState({ confirmPassword: event.target.value });
+ this.validatePassword(this.state.password, event.target.value);
+ };
+
+ validatePassword = (password, confirmPassword) => {
+ const emptyPassword = validator.isEmpty(password);
+ const validPassword = validator.isLength(password, { min: 8, max: 9999 });
+ const emptyConfirmPassword = validator.isEmpty(confirmPassword);
+ const validConfirmPassword = confirmPassword === password;
+
+ const t = this.props.t;
+
+ this.setState({
+ errorPassword: !emptyPassword && !validPassword ? t('account-recovery.new-password-form.error.invalid-password') : '',
+ errorConfirmPassword: !emptyConfirmPassword && !validConfirmPassword ? t('account-recovery.new-password-form.error.invalid-confirm-password') : '',
+ submitButtonDisabled: !validPassword || !validConfirmPassword
+ });
+ };
+
+ render() {
+ const { t, previous } = this.props;
+ return (
+ <form className='account-recovery-form new-password' onSubmit={this.submitHandler}>
+ <img
+ className='account-recovery-progress'
+ src='/public/images/account-recovery/step_3.svg'
+ alt={t('account-recovery.new-password-form.image-description')}
+ />
+ <h1>{t('account-recovery.new-password-form.title')}</h1>
+ <InputField
+ type='password' name='new-password' value={this.state.password}
+ label={t('account-recovery.new-password-form.input-label1')}
+ errorText={this.state.errorPassword} onChange={this.handleChangePassword}
+ />
+ <InputField
+ type='password' name='confirm-password' value={this.state.confirmPassword}
+ label={t('account-recovery.new-password-form.input-label2')}
+ errorText={this.state.errorConfirmPassword} onChange={this.handleChangeConfirmPassword}
+ />
+ <SubmitButton buttonText={t('account-recovery.button-next')} disabled={this.state.submitButtonDisabled} />
+ <BackLink text={t('account-recovery.back')} onClick={previous} />
+ </form>
+ );
+ }
+}
+
+NewPasswordForm.propTypes = {
+ t: React.PropTypes.func.isRequired,
+ next: React.PropTypes.func.isRequired,
+ previous: React.PropTypes.func.isRequired,
+ userCode: React.PropTypes.string.isRequired
+};
+
+export default translate('', { wait: true })(NewPasswordForm);
diff --git a/web-ui/src/account_recovery/new_password_form/new_password_form.scss b/web-ui/src/account_recovery/new_password_form/new_password_form.scss
new file mode 100644
index 00000000..77623262
--- /dev/null
+++ b/web-ui/src/account_recovery/new_password_form/new_password_form.scss
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+.new-password {
+ .input-field-group:first-of-type {
+ margin-bottom: 0;
+ }
+}
diff --git a/web-ui/src/account_recovery/new_password_form/new_password_form.spec.js b/web-ui/src/account_recovery/new_password_form/new_password_form.spec.js
new file mode 100644
index 00000000..c29487a7
--- /dev/null
+++ b/web-ui/src/account_recovery/new_password_form/new_password_form.spec.js
@@ -0,0 +1,161 @@
+import { shallow } from 'enzyme';
+import expect from 'expect';
+import React from 'react';
+import fetchMock from 'fetch-mock';
+import { NewPasswordForm } from './new_password_form';
+
+describe('NewPasswordForm', () => {
+ let newPasswordForm;
+ let mockPrevious;
+ let mockNext;
+ let mockTranslations;
+
+ beforeEach(() => {
+ mockTranslations = key => key;
+ mockPrevious = expect.createSpy();
+ newPasswordForm = shallow(
+ <NewPasswordForm t={mockTranslations} previous={mockPrevious} userCode='def234' />
+ );
+ });
+
+ it('renders title for new password form', () => {
+ expect(newPasswordForm.find('h1').text()).toEqual('account-recovery.new-password-form.title');
+ });
+
+ it('renders input for new password', () => {
+ expect(newPasswordForm.find('InputField').at(0).props().type).toEqual('password');
+ expect(newPasswordForm.find('InputField').at(0).props().label).toEqual('account-recovery.new-password-form.input-label1');
+ });
+
+ it('renders input to confirm new password', () => {
+ expect(newPasswordForm.find('InputField').at(1).props().type).toEqual('password');
+ expect(newPasswordForm.find('InputField').at(1).props().label).toEqual('account-recovery.new-password-form.input-label2');
+ });
+
+ it('renders submit button', () => {
+ expect(newPasswordForm.find('SubmitButton').props().buttonText).toEqual('account-recovery.button-next');
+ });
+
+ it('returns to previous step on link click', () => {
+ newPasswordForm.find('BackLink').simulate('click');
+ expect(mockPrevious).toHaveBeenCalled();
+ });
+
+ describe('Submit', () => {
+ beforeEach((done) => {
+ mockNext = expect.createSpy().andCall(() => done());
+ newPasswordForm = shallow(
+ <NewPasswordForm t={mockTranslations} previous={mockPrevious} userCode='def234' next={mockNext} />
+ );
+ fetchMock.post('/account-recovery', 200);
+ newPasswordForm.find('InputField[name="new-password"]').simulate('change', { target: { value: '123' } });
+ newPasswordForm.find('InputField[name="confirm-password"]').simulate('change', { target: { value: '456' } });
+ newPasswordForm.find('form').simulate('submit', { preventDefault: expect.createSpy() });
+ });
+
+ it('posts to account recovery', () => {
+ expect(fetchMock.called('/account-recovery')).toBe(true, 'POST was not called');
+ });
+
+ it('sends user code as content', () => {
+ expect(fetchMock.lastOptions('/account-recovery').body).toContain('"userCode":"def234"');
+ });
+
+ it('sends password as content', () => {
+ expect(fetchMock.lastOptions('/account-recovery').body).toContain('"password":"123"');
+ });
+
+ it('sends confirm password as content', () => {
+ expect(fetchMock.lastOptions('/account-recovery').body).toContain('"confirmPassword":"456"');
+ });
+ });
+
+ describe('Password validation', () => {
+ let newPasswordFormInstance;
+
+ beforeEach(() => {
+ newPasswordFormInstance = newPasswordForm.instance();
+ });
+
+ it('verifies initial state', () => {
+ expect(newPasswordFormInstance.state.errorPassword).toEqual('');
+ expect(newPasswordFormInstance.state.errorConfirmPassword).toEqual('');
+ expect(newPasswordForm.find('SubmitButton').props().disabled).toBe(true);
+ });
+
+ context('with valid fields', () => {
+ beforeEach(() => {
+ newPasswordFormInstance.validatePassword('12345678', '12345678');
+ });
+
+ it('does not set error in state', () => {
+ expect(newPasswordFormInstance.state.errorPassword).toEqual('');
+ expect(newPasswordFormInstance.state.errorConfirmPassword).toEqual('');
+ });
+
+ it('enables submit button', () => {
+ expect(newPasswordForm.find('SubmitButton').props().disabled).toBe(false);
+ });
+ });
+
+ context('with invalid password', () => {
+ beforeEach(() => {
+ newPasswordFormInstance.validatePassword('1234', '');
+ });
+
+ it('sets password error in state', () => {
+ expect(newPasswordFormInstance.state.errorPassword).toEqual('account-recovery.new-password-form.error.invalid-password');
+ });
+
+ it('disables submit button', () => {
+ expect(newPasswordForm.find('SubmitButton').props().disabled).toBe(true);
+ });
+ });
+
+ context('with invalid confirm password', () => {
+ beforeEach(() => {
+ newPasswordFormInstance.validatePassword('12345678', '1234');
+ });
+
+ it('sets confirm password error in state', () => {
+ expect(newPasswordFormInstance.state.errorConfirmPassword).toEqual('account-recovery.new-password-form.error.invalid-confirm-password');
+ });
+
+ it('disables submit button', () => {
+ expect(newPasswordForm.find('SubmitButton').props().disabled).toBe(true);
+ });
+ });
+
+ context('with empty fields', () => {
+ it('does not set error in state if both empty', () => {
+ newPasswordFormInstance.validatePassword('', '');
+ expect(newPasswordFormInstance.state.errorPassword).toEqual('');
+ expect(newPasswordFormInstance.state.errorConfirmPassword).toEqual('');
+ });
+
+ it('does not set confirm password error in state if empty', () => {
+ newPasswordFormInstance.validatePassword('12345678', '');
+ expect(newPasswordFormInstance.state.errorConfirmPassword).toEqual('');
+ });
+
+ it('sets confirm password error in state if not empty', () => {
+ newPasswordFormInstance.validatePassword('', '12345678');
+ expect(newPasswordFormInstance.state.errorConfirmPassword).toEqual('account-recovery.new-password-form.error.invalid-confirm-password');
+ });
+
+ it('disables submit button if empty confirm password', () => {
+ newPasswordFormInstance.validatePassword('12345678', '');
+ expect(newPasswordForm.find('SubmitButton').props().disabled).toBe(true);
+ });
+
+ it('disables submit button if empty password', () => {
+ newPasswordFormInstance.validatePassword('', '12345678');
+ expect(newPasswordForm.find('SubmitButton').props().disabled).toBe(true);
+ });
+ });
+
+ it('calls next handler on success', () => {
+ expect(mockNext).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/web-ui/src/account_recovery/page.js b/web-ui/src/account_recovery/page.js
new file mode 100644
index 00000000..94927a16
--- /dev/null
+++ b/web-ui/src/account_recovery/page.js
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from 'react';
+import { translate } from 'react-i18next';
+import DocumentTitle from 'react-document-title';
+import Header from 'src/common/header/header';
+import AdminRecoveryCodeForm from 'src/account_recovery/admin_recovery_code_form/admin_recovery_code_form';
+import UserRecoveryCodeForm from 'src/account_recovery/user_recovery_code_form/user_recovery_code_form';
+import NewPasswordForm from 'src/account_recovery/new_password_form/new_password_form';
+import BackupAccountStep from 'src/account_recovery/backup_account_step/backup_account_step';
+import Footer from 'src/common/footer/footer';
+
+import 'font-awesome/scss/font-awesome.scss';
+import './page.scss';
+
+
+export class Page extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = { step: 0, userCode: '' };
+ }
+
+ nextStep = (event) => {
+ if (event) {
+ event.preventDefault();
+ }
+ this.setState({ step: this.state.step + 1 });
+ }
+
+ previousStep = () => {
+ this.setState({ step: this.state.step - 1 });
+ }
+
+ saveUserCode = (event) => {
+ this.setState({ userCode: event.target.value });
+ }
+
+ steps = () => ({
+ 0: <AdminRecoveryCodeForm next={this.nextStep} />,
+ 1:
+ (<UserRecoveryCodeForm
+ previous={this.previousStep}
+ next={this.nextStep}
+ saveCode={this.saveUserCode}
+ />),
+ 2:
+ (<NewPasswordForm
+ previous={this.previousStep}
+ userCode={this.state.userCode}
+ next={this.nextStep}
+ />),
+ 3: <BackupAccountStep />
+ })
+
+ mainContent = () => this.steps()[this.state.step];
+
+ render() {
+ const t = this.props.t;
+ return (
+ <DocumentTitle title={t('account-recovery.page-title')}>
+ <div className='page'>
+ <Header />
+ <section>
+ <div className='container'>
+ {this.mainContent()}
+ </div>
+ </section>
+ <Footer />
+ </div>
+ </DocumentTitle>
+ );
+ }
+}
+
+Page.propTypes = {
+ t: React.PropTypes.func.isRequired
+};
+
+export default translate('', { wait: true })(Page);
diff --git a/web-ui/src/account_recovery/page.scss b/web-ui/src/account_recovery/page.scss
new file mode 100644
index 00000000..378bf63c
--- /dev/null
+++ b/web-ui/src/account_recovery/page.scss
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+@import "~scss/vendor/reset";
+@import "~scss/base/colors";
+@import "~scss/base/fonts";
+
+html, body, #root {
+ height: 100%;
+}
+
+body, #root {
+ min-height: 100%;
+}
+
+a {
+ text-decoration: none;
+}
+
+.container {
+ background: $white;
+ margin: 3% auto;
+ box-shadow: 0 2px 3px 0 $shadow;
+ width: 84%;
+ padding: 6% 5%;
+}
+
+.page {
+ font-family: "Open Sans", "Microsoft YaHei", "Hiragino Sans GB", "Hiragino Sans GB W3", "微软雅黑", "Helvetica Neue", Arial, sans-serif;
+ background: $dark_blue; /* For browsers that do not support gradients */
+ background: -webkit-linear-gradient(left top, $dark_blue, $middle_blue); /* For Safari 5.1 to 6.0 */
+ background: -o-linear-gradient(bottom right, $dark_blue, $middle_blue); /* For Opera 11.1 to 12.0 */
+ background: -moz-linear-gradient(bottom right, $dark_blue, $middle_blue); /* For Firefox 3.6 to 15 */
+ background: linear-gradient(to bottom right, $dark_blue, $middle_blue); /* Standard syntax */
+ color: $dark_grey_text;
+ min-height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+section {
+ flex: 1 0 auto;
+}
+
+h1 {
+ font-size: 1.3em;
+ font-weight: 600;
+ text-align: center;
+}
+
+p {
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+}
+
+.account-recovery-form {
+ display: flex;
+ flex-direction: column;
+
+ .account-recovery-progress {
+ align-self: center;
+ width: 100%;
+ }
+
+ .input-field-group {
+ margin-top: 0;
+ }
+}
+
+@media only screen and (min-width : 500px) {
+ body {
+ font-size: 1.3em;
+ }
+
+ .account-recovery-form {
+ align-items: center;
+
+ .account-recovery-progress {
+ margin-bottom: 1.4em;
+ width: 80%;
+ }
+
+ h1 {
+ width: 80%;
+ }
+ }
+}
+
+@media only screen and (min-width : 960px) {
+ .container {
+ width: 60%;
+ max-width: 700px;
+ padding: 3em;
+ }
+
+ .account-recovery-form {
+ h1 {
+ max-width: 80%;
+ width: auto;
+ }
+
+ .input-field-group, .submit-button {
+ width: 60%;
+ align-self: center;
+ }
+ }
+}
diff --git a/web-ui/src/account_recovery/page.spec.js b/web-ui/src/account_recovery/page.spec.js
new file mode 100644
index 00000000..8e4ccc33
--- /dev/null
+++ b/web-ui/src/account_recovery/page.spec.js
@@ -0,0 +1,91 @@
+import { shallow } from 'enzyme';
+import expect from 'expect';
+import React from 'react';
+
+import { Page } from 'src/account_recovery/page';
+import Header from 'src/common/header/header';
+import Footer from 'src/common/footer/footer';
+
+import AdminRecoveryCodeFormWrapper from './admin_recovery_code_form/admin_recovery_code_form';
+import UserRecoveryCodeFormWrapper from './user_recovery_code_form/user_recovery_code_form';
+import NewPasswordFormWrapper from './new_password_form/new_password_form';
+import BackupAccountStepWrapper from './backup_account_step/backup_account_step';
+
+describe('Account Recovery Page', () => {
+ let page;
+ let pageInstance;
+
+ beforeEach(() => {
+ const mockTranslations = key => key;
+ page = shallow(<Page t={mockTranslations} />);
+ pageInstance = page.instance();
+ });
+
+ it('renders account recovery page title', () => {
+ expect(page.props().title).toEqual('account-recovery.page-title');
+ });
+
+ it('renders header without logout button', () => {
+ expect(page.find(Header).props().renderLogout).toEqual(false);
+ });
+
+ it('renders footer', () => {
+ expect(page.find(Footer).length).toEqual(1);
+ });
+
+ it('saves user code', () => {
+ pageInstance.saveUserCode({ target: { value: '123' } });
+ expect(pageInstance.state.userCode).toEqual('123');
+ });
+
+ it('prevents default event before next', () => {
+ const eventSpy = expect.createSpy();
+ pageInstance.nextStep({ preventDefault: eventSpy });
+
+ expect(eventSpy).toHaveBeenCalled();
+ });
+
+ context('main content', () => {
+ it('renders admin recovery code form as default form', () => {
+ expect(page.find(AdminRecoveryCodeFormWrapper).length).toEqual(1);
+ expect(page.find(UserRecoveryCodeFormWrapper).length).toEqual(0);
+ expect(page.find(NewPasswordFormWrapper).length).toEqual(0);
+ });
+
+ it('renders user recovery code form when admin code submitted', () => {
+ pageInstance.nextStep();
+
+ expect(page.find(UserRecoveryCodeFormWrapper).length).toEqual(1);
+ });
+
+ it('returns to admin code form on user code form back link', () => {
+ pageInstance.nextStep();
+ pageInstance.previousStep();
+
+ expect(page.find(AdminRecoveryCodeFormWrapper).length).toEqual(1);
+ });
+
+ it('renders new password form when user code submitted', () => {
+ pageInstance.nextStep();
+ pageInstance.nextStep();
+
+ expect(page.find(NewPasswordFormWrapper).length).toEqual(1);
+ });
+
+ it('returns to user code form on new password form back link', () => {
+ pageInstance.nextStep();
+ pageInstance.nextStep();
+ pageInstance.previousStep();
+
+ expect(page.find(UserRecoveryCodeFormWrapper).length).toEqual(1);
+ });
+
+ it('renders backup account form after submitting new password', () => {
+ pageInstance.nextStep();
+ pageInstance.nextStep();
+ pageInstance.nextStep();
+
+ expect(page.find(BackupAccountStepWrapper).length).toEqual(1);
+ });
+ });
+});
diff --git a/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.js b/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.js
new file mode 100644
index 00000000..c39c894d
--- /dev/null
+++ b/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.js
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from 'react';
+import { translate } from 'react-i18next';
+
+import InputField from 'src/common/input_field/input_field';
+import SubmitButton from 'src/common/submit_button/submit_button';
+import BackLink from 'src/common/back_link/back_link';
+
+import './user_recovery_code_form.scss';
+
+export const UserRecoveryCodeForm = ({ t, previous, next, saveCode }) => (
+ <form className='account-recovery-form user-code' onSubmit={next}>
+ <img
+ className='account-recovery-progress'
+ src='/public/images/account-recovery/step_2.svg'
+ alt={t('account-recovery.user-form.image-description')}
+ />
+ <h1>{t('account-recovery.user-form.title')}</h1>
+ <div className='user-code-form-content'>
+ <img
+ className='user-code-image'
+ src='/public/images/account-recovery/codes.svg'
+ alt=''
+ />
+ <p>{t('account-recovery.user-form.description')}</p>
+ </div>
+ <InputField
+ name='user-code' label={t('account-recovery.user-form.input-label')}
+ onChange={saveCode}
+ />
+ <SubmitButton buttonText={t('account-recovery.button-next')} />
+ <BackLink text={t('account-recovery.back')} onClick={previous} />
+ </form>
+);
+
+UserRecoveryCodeForm.propTypes = {
+ t: React.PropTypes.func.isRequired,
+ previous: React.PropTypes.func.isRequired,
+ next: React.PropTypes.func.isRequired,
+ saveCode: React.PropTypes.func.isRequired
+};
+
+export default translate('', { wait: true })(UserRecoveryCodeForm);
diff --git a/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.scss b/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.scss
new file mode 100644
index 00000000..55f5621b
--- /dev/null
+++ b/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.scss
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+.user-code-form-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.user-code-image {
+ margin: 1em 0;
+ align-self: center;
+ height: 4em;
+}
+
+@media only screen and (min-width : 500px) {
+ .user-code-form-content {
+ flex-direction: row;
+ width: 80%;
+ }
+
+ .user-code-image {
+ margin-right: 1.6em;
+ }
+}
diff --git a/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.spec.js b/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.spec.js
new file mode 100644
index 00000000..386c3a19
--- /dev/null
+++ b/web-ui/src/account_recovery/user_recovery_code_form/user_recovery_code_form.spec.js
@@ -0,0 +1,55 @@
+import { shallow } from 'enzyme';
+import expect from 'expect';
+import React from 'react';
+import { UserRecoveryCodeForm } from './user_recovery_code_form';
+
+describe('UserRecoveryCodeForm', () => {
+ let userRecoveryCodeForm;
+ let mockNext;
+ let mockPrevious;
+ let mockSaveCode;
+
+ beforeEach(() => {
+ const mockTranslations = key => key;
+ mockNext = expect.createSpy();
+ mockPrevious = expect.createSpy();
+ mockSaveCode = expect.createSpy();
+ userRecoveryCodeForm = shallow(
+ <UserRecoveryCodeForm
+ t={mockTranslations} next={mockNext}
+ previous={mockPrevious} saveCode={mockSaveCode}
+ />
+ );
+ });
+
+ it('renders title for user recovery code', () => {
+ expect(userRecoveryCodeForm.find('h1').text()).toEqual('account-recovery.user-form.title');
+ });
+
+ it('renders description', () => {
+ expect(userRecoveryCodeForm.find('p').text()).toEqual('account-recovery.user-form.description');
+ });
+
+ it('renders input for user code', () => {
+ expect(userRecoveryCodeForm.find('InputField').props().label).toEqual('account-recovery.user-form.input-label');
+ });
+
+ it('renders submit button', () => {
+ expect(userRecoveryCodeForm.find('SubmitButton').props().buttonText).toEqual('account-recovery.button-next');
+ });
+
+ it('submits form to next step', () => {
+ userRecoveryCodeForm.find('form').simulate('submit');
+ expect(mockNext).toHaveBeenCalled();
+ });
+
+ it('returns to previous step on link click', () => {
+ userRecoveryCodeForm.find('BackLink').simulate('click');
+ expect(mockPrevious).toHaveBeenCalled();
+ });
+
+ it('saves code on input change', () => {
+ userRecoveryCodeForm.find('InputField').simulate('change', '123');
+ expect(mockSaveCode).toHaveBeenCalledWith('123');
+ });
+});
diff --git a/web-ui/src/backup_account/backup_email/backup_email.js b/web-ui/src/backup_account/backup_email/backup_email.js
index 09863950..ac64f02e 100644
--- a/web-ui/src/backup_account/backup_email/backup_email.js
+++ b/web-ui/src/backup_account/backup_email/backup_email.js
@@ -18,10 +18,12 @@
import 'isomorphic-fetch';
import React from 'react';
import { translate } from 'react-i18next';
+import validator from 'validator';
+
+import { submitForm } from 'src/common/util';
import SubmitButton from 'src/common/submit_button/submit_button';
import InputField from 'src/common/input_field/input_field';
-import validator from 'validator';
-import browser from 'helpers/browser';
+import BackLink from 'src/common/back_link/back_link';
import './backup_email.scss';
@@ -29,7 +31,7 @@ export class BackupEmail extends React.Component {
constructor(props) {
super(props);
- this.state = { error: '', submitButtonDisabled: true };
+ this.state = { error: '', submitButtonDisabled: true, backupEmail: '' };
}
validateEmail = (event) => {
@@ -40,21 +42,20 @@ export class BackupEmail extends React.Component {
error: !emptyEmail && !validEmail ? t('backup-account.backup-email.error.invalid-email') : '',
submitButtonDisabled: !validEmail || emptyEmail
});
- }
+ };
submitHandler = (event) => {
- event.preventDefault();
+ submitForm(event, '/backup-account', {
+ backupEmail: this.state.backupEmail
+ }).then((response) => {
+ if (response.ok) this.props.onSubmit('success');
+ else this.props.onSubmit('error');
+ });
+ };
- fetch('/backup-account', {
- credentials: 'same-origin',
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- csrftoken: [browser.getCookie('XSRF-TOKEN')]
- })
- }).then(() => this.props.onSubmit('success'));
+ handleChange = (event) => {
+ this.setState({ backupEmail: event.target.value });
+ this.validateEmail(event);
}
render() {
@@ -70,14 +71,12 @@ export class BackupEmail extends React.Component {
<h1>{t('backup-account.backup-email.title')}</h1>
<p>{t('backup-account.backup-email.paragraph1')}</p>
<p>{t('backup-account.backup-email.paragraph2')}</p>
- <InputField name='email' label={t('backup-account.backup-email.input-label')} errorText={this.state.error} onChange={this.validateEmail} />
+ <InputField name='email' value={this.state.backupEmail} label={t('backup-account.backup-email.input-label')} errorText={this.state.error} onChange={this.handleChange} />
<SubmitButton buttonText={t('backup-account.backup-email.button')} disabled={this.state.submitButtonDisabled} />
- <div className='link-content'>
- <a href='/' className='link'>
- <i className='fa fa-angle-left' aria-hidden='true' />
- <span>{t('back-to-inbox')}</span>
- </a>
- </div>
+ <BackLink
+ href='/'
+ text={t('back-to-inbox')}
+ />
</form>
</div>
);
diff --git a/web-ui/src/backup_account/backup_email/backup_email.spec.js b/web-ui/src/backup_account/backup_email/backup_email.spec.js
index 48199738..a34afa06 100644
--- a/web-ui/src/backup_account/backup_email/backup_email.spec.js
+++ b/web-ui/src/backup_account/backup_email/backup_email.spec.js
@@ -3,12 +3,12 @@ import expect from 'expect';
import React from 'react';
import fetchMock from 'fetch-mock';
import { BackupEmail } from 'src/backup_account/backup_email/backup_email';
-import browser from 'helpers/browser';
describe('BackupEmail', () => {
let backupEmail;
let mockOnSubmit;
let mockTranslations;
+ let backupEmailInstance;
beforeEach(() => {
mockOnSubmit = expect.createSpy();
@@ -30,8 +30,6 @@ describe('BackupEmail', () => {
});
describe('Email validation', () => {
- let backupEmailInstance;
-
beforeEach(() => {
backupEmailInstance = backupEmail.instance();
});
@@ -84,42 +82,64 @@ describe('BackupEmail', () => {
});
});
+ describe('Email changing handler', () => {
+ beforeEach(() => {
+ backupEmailInstance = backupEmail.instance();
+ });
+
+ it('sets user backup email in the state', () => {
+ backupEmailInstance.handleChange({ target: { value: 'test@test.com' } });
+ expect(backupEmailInstance.state.backupEmail).toEqual('test@test.com');
+ });
+ });
+
describe('Submit', () => {
let preventDefaultSpy;
- beforeEach((done) => {
- mockOnSubmit = expect.createSpy().andCall(() => done());
+ beforeEach(() => {
preventDefaultSpy = expect.createSpy();
- expect.spyOn(browser, 'getCookie').andReturn('abc123');
+ });
- backupEmail = shallow(<BackupEmail t={mockTranslations} onSubmit={mockOnSubmit} />);
+ context('on success', () => {
+ beforeEach((done) => {
+ mockOnSubmit = expect.createSpy().andCall(() => done());
- fetchMock.post('/backup-account', 204);
- backupEmail.find('form').simulate('submit', { preventDefault: preventDefaultSpy });
- });
+ fetchMock.post('/backup-account', 204);
+ backupEmail = shallow(<BackupEmail t={mockTranslations} onSubmit={mockOnSubmit} />);
- it('posts backup email', () => {
- expect(fetchMock.called('/backup-account')).toBe(true, 'Backup account POST was not called');
- });
+ backupEmail.find('InputField').simulate('change', { target: { value: 'test@test.com' } });
+ backupEmail.find('form').simulate('submit', { preventDefault: preventDefaultSpy });
+ });
- it('sends csrftoken as content', () => {
- expect(fetchMock.lastOptions('/backup-account').body).toContain('"csrftoken":["abc123"]');
- });
+ it('posts backup email', () => {
+ expect(fetchMock.called('/backup-account')).toBe(true, 'Backup account POST was not called');
+ });
- it('sends content-type header', () => {
- expect(fetchMock.lastOptions('/backup-account').headers['Content-Type']).toEqual('application/json');
- });
+ it('sends user email as content', () => {
+ expect(fetchMock.lastOptions('/backup-account').body).toContain('"backupEmail":"test@test.com"');
+ });
- it('sends same origin headers', () => {
- expect(fetchMock.lastOptions('/backup-account').credentials).toEqual('same-origin');
+ it('calls onSubmit from props with success', () => {
+ expect(mockOnSubmit).toHaveBeenCalledWith('success');
+ });
});
- it('prevents default call to refresh page', () => {
- expect(preventDefaultSpy).toHaveBeenCalled();
- });
+ context('on error', () => {
+ beforeEach((done) => {
+ mockOnSubmit = expect.createSpy().andCall(() => done());
+
+ fetchMock.post('/backup-account', 500);
+ backupEmail = shallow(<BackupEmail t={mockTranslations} onSubmit={mockOnSubmit} />);
+ backupEmail.find('form').simulate('submit', { preventDefault: preventDefaultSpy });
+ });
- it('calls onSubmit from props when success', () => {
- expect(mockOnSubmit).toHaveBeenCalledWith('success');
+ it('calls onSubmit from props with error', () => {
+ expect(mockOnSubmit).toHaveBeenCalledWith('error');
+ });
});
});
+
+ afterEach(() => {
+ fetchMock.restore();
+ });
});
diff --git a/web-ui/src/backup_account/confirmation/confirmation.js b/web-ui/src/backup_account/confirmation/confirmation.js
index 41637dab..49b0d19c 100644
--- a/web-ui/src/backup_account/confirmation/confirmation.js
+++ b/web-ui/src/backup_account/confirmation/confirmation.js
@@ -18,6 +18,7 @@
import React from 'react';
import { translate } from 'react-i18next';
import SubmitButton from 'src/common/submit_button/submit_button';
+import BackLink from 'src/common/back_link/back_link';
import './confirmation.scss';
@@ -29,12 +30,10 @@ export const Confirmation = ({ t }) => (
<form action='/'>
<SubmitButton buttonText={t('backup-account.confirmation.button')} type='submit' />
</form>
- <div className='link-content'>
- <a href='/backup-account' className='link'>
- <i className='fa fa-angle-left' aria-hidden='true' />
- <span>{t('backup-account.confirmation.retry-button')}</span>
- </a>
- </div>
+ <BackLink
+ href='/backup-account'
+ text={t('backup-account.confirmation.retry-button')}
+ />
</div>
);
diff --git a/web-ui/src/backup_account/confirmation/confirmation.spec.js b/web-ui/src/backup_account/confirmation/confirmation.spec.js
index 291d156d..7a6f38ca 100644
--- a/web-ui/src/backup_account/confirmation/confirmation.spec.js
+++ b/web-ui/src/backup_account/confirmation/confirmation.spec.js
@@ -20,10 +20,10 @@ describe('Confirmation', () => {
});
it('renders confirmation retry button', () => {
- expect(page.find('a').text()).toEqual('backup-account.confirmation.retry-button');
+ expect(page.find('BackLink').props().text).toEqual('backup-account.confirmation.retry-button');
});
it('retries button redirects to backup account', () => {
- expect(page.find('a').props().href).toEqual('/backup-account');
+ expect(page.find('BackLink').props().href).toEqual('/backup-account');
});
});
diff --git a/web-ui/src/backup_account/page.js b/web-ui/src/backup_account/page.js
index 49e4b316..e7663205 100644
--- a/web-ui/src/backup_account/page.js
+++ b/web-ui/src/backup_account/page.js
@@ -22,6 +22,7 @@ import Footer from 'src/common/footer/footer';
import Header from 'src/common/header/header';
import BackupEmail from 'src/backup_account/backup_email/backup_email';
import Confirmation from 'src/backup_account/confirmation/confirmation';
+import SnackbarNotification from 'src/common/snackbar_notification/snackbar_notification';
import 'font-awesome/scss/font-awesome.scss';
import './page.scss';
@@ -36,22 +37,30 @@ export class Page extends React.Component {
saveBackupEmail = (status) => {
this.setState({ status });
- }
+ };
mainContent = () => {
if (this.state.status === 'success') return <Confirmation />;
return <BackupEmail onSubmit={this.saveBackupEmail} />;
};
+ showSnackbarOnError = (t) => {
+ if (this.state.status === 'error') {
+ return <SnackbarNotification message={t('backup-account.error.submit-error')} isError />;
+ }
+ return undefined; // To satisfy eslint error - consistent-return
+ };
+
render() {
const t = this.props.t;
return (
<DocumentTitle title={t('backup-account.page-title')}>
<div className='page'>
- <Header />
+ <Header renderLogout />
<section>
{this.mainContent()}
</section>
+ {this.showSnackbarOnError(t)}
<Footer />
</div>
</DocumentTitle>
diff --git a/web-ui/src/backup_account/page.scss b/web-ui/src/backup_account/page.scss
index 71e3f074..d4f1f887 100644
--- a/web-ui/src/backup_account/page.scss
+++ b/web-ui/src/backup_account/page.scss
@@ -64,20 +64,6 @@ p {
margin-bottom: 0.5em;
}
-.link {
- color: $dark_blue;
- font-style: italic;
- font-size: 0.8em;
-
- .fa {
- font-size: 1.6em;
- position: relative;
- top: 3px;
- margin-right: 0.3em;
- }
-
-}
-
@media only screen and (min-width : 500px) {
body {
font-size: 1.3em;
diff --git a/web-ui/src/backup_account/page.spec.js b/web-ui/src/backup_account/page.spec.js
index bd7bb884..8c014ee4 100644
--- a/web-ui/src/backup_account/page.spec.js
+++ b/web-ui/src/backup_account/page.spec.js
@@ -4,6 +4,8 @@ import React from 'react';
import { Page } from 'src/backup_account/page';
import BackupEmail from 'src/backup_account/backup_email/backup_email';
import Confirmation from 'src/backup_account/confirmation/confirmation';
+import SnackbarNotification from 'src/common/snackbar_notification/snackbar_notification';
+import Header from 'src/common/header/header';
describe('BackupAccount', () => {
let page;
@@ -17,6 +19,10 @@ describe('BackupAccount', () => {
expect(page.props().title).toEqual('backup-account.page-title');
});
+ it('renders header with logout button', () => {
+ expect(page.find(Header).props().renderLogout).toEqual(true);
+ });
+
describe('save backup email', () => {
let pageInstance;
@@ -41,5 +47,29 @@ describe('BackupAccount', () => {
pageInstance.saveBackupEmail('success');
expect(page.find(Confirmation).length).toEqual(1);
});
+
+ context('on submit error', () => {
+ beforeEach(() => {
+ pageInstance.saveBackupEmail('error');
+ });
+
+ it('returns snackbar component on error', () => {
+ const snackbar = pageInstance.showSnackbarOnError(pageInstance.props.t);
+ expect(snackbar).toEqual(<SnackbarNotification message='backup-account.error.submit-error' isError />);
+ });
+
+ it('returns nothing when there is no error', () => {
+ pageInstance.saveBackupEmail('success');
+ const snackbar = pageInstance.showSnackbarOnError(pageInstance.props.t);
+ expect(snackbar).toEqual(undefined);
+ });
+
+ it('renders snackbar notification on error', () => {
+ const snackbar = page.find(SnackbarNotification);
+ expect(snackbar).toExist();
+ expect(snackbar.props().message).toEqual('backup-account.error.submit-error');
+ expect(snackbar.props().isError).toEqual(true);
+ });
+ });
});
});
diff --git a/web-ui/src/common/back_link/back_link.js b/web-ui/src/common/back_link/back_link.js
new file mode 100644
index 00000000..bb5ffbea
--- /dev/null
+++ b/web-ui/src/common/back_link/back_link.js
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from 'react';
+
+import './back_link.scss';
+
+const icon = <i className='fa fa-angle-left' aria-hidden='true' />;
+
+const button = (text, options) => (
+ <button className='link' {...options}>{icon}<span>{text}</span></button>
+);
+
+const link = (text, options) => (
+ <a className='link' {...options}>{icon}<span>{text}</span></a>
+);
+
+const BackLink = ({ text, ...other }) => (
+ <div className='link-content'>
+ { other.href ? link(text, other) : button(text, other) }
+ </div>
+);
+
+BackLink.propTypes = {
+ text: React.PropTypes.string.isRequired
+};
+
+export default BackLink;
diff --git a/web-ui/src/common/back_link/back_link.scss b/web-ui/src/common/back_link/back_link.scss
new file mode 100644
index 00000000..5541d9d9
--- /dev/null
+++ b/web-ui/src/common/back_link/back_link.scss
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+@import "~scss/base/colors";
+
+.link {
+ color: $dark_blue;
+ font-style: italic;
+ font-size: 0.8em;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ background: transparent;
+
+ .fa {
+ font-size: 1.6em;
+ position: relative;
+ top: 3px;
+ margin-right: 0.3em;
+ }
+}
diff --git a/web-ui/src/common/back_link/back_link.spec.js b/web-ui/src/common/back_link/back_link.spec.js
new file mode 100644
index 00000000..5f49a6f9
--- /dev/null
+++ b/web-ui/src/common/back_link/back_link.spec.js
@@ -0,0 +1,41 @@
+import { shallow } from 'enzyme';
+import expect from 'expect';
+import React from 'react';
+import BackLink from 'src/common/back_link/back_link';
+
+describe('BackLink', () => {
+ context('as link', () => {
+ let backLink;
+
+ beforeEach(() => {
+ backLink = shallow(<BackLink text='Back to inbox' href='/' />);
+ });
+
+ it('renders link with text', () => {
+ expect(backLink.find('a').text()).toEqual('Back to inbox');
+ });
+
+ it('adds link action', () => {
+ expect(backLink.find('a').props().href).toEqual('/');
+ });
+ });
+
+ context('as button', () => {
+ let backLink;
+ let mockClick;
+
+ beforeEach(() => {
+ mockClick = expect.createSpy();
+ backLink = shallow(<BackLink text='Back to inbox' onClick={mockClick} />);
+ });
+
+ it('renders button with text', () => {
+ expect(backLink.find('button').text()).toEqual('Back to inbox');
+ });
+
+ it('adds button click event', () => {
+ backLink.find('button').simulate('click');
+ expect(mockClick).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/web-ui/src/common/header/header.js b/web-ui/src/common/header/header.js
index 715d54c6..3ad924c0 100644
--- a/web-ui/src/common/header/header.js
+++ b/web-ui/src/common/header/header.js
@@ -19,7 +19,7 @@ import React from 'react';
import Logout from 'src/common/logout/logout';
import './header.scss';
-export const Header = () => (
+export const Header = ({ renderLogout }) => (
<header className='header-wrapper'>
<div className='header-content'>
<a href='/'>
@@ -30,10 +30,18 @@ export const Header = () => (
/>
</a>
<div className='header-icons'>
- <Logout />
+ { renderLogout ? <Logout /> : <div /> }
</div>
</div>
</header>
);
+Header.propTypes = {
+ renderLogout: React.PropTypes.bool
+};
+
+Header.defaultProps = {
+ renderLogout: false
+};
+
export default Header;
diff --git a/web-ui/src/common/header/header.spec.js b/web-ui/src/common/header/header.spec.js
index 81a952c7..0c11713b 100644
--- a/web-ui/src/common/header/header.spec.js
+++ b/web-ui/src/common/header/header.spec.js
@@ -11,11 +11,16 @@ describe('Header', () => {
header = shallow(<Header />);
});
- it('renders the header containing the logout button', () => {
- expect(header.find('header').find(Logout)).toExist();
- });
-
it('renders the header pixelated logo', () => {
expect(header.find('header').find('img').props().alt).toEqual('Pixelated');
});
+
+ it('renders the header containing the logout button when renderLogout is true', () => {
+ header = shallow(<Header renderLogout />);
+ expect(header.find('header').find(Logout).length).toEqual(1);
+ });
+
+ it('hides logout button when renderLogout is false', () => {
+ expect(header.find('header').find(Logout).length).toEqual(0);
+ });
});
diff --git a/web-ui/src/common/link_button/link_button.js b/web-ui/src/common/link_button/link_button.js
new file mode 100644
index 00000000..e7fd80b0
--- /dev/null
+++ b/web-ui/src/common/link_button/link_button.js
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from 'react';
+import FlatButton from 'material-ui/FlatButton';
+
+import './link_button.scss';
+
+const labelStyle = {
+ textTransform: 'none',
+ color: 'inherit',
+ fontSize: 'inherit',
+ width: '100%',
+ padding: '0'
+};
+
+const linkButtonStyle = {
+ color: 'inherit',
+ borderRadius: '0',
+ minHeight: '36px',
+ height: 'auto',
+ lineHeight: '20px',
+ padding: '12px 0'
+};
+
+const LinkButton = ({ buttonText, href }) => (
+ <div className='link-button'>
+ <FlatButton
+ href={href}
+ containerElement='a'
+ label={buttonText}
+ labelStyle={labelStyle}
+ hoverColor={'#ff9c00'}
+ style={linkButtonStyle}
+ />
+ </div>
+);
+
+LinkButton.propTypes = {
+ buttonText: React.PropTypes.string.isRequired,
+ href: React.PropTypes.string.isRequired
+};
+
+export default LinkButton;
diff --git a/web-ui/src/common/link_button/link_button.scss b/web-ui/src/common/link_button/link_button.scss
new file mode 100644
index 00000000..3b4a534c
--- /dev/null
+++ b/web-ui/src/common/link_button/link_button.scss
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+@import "~scss/base/colors";
+
+.link-button > a {
+ width: 100%;
+}
+
+.link-button {
+ width: 100%;
+ margin-top: 2em;
+ border: 2px solid $light_orange;
+ border-radius: 2px;
+ font-size: 1em;
+ color: $light_orange;
+ &:hover {
+ color: $white;
+ }
+}
+
+@media only screen and (min-width : 500px) {
+ .link-button {
+ width: 70%;
+ align-self: center;
+ }
+}
+
+@media only screen and (min-width : 960px) {
+ .link-button {
+ width: 300px;
+ margin-bottom: 1em;
+ font-size: 0.8em;
+ }
+}
diff --git a/web-ui/src/common/link_button/link_button.spec.js b/web-ui/src/common/link_button/link_button.spec.js
new file mode 100644
index 00000000..945afffe
--- /dev/null
+++ b/web-ui/src/common/link_button/link_button.spec.js
@@ -0,0 +1,20 @@
+import { shallow } from 'enzyme';
+import expect from 'expect';
+import React from 'react';
+import LinkButton from 'src/common/link_button/link_button';
+
+describe('LinkButton', () => {
+ let linkButton;
+
+ beforeEach(() => {
+ linkButton = shallow(<LinkButton buttonText='Go To Link' href='/some-link' />);
+ });
+
+ it('renders link button with given button text', () => {
+ expect(linkButton.find('FlatButton').props().label).toEqual('Go To Link');
+ });
+
+ it('renders link button with given href', () => {
+ expect(linkButton.find('FlatButton').props().href).toEqual('/some-link');
+ });
+});
diff --git a/web-ui/src/common/snackbar_notification/snackbar_notification.js b/web-ui/src/common/snackbar_notification/snackbar_notification.js
new file mode 100644
index 00000000..8a7e5094
--- /dev/null
+++ b/web-ui/src/common/snackbar_notification/snackbar_notification.js
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from 'react';
+import Snackbar from 'material-ui/Snackbar';
+import { red500, blue500 } from 'material-ui/styles/colors';
+
+import './snackbar_notification.scss';
+
+const notificationStyle = () => ({
+ top: 0,
+ left: 'auto',
+ bottom: 'auto',
+ alignSelf: 'center',
+ transform: 'translate3d(0, 0px, 0)'
+});
+
+const contentStyle = {
+ textAlign: 'center'
+};
+
+const getStyleByType = (isError) => {
+ const style = { height: 'auto' };
+ style.backgroundColor = isError ? red500 : blue500;
+ return style;
+};
+
+const SnackbarNotification = ({ message, isError = false, autoHideDuration = 5000 }) => (
+ <Snackbar
+ id='snackbar'
+ open
+ bodyStyle={getStyleByType(isError)}
+ message={message}
+ autoHideDuration={autoHideDuration}
+ contentStyle={contentStyle}
+ style={notificationStyle()}
+ />
+);
+
+SnackbarNotification.propTypes = {
+ message: React.PropTypes.string.isRequired,
+ isError: React.PropTypes.bool,
+ autoHideDuration: React.PropTypes.number
+};
+
+SnackbarNotification.defaultProps = {
+ isError: false,
+ autoHideDuration: 5000
+};
+
+export default SnackbarNotification;
diff --git a/web-ui/src/common/snackbar_notification/snackbar_notification.scss b/web-ui/src/common/snackbar_notification/snackbar_notification.scss
new file mode 100644
index 00000000..e37ba4ae
--- /dev/null
+++ b/web-ui/src/common/snackbar_notification/snackbar_notification.scss
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2017 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+//TODO: Refer to Issue - https://github.com/callemall/material-ui/issues/3860
+#snackbar > div {
+ line-height: 20px !important;
+ padding: 24px !important;
+}
diff --git a/web-ui/src/common/snackbar_notification/snackbar_notification.spec.js b/web-ui/src/common/snackbar_notification/snackbar_notification.spec.js
new file mode 100644
index 00000000..24b79e71
--- /dev/null
+++ b/web-ui/src/common/snackbar_notification/snackbar_notification.spec.js
@@ -0,0 +1,31 @@
+import { shallow } from 'enzyme';
+import expect from 'expect';
+import React from 'react';
+import SnackbarNotification from 'src/common/snackbar_notification/snackbar_notification';
+import Snackbar from 'material-ui/Snackbar';
+import { red500 } from 'material-ui/styles/colors';
+
+describe('SnackbarNotification', () => {
+ let snackbarNotification;
+
+ beforeEach(() => {
+ snackbarNotification = shallow(<SnackbarNotification message={'Error Message'} isError />);
+ });
+
+ it('renders snackbar with error message', () => {
+ expect(snackbarNotification.find(Snackbar).props().message).toEqual('Error Message');
+ });
+
+ it('renders snackbar with open as true', () => {
+ expect(snackbarNotification.find(Snackbar).props().open).toEqual(true);
+ });
+
+ it('renders snackbar with error body style', () => {
+ expect(snackbarNotification.find(Snackbar).props().bodyStyle)
+ .toEqual({ height: 'auto', backgroundColor: red500 });
+ });
+
+ it('renders snackbar with default auto-hide duration', () => {
+ expect(snackbarNotification.find(Snackbar).props().autoHideDuration).toEqual(5000);
+ });
+});
diff --git a/web-ui/src/common/submit_button/submit_button.js b/web-ui/src/common/submit_button/submit_button.js
index 1224c7bd..f77a5596 100644
--- a/web-ui/src/common/submit_button/submit_button.js
+++ b/web-ui/src/common/submit_button/submit_button.js
@@ -30,7 +30,7 @@ const buttonStyle = {
height: '48px'
};
-const SubmitButton = ({ buttonText, disabled = false }) => (
+const SubmitButton = ({ buttonText, disabled = false, ...other }) => (
<div className='submit-button'>
<RaisedButton
type='submit'
@@ -41,6 +41,7 @@ const SubmitButton = ({ buttonText, disabled = false }) => (
overlayStyle={buttonStyle}
fullWidth
primary
+ {...other}
/>
</div>
);
diff --git a/web-ui/src/common/util.js b/web-ui/src/common/util.js
index effb3d9c..c70a8444 100644
--- a/web-ui/src/common/util.js
+++ b/web-ui/src/common/util.js
@@ -1,8 +1,27 @@
+import browser from 'helpers/browser';
+
export const hasQueryParameter = (param) => {
const decodedUri = decodeURIComponent(window.location.search.substring(1));
return !(decodedUri.split('&').indexOf(param) < 0);
};
+export const submitForm = (event, url, body = {}) => {
+ event.preventDefault();
+
+ return fetch(url, {
+ credentials: 'same-origin',
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ csrftoken: [browser.getCookie('XSRF-TOKEN')],
+ ...body
+ })
+ });
+};
+
export default {
- hasQueryParameter
+ hasQueryParameter,
+ submitForm
};
diff --git a/web-ui/src/common/util.spec.js b/web-ui/src/common/util.spec.js
index 805d9dd5..a79859a0 100644
--- a/web-ui/src/common/util.spec.js
+++ b/web-ui/src/common/util.spec.js
@@ -1,4 +1,7 @@
import expect from 'expect';
+import fetchMock from 'fetch-mock';
+
+import browser from 'helpers/browser';
import Util from 'src/common/util';
describe('Utils', () => {
@@ -17,4 +20,36 @@ describe('Utils', () => {
expect(Util.hasQueryParameter('error')).toBe(false);
});
});
+
+ describe('submitForm', () => {
+ const event = {};
+
+ beforeEach(() => {
+ event.preventDefault = expect.createSpy();
+ expect.spyOn(browser, 'getCookie').andReturn('abc123');
+
+ fetchMock.post('/some-url', 200);
+ Util.submitForm(event, '/some-url', { userCode: '123' });
+ });
+
+ it('sends csrftoken as content', () => {
+ expect(fetchMock.lastOptions('/some-url').body).toContain('"csrftoken":["abc123"]');
+ });
+
+ it('sends body as content', () => {
+ expect(fetchMock.lastOptions('/some-url').body).toContain('"userCode":"123"');
+ });
+
+ it('sends content-type header', () => {
+ expect(fetchMock.lastOptions('/some-url').headers['Content-Type']).toEqual('application/json');
+ });
+
+ it('sends same origin headers', () => {
+ expect(fetchMock.lastOptions('/some-url').credentials).toEqual('same-origin');
+ });
+
+ it('prevents default call to refresh page', () => {
+ expect(event.preventDefault).toHaveBeenCalled();
+ });
+ });
});
diff --git a/web-ui/src/login/opensans.css b/web-ui/src/login/opensans.css
index 8795bdf7..74f98822 100644
--- a/web-ui/src/login/opensans.css
+++ b/web-ui/src/login/opensans.css
@@ -2,68 +2,68 @@
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
- src: local("Open Sans Light"), local("OpenSans-Light"), url("/assets/fonts/OpenSans-Light.woff") format("woff");
+ src: local("Open Sans Light"), local("OpenSans-Light"), url("fonts/OpenSans-Light.woff") format("woff");
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
- src: local("Open Sans"), local("OpenSans"), url("/assets/fonts/OpenSans.woff") format("woff");
+ src: local("Open Sans"), local("OpenSans"), url("fonts/OpenSans.woff") format("woff");
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
- src: local("Open Sans Semibold"), local("OpenSans-Semibold"), url("/assets/fonts/OpenSans-Semibold.woff") format("woff");
+ src: local("Open Sans Semibold"), local("OpenSans-Semibold"), url("fonts/OpenSans-Semibold.woff") format("woff");
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
- src: local("Open Sans Bold"), local("OpenSans-Bold"), url("/assets/fonts/OpenSans-Bold.woff") format("woff");
+ src: local("Open Sans Bold"), local("OpenSans-Bold"), url("fonts/OpenSans-Bold.woff") format("woff");
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
- src: local("Open Sans Extrabold"), local("OpenSans-Extrabold"), url("/assets/fonts/OpenSans-Extrabold.woff") format("woff");
+ src: local("Open Sans Extrabold"), local("OpenSans-Extrabold"), url("fonts/OpenSans-Extrabold.woff") format("woff");
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
- src: local("Open Sans Light Italic"), local("OpenSansLight-Italic"), url("/assets/fonts/OpenSansLight-Italic.woff") format("woff");
+ src: local("Open Sans Light Italic"), local("OpenSansLight-Italic"), url("fonts/OpenSansLight-Italic.woff") format("woff");
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
- src: local("Open Sans Italic"), local("OpenSans-Italic"), url("/assets/fonts/OpenSans-Italic.woff") format("woff");
+ src: local("Open Sans Italic"), local("OpenSans-Italic"), url("fonts/OpenSans-Italic.woff") format("woff");
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
- src: local("Open Sans Semibold Italic"), local("OpenSans-SemiboldItalic"), url("/assets/fonts/OpenSans-SemiboldItalic.woff ") format("woff");
+ src: local("Open Sans Semibold Italic"), local("OpenSans-SemiboldItalic"), url("fonts/OpenSans-SemiboldItalic.woff ") format("woff");
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
- src: local("Open Sans Bold Italic"), local("OpenSans-BoldItalic"), url("/assets/fonts/OpenSans-BoldItalic.woff") format("woff");
+ src: local("Open Sans Bold Italic"), local("OpenSans-BoldItalic"), url("fonts/OpenSans-BoldItalic.woff") format("woff");
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 800;
- src: local("Open Sans Extrabold Italic"), local("OpenSans-ExtraboldItalic"), url("/assets/fonts/OpenSans-ExtraboldItalic.woff") format("woff");
+ src: local("Open Sans Extrabold Italic"), local("OpenSans-ExtraboldItalic"), url("fonts/OpenSans-ExtraboldItalic.woff") format("woff");
}
diff --git a/web-ui/test/integration/account_recovery.spec.js b/web-ui/test/integration/account_recovery.spec.js
new file mode 100644
index 00000000..708b693f
--- /dev/null
+++ b/web-ui/test/integration/account_recovery.spec.js
@@ -0,0 +1,59 @@
+import { mount } from 'enzyme';
+import expect from 'expect';
+import React from 'react';
+import App from 'src/common/app';
+import AccountRecoveryPage from 'src/account_recovery/page';
+import testI18n from './i18n';
+
+describe('Account Recovery Page', () => {
+ context('New password validation', () => {
+ let app;
+ let accountRecoveryPage;
+
+ beforeEach(() => {
+ app = mount(<App i18n={testI18n} child={<AccountRecoveryPage />} />);
+ accountRecoveryPage = app.find('Page');
+ accountRecoveryPage.find('form').simulate('submit');
+ accountRecoveryPage.find('form').simulate('submit');
+ });
+
+ it('shows no validation error with valid password', () => {
+ accountRecoveryPage.find('input[name="new-password"]').simulate('change', { target: { value: '12345678' } });
+ // workaround because of an enzyme bug https://github.com/airbnb/enzyme/issues/534
+ const inputField = accountRecoveryPage.findWhere(element => element.props().name === 'new-password').find('InputField');
+ expect(inputField.props().errorText).toEqual('');
+ });
+
+ it('shows validation error with invalid password', () => {
+ accountRecoveryPage.find('input[name="new-password"]').simulate('change', { target: { value: '1234' } });
+ const inputField = accountRecoveryPage.findWhere(element => element.props().name === 'new-password').find('InputField');
+ expect(inputField.props().errorText).toEqual('A better password has at least 8 characters');
+ });
+
+ it('shows no validation error with valid confirm password', () => {
+ accountRecoveryPage.find('input[name="new-password"]').simulate('change', { target: { value: '12345678' } });
+ accountRecoveryPage.find('input[name="confirm-password"]').simulate('change', { target: { value: '12345678' } });
+ const inputField = accountRecoveryPage.findWhere(element => element.props().name === 'confirm-password').find('InputField');
+ expect(inputField.props().errorText).toEqual('');
+ });
+
+ it('shows validation error with invalid confirm password', () => {
+ accountRecoveryPage.find('input[name="new-password"]').simulate('change', { target: { value: '12345678' } });
+ accountRecoveryPage.find('input[name="confirm-password"]').simulate('change', { target: { value: '1234' } });
+ const inputField = accountRecoveryPage.findWhere(element => element.props().name === 'confirm-password').find('InputField');
+ expect(inputField.props().errorText).toEqual('Password and confirmation don\'t match');
+ });
+
+ it('disables button if empty fields', () => {
+ accountRecoveryPage.find('input[name="new-password"]').simulate('change', { target: { value: '' } });
+ accountRecoveryPage.find('input[name="confirm-password"]').simulate('change', { target: { value: '' } });
+ expect(accountRecoveryPage.find('SubmitButton').props().disabled).toEqual(true);
+ });
+
+ it('enables button if valid fields', () => {
+ accountRecoveryPage.find('input[name="new-password"]').simulate('change', { target: { value: '12345678' } });
+ accountRecoveryPage.find('input[name="confirm-password"]').simulate('change', { target: { value: '12345678' } });
+ expect(accountRecoveryPage.find('SubmitButton').props().disabled).toEqual(false);
+ });
+ });
+});
diff --git a/web-ui/test/integration/translations.spec.js b/web-ui/test/integration/translations.spec.js
index fe17838b..9b5256ee 100644
--- a/web-ui/test/integration/translations.spec.js
+++ b/web-ui/test/integration/translations.spec.js
@@ -2,11 +2,45 @@ import { mount } from 'enzyme';
import expect from 'expect';
import React from 'react';
import App from 'src/common/app';
+import AccountRecoveryPage from 'src/account_recovery/page';
import BackupAccountPage from 'src/backup_account/page';
import LoginPage from 'src/login/page';
import testI18n from './i18n';
describe('Translations', () => {
+ context('Account Recovery Page', () => {
+ it('translates all keys on admin recovery code step', () => {
+ const app = mount(<App i18n={testI18n} child={<AccountRecoveryPage />} />);
+ expect(app.text()).toNotContain('untranslated', 'Unstranslated message found in the text: ' + app.text());
+ });
+
+ it('translates all keys on user recovery code step', () => {
+ const app = mount(<App i18n={testI18n} child={<AccountRecoveryPage />} />);
+ app.find('form.admin-code').simulate('submit');
+
+ expect(app.text()).toNotContain('untranslated', 'Unstranslated message found in the text: ' + app.text());
+ });
+
+ it('translates all keys on new password step', () => {
+ const app = mount(<App i18n={testI18n} child={<AccountRecoveryPage />} />);
+ app.find('form.admin-code').simulate('submit');
+ app.find('form.user-code').simulate('submit');
+
+ expect(app.text()).toNotContain('untranslated', 'Unstranslated message found in the text: ' + app.text());
+ });
+
+ it('translates all keys on backup account step', () => {
+ const app = mount(<App i18n={testI18n} child={<AccountRecoveryPage />} />);
+ app.find('form.admin-code').simulate('submit');
+ app.find('form.user-code').simulate('submit');
+ app.find('input[name="new-password"]').simulate('change', {target: {value: '11'}});
+ app.find('input[name="confirm-password"]').simulate('change', {target: {value: '11'}});
+ app.find('form.new-password').simulate('submit');
+
+ expect(app.text()).toNotContain('untranslated', 'Unstranslated message found in the text: ' + app.text());
+ });
+ });
+
context('Backup Account Page', () => {
it('translates all key', () => {
const app = mount(<App i18n={testI18n} child={<BackupAccountPage />} />);
diff --git a/web-ui/webpack.config.js b/web-ui/webpack.config.js
index 6a44e4a1..e82cf88b 100644
--- a/web-ui/webpack.config.js
+++ b/web-ui/webpack.config.js
@@ -21,11 +21,12 @@ var commonConfiguration = {
var publicAssets = Object.assign({}, commonConfiguration, {
entry: {
'login': './src/login/login.js',
+ 'account_recovery': './src/account_recovery/account_recovery.js'
},
output: {
path: path.join(__dirname, 'dist/public'),
filename: '[name].js',
- publicPath: '/assets/'
+ publicPath: '/public/'
},
plugins: [
publicAssetsWebpack,
diff --git a/web-ui/webpack.production.config.js b/web-ui/webpack.production.config.js
index 92a4f12b..eee944dd 100644
--- a/web-ui/webpack.production.config.js
+++ b/web-ui/webpack.production.config.js
@@ -19,7 +19,13 @@ var commonConfiguration = {
};
var commonPlugins = [
- new webpack.optimize.UglifyJsPlugin(),
+ new webpack.optimize.UglifyJsPlugin({
+ comments: false,
+ compress: {
+ warnings: false,
+ drop_console: true
+ }
+ }),
new webpack.optimize.DedupePlugin(),
new webpack.DefinePlugin({
'process.env': {
@@ -31,11 +37,12 @@ var commonPlugins = [
var publicAssets = Object.assign({}, commonConfiguration, {
entry: {
'login': './src/login/login.js',
+ 'account_recovery': './src/account_recovery/account_recovery.js'
},
output: {
path: path.join(__dirname, 'dist/public'),
filename: '[name].js',
- publicPath: '/assets/'
+ publicPath: '/public/'
},
plugins: commonPlugins.concat([ publicAssetsWebpack ])
});