diff --git a/.travis.yml b/.travis.yml index 737bc2a6..a31d5514 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,8 +26,6 @@ jobs: - nvm install node - pip install tox before_script: - - jwm & - - sleep 10 - npm install - npm run package script: tox diff --git a/docs/css/extra.css b/docs/css/extra.css index 1d113bdd..7ce898cc 100644 --- a/docs/css/extra.css +++ b/docs/css/extra.css @@ -8,7 +8,7 @@ h1 { a.button-link { display: block; - margin: 0px auto; + margin: .5em auto; padding: 0.5em; width: 12em; color: white; @@ -17,3 +17,14 @@ a.button-link { border-radius: 4px; text-align: center; } + +div.right { + float: right; + clear: right; + width: 40%; + padding-left: 2em; +} + +.rst-content .admonition-title::before { + display: none; !important +} diff --git a/docs/faqs.md b/docs/faqs.md new file mode 100644 index 00000000..6e2496a7 --- /dev/null +++ b/docs/faqs.md @@ -0,0 +1,53 @@ +## What’s the difference between Lockbox and the Firefox password manager? + +Firefox password manager is the built-in feature that saves and autofills website login information. You can protect these logins with a master password. + +Lockbox is a stand-alone password manager extension that you can secure with a Firefox Account for newer encryption than what is offered with password manager. + +The alpha version of Lockbox lets you create, store and manage entries (a site’s username and password) and copy and paste login information. We realize managing passwords this way may feel very manual, but we plan to add features like autofill and password generation in future releases. We are also working to build cloud backup, create a mobile app, and support multiple browsers. + +## Can I use Lockbox and the Firefox password manager at the same time? + +No. When you install Lockbox, Firefox automatically disables the password manager. If you disable or delete Lockbox, Firefox re-enables password manager on the browser’s next restart. + +## If I disable or delete Lockbox, will login information from my entries transfer into the password manager? + +No. But login information you previously added to password manager will still be available. + +## Does Lockbox import my information from password manager? + +Not in the current alpha version. + +## What security technology does Lockbox use? + +When you protect Lockbox with a Firefox Account, Lockbox uses [AES256-GCM](https://en.wikipedia.org/wiki/Galois/Counter_Mode) encryption, a tamper-resistent block cipher technology, to protect your data. Lockbox also uses [HMAC SHA-256](https://en.wikipedia.org/wiki/Hash-based_message_authentication_code) to “hash” searchable data for additional security. + +## How do I disable or delete Lockbox? + +1. Click the menu button ![menu](https://user-images.githubusercontent.com/49511/33676293-a3470a0c-da72-11e7-9f93-2f054bc16cb9.png) + and choose Add-ons ![extensions](https://user-images.githubusercontent.com/49511/33676294-a35f8b5e-da72-11e7-8bfa-186708b20aab.png) +2. ![disable](https://user-images.githubusercontent.com/49511/33676295-a3732b32-da72-11e7-9920-43c8b6d25134.png) or ![remove](https://user-images.githubusercontent.com/49511/33676296-a38aa708-da72-11e7-9c15-7960d17422b7.png) Lockbox + +## If I delete Lockbox, what happens to the entries I’ve saved? + +The alpha version of Lockbox doesn’t offer backup or synchronization. You’ll need to re-add login information to Lockbox after installing it again. + +## What if I forget my Firefox Account password? + +Firefox Accounts do not offer password recovery functionality. If you added a Firefox Account to Lockbox and forget your password, you’ll need to reset your Firefox Account. Note that you’ll lose all saved Lockbox entries. + +## Will Lockbox work with other password managers? + +The alpha version of Lockbox hasn’t been tested widely with other password managers. We recommend disabling or deleting other password managers from Firefox before installing Lockbox. + +## Do Lockbox entries sync to other computers with Lockbox installed? + +Yes, if you secure Lockbox with a Firefox Account. + +## Can I try Lockbox if I don’t have a Mozilla.com email address? + +Sure. To get started, click the Install Lockbox button on the Introduction page. Note that this is an alpha version. Features and functionality will change as we continue developing Lockbox. + +## If I already have a Firefox Account, can I create a separate Firefox Account to use only with Lockbox? + +You can create a new account using a different email address than the one you use for your existing Firefox Account. Note that the new Firefox Account won’t sync bookmarks, history and open tabs unless you use it to sign into the browser. diff --git a/docs/images/tour-01.welcome.png b/docs/images/tour-01.welcome.png new file mode 100644 index 00000000..5c1bc28d Binary files /dev/null and b/docs/images/tour-01.welcome.png differ diff --git a/docs/images/tour-02.create-entry.png b/docs/images/tour-02.create-entry.png new file mode 100644 index 00000000..1af1a580 Binary files /dev/null and b/docs/images/tour-02.create-entry.png differ diff --git a/docs/images/tour-03.doorhanger-search.png b/docs/images/tour-03.doorhanger-search.png new file mode 100644 index 00000000..b970619d Binary files /dev/null and b/docs/images/tour-03.doorhanger-search.png differ diff --git a/docs/images/tour-04.signup-fxa.png b/docs/images/tour-04.signup-fxa.png new file mode 100644 index 00000000..19bca2d5 Binary files /dev/null and b/docs/images/tour-04.signup-fxa.png differ diff --git a/docs/index.md b/docs/index.md index 7f3a3dde..6b48ab73 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,14 +1,45 @@ -# Lockbox Extension +# Lockbox for desktop -*This is just one component of the Lockbox product. Please see the -[Lockbox website][org-website] for more documentation and context.* +!!! right "Install the extension" + [Install Lockbox][install-link]{: .button-link } + Have questions about how Lockbox works? [Check out the FAQs][faq-link] -Click below to install the Lockbox extension: +!!! right "Contribute" + You can also contribute by: + + - Developing code + - Reporting bugs + + [Learn how to get started][contribute-link] -[Install][install-link]{: .button-link } +The Lockbox extension is a simple, stand-alone password manager that works +with Firefox for desktop. It’s the first of several planned experiments +designed to help us test and improve password management and online +security. -**Note: This is a rapidly evolving prototype that will change. Any data stored -is not guaranteed to be retained in future updates.** +Install it and sign in with your Firefox Account to encrypt your data with +tamper-resistant block cipher technology. Then [share feedback +here](feedback-link). + +## Get Started + +1. Install Lockbox, and it will automatically disable Firefox’s password manager. + ![install lockbox](./images/tour-01.welcome.png) + +2. Create an entry with a website name, URL, username, and password. + ![create an entry](./images/tour-02.create-entry.png) + +3. Search or browse in the toolbar menu or on the full tab to find the password you need. + ![search from doorhanger](./images/tour-03.doorhanger-search.png) + +4. Sign up or sign in with a Firefox Account to encrypt your entries. + ![sinup for fxa](./images/tour-04.signup-fxa.png) + +_This is just one component of the Lockbox product. Please see the [Lockbox +website][website-link] for more documentation and context._ [install-link]: https://testpilot.firefox.com/files/lockbox@mozilla.com/latest -[org-website]: https://mozilla-lockbox.github.io/ +[faq-link]: /faqs/ +[contribute-link]: /contributing/ +[website-link]: https://mozilla-lockbox.github.io/ +[feedback-link]: https://qsurvey.mozilla.com/s3/Lockbox-Input diff --git a/docs/metrics.md b/docs/metrics.md index 813551a0..513cd2de 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -144,9 +144,11 @@ All events are currently implemented under the **category: lockboxV0**. The `ext 9. `feedbackClick` fires when the user clicks the "Send Feedback" button. **objects**: manage -10. `resetRequested` fires when the user clicks the "Reset" button in the Lockbox settings. **objects**: settings +10. `faqClick` fires when the user clicks the "FAQ" button. **objects**: manage -11. `resetCompleted` fires when the user completes a reset of their Lockbox data in the Lockbox settings. **objects**: settings +11. `resetRequested` fires when the user clicks the "Reset" button in the Lockbox settings. **objects**: settings + +12. `resetCompleted` fires when the user completes a reset of their Lockbox data in the Lockbox settings. **objects**: settings ## List of Planned Events diff --git a/docs/release-notes.md b/docs/release-notes.md index 4d1cde84..d9dc94ad 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,5 +1,28 @@ # Lockbox Release Notes +## 0.1.4-alpha + +_Date: 2017-12-11_ + +### What's New + +- Access your saved Lockbox entries from a doorhanger experience ([#338](https://github.com/mozilla-lockbox/lockbox-extension/pull/362)) +- Secure your Lockbox with a Firefox Account ([#362](https://github.com/mozilla-lockbox/lockbox-extension/pull/362)) +- See the visual design and polish come together for the entire experience ([#351](https://github.com/mozilla-lockbox/lockbox-extension/pull/351)) +- Get help and instructions when you first get started ([#392](https://github.com/mozilla-lockbox/lockbox-extension/issues/392)) +- Get additional support from the updated [Lockbox website](https://mozilla-lockbox.github.io/lockbox-extension/), including the FAQ ([#345](https://github.com/mozilla-lockbox/lockbox-extension/issues/345)) + + +### What's Fixed + +### Known Issues + +* **Any existing Lockbox entries from previous versions have been removed.** Previous versions were storing and encrypting data differently than we are now. In order to add our new security features your old data can no longer be read/accessed and you'll see an empty state after you upgrade. +* Once you link a Firefox Account to Lockbox, you cannot unlink it from that account. +* Once you link a Firefox Account to Lockbox, signing in with a different account can render Lockbox unusable until you quit and restart Firefox. +* Once you link a Firefox Account to Lockbox, resetting your Firefox Account password through "forgot your password" will render all your logins inaccessible; the only recourse is to reset Lockbox and start over. +* Firefox's default prompt to save logins is only disabled on new installs of this extension; updating Lockbox will not change your current Firefox preferences. + ## 0.1.3-alpha _Date: 2017-11-29_ diff --git a/mkdocs.yml b/mkdocs.yml index 4847fa4f..e5de7878 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,14 +8,12 @@ extra_css: markdown_extensions: - attr_list +- admonition pages: - 'Introduction': 'index.md' -- 'Installing': 'install.md' -- 'User Guide': 'user-guide.md' - 'Release Notes': 'release-notes.md' -- 'Contributing': 'contributing.md' -- 'Code of Conduct': 'code_of_conduct.md' -- 'API Guide': 'api.md' -- 'Metrics Guide': 'metrics.md' -- 'Releases': 'releases.md' +- 'FAQs': 'faqs.md' +- 'Contribute': 'contributing.md' +- 'Source Code': 'install.md' +- 'Metrics': 'metrics.md' diff --git a/package-lock.json b/package-lock.json index e6c04c8f..ba39ec07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "lockbox", - "version": "0.1.3-alpha", + "version": "0.1.4-alpha", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -9771,7 +9771,7 @@ } }, "lockbox-datastore": { - "version": "git+https://github.com/mozilla-lockbox/lockbox-datastore.git#dff86556265e63ae50a05f9cec2333e671f4f843", + "version": "git+https://github.com/linuxwolf/lockbox-datastore.git#c7c72a134800c52c98b1dbd91f889b6cf7dbf2be", "requires": { "dexie": "1.5.1", "fake-indexeddb": "2.0.3", diff --git a/package.json b/package.json index 779b7174..91f9eebd 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,9 @@ "title": "Lockbox", "name": "lockbox", "id": "lockbox@mozilla.com", - "version": "0.1.3-alpha", + "version": "0.1.4-alpha", "main": "dist/bootstrap.js", - "description": "A Lockbox extension for Firefox", + "description": "The simple way to store, retrieve and manage website login info", "author": "Lockbox Team ", "engines": { "firefox": ">=57" @@ -55,7 +55,7 @@ "fluent-langneg": "^0.1.0", "fluent-react": "^0.4.1", "intl-pluralrules": "^0.1.0", - "lockbox-datastore": "git+https://github.com/mozilla-lockbox/lockbox-datastore.git", + "lockbox-datastore": "git+https://github.com/linuxwolf/lockbox-datastore.git#binkey", "node-jose": "^0.10.0", "prop-types": "^15.6.0", "react": "^16.1.1", diff --git a/requirements/tests.txt b/requirements/tests.txt index 9c10623d..4985a72e 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -1,5 +1,6 @@ +fxapom==1.10.1 PyPOM==1.2.0 -pytest==3.2.2 +pytest==3.3.0 pytest-selenium==1.11.0 pytest-xdist==1.18.2 -selenium==3.6.0 +selenium==3.8.0 diff --git a/src/bootstrap.js b/src/bootstrap.js index 02cb3219..fa785480 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -/* global ADDON_INSTALL, ADDON_UNINSTALL */ +/* global ADDON_INSTALL, ADDON_UPGRADE, ADDON_UNINSTALL */ /* eslint-disable no-unused-vars */ const { utils: Cu } = Components; @@ -18,8 +18,35 @@ const ORIGINAL_REMEMBER_SIGNONS_PREF = // category name. In order to do this, every time we update the events in any // way, we must also give them a unique category name. If you're updating the // events, please increment the version number here by 1. -const TELEMETRY_CATEGORY = "lockboxv0"; +const TELEMETRY_CATEGORY = "lockboxv1"; +class EventDispatcher { + constructor() { + this.pendingEvents = []; + } + + record(event) { + if (this.port) { + this.port.postMessage(event); + return true; + } + + this.pendingEvents.push(event); + return false; + } + + connect(port) { + this.port = port; + + const events = this.pendingEvents; + this.pendingEvents = []; + for (let evt of events) { + this.port.postMessage(evt); + } + } +} + +const dispatcher = new EventDispatcher(); function startup({webExtension}, reason) { try { Services.telemetry.registerEvents(TELEMETRY_CATEGORY, { @@ -34,52 +61,49 @@ function startup({webExtension}, reason) { }, "displayView": { methods: ["render"], - objects: ["firstrun", "manage", "popupUnlock"], - extra_keys: ["fxauid"], - }, - "fxaSignIn": { - methods: ["render"], - objects: ["signInPage"], - }, - "confirmPW": { - methods: ["click"], - objects: ["confirmPWPage"], - }, - "setupDone": { - methods: ["click"], - objects: ["setupDoneButton"], + objects: ["firstrun", "popupUnlock", "manage", "doorhanger"], extra_keys: ["fxauid"], }, "itemAdding": { methods: ["itemAdding"], - objects: ["addItemForm"], + objects: ["manage"], extra_keys: ["fxauid"], }, "itemUpdating": { methods: ["itemUpdating"], - objects: ["updatingItemForm"], + objects: ["manage"], extra_keys: ["fxauid"], }, "itemDeleting": { methods: ["itemDeleting"], - objects: ["updatingItemForm"], + objects: ["manage"], extra_keys: ["fxauid"], }, + "itemAdded": { + methods: ["itemAdded"], + objects: ["manage"], + extra_keys: ["itemid", "fxauid"], + }, + "itemUpdated": { + methods: ["itemUpdated"], + objects: ["manage"], + extra_keys: ["itemid", "fxauid"], + }, + "itemDeleted": { + methods: ["itemDeleted"], + objects: ["manage"], + extra_keys: ["itemid", "fxauid"], + }, "itemSelected": { methods: ["itemSelected"], - objects: ["itemList"], + objects: ["manage", "doorhanger"], extra_keys: ["fxauid"], }, "addClick": { methods: ["addClick"], - objects: ["addButton"], + objects: ["manage"], extra_keys: ["fxauid"], }, - "itemAdded": { - methods: ["itemAdded"], - objects: ["addItemForm"], - extra_keys: ["itemid", "fxauid"], - }, "datastore": { methods: ["added", "updated", "deleted"], objects: ["datastore"], @@ -90,9 +114,14 @@ function startup({webExtension}, reason) { objects: ["manage"], extra_keys: ["fxauid"], }, + "faq": { + methods: ["faqClick"], + objects: ["manage"], + extra_keys: ["fxauid"], + }, "itemCopied": { methods: ["usernameCopied", "passwordCopied"], - objects: ["itemDetails"], + objects: ["manage", "doorhanger"], extra_keys: ["fxauid"], }, "resetRequested": { @@ -105,6 +134,24 @@ function startup({webExtension}, reason) { objects: ["settings"], extra_keys: ["fxauid"], }, + "setupGuest": { + methods: ["click"], + objects: ["welcomeGuest"], + }, + "fxaStart": { + methods: ["click"], + objects: ["welcomeSignin", "manageAcctCreate", "manageAcctSignin", "unlockSignin"], + }, + "fxaAuth": { + methods: ["fxaUpgrade", "fxaSignin", "fxaSignout"], + objects: ["accounts"], + extra_keys: ["fxauid"], + }, + "fxaFail": { + methods: ["fxaFailed"], + objects: ["accounts"], + extra_keys: ["message"], + }, }); } catch (e) { if (e.message === "Attempt to register event that is already registered.") { @@ -128,6 +175,10 @@ function startup({webExtension}, reason) { respond({}); } }); + + browser.runtime.onConnect.addListener((port) => { + dispatcher.connect(port); + }); }); } @@ -143,6 +194,14 @@ function install(data, reason) { Services.prefs.getBoolPref(REMEMBER_SIGNONS_PREF) ); Services.prefs.setBoolPref(REMEMBER_SIGNONS_PREF, false); + + dispatcher.record({ type: "extension_installed" }); + } else if (reason === ADDON_UPGRADE) { + dispatcher.record({ + type: "extension_upgraded", + version: data.newVersion, + oldVersion: data.oldVersion, + }); } } @@ -165,3 +224,5 @@ startup; shutdown; install; uninstall; +dispatcher; +EventDispatcher; diff --git a/src/webextension/background/accounts/configs.json b/src/webextension/background/accounts/configs.json new file mode 100644 index 00000000..c3683544 --- /dev/null +++ b/src/webextension/background/accounts/configs.json @@ -0,0 +1,32 @@ +{ + "production": { + "client_id": "1b024772203a0849", + "redirect_uri": "https://2aa95473a5115d5f3deb36bb6875cf76f05e4c4d.extensions.allizom.org/", + "authorization_endpoint": "https://oauth.accounts.firefox.com/v1/authorization", + "token_endpoint": "https://oauth.accounts.firefox.com/v1/token", + "userinfo_endpoint": "https://profile.accounts.firefox.com/v1/profile", + "scopes": ["openid", "profile", "https://identity.mozilla.com/apps/lockbox"], + "pkce": true, + "app_keys": true + }, + "dev-latest": { + "client_id": "f69b2d16e724b432", + "client_secret": "32052e102892dd07b1885c38887849d8283d1407350ee009b6377b8f542a272c", + "redirect_uri": "https://2aa95473a5115d5f3deb36bb6875cf76f05e4c4d.extensions.allizom.org/", + "authorization_endpoint": "https://oauth-latest.dev.lcip.org/v1/authorization", + "token_endpoint": "https://oauth-latest.dev.lcip.org/v1/token", + "userinfo_endpoint": "https://latest.dev.lcip.org/profile/v1/profile", + "scopes": ["openid", "profile", "https://identity.mozilla.com/apps/lockbox"], + "pkce": false + }, + "scoped-keys": { + "client_id": "37fdfa37698f251a", + "redirect_uri": "https://2aa95473a5115d5f3deb36bb6875cf76f05e4c4d.extensions.allizom.org/", + "authorization_endpoint": "https://oauth-latest-keys.dev.lcip.org/v1/authorization", + "token_endpoint": "https://oauth-latest-keys.dev.lcip.org/v1/token", + "userinfo_endpoint": "https://latest-keys.dev.lcip.org/profile/v1/profile", + "scopes": ["openid", "profile", "https://identity.mozilla.com/apps/lockbox"], + "pkce": true, + "app_keys": true + } +} diff --git a/src/webextension/background/accounts/index.js b/src/webextension/background/accounts/index.js new file mode 100644 index 00000000..d9c15ae0 --- /dev/null +++ b/src/webextension/background/accounts/index.js @@ -0,0 +1,250 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const DEFAULT_CONFIG = "production"; + +import jose from "node-jose"; + +import configs from "./configs.json"; + +async function generateAuthzURL(config, props) { + const queryParams = new URLSearchParams(); + queryParams.set("response_type", "code"); + queryParams.set("client_id", config.client_id); + queryParams.set("redirect_uri", config.redirect_uri); + queryParams.set("access_type", "offline"); + queryParams.set("scope", config.scopes.join(" ")); + if (config.action) { + queryParams.set("action", config.action); + } + + const state = props.state = jose.util.base64url.encode(jose.util.randomBytes(16)); + queryParams.set("state", state); + if (config.pkce) { + props.pkce = jose.util.base64url.encode(jose.util.randomBytes(32)); + let challenge = new TextEncoder().encode(props.pkce); + challenge = await jose.JWA.digest("SHA-256", challenge); + challenge = jose.util.base64url.encode(challenge); + queryParams.set("code_challenge", challenge); + queryParams.set("code_challenge_method", "S256"); + } + if (config.app_keys) { + const keystore = jose.JWK.createKeyStore(); + props.appKey = await keystore.generate("EC", "P-256"); + const keysJWK = jose.util.base64url.encode(JSON.stringify(props.appKey)); + queryParams.set("keys_jwk", keysJWK); + } + return `${config.authorization_endpoint}?${queryParams}`; +} + +function processAuthzResponse(url, props) { + const queryParams = url.searchParams; + if (queryParams.get("state") !== props.state) { + throw new Error("invalid oauth state"); + } + const code = queryParams.get("code"); + if (!code) { + throw new Error("invalid oauth authorization code"); + } + return code; +} + +async function fetchFromEndPoint(name, url, request) { + const response = await fetch(url, request); + let body; + try { + body = await response.json(); + } catch (err) { + body = {}; + } + if (!response.ok) { + const error = new Error(`failed ${name} request: ${body.message || response.statusText}`); + error.errno = body.errno; + throw error; + } + return body; +} + +export const GUEST = "guest"; +export const UNAUTHENTICATED = "unauthenticated"; +export const AUTHENTICATED = "authenticated"; + +export const APP_KEY_NAME = "https://identity.mozilla.com/apps/lockbox"; + +export class Account { + constructor({config = DEFAULT_CONFIG, info}) { + // TODO: verify configuration (when there is one) + this.config = config; + this.info = info || undefined; + } + + toJSON() { + const { config } = this; + let { info } = this; + if (info) { + // only exporta specific whitelist of values + info = { + uid: info.uid, + access_token: info.access_token || undefined, + expires_at: info.expires_at || undefined, + id_token: info.id_token || undefined, + }; + } + return { + config, + info, + }; + } + + get mode() { + const info = this.info; + if (!info || !info.uid) { + return GUEST; + } + if (!info.refresh_token) { + return UNAUTHENTICATED; + } + return AUTHENTICATED; + } + + get signedIn() { return this.mode === AUTHENTICATED; } + + get uid() { return (this.info && this.info.uid) || undefined; } + get email() { return (this.info && this.info.email) || undefined; } + get keys() { return (this.info && this.info.keys) || new Map(); } + + async signIn(action = "signin") { + let cfg = configs[this.config]; + + const props = {}; + let url, request; + + // request authorization + cfg = { + ...cfg, + action, + }; + url = await generateAuthzURL(cfg, props); + const authzRsp = await browser.identity.launchWebAuthFlow({ + url, + interactive: true, + }); + const authzCode = processAuthzResponse(new URL(authzRsp), props); + + // exchange token + const tokenParams = { + grant_type: "authorization_code", + code: authzCode, + client_id: cfg.client_id, + }; + if (cfg.pkce) { + tokenParams.code_verifier = props.pkce; + } else { + tokenParams.client_secret = cfg.client_secret; + } + url = cfg.token_endpoint; + request = { + method: "post", + headers: { + "content-type": "application/json", + }, + cache: "no-cache", + body: JSON.stringify(tokenParams), + }; + const oauthInfo = await fetchFromEndPoint("token", url, request); + // console.log(`oauth info == ${JSON.stringify(oauthInfo)}`); + + const keys = new Map(); + if (oauthInfo.keys_jwe) { + let bundle = await jose.JWE.createDecrypt(props.appKey).decrypt(oauthInfo.keys_jwe); + bundle = JSON.parse(new TextDecoder().decode(bundle.payload)); + const pending = Object.keys(bundle).map(async (name) => { + let key = bundle[name]; + key = await jose.JWK.asKey(key); + keys.set(name, key); + }); + await Promise.all(pending); + } + + // retrieve user info + url = cfg.userinfo_endpoint; + request = { + method: "get", + headers: { + authorization: `Bearer ${oauthInfo.access_token}`, + }, + cache: "no-cache", + }; + const userInfo = await fetchFromEndPoint("userinfo", url, request); + + this.info = { + uid: userInfo.uid, + email: userInfo.email, + access_token: oauthInfo.access_token, + expires_at: (Date.now / 1000) + oauthInfo.expires_in, + refresh_token: oauthInfo.refresh_token, + id_token: oauthInfo.id_token, + keys, + }; + return this; + } + + async signOut() { + // TODO: implement a complete signout/forget + // TODO: something server side? + this.info = undefined; + return this; + } + + details() { + return { + mode: this.mode, + uid: this.uid, + email: this.email, + }; + } +} + + +let account; +export default function getAccount() { + if (!account) { + account = new Account({}); + } + return account; +} + +export async function loadAccount(storage) { + const stored = await storage.get("account"); + if (stored && stored.account) { + account = new Account(stored.account); + } + return getAccount(); +} + +export async function saveAccount(storage) { + const account = getAccount().toJSON(); + await storage.set({ account }); +} + +export function setAccount(config, info) { + account = config ? new Account({config, info}) : undefined; +} + +export async function openAccount(storage) { + let account; + + try { + // attempt to load account (FxA) data + account = await loadAccount(storage); + // eslint-disable-next-line no-console + console.log(`loaded account for (${account.mode.toString()}) '${account.uid || ""}'`); + } catch (err) { + // eslint-disable-next-line no-console + console.error(`loading account failed (fallback to empty GUEST): ${err.message}`); + account = getAccount(); + } + + return account; +} diff --git a/src/webextension/background/authorization/index.js b/src/webextension/background/authorization/index.js deleted file mode 100644 index 33f9c1c7..00000000 --- a/src/webextension/background/authorization/index.js +++ /dev/null @@ -1,82 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -const DEFAULT_CONFIG = "dev-latest"; - -import UUID from "uuid"; - -export class Authorization { - constructor({config = DEFAULT_CONFIG, info}) { - // TODO: verify configuration (when there is one) - this.config = config; - this.info = info || undefined; - } - - toJSON() { - let { config, info } = this; - if (info) { - info = { ...info }; - delete info.email; - } - return { - config, - info, - }; - } - - get signedIn() { return this.info !== undefined; } - get verified() { return (this.info && this.info.verified) || false; } - - get uid() { return (this.info && this.info.uid) || undefined; } - get email() { return (this.info && this.info.email) || undefined; } - - async signIn(interactive = true) { - let uid = UUID(); - this.info = { - uid, - }; - return this.info; - } - - async signOut() { - // TODO: something server side? - this.info = undefined; - } - - async verify(password) { - if (!this.signedIn) { - throw new Error("not signed in"); - } - - // TODO: do something real! - this.info.verified = true; - - return password; - } -} - -let authorization; -export default function getAuthorization() { - if (!authorization) { - authorization = new Authorization({}); - } - return authorization; -} - -export async function loadAuthorization(storage) { - let stored = await storage.get("authz"); - if (stored && stored.authz) { - authorization = new Authorization(stored.authz); - } - return getAuthorization(); -} - -export async function saveAuthorization(storage) { - let authz = getAuthorization().toJSON(); - await storage.set({ authz }); -} - -export function setAuthorization(config, info) { - authorization = config ? new Authorization({config, info}) : undefined; -} diff --git a/src/webextension/background/browser-action.js b/src/webextension/background/browser-action.js index ecca9abc..fca9bac2 100644 --- a/src/webextension/background/browser-action.js +++ b/src/webextension/background/browser-action.js @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { openView } from "./views"; +import getAccount, * as accounts from "./accounts"; import * as telemetry from "./telemetry"; let listener; @@ -37,25 +38,33 @@ function uninstallListener() { listener = null; } -export default async function updateBrowserAction(ds) { +function installEntriesAction() { + return installPopup("list/popup/index.html"); +} + +export default async function updateBrowserAction({account = getAccount(), datastore}) { // clear listener // XXXX: be more efficient with this? uninstallListener(); uninstallPopup(); - const iconpath = ds.locked ? "icons/lb_locked.svg" : "icons/lb_unlocked.svg"; + const iconpath = datastore.locked ? "icons/lb_locked.svg" : "icons/lb_unlocked.svg"; browser.browserAction.setIcon({ path: iconpath }); - if (!ds.initialized) { + if (!datastore.initialized) { // setup first-run popup return installListener("firstrun"); - } else if (ds.locked) { + } + if (datastore.locked) { + if (account.mode === accounts.GUEST) { + // unlock on user's behalf ... + // XXXX: is this a bad idea or terrible idea? + await datastore.unlock(); + return installEntriesAction(); + } // setup unlock popup - return installPopup("popup/unlock/index.html"); + return installPopup("unlock/index.html"); } - if (process.env.ENABLE_DOORHANGER) { - return installPopup("list/popup/index.html"); - } - return installListener("manage"); + return installEntriesAction(); } diff --git a/src/webextension/background/datastore.js b/src/webextension/background/datastore.js index dec559a2..087a5910 100644 --- a/src/webextension/background/datastore.js +++ b/src/webextension/background/datastore.js @@ -20,9 +20,10 @@ async function recordMetric(method, itemid, fields) { telemetry.recordEvent(method, "datastore", extra); } -export default async function openDataStore() { +export default async function openDataStore(cfg = {}) { if (!datastore) { datastore = await DataStore.open({ + ...cfg, recordMetric, }); } diff --git a/src/webextension/background/index.js b/src/webextension/background/index.js index ab972b32..06157690 100644 --- a/src/webextension/background/index.js +++ b/src/webextension/background/index.js @@ -3,24 +3,16 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import openDataStore from "./datastore"; -import { loadAuthorization } from "./authorization"; +import { openAccount, GUEST } from "./accounts"; import initializeMessagePorts from "./message-ports"; import updateBrowserAction from "./browser-action"; -// XXX: For now, initialize the datastore on startup and then hook up the -// button. Eventually, we'll have UX to create new datastores (and persist -// existing ones). -openDataStore().then(async (ds) => { - try { - // attempt to load authorization (FxA) data - let authz = await loadAuthorization(browser.storage.local); - // eslint-disable-next-line no-console - console.log(`loaded authorization for '${authz.uid || ""}'`); - } catch (err) { - // eslint-disable-next-line no-console - console.error(`loading failed: ${err.message}`); +openAccount(browser.storage.local).then(async (account) => { + let datastore = await openDataStore({ salt: account.uid }); + if (datastore.initialized && account.mode === GUEST) { + await datastore.unlock(); } initializeMessagePorts(); - await updateBrowserAction(ds); + await updateBrowserAction({account, datastore}); }); diff --git a/src/webextension/background/message-ports.js b/src/webextension/background/message-ports.js index e48e083e..680cc526 100644 --- a/src/webextension/background/message-ports.js +++ b/src/webextension/background/message-ports.js @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import openDataStore from "./datastore"; -import getAuthorization, { saveAuthorization } from "./authorization/index"; +import getAccount, * as accounts from "./accounts"; import updateBrowserAction from "./browser-action"; import * as telemetry from "./telemetry"; import { openView, closeView } from "./views"; @@ -19,7 +19,28 @@ function broadcast(message, excludedSender) { } } +let addonPort; + export default function initializeMessagePorts() { + // setup port to receive messages from bootstrapped addon + addonPort = browser.runtime.connect({ name: "webext-to-legacy" }); + addonPort.onMessage.addListener(async (message) => { + switch (message.type) { + case "extension_installed": + openView("firstrun"); + break; + case "extension_upgraded": + openDataStore().then(async (datastore) => { + if (!datastore.initialized) { + openView("firstrun"); + } + }); + break; + default: + break; + } + }); + browser.runtime.onConnect.addListener((port) => { ports.add(port); port.onDisconnect.addListener(() => ports.delete(port)); @@ -27,46 +48,100 @@ export default function initializeMessagePorts() { browser.runtime.onMessage.addListener(async (message, sender) => { switch (message.type) { + case "get_account_details": + return {account: getAccount().details()}; case "open_view": return openView(message.name).then(() => ({})); case "close_view": return closeView(message.name).then(() => ({})); - case "signin": - return getAuthorization().signIn(message.interactive); case "initialize": - return openDataStore().then(async (ds) => { - await ds.initialize({ - password: message.password, - }); - await saveAuthorization(browser.storage.local); - await updateBrowserAction(ds); + return openDataStore().then(async (datastore) => { + await datastore.initialize(); + // TODO: be more implicit on saving account info + await accounts.saveAccount(browser.storage.local); + await updateBrowserAction({datastore}); + if (message.view) { + openView(message.view); + } + + return {}; + }); + case "upgrade_account": + return openDataStore().then(async (datastore) => { + const account = await getAccount().signIn(message.action); + const appKey = account.keys.get("https://identity.mozilla.com/apps/lockbox"); + const salt = account.uid; + + try { + if (datastore.initialized && datastore.locked) { + await datastore.unlock(); + } + await datastore.initialize({ appKey, salt, rebase: true }); + // FIXME: be more implicit on saving account info + await accounts.saveAccount(browser.storage.local); + await updateBrowserAction({ account, datastore }); + telemetry.recordEvent("fxaUpgrade", "accounts"); + } catch (err) { + telemetry.recordEvent("fxaFailed", "accounts", err.message); + throw err; + } + + broadcast({ type: "account_details_updated", account: account.details() }); + if (message.view) { + openView(message.view); + } + return {}; }); case "reset": - return openDataStore().then(async (ds) => { + return openDataStore().then(async (datastore) => { + const account = getAccount(); + await closeView(); - await ds.reset(); + await datastore.reset(); + await account.signOut(); // TODO: put other reset calls here - await updateBrowserAction(ds); - await openView("firstrun"); + await updateBrowserAction({datastore}); + broadcast({type: "account_details_updated", account: account.details()}); + openView("firstrun"); return {}; }); - case "unlock": - return openDataStore().then(async (ds) => { - await ds.unlock(message.password); - await updateBrowserAction(ds); + case "signin": + return openDataStore().then(async (datastore) => { + const account = getAccount(); + let appKey; + try { + if (account.mode === accounts.UNAUTHENTICATED) { + await account.signIn(); + appKey = account.keys.get(accounts.APP_KEY_NAME); + } + await datastore.unlock(appKey); + await updateBrowserAction({ datastore }); + telemetry.recordEvent("fxaSignin", "accounts"); + } catch (err) { + telemetry.recordEvent("fxaFailed", "accounts", err.message); + throw err; + } + + broadcast({ type: "account_details_updated", account: account.details() }); + if (message.view) { + openView(message.view); + } + return {}; }); - case "lock": - return openDataStore().then(async (ds) => { - await ds.lock(); - await updateBrowserAction(ds); + case "signout": + return openDataStore().then(async (datastore) => { + // TODO: perform (light) signout from FxA + await datastore.lock(); + await updateBrowserAction({datastore}); + return {}; }); @@ -75,7 +150,6 @@ export default function initializeMessagePorts() { return {items: Array.from((await ds.list()).values(), makeItemSummary)}; }); - case "add_item": return openDataStore().then(async (ds) => { const item = await ds.add(message.item); diff --git a/src/webextension/background/telemetry.js b/src/webextension/background/telemetry.js index 77220a47..1bad78ad 100644 --- a/src/webextension/background/telemetry.js +++ b/src/webextension/background/telemetry.js @@ -2,10 +2,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import getAuthorization from "./authorization/index"; +import getAccount from "./accounts"; export async function recordEvent(method, object, extra) { - const fxauid = getAuthorization().uid; + const fxauid = getAccount().uid; if (fxauid) { extra = {...(extra || {}), fxauid}; } diff --git a/src/webextension/firstrun/components/app.css b/src/webextension/firstrun/components/app.css index b93dab3a..aba15e97 100644 --- a/src/webextension/firstrun/components/app.css +++ b/src/webextension/firstrun/components/app.css @@ -13,7 +13,7 @@ body { } .firstrun { - width: 459px; + width: 612px; margin: 3em auto; display: flex; flex-flow: column nowrap; diff --git a/src/webextension/firstrun/components/app.js b/src/webextension/firstrun/components/app.js index 70c2f61c..d36ce434 100644 --- a/src/webextension/firstrun/components/app.js +++ b/src/webextension/firstrun/components/app.js @@ -6,14 +6,12 @@ import { Localized } from "fluent-react"; import React from "react"; import DocumentTitle from "react-document-title"; -import Welcome from "./welcome"; -import MasterPasswordSetup from "./master-password-setup"; +import Intro from "./intro"; +import StartUsing from "./using"; import styles from "./app.css"; export default function App() { - // Eventually, we'll have a feedback button up top here, and maybe some other - // stuff. const imgSrc = browser.extension.getURL("/images/nessie_v2.svg"); return ( @@ -21,8 +19,8 @@ export default function App() {
- - + +
diff --git a/src/webextension/firstrun/components/welcome.css b/src/webextension/firstrun/components/intro.css similarity index 51% rename from src/webextension/firstrun/components/welcome.css rename to src/webextension/firstrun/components/intro.css index 8b660562..cca2ed8d 100644 --- a/src/webextension/firstrun/components/welcome.css +++ b/src/webextension/firstrun/components/intro.css @@ -2,18 +2,35 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -.welcome { +.intro { display: flex; flex-flow: column nowrap; justify-content: center; align-items: center; } -.welcome > h1, -.welcome > p { +.intro > h1, +.intro > h2, +.intro > p { text-align: center; + line-height: 1.5; + letter-spacing: 0.2px; } -.welcome > h1 { - font-size: 2em; - font-weight: normal; + +.intro > h1 { + font-size: 33px; + font-weight: 300; + margin: 0; +} + +.intro > h2 { + font-size: 17px; + font-weight: 300; + font-style: italic; + margin: 0px; + text-transform: lowercase; +} + +.intro > p { + font-size: 15px; } diff --git a/src/webextension/firstrun/components/intro.js b/src/webextension/firstrun/components/intro.js new file mode 100644 index 00000000..00571d9e --- /dev/null +++ b/src/webextension/firstrun/components/intro.js @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Localized } from "fluent-react"; +import React from "react"; + +import styles from "./intro.css"; + +export default function Intro() { + return ( +
+ +

wELCOMe

+
+ +

mORe wELCOMe

+
+
+ ); +} diff --git a/src/webextension/firstrun/components/master-password-setup.css b/src/webextension/firstrun/components/master-password-setup.css deleted file mode 100644 index 35b9e889..00000000 --- a/src/webextension/firstrun/components/master-password-setup.css +++ /dev/null @@ -1,33 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -.master-password-setup { - display: flex; - flex-flow: column nowrap; - justify-content: center; - align-content: stretch; - margin: 1em auto; - width: 306px; -} - -.master-password-setup > * { - margin: 0.5em 0; -} - -.master-password-setup > h3 { - font-size: 1em; - width: auto; - align-self: center; -} - -.master-password-setup > .error { - color: red; - text-align: center; - font-weight: bold; -} - -.master-password-setup > button { - text-align: center; - height: 3em; -} diff --git a/src/webextension/firstrun/components/master-password-setup.js b/src/webextension/firstrun/components/master-password-setup.js deleted file mode 100644 index 4227210f..00000000 --- a/src/webextension/firstrun/components/master-password-setup.js +++ /dev/null @@ -1,107 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import { Localized } from "fluent-react"; -import React from "react"; - -import Button from "../../widgets/button"; -import LabelText from "../../widgets/label-text"; -import PasswordInput from "../../widgets/password-input"; -import * as telemetry from "../../telemetry"; - -import styles from "./master-password-setup.css"; - -export default class MasterPasswordSetup extends React.Component { - constructor() { - super(); - this.state = { - password: "", - confirmPassword: "", - }; - } - - componentDidMount() { - this._firstField.focus(); - } - - handleChange(event) { - this.setState({ - [event.target.name]: event.target.value, - error: undefined, - }); - } - - async handleSubmit() { - let { password, confirmPassword } = this.state; - if (password !== confirmPassword) { - // TODO: Localize this! - this.setState({ - error: "Passwords do not match", - }); - } else { - try { - await browser.runtime.sendMessage({ - type: "signin", - interactive: false, - }); - await browser.runtime.sendMessage({ - type: "initialize", - password, - }); - - telemetry.recordEvent("lockbox", "click", "setupDoneButton"); - - await browser.runtime.sendMessage({ - type: "open_view", - name: "manage", - }); - await browser.runtime.sendMessage({ - type: "close_view", - name: "firstrun", - }); - } catch (err) { - // eslint-disable-next-line no-console - console.error(`initialize failed: ${err.message}`); - // TODO: Localize this! - this.setState({ - error: "Could not initialize!", - }); - } - } - } - - render() { - const { error = "\u00a0" } = this.state; - const controlledProps = (name) => { - return {name, value: this.state[name], - onChange: (e) => this.handleChange(e)}; - }; - - return ( -
{ evt.preventDefault(); this.handleSubmit(); }}> - -

eNTEr mASTEr pASSWORd

-
- - -
{error}
- - - -
- ); - } -} diff --git a/src/webextension/firstrun/components/using.css b/src/webextension/firstrun/components/using.css new file mode 100644 index 00000000..d3e02e2a --- /dev/null +++ b/src/webextension/firstrun/components/using.css @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.using { + display: flex; + flex-flow: column nowrap; + align-items: center; +} + +.using h1, +.using h2, +.using p { + text-align: center; +} + +.using > .actions { + margin: 0; + display: flex; + flex-flow: column nowrap; + align-items: stretch; +} + +.using > .actions > h2 { + margin: 1em 0 .5em; + font-size: 22px; + font-weight: 300; + line-height: 1.5; + letter-spacing: 0.2px; +} + +.using > .actions > button { + margin: 0; + font-size: 15px; +} diff --git a/src/webextension/firstrun/components/using.js b/src/webextension/firstrun/components/using.js new file mode 100644 index 00000000..78ba0fd8 --- /dev/null +++ b/src/webextension/firstrun/components/using.js @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Localized } from "fluent-react"; +import React from "react"; + +import Button from "../../widgets/button"; +import * as telemetry from "../../telemetry"; + +import styles from "./using.css"; + +export default function StartUsing() { + const doGuest = async () => { + telemetry.recordEvent("click", "welcomeGuest"); + browser.runtime.sendMessage({ + type: "initialize", + view: "manage", + }); + browser.runtime.sendMessage({ + type: "close_view", + name: "firstrun", + }); + }; + const doReturning = async () => { + telemetry.recordEvent("click", "welcomeSignin"); + browser.runtime.sendMessage({ + type: "upgrade_account", + view: "manage", + }); + browser.runtime.sendMessage({ + type: "close_view", + name: "firstrun", + }); + }; + + return ( +
+
+ +

gUESt

+
+ + + + +

rETURNINg

+
+ + + +
+
+ ); +} diff --git a/src/webextension/firstrun/components/welcome.js b/src/webextension/firstrun/components/welcome.js deleted file mode 100644 index 816512cc..00000000 --- a/src/webextension/firstrun/components/welcome.js +++ /dev/null @@ -1,42 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import { Localized } from "fluent-react"; -import React from "react"; - -import styles from "./welcome.css"; - -export default function Welcome() { - return ( -
- -

wELCOMe

-
- -

Cras justo odio, dapibus ac facilisis in, egestas eget quam. Sed - posuere consectetur est at lobortis. Lorem ipsum dolor sit amet, - consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur - adipiscing elit. Etiam porta sem malesuada magna mollis euismod. Cras - mattis consectetur purus sit amet fermentum.

-
-

- - Lorem ipsum dolor sit amet, consectetur. - Mauris, aliquam vel pellentesque et, mattis bibendum tellus. Fusce - sodales, tellus a auctor accumsan, diam risus pharetra orci, at lacinia - libero eros ut erat. Fusce ex neque, pharetra id rhoncus in, - pellentesque quis urna. - -

- -

Curabitur blandit tempus porttitor. Nulla vitae elit libero, a - pharetra augue. Vestibulum id ligula porta felis euismod semper. - Maecenas sed diam eget risus varius blandit sit amet non magna. - Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis - vestibulum. Maecenas sed diam eget risus varius blandit sit amet - non magna.

-
-
- ); -} diff --git a/src/webextension/firstrun/index.js b/src/webextension/firstrun/index.js index dd84bc1a..1771568b 100644 --- a/src/webextension/firstrun/index.js +++ b/src/webextension/firstrun/index.js @@ -12,7 +12,7 @@ import * as telemetry from "../telemetry"; telemetry.recordEvent("render", "firstrun"); ReactDOM.render( - , diff --git a/src/webextension/icons/arrowhead-left-16.svg b/src/webextension/icons/arrowhead-left-16.svg new file mode 100644 index 00000000..59c9c5d7 --- /dev/null +++ b/src/webextension/icons/arrowhead-left-16.svg @@ -0,0 +1,6 @@ + + + + diff --git a/src/webextension/icons/arrowhead-right-16.svg b/src/webextension/icons/arrowhead-right-16.svg new file mode 100644 index 00000000..cc6a0285 --- /dev/null +++ b/src/webextension/icons/arrowhead-right-16.svg @@ -0,0 +1,6 @@ + + + + diff --git a/src/webextension/icons/external-link.svg b/src/webextension/icons/external-link.svg new file mode 100644 index 00000000..d9b102ee --- /dev/null +++ b/src/webextension/icons/external-link.svg @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/src/webextension/images/intro-step-1.png b/src/webextension/images/intro-step-1.png new file mode 100644 index 00000000..7654052b Binary files /dev/null and b/src/webextension/images/intro-step-1.png differ diff --git a/src/webextension/images/intro-step-2.png b/src/webextension/images/intro-step-2.png new file mode 100644 index 00000000..d2f84829 Binary files /dev/null and b/src/webextension/images/intro-step-2.png differ diff --git a/src/webextension/images/intro-step-3.png b/src/webextension/images/intro-step-3.png new file mode 100644 index 00000000..48d45e18 Binary files /dev/null and b/src/webextension/images/intro-step-3.png differ diff --git a/src/webextension/list/actions.js b/src/webextension/list/actions.js index ce7a09c8..a642b81a 100644 --- a/src/webextension/list/actions.js +++ b/src/webextension/list/actions.js @@ -2,7 +2,8 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as telemetry from "../telemetry"; +export const GET_ACCOUNT_DETAILS_STARTING = Symbol("GET_ACCOUNT_DETAILS_STARTING"); +export const GET_ACCOUNT_DETAILS_COMPLETED = Symbol("GET_ACCOUNT_DETAILS_COMPLETED"); export const LIST_ITEMS_STARTING = Symbol("LIST_ITEMS_STARTING"); export const LIST_ITEMS_COMPLETED = Symbol("LIST_ITEMS_COMPLETED"); @@ -19,6 +20,8 @@ export const REMOVE_ITEM_COMPLETED = Symbol("REMOVE_ITEM_COMPLETED"); export const SELECT_ITEM_STARTING = Symbol("SELECT_ITEM_STARTING"); export const SELECT_ITEM_COMPLETED = Symbol("SELECT_ITEM_COMPLETED"); +export const COPIED_FIELD = Symbol("COPIED_FIELD"); + export const START_NEW_ITEM = Symbol("START_NEW_ITEM"); export const EDIT_CURRENT_ITEM = Symbol("EDIT_CURRENT_ITEM"); export const EDITOR_CHANGED = Symbol("EDITOR_CHANGED"); @@ -29,10 +32,47 @@ export const FILTER_ITEMS = Symbol("FILTER_ITEMS"); export const SHOW_MODAL = Symbol("SHOW_MODAL"); export const HIDE_MODAL = Symbol("HIDE_MODAL"); +export const SEND_FEEDBACK = Symbol("SEND_FEEDBACK"); +export const OPEN_FAQ = Symbol("OPEN_FAQ"); + // The action ID is used for debugging to correlate async actions with each // other (i.e. FOO_STARTING and FOO_COMPLETED). let nextActionId = 0; +const FEEDBACK_URL = "https://qsurvey.mozilla.com/s3/Lockbox-Input"; +const FAQ_URL = "https://mozilla-lockbox.github.io/lockbox-extension/faqs/"; + +export function getAccountDetails() { + return async (dispatch) => { + const actionId = nextActionId++; + dispatch(getAccountDetailsStarting(actionId)); + + const response = await browser.runtime.sendMessage({ + type: "get_account_details", + }); + dispatch(getAccountDetailsCompleted(actionId, response.account)); + }; +} + +function getAccountDetailsStarting(actionId) { + return { + type: GET_ACCOUNT_DETAILS_STARTING, + actionId, + }; +} + +function getAccountDetailsCompleted(actionId, account) { + return { + type: GET_ACCOUNT_DETAILS_COMPLETED, + actionId, + account, + }; +} + +export function accountDetailsUpdated(account) { + return getAccountDetailsCompleted(undefined, account); +} + export function listItems() { return async (dispatch) => { const actionId = nextActionId++; @@ -64,14 +104,12 @@ export function addItem(details) { return async (dispatch) => { const actionId = nextActionId++; dispatch(addItemStarting(actionId, details)); - telemetry.recordEvent("itemAdding", "addItemForm"); const response = await browser.runtime.sendMessage({ type: "add_item", item: details, }); dispatch(addItemCompleted(actionId, response.item, true)); - telemetry.recordEvent("itemAdded", "addItemForm"); }; } @@ -103,7 +141,6 @@ export function updateItem(item) { return async (dispatch) => { const actionId = nextActionId++; dispatch(updateItemStarting(actionId, item)); - telemetry.recordEvent("itemUpdating", "updatingItemForm"); const response = await browser.runtime.sendMessage({ type: "update_item", @@ -145,7 +182,6 @@ export function removeItem(id) { return async (dispatch) => { const actionId = nextActionId++; dispatch(removeItemStarting(actionId, id)); - telemetry.recordEvent("itemDeleting", "updatingItemForm"); await browser.runtime.sendMessage({ type: "remove_item", @@ -201,7 +237,6 @@ export function selectItem(id) { id, }); dispatch(selectItemCompleted(actionId, response.item)); - telemetry.recordEvent("itemSelected", "itemList"); }; } @@ -221,6 +256,13 @@ function selectItemCompleted(actionId, item) { }; } +export function copiedField(field) { + return { + type: COPIED_FIELD, + field, + }; +} + export function startNewItem() { return { type: START_NEW_ITEM, @@ -276,3 +318,17 @@ export function hideModal() { type: HIDE_MODAL, }; } + +export function sendFeedback() { + window.open(FEEDBACK_URL, "_blank"); + return { + type: SEND_FEEDBACK, + }; +} + +export function openFAQ() { + window.open(FAQ_URL, "_blank"); + return { + type: OPEN_FAQ, + }; +} diff --git a/src/webextension/list/components/item-fields.css b/src/webextension/list/components/item-fields.css index eb92de5b..33d4b63c 100644 --- a/src/webextension/list/components/item-fields.css +++ b/src/webextension/list/components/item-fields.css @@ -21,13 +21,14 @@ margin-top: 0; } +.copy-button { + color: #0060df; +} + .inline-button { display: flex; - overflow: hidden; } .inline-button > *:first-child { flex: 1; - overflow: hidden; - overflow-wrap: break-word; } diff --git a/src/webextension/list/components/item-fields.js b/src/webextension/list/components/item-fields.js index 8e102168..98f2da1b 100644 --- a/src/webextension/list/components/item-fields.js +++ b/src/webextension/list/components/item-fields.js @@ -16,16 +16,12 @@ import TextArea from "../../widgets/text-area"; import styles from "./item-fields.css"; -import * as telemetry from "../../telemetry"; - const PASSWORD_DOT = "\u25cf"; -function CopyToClipboardButton({text, field, ...props}) { +function CopyToClipboardButton({text, field, onCopy, ...props}) { return ( - { - telemetry.recordEvent(`${field}Copied`, "itemDetails"); - }}> -