├── .babelrc
├── .dev.sample.json
├── .github
├── CODEOWNERS
├── FUNDING.yml
├── changelog_configuration.json
└── workflows
│ ├── lint.yaml
│ └── release.yaml
├── .gitignore
├── .jshintrc
├── .nvmrc
├── CHANGELOG
├── DEVELOPMENT.md
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── reviewers
├── firefox-beta.md
└── firefox.md
├── scripts
├── build-zip.js
└── generateBuildConfig.js
├── src
├── background
│ ├── context-menu.js
│ ├── create-alias.js
│ ├── index.js
│ └── onboarding.js
├── content_script
│ ├── input_tools.css
│ └── input_tools.js
├── icons
│ ├── icon_128.png
│ ├── icon_16.png
│ ├── icon_32.png
│ ├── icon_48.png
│ ├── icon_96.png
│ ├── icon_beta_128.png
│ └── icon_beta_48.png
├── images
│ ├── arrow-up.png
│ ├── back-button.svg
│ ├── chrome-permission-screenshot.png
│ ├── firefox-permission-screenshot.png
│ ├── horizontal-logo.svg
│ ├── icon-copy.svg
│ ├── icon-dropdown.svg
│ ├── icon-more.svg
│ ├── icon-puzzle.png
│ ├── icon-settings.svg
│ ├── icon-simplelogin.png
│ ├── icon-trash.svg
│ ├── loading-three-dots.svg
│ ├── loading.svg
│ ├── proton.svg
│ └── sl-button-demo.png
├── manifest.json
└── popup
│ ├── APIService.js
│ ├── App-color.scss
│ ├── App-scrollbar.scss
│ ├── App.scss
│ ├── App.vue
│ ├── EventManager.js
│ ├── Navigation.js
│ ├── SLStorage.js
│ ├── Theme.js
│ ├── Theme.scss
│ ├── Utils.js
│ ├── buildConfig.json
│ ├── components
│ ├── AliasMoreOptions.vue
│ ├── ApiKeySetting.vue
│ ├── AppSettings.vue
│ ├── ExpandTransition.vue
│ ├── Header.vue
│ ├── Login.vue
│ ├── Main.vue
│ ├── NewAliasResult.vue
│ ├── ReverseAlias.vue
│ ├── SelfHostSetting.vue
│ ├── SplashScreen.vue
│ └── TextareaAutosize.vue
│ ├── popup.html
│ └── popup.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "@babel/plugin-proposal-optional-chaining"
4 | ],
5 | "presets": [
6 | ["@babel/preset-env", {
7 | "useBuiltIns": "usage",
8 | "corejs": 3,
9 | "targets": {
10 | // https://jamie.build/last-2-versions
11 | "browsers": ["> 0.25%", "not ie 11", "not op_mini all"]
12 | }
13 | }]
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/.dev.sample.json:
--------------------------------------------------------------------------------
1 | {
2 | "DEFAULT_API_URL": "https://app.simplelogin.io",
3 | "EXTRA_ALLOWED_DOMAINS": [],
4 | "permissions": []
5 | }
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | ## code changes will send PR to following users
2 | * @acasajus @cquintana92 @nguyenkims
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | patreon: simplelogin
2 | open_collective: simplelogin
3 | custom: ["https://www.paypal.me/RealSimpleLogin"]
4 |
--------------------------------------------------------------------------------
/.github/changelog_configuration.json:
--------------------------------------------------------------------------------
1 | {
2 | "template": "${{CHANGELOG}}",
3 | "pr_template": "- ${{TITLE}} #${{NUMBER}}",
4 | "empty_template": "- no changes",
5 | "categories": [
6 | {
7 | "title": "## 🚀 Features",
8 | "labels": ["feature"]
9 | },
10 | {
11 | "title": "## 🐛 Fixes",
12 | "labels": ["fix", "bug"]
13 | },
14 | {
15 | "title": "## 🔧 Enhancements",
16 | "labels": ["enhancement"]
17 | }
18 | ],
19 | "ignore_labels": ["ignore"],
20 | "tag_resolver": {
21 | "method": "sort"
22 | }
23 | }
--------------------------------------------------------------------------------
/.github/workflows/lint.yaml:
--------------------------------------------------------------------------------
1 | name: Lint
2 | on: [push, pull_request]
3 |
4 | jobs:
5 | lint:
6 | runs-on: ubuntu-latest
7 | steps:
8 | - name: Checkout repository
9 | uses: actions/checkout@v2
10 |
11 | - name: Install NodeJS
12 | uses: actions/setup-node@v3
13 | with:
14 | node-version: 16
15 |
16 | - name: Perform linting
17 | run: |
18 | npm install
19 | npm run prettier:check
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Build the extension
2 | on:
3 | push:
4 | tags:
5 | - '[0-9]+.[0-9]+.[0-9]+'
6 |
7 | jobs:
8 | create-release:
9 | name: create-release
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout repository
13 | uses: actions/checkout@v4
14 | with:
15 | fetch-depth: 1
16 |
17 | - name: Install dependencies and check tag format
18 | run: |
19 | sudo apt update && sudo apt install -y jq
20 | PACKAGE_JSON_VERSION=$(jq -Mr '.version' package.json)
21 | EXTENSION_VERSION=${GITHUB_REF#refs/tags/}
22 | if [[ "${PACKAGE_JSON_VERSION}" != "${EXTENSION_VERSION}" ]]; then
23 | echo "Tag name does not match the version in package.json"
24 | echo "package.json: [${PACKAGE_JSON_VERSION}] | tag: [${EXTENSION_VERSION}]"
25 | exit 1
26 | fi
27 | echo "EXTENSION_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
28 |
29 | - name: Create artifacts directory
30 | run: mkdir artifacts
31 |
32 | - name: Build Changelog
33 | id: build_changelog
34 | uses: mikepenz/release-changelog-builder-action@v3
35 | with:
36 | configuration: ".github/changelog_configuration.json"
37 | env:
38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39 |
40 | - name: Prepare Slack notification contents
41 | run: |
42 | changelog=$(cat << EOH
43 | ${{ steps.build_changelog.outputs.changelog }}
44 | EOH
45 | )
46 | messageWithoutNewlines=$(echo "${changelog}" | awk '{printf "%s\\n", $0}')
47 | messageWithoutDoubleQuotes=$(echo "${messageWithoutNewlines}" | sed "s/\"/'/g")
48 | echo "${messageWithoutDoubleQuotes}"
49 |
50 | echo "${messageWithoutDoubleQuotes}" > artifacts/slack-changelog
51 |
52 | - name: Create GitHub release
53 | id: release
54 | uses: actions/create-release@v1
55 | env:
56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
57 | with:
58 | tag_name: ${{ env.EXTENSION_VERSION }}
59 | release_name: ${{ env.EXTENSION_VERSION }}
60 | body: ${{ steps.build_changelog.outputs.changelog }}
61 |
62 | - name: Save release upload URL to artifact
63 | run: echo "${{ steps.release.outputs.upload_url }}" > artifacts/release-upload-url
64 |
65 | - name: Save version number to artifact
66 | run: echo "${{ env.EXTENSION_VERSION }}" > artifacts/release-version
67 |
68 | - name: Upload artifacts
69 | uses: actions/upload-artifact@v4
70 | with:
71 | name: artifacts
72 | path: artifacts
73 |
74 | build-release:
75 | name: build-release
76 | needs: ['create-release']
77 | runs-on: ubuntu-latest
78 | strategy:
79 | max-parallel: 4
80 | matrix:
81 | variant: ['full', 'lite', 'mac', 'beta', 'beta-firefox']
82 |
83 | steps:
84 | - name: Build info
85 | run: echo "Starting build process for variant = ${{ matrix.variant }}"
86 |
87 | - name: Checkout repository
88 | uses: actions/checkout@v4
89 | with:
90 | fetch-depth: 1
91 |
92 | - name: Install NodeJS
93 | uses: actions/setup-node@v3
94 | with:
95 | node-version: 16
96 |
97 | - name: Get release download URL
98 | uses: actions/download-artifact@v4
99 | with:
100 | name: artifacts
101 | path: artifacts
102 |
103 | - name: Read artifacts
104 | shell: bash
105 | run: |
106 | # Set the release_upload_url
107 | release_upload_url="$(cat artifacts/release-upload-url)"
108 | echo "RELEASE_UPLOAD_URL=$release_upload_url" >> $GITHUB_ENV
109 | echo "release upload url: $RELEASE_UPLOAD_URL"
110 |
111 | # Set the release_version
112 | release_version="$(cat artifacts/release-version)"
113 | echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
114 | echo "release version: $RELEASE_VERSION"
115 |
116 | - name: Generate buildConfig
117 | shell: bash
118 | env:
119 | ENABLE_LOGIN_WITH_PROTON: true
120 | run: |
121 | npm run generate:buildconfig
122 |
123 | - name: Build lite version
124 | if: matrix.variant == 'lite'
125 | shell: bash
126 | run: |
127 | npm install
128 | npm run build:lite
129 | SUFFIX=lite npm run build-zip
130 |
131 | - name: Build beta version for Chromium
132 | if: matrix.variant == 'beta'
133 | shell: bash
134 | run: |
135 | npm install
136 | npm run build:beta
137 | npm run build-zip
138 |
139 | - name: Build beta version for Firefox
140 | if: matrix.variant == 'beta-firefox'
141 | shell: bash
142 | run: |
143 | npm install
144 | npm run build:firefox:beta
145 |
146 | # dist-zip/simplelogin-extension-beta-firefox-v... can be submitted to firefox
147 | SUFFIX=firefox npm run build-zip
148 |
149 | - name: Build production version for Firefox
150 | if: matrix.variant == 'beta-firefox'
151 | shell: bash
152 | run: |
153 | npm install
154 | npm run build:firefox
155 |
156 | # dist-zip/simplelogin-extension-beta-firefox-v... can be submitted to firefox
157 | SUFFIX=firefox npm run build-zip
158 |
159 | - name: Build production version for Chromium
160 | if: matrix.variant == 'full'
161 | shell: bash
162 | run: |
163 | npm install
164 | npm run build
165 | npm run build-zip
166 |
167 | - name: Build Mac version
168 | if: matrix.variant == 'mac'
169 | shell: bash
170 | run: |
171 | npm install
172 | npm run build:mac
173 | SUFFIX=mac npm run build-zip
174 |
175 | - name: Package extension
176 | run: |
177 | ZIP_NAME=$(find dist-zip -type f -name '*.zip' | head -n 1)
178 | ASSET_NAME=$(basename "${ZIP_NAME}")
179 |
180 | echo "ASSET_PATH=${ZIP_NAME}" >> $GITHUB_ENV
181 | echo "ASSET_NAME=${ASSET_NAME}" >> $GITHUB_ENV
182 |
183 | - name: Upload release archive
184 | uses: actions/upload-release-asset@v1.0.1
185 | env:
186 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
187 | with:
188 | upload_url: ${{ env.RELEASE_UPLOAD_URL }}
189 | asset_path: ${{ env.ASSET_PATH }}
190 | asset_name: ${{ env.ASSET_NAME }}
191 | asset_content_type: application/octet-stream
192 |
193 | notify:
194 | name: notify
195 | needs: ['create-release', 'build-release']
196 | runs-on: ubuntu-latest
197 | steps:
198 | - name: Get artifacts
199 | uses: actions/download-artifact@v4
200 | with:
201 | name: artifacts
202 | path: artifacts
203 |
204 | - name: Read artifacts
205 | shell: bash
206 | run: |
207 | # Set the slack-changelog
208 | slack_changelog=$(cat artifacts/slack-changelog)
209 | echo "SLACK_CHANGELOG=$slack_changelog" >> $GITHUB_ENV
210 | echo "slack changelog: $SLACK_CHANGELOG"
211 |
212 | - name: Post notification to Slack
213 | uses: slackapi/slack-github-action@v1.19.0
214 | with:
215 | channel-id: ${{ secrets.SLACK_CHANNEL_ID }}
216 | payload: |
217 | {
218 | "blocks": [
219 | {
220 | "type": "header",
221 | "text": {
222 | "type": "plain_text",
223 | "text": "New tag created on browser-extension",
224 | "emoji": true
225 | }
226 | },
227 | {
228 | "type": "section",
229 | "text": {
230 | "type": "mrkdwn",
231 | "text": "*Tag: ${{ github.ref_name }}* (${{ github.sha }})"
232 | }
233 | },
234 | {
235 | "type": "section",
236 | "text": {
237 | "type": "mrkdwn",
238 | "text": "*Changelog:*\n${{ env.SLACK_CHANGELOG }}"
239 | }
240 | }
241 | ]
242 | }
243 | env:
244 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /*.log
3 | /dist
4 | /dist-zip
5 | .DS_Store
6 | .idea/
7 | .dev.json
8 | code-for-reviewer/
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "esversion": 9
3 | }
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v16.20.2
2 |
--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to SimpleLogin Chrome/Firefox extension will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [Unreleased]
9 |
10 | ## [3.0.7] - 2025-03-20
11 |
12 | - Do not try to rerun extension setup if already logged in
13 |
14 | ## [3.0.6] - 2024-09-04
15 |
16 | - Fix onboarding on Safari
17 |
18 | ## [3.0.1-3.0.4] - 2024-04-11
19 |
20 | - Automate firefox review assets
21 |
22 | ## [3.0.0] - 2024-04-11
23 |
24 | - Use Manifest v3
25 |
26 | ## [2.11.1] - 2024-02-22
27 | - Fix alias list not displaying on Mac
28 |
29 | ## [2.11.0] - 2024-02-15
30 | - Fix upgrade button not working
31 | - Prepare the Safari extension
32 | - Enforce the reverse alias limit
33 |
34 | ## [2.10.2] - 2023-09-18
35 |
36 | - Use internal Sentry
37 |
38 | ## [2.10.0] - 2023-07-05
39 |
40 | - Fix sl-button class conflict
41 |
42 | ## [2.9.2] - 2022-11-10
43 | - Updated gecko ID for Firefox
44 |
45 | ## [2.9.1] - 2022-11-10
46 |
47 | - Support dark and OS theme
48 | - Support communication with future Mac app
49 |
50 | ## [2.7.0] - 2022-08-02
51 |
52 | - Add "Login with Proton"
53 | - Show user info on app settings page
54 | - Remove tabs permission
55 | - Fix the "create totally random alias" button includes hostname in alias
56 | - Upgrade dependencies
57 |
58 |
59 | ## [2.6.3] - 2022-05-20
60 |
61 | - Remove cookies permission
62 |
63 | ## [2.6.2] - 2022-05-20
64 |
65 | - Fixed CI
66 |
67 | ## [2.6.1] - 2022-05-20
68 |
69 | - Add version to settings page
70 | - Improve the CI
71 |
72 | ## [2.6.0] - 2022-05-20
73 |
74 | - Require previously optional permissions on install
75 | - Improve the onboarding
76 | - Update outdated packages
77 |
78 | ## [2.5.2] - 2021-10-28
79 |
80 | Fix icon not displayed
81 |
82 | ## [2.5.1] - 2021-10-28
83 |
84 | Provide additional size icons
85 |
86 | ## [2.5.0] - 2021-10-05
87 | - Support create reverse-alias
88 | - Improve UI
89 | - Update dependencies
90 |
91 | ## [2.4.3] - 2021-08-03
92 | - Support dot sign (.) in alias prefix
93 | - Update some wordings, improve UI
94 |
95 |
96 | ## [2.4.2] - 2020-09-26
97 | - Fix onboarding screen showing up on browser restart
98 |
99 | ## [2.4.1] - 2020-09-26
100 | - Reduce display glitch on Firefox
101 |
102 | ## [2.4.0] - 2020-09-26
103 | - Ctrl+Shift+S (or Cmd+Shift+S on MacOS) to open SimpleLogin pop-up
104 | - Ctrl+Shift+X (or Cmd+Shift+X on MacOS) to create a random alias
105 | - Fix window not displayed when a new alias is created
106 |
107 | ## [2.3.1] - 2020-09-07
108 | - Fix the hostname issue
109 |
110 | ## [2.3.0] - 2020-09-05
111 | - Better onboarding process
112 |
113 | ## [2.2.0] - 2020-09-03
114 | - Fix Firefox overflow menu
115 | - Better onboarding process
116 | - Do not require tabs permission.
117 |
118 | ## [2.1.0] - 2020-08-21
119 | - Improve error alert
120 |
121 | ## [2.1.0.0] - 2020-08-13 (Beta version)
122 | - Right click to create alias
123 | - Able to change alias from name, toggle PGP
124 | - Able to choose alias's mailbox(es)
125 | - UI improved
126 |
127 | ## [2.0.0.0] - 2020-07-30 (Beta version)
128 | - Create alias via the SimpleLogin button displayed in the email field
129 |
130 | ## [1.10.0] - 2020-07-16
131 | - Able to add and edit alias note
132 | - Lots of UI touches 🎨
133 |
134 | ## [1.9.1] - 2020-07-12
135 | - Take into account the case alias creation time is expired
136 |
137 | ## [1.9.0] - 2020-07-11
138 | - Able to enable/disable an alias
139 | - Able to delete an alias
140 |
141 | ## [1.8.0] - 2020-07-08
142 | - Big refactoring to make the code more modulable
143 | - Add some UI touches & fixes: log out button on top, navigation button, etc.
144 |
145 | ## [1.7.0] - 2020-07-04
146 | You can now login with email/password!
147 |
148 | ## [1.6.0] - 2020-06-24
149 | Handle 429 error.
150 | Fix Firefox scroll bar
--------------------------------------------------------------------------------
/DEVELOPMENT.md:
--------------------------------------------------------------------------------
1 | # DEVELOPMENT
2 |
3 | This document contains an overview on how the extension is organized, which parts does it have and how does it work.
4 |
5 | ## General overview
6 |
7 | The extension consists of 2 main screens:
8 |
9 | - main screen: displays email alias recommendation, alias creation and existing alias.
10 | - new alias screen: when a new alias is created, user is redirected to this screen so they can copy it.
11 |
12 |
13 | ## How to change the domain where the extension is connecting to
14 |
15 | In order to change the backend URL, you will need to:
16 |
17 | 1. Copy the `.dev.sample.json` file into a `.dev.json` file.
18 | 2. Edit the `DEFAULT_API_URL` parameter and enter the URL you want to use.
19 | 3. You may need to run `npm start` again in order for the changes to take effect.
20 |
21 | ## How does the extension setup work
22 |
23 | The extension setup process works like the following:
24 |
25 | 1. The webpage sends a message (the code can be found [here](https://github.com/simple-login/app/blob/0e3be23acc7978f6e2b1127ed78dc2147cf43515/templates/onboarding/index.html#L41-L42) and [here](https://github.com/simple-login/app/blob/0e3be23acc7978f6e2b1127ed78dc2147cf43515/templates/onboarding/setup_done.html#L31-L32))
26 | 2. The extension has a listener for events on the page, and detects it [like this](https://github.com/simple-login/browser-extension/blob/55629849838b716dabcb008898c97c4ee1118da1/src/content_script/input_tools.js#L257)
27 | 3. Once the event has been detected, the extension sends it to the background context [with this call](https://github.com/simple-login/browser-extension/blob/55629849838b716dabcb008898c97c4ee1118da1/src/content_script/input_tools.js#L260)
28 | 4. The background context [detects the event](https://github.com/simple-login/browser-extension/blob/master/src/background/index.js#L119-L120) and performs the setup. This message can only come from one of the authorized domains (see the "Add custom allowed domains" section to see how this works).
29 | 5. The setup consists on a HTTP request that will use the cookies for the SimpleLogin domain, and it will receive an API Key in the response. This API Key will be stored on the `SLStorage` and be used from then on.
30 | 6. Once the setup has been done, the user will be redirected to a page where they will be able to test the extension.
31 |
32 | Here you have a full definition of the flow:
33 |
34 | 1. Once the extension is installed, the user will be prompted with a webpage (`/onboarding`) where two things can happen:
35 | 1. If the user is already logged in, the webpage will send the message for performing the extension setup.
36 | 1. Once the setup is done, they will be redirected to the `/onboarding/final` page.
37 | 2. If the user is not logged in, they will be prompted to log in.
38 | 1. After they log in, they will be redirected to the `/onboarding/setup_done` page.
39 | 2. The page will send the message for performing the extension setup.
40 | 3. Once the setup is done, they will be redirected to the `/onboarding/final` page.
41 | 2. Once the user reaches the `/onboarding/final` page, the extension will be correctly set up, the user will be able to use it, and the page will contain the extension version at the bottom of the page
42 |
43 |
44 | ## Add custom allowed domains
45 |
46 | The messages for both performing the extension setup and for checking if it's installed are only allowed if they come from a [predefined set of origins](https://github.com/simple-login/browser-extension/blob/55629849838b716dabcb008898c97c4ee1118da1/src/background/index.js#L72-L77).
47 |
48 | However, for testing purposes there is a parameter that can be added to your dev config. You can find it in your `.dev.json`, under the name `EXTRA_ALLOWED_DOMAINS`.
49 |
50 | Keep in mind that the domains you write here will be converted to regex, so if you want to allow `*.local` you may need to write it as `.*.local`. Also take into account that only the hostname portion will be used (that means, if your page is `someserver.com:1234` only the `someserver.com` portion will be evaluated).
51 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 SimpleLogin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | SimpleLogin Chrome/Firefox extension
2 | ---
3 | <p>
4 | <a href="https://chrome.google.com/webstore/detail/simplelogin-protect-your/dphilobhebphkdjbpfohgikllaljmgbn">
5 | <img src="https://img.shields.io/chrome-web-store/rating/dphilobhebphkdjbpfohgikllaljmgbn?label=Chrome%20Extension">
6 | </a>
7 |
8 | <a href="https://addons.mozilla.org/en-GB/firefox/addon/simplelogin/">
9 | <img src="https://img.shields.io/amo/rating/simplelogin?label=Firefox%20Add-On&logo=SimpleLogin">
10 | </a>
11 |
12 | <a href="./LICENSE">
13 | <img src="https://img.shields.io/github/license/simple-login/app">
14 | </a>
15 |
16 | </p>
17 |
18 | SimpleLogin is the **open-source** privacy-first email alias and Single Sign-On (SSO) Identity Provider.
19 |
20 | More info on our website at https://simplelogin.io
21 |
22 | The extension uses VueJS with https://github.com/Kocal/vue-web-extension boilerplate.
23 |
24 | ## How to get the extension
25 |
26 | You can directly install the extension by visiting the store page for your browser:
27 |
28 | - [Google Chrome / Brave / Opera / Chromium-based](https://chrome.google.com/webstore/detail/simpleloginreceive-send-e/dphilobhebphkdjbpfohgikllaljmgbn)
29 | - [Mozilla Firefox](https://addons.mozilla.org/firefox/addon/simplelogin/)
30 | - [Microsoft Edge](https://microsoftedge.microsoft.com/addons/detail/simpleloginreceive-sen/diacfpipniklenphgljfkmhinphjlfff)
31 |
32 | ## Development information
33 |
34 | You can find more information about how the extension works and which parts it has in [DEVELOPMENT.md](./DEVELOPMENT.md)
35 |
36 | ## Contributing Guide
37 |
38 | All work on SimpleLogin Chrome/Firefox extension happens directly on GitHub.
39 |
40 | This project has been tested with Node v20.2.0 and NPM 9.6.6
41 |
42 |
43 | To run the extension locally, please first install all dependencies with `npm install`.
44 |
45 | ## Chrome
46 |
47 | Run `npm start` to generate the `/dist` folder that can be installed into Chrome.
48 |
49 | In case of `Error: error:0308010C:digital envelope routines::unsupported` error, the workaround is to accept OPEN SSL by running this command before running `npm start`
50 |
51 | ```bash
52 | export NODE_OPTIONS=--openssl-legacy-provider
53 | ````
54 |
55 | ## Firefox
56 |
57 | Run `npm run start:firefox` to generate the `/dist` folder which can then be installed on Firefox, more info on https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension#installing
58 |
59 | ## Code formatting
60 |
61 | The code is formatted using `prettier`, make sure to run it before creating the commit, otherwise the GitHub lint workflow will mark the check as not passing:
62 |
63 | ```bash
64 | npm run prettier:write
65 | ```
66 |
67 | ## How to generate a release
68 |
69 | 1. Increment the version in `package.json`.
70 | 2. Update CHANGELOG with the changes.
71 | 3. Create a tag and push it to the repository. The tag name must match the version set in `package.json`.
72 | 4. Wait until the CI process generates the extension ZIP and uploads it to GitHub. You will be able to find the generated zip as an artifact attached to the [GitHub release](https://github.com/simple-login/browser-extension/releases).
73 | 5. Upload the extension to the Chrome, Firefox and Edge stores.
74 |
75 | ### Firefox
76 |
77 | For Firefox, the code source must be submitted too. To faciliate the review process, the code source can be generated using the following script
78 |
79 | For beta version:
80 |
81 | ```bash
82 | # create the code source for firefox reviewer
83 | rm -rf code-for-reviewer && mkdir code-for-reviewer
84 |
85 | # copy the minimum files
86 | cp -r src LICENSE CHANGELOG scripts package.json package-lock.json webpack.config.js .dev.sample.json .babelrc code-for-reviewer
87 |
88 | # override the readme
89 | cp reviewers/firefox-beta.md code-for-reviewer/README.md
90 | ```
91 |
92 | For prod version
93 |
94 | ```bash
95 | # create the code source for firefox reviewer
96 | rm -rf code-for-reviewer && mkdir code-for-reviewer
97 |
98 | # copy the minimum files
99 | cp -r src LICENSE CHANGELOG scripts package.json package-lock.json webpack.config.js .dev.sample.json .babelrc code-for-reviewer
100 |
101 | # override the readme
102 | cp reviewers/firefox.md code-for-reviewer/README.md
103 | ```
104 |
105 |
106 | ## How to build the extension locally
107 |
108 | In order to build the extension yourself, please follow these steps:
109 |
110 | - Make sure you have the dependencies installed and up-to-date with `npm install`.
111 | - Run the build process with `npm run build`.
112 | - Create the zip package with `npm run build-zip`. You will find the extension in the `dist-zip/` directory.
113 | - If you want to use it on Firefox you will need to enter the `dist/` directory and run `web-ext build`. You will find the extension in the `dist/web-ext-artifacts/` directory.
114 |
115 | - (Optional, only useful for beta build) Build beta version: change `betaRev` in `package.json`, then generate zip file using
116 |
117 | ## How to build a version for Mac
118 |
119 | For the development, you can run `npm run start:mac` for the Mac app.
120 |
121 | For the production release, `npm run build:mac`
122 |
123 | ```bash
124 | npm run build:beta && npm run build-zip
125 | ```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "simplelogin-extension",
3 | "version": "3.0.7",
4 | "betaRev": "0",
5 | "description": "SimpleLogin Browser Extension",
6 | "author": "extension@simplelogin.io",
7 | "license": "MIT",
8 | "scripts": {
9 | "prettier": "prettier \"src/**/*.{js,vue}\"",
10 | "prettier:write": "npm run prettier -- --write",
11 | "prettier:check": "prettier --check \"src/**/*.{js,vue}\"",
12 | "build": "cross-env NODE_ENV=production webpack",
13 | "build:dev": "cross-env NODE_ENV=development webpack",
14 | "build:firefox": "cross-env NODE_ENV=production FIREFOX=1 webpack",
15 | "build:firefox:beta": "cross-env NODE_ENV=production BETA=1 FIREFOX=1 webpack",
16 | "build:lite": "cross-env NODE_ENV=production LITE=1 webpack",
17 | "build:beta": "cross-env NODE_ENV=production BETA=1 webpack",
18 | "build:mac": "cross-env NODE_ENV=production MAC=1 webpack",
19 | "build-zip": "node scripts/build-zip.js",
20 | "generate:buildconfig": "node scripts/generateBuildConfig.js",
21 | "start": "cross-env HMR=true NODE_ENV=development BETA=1 webpack -- --watch",
22 | "start:mac": "cross-env HMR=true NODE_ENV=development BETA=1 MAC=1 webpack -- --watch",
23 | "start:firefox": "cross-env HMR=true NODE_ENV=development BETA=1 FIREFOX=1 webpack -- --watch"
24 | },
25 | "dependencies": {
26 | "@fortawesome/fontawesome-svg-core": "^1.2.36",
27 | "@fortawesome/free-solid-svg-icons": "^5.15.4",
28 | "@fortawesome/vue-fontawesome": "^0.1.10",
29 | "@sentry/browser": "^5.25.0",
30 | "@sentry/integrations": "^5.30.0",
31 | "bootstrap": "^4.6.1",
32 | "bootstrap-vue": "^2.21.2",
33 | "browser": "^0.2.6",
34 | "tippy.js": "^6.3.7",
35 | "v-clipboard": "^2.2.3",
36 | "vue": "^2.6.14",
37 | "vue-js-modal": "^1.3.35",
38 | "vue-js-toggle-button": "^1.3.3",
39 | "vue-router": "^3.5.2",
40 | "vue-toasted": "^1.1.28",
41 | "webextension-polyfill": "^0.9.0"
42 | },
43 | "devDependencies": {
44 | "@babel/core": "^7.1.2",
45 | "@babel/plugin-proposal-class-properties": "^7.10.4",
46 | "@babel/plugin-proposal-optional-chaining": "^7.0.0",
47 | "@babel/preset-env": "^7.1.0",
48 | "@babel/runtime-corejs3": "^7.4.0",
49 | "archiver": "^3.0.0",
50 | "babel-loader": "^8.0.2",
51 | "copy-webpack-plugin": "^6.4.1",
52 | "core-js": "^3.22.7",
53 | "cross-env": "^5.2.0",
54 | "css-loader": "^2.1.1",
55 | "ejs": "^3.1.8",
56 | "file-loader": "^6.2.0",
57 | "mini-css-extract-plugin": "^0.4.4",
58 | "prettier": "^2.6.2",
59 | "sass": "^1.52.1",
60 | "sass-loader": "^10.1.1",
61 | "vue-loader": "^15.4.2",
62 | "vue-template-compiler": "^2.6.14",
63 | "web-ext-types": "^2.1.0",
64 | "webpack": "^4.46.0",
65 | "webpack-cli": "^4.9.2"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/reviewers/firefox-beta.md:
--------------------------------------------------------------------------------
1 | SimpleLogin Chrome/Firefox extension
2 | ---
3 |
4 | Please find below the instructions for building the SimpleLogin extension from source.
5 |
6 | This project has been tested with Node v20.2.0 and NPM 9.6.6.
7 |
8 | Please run the following commands to install dependencies and generate a build
9 |
10 | ```bash
11 | export NODE_OPTIONS=--openssl-legacy-provider
12 | npm install
13 | npm run build:firefox:beta
14 | npm run build-zip
15 | ```
16 |
17 | After that the build should be available in `/dist` folder. Its zip file can be found in `dist-zip` folder.
--------------------------------------------------------------------------------
/reviewers/firefox.md:
--------------------------------------------------------------------------------
1 | SimpleLogin Chrome/Firefox extension
2 | ---
3 |
4 | Please find below the instructions for building the SimpleLogin extension from source.
5 |
6 | This project has been tested with Node v20.2.0 and NPM 9.6.6.
7 |
8 | Please run the following commands to install dependencies and generate a build
9 |
10 | ```bash
11 | export NODE_OPTIONS=--openssl-legacy-provider
12 | npm install
13 | npm run build:firefox
14 | npm run build-zip
15 | ```
16 |
17 | After that the build should be available in `/dist` folder. Its zip file can be found in `dist-zip` folder.
--------------------------------------------------------------------------------
/scripts/build-zip.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const process = require('process');
4 | const fs = require('fs');
5 | const path = require('path');
6 | const archiver = require('archiver');
7 |
8 | const DEST_DIR = path.join(__dirname, '../dist');
9 | const DEST_ZIP_DIR = path.join(__dirname, '../dist-zip');
10 |
11 | const extractExtensionData = () => {
12 | const extPackageJson = require('../package.json');
13 | const distManifestJson = require('../dist/manifest.json');
14 | const isBeta = distManifestJson.name.match(/beta/i);
15 | const betaRev = extPackageJson.betaRev;
16 |
17 | return {
18 | name: extPackageJson.name + (isBeta ? '-beta' : '-release'),
19 | version: extPackageJson.version + (isBeta ? ('.' + betaRev) : ''),
20 | };
21 | };
22 |
23 | const makeDestZipDirIfNotExists = () => {
24 | if(!fs.existsSync(DEST_ZIP_DIR)) {
25 | fs.mkdirSync(DEST_ZIP_DIR);
26 | }
27 | };
28 |
29 | const buildZip = (src, dist, zipFilename) => {
30 | console.info(`Building ${zipFilename}...`);
31 |
32 | const archive = archiver('zip', { zlib: { level: 9 }});
33 | const stream = fs.createWriteStream(path.join(dist, zipFilename));
34 |
35 | return new Promise((resolve, reject) => {
36 | archive
37 | .directory(src, false)
38 | .on('error', err => reject(err))
39 | .pipe(stream);
40 |
41 | stream.on('close', () => resolve());
42 | archive.finalize();
43 | });
44 | };
45 |
46 | const extractSuffix = () => {
47 | if (process.env.SUFFIX) {
48 | return `-${process.env.SUFFIX}`;
49 | }
50 | return '';
51 | };
52 |
53 | const main = () => {
54 | const {name, version} = extractExtensionData();
55 | const suffix = extractSuffix();
56 | const zipFilename = `${name}${suffix}-v${version}.zip`;
57 |
58 | makeDestZipDirIfNotExists();
59 |
60 | buildZip(DEST_DIR, DEST_ZIP_DIR, zipFilename)
61 | .then(() => console.info('OK'))
62 | .catch(console.err);
63 | };
64 |
65 | main();
66 |
--------------------------------------------------------------------------------
/scripts/generateBuildConfig.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 |
4 | const PATH = path.join(__dirname, '../src', 'popup', 'buildConfig.json');
5 |
6 | const isLoginWithProtonEnabled = () => {
7 | const enableLoginWithProton = process.env.ENABLE_LOGIN_WITH_PROTON;
8 | if (enableLoginWithProton == undefined || enableLoginWithProton === 'true') {
9 | return true;
10 | }
11 | return false;
12 | };
13 |
14 | const config = {
15 | features: {
16 | loginWithProtonEnabled: isLoginWithProtonEnabled()
17 | },
18 | buildTime: new Date().getTime()
19 | };
20 |
21 | fs.writeFileSync(PATH, JSON.stringify(config, null, 2));
22 |
--------------------------------------------------------------------------------
/src/background/context-menu.js:
--------------------------------------------------------------------------------
1 | import { handleNewRandomAlias } from "./create-alias";
2 | import browser from "webextension-polyfill";
3 |
4 | function displayAndCopy(alias, error) {
5 | function copyTextToClipboard(text) {
6 | if (!text) return;
7 | var textArea = document.createElement("textarea");
8 | textArea.value = text;
9 |
10 | textArea.style.top = "0";
11 | textArea.style.left = "0";
12 | textArea.style.position = "fixed";
13 |
14 | document.body.appendChild(textArea);
15 | textArea.focus();
16 | textArea.select();
17 |
18 | try {
19 | document.execCommand("copy");
20 | } catch (err) {}
21 |
22 | document.body.removeChild(textArea);
23 | }
24 |
25 | function showSLDialog(message) {
26 | let slDialog = document.createElement("div");
27 | slDialog.style.position = "fixed";
28 | slDialog.style.bottom = "0";
29 | slDialog.style.right = "0";
30 | slDialog.style.margin = "0.7em";
31 | slDialog.style.padding = "0.7em";
32 | slDialog.style.fontFamily = "Verdana, Arial, Helvetica, sans-serif";
33 | slDialog.style.fontSize = "1em";
34 | slDialog.style.pointerEvents = "none";
35 | slDialog.style.zIndex = "999999";
36 | slDialog.style.background = "white";
37 | slDialog.style.border = "2px solid #777";
38 | slDialog.style.borderRadius = "5px";
39 | slDialog.innerText = JSON.stringify(message);
40 |
41 | document.body.appendChild(slDialog);
42 |
43 | setTimeout(function () {
44 | document.body.removeChild(slDialog);
45 | }, 3000);
46 | }
47 |
48 | showSLDialog(alias ? alias + " copied to clipboard" : "ERROR: " + error);
49 | copyTextToClipboard(alias);
50 | }
51 |
52 | function generateAliasHandlerJS(tab, res) {
53 | chrome.scripting
54 | .executeScript({
55 | target: { tabId: tab.id },
56 | func: displayAndCopy,
57 | args: [res.alias || null, res.error || null],
58 | })
59 | .then(() => console.log("injected a function"));
60 | }
61 |
62 | async function handleOnClickContextMenu(info, tab) {
63 | const res = await handleNewRandomAlias(info.pageUrl);
64 | generateAliasHandlerJS(tab, res);
65 | }
66 |
67 | export { handleOnClickContextMenu, generateAliasHandlerJS };
68 |
--------------------------------------------------------------------------------
/src/background/create-alias.js:
--------------------------------------------------------------------------------
1 | import {
2 | callAPI,
3 | API_ROUTE,
4 | API_ON_ERR,
5 | reloadSettings,
6 | } from "../popup/APIService";
7 | import Utils from "../popup/Utils";
8 |
9 | /**
10 | * Create random alias
11 | */
12 | async function handleNewRandomAlias(currentUrl) {
13 | await reloadSettings();
14 | const hostname = await Utils.getHostName(currentUrl);
15 | try {
16 | const res = await callAPI(
17 | API_ROUTE.NEW_RANDOM_ALIAS,
18 | {
19 | hostname,
20 | },
21 | {
22 | note: await Utils.getDefaultNote(),
23 | },
24 | API_ON_ERR.THROW
25 | );
26 |
27 | return res.data;
28 | } catch (err) {
29 | // rate limit reached
30 | if (err.response.status === 429) {
31 | return {
32 | error:
33 | "Rate limit exceeded - please wait 60s before creating new alias",
34 | };
35 | } else if (err.response.data.error) {
36 | return {
37 | error: err.response.data.error,
38 | };
39 | } else {
40 | return {
41 | error: "Unknown error",
42 | };
43 | }
44 | }
45 | }
46 |
47 | export { handleNewRandomAlias };
48 |
--------------------------------------------------------------------------------
/src/background/index.js:
--------------------------------------------------------------------------------
1 | import browser from "webextension-polyfill";
2 | import APIService, { API_ROUTE } from "../popup/APIService";
3 | import SLStorage from "../popup/SLStorage";
4 | import Onboarding from "./onboarding";
5 |
6 | import { handleNewRandomAlias } from "./create-alias";
7 | import {
8 | handleOnClickContextMenu,
9 | generateAliasHandlerJS,
10 | } from "./context-menu";
11 | import Utils from "../popup/Utils";
12 |
13 | /**
14 | * Get app settings
15 | */
16 | async function handleGetAppSettings() {
17 | const apiKey = await SLStorage.get(SLStorage.SETTINGS.API_KEY);
18 | return {
19 | showSLButton:
20 | apiKey !== "" && (await SLStorage.get(SLStorage.SETTINGS.SHOW_SL_BUTTON)),
21 | isLoggedIn: apiKey !== "",
22 | url: await SLStorage.get(SLStorage.SETTINGS.API_URL),
23 | SLButtonPosition: await SLStorage.get(
24 | SLStorage.SETTINGS.SL_BUTTON_POSITION
25 | ),
26 | };
27 | }
28 |
29 | async function finalizeExtensionSetup(apiKey) {
30 | if (!apiKey) {
31 | return;
32 | }
33 |
34 | await SLStorage.set(SLStorage.SETTINGS.API_KEY, apiKey);
35 |
36 | const currentTab = await browser.tabs.query({
37 | active: true,
38 | currentWindow: true,
39 | });
40 |
41 | const apiUrl = await SLStorage.get(SLStorage.SETTINGS.API_URL);
42 | const url = `${apiUrl}/onboarding/final`;
43 | await browser.tabs.update(currentTab[0].id, {
44 | url,
45 | });
46 | }
47 |
48 | async function handleExtensionSetup() {
49 | const apiUrl = await SLStorage.get(SLStorage.SETTINGS.API_URL);
50 |
51 | const url = apiUrl + API_ROUTE.GET_API_KEY_FROM_COOKIE.path;
52 | const res = await fetch(url, {
53 | method: "POST",
54 | headers: {
55 | "Content-Type": "application/json",
56 | "X-Sl-Allowcookies": true,
57 | },
58 | body: JSON.stringify({
59 | device: Utils.getDeviceName(),
60 | }),
61 | });
62 |
63 | if (res.ok) {
64 | const apiRes = await res.json();
65 | const apiKey = apiRes.api_key;
66 | finalizeExtensionSetup(apiKey);
67 | } else {
68 | console.error("api error");
69 | }
70 | }
71 |
72 | /**
73 | * Check if a message comes from an authorized source
74 | * @param {string} url
75 | * @returns Promise<boolean>
76 | */
77 | const isMessageAllowed = async (url) => {
78 | const requestUrl = new URL(url);
79 | const apiUrlValue = await SLStorage.get(SLStorage.SETTINGS.API_URL);
80 | const apiUrl = new URL(apiUrlValue);
81 |
82 | let allowedSources = [
83 | new RegExp(apiUrl.hostname),
84 | new RegExp("^app\\.simplelogin\\.ioquot;),
85 | new RegExp("^.*\\.protonmail\\.chquot;),
86 | new RegExp("^.*\\.protonmail\\.comquot;),
87 | ];
88 |
89 | const extraAllowedDomains =
90 | SLStorage.DEFAULT_SETTINGS[SLStorage.SETTINGS.EXTRA_ALLOWED_DOMAINS];
91 | for (const extra of extraAllowedDomains) {
92 | allowedSources.push(new RegExp(extra));
93 | }
94 |
95 | for (const source of allowedSources) {
96 | if (source.test(requestUrl.host)) return true;
97 | }
98 | return false;
99 | };
100 |
101 | /**
102 | * Handle the event of a page querying if the SL extension is installed
103 | * @return {{data: {version: string}, tag: string}}
104 | */
105 | const handleExtensionInstalledQuery = () => {
106 | const manifest = browser.runtime.getManifest();
107 | return {
108 | tag: "EXTENSION_INSTALLED_RESPONSE",
109 | data: {
110 | version: manifest.version,
111 | },
112 | };
113 | };
114 |
115 | /**
116 | * Register onMessage listener
117 | */
118 | browser.runtime.onMessage.addListener(async function (request, sender) {
119 | // Check messages allowed from everywhere
120 | if (request.tag === "NEW_RANDOM_ALIAS") {
121 | return await handleNewRandomAlias(request.currentUrl);
122 | } else if (request.tag === "GET_APP_SETTINGS") {
123 | return await handleGetAppSettings();
124 | }
125 |
126 | // Check messages allowed only from authorized sources
127 | const messageAllowed = await isMessageAllowed(sender.url);
128 | if (!messageAllowed) return;
129 |
130 | if (request.tag === "EXTENSION_SETUP") {
131 | // On Safari the background script won't set cookies properly in API calls (see https://bugs.webkit.org/show_bug.cgi?id=260676),
132 | // so we will return the API URL to the content script which will make the API call with cookies properly set
133 | return process.env.MAC
134 | ? await SLStorage.get(SLStorage.SETTINGS.API_URL)
135 | : await handleExtensionSetup();
136 | } else if (request.tag === "EXTENSION_INSTALLED_QUERY") {
137 | return handleExtensionInstalledQuery();
138 | } else if (request.tag === "SAFARI_FINALIZE_EXTENSION_SETUP") {
139 | return await finalizeExtensionSetup(request.data);
140 | }
141 | });
142 |
143 | /**
144 | * Register context menu
145 | */
146 | browser.contextMenus.create({
147 | id: "sl-random",
148 | title: "Create random email alias (copied)",
149 | contexts: ["all"],
150 | });
151 |
152 | browser.contextMenus.onClicked.addListener(handleOnClickContextMenu);
153 |
154 | /**
155 | * Shortcuts and hotkeys listener
156 | */
157 | browser.commands.onCommand.addListener(async (command) => {
158 | if (command === "generate-random-alias") {
159 | const currentTab = (
160 | await browser.tabs.query({ active: true, currentWindow: true })
161 | )[0];
162 | const res = await handleNewRandomAlias(currentTab.url);
163 | generateAliasHandlerJS(currentTab, res);
164 | }
165 | });
166 |
167 | APIService.initService();
168 | Onboarding.initService();
169 |
--------------------------------------------------------------------------------
/src/background/onboarding.js:
--------------------------------------------------------------------------------
1 | import browser from "webextension-polyfill";
2 | import SLStorage from "../popup/SLStorage";
3 |
4 | function initService() {
5 | browser.runtime.onInstalled.addListener(async function ({ reason }) {
6 | if (reason === "install") {
7 | await SLStorage.clear();
8 |
9 | const apiUrl = await SLStorage.get(SLStorage.SETTINGS.API_URL);
10 | await browser.tabs.create({
11 | url: `${apiUrl}/onboarding`,
12 | });
13 | }
14 | });
15 | }
16 |
17 | export default { initService };
18 |
--------------------------------------------------------------------------------
/src/content_script/input_tools.css:
--------------------------------------------------------------------------------
1 | .simplelogin-extension--button-wrapper {
2 | position: absolute;
3 | z-index: 999999;
4 | }
5 |
6 | .simplelogin-extension--button {
7 | cursor: pointer;
8 | box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.3);
9 | max-height: 30px;
10 | max-width: 30px;
11 | position: absolute;
12 | top: 50%;
13 | left: 50%;
14 | transform: translate(-50%, -50%);
15 | border-radius: 100px;
16 | background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!-- Generator: Adobe Illustrator 23.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E%3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='-30 -68.5 345 286.5' style='enable-background:new 0 0 265 156.86; background-color: white;' xml:space='preserve'%3E%3Cstyle type='text/css'%3E .st0%7Bfill:%23BE1E2D;%7D .st1%7Bfill:url(%23SVGID_1_);%7D%0A%3C/style%3E%3Cg%3E%3ClinearGradient id='SVGID_1_' gradientUnits='userSpaceOnUse' x1='0.1062' y1='77.7856' x2='696.6286' y2='77.7856'%3E%3Cstop offset='0' style='stop-color:%23EE307C'/%3E%3Cstop offset='1' style='stop-color:%23AA2990'/%3E%3C/linearGradient%3E%3Cpath class='st1' d='M66.82-0.04L66.77,0h0.08L66.82-0.04z M257.49,0H66.77l-2.42,2.5l-0.42,0.46c-1.83,1.92-3.75,3.75-5.66,5.5 C43.66,21.9,26.09,32.06,7.02,37.06c-0.54,0.17-1.08,0.29-1.62,0.46L2.98,38.1l-0.54,2.42c-0.21,0.87-0.37,1.75-0.54,2.67 c-1.17,6.16-1.79,12.49-1.79,18.78c0,20.57,6.2,40.31,17.95,57.09c10.58,15.12,24.9,26.9,41.64,34.23 c0.42,0.21,0.87,0.42,1.33,0.58c1,0.46,2,0.83,3,1.25l1.21,0.46l0.17,0.04l0.08-0.04c0.08,0.04,0.21,0.04,0.29,0.04h191.72 c4.16,0,7.5-3.37,7.5-7.5V7.45C264.99,3.33,261.65,0,257.49,0z M244.54,11.95l-79.83,69.58c-1.67,1.42-4.08,1.42-5.7-0.04 l-26.03-23.07c-0.12-5.58-0.71-11.12-1.79-16.53c-0.08-0.46-0.17-0.92-0.25-1.42l-0.54-2.42l-2.42-0.62 c-0.58-0.12-1.17-0.29-1.75-0.42c-13.24-3.54-25.78-9.58-37.06-17.45l-8.62-7.62H244.54z M137.11,77.62l-6.54,5.79 c0.75-3.29,1.33-6.62,1.75-10.04L137.11,77.62z M65.52,147.12c-2.46-1-4.87-2.12-7.25-3.33C27.92,128.25,8.1,96.52,8.1,61.96 c0-5.83,0.54-11.66,1.67-17.32c17.78-4.79,34.27-13.24,48.51-24.9c2.96-2.37,5.79-4.91,8.54-7.58 c13.49,13.12,29.52,23.11,47.1,29.36c3.16,1.17,6.37,2.17,9.66,3.04c0.37,2.08,0.71,4.21,0.92,6.33c0.37,3.37,0.58,6.83,0.58,10.29 c0,1.87-0.04,3.75-0.21,5.62c-0.54,9.24-2.5,18.24-5.7,26.69C110,117.76,90.68,137.67,65.52,147.12z M88.8,144.04 c14.12-9.49,25.53-22.36,33.19-37.31l23.94-21.15l12.99,11.62c1.62,1.46,4.12,1.46,5.75,0l13.24-11.74l66.46,58.51L88.8,144.04z M186.95,77.58l66.5-59.09l0.13,117.84L186.95,77.58z M83.47,124.92c3.16-2.33,6.16-4.96,8.83-7.79L83.47,124.92z M113.12,56.42 c-0.21-1.67-0.46-3.33-0.75-5c-2.62-0.71-5.21-1.54-7.75-2.5c0,0-0.04,0-0.04-0.04c-0.67-0.21-1.29-0.5-1.96-0.75 c-1.46-0.58-2.91-1.21-4.37-1.92c-1-0.46-2.04-0.92-3.04-1.46c-0.71-0.33-1.46-0.71-2.17-1.12c-2.29-1.21-4.54-2.54-6.75-3.96 c-1.37-0.83-2.71-1.75-4-2.67c-0.13-0.04-0.21-0.13-0.29-0.21c-1.42-0.96-2.79-2-4.16-3.04c-2.71-2.08-5.33-4.29-7.87-6.62 c-1.08-0.96-2.17-1.96-3.21-3c-2.71,2.62-5.54,5.16-8.49,7.58C47.2,40.73,34.54,47.85,20.93,51.51c-0.87,4.58-1.33,9.24-1.33,13.95 c0,5.62,0.67,11.16,1.87,16.53c4.75,20.65,18.2,38.98,36.81,49.18c2.42,1.33,4.87,2.5,7.45,3.54c1.5-0.58,3-1.21,4.46-1.87 c0.04,0,0.08-0.04,0.08-0.04c1.21-0.54,2.42-1.12,3.58-1.75c1.08-0.54,2.12-1.12,3.16-1.79c0.33-0.17,0.67-0.37,1-0.58 c0.54-0.33,1.08-0.67,1.58-1c0.13-0.08,0.25-0.12,0.33-0.25c0.96-0.58,1.83-1.21,2.71-1.87c0.29-0.17,0.58-0.37,0.83-0.62 l8.83-7.79c0.08,0,0.08,0,0.08-0.04c8.74-9.08,15.2-20.36,18.53-32.65c1.75-6.33,2.67-12.95,2.67-19.65 C113.58,62,113.41,59.21,113.12,56.42z M70.1,99.23l-6.79,6.79h-0.04L35.17,77.91l8.62-8.7l19.53,19.53l6.79-6.79L93.8,58.21 l8.66,8.66L70.1,99.23z'/%3E%3C/g%3E%3C/svg%3E");
17 | transition: all 0.2s ease;
18 | pointer-events: all;
19 | background-repeat: no-repeat;
20 | background-position: center center;
21 | background-color: white;
22 | background-size: contain;
23 | }
24 |
25 | .simplelogin-extension--button:hover {
26 | transform: translate(-50%, -50%) scale(1.2);
27 | }
28 |
29 | .simplelogin-extension--button.loading {
30 | background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' style='margin: auto; background: none; display: block; shape-rendering: auto;' width='200px' height='200px' viewBox='0 0 100 100' preserveAspectRatio='xMidYMid'%3E%3Ccircle cx='50' cy='50' fill='none' stroke='%23aa2990' stroke-width='10' r='35' stroke-dasharray='164.93361431346415 56.97787143782138' transform='rotate(178.465 50 50)'%3E%3CanimateTransform attributeName='transform' type='rotate' repeatCount='indefinite' dur='1s' values='0 50 50;360 50 50' keyTimes='0;1'%3E%3C/animateTransform%3E%3C/circle%3E%3C/svg%3E");
31 | }
32 |
33 | .simplelogin-extension--button.loading:hover {
34 | transform: translate(-50%, -50%);
35 | }
36 |
--------------------------------------------------------------------------------
/src/content_script/input_tools.js:
--------------------------------------------------------------------------------
1 | if (!window._hasExecutedSlExtension) {
2 | window._hasExecutedSlExtension = true;
3 |
4 | /**
5 | * Send message to background.js and resolve with the response
6 | * @param {string} tag
7 | * @param {object} data
8 | */
9 | const sendMessageToBackground = (tag, data = null) => {
10 | const _browser = window.chrome || browser;
11 | return new Promise((resolve) => {
12 | try {
13 | _browser.runtime.sendMessage(
14 | {
15 | tag,
16 | data,
17 | },
18 | function (response) {
19 | resolve(response);
20 | }
21 | );
22 | } catch (e) {
23 | // Extension context invalidated.
24 | console.error(e);
25 | }
26 | });
27 | };
28 |
29 | const slButtonLogic = async () => {
30 | if (!window.hasSLButton) {
31 | window.hasSLButton = true;
32 |
33 | const InputTools = {
34 | isLoading: false,
35 |
36 | // store tracked input elements
37 | trackedElements: [],
38 |
39 | init(target) {
40 | InputTools.queryEmailInputAndApply(target, (element) => {
41 | if (!InputTools.isValidEmailInput(element)) {
42 | return;
43 | }
44 |
45 | // ignore if this elements has already been tracked
46 | const i = InputTools.trackedElements.indexOf(element);
47 | if (i === -1) {
48 | InputTools.trackedElements.push(element);
49 | InputTools.addSLButtonToInput(element);
50 | }
51 | });
52 | },
53 |
54 | destroy(target) {
55 | InputTools.queryEmailInputAndApply(target, (element) => {
56 | // remove element from tracking list
57 | const i = InputTools.trackedElements.indexOf(element);
58 | if (i !== -1) {
59 | InputTools.trackedElements.splice(i, 1);
60 | }
61 | });
62 | },
63 |
64 | queryEmailInputAndApply(target, actionFunction) {
65 | if (!target.querySelectorAll) return;
66 | const elements = target.querySelectorAll(
67 | "input[type='email'],input[name*='email'],input[id*='email']"
68 | );
69 | for (const element of elements) {
70 | actionFunction(element);
71 | }
72 | },
73 |
74 | isValidEmailInput(element) {
75 | const style = getComputedStyle(element);
76 | return (
77 | // check if element is visible
78 | style.visibility !== "hidden" &&
79 | style.display !== "none" &&
80 | style.opacity !== "0" &&
81 | style.pointerEvents === "auto" &&
82 | // check if element is not disabled
83 | !element.disabled &&
84 | // for example, we must filter out a checkbox with name*=email
85 | // check if element is text or email input
86 | (element.type === "text" || element.type === "email")
87 | );
88 | },
89 |
90 | addSLButtonToInput(inputElem) {
91 | // create wrapper for SL button
92 | const btnWrapper = InputTools.newDiv(
93 | "simplelogin-extension--button-wrapper"
94 | );
95 | const inputSumHeight =
96 | inputElem.getBoundingClientRect().height + "px";
97 | btnWrapper.style.height = inputSumHeight;
98 | btnWrapper.style.width = inputSumHeight;
99 | document.body.appendChild(btnWrapper);
100 |
101 | // create the SL button
102 | const slButton = InputTools.newDiv("simplelogin-extension--button");
103 | slButton.addEventListener("click", function () {
104 | InputTools.handleOnClickSLButton(inputElem, slButton);
105 | });
106 | slButton.style.height = inputSumHeight;
107 | slButton.style.width = inputSumHeight;
108 | btnWrapper.appendChild(slButton);
109 |
110 | InputTools.placeBtnToTheRightOfElement(inputElem, btnWrapper);
111 | },
112 |
113 | newDiv(...className) {
114 | const div = document.createElement("div");
115 | div.classList.add(...className);
116 | return div;
117 | },
118 |
119 | placeBtnToTheRightOfElement(inputElem, btnWrapper) {
120 | let intervalId = 0;
121 |
122 | function updatePosition() {
123 | // check is element is removed from trackedElements
124 | const i = InputTools.trackedElements.indexOf(inputElem);
125 | if (i === -1) {
126 | btnWrapper.parentNode.removeChild(btnWrapper);
127 | clearInterval(intervalId);
128 | }
129 |
130 | // get dimension & position of input
131 | const inputCoords = inputElem.getBoundingClientRect();
132 | const inputStyle = getComputedStyle(inputElem);
133 | const elemWidth = InputTools.dimensionToInt(btnWrapper.style.width);
134 | const pageXOffset = window.pageXOffset;
135 | const pageYOffset = window.pageYOffset;
136 | const buttonXOffset =
137 | SLSettings.SLButtonPosition === "right-inside"
138 | ? -elemWidth * 1.02
139 | : elemWidth * 0.02;
140 |
141 | // calculate elem position
142 | const left =
143 | InputTools.sumPixel([
144 | inputCoords.left,
145 | pageXOffset,
146 | inputElem.offsetWidth,
147 | buttonXOffset,
148 | -inputStyle.paddingRight,
149 | ]) + "px";
150 |
151 | const top =
152 | InputTools.sumPixel([inputCoords.top, pageYOffset]) + "px";
153 |
154 | if (btnWrapper.style.left !== left) {
155 | btnWrapper.style.left = left;
156 | }
157 |
158 | if (btnWrapper.style.top !== top) {
159 | btnWrapper.style.top = top;
160 | }
161 | }
162 |
163 | intervalId = setInterval(updatePosition, 200);
164 | updatePosition();
165 | },
166 |
167 | async handleOnClickSLButton(inputElem, slButton) {
168 | if (InputTools.isLoading) {
169 | return;
170 | }
171 | InputTools.isLoading = true;
172 | slButton.classList.add("loading");
173 |
174 | let res = await sendMessageToBackground("NEW_RANDOM_ALIAS", {
175 | currentUrl: window.location.href,
176 | });
177 | if (res.error) {
178 | alert("SimpleLogin Error: " + res.error);
179 | res = { alias: "" };
180 | }
181 |
182 | InputTools.isLoading = false;
183 | slButton.classList.remove("loading");
184 |
185 | inputElem.value = res.alias;
186 | },
187 |
188 | sumPixel(dimensions) {
189 | let sum = 0;
190 | for (const dim of dimensions) {
191 | sum += !isNaN(dim) ? dim : InputTools.dimensionToInt(dim);
192 | }
193 | return sum;
194 | },
195 |
196 | dimensionToInt(dim) {
197 | try {
198 | const pixel = parseFloat(dim.replace(/[^0-9.]+/g, ""));
199 | return isNaN(pixel) ? 0 : pixel;
200 | } catch (e) {
201 | return 0;
202 | }
203 | },
204 | };
205 |
206 | const MutationObserver =
207 | window.MutationObserver ||
208 | window.WebKitMutationObserver ||
209 | window.MozMutationObserver;
210 |
211 | /**
212 | * Add DOM mutations listener
213 | */
214 | const addMutationObserver = () => {
215 | const mutationObserver = new MutationObserver(function (mutations) {
216 | mutations.forEach(function (mutation) {
217 | for (const addedNode of mutation.addedNodes) {
218 | // add SLButton for newly added nodes
219 | InputTools.init(addedNode);
220 | }
221 |
222 | for (const removedNode of mutation.removedNodes) {
223 | // destroy SLButton for removed nodes
224 | InputTools.destroy(removedNode);
225 | }
226 | });
227 | });
228 |
229 | const target = document.body;
230 | if (!target) return;
231 |
232 | mutationObserver.observe(target, {
233 | childList: true,
234 | subtree: true,
235 | });
236 | };
237 |
238 | const SLSettings = await sendMessageToBackground("GET_APP_SETTINGS");
239 | if (SLSettings.showSLButton) {
240 | InputTools.init(document);
241 | addMutationObserver();
242 | }
243 | }
244 | };
245 |
246 | const slRegisterListener = () => {
247 | if (!window.hasSlListenerRegistered) {
248 | window.hasSlListenerRegistered = true;
249 |
250 | let hasProcessedSetup = false;
251 |
252 | /**
253 | * Callback called for every event
254 | * @param {MessageEvent<any>} event
255 | */
256 | const onEvent = async (event) => {
257 | if (event.source !== window) return;
258 | if (!event.data) return;
259 | if (!event.data.tag) return;
260 | if (event.data.tag === "PERFORM_EXTENSION_SETUP") {
261 | const SLSettings = await sendMessageToBackground("GET_APP_SETTINGS");
262 | if (SLSettings.isLoggedIn) {
263 | console.log(
264 | "Received PERFORM_EXTENSION_SETUP but extension is already logged in. Redirecting to dashboard"
265 | );
266 | window.location.href = `${SLSettings.url}/dashboard/`;
267 | return;
268 | }
269 |
270 | if (!hasProcessedSetup) {
271 | hasProcessedSetup = true;
272 | const apiUrl = await sendMessageToBackground("EXTENSION_SETUP");
273 | // if apiUrl is undefined then the Chromium/Firefox extension has already finished setup
274 | if (!apiUrl) {
275 | return;
276 | }
277 | // else if apiUrl is defined, we are in Safari and need to setup the Safari extension
278 | const url = apiUrl + "/api/api_key";
279 | const res = await fetch(url, {
280 | method: "POST",
281 | headers: {
282 | "Content-Type": "application/json",
283 | "X-Sl-Allowcookies": true,
284 | },
285 | body: JSON.stringify({
286 | device: "Safari extension",
287 | }),
288 | });
289 |
290 | if (res.ok) {
291 | const apiRes = await res.json();
292 | const apiKey = apiRes.api_key;
293 | await sendMessageToBackground(
294 | "SAFARI_FINALIZE_EXTENSION_SETUP",
295 | apiKey
296 | );
297 | }
298 | }
299 | } else if (event.data.tag === "EXTENSION_INSTALLED_QUERY") {
300 | const res = await sendMessageToBackground(
301 | "EXTENSION_INSTALLED_QUERY"
302 | );
303 | window.postMessage(res);
304 | }
305 | };
306 |
307 | window.addEventListener("message", onEvent);
308 | }
309 | };
310 |
311 | slButtonLogic();
312 | slRegisterListener();
313 | }
314 |
--------------------------------------------------------------------------------
/src/icons/icon_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/icons/icon_128.png
--------------------------------------------------------------------------------
/src/icons/icon_16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/icons/icon_16.png
--------------------------------------------------------------------------------
/src/icons/icon_32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/icons/icon_32.png
--------------------------------------------------------------------------------
/src/icons/icon_48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/icons/icon_48.png
--------------------------------------------------------------------------------
/src/icons/icon_96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/icons/icon_96.png
--------------------------------------------------------------------------------
/src/icons/icon_beta_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/icons/icon_beta_128.png
--------------------------------------------------------------------------------
/src/icons/icon_beta_48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/icons/icon_beta_48.png
--------------------------------------------------------------------------------
/src/images/arrow-up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/images/arrow-up.png
--------------------------------------------------------------------------------
/src/images/back-button.svg:
--------------------------------------------------------------------------------
1 | <?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg height="512px" id="Layer_1" style="enable-background:new 0 0 512 512;" version="1.1" viewBox="0 0 512 512" width="512px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><polygon points="352,128.4 319.7,96 160,256 160,256 160,256 319.7,416 352,383.6 224.7,256 "/></svg>
--------------------------------------------------------------------------------
/src/images/chrome-permission-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/images/chrome-permission-screenshot.png
--------------------------------------------------------------------------------
/src/images/firefox-permission-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/images/firefox-permission-screenshot.png
--------------------------------------------------------------------------------
/src/images/horizontal-logo.svg:
--------------------------------------------------------------------------------
1 | <?xml version="1.0" encoding="utf-8"?>
2 | <!-- Generator: Adobe Illustrator 23.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3 | <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4 | viewBox="0 0 1024 156.86" style="enable-background:new 0 0 1024 156.86;" xml:space="preserve">
5 | <style type="text/css">
6 | .st0{fill:#BE1E2D;}
7 | .st1{fill:url(#SVGID_1_);}
8 | </style>
9 | <g>
10 | <path class="st0" d="M763.52,124.2h-49.08V29.6h11.08v84.57h38V124.2z"/>
11 | <path class="st0" d="M804.82,125.79c-9.98,0-17.95-3.15-23.91-9.47c-5.96-6.31-8.94-14.68-8.94-25.1c0-11.35,3.1-20.21,9.3-26.59
12 | s14.58-9.57,25.13-9.57c10.07,0,17.93,3.1,23.59,9.3c5.65,6.2,8.48,14.8,8.48,25.79c0,10.78-3.05,19.41-9.14,25.89
13 | C823.24,122.54,815.07,125.79,804.82,125.79z M805.61,64.17c-6.95,0-12.45,2.37-16.49,7.09c-4.05,4.73-6.07,11.25-6.07,19.56
14 | c0,8.01,2.04,14.32,6.14,18.93c4.09,4.62,9.57,6.93,16.43,6.93c6.99,0,12.37-2.26,16.13-6.79c3.76-4.53,5.64-10.97,5.64-19.33
15 | c0-8.44-1.88-14.95-5.64-19.53C817.98,66.46,812.6,64.17,805.61,64.17z"/>
16 | <path class="st0" d="M913.41,118.79c0,24.8-11.87,37.21-35.62,37.21c-8.36,0-15.66-1.58-21.9-4.75v-10.82
17 | c7.61,4.22,14.87,6.33,21.77,6.33c16.62,0,24.94-8.84,24.94-26.52v-7.39h-0.26c-5.15,8.62-12.89,12.93-23.22,12.93
18 | c-8.4,0-15.16-3-20.29-9c-5.12-6-7.69-14.06-7.69-24.18c0-11.48,2.76-20.6,8.28-27.38c5.52-6.77,13.07-10.16,22.66-10.16
19 | c9.1,0,15.86,3.65,20.25,10.95h0.26v-9.37h10.82V118.79z M902.59,93.66V83.7c0-5.36-1.81-9.96-5.44-13.79
20 | c-3.63-3.83-8.15-5.74-13.56-5.74c-6.69,0-11.92,2.43-15.7,7.29c-3.78,4.86-5.67,11.67-5.67,20.42c0,7.52,1.81,13.54,5.44,18.04
21 | c3.63,4.51,8.43,6.76,14.41,6.76c6.07,0,11.01-2.15,14.81-6.46C900.68,105.91,902.59,100.39,902.59,93.66z"/>
22 | <path class="st0" d="M940.85,39.5c-1.94,0-3.58-0.66-4.95-1.98c-1.36-1.32-2.05-2.99-2.05-5.01c0-2.02,0.68-3.7,2.05-5.05
23 | c1.36-1.34,3.01-2.01,4.95-2.01c1.98,0,3.66,0.67,5.05,2.01c1.39,1.34,2.08,3.02,2.08,5.05c0,1.94-0.69,3.58-2.08,4.95
24 | C944.51,38.82,942.83,39.5,940.85,39.5z M946.13,124.2h-10.82V56.65h10.82V124.2z"/>
25 | <path class="st0" d="M1024.11,124.2h-10.82V85.68c0-14.34-5.23-21.51-15.7-21.51c-5.41,0-9.89,2.04-13.43,6.1
26 | c-3.54,4.07-5.31,9.2-5.31,15.4v38.53h-10.82V56.65h10.82v11.22h0.26c5.1-8.53,12.49-12.8,22.17-12.8c7.39,0,13.04,2.39,16.95,7.16
27 | c3.91,4.77,5.87,11.67,5.87,20.68V124.2z"/>
28 | <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="0.1062" y1="77.7856" x2="696.6286" y2="77.7856">
29 | <stop offset="0" style="stop-color:#EE307C"/>
30 | <stop offset="1" style="stop-color:#AA2990"/>
31 | </linearGradient>
32 | <path class="st1" d="M306.35,120.38v-13.06c1.49,1.32,3.29,2.51,5.38,3.56c2.09,1.06,4.29,1.95,6.6,2.67
33 | c2.31,0.72,4.63,1.29,6.96,1.68c2.33,0.4,4.49,0.59,6.46,0.59c6.82,0,11.91-1.26,15.27-3.79c3.36-2.53,5.05-6.17,5.05-10.92
34 | c0-2.55-0.56-4.77-1.68-6.66c-1.12-1.89-2.67-3.62-4.65-5.18c-1.98-1.56-4.32-3.06-7.03-4.49c-2.7-1.43-5.62-2.94-8.74-4.52
35 | c-3.3-1.67-6.38-3.36-9.24-5.08s-5.34-3.61-7.45-5.67c-2.11-2.07-3.77-4.41-4.98-7.03c-1.21-2.62-1.81-5.68-1.81-9.2
36 | c0-4.31,0.94-8.06,2.84-11.25c1.89-3.19,4.38-5.82,7.45-7.88c3.08-2.07,6.59-3.61,10.52-4.62c3.93-1.01,7.95-1.52,12.04-1.52
37 | c9.32,0,16.12,1.12,20.39,3.36v12.47c-5.59-3.87-12.76-5.81-21.51-5.81c-2.42,0-4.84,0.25-7.26,0.76
38 | c-2.42,0.51-4.57,1.33-6.46,2.47c-1.89,1.14-3.43,2.62-4.62,4.42c-1.19,1.8-1.78,4-1.78,6.6c0,2.42,0.45,4.51,1.35,6.27
39 | c0.9,1.76,2.23,3.37,3.99,4.82c1.76,1.45,3.9,2.86,6.43,4.22c2.53,1.36,5.44,2.86,8.74,4.49c3.39,1.67,6.6,3.43,9.63,5.28
40 | c3.04,1.85,5.7,3.89,7.98,6.14c2.29,2.24,4.1,4.73,5.44,7.45c1.34,2.73,2.01,5.85,2.01,9.37c0,4.66-0.91,8.61-2.74,11.84
41 | c-1.82,3.23-4.29,5.86-7.39,7.88c-3.1,2.02-6.67,3.49-10.72,4.39c-4.05,0.9-8.31,1.35-12.8,1.35c-1.5,0-3.34-0.12-5.54-0.36
42 | c-2.2-0.24-4.44-0.59-6.73-1.06c-2.29-0.46-4.45-1.03-6.5-1.71C309.22,121.97,307.58,121.21,306.35,120.38z M386.63,39.5
43 | c-1.94,0-3.58-0.66-4.95-1.98c-1.36-1.32-2.05-2.99-2.05-5.01c0-2.02,0.68-3.7,2.05-5.05c1.36-1.34,3.01-2.01,4.95-2.01
44 | c1.98,0,3.66,0.67,5.05,2.01c1.39,1.34,2.08,3.02,2.08,5.05c0,1.94-0.69,3.58-2.08,4.95C390.29,38.82,388.61,39.5,386.63,39.5z
45 | M391.91,124.2h-10.82V56.65h10.82V124.2z M509.73,124.2h-10.82V85.41c0-7.48-1.16-12.89-3.46-16.23
46 | c-2.31-3.34-6.19-5.01-11.64-5.01c-4.62,0-8.54,2.11-11.78,6.33c-3.23,4.22-4.85,9.28-4.85,15.17v38.53h-10.82V84.09
47 | c0-13.28-5.12-19.92-15.37-19.92c-4.75,0-8.67,1.99-11.74,5.97c-3.08,3.98-4.62,9.16-4.62,15.54v38.53h-10.82V56.65h10.82v10.69
48 | h0.26c4.79-8.18,11.79-12.27,20.98-12.27c4.62,0,8.64,1.29,12.07,3.86c3.43,2.57,5.78,5.95,7.06,10.13
49 | c5.01-9.32,12.49-13.99,22.43-13.99c14.87,0,22.3,9.17,22.3,27.51V124.2z M541.27,114.44H541v40.83h-10.82V56.65H541v11.88h0.26
50 | c5.32-8.97,13.11-13.46,23.35-13.46c8.71,0,15.5,3.02,20.38,9.07c4.88,6.05,7.32,14.15,7.32,24.31c0,11.3-2.75,20.35-8.25,27.15
51 | c-5.5,6.79-13.02,10.19-22.56,10.19C552.77,125.79,546.02,122,541.27,114.44z M541,87.19v9.43c0,5.59,1.81,10.32,5.44,14.22
52 | c3.63,3.89,8.24,5.84,13.82,5.84c6.55,0,11.69-2.51,15.4-7.52c3.72-5.01,5.57-11.98,5.57-20.91c0-7.52-1.74-13.41-5.21-17.68
53 | c-3.47-4.27-8.18-6.4-14.12-6.4c-6.29,0-11.35,2.19-15.17,6.56S541,80.6,541,87.19z M620.43,124.2h-10.82V24.19h10.82V124.2z
54 | M696.63,93.13h-47.7c0.17,7.52,2.2,13.33,6.07,17.42c3.87,4.09,9.19,6.13,15.96,6.13c7.61,0,14.6-2.51,20.98-7.52v10.16
55 | c-5.94,4.31-13.79,6.46-23.55,6.46c-9.54,0-17.04-3.07-22.5-9.2c-5.45-6.13-8.18-14.77-8.18-25.89c0-10.51,2.98-19.08,8.94-25.7
56 | c5.96-6.62,13.36-9.93,22.2-9.93c8.84,0,15.68,2.86,20.52,8.58s7.26,13.66,7.26,23.81V93.13z M685.54,83.96
57 | c-0.04-6.24-1.55-11.1-4.52-14.58c-2.97-3.47-7.09-5.21-12.37-5.21c-5.1,0-9.43,1.83-13,5.48c-3.56,3.65-5.76,8.42-6.6,14.31
58 | H685.54z M66.82-0.04L66.77,0h0.08L66.82-0.04z M257.49,0H66.77l-2.42,2.5l-0.42,0.46c-1.83,1.92-3.75,3.75-5.66,5.5
59 | C43.66,21.9,26.09,32.06,7.02,37.06c-0.54,0.17-1.08,0.29-1.62,0.46L2.98,38.1l-0.54,2.42c-0.21,0.87-0.37,1.75-0.54,2.67
60 | c-1.17,6.16-1.79,12.49-1.79,18.78c0,20.57,6.2,40.31,17.95,57.09c10.58,15.12,24.9,26.9,41.64,34.23
61 | c0.42,0.21,0.87,0.42,1.33,0.58c1,0.46,2,0.83,3,1.25l1.21,0.46l0.17,0.04l0.08-0.04c0.08,0.04,0.21,0.04,0.29,0.04h191.72
62 | c4.16,0,7.5-3.37,7.5-7.5V7.45C264.99,3.33,261.65,0,257.49,0z M244.54,11.95l-79.83,69.58c-1.67,1.42-4.08,1.42-5.7-0.04
63 | l-26.03-23.07c-0.12-5.58-0.71-11.12-1.79-16.53c-0.08-0.46-0.17-0.92-0.25-1.42l-0.54-2.42l-2.42-0.62
64 | c-0.58-0.12-1.17-0.29-1.75-0.42c-13.24-3.54-25.78-9.58-37.06-17.45l-8.62-7.62H244.54z M137.11,77.62l-6.54,5.79
65 | c0.75-3.29,1.33-6.62,1.75-10.04L137.11,77.62z M65.52,147.12c-2.46-1-4.87-2.12-7.25-3.33C27.92,128.25,8.1,96.52,8.1,61.96
66 | c0-5.83,0.54-11.66,1.67-17.32c17.78-4.79,34.27-13.24,48.51-24.9c2.96-2.37,5.79-4.91,8.54-7.58
67 | c13.49,13.12,29.52,23.11,47.1,29.36c3.16,1.17,6.37,2.17,9.66,3.04c0.37,2.08,0.71,4.21,0.92,6.33c0.37,3.37,0.58,6.83,0.58,10.29
68 | c0,1.87-0.04,3.75-0.21,5.62c-0.54,9.24-2.5,18.24-5.7,26.69C110,117.76,90.68,137.67,65.52,147.12z M88.8,144.04
69 | c14.12-9.49,25.53-22.36,33.19-37.31l23.94-21.15l12.99,11.62c1.62,1.46,4.12,1.46,5.75,0l13.24-11.74l66.46,58.51L88.8,144.04z
70 | M186.95,77.58l66.5-59.09l0.13,117.84L186.95,77.58z M83.47,124.92c3.16-2.33,6.16-4.96,8.83-7.79L83.47,124.92z M113.12,56.42
71 | c-0.21-1.67-0.46-3.33-0.75-5c-2.62-0.71-5.21-1.54-7.75-2.5c0,0-0.04,0-0.04-0.04c-0.67-0.21-1.29-0.5-1.96-0.75
72 | c-1.46-0.58-2.91-1.21-4.37-1.92c-1-0.46-2.04-0.92-3.04-1.46c-0.71-0.33-1.46-0.71-2.17-1.12c-2.29-1.21-4.54-2.54-6.75-3.96
73 | c-1.37-0.83-2.71-1.75-4-2.67c-0.13-0.04-0.21-0.13-0.29-0.21c-1.42-0.96-2.79-2-4.16-3.04c-2.71-2.08-5.33-4.29-7.87-6.62
74 | c-1.08-0.96-2.17-1.96-3.21-3c-2.71,2.62-5.54,5.16-8.49,7.58C47.2,40.73,34.54,47.85,20.93,51.51c-0.87,4.58-1.33,9.24-1.33,13.95
75 | c0,5.62,0.67,11.16,1.87,16.53c4.75,20.65,18.2,38.98,36.81,49.18c2.42,1.33,4.87,2.5,7.45,3.54c1.5-0.58,3-1.21,4.46-1.87
76 | c0.04,0,0.08-0.04,0.08-0.04c1.21-0.54,2.42-1.12,3.58-1.75c1.08-0.54,2.12-1.12,3.16-1.79c0.33-0.17,0.67-0.37,1-0.58
77 | c0.54-0.33,1.08-0.67,1.58-1c0.13-0.08,0.25-0.12,0.33-0.25c0.96-0.58,1.83-1.21,2.71-1.87c0.29-0.17,0.58-0.37,0.83-0.62
78 | l8.83-7.79c0.08,0,0.08,0,0.08-0.04c8.74-9.08,15.2-20.36,18.53-32.65c1.75-6.33,2.67-12.95,2.67-19.65
79 | C113.58,62,113.41,59.21,113.12,56.42z M70.1,99.23l-6.79,6.79h-0.04L35.17,77.91l8.62-8.7l19.53,19.53l6.79-6.79L93.8,58.21
80 | l8.66,8.66L70.1,99.23z"/>
81 | </g>
82 | </svg>
83 |
--------------------------------------------------------------------------------
/src/images/icon-copy.svg:
--------------------------------------------------------------------------------
1 | <?xml version="1.0" encoding="utf-8"?>
2 | <!-- Generated by IcoMoon.io -->
3 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
4 | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
5 | <path fill="#b02a8f" d="M10 4v-4h-7l-3 3v9h6v4h10v-12h-6zM3 1.414v1.586h-1.586l1.586-1.586zM1 11v-7h3v-3h5v3l-3 3v4h-5zM9 5.414v1.586h-1.586l1.586-1.586zM15 15h-8v-7h3v-3h5v10z"></path>
6 | </svg>
7 |
--------------------------------------------------------------------------------
/src/images/icon-dropdown.svg:
--------------------------------------------------------------------------------
1 | <?xml version="1.0" encoding="utf-8"?>
2 | <!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon -->
3 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
4 | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve" width="14" height="14">
5 | <g><path fill="#b02a8f" d="M849.4,238L500,587.4L150.6,238c-32.1-32.1-84.3-32.1-116.5,0C2,270.1,2,322.3,34.1,354.4l407.6,407.6c32.1,32.1,84.3,32.1,116.5,0l407.6-407.6c32.1-32.1,32.1-84.3,0-116.4C933.7,205.8,881.5,205.8,849.4,238L849.4,238z"/></g>
6 | </svg>
--------------------------------------------------------------------------------
/src/images/icon-more.svg:
--------------------------------------------------------------------------------
1 | <svg id="Capa_1" enable-background="new 0 0 515.555 515.555" height="16" viewBox="0 0 515.555 515.555" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m303.347 18.875c25.167 25.167 25.167 65.971 0 91.138s-65.971 25.167-91.138 0-25.167-65.971 0-91.138c25.166-25.167 65.97-25.167 91.138 0" fill="#b02a8f" /><path d="m303.347 212.209c25.167 25.167 25.167 65.971 0 91.138s-65.971 25.167-91.138 0-25.167-65.971 0-91.138c25.166-25.167 65.97-25.167 91.138 0" fill="#b02a8f" /><path d="m303.347 405.541c25.167 25.167 25.167 65.971 0 91.138s-65.971 25.167-91.138 0-25.167-65.971 0-91.138c25.166-25.167 65.97-25.167 91.138 0" fill="#b02a8f" /></svg>
--------------------------------------------------------------------------------
/src/images/icon-puzzle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/images/icon-puzzle.png
--------------------------------------------------------------------------------
/src/images/icon-settings.svg:
--------------------------------------------------------------------------------
1 | <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
2 | <path d="M0 0h48v48h-48z" fill="none"/>
3 | <path d="M38.86 25.95c.08-.64.14-1.29.14-1.95s-.06-1.31-.14-1.95l4.23-3.31c.38-.3.49-.84.24-1.28l-4-6.93c-.25-.43-.77-.61-1.22-.43l-4.98 2.01c-1.03-.79-2.16-1.46-3.38-1.97l-.75-5.3c-.09-.47-.5-.84-1-.84h-8c-.5 0-.91.37-.99.84l-.75 5.3c-1.22.51-2.35 1.17-3.38 1.97l-4.98-2.01c-.45-.17-.97 0-1.22.43l-4 6.93c-.25.43-.14.97.24 1.28l4.22 3.31c-.08.64-.14 1.29-.14 1.95s.06 1.31.14 1.95l-4.22 3.31c-.38.3-.49.84-.24 1.28l4 6.93c.25.43.77.61 1.22.43l4.98-2.01c1.03.79 2.16 1.46 3.38 1.97l.75 5.3c.08.47.49.84.99.84h8c.5 0 .91-.37.99-.84l.75-5.3c1.22-.51 2.35-1.17 3.38-1.97l4.98 2.01c.45.17.97 0 1.22-.43l4-6.93c.25-.43.14-.97-.24-1.28l-4.22-3.31zm-14.86 5.05c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z" fill="#b02a8f" />
4 | </svg>
5 |
--------------------------------------------------------------------------------
/src/images/icon-simplelogin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/images/icon-simplelogin.png
--------------------------------------------------------------------------------
/src/images/icon-trash.svg:
--------------------------------------------------------------------------------
1 | <?xml version="1.0" encoding="utf-8"?>
2 | <!-- Generated by IcoMoon.io -->
3 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
4 | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
5 | <path fill="#dc3545" d="M2 5v10c0 0.55 0.45 1 1 1h9c0.55 0 1-0.45 1-1v-10h-11zM5 14h-1v-7h1v7zM7 14h-1v-7h1v7zM9 14h-1v-7h1v7zM11 14h-1v-7h1v7z"></path>
6 | <path fill="#dc3545" d="M13.25 2h-3.25v-1.25c0-0.412-0.338-0.75-0.75-0.75h-3.5c-0.412 0-0.75 0.338-0.75 0.75v1.25h-3.25c-0.413 0-0.75 0.337-0.75 0.75v1.25h13v-1.25c0-0.413-0.338-0.75-0.75-0.75zM9 2h-3v-0.987h3v0.987z"></path>
7 | </svg>
8 |
--------------------------------------------------------------------------------
/src/images/loading-three-dots.svg:
--------------------------------------------------------------------------------
1 | <!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
2 | <svg width="120" height="30" viewBox="0 0 120 30" xmlns="http://www.w3.org/2000/svg" fill="#b02a8f">
3 | <circle cx="15" cy="15" r="15">
4 | <animate attributeName="r" from="15" to="15"
5 | begin="0s" dur="0.8s"
6 | values="15;9;15" calcMode="linear"
7 | repeatCount="indefinite" />
8 | <animate attributeName="fill-opacity" from="1" to="1"
9 | begin="0s" dur="0.8s"
10 | values="1;.5;1" calcMode="linear"
11 | repeatCount="indefinite" />
12 | </circle>
13 | <circle cx="60" cy="15" r="9" fill-opacity="0.3">
14 | <animate attributeName="r" from="9" to="9"
15 | begin="0s" dur="0.8s"
16 | values="9;15;9" calcMode="linear"
17 | repeatCount="indefinite" />
18 | <animate attributeName="fill-opacity" from="0.5" to="0.5"
19 | begin="0s" dur="0.8s"
20 | values=".5;1;.5" calcMode="linear"
21 | repeatCount="indefinite" />
22 | </circle>
23 | <circle cx="105" cy="15" r="15">
24 | <animate attributeName="r" from="15" to="15"
25 | begin="0s" dur="0.8s"
26 | values="15;9;15" calcMode="linear"
27 | repeatCount="indefinite" />
28 | <animate attributeName="fill-opacity" from="1" to="1"
29 | begin="0s" dur="0.8s"
30 | values="1;.5;1" calcMode="linear"
31 | repeatCount="indefinite" />
32 | </circle>
33 | </svg>
34 |
--------------------------------------------------------------------------------
/src/images/loading.svg:
--------------------------------------------------------------------------------
1 | <?xml version="1.0" encoding="utf-8"?>
2 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="200px" height="200px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
3 | <circle cx="50" cy="50" fill="none" stroke="#b02a8f" stroke-width="10" r="35" stroke-dasharray="164.93361431346415 56.97787143782138" transform="rotate(178.465 50 50)">
4 | <animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" values="0 50 50;360 50 50" keyTimes="0;1"></animateTransform>
5 | </circle>
6 | <!-- [ldio] generated by https://loading.io/ --></svg>
--------------------------------------------------------------------------------
/src/images/proton.svg:
--------------------------------------------------------------------------------
1 | <svg width="12" height="16" viewBox="0 0 317 400" fill="none" xmlns="http://www.w3.org/2000/svg">
2 | <g clip-path="url(#clip0_4917_2728)">
3 | <path d="M-7.51553e-05 295.38V399.38H72.9999V299.89C72.9999 290.21 76.8455 280.925 83.6906 274.08C90.5357 267.235 99.8195 263.39 109.5 263.39H184.35C201.643 263.39 218.767 259.984 234.744 253.365C250.721 246.747 265.238 237.047 277.466 224.818C289.693 212.589 299.393 198.072 306.009 182.095C312.626 166.118 316.031 148.993 316.03 131.7C316.033 114.406 312.629 97.2805 306.012 81.302C299.396 65.3235 289.697 50.8052 277.469 38.5754C265.241 26.3457 250.724 16.6444 234.747 10.0256C218.769 3.40684 201.644 -0.00024434 184.35 -0.000244141H-7.51553e-05V130H72.9999V68.7H179.41C195.934 68.7 211.781 75.2633 223.466 86.9465C235.151 98.6298 241.717 114.476 241.72 131C241.72 147.525 235.155 163.374 223.47 175.06C211.785 186.745 195.936 193.31 179.41 193.31H102.04C88.6359 193.305 75.3623 195.942 62.978 201.07C50.5936 206.198 39.3412 213.716 29.8644 223.196C20.3877 232.675 12.8723 243.93 7.74797 256.316C2.62361 268.702 -0.00927507 281.976 -7.51553e-05 295.38Z" fill="url(#paint0_radial_4917_2728)"/>
4 | <path d="M109.48 263.38C95.1024 263.379 80.8655 266.21 67.5822 271.711C54.2989 277.213 42.2296 285.277 32.0631 295.443C21.8967 305.61 13.8324 317.679 8.33096 330.962C2.82954 344.245 -0.00141216 358.483 -9.87253e-05 372.86V399.37H72.9999V299.88C72.9999 290.203 76.8427 280.922 83.6835 274.077C90.5242 267.233 99.8029 263.385 109.48 263.38Z" fill="url(#paint1_linear_4917_2728)"/>
5 | </g>
6 | <defs>
7 | <radialGradient id="paint0_radial_4917_2728" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(317.15 -55.4503) scale(401.97 401.97)">
8 | <stop stop-color="#A995FF"/>
9 | <stop offset="1" stop-color="#6652F5"/>
10 | </radialGradient>
11 | <linearGradient id="paint1_linear_4917_2728" x1="54.7399" y1="379.7" x2="54.7399" y2="226.88" gradientUnits="userSpaceOnUse">
12 | <stop stop-color="#6D4BFD"/>
13 | <stop offset="1" stop-color="#1C0554"/>
14 | </linearGradient>
15 | <clipPath id="clip0_4917_2728">
16 | <rect width="316.02" height="399.37" fill="white"/>
17 | </clipPath>
18 | </defs>
19 | </svg>
20 |
--------------------------------------------------------------------------------
/src/images/sl-button-demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/images/sl-button-demo.png
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "SimpleLogin by Proton: Secure Email Aliases",
3 | "short_name": "SimpleLogin",
4 | "description": "Easily create a different email for each website to hide your real email. Protect your inbox against spams, phishing, data breaches",
5 | "version": null,
6 | "manifest_version": 3,
7 | "icons": {
8 | "16": "icons/icon_16.png",
9 | "32": "icons/icon_32.png",
10 | "48": "icons/icon_48.png",
11 | "96": "icons/icon_96.png",
12 | "128": "icons/icon_128.png"
13 | },
14 | "permissions": [
15 | "activeTab",
16 | "storage",
17 | "contextMenus",
18 | "scripting",
19 | "tabs"
20 | ],
21 | "host_permissions": [
22 | "https://*.simplelogin.io/*",
23 | "http://*/*",
24 | "https://*/*"
25 | ],
26 | "action": {
27 | "default_title": "SimpleLogin",
28 | "default_popup": "popup/popup.html",
29 | "default_icon": {
30 | "16": "icons/icon_16.png",
31 | "32": "icons/icon_32.png",
32 | "48": "icons/icon_48.png",
33 | "96": "icons/icon_96.png",
34 | "128": "icons/icon_128.png"
35 | }
36 | },
37 |
38 | "browser_specific_settings": {
39 | "gecko": {
40 | "id": "addon@simplelogin",
41 | "strict_min_version": "109.0"
42 | }
43 | },
44 | "commands": {
45 | "generate-random-alias": {
46 | "suggested_key": {
47 | "default": "Ctrl+Shift+X"
48 | },
49 | "description": "Generate a random email alias"
50 | },
51 | "_execute_browser_action": {
52 | "suggested_key": {
53 | "default": "Ctrl+Shift+S"
54 | },
55 | "description": "Open the extension action menu"
56 | }
57 | },
58 | "content_scripts": [
59 | {
60 | "js": ["content_script/input_tools.js"],
61 | "css": ["content_script/input_tools.css"],
62 | "matches": ["http://*/*", "https://*/*"],
63 | "exclude_matches" : ["https://app.simplelogin.io/dashboard/*"],
64 | "run_at": "document_idle"
65 | }
66 | ]
67 | }
68 |
--------------------------------------------------------------------------------
/src/popup/APIService.js:
--------------------------------------------------------------------------------
1 | import EventManager from "./EventManager";
2 | import Navigation from "./Navigation";
3 | import SLStorage from "./SLStorage";
4 | import Utils from "./Utils";
5 |
6 | const API_ROUTE = {
7 | GET_USER_INFO: { method: "GET", path: "/api/user_info" },
8 | LOGOUT: { method: "GET", path: "/api/logout" },
9 | LOGIN: { method: "POST", path: "/api/auth/login" },
10 | MFA: { method: "POST", path: "/api/auth/mfa" },
11 | GET_ALIAS_OPTIONS: {
12 | method: "GET",
13 | path: "/api/v4/alias/options?hostname=:hostname",
14 | },
15 | GET_MAILBOXES: {
16 | method: "GET",
17 | path: "/api/mailboxes",
18 | },
19 | GET_ALIASES: { method: "POST", path: "/api/v2/aliases?page_id=:page_id" },
20 | NEW_ALIAS: {
21 | method: "POST",
22 | path: "/api/v2/alias/custom/new?hostname=:hostname",
23 | },
24 | NEW_RANDOM_ALIAS: {
25 | method: "POST",
26 | path: "/api/alias/random/new?hostname=:hostname",
27 | },
28 | TOGGLE_ALIAS: { method: "POST", path: "/api/aliases/:alias_id/toggle" },
29 | EDIT_ALIAS: { method: "PUT", path: "/api/aliases/:alias_id" },
30 | DELETE_ALIAS: { method: "DELETE", path: "/api/aliases/:alias_id" },
31 | CREATE_REVERSE_ALIAS: {
32 | method: "POST",
33 | path: "/api/aliases/:alias_id/contacts",
34 | },
35 | GET_API_KEY_FROM_COOKIE: { method: "POST", path: "/api/api_key" },
36 | };
37 |
38 | const API_ON_ERR = {
39 | IGNORE: 1,
40 | TOAST: 2,
41 | THROW: 3,
42 | };
43 |
44 | const SETTINGS = {
45 | apiKey: "",
46 | apiUrl: "",
47 | };
48 |
49 | const initService = async () => {
50 | await reloadSettings();
51 |
52 | EventManager.addListener(EventManager.EVENT.SETTINGS_CHANGED, reloadSettings);
53 | };
54 |
55 | const reloadSettings = async () => {
56 | SETTINGS.apiKey = await SLStorage.get(SLStorage.SETTINGS.API_KEY);
57 | SETTINGS.apiUrl = await SLStorage.get(SLStorage.SETTINGS.API_URL);
58 | };
59 |
60 | const callAPI = async function (
61 | route,
62 | params = {},
63 | data = {},
64 | errHandlerMethod = API_ON_ERR.THROW
65 | ) {
66 | const { method, path } = route;
67 | const url = SETTINGS.apiUrl + bindQueryParams(path, params);
68 | const headers = {};
69 |
70 | if (SETTINGS.apiKey) {
71 | headers["Authentication"] = SETTINGS.apiKey;
72 | }
73 |
74 | let fetchParam = {
75 | method: method,
76 | headers: headers,
77 | };
78 | if (method === "POST" || method === "PUT") {
79 | fetchParam.body = JSON.stringify(data);
80 | headers["Content-Type"] = "application/json";
81 | }
82 |
83 | let res = await fetch(url, fetchParam);
84 | if (res.ok) {
85 | const apiRes = await res.json();
86 | // wrap apiRes in data to look like axios which was used before
87 | return {
88 | status: res.status,
89 | data: apiRes,
90 | };
91 | } else {
92 | if (errHandlerMethod !== API_ON_ERR.IGNORE) {
93 | console.error(res);
94 | }
95 |
96 | if (res.status === 401) {
97 | await handle401Error();
98 | return null;
99 | }
100 |
101 | if (errHandlerMethod === API_ON_ERR.TOAST) {
102 | let apiRes = await res.json();
103 | if (apiRes.error) {
104 | Utils.showError(apiRes.error);
105 | } else {
106 | Utils.showError("Unknown error");
107 | }
108 | return null;
109 | }
110 |
111 | if (errHandlerMethod === API_ON_ERR.THROW) {
112 | throw {
113 | response: {
114 | status: res.status,
115 | data: await res.json(),
116 | },
117 | };
118 | }
119 | }
120 | };
121 |
122 | async function handle401Error() {
123 | Utils.showError("Authentication error, please login again");
124 | await SLStorage.remove(SLStorage.SETTINGS.API_KEY);
125 | EventManager.broadcast(EventManager.EVENT.SETTINGS_CHANGED);
126 | Navigation.clearHistoryAndNavigateTo(Navigation.PATH.LOGIN);
127 | }
128 |
129 | function bindQueryParams(url, params) {
130 | for (const key of Object.keys(params)) {
131 | url = url.replace(`:${key}`, encodeURIComponent(params[key]));
132 | }
133 |
134 | return url;
135 | }
136 |
137 | export { callAPI, API_ROUTE, API_ON_ERR, reloadSettings };
138 | export default { initService };
139 |
--------------------------------------------------------------------------------
/src/popup/App-color.scss:
--------------------------------------------------------------------------------
1 | $primary: #b02a8f;
2 | $success: #b02a8f;
--------------------------------------------------------------------------------
/src/popup/App-scrollbar.scss:
--------------------------------------------------------------------------------
1 | .app .content {
2 | scrollbar-width: thin;
3 | }
4 |
5 | .content::-webkit-scrollbar-track {
6 | background: rgba(0,0,0,0);
7 | }
8 |
9 | .content::-webkit-scrollbar {
10 | width: 0.6em;
11 | }
12 |
13 | .content::-webkit-scrollbar-thumb {
14 | background: transparent;
15 | border: solid 2px transparent;
16 | box-shadow: inset 0 0 10px 10px rgba(0, 0, 0, 0.25);
17 | border-radius: 16px;
18 | }
--------------------------------------------------------------------------------
/src/popup/App.scss:
--------------------------------------------------------------------------------
1 | @import "./App-color.scss";
2 | @import "./App-scrollbar.scss";
3 | @import "./Theme.scss";
4 | @import "~bootstrap/scss/bootstrap.scss";
5 | @import "~bootstrap-vue/src/index.scss";
6 | @import "~tippy.js/dist/tippy.css";
7 |
8 | body {
9 | box-sizing: content-box !important;
10 | width: 470px;
11 | background-color: var(--bg-color);
12 | color: var(--text-color);
13 | }
14 |
15 | input.form-control,
16 | select.form-control,
17 | textarea.form-control,
18 | .dropdown-menu.show {
19 | color: var(--text-color);
20 | background-color: var(--input-bg-color);
21 | border-color: var(--input-border-color);
22 | }
23 |
24 | input.form-control:disabled,
25 | select.form-control:disabled,
26 | textarea.form-control:disabled {
27 | color: var(--text-color);
28 | background-color: var(--bg-color);
29 | border-color: var(--input-border-color);
30 | }
31 |
32 | input.form-control:focus,
33 | select.form-control:focus,
34 | textarea.form-control:focus {
35 | color: var(--text-color);
36 | background-color: var(--input-bg-focus);
37 | border-color: var(--input-border-color);
38 | }
39 |
40 | .v--modal-box.v--modal.vue-dialog div,
41 | .v--modal-box.v--modal.vue-dialog button {
42 | color: var(--text-color);
43 | background-color: var(--input-bg-color);
44 | border-color: var(--delimiter-color);
45 | }
46 |
47 | .header {
48 | height: 45px;
49 | width: 460px;
50 | position: absolute;
51 | top: 0;
52 | left: 0;
53 | z-index: 10;
54 | }
55 |
56 | .header .back {
57 | cursor: pointer;
58 | }
59 |
60 | .overlay {
61 | position: fixed;
62 | top: 0;
63 | left: 0;
64 | z-index: 20;
65 | height: 100vh;
66 | width: 100vw;
67 | min-height: 350px;
68 | background-color: var(--overlay-background-color);
69 | }
70 |
71 | .overlay-content {
72 | position: fixed;
73 | z-index: 21;
74 | top: 50%;
75 | left: 50%;
76 | transform: translate(-50%, -50%);
77 | text-align: center;
78 | }
79 |
80 | .app {
81 | width: 470px;
82 | box-sizing: content-box !important;
83 | overflow-y: hidden;
84 | }
85 |
86 | .app .content {
87 | box-sizing: border-box;
88 | margin-top: 45px;
89 | padding-top: 15px;
90 | padding-bottom: 40px;
91 | max-height: 500px;
92 | overflow-y: auto;
93 | }
94 |
95 | .splash .logo {
96 | width: 200px;
97 | }
98 |
99 | .splash .loading {
100 | width: 30px;
101 | padding-top: 20px;
102 | }
103 |
104 | .splash.overlay {
105 | background-color: var(--background-color);
106 | }
107 |
108 | em {
109 | font-style: normal;
110 | background-color: #ffff00;
111 | }
112 |
113 | .small-text {
114 | font-size: 12px;
115 | font-weight: lighter;
116 | }
117 |
118 | .tooltip-inner {
119 | max-width: 400px;
120 | }
121 |
122 | .card-rating {
123 | border-radius: 8px;
124 | border: 1px solid var(--brand-color);
125 | }
126 |
127 | /* list aliases */
128 | .vue-js-switch {
129 | margin-bottom: 0;
130 | }
131 |
132 | .list-item-alias .disabled {
133 | opacity: 0.7;
134 | }
135 |
136 | .list-item-email {
137 | margin-right: 30px !important;
138 | position: relative;
139 | overflow: hidden;
140 | cursor: pointer;
141 | }
142 |
143 | .list-item-email > a {
144 | white-space: nowrap;
145 | }
146 |
147 | .list-item-email .email-sub {
148 | font-size: 12px;
149 | }
150 |
151 | .list-item-email-fade {
152 | right: 0;
153 | width: 30px;
154 | height: 100%;
155 | background: linear-gradient(to right, transparent, var(--bg-color));
156 | top: 0;
157 | position: absolute;
158 | }
159 |
160 | .list-item-alias .alias-note-preview {
161 | font-size: 12px;
162 | max-height: calc(12px * 1.5 * 3); /* font-size * line-height */
163 | overflow-y: hidden;
164 | }
165 |
166 | .header {
167 | .actions-container {
168 | position: absolute;
169 | right: 0.5rem;
170 | }
171 |
172 | .header-button {
173 | height: 20px;
174 | margin-top: 2px;
175 | margin-left: 10px;
176 | margin-right: 7px;
177 | color: var(--brand-color);
178 | cursor: pointer;
179 | }
180 | }
181 |
182 | .app-header-menu {
183 | left: auto !important;
184 | float: right !important;
185 | right: 10px;
186 | margin-top: 5px !important;
187 | }
188 |
189 | /* toasted: white close button */
190 | .toasted.toasted-primary > .action.ripple {
191 | color: white !important;
192 | }
193 |
194 | /* button img */
195 | .btn-svg {
196 | cursor: pointer;
197 | padding: 5px;
198 | display: inline;
199 | }
200 |
201 | /* send btn*/
202 | .btn-send {
203 | width: 14px;
204 | height: 14px;
205 | color: var(--brand-color);
206 | }
207 |
208 | /* more options */
209 | .btn-delete:hover,
210 | .btn-svg:hover {
211 | background-color: var(--delete-button-hover-color);
212 | }
213 |
214 | .btn-delete {
215 | float: right;
216 | }
217 |
218 | .btn-delete > img {
219 | vertical-align: unset;
220 | height: 14px;
221 | }
222 |
223 | .more-options {
224 | margin-top: 10px;
225 | }
226 |
227 | .cursor {
228 | cursor: pointer;
229 | }
230 |
231 | span.link {
232 | color: var(--brand-color);
233 | }
234 |
235 | .more-options > .action {
236 | margin-top: 10px;
237 | min-height: 25px;
238 | }
239 |
240 | .more-options > label {
241 | margin-bottom: 0;
242 | font-size: 12px;
243 | }
244 |
245 | /* BETA badge */
246 | .beta-badge {
247 | padding: 0.1em 0.4em;
248 | font-size: 0.65em;
249 | margin-left: 1em;
250 | border: 0.7px solid var(--brand-color);
251 | display: inline-block;
252 | color: var(--brand-color);
253 | }
254 |
255 | /* App Settings */
256 | table.settings-list > tr {
257 | border-bottom: 1px solid var(--delimiter-color);
258 | }
259 |
260 | table.settings-list > tr > td {
261 | vertical-align: top;
262 | padding: 6px;
263 | }
264 |
265 | table.settings-list {
266 | margin-bottom: 3em;
267 |
268 | tr.disabled {
269 | opacity: 0.7;
270 | pointer-events: none;
271 | }
272 | }
273 |
274 | /* Quick fix for Firefox Overflow Menu */
275 | .app.ff-overflow-menu {
276 | width: auto;
277 | font-size: 88%;
278 |
279 | .content {
280 | max-height: 500px;
281 | overflow-y: auto;
282 | scrollbar-width: thin;
283 | }
284 |
285 | textarea,
286 | input {
287 | font-size: 88%;
288 | }
289 |
290 | .header {
291 | width: 100%;
292 |
293 | .dashboard-btn {
294 | height: 20px;
295 | margin-top: 2px;
296 | padding: 0 0.5rem !important;
297 | }
298 | }
299 | }
300 |
--------------------------------------------------------------------------------
/src/popup/App.vue:
--------------------------------------------------------------------------------
1 | <template>
2 | <div class="app" :class="{ 'ff-overflow-menu': isInsideOverflowMenu }">
3 | <v-dialog />
4 | <sl-header :useCompactLayout="isInsideOverflowMenu" />
5 | <router-view />
6 | </div>
7 | </template>
8 |
9 | <script>
10 | import "./App.scss";
11 | import VueRouter from "vue-router";
12 | import Navigation from "./Navigation";
13 |
14 | import SplashScreen from "./components/SplashScreen";
15 | import Header from "./components/Header";
16 | import Login from "./components/Login";
17 | import SelfHostSetting from "./components/SelfHostSetting";
18 | import ApiKeySetting from "./components/ApiKeySetting";
19 | import Main from "./components/Main";
20 | import NewAliasResult from "./components/NewAliasResult";
21 | import ReverseAlias from "./components/ReverseAlias";
22 | import AppSettings from "./components/AppSettings";
23 | import SLStorage from "./SLStorage";
24 | import Utils from "./Utils";
25 | import APIService from "./APIService";
26 | import { getSavedTheme, setThemeClass } from "./Theme";
27 |
28 | const components = {
29 | "sl-header": Header,
30 | SplashScreen,
31 | Login,
32 | SelfHostSetting,
33 | ApiKeySetting,
34 | Main,
35 | NewAliasResult,
36 | ReverseAlias,
37 | AppSettings,
38 | };
39 |
40 | const routes = Navigation.getRoutes(components);
41 |
42 | const router = new VueRouter({
43 | mode: "abstract",
44 | routes,
45 | });
46 |
47 | export default {
48 | router,
49 | components,
50 | data() {
51 | return {
52 | isInsideOverflowMenu: false,
53 | appScale: 1,
54 | };
55 | },
56 | async mounted() {
57 | await APIService.initService();
58 | await setThemeClass(await getSavedTheme());
59 | Utils.setToasted(this.$toasted);
60 | Navigation.setRouter(this.$router);
61 | Navigation.navigateTo(Navigation.PATH.ROOT);
62 | this.detectOverflowMenu();
63 | },
64 | methods: {
65 | detectOverflowMenu() {
66 | const appElem = document.querySelector(".app");
67 | const appWidth = +getComputedStyle(appElem).width.replace("px", "");
68 | const windowWidth = window.innerWidth;
69 | if (windowWidth < appWidth) {
70 | this.isInsideOverflowMenu = true;
71 | }
72 | },
73 | },
74 | };
75 | </script>
76 |
--------------------------------------------------------------------------------
/src/popup/EventManager.js:
--------------------------------------------------------------------------------
1 | const listeners = {};
2 |
3 | class EventManager {
4 | static EVENT = {
5 | SETTINGS_CHANGED: "settings_changed",
6 | };
7 |
8 | static addListener(eventName, callback) {
9 | if (!listeners[eventName]) {
10 | listeners[eventName] = [];
11 | }
12 | if (listeners[eventName].indexOf(callback) === -1) {
13 | // make sure the callback function is added only once
14 | listeners[eventName].push(callback);
15 | }
16 | }
17 |
18 | static removeListener(callback) {
19 | for (const eventCallbacks of Object.values(listeners)) {
20 | const index = eventCallbacks.indexOf(callback);
21 | if (index !== -1) {
22 | eventCallbacks.splice(index, 1);
23 | }
24 | }
25 | }
26 |
27 | static broadcast(eventName, data) {
28 | if (listeners[eventName]) {
29 | for (const callback of listeners[eventName]) {
30 | callback(data);
31 | }
32 | }
33 | }
34 | }
35 |
36 | export default EventManager;
37 |
--------------------------------------------------------------------------------
/src/popup/Navigation.js:
--------------------------------------------------------------------------------
1 | let router = null;
2 |
3 | const PATH = {
4 | ROOT: "/",
5 | MAIN: "/main",
6 | NEW_ALIAS_RESULT: "/new-alias-result/",
7 | LOGIN: "/login",
8 | API_KEY_SETTING: "/api-key-setting",
9 | SELF_HOST_SETTING: "/self-host-setting",
10 | REVERSE_ALIAS: "/reverse-alias",
11 | APP_SETTINGS: "/app-settings",
12 | };
13 |
14 | class Navigation {
15 | static PATH = PATH;
16 |
17 | static getRoutes(components) {
18 | return [
19 | {
20 | path: Navigation.PATH.ROOT,
21 | component: components.SplashScreen,
22 | },
23 | {
24 | path: Navigation.PATH.LOGIN,
25 | component: components.Login,
26 | },
27 | {
28 | path: Navigation.PATH.API_KEY_SETTING,
29 | component: components.ApiKeySetting,
30 | },
31 | {
32 | path: Navigation.PATH.SELF_HOST_SETTING,
33 | component: components.SelfHostSetting,
34 | },
35 | {
36 | path: Navigation.PATH.MAIN,
37 | component: components.Main,
38 | },
39 | {
40 | path: Navigation.PATH.NEW_ALIAS_RESULT,
41 | component: components.NewAliasResult,
42 | },
43 | {
44 | path: Navigation.PATH.REVERSE_ALIAS,
45 | component: components.ReverseAlias,
46 | },
47 | {
48 | path: Navigation.PATH.APP_SETTINGS,
49 | component: components.AppSettings,
50 | },
51 | ];
52 | }
53 |
54 | static setRouter($router) {
55 | router = $router;
56 | }
57 |
58 | static navigateTo(path, canGoBack) {
59 | if (canGoBack) {
60 | router.push(path);
61 | } else {
62 | router.replace(path);
63 | }
64 | }
65 |
66 | static canGoBack() {
67 | return router.history.index > 0;
68 | }
69 |
70 | static navigateBack() {
71 | router.go(-1);
72 | }
73 |
74 | static clearHistoryAndNavigateTo(path) {
75 | router.history.stack = [];
76 | router.history.index = -1;
77 | setTimeout(() => router.push(path), 10);
78 | }
79 | }
80 |
81 | export default Navigation;
82 |
--------------------------------------------------------------------------------
/src/popup/SLStorage.js:
--------------------------------------------------------------------------------
1 | import Utils from "./Utils";
2 | import browser from "webextension-polyfill";
3 | import { THEME_SYSTEM } from "./Theme";
4 |
5 | const TEMP = {};
6 |
7 | class SLStorage {
8 | static SETTINGS = {
9 | API_URL: "apiUrl",
10 | API_KEY: "apiKey",
11 | NOT_ASKING_RATE: "notAskingRate",
12 | SHOW_SL_BUTTON: "showSLButton",
13 | SL_BUTTON_POSITION: "SLButtonPosition",
14 | THEME: "SLTheme",
15 | EXTRA_ALLOWED_DOMAINS: [],
16 | };
17 |
18 | static DEFAULT_SETTINGS = {
19 | [SLStorage.SETTINGS.API_URL]: devConfig
20 | ? devConfig.DEFAULT_API_URL
21 | : "https://app.simplelogin.io",
22 | [SLStorage.SETTINGS.API_KEY]: "",
23 | [SLStorage.SETTINGS.NOT_ASKING_RATE]: false,
24 | [SLStorage.SETTINGS.SHOW_SL_BUTTON]: true,
25 | [SLStorage.SETTINGS.SL_BUTTON_POSITION]: "right-inside",
26 | [SLStorage.SETTINGS.THEME]: THEME_SYSTEM,
27 | [SLStorage.SETTINGS.EXTRA_ALLOWED_DOMAINS]: devConfig
28 | ? devConfig.EXTRA_ALLOWED_DOMAINS
29 | : [],
30 | };
31 |
32 | static set(key, value) {
33 | return browser.storage.sync.set({ [key]: value });
34 | }
35 |
36 | static async get(key) {
37 | const data = await browser.storage.sync.get(key);
38 |
39 | if (data[key] === undefined || data[key] === null) {
40 | return SLStorage.DEFAULT_SETTINGS[key] || "";
41 | } else {
42 | return data[key];
43 | }
44 | }
45 |
46 | static remove(key) {
47 | return browser.storage.sync.remove(key);
48 | }
49 |
50 | static clear() {
51 | return browser.storage.sync.clear();
52 | }
53 |
54 | static setTemporary(key, value) {
55 | TEMP[key] = Utils.cloneObject(value);
56 | }
57 |
58 | static getTemporary(key) {
59 | return TEMP[key];
60 | }
61 | }
62 |
63 | export default SLStorage;
64 |
--------------------------------------------------------------------------------
/src/popup/Theme.js:
--------------------------------------------------------------------------------
1 | import SLStorage from "./SLStorage";
2 |
3 | export const THEME_LIGHT = "theme-light";
4 | export const THEME_DARK = "theme-dark";
5 | export const THEME_SYSTEM = "theme-system";
6 |
7 | export const THEMES = [THEME_LIGHT, THEME_DARK, THEME_SYSTEM];
8 |
9 | export const THEME_LABELS = {
10 | [THEME_LIGHT]: "Light",
11 | [THEME_DARK]: "Dark",
12 | [THEME_SYSTEM]: "System",
13 | };
14 |
15 | export async function getSavedTheme() {
16 | return (await SLStorage.get(SLStorage.SETTINGS.THEME)) ?? THEME_SYSTEM;
17 | }
18 |
19 | export async function setThemeClass(nextTheme, prevTheme) {
20 | await SLStorage.set(SLStorage.SETTINGS.THEME, nextTheme);
21 |
22 | if (prevTheme === undefined) {
23 | return document.body.classList.add(nextTheme);
24 | }
25 |
26 | document.body.classList.replace(prevTheme, nextTheme);
27 | }
28 |
--------------------------------------------------------------------------------
/src/popup/Theme.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | --brand-color: #b02a8f;
3 | --muted-brand-color: rgba(176, 42, 143, 0.7);
4 | }
5 |
6 | .theme-light {
7 | --bg-color: white;
8 | --input-bg-color: white;
9 | --overlay-bg-color: rgba(255, 255, 255, 0.8);
10 | --text-color: black;
11 | --delimiter-color: #bbb;
12 | --delete-button-hover-color: rgba(0, 0, 0, 0.1);
13 | }
14 |
15 | .theme-dark {
16 | --bg-color: #222;
17 | --input-bg-color: #333;
18 | --input-bg-focus: #444;
19 | --input-border-color: black;
20 | --overlay-bg-color: rgba(0, 0, 0, 0.8);
21 | --text-color: #ddd;
22 | --delimiter-color: #555;
23 | --delete-button-hover-color: rgba(255, 255, 255, 0.1);
24 |
25 | // For switching colors of monochrome images
26 | .invertable, .btn.dropdown-toggle {
27 | filter: invert(0.8);
28 | }
29 |
30 | .btn-primary.btn-primary-muted {
31 | background-color: var(--muted-brand-color);
32 | }
33 | }
34 |
35 | @media (prefers-color-scheme: light) {
36 | // copy of .theme-light
37 | .theme-system {
38 | --bg-color: white;
39 | --input-bg-color: white;
40 | --overlay-bg-color: rgba(255, 255, 255, 0.8);
41 | --text-color: black;
42 | --delimiter-color: #bbb;
43 | --delete-button-hover-color: rgba(0, 0, 0, 0.1);
44 | }
45 | }
46 |
47 | @media (prefers-color-scheme: dark) {
48 | // copy of .theme-dark
49 | .theme-system {
50 | --bg-color: #222;
51 | --input-bg-color: #333;
52 | --input-bg-focus: #444;
53 | --input-border-color: black;
54 | --overlay-bg-color: rgba(0, 0, 0, 0.8);
55 | --text-color: #ddd;
56 | --delimiter-color: #555;
57 | --delete-button-hover-color: rgba(255, 255, 255, 0.1);
58 |
59 | // For switching colors of monochrome images
60 | .invertable, .btn.dropdown-toggle {
61 | filter: invert(0.8);
62 | }
63 |
64 | .btn-primary.btn-primary-muted {
65 | background-color: var(--muted-brand-color);
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/popup/Utils.js:
--------------------------------------------------------------------------------
1 | const browser = require("webextension-polyfill");
2 | const buildConfig = require("./buildConfig.json");
3 |
4 | let toasted = null;
5 |
6 | class Utils {
7 | static getRandomIntBetween(min, max) {
8 | return Math.floor(min + Math.random() * Math.floor(max));
9 | }
10 |
11 | static async getHostName(currentUrl) {
12 | try {
13 | if (currentUrl) {
14 | const url = new URL(currentUrl);
15 | return url.hostname;
16 | } else {
17 | const result = await browser.tabs.query({
18 | active: true,
19 | currentWindow: true,
20 | });
21 | const url = new URL(result[0].url);
22 | return url.hostname;
23 | }
24 | } catch (error) {
25 | console.log(error);
26 | }
27 | }
28 |
29 | static async getDefaultNote() {
30 | const hostName = await Utils.getHostName();
31 | let note = "";
32 |
33 | // ignore hostName that doesn't look like an url
34 | if (hostName && hostName.indexOf(".") > -1) {
35 | note = `Used on ${hostName}`;
36 | }
37 |
38 | return note;
39 | }
40 |
41 | static getDeviceName() {
42 | const isFirefox = typeof InstallTrigger !== "undefined";
43 | const browserName = isFirefox ? "Firefox" : "Chrome";
44 | return `${browserName} (${navigator.platform})`;
45 | }
46 |
47 | static getExtensionURL() {
48 | const isFirefox = typeof InstallTrigger !== "undefined",
49 | firefoxExtensionUrl =
50 | "https://addons.mozilla.org/en-GB/firefox/addon/simplelogin/",
51 | chromeExtensionUrl =
52 | "https://chrome.google.com/webstore/detail/simplelogin-your-anti-spa/dphilobhebphkdjbpfohgikllaljmgbn";
53 | return isFirefox ? firefoxExtensionUrl : chromeExtensionUrl;
54 | }
55 |
56 | static setToasted($toasted) {
57 | toasted = $toasted;
58 | }
59 |
60 | static showSuccess(message) {
61 | if (toasted) {
62 | toasted.show(message, {
63 | type: "success",
64 | duration: 2500,
65 | });
66 | }
67 | }
68 |
69 | static showError(message) {
70 | if (toasted) {
71 | toasted.show(message, {
72 | type: "error",
73 | duration: 3000,
74 | action: {
75 | text: "×",
76 | onClick: (e, toastObject) => {
77 | toastObject.goAway(0);
78 | },
79 | },
80 | });
81 | }
82 | }
83 |
84 | static cloneObject(obj) {
85 | return JSON.parse(JSON.stringify(obj));
86 | }
87 |
88 | static getBuildConfig() {
89 | return buildConfig;
90 | }
91 | }
92 |
93 | export default Utils;
94 |
--------------------------------------------------------------------------------
/src/popup/buildConfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "features": {
3 | "loginWithProtonEnabled": true
4 | },
5 | "buildTime": 1655462531232
6 | }
--------------------------------------------------------------------------------
/src/popup/components/AliasMoreOptions.vue:
--------------------------------------------------------------------------------
1 | <!--
2 | Original implementation:
3 | https://github.com/devstark-com/vue-textarea-autosize/blob/8e767ea21863b3e8607b1808b89e7b5a0e3aa98c/src/components/TextareaAutosize.vue
4 |
5 | MIT License
6 | -->
7 |
8 | <template>
9 | <expand-transition>
10 | <div class="more-options" v-if="show">
11 | <label>Mailboxes</label>
12 | <div>
13 | <b-dropdown size="sm" variant="outline">
14 | <b-dropdown-form>
15 | <b-form-checkbox
16 | v-for="mailbox in mailboxes"
17 | :key="mailbox.id"
18 | :checked="
19 | findIndexOfMailbox(mailbox.id, moreOptions.mailboxes) !== -1
20 | "
21 | @change="toggleMailbox(mailbox)"
22 | >
23 | {{ mailbox.email }}
24 | </b-form-checkbox>
25 | </b-dropdown-form>
26 | </b-dropdown>
27 |
28 | {{
29 | moreOptions.mailboxes.length > 0
30 | ? moreOptions.mailboxes.map((mb) => mb.email).join(", ")
31 | : "Please select at least one mailbox"
32 | }}
33 | </div>
34 |
35 | <label>Alias Note</label>
36 | <TextareaAutosize
37 | placeholder="Note, can be anything to help you remember why you created this alias. This field is optional."
38 | class="form-control"
39 | style="width: 100%"
40 | v-model="moreOptions.note"
41 | :disabled="loading"
42 | ></TextareaAutosize>
43 |
44 | <label>
45 | From Name
46 | <font-awesome-icon
47 | v-b-tooltip.hover.top="
48 | 'This name is used when you send or reply from alias. You may need to use a pseudonym because the receiver can see it.'
49 | "
50 | icon="question-circle"
51 | />
52 | </label>
53 | <b-input
54 | v-model="moreOptions.name"
55 | placeholder="From name"
56 | :disabled="loading"
57 | />
58 |
59 | <div class="advanced-options mt-2" v-if="alias.support_pgp">
60 | <b-form-checkbox
61 | :checked="!moreOptions.disable_pgp"
62 | @change="toggleAliasPGP"
63 | >Enable PGP</b-form-checkbox
64 | >
65 | </div>
66 |
67 | <div class="action">
68 | <button
69 | class="btn btn-sm btn-primary"
70 | v-on:click="handleClickSave"
71 | :disabled="loading || !canSave()"
72 | >
73 | <font-awesome-icon icon="save" />
74 | {{ btnSaveLabel || "Save" }}
75 | </button>
76 |
77 | <button
78 | class="btn btn-sm btn-delete"
79 | style="color: #dc3545"
80 | v-on:click="handleClickDelete"
81 | :disabled="loading"
82 | >
83 | <font-awesome-icon icon="trash" />
84 | Delete
85 | </button>
86 | </div>
87 | </div>
88 | </expand-transition>
89 | </template>
90 |
91 | <script>
92 | import Utils from "../Utils";
93 | import ExpandTransition from "./ExpandTransition";
94 | import TextareaAutosize from "./TextareaAutosize";
95 | import { callAPI, API_ROUTE, API_ON_ERR } from "../APIService";
96 |
97 | export default {
98 | props: {
99 | alias: {
100 | type: Object,
101 | required: true,
102 | },
103 | index: {
104 | type: Number,
105 | required: true,
106 | },
107 | show: {
108 | type: Boolean,
109 | required: true,
110 | },
111 | mailboxes: {
112 | type: Array,
113 | required: true,
114 | },
115 | btnSaveLabel: {
116 | type: String,
117 | },
118 | },
119 | components: {
120 | TextareaAutosize,
121 | "expand-transition": ExpandTransition,
122 | },
123 | data() {
124 | return {
125 | moreOptions: {
126 | note: "",
127 | name: "",
128 | disable_pgp: false,
129 | mailboxes: [],
130 | },
131 | loading: false,
132 | hasMailboxesChanges: false, // to be used in canSaved()
133 | canAlwaysSave: false, // to be used in canSaved()
134 | };
135 | },
136 | mounted() {
137 | this.$watch("show", (newValue) => {
138 | if (newValue) {
139 | this.showMoreOptions();
140 | }
141 | });
142 |
143 | if (this.show) {
144 | this.showMoreOptions();
145 | }
146 |
147 | if (this.btnSaveLabel) {
148 | this.canAlwaysSave = true;
149 | }
150 | },
151 | methods: {
152 | showMoreOptions() {
153 | this.moreOptions = {
154 | note: this.alias.note,
155 | name: this.alias.name,
156 | disable_pgp: !!this.alias.disable_pgp,
157 | mailboxes: Utils.cloneObject(this.alias.mailboxes),
158 | };
159 |
160 | this.hasMailboxesChanges = false;
161 | },
162 |
163 | handleClickDelete() {
164 | this.$modal.show("dialog", {
165 | title: `Delete ${this.alias.email}`,
166 | text: "Do you really want to delete this alias?",
167 | buttons: [
168 | {
169 | title: "Yes",
170 | handler: () => {
171 | this.$modal.hide("dialog");
172 | this.deleteAlias();
173 | },
174 | },
175 | {
176 | title: "No",
177 | default: true,
178 | handler: () => {
179 | this.$modal.hide("dialog");
180 | },
181 | },
182 | ],
183 | });
184 | },
185 |
186 | async deleteAlias() {
187 | this.loading = true;
188 | const res = await callAPI(
189 | API_ROUTE.DELETE_ALIAS,
190 | {
191 | alias_id: this.alias.id,
192 | },
193 | {},
194 | API_ON_ERR.TOAST
195 | );
196 | if (res) {
197 | this.$emit("deleted", {
198 | index: this.index,
199 | data: this.alias,
200 | });
201 | } else {
202 | this.loading = false;
203 | }
204 | },
205 |
206 | canSave() {
207 | return (
208 | this.moreOptions.mailboxes.length > 0 &&
209 | (this.alias.note !== this.moreOptions.note ||
210 | this.alias.name !== this.moreOptions.name ||
211 | !!this.alias.disable_pgp !== this.moreOptions.disable_pgp ||
212 | this.hasMailboxesChanges ||
213 | this.canAlwaysSave)
214 | );
215 | },
216 |
217 | async handleClickSave() {
218 | this.loading = true;
219 | const savedData = {
220 | note: this.moreOptions.note,
221 | name: this.moreOptions.name,
222 | disable_pgp: this.moreOptions.disable_pgp,
223 | mailbox_ids: this.moreOptions.mailboxes.map((mb) => mb.id),
224 | };
225 | const res = await callAPI(
226 | API_ROUTE.EDIT_ALIAS,
227 | {
228 | alias_id: this.alias.id,
229 | },
230 | savedData,
231 | API_ON_ERR.TOAST
232 | );
233 | if (res) {
234 | Utils.showSuccess("Updated alias");
235 | this.$emit("changed", {
236 | index: this.index,
237 | data: this.moreOptions,
238 | });
239 | }
240 | this.loading = false;
241 | },
242 |
243 | toggleAliasPGP() {
244 | this.moreOptions.disable_pgp = !this.moreOptions.disable_pgp;
245 | },
246 |
247 | findIndexOfMailbox(id, mailboxes) {
248 | let index = -1;
249 | for (const i in mailboxes) {
250 | if (mailboxes[i].id === id) {
251 | index = i;
252 | }
253 | }
254 | return index;
255 | },
256 |
257 | toggleMailbox(mailbox) {
258 | const i = this.findIndexOfMailbox(mailbox.id, this.moreOptions.mailboxes);
259 | if (i === -1) {
260 | this.moreOptions.mailboxes.push(mailbox);
261 | } else {
262 | this.moreOptions.mailboxes.splice(i, 1);
263 | }
264 |
265 | // check if there are changes
266 | const oldMailboxIds = this.alias.mailboxes
267 | .map((mb) => mb.id)
268 | .sort()
269 | .join(",");
270 | const newMailboxIds = this.moreOptions.mailboxes
271 | .map((mb) => mb.id)
272 | .sort()
273 | .join(",");
274 | this.hasMailboxesChanges = oldMailboxIds !== newMailboxIds;
275 | },
276 | },
277 | };
278 | </script>
279 |
--------------------------------------------------------------------------------
/src/popup/components/ApiKeySetting.vue:
--------------------------------------------------------------------------------
1 | <template>
2 | <div class="content">
3 | <div class="p-3 container">
4 | <p>To get started, please follow these 3 simple steps:</p>
5 |
6 | <div class="mb-2">
7 | <span class="badge badge-primary badge-pill">1</span>
8 | Create your SimpleLogin account
9 | <a :href="apiUrl + '/auth/register'" target="_blank">here</a>
10 | if this is not already done.
11 | </div>
12 |
13 | <div class="mb-2">
14 | <span class="badge badge-primary badge-pill">2</span>
15 | Create and copy your
16 | <em>API Key</em>
17 | <a :href="apiUrl + '/dashboard/api_key'" target="_blank">here</a>.
18 | </div>
19 |
20 | <div class="mb-2">
21 | <span class="badge badge-primary badge-pill">3</span>
22 | Paste the
23 | <em>API Key</em> here 👇🏽
24 | </div>
25 |
26 | <input
27 | v-model="apiKey"
28 | v-on:keyup.enter="saveApiKey"
29 | placeholder="API Key"
30 | autofocus
31 | class="form-control mt-3 w-100"
32 | />
33 |
34 | <button @click="saveApiKey" class="btn btn-primary btn-block mt-2">
35 | Set API Key
36 | </button>
37 | </div>
38 | </div>
39 | </template>
40 |
41 | <script>
42 | import SLStorage from "../SLStorage";
43 | import EventManager from "../EventManager";
44 | import Navigation from "../Navigation";
45 | import Utils from "../Utils";
46 | import { API_ROUTE } from "../APIService";
47 |
48 | export default {
49 | data() {
50 | return {
51 | apiKey: "",
52 | apiUrl: "",
53 | };
54 | },
55 | async mounted() {
56 | this.apiUrl = await SLStorage.get(SLStorage.SETTINGS.API_URL);
57 | },
58 | methods: {
59 | async saveApiKey() {
60 | if (this.apiKey === "") {
61 | Utils.showError("API Key cannot be empty");
62 | return;
63 | }
64 |
65 | // check api key
66 | const res = await fetch(this.apiUrl + API_ROUTE.GET_USER_INFO.path, {
67 | headers: { Authentication: this.apiKey },
68 | });
69 | if (res.ok) {
70 | const apiRes = await res.json();
71 | const userName = apiRes.name || apiRes.email;
72 | await SLStorage.set(SLStorage.SETTINGS.API_KEY, this.apiKey);
73 | EventManager.broadcast(EventManager.EVENT.SETTINGS_CHANGED);
74 |
75 | Utils.showSuccess(`Hi ${userName}!`);
76 | Navigation.clearHistoryAndNavigateTo(Navigation.PATH.MAIN);
77 | } else {
78 | Utils.showError("Incorrect API Key.");
79 | }
80 | },
81 | },
82 | computed: {},
83 | };
84 | </script>
85 |
--------------------------------------------------------------------------------
/src/popup/components/AppSettings.vue:
--------------------------------------------------------------------------------
1 | <template>
2 | <div class="content">
3 | <div class="p-3 container">
4 | <p class="font-weight-bold align-self-center">
5 | App Settings ({{ userEmail }})
6 | </p>
7 |
8 | <div v-if="freeAccount">
9 | <small>
10 | Currently you have a free SimpleLogin account. Upgrade your account to
11 | create unlimited aliases, add more mailboxes, create aliases
12 | on-the-fly with your domain or SimpleLogin subdomain and more.
13 | </small>
14 | <button @click="upgrade" class="btn btn-primary btn-sm">
15 | Upgrade your SimpleLogin account
16 | </button>
17 | <hr />
18 | </div>
19 |
20 | <table class="settings-list">
21 | <tr>
22 | <td>
23 | <toggle-button
24 | :value="showSLButton"
25 | :sync="true"
26 | color="#b02a8f"
27 | :width="30"
28 | :height="18"
29 | @change="handleToggleSLButton()"
30 | />
31 | </td>
32 | <td>
33 | Show SimpleLogin button on email input fields<br />
34 | <small>
35 | If enabled, you can quickly create a random alias by clicking on
36 | the SimpleLogin button placed next to the email field.
37 | <a
38 | :href="reportURISLButton"
39 | v-show="showSLButton"
40 | target="_blank"
41 | >
42 | <br />
43 | <font-awesome-icon icon="bug" />
44 | Report an issue
45 | </a>
46 | </small>
47 | </td>
48 | </tr>
49 |
50 | <tr v-show="showSLButton">
51 | <td>
52 | <toggle-button
53 | :value="positionSLButton === 'right-outside'"
54 | :sync="true"
55 | color="#b02a8f"
56 | :width="30"
57 | :height="18"
58 | @change="handleToggleSLButtonOutside()"
59 | />
60 | </td>
61 | <td>
62 | Place SimpleLogin button outside the input<br />
63 | <small>
64 | Display the SimpleLogin button next to the email field instead of
65 | inside the field. This can avoid having overlapping buttons with
66 | other extensions like Dashlane, LastPass, etc
67 | </small>
68 | </td>
69 | </tr>
70 |
71 | <tr>
72 | <td></td>
73 | <td>
74 | SimpleLogin extension Theme<br />
75 | <small>
76 | System theme automatically switches between Light and Dark -
77 | according to system preference.
78 | </small>
79 | <div
80 | class="input-group-sm w-50"
81 | style="padding-top: 6px; padding-bottom: 6px"
82 | >
83 | <select v-model="theme" class="form-control">
84 | <option
85 | v-for="themeOption in THEMES"
86 | :key="themeOption"
87 | :value="themeOption"
88 | >
89 | {{ THEME_LABELS[themeOption] }}
90 | </option>
91 | </select>
92 | </div>
93 | </td>
94 | </tr>
95 | </table>
96 |
97 | <button
98 | @click="handleLogout"
99 | class="btn btn-outline-primary btn-block mt-2"
100 | >
101 | Logout
102 | </button>
103 |
104 | <div
105 | class="font-weight-light"
106 | style="position: fixed; bottom: 0; right: 2px; font-size: 0.8rem"
107 | >
108 | Version: {{ extension_version }}
109 | </div>
110 | </div>
111 | </div>
112 | </template>
113 |
114 | <script>
115 | import SLStorage from "../SLStorage";
116 | import EventManager from "../EventManager";
117 | import Navigation from "../Navigation";
118 | import Utils from "../Utils";
119 | import { callAPI, API_ROUTE, API_ON_ERR } from "../APIService";
120 | import { setThemeClass, THEME_LABELS, THEMES, getSavedTheme } from "../Theme";
121 |
122 | export default {
123 | data() {
124 | return {
125 | showSLButton: false,
126 | positionSLButton: "right-inside",
127 | reportURISLButton: "",
128 | extension_version: "development",
129 | userEmail: "",
130 | theme: "",
131 | freeAccount: false,
132 | THEMES,
133 | THEME_LABELS,
134 | };
135 | },
136 | async mounted() {
137 | this.showSLButton = await SLStorage.get(SLStorage.SETTINGS.SHOW_SL_BUTTON);
138 | this.positionSLButton = await SLStorage.get(
139 | SLStorage.SETTINGS.SL_BUTTON_POSITION
140 | );
141 | this.theme = await getSavedTheme();
142 |
143 | await this.setMailToUri();
144 | this.extension_version = browser.runtime.getManifest().version;
145 |
146 | // check api key
147 | let userInfo = await callAPI(
148 | API_ROUTE.GET_USER_INFO,
149 | {},
150 | {},
151 | API_ON_ERR.TOAST
152 | );
153 | this.userEmail = userInfo.data.email;
154 | if (userInfo.data.in_trial) {
155 | this.freeAccount = true;
156 | } else {
157 | this.freeAccount = !userInfo.data.is_premium;
158 | }
159 | },
160 | methods: {
161 | async handleToggleSLButton() {
162 | this.showSLButton = !this.showSLButton;
163 | await SLStorage.set(SLStorage.SETTINGS.SHOW_SL_BUTTON, this.showSLButton);
164 | this.showSavedSettingsToast();
165 | },
166 |
167 | async handleToggleSLButtonOutside() {
168 | this.positionSLButton =
169 | this.positionSLButton === "right-outside"
170 | ? "right-inside"
171 | : "right-outside";
172 | await SLStorage.set(
173 | SLStorage.SETTINGS.SL_BUTTON_POSITION,
174 | this.positionSLButton
175 | );
176 | this.showSavedSettingsToast();
177 | },
178 |
179 | showSavedSettingsToast() {
180 | Utils.showSuccess("Settings saved");
181 | },
182 |
183 | async handleLogout() {
184 | await callAPI(API_ROUTE.LOGOUT, {}, {}, API_ON_ERR.IGNORE);
185 | await SLStorage.remove(SLStorage.SETTINGS.API_KEY);
186 | EventManager.broadcast(EventManager.EVENT.SETTINGS_CHANGED);
187 | Navigation.clearHistoryAndNavigateTo(Navigation.PATH.LOGIN);
188 |
189 | if (process.env.MAC) {
190 | console.log("send log out event to host app");
191 | await browser.runtime.sendNativeMessage(
192 | "application.id",
193 | JSON.stringify({
194 | logged_out: {},
195 | })
196 | );
197 | }
198 | },
199 |
200 | async setMailToUri() {
201 | const subject = encodeURIComponent("Problem with SLButton feature");
202 | const hostname = await Utils.getHostName();
203 | const body = encodeURIComponent(
204 | "(Optional) Affected website: " + hostname
205 | );
206 | this.reportURISLButton = `mailto:extension@simplelogin.io?subject=${subject}&body=${body}`;
207 | },
208 | async upgrade() {
209 | if (process.env.MAC) {
210 | try {
211 | console.log("send upgrade event to host app");
212 | await browser.runtime.sendNativeMessage(
213 | "application.id",
214 | JSON.stringify({
215 | upgrade: {},
216 | })
217 | );
218 | } catch (error) {
219 | console.info("can't send data to native app", error);
220 | }
221 | } else {
222 | let apiUrl = await SLStorage.get(SLStorage.SETTINGS.API_URL);
223 | let upgradeURL = apiUrl + "/dashboard/pricing";
224 | browser.tabs.create({ url: upgradeURL });
225 | }
226 | },
227 | },
228 | computed: {},
229 | watch: {
230 | theme: async function (nextTheme, prevTheme) {
231 | if (!prevTheme) {
232 | return;
233 | }
234 |
235 | setThemeClass(nextTheme, prevTheme);
236 | this.showSavedSettingsToast();
237 | },
238 | },
239 | };
240 | </script>
241 |
--------------------------------------------------------------------------------
/src/popup/components/ExpandTransition.vue:
--------------------------------------------------------------------------------
1 | <script>
2 | const delay = (ms) => new Promise((r) => setTimeout(r, ms));
3 |
4 | export default {
5 | name: `ExpandTransition`,
6 | functional: true,
7 | render(createElement, context) {
8 | const data = {
9 | props: {
10 | name: `expand`,
11 | },
12 | on: {
13 | afterEnter(element) {
14 | // eslint-disable-next-line no-param-reassign
15 | element.style.height = `auto`;
16 | },
17 | async enter(element) {
18 | const { width } = getComputedStyle(element);
19 |
20 | /* eslint-disable no-param-reassign */
21 | element.style.width = width;
22 | element.style.position = `absolute`;
23 | element.style.visibility = `hidden`;
24 | element.style.height = `auto`;
25 | /* eslint-enable */
26 |
27 | await delay(10);
28 | const { height } = getComputedStyle(element);
29 |
30 | /* eslint-disable no-param-reassign */
31 | element.style.width = null;
32 | element.style.position = null;
33 | element.style.visibility = null;
34 | element.style.height = 0;
35 | /* eslint-enable */
36 |
37 | // Force repaint to make sure the
38 | // animation is triggered correctly.
39 | // eslint-disable-next-line no-unused-expressions
40 | getComputedStyle(element).height;
41 |
42 | requestAnimationFrame(() => {
43 | // eslint-disable-next-line no-param-reassign
44 | element.style.height = height;
45 | });
46 | },
47 | leave(element) {
48 | const { height } = getComputedStyle(element);
49 |
50 | // eslint-disable-next-line no-param-reassign
51 | element.style.height = height;
52 |
53 | // Force repaint to make sure the
54 | // animation is triggered correctly.
55 | // eslint-disable-next-line no-unused-expressions
56 | getComputedStyle(element).height;
57 |
58 | requestAnimationFrame(() => {
59 | // eslint-disable-next-line no-param-reassign
60 | element.style.height = 0;
61 | });
62 | },
63 | },
64 | };
65 |
66 | return createElement(`transition`, data, context.children);
67 | },
68 | };
69 | </script>
70 |
71 | <style scoped>
72 | * {
73 | will-change: height;
74 | transform: translateZ(0);
75 | backface-visibility: hidden;
76 | perspective: 1000px;
77 | }
78 | </style>
79 |
80 | <style>
81 | .expand-enter-active,
82 | .expand-leave-active {
83 | transition: height 0.2s ease-in-out;
84 | overflow: hidden;
85 | }
86 |
87 | .expand-enter,
88 | .expand-leave-to {
89 | height: 0;
90 | }
91 | </style>
92 |
--------------------------------------------------------------------------------
/src/popup/components/Header.vue:
--------------------------------------------------------------------------------
1 | <template>
2 | <div class="header">
3 | <div
4 | class="row mt-2 pb-2 ml-3 mr-2"
5 | style="border-bottom: 1px var(--delimiter-color) solid"
6 | >
7 | <div>
8 | <div
9 | v-on:click="navigateBack()"
10 | v-bind:class="{ back: canBack }"
11 | style="display: inline-block; color: var(--text-color)"
12 | >
13 | <img
14 | v-if="canBack"
15 | src="/images/back-button.svg"
16 | class="invertable"
17 | style="height: 20px"
18 | />
19 | <img
20 | class="sl-logo"
21 | src="/images/horizontal-logo.svg"
22 | style="height: 18px"
23 | />
24 | </div>
25 | <div class="beta-badge" v-if="isBeta">BETA</div>
26 | </div>
27 |
28 | <div v-if="apiKey === ''" class="actions-container">
29 | <span @click="goToSelfHostSetting" class="header-button float-right">
30 | Settings
31 | </span>
32 | </div>
33 |
34 | <div v-if="apiKey !== ''" class="actions-container">
35 | <span
36 | class="header-button float-right"
37 | @click="onClickSettingButton"
38 | v-show="canShowSettingsButton"
39 | title="Settings"
40 | v-b-tooltip.hover.bottomleft
41 | >
42 | <font-awesome-icon icon="cog" />
43 | </span>
44 |
45 | <a
46 | :href="reportBugUri"
47 | target="_blank"
48 | class="header-button float-right"
49 | title="Report an issue"
50 | v-if="isBeta"
51 | v-b-tooltip.hover.bottomleft
52 | >
53 | <font-awesome-icon icon="bug" />
54 | </a>
55 |
56 | <a
57 | :href="apiUrl + '/dashboard/'"
58 | target="_blank"
59 | class="dashboard-btn float-right"
60 | style="padding: 0.25rem 0.5rem; font-size: 0.875rem"
61 | title="Dashboard"
62 | v-b-tooltip.hover
63 | :disabled="!useCompactLayout"
64 | >
65 | <span v-if="!useCompactLayout">Dashboard</span>
66 | <font-awesome-icon icon="external-link-alt" />
67 | </a>
68 | </div>
69 | </div>
70 | </div>
71 | </template>
72 |
73 | <script>
74 | import SLStorage from "../SLStorage";
75 | import EventManager from "../EventManager";
76 | import Navigation from "../Navigation";
77 | import Utils from "../Utils";
78 |
79 | export default {
80 | name: "sl-header",
81 | props: {
82 | useCompactLayout: Boolean,
83 | },
84 | data() {
85 | return {
86 | apiKey: "",
87 | apiUrl: "",
88 | canBack: false,
89 | showDropdownMenu: false,
90 | isBeta: process.env.BETA,
91 | canShowSettingsButton: true,
92 | reportBugUri: "",
93 | };
94 | },
95 | async mounted() {
96 | this.apiKey = await SLStorage.get(SLStorage.SETTINGS.API_KEY);
97 | this.apiUrl = await SLStorage.get(SLStorage.SETTINGS.API_URL);
98 |
99 | EventManager.addListener(EventManager.EVENT.SETTINGS_CHANGED, async () => {
100 | this.apiKey = await SLStorage.get(SLStorage.SETTINGS.API_KEY);
101 | this.apiUrl = await SLStorage.get(SLStorage.SETTINGS.API_URL);
102 | });
103 |
104 | this.setReportBugUri();
105 | },
106 | watch: {
107 | $route(to, from) {
108 | this.canBack = Navigation.canGoBack();
109 | this.showDropdownMenu = false;
110 | this.canShowSettingsButton = to.path !== Navigation.PATH.APP_SETTINGS;
111 | },
112 | },
113 | methods: {
114 | goToSelfHostSetting: function () {
115 | Navigation.navigateTo(Navigation.PATH.SELF_HOST_SETTING, true);
116 | },
117 |
118 | navigateBack: function () {
119 | if (this.canBack) {
120 | Navigation.navigateBack();
121 | }
122 | },
123 |
124 | onClickSettingButton: function () {
125 | Navigation.navigateTo(Navigation.PATH.APP_SETTINGS, true);
126 | },
127 |
128 | async setReportBugUri() {
129 | const subject = encodeURIComponent("Report an issue on SimpleLogin");
130 | const hostname = await Utils.getHostName();
131 | const body = encodeURIComponent(
132 | "(Optional) Affected website: " +
133 | hostname +
134 | "\n" +
135 | "(Optional) Browser info: " +
136 | navigator.vendor +
137 | "; " +
138 | navigator.userAgent
139 | );
140 | this.reportBugUri = `mailto:extension@simplelogin.io?subject=${subject}&body=${body}`;
141 | },
142 | },
143 | computed: {},
144 | };
145 | </script>
146 |
--------------------------------------------------------------------------------
/src/popup/components/Login.vue:
--------------------------------------------------------------------------------
1 | <template>
2 | <div class="content">
3 | <!-- Login/register screen -->
4 | <div v-if="!isShowMfa" class="p-6 container" style="min-height: 350px">
5 | <h1 class="h5 mb-3">
6 | Welcome to
7 | <a href="https://simplelogin.io" target="_blank"
8 | >SimpleLogin
9 | <font-awesome-icon
10 | icon="long-arrow-alt-up"
11 | :transform="{ rotate: 45 }" /></a
12 | >, the most powerful email alias solution!
13 | </h1>
14 |
15 | <form @submit.prevent="login">
16 | <div class="form-group">
17 | <label>Email</label>
18 |
19 | <input
20 | v-model="email"
21 | class="form-control"
22 | type="email"
23 | autofocus
24 | required
25 | />
26 | </div>
27 |
28 | <div class="form-group">
29 | <label>Password</label>
30 | <input v-model="password" type="password" class="form-control" />
31 | </div>
32 |
33 | <button class="btn btn-primary btn-block mt-2">Login</button>
34 | </form>
35 |
36 | <!-- Login with Proton -->
37 | <div v-if="loginWithProtonEnabled">
38 | <div class="text-center my-2 text-gray"><span>or</span></div>
39 |
40 | <a
41 | class="btn btn-primary btn-block mt-2 proton-button"
42 | target="_blank"
43 | :href="apiUrl + '/auth/proton/login?next=/onboarding/setup_done'"
44 | >
45 | <img class="mr-2" src="/images/proton.svg" />
46 | Login with Proton
47 | </a>
48 | </div>
49 |
50 | <div class="text-center mt-2">
51 | <button @click="showApiKeySetup" class="mt-2 btn btn-link text-center">
52 | Sign in with API Key
53 | </button>
54 | </div>
55 |
56 | <div class="text-center">
57 | Don't have an account yet?
58 | <a
59 | :href="apiUrl + '/auth/register?next=%2Fdashboard%2Fsetup_done'"
60 | target="_blank"
61 | >
62 | Sign Up
63 | </a>
64 | </div>
65 | </div>
66 | <!-- END Login/register screen -->
67 |
68 | <!-- MFA screen -->
69 | <div v-else class="p-6 container" style="min-height: 350px">
70 | <div class="p-3">
71 | <div class="mb-2">
72 | Your account is protected with Two Factor Authentication. <br />
73 | </div>
74 |
75 | <div>
76 | <b>Token</b>
77 | <p>Please enter the 2FA code from your 2FA authenticator</p>
78 | </div>
79 |
80 | <div style="margin: auto">
81 | <input
82 | v-model="mfaCode"
83 | v-on:keyup.enter="submitMfaCode"
84 | placeholder="xxxxxx"
85 | autofocus
86 | class="form-control mt-3 w-100"
87 | />
88 | <button @click="submitMfaCode" class="btn btn-primary btn-block mt-2">
89 | Submit
90 | </button>
91 | </div>
92 | </div>
93 | </div>
94 | <!-- END MFA screen -->
95 | </div>
96 | </template>
97 |
98 | <script>
99 | import Utils from "../Utils";
100 | import SLStorage from "../SLStorage";
101 | import EventManager from "../EventManager";
102 | import Navigation from "../Navigation";
103 | import { callAPI, API_ROUTE, API_ON_ERR } from "../APIService";
104 |
105 | export default {
106 | data() {
107 | return {
108 | email: "",
109 | password: "",
110 | mfaKey: "",
111 | mfaCode: "",
112 | isShowMfa: false,
113 | apiUrl: "",
114 | loginWithProtonEnabled:
115 | Utils.getBuildConfig().features.loginWithProtonEnabled,
116 | };
117 | },
118 | async mounted() {
119 | this.apiUrl = await SLStorage.get(SLStorage.SETTINGS.API_URL);
120 | },
121 | methods: {
122 | async login() {
123 | try {
124 | const res = await callAPI(
125 | API_ROUTE.LOGIN,
126 | {},
127 | {
128 | email: this.email,
129 | password: this.password,
130 | device: Utils.getDeviceName(),
131 | }
132 | );
133 |
134 | if (res.data.api_key) {
135 | const userName = res.data.name || res.data.email;
136 | await SLStorage.set(SLStorage.SETTINGS.API_KEY, res.data.api_key);
137 | EventManager.broadcast(EventManager.EVENT.SETTINGS_CHANGED);
138 |
139 | Utils.showSuccess(`Hi ${userName}!`);
140 |
141 | Navigation.navigateTo(Navigation.PATH.MAIN);
142 | } else if (res.data.mfa_enabled) {
143 | this.mfaKey = res.data.mfa_key;
144 | this.isShowMfa = true;
145 | }
146 | } catch (err) {
147 | // FIDO
148 | if (err.response.status === 403) {
149 | Utils.showError(
150 | "WebAuthn/FIDO is not supported on browser extension yet, please use API Key to login"
151 | );
152 | } else {
153 | Utils.showError("Email or Password incorrect");
154 | }
155 | }
156 | },
157 |
158 | async submitMfaCode() {
159 | try {
160 | const res = await callAPI(
161 | API_ROUTE.MFA,
162 | {},
163 | {
164 | mfa_token: this.mfaCode,
165 | mfa_key: this.mfaKey,
166 | device: Utils.getDeviceName(),
167 | }
168 | );
169 |
170 | const userName = res.data.name || res.data.email;
171 | await SLStorage.set(SLStorage.SETTINGS.API_KEY, res.data.api_key);
172 | EventManager.broadcast(EventManager.EVENT.SETTINGS_CHANGED);
173 |
174 | Utils.showSuccess(`Hi ${userName}!`);
175 |
176 | Navigation.navigateTo(Navigation.PATH.MAIN);
177 | } catch (err) {
178 | Utils.showError("Incorrect MFA Code");
179 | this.mfaCode = "";
180 | }
181 | },
182 |
183 | showApiKeySetup: function () {
184 | Navigation.navigateTo(Navigation.PATH.API_KEY_SETTING, true);
185 | },
186 | },
187 | computed: {},
188 | };
189 | </script>
190 |
191 | <style lang="css">
192 | .proton-button {
193 | border-color: #6d4aff;
194 | background-color: var(--bg-color);
195 | color: #6d4aff;
196 | }
197 | .proton-button:hover {
198 | border-color: #6d4aff;
199 | background-color: #1b1340;
200 | color: var(--text-color);
201 | }
202 | .text-gray {
203 | color: #868e96;
204 | }
205 | </style>
206 |
--------------------------------------------------------------------------------
/src/popup/components/Main.vue:
--------------------------------------------------------------------------------
1 | <template>
2 | <div class="content" ref="content">
3 | <v-dialog />
4 |
5 | <!-- Main Page -->
6 | <div class="container">
7 | <div v-if="recommendation.show" class="text-center">
8 | <div class="" style="font-size: 14px">
9 | You created this alias on this website before:
10 | </div>
11 | <div class="flex-grow-1">
12 | <a
13 | v-clipboard="() => recommendation.alias"
14 | v-clipboard:success="clipboardSuccessHandler"
15 | v-clipboard:error="clipboardErrorHandler"
16 | class="cursor"
17 | >
18 | <span class="text-success recommended-alias">{{
19 | recommendation.alias
20 | }}</span>
21 | </a>
22 | </div>
23 |
24 | <hr />
25 | </div>
26 |
27 | <div>
28 | <form @submit.prevent="createCustomAlias">
29 | <div class="row mb-2">
30 | <div
31 | class="col align-self-start input-group-sm"
32 | style="padding-right: 0"
33 | >
34 | <input
35 | v-model="aliasPrefix"
36 | class="form-control"
37 | placeholder="Alias prefix"
38 | :disabled="loading || !canCreate"
39 | autofocus
40 | required
41 | />
42 | </div>
43 |
44 | <div
45 | class="col align-self-start input-group-sm"
46 | style="padding-left: 5px; padding-right: 5px"
47 | >
48 | <select
49 | v-model="signedSuffix"
50 | class="form-control"
51 | :disabled="loading || !canCreate"
52 | >
53 | <option
54 | v-for="suffix in aliasSuffixes"
55 | v-bind:key="suffix[0]"
56 | :value="suffix"
57 | >
58 | {{ suffix[0] }}
59 | </option>
60 | </select>
61 | </div>
62 |
63 | <button
64 | :disabled="loading || !canCreate"
65 | style="margin-right: 15px"
66 | class="btn btn-primary btn-sm align-self-start"
67 | >
68 | Create
69 | </button>
70 | </div>
71 | <div
72 | class="row text-danger"
73 | style="font-size: 12px"
74 | v-if="aliasPrefixError != ''"
75 | >
76 | <div class="col">
77 | {{ aliasPrefixError }}
78 | </div>
79 | </div>
80 | </form>
81 | </div>
82 |
83 | <div class="mb-1 text-center" v-if="aliasPrefix" style="font-size: 14px">
84 | You're about to create alias
85 | <span class="text-primary">{{ aliasPrefix }}{{ signedSuffix[0] }}</span>
86 | </div>
87 |
88 | <hr />
89 | <div class="text-center">
90 | <button
91 | :disabled="loading || !canCreate"
92 | style="margin-left: 15px"
93 | class="btn btn-outline-primary btn-sm"
94 | @click="createRandomAlias"
95 | >
96 | <font-awesome-icon icon="random" />
97 | OR create a totally random alias
98 | </button>
99 | </div>
100 |
101 | <div v-if="!canCreate">
102 | <p class="text-danger" style="font-size: 14px">
103 | You have reached limit number of email aliases in free plan, please
104 | <span
105 | @click="upgrade"
106 | style="cursor: pointer; color: blue; text-decoration: underline"
107 | >upgrade</span
108 | >
109 | or reuse one of the existing aliases.
110 | </p>
111 | </div>
112 | <hr />
113 |
114 | <div v-if="aliasArray.length > 0 || searchString !== ''">
115 | <div class="mx-auto font-weight-bold text-center mb-2">
116 | OR use an existing alias
117 | </div>
118 |
119 | <div class="mx-auto" style="max-width: 60%">
120 | <input
121 | v-model="searchString"
122 | v-on:keyup.enter="loadAlias"
123 | class="form-control form-control-sm"
124 | placeholder="Search"
125 | />
126 |
127 | <div class="small-text mt-1">
128 | Type enter to search.
129 | <button
130 | v-if="searchString"
131 | @click="resetSearch"
132 | class="float-right"
133 | style="color: blue; border: none; padding: 0; background: none"
134 | >
135 | Reset
136 | </button>
137 | </div>
138 | </div>
139 |
140 | <!-- list alias -->
141 | <div v-if="aliasArray.length > 0">
142 | <div v-for="(alias, index) in aliasArray" v-bind:key="alias.id">
143 | <div class="p-2 my-2 list-item-alias">
144 | <div class="d-flex" v-bind:class="{ disabled: !alias.enabled }">
145 | <div
146 | class="flex-grow-1 list-item-email"
147 | v-clipboard="() => alias.email"
148 | v-clipboard:success="clipboardSuccessHandler"
149 | v-clipboard:error="clipboardErrorHandler"
150 | >
151 | <a class="cursor" v-b-tooltip.hover.top="'Click to Copy'">
152 | {{ alias.email }}
153 | </a>
154 | <div class="list-item-email-fade" />
155 | </div>
156 | <div style="white-space: nowrap">
157 | <toggle-button
158 | :value="alias.enabled"
159 | color="#b02a8f"
160 | :width="30"
161 | :height="18"
162 | @change="toggleAlias(alias)"
163 | />
164 |
165 | <div
166 | class="btn-svg btn-send"
167 | @click="handleReverseAliasClick(alias)"
168 | >
169 | <font-awesome-icon icon="paper-plane" />
170 | </div>
171 |
172 | <img
173 | src="/images/icon-dropdown.svg"
174 | v-if="alias"
175 | v-bind:style="{
176 | transform: alias.showMoreOptions ? 'rotate(180deg)' : '',
177 | }"
178 | v-on:click="toggleMoreOptions(index)"
179 | class="btn-svg"
180 | />
181 | </div>
182 | </div>
183 |
184 | <div
185 | v-if="alias.note"
186 | class="font-weight-light alias-note-preview"
187 | >
188 | {{ alias.note }}
189 | </div>
190 |
191 | <div class="font-weight-lighter" style="font-size: 11px">
192 | {{ alias.nb_forward }} forwards, {{ alias.nb_reply }} replies,
193 | {{ alias.nb_block }} blocks.
194 | </div>
195 |
196 | <alias-more-options
197 | :alias="alias"
198 | :index="index"
199 | :show="!!alias.showMoreOptions"
200 | :mailboxes="mailboxes"
201 | @changed="handleAliasChanged"
202 | @deleted="handleAliasDeleted"
203 | />
204 | </div>
205 | </div>
206 | </div>
207 | </div>
208 |
209 | <div v-if="isFetchingAlias" class="text-secondary mx-auto text-center">
210 | <img
211 | src="/images/loading-three-dots.svg"
212 | style="width: 80px; margin: 20px"
213 | />
214 | </div>
215 | </div>
216 | <!-- END Main Page -->
217 | </div>
218 | </template>
219 |
220 | <script>
221 | import Utils from "../Utils";
222 | import SLStorage from "../SLStorage";
223 | import Navigation from "../Navigation";
224 | import AliasMoreOptions from "./AliasMoreOptions";
225 | import { callAPI, API_ROUTE, API_ON_ERR } from "../APIService";
226 | import tippy from "tippy.js";
227 |
228 | const ALIAS_PREFIX_REGEX = /^[0-9a-z-_.]+$/;
229 |
230 | export default {
231 | components: {
232 | "alias-more-options": AliasMoreOptions,
233 | },
234 | data() {
235 | return {
236 | apiUrl: "",
237 | apiKey: "",
238 | loading: true,
239 |
240 | // variables for creating alias
241 | hostName: "", // hostName obtained from chrome tabs query
242 | canCreate: true,
243 | aliasSuffixes: [],
244 | aliasPrefix: "",
245 | aliasPrefixError: "",
246 | signedSuffix: "",
247 | recommendation: {
248 | show: false,
249 | alias: "",
250 | },
251 | mailboxes: [],
252 |
253 | // variables for list alias
254 | isFetchingAlias: true,
255 | searchString: "",
256 | aliasArray: [], // array of existing alias
257 |
258 | canCreateReverseAlias: false,
259 | };
260 | },
261 | async mounted() {
262 | try {
263 | this.hostName = await Utils.getHostName();
264 | this.apiUrl = await SLStorage.get(SLStorage.SETTINGS.API_URL);
265 | this.apiKey = await SLStorage.get(SLStorage.SETTINGS.API_KEY);
266 |
267 | this.contentElem = this.$refs.content;
268 |
269 | await this.getUserOptions();
270 | await this.getUserInfo();
271 |
272 | if (this.apiKey && process.env.MAC) {
273 | console.log("send api key to host app");
274 | await browser.runtime.sendNativeMessage(
275 | "application.id",
276 | JSON.stringify({
277 | logged_in: {
278 | data: {
279 | api_key: this.apiKey,
280 | api_url: this.apiUrl,
281 | },
282 | },
283 | })
284 | );
285 | }
286 | } catch (e) {
287 | console.error("Can't display alias list ", e);
288 | }
289 | },
290 | methods: {
291 | // get alias options and mailboxes
292 | async getUserOptions() {
293 | this.loading = true;
294 |
295 | const results = await Promise.all([
296 | callAPI(
297 | API_ROUTE.GET_ALIAS_OPTIONS,
298 | {
299 | hostname: this.hostName,
300 | },
301 | API_ON_ERR.TOAST
302 | ),
303 | callAPI(API_ROUTE.GET_MAILBOXES, {}, API_ON_ERR.TOAST),
304 | ]);
305 |
306 | const aliasOptions = results[0].data;
307 | const { mailboxes } = results[1].data;
308 |
309 | if (aliasOptions.recommendation) {
310 | this.recommendation.show = true;
311 | this.recommendation.alias = aliasOptions.recommendation.alias;
312 | }
313 |
314 | this.aliasSuffixes = aliasOptions.suffixes;
315 | this.signedSuffix = this.aliasSuffixes[0];
316 | this.aliasPrefix = aliasOptions.prefix_suggestion;
317 | this.canCreate = aliasOptions.can_create;
318 | this.mailboxes = mailboxes;
319 |
320 | this.loading = false;
321 |
322 | await this.loadAlias();
323 |
324 | tippy(".recommended-alias", {
325 | content: "Click to copy",
326 | placement: "bottom",
327 | });
328 | },
329 |
330 | async getUserInfo() {
331 | const userInfo = await callAPI(
332 | API_ROUTE.GET_USER_INFO,
333 | {},
334 | {},
335 | API_ON_ERR.TOAST
336 | );
337 | this.canCreateReverseAlias = userInfo.data.can_create_reverse_alias;
338 | },
339 |
340 | async loadAlias() {
341 | const contentElem = this.contentElem;
342 | this.aliasArray = [];
343 | let currentPage = 0;
344 |
345 | this.aliasArray = await this.fetchAlias(currentPage, this.searchString);
346 |
347 | let allAliasesAreLoaded = false;
348 |
349 | let that = this;
350 | if (this.onScrollCallback) {
351 | contentElem.removeEventListener("scroll", this.onScrollCallback);
352 | }
353 |
354 | this.onScrollCallback = async function () {
355 | if (that.isFetchingAlias || allAliasesAreLoaded) return;
356 |
357 | let bottomOfWindow =
358 | contentElem.scrollTop + contentElem.clientHeight >
359 | contentElem.scrollHeight - 100;
360 |
361 | if (bottomOfWindow) {
362 | currentPage += 1;
363 |
364 | let newAliases = await that.fetchAlias(
365 | currentPage,
366 | that.searchString
367 | );
368 |
369 | allAliasesAreLoaded = newAliases.length === 0;
370 | that.aliasArray = mergeAliases(that.aliasArray, newAliases);
371 | }
372 | };
373 |
374 | contentElem.addEventListener("scroll", this.onScrollCallback);
375 | },
376 |
377 | async fetchAlias(page, query) {
378 | this.isFetchingAlias = true;
379 | try {
380 | const { data } = await callAPI(
381 | API_ROUTE.GET_ALIASES,
382 | {
383 | page_id: page,
384 | },
385 | {
386 | query,
387 | }
388 | );
389 | this.isFetchingAlias = false;
390 | return data.aliases;
391 | } catch (e) {
392 | Utils.showError("Cannot fetch list alias");
393 | this.isFetchingAlias = false;
394 | return [];
395 | }
396 | },
397 |
398 | async resetSearch() {
399 | this.searchString = "";
400 | await this.loadAlias();
401 | },
402 |
403 | async createCustomAlias() {
404 | if (this.loading) return;
405 |
406 | // check aliasPrefix
407 | this.aliasPrefixError = "";
408 | if (this.aliasPrefix.match(ALIAS_PREFIX_REGEX) === null) {
409 | this.aliasPrefixError =
410 | "Only lowercase letters, dots, numbers, dashes (-) and underscores (_) are currently supported.";
411 | return;
412 | }
413 |
414 | this.loading = true;
415 |
416 | try {
417 | const res = await callAPI(
418 | API_ROUTE.NEW_ALIAS,
419 | {
420 | hostname: this.hostName,
421 | },
422 | {
423 | alias_prefix: this.aliasPrefix,
424 | signed_suffix: this.signedSuffix[1],
425 | note: await Utils.getDefaultNote(),
426 | }
427 | );
428 |
429 | if (res.status === 201) {
430 | SLStorage.setTemporary("newAliasData", res.data);
431 | SLStorage.setTemporary("userMailboxes", this.mailboxes);
432 | Navigation.navigateTo(Navigation.PATH.NEW_ALIAS_RESULT);
433 | } else {
434 | Utils.showError(res.data.error);
435 | }
436 | } catch (err) {
437 | // rate limit reached
438 | if (err.response.status === 429) {
439 | Utils.showError(
440 | "Rate limit exceeded - please wait 60s before creating new alias"
441 | );
442 | } else if (err.response.status === 409) {
443 | Utils.showError("Alias already chosen, please select another one");
444 | } else if (err.response.status === 412) {
445 | // can happen when the alias creation time slot is expired,
446 | // i.e user waits for too long before creating the alias
447 | Utils.showError(err.response.data.error);
448 |
449 | // get new aliasSuffixes
450 | this.getAliasOptions();
451 | } else {
452 | Utils.showError("Unknown error");
453 | }
454 | }
455 |
456 | this.loading = false;
457 | },
458 |
459 | async createRandomAlias() {
460 | if (this.loading) return;
461 | this.loading = true;
462 |
463 | try {
464 | const res = await callAPI(
465 | API_ROUTE.NEW_RANDOM_ALIAS,
466 | {
467 | hostname: "",
468 | },
469 | {
470 | note: await Utils.getDefaultNote(),
471 | }
472 | );
473 |
474 | if (res.status === 201) {
475 | SLStorage.setTemporary("newAliasData", res.data);
476 | SLStorage.setTemporary("userMailboxes", this.mailboxes);
477 | Navigation.navigateTo(Navigation.PATH.NEW_ALIAS_RESULT);
478 | } else {
479 | Utils.showError(res.data.error);
480 | }
481 | } catch (err) {
482 | // rate limit reached
483 | if (err.response.status === 429) {
484 | Utils.showError(
485 | "Rate limit exceeded - please wait 60s before creating new alias"
486 | );
487 | } else {
488 | Utils.showError("Unknown error");
489 | }
490 | }
491 |
492 | this.loading = false;
493 | },
494 | async toggleAlias(alias) {
495 | const lastState = alias.enabled;
496 | alias.loading = true;
497 | const res = await callAPI(
498 | API_ROUTE.TOGGLE_ALIAS,
499 | {
500 | alias_id: alias.id,
501 | },
502 | {},
503 | API_ON_ERR.TOAST
504 | );
505 |
506 | if (res) {
507 | alias.enabled = res.data.enabled;
508 | Utils.showSuccess(
509 | alias.email + " is " + (alias.enabled ? "enabled" : "disabled")
510 | );
511 | } else {
512 | alias.enabled = lastState;
513 | }
514 |
515 | alias.loading = false;
516 | },
517 |
518 | // More options
519 | toggleMoreOptions(index) {
520 | const alias = this.aliasArray[index];
521 | this.$set(this.aliasArray, index, {
522 | ...alias,
523 | showMoreOptions: !alias.showMoreOptions,
524 | });
525 | },
526 | handleAliasDeleted(event) {
527 | this.aliasArray.splice(event.index, 1);
528 | },
529 | handleAliasChanged(event) {
530 | const alias = this.aliasArray[event.index];
531 | for (const key in event.data) {
532 | alias[key] = event.data[key];
533 | }
534 | },
535 |
536 | async upgrade() {
537 | if (process.env.MAC) {
538 | try {
539 | console.log("send upgrade event to host app");
540 | await browser.runtime.sendNativeMessage(
541 | "application.id",
542 | JSON.stringify({
543 | upgrade: {},
544 | })
545 | );
546 | } catch (error) {
547 | console.info("can't send data to native app", error);
548 | }
549 | } else {
550 | let upgradeURL = this.apiUrl + "/dashboard/pricing";
551 | browser.tabs.create({ url: upgradeURL });
552 | }
553 | },
554 |
555 | handleReverseAliasClick(alias) {
556 | if (this.canCreateReverseAlias) {
557 | SLStorage.setTemporary("alias", alias);
558 | Navigation.navigateTo(Navigation.PATH.REVERSE_ALIAS, true);
559 | } else {
560 | this.$modal.show("dialog", {
561 | title: `Send emails`,
562 | text: "Sending a new email using an alias is a premium feature.",
563 | buttons: [
564 | {
565 | title: "Cancel",
566 | handler: () => {
567 | this.$modal.hide("dialog");
568 | },
569 | },
570 | {
571 | title: "Upgrade now",
572 | handler: () => {
573 | this.$modal.hide("dialog");
574 | this.upgrade();
575 | },
576 | },
577 | ],
578 | });
579 | }
580 | },
581 |
582 | // Clipboard
583 | clipboardSuccessHandler({ value, event }) {
584 | Utils.showSuccess(value + " copied to clipboard");
585 | },
586 |
587 | clipboardErrorHandler({ value, event }) {
588 | console.error("error", value);
589 | },
590 | },
591 | computed: {},
592 | };
593 |
594 | // merge newAliases into currentAliases. If conflict, keep the new one
595 | function mergeAliases(currentAliases, newAliases) {
596 | // dict of aliasId and alias to speed up research
597 | let newAliasesDict = {};
598 | for (var i = 0; i < newAliases.length; i++) {
599 | let alias = newAliases[i];
600 | newAliasesDict[alias.id] = alias;
601 | }
602 |
603 | let ret = [];
604 |
605 | // keep track of added aliases
606 | let alreadyAddedId = {};
607 | for (var i = 0; i < currentAliases.length; i++) {
608 | let alias = currentAliases[i];
609 | if (newAliasesDict[alias.id]) ret.push(newAliasesDict[alias.id]);
610 | else ret.push(alias);
611 |
612 | alreadyAddedId[alias.id] = true;
613 | }
614 |
615 | for (var i = 0; i < newAliases.length; i++) {
616 | let alias = newAliases[i];
617 | if (!alreadyAddedId[alias.id]) {
618 | ret.push(alias);
619 | }
620 | }
621 |
622 | return ret;
623 | }
624 | </script>
625 |
--------------------------------------------------------------------------------
/src/popup/components/NewAliasResult.vue:
--------------------------------------------------------------------------------
1 | <template>
2 | <div class="content">
3 | <div class="p-2 container">
4 | <div class="m-2 p-2">
5 | <p class="font-weight-bold">Alias is created</p>
6 | <p>
7 | <a
8 | v-clipboard="() => newAliasData.alias"
9 | v-clipboard:success="clipboardSuccessHandler"
10 | v-clipboard:error="clipboardErrorHandler"
11 | class="cursor new-alias"
12 | >
13 | <span class="text-success">
14 | {{ newAliasData.alias }}
15 | </span>
16 | </a>
17 | </p>
18 |
19 | <alias-more-options
20 | :alias="newAliasData"
21 | :index="0"
22 | :show="true"
23 | :mailboxes="mailboxes"
24 | btnSaveLabel="Save & Back"
25 | @changed="backToMainPage"
26 | @deleted="backToMainPage"
27 | />
28 |
29 | <div class="mt-5 p-3 card-rating" v-if="showVoteScreen">
30 | Happy with SimpleLogin?<br />
31 | Please support us by
32 | <a :href="extensionUrl" target="_blank" rel="noreferrer noopener">
33 | <font-awesome-icon icon="star" /> Rating this extension </a
34 | ><br />
35 | Thank you!
36 |
37 | <br />
38 |
39 | <a
40 | @click="doNotAskRateAgain"
41 | class="text-secondary cursor"
42 | style="font-size: 0.7em"
43 | >
44 | Do not ask again
45 | </a>
46 | </div>
47 | </div>
48 | </div>
49 | </div>
50 | </template>
51 |
52 | <script>
53 | import SLStorage from "../SLStorage";
54 | import Navigation from "../Navigation";
55 | import EventManager from "../EventManager";
56 | import Utils from "../Utils";
57 | import AliasMoreOptions from "./AliasMoreOptions";
58 | import { callAPI, API_ROUTE, API_ON_ERR } from "../APIService";
59 | import tippy from "tippy.js";
60 |
61 | export default {
62 | components: {
63 | "alias-more-options": AliasMoreOptions,
64 | },
65 | data() {
66 | return {
67 | mailboxes: SLStorage.getTemporary("userMailboxes"),
68 | newAliasData: SLStorage.getTemporary("newAliasData"),
69 | showVoteScreen: false,
70 | extensionUrl: Utils.getExtensionURL(),
71 | };
72 | },
73 | async mounted() {
74 | this.newAlias = this.$route.params.email;
75 | let notAskingRate = await SLStorage.get(SLStorage.SETTINGS.NOT_ASKING_RATE);
76 | if (!!notAskingRate) this.showVoteScreen = false;
77 | // TODO showVoteScreen 1 day after user installed plugin
78 | else this.showVoteScreen = Utils.getRandomIntBetween(0, 10) % 2 === 0;
79 |
80 | tippy(".new-alias", {
81 | content: "Click to copy",
82 | trigger: "manual",
83 | showOnCreate: true,
84 | });
85 | },
86 | methods: {
87 | // Clipboard
88 | clipboardSuccessHandler({ value, event }) {
89 | Utils.showSuccess(value + " copied to clipboard");
90 | },
91 |
92 | clipboardErrorHandler({ value, event }) {
93 | console.error("error", value);
94 | },
95 |
96 | async backToMainPage() {
97 | Navigation.navigateTo(Navigation.PATH.MAIN);
98 | },
99 |
100 | doNotAskRateAgain() {
101 | this.showVoteScreen = false;
102 | SLStorage.set(SLStorage.SETTINGS.NOT_ASKING_RATE, true);
103 | },
104 | },
105 | computed: {},
106 | };
107 | </script>
108 |
--------------------------------------------------------------------------------
/src/popup/components/ReverseAlias.vue:
--------------------------------------------------------------------------------
1 | <template>
2 | <div class="content">
3 | <div class="p-2 container">
4 | <!-- Reverse-alias screen -->
5 | <div class="m-2 p-2" v-if="!createdReverseAlias">
6 | <p>
7 | Send emails from
8 | <span class="font-weight-bold">{{ alias.email }}</span>
9 | </p>
10 | <small>
11 | To send an email from your alias to a contact, you need to create a
12 | <b>reverse-alias</b>, a special email address. When you send an email
13 | to the reverse-alias, the email will be sent from your alias to the
14 | contact.<br /><br />
15 | This Youtube video can also quickly walk you through the steps:
16 | <a
17 | href="https://www.youtube.com/watch?v=VsypF-DBaow"
18 | target="_blank"
19 | rel="noopener noreferrer"
20 | >How to send emails from an alias</a
21 | >
22 | </small>
23 | <br /><br />
24 | <label>
25 | Receiver:
26 | <font-awesome-icon
27 | v-b-tooltip.hover.top="'Where do you want to send the email?'"
28 | icon="question-circle"
29 | />
30 | </label>
31 | <b-input
32 | v-model="receiverEmail"
33 | v-on:keyup.enter="createReverseAlias"
34 | placeholder="First Last <email@example.com>"
35 | :disabled="loading"
36 | />
37 | </div>
38 |
39 | <!-- Created screen -->
40 | <div class="m-2 p-2" v-else>
41 | <p class="font-weight-bold">
42 | {{
43 | createdReverseAlias.existed
44 | ? "You have created this reverse-alias before:"
45 | : "Reverse-alias is created:"
46 | }}
47 | </p>
48 | <p>
49 | <a
50 | v-clipboard="() => createdReverseAlias.reverse_alias"
51 | v-clipboard:success="clipboardSuccessHandler"
52 | v-clipboard:error="clipboardErrorHandler"
53 | v-b-tooltip.hover
54 | title="Click to Copy"
55 | class="cursor"
56 | >
57 | <span class="text-success">
58 | {{ createdReverseAlias.reverse_alias }}
59 | </span>
60 | </a>
61 | </p>
62 | <small>
63 | You can send email from one of these mailbox(es):
64 | <ul style="margin-bottom: 0">
65 | <li v-for="mailbox in alias.mailboxes" v-bind:key="mailbox.id">
66 | {{ mailbox.email }}
67 | </li>
68 | </ul>
69 | The email will be forwarded to
70 | <b>{{ createdReverseAlias.contact }}</b
71 | >.<br />
72 | The receiver will see <b>{{ alias.email }}</b> as your email
73 | address.<br />
74 | </small>
75 | </div>
76 |
77 | <div class="m-2 p-2">
78 | <button
79 | class="btn btn-sm btn-primary"
80 | @click="createReverseAlias"
81 | :disabled="loading || !receiverEmail"
82 | v-if="!createdReverseAlias"
83 | >
84 | Create a reverse-alias
85 | </button>
86 |
87 | <button class="btn btn-sm btn-primary" @click="backToMainPage" v-else>
88 | <font-awesome-icon icon="arrow-left" />
89 | Back
90 | </button>
91 | </div>
92 | </div>
93 | </div>
94 | </template>
95 |
96 | <script>
97 | import SLStorage from "../SLStorage";
98 | import Navigation from "../Navigation";
99 | import Utils from "../Utils";
100 | import { callAPI, API_ROUTE, API_ON_ERR } from "../APIService";
101 |
102 | export default {
103 | data() {
104 | return {
105 | alias: SLStorage.getTemporary("alias"),
106 | createdReverseAlias: null,
107 | loading: false,
108 | receiverEmail: "",
109 | };
110 | },
111 | methods: {
112 | // Clipboard
113 | clipboardSuccessHandler({ value, event }) {
114 | Utils.showSuccess(value + " copied to clipboard");
115 | },
116 |
117 | clipboardErrorHandler({ value, event }) {
118 | console.error("error", value);
119 | },
120 |
121 | // Create reverse-alias
122 | async createReverseAlias() {
123 | this.loading = true;
124 | const response = await callAPI(
125 | API_ROUTE.CREATE_REVERSE_ALIAS,
126 | {
127 | alias_id: this.alias.id,
128 | },
129 | {
130 | contact: this.receiverEmail,
131 | },
132 | API_ON_ERR.TOAST
133 | );
134 | this.createdReverseAlias = response ? response.data : null;
135 | this.loading = false;
136 | },
137 |
138 | backToMainPage() {
139 | Navigation.navigateBack();
140 | },
141 | },
142 | computed: {},
143 | };
144 | </script>
145 |
--------------------------------------------------------------------------------
/src/popup/components/SelfHostSetting.vue:
--------------------------------------------------------------------------------
1 | <template>
2 | <div class="content">
3 | <div class="p-3">
4 | <div class="mb-2">
5 | If you self-host SimpleLogin, you can change the API URL to your server
6 | address.
7 | </div>
8 | <div class="mb-2">The default API URL is https://app.simplelogin.io</div>
9 |
10 | <div style="margin: auto">
11 | <input
12 | v-model="apiUrl"
13 | v-on:keyup.enter="saveApiUrl"
14 | placeholder="https://app.simplelogin.io"
15 | autofocus
16 | class="form-control mt-3 w-100"
17 | />
18 | <button @click="saveApiUrl" class="btn btn-primary btn-block mt-2">
19 | Set API URL
20 | </button>
21 | </div>
22 | </div>
23 | </div>
24 | </template>
25 |
26 | <script>
27 | import Utils from "../Utils";
28 | import SLStorage from "../SLStorage";
29 | import EventManager from "../EventManager";
30 |
31 | export default {
32 | data() {
33 | return {
34 | apiUrl: "",
35 | };
36 | },
37 | async mounted() {
38 | this.apiUrl = await SLStorage.get(SLStorage.SETTINGS.API_URL);
39 | },
40 | methods: {
41 | async saveApiUrl() {
42 | // remove last slash
43 | this.apiUrl = this.apiUrl.replace(/\/$/, "");
44 |
45 | // save apiUrl to storage
46 | await SLStorage.set(
47 | SLStorage.SETTINGS.API_URL,
48 | this.apiUrl !== ""
49 | ? this.apiUrl
50 | : SLStorage.DEFAULT_SETTINGS[SLStorage.SETTINGS.API_URL]
51 | );
52 | EventManager.broadcast(EventManager.EVENT.SETTINGS_CHANGED);
53 |
54 | Utils.showSuccess("API URL saved successfully");
55 | },
56 | },
57 | };
58 | </script>
59 |
--------------------------------------------------------------------------------
/src/popup/components/SplashScreen.vue:
--------------------------------------------------------------------------------
1 | <template>
2 | <div v-if="show" style="height: 400px">
3 | <div class="splash overlay">
4 | <div class="overlay-content">
5 | <img class="logo" src="/images/horizontal-logo.svg" /><br />
6 | <img class="loading" src="/images/loading.svg" />
7 | </div>
8 | </div>
9 | </div>
10 | </template>
11 |
12 | <script>
13 | import SLStorage from "../SLStorage";
14 | import EventManager from "../EventManager";
15 | import Navigation from "../Navigation";
16 | import Utils from "../Utils";
17 | import { API_ROUTE } from "../APIService";
18 |
19 | export default {
20 | name: "sl-loading",
21 | data() {
22 | return {
23 | apiKey: "",
24 | show: false,
25 | };
26 | },
27 | async mounted() {
28 | this.apiKey = await SLStorage.get(SLStorage.SETTINGS.API_KEY);
29 |
30 | // only show after waiting for more than 500ms
31 | this.timeoutId = setTimeout(() => {
32 | this.show = true;
33 | }, 500);
34 |
35 | if (this.apiKey !== "") {
36 | Navigation.navigateTo(Navigation.PATH.MAIN, true);
37 | } else {
38 | // try to get api key when user is already logged in
39 | const apiUrl = await SLStorage.get(SLStorage.SETTINGS.API_URL);
40 |
41 | const res = await fetch(apiUrl + API_ROUTE.GET_API_KEY_FROM_COOKIE.path, {
42 | method: "POST",
43 | body: JSON.stringify({
44 | device: Utils.getDeviceName(),
45 | }),
46 | headers: {
47 | "X-Sl-Allowcookies": true,
48 | },
49 | });
50 |
51 | if (res.ok) {
52 | let apiRes = await res.json();
53 | this.apiKey = apiRes.api_key || "";
54 | if (this.apiKey) {
55 | await SLStorage.set(SLStorage.SETTINGS.API_KEY, this.apiKey);
56 | EventManager.broadcast(EventManager.EVENT.SETTINGS_CHANGED);
57 |
58 | Navigation.navigateTo(Navigation.PATH.MAIN, true);
59 | } else {
60 | Navigation.navigateTo(Navigation.PATH.LOGIN, true);
61 | }
62 | } else {
63 | // user is probably not logged in
64 | Navigation.navigateTo(Navigation.PATH.LOGIN, true);
65 | }
66 | }
67 | },
68 | beforeDestroy() {
69 | clearTimeout(this.timeoutId);
70 | },
71 | methods: {},
72 | computed: {},
73 | };
74 | </script>
75 |
--------------------------------------------------------------------------------
/src/popup/components/TextareaAutosize.vue:
--------------------------------------------------------------------------------
1 | <template>
2 | <textarea :style="computedStyles" v-model="val" @focus="resize"></textarea>
3 | </template>
4 | <script>
5 | export default {
6 | name: "TextareaAutosize",
7 | props: {
8 | value: {
9 | type: [String, Number],
10 | default: "",
11 | },
12 | autosize: {
13 | type: Boolean,
14 | default: true,
15 | },
16 | minHeight: {
17 | type: [Number],
18 | default: null,
19 | },
20 | maxHeight: {
21 | type: [Number],
22 | default: null,
23 | },
24 | /*
25 | * Force !important for style properties
26 | */
27 | important: {
28 | type: [Boolean, Array],
29 | default: false,
30 | },
31 | },
32 | data() {
33 | return {
34 | // data property for v-model binding with real textarea tag
35 | val: null,
36 | // works when content height becomes more then value of the maxHeight property
37 | maxHeightScroll: false,
38 | height: "auto",
39 | };
40 | },
41 | computed: {
42 | computedStyles() {
43 | if (!this.autosize) return {};
44 | return {
45 | resize: !this.isResizeImportant ? "none" : "none !important",
46 | height: this.height,
47 | overflow: this.maxHeightScroll
48 | ? "auto"
49 | : !this.isOverflowImportant
50 | ? "hidden"
51 | : "hidden !important",
52 | };
53 | },
54 | isResizeImportant() {
55 | const imp = this.important;
56 | return imp === true || (Array.isArray(imp) && imp.includes("resize"));
57 | },
58 | isOverflowImportant() {
59 | const imp = this.important;
60 | return imp === true || (Array.isArray(imp) && imp.includes("overflow"));
61 | },
62 | isHeightImportant() {
63 | const imp = this.important;
64 | return imp === true || (Array.isArray(imp) && imp.includes("height"));
65 | },
66 | },
67 | watch: {
68 | value(val) {
69 | this.val = val;
70 | },
71 | val(val) {
72 | this.$nextTick(this.resize);
73 | this.$emit("input", val);
74 | },
75 | minHeight() {
76 | this.$nextTick(this.resize);
77 | },
78 | maxHeight() {
79 | this.$nextTick(this.resize);
80 | },
81 | autosize(val) {
82 | if (val) this.resize();
83 | },
84 | },
85 | methods: {
86 | resize() {
87 | const important = this.isHeightImportant ? "important" : "";
88 | this.height = `auto${important ? " !important" : ""}`;
89 | this.$nextTick(() => {
90 | let contentHeight = this.$el.scrollHeight + 1;
91 |
92 | if (this.minHeight) {
93 | contentHeight =
94 | contentHeight < this.minHeight ? this.minHeight : contentHeight;
95 | }
96 |
97 | if (this.maxHeight) {
98 | if (contentHeight > this.maxHeight) {
99 | contentHeight = this.maxHeight;
100 | this.maxHeightScroll = true;
101 | } else {
102 | this.maxHeightScroll = false;
103 | }
104 | }
105 |
106 | const heightVal = contentHeight + "px";
107 | this.height = `${heightVal}${important ? " !important" : ""}`;
108 | });
109 |
110 | return this;
111 | },
112 | },
113 | created() {
114 | this.val = this.value;
115 | },
116 | mounted() {
117 | this.resize();
118 | },
119 | };
120 | </script>
121 |
--------------------------------------------------------------------------------
/src/popup/popup.html:
--------------------------------------------------------------------------------
1 | <!DOCTYPE html>
2 | <html lang="en">
3 | <head>
4 | <meta charset="UTF-8">
5 | <title>SimpleLogin Extension</title>
6 | <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
7 |
8 | <link rel="stylesheet" href="popup.css">
9 | <% if (NODE_ENV === 'development') { %>
10 | <!-- Load some resources only in development environment -->
11 | <% } %>
12 | </head>
13 | <body style="overflow: hidden;">
14 | <div id="app"></div>
15 | <script src="popup.js"></script>
16 | </body>
17 | </html>
18 |
--------------------------------------------------------------------------------
/src/popup/popup.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import App from "./App";
3 | import Clipboard from "v-clipboard";
4 | import Toasted from "vue-toasted";
5 | import BootstrapVue from "bootstrap-vue";
6 | import SLStorage from "./SLStorage";
7 |
8 | import * as Sentry from "@sentry/browser";
9 | import * as Integrations from "@sentry/integrations";
10 | import VModal from "vue-js-modal";
11 | import VueRouter from "vue-router";
12 | import ToggleButton from "vue-js-toggle-button";
13 |
14 | import { library } from "@fortawesome/fontawesome-svg-core";
15 | import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
16 | import {
17 | faRandom,
18 | faExternalLinkAlt,
19 | faTrash,
20 | faLongArrowAltUp,
21 | faChevronLeft,
22 | faCopy,
23 | faStar,
24 | faSave,
25 | faBug,
26 | faQuestionCircle,
27 | faCog,
28 | faPaperPlane,
29 | faArrowLeft,
30 | } from "@fortawesome/free-solid-svg-icons";
31 |
32 | library.add(
33 | faRandom,
34 | faExternalLinkAlt,
35 | faTrash,
36 | faLongArrowAltUp,
37 | faChevronLeft,
38 | faCopy,
39 | faStar,
40 | faSave,
41 | faBug,
42 | faQuestionCircle,
43 | faCog,
44 | faPaperPlane,
45 | faArrowLeft
46 | );
47 |
48 | global.browser = require("webextension-polyfill");
49 | Vue.prototype.$browser = global.browser;
50 |
51 | // async wrapper
52 | async function initApp() {
53 | const apiUrl = await SLStorage.get(SLStorage.SETTINGS.API_URL);
54 |
55 | if (
56 | // only enable Sentry for non self-hosting users
57 | apiUrl === SLStorage.DEFAULT_SETTINGS[SLStorage.SETTINGS.API_URL] &&
58 | // and not in development mode
59 | process.env.NODE_ENV !== "development"
60 | ) {
61 | Sentry.init({
62 | dsn: "https://6990c2b0a6e94b57a2b80587efcb4354@api.protonmail.ch/core/v4/reports/sentry/51",
63 | integrations: [
64 | new Integrations.Vue({ Vue, attachProps: true, logErrors: true }),
65 | ],
66 | environment: process.env.BETA ? "beta" : "prod",
67 | });
68 | }
69 |
70 | Vue.use(Clipboard);
71 | Vue.use(Toasted, { duration: 1000, position: "bottom-right" });
72 | Vue.use(BootstrapVue);
73 | Vue.use(VModal, { dialog: true });
74 | Vue.use(VueRouter);
75 | Vue.use(ToggleButton);
76 | Vue.component("font-awesome-icon", FontAwesomeIcon);
77 |
78 | /* eslint-disable no-new */
79 | new Vue({
80 | el: "#app",
81 | render: (h) => h(App),
82 | });
83 | }
84 |
85 | initApp();
86 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const ejs = require('ejs');
3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
4 | const CopyWebpackPlugin = require('copy-webpack-plugin');
5 | const { VueLoaderPlugin } = require('vue-loader');
6 | const { version, betaRev } = require('./package.json');
7 | const fs = require('fs');
8 |
9 |
10 | const loadDevConfig = () => {
11 | if (fs.existsSync('./.dev.json')) {
12 | return JSON.parse(fs.readFileSync('./.dev.json').toString());
13 | } else {
14 | return JSON.parse(fs.readFileSync('./.dev.sample.json').toString());
15 | }
16 | };
17 |
18 | const devConfig = loadDevConfig();
19 |
20 | const config = {
21 | mode: process.env.NODE_ENV,
22 | context: __dirname + '/src',
23 | entry: {
24 | 'background': './background/index.js',
25 | 'popup/popup': './popup/popup.js',
26 | },
27 | output: {
28 | path: __dirname + '/dist',
29 | filename: '[name].js',
30 | },
31 | resolve: {
32 | extensions: ['.js', '.vue'],
33 | },
34 | module: {
35 | rules: [
36 | {
37 | test: /\.vue$/,
38 | use: 'vue-loader',
39 | },
40 | {
41 | test: /\.js$/,
42 | loader: 'babel-loader',
43 | exclude: /node_modules/,
44 | options: {
45 | presets: [
46 | {'plugins': ['@babel/plugin-proposal-class-properties']
47 | }
48 | ]
49 | }
50 | },
51 | {
52 | test: /\.css$/,
53 | use: [MiniCssExtractPlugin.loader, 'css-loader'],
54 | },
55 | {
56 | test: /\.scss$/,
57 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
58 | },
59 | {
60 | test: /\.sass$/,
61 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader?indentedSyntax'],
62 | },
63 | {
64 | test: /\.(png|jpg|jpeg|gif|svg|ico)$/,
65 | loader: 'file-loader',
66 | options: {
67 | name: '[name].[ext]',
68 | outputPath: '/images/',
69 | emitFile: false,
70 | },
71 | },
72 | {
73 | test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
74 | loader: 'file-loader',
75 | options: {
76 | name: '[name].[ext]',
77 | outputPath: '/fonts/',
78 | emitFile: false,
79 | },
80 | },
81 | ],
82 | },
83 | plugins: [
84 | new webpack.DefinePlugin({
85 | global: 'window',
86 | }),
87 | new VueLoaderPlugin(),
88 | new MiniCssExtractPlugin({
89 | filename: '[name].css',
90 | }),
91 | new CopyWebpackPlugin({
92 | patterns: [
93 | { from: 'icons', to: 'icons', globOptions: { ignoreFiles: ['icon.xcf'] } },
94 | { from: 'images', to: 'images' },
95 | { from: 'content_script', to: 'content_script' },
96 | { from: 'popup/popup.html', to: 'popup/popup.html', transform: transformHtml },
97 | {
98 | from: 'manifest.json',
99 | to: 'manifest.json',
100 | transform: (content) => {
101 | const jsonContent = JSON.parse(content);
102 | jsonContent.version = version;
103 |
104 | if (config.mode === 'development') {
105 | jsonContent.permissions = jsonContent.permissions.concat(devConfig.permissions);
106 | }
107 |
108 | if (process.env.BETA) {
109 | const geckoId = jsonContent.browser_specific_settings.gecko.id;
110 | jsonContent.name = 'SimpleLogin BETA';
111 | jsonContent.icons = {
112 | '48': 'icons/icon_beta_48.png',
113 | '128': 'icons/icon_beta_128.png'
114 | };
115 | jsonContent.version = version + '.' + betaRev;
116 | jsonContent.browser_specific_settings.gecko.id = geckoId.replace('@', '-beta@');
117 | }
118 |
119 | if (process.env.FIREFOX) {
120 | jsonContent.background = {
121 | "scripts": ["background.js"]
122 | };
123 | } else { // CHROME
124 | jsonContent.background = {
125 | "service_worker": "background.js",
126 | "type": "module"
127 | };
128 | }
129 |
130 | if (process.env.LITE) {
131 | // Remove "All sites" permissions
132 | const PERMISSIONS_TO_REMOVE = [
133 | "https://*/*",
134 | "http://*/*"
135 | ];
136 |
137 | const finalPermissions = [];
138 | for (const perm of jsonContent.permissions) {
139 | if (!PERMISSIONS_TO_REMOVE.includes(perm)) {
140 | finalPermissions.push(perm);
141 | }
142 | }
143 | jsonContent.permissions = finalPermissions;
144 |
145 | // Change metadata
146 | jsonContent.name = "SimpleLogin Without SL icon";
147 | jsonContent.short_name = "SimpleLogin Without SL icon";
148 | }
149 |
150 | if (process.env.MAC) {
151 | jsonContent.permissions.push("nativeMessaging");
152 | }
153 |
154 | return JSON.stringify(jsonContent, null, 2);
155 | },
156 | },
157 | ]
158 | }),
159 | ],
160 | };
161 |
162 | console.log(`[Build] Using config.mode = ${config.mode}`);
163 |
164 | if (config.mode === 'development') {
165 | const pluginConfig = {
166 | 'devConfig': JSON.stringify(devConfig),
167 | 'process.env.BETA': JSON.stringify(!!process.env.BETA),
168 | };
169 |
170 | console.log(`[development] Using pluginConfig: ${JSON.stringify(pluginConfig)}`);
171 | config.plugins = (config.plugins || []).concat([
172 | new webpack.DefinePlugin(pluginConfig),
173 | ]);
174 | }
175 |
176 | if (process.env.MAC){
177 | config.plugins = (config.plugins || []).concat([
178 | new webpack.DefinePlugin({
179 | 'process.env.MAC': JSON.stringify(!!process.env.MAC),
180 | }),
181 | ]);
182 | }
183 |
184 | if (config.mode === 'production') {
185 | const pluginConfig = {
186 | 'devConfig': 'null',
187 | 'process.env': {
188 | 'NODE_ENV': '"production"',
189 | 'BETA': JSON.stringify(!!process.env.BETA),
190 | }
191 | };
192 | console.log(`[production] Using pluginConfig: ${JSON.stringify(pluginConfig)}`);
193 | config.plugins = (config.plugins || []).concat([
194 | new webpack.DefinePlugin(pluginConfig),
195 | ]);
196 | }
197 |
198 | function transformHtml(content) {
199 | return ejs.render(content.toString(), {
200 | ...process.env,
201 | devConfig,
202 | });
203 | }
204 |
205 | if (config.mode === 'production') {
206 | config.devtool="source-map";
207 | } else {
208 | config.devtool="cheap-source-map";
209 | }
210 |
211 | module.exports = config;
--------------------------------------------------------------------------------