├── src ├── app │ ├── constants.ts │ ├── public │ │ ├── EmailUnsubscribeContainer.scss │ │ ├── helper.ts │ │ ├── Content.tsx │ │ ├── Footer.tsx │ │ ├── BackButton.tsx │ │ ├── Main.tsx │ │ ├── Header.tsx │ │ ├── StandardSupportDropdown.tsx │ │ ├── SupportDropdown.tsx │ │ ├── SupportDropdownButton.tsx │ │ ├── LanguageSelect.tsx │ │ ├── Layout.scss │ │ ├── Layout.tsx │ │ └── EmailUnsubscribeContainer.tsx │ ├── reset │ │ ├── NewPasswordForm.tsx │ │ ├── RequestRecoveryForm.tsx │ │ ├── RequestResetTokenForm.tsx │ │ └── ValidateResetTokenForm.tsx │ ├── app.scss │ ├── components │ │ ├── EmailUnsubscribeBorderedContainer.scss │ │ ├── EmailUnsubscribeLayout.scss │ │ ├── SettingsListItem.tsx │ │ ├── EmailUnsubscribeCategories.tsx │ │ ├── EmailUnsubscribeBorderedContainer.tsx │ │ ├── PrivateMainAreaLoading.tsx │ │ ├── EmailUnsubscribeLayout.tsx │ │ ├── EmailResubscribed.tsx │ │ ├── EmailUnsubscribed.tsx │ │ ├── EmailSubscriptionManagement.tsx │ │ └── PrivateMainSettingsAreaWithPermissions.tsx │ ├── signup │ │ ├── Loader.tsx │ │ ├── CreatingAccount.tsx │ │ ├── helpers │ │ │ ├── handleCreateExternalUser.ts │ │ │ ├── authApi.ts │ │ │ ├── handleCreateUser.ts │ │ │ └── humanApi.tsx │ │ ├── Complete.tsx │ │ ├── SignupSupportDropdown.tsx │ │ ├── CheckoutButton.tsx │ │ ├── interfaces.ts │ │ ├── constants.ts │ │ ├── InsecureEmailInfo.tsx │ │ └── VerificationCodeForm.tsx │ ├── index.tsx │ ├── content │ │ ├── AccountSidebarVersion.tsx │ │ ├── SetupMainContainer.tsx │ │ ├── PrivateApp.tsx │ │ ├── PrivateMainSettingsAreaWithPermissions.tsx │ │ ├── AccountPublicApp.tsx │ │ ├── AccountSidebar.tsx │ │ └── MainContainer.tsx │ ├── Setup.tsx │ ├── containers │ │ ├── drive │ │ │ ├── DriveSettingsRouter.tsx │ │ │ ├── DriveSettingsSidebarList.tsx │ │ │ └── DriveGeneralSettings.tsx │ │ ├── vpn │ │ │ ├── VpnSettingsRouter.tsx │ │ │ ├── VpnDownloadSettings.tsx │ │ │ ├── VpnOpenVpnIKEv2Settings.tsx │ │ │ ├── VpnSettingsSidebarList.tsx │ │ │ └── VpnUpgradeSection.tsx │ │ ├── mail │ │ │ ├── MailAutoReplySettings.tsx │ │ │ ├── MailImapSmtpSettings.tsx │ │ │ ├── MailFiltersSettings.tsx │ │ │ ├── MailIdentityAndAddressesSettings.tsx │ │ │ ├── MailAppearanceSettings.tsx │ │ │ ├── MailDomainNamesSettings.tsx │ │ │ ├── MailFoldersAndLabelsSettings.tsx │ │ │ ├── MailEncryptionKeysSettings.tsx │ │ │ ├── MailImportAndExportSettings.tsx │ │ │ ├── MailGeneralSettings.tsx │ │ │ ├── MailSettingsRouter.tsx │ │ │ └── MailSettingsSidebarList.tsx │ │ ├── contacts │ │ │ ├── ContactsSettingsRouter.tsx │ │ │ ├── ContactsGeneralSettings.tsx │ │ │ ├── ContactsImportSettings.tsx │ │ │ └── ContactsSettingsSidebarList.tsx │ │ ├── account │ │ │ ├── AccountSecuritySettings.tsx │ │ │ ├── AccountSettingsSidebarList.tsx │ │ │ ├── AccountPasswordAndRecoverySettings.tsx │ │ │ ├── AccountPaymentSettings.tsx │ │ │ └── AccountDashboardSettings.tsx │ │ ├── organization │ │ │ ├── OrganizationUsersAndAddressesSettings.tsx │ │ │ ├── OrganizationMultiUserSupportSettings.tsx │ │ │ ├── OrganizationSettingsSidebarList.tsx │ │ │ └── OrganizationKeysSettings.tsx │ │ ├── calendar │ │ │ ├── CalendarGeneralSettings.tsx │ │ │ ├── CalendarSettingsSidebarList.tsx │ │ │ ├── CalendarCalendarsSettings.tsx │ │ │ └── CalendarSettingsRouter.tsx │ │ └── SetupInternalAccountContainer.tsx │ ├── App.tsx │ └── login │ │ ├── UnlockForm.tsx │ │ ├── GenerateInternalAddressConfirmForm.tsx │ │ ├── LoginSupportDropdown.tsx │ │ ├── SetPasswordForm.tsx │ │ ├── GenerateInternalAddressForm.tsx │ │ ├── TOTPForm.tsx │ │ ├── GenerateInternalAddressStep.tsx │ │ └── LoginForm.tsx ├── assets │ ├── logoConfig.js │ └── protonaccount.svg ├── app.ejs └── .htaccess ├── tsconfig.json ├── rtl.setup.js ├── test └── test.spec.js ├── .gitlab-ci.yml ├── .prettierrc ├── .eslintrc.json ├── babel.config.js ├── jest.config.js ├── .gitignore ├── CHANGELOG.md ├── package.json └── README.md /src/app/constants.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "proton-shared/tsconfig.base.json" 3 | } 4 | -------------------------------------------------------------------------------- /rtl.setup.js: -------------------------------------------------------------------------------- 1 | import "@testing-library/react/cleanup-after-each"; 2 | import "@testing-library/jest-dom/extend-expect"; 3 | -------------------------------------------------------------------------------- /test/test.spec.js: -------------------------------------------------------------------------------- 1 | describe('test', () => { 2 | it('', () => { 3 | expect(1).toEqual(1); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /src/app/public/EmailUnsubscribeContainer.scss: -------------------------------------------------------------------------------- 1 | .email-unsubscribe-container--main { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/reset/NewPasswordForm.tsx: -------------------------------------------------------------------------------- 1 | import SetPasswordForm from '../login/SetPasswordForm'; 2 | 3 | export default SetPasswordForm; 4 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - project: 'deploy-app/fe-scripts' 3 | ref: master 4 | file: '/jobs/webapp/open-source.gitlab-ci.yaml' 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "arrowParens": "always", 4 | "singleQuote": true, 5 | "tabWidth": 4, 6 | "proseWrap": "never" 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["proton-lint"], 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "project": "./tsconfig.json" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/app/app.scss: -------------------------------------------------------------------------------- 1 | @import '~design-system/scss/proton-account'; 2 | @import 3 | '~design-system/scss/specifics/placeholder-loading', 4 | '~design-system/scss/specifics/settings'; 5 | -------------------------------------------------------------------------------- /src/app/components/EmailUnsubscribeBorderedContainer.scss: -------------------------------------------------------------------------------- 1 | @import '~design-system/scss/config/'; 2 | 3 | .email-unsubscribe-container { 4 | width: rem(480); 5 | max-width: 80%; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/signup/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FullLoader } from 'react-components'; 3 | 4 | const Loader = () => { 5 | return ; 6 | }; 7 | 8 | export default Loader; 9 | -------------------------------------------------------------------------------- /src/app/components/EmailUnsubscribeLayout.scss: -------------------------------------------------------------------------------- 1 | @import '~design-system/scss/config/'; 2 | 3 | .email-unsubscribe-layout--logo { 4 | width: rem(90); 5 | } 6 | 7 | .email-unsubscribe-layout--main { 8 | width: rem(320); 9 | max-width: 80%; 10 | } 11 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | "@babel/preset-env", 4 | "@babel/preset-react", 5 | "@babel/preset-typescript" 6 | ], 7 | plugins: [ 8 | "@babel/plugin-proposal-object-rest-spread", 9 | "@babel/plugin-transform-runtime" 10 | ] 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | import React from 'react'; 3 | import 'core-js/stable'; 4 | import 'regenerator-runtime/runtime'; 5 | import 'yetch/polyfill'; 6 | 7 | import App from './App'; 8 | 9 | ReactDOM.render(, document.querySelector('.app-root')); 10 | -------------------------------------------------------------------------------- /src/app/public/helper.ts: -------------------------------------------------------------------------------- 1 | import { APPS, APPS_CONFIGURATION, APP_NAMES } from 'proton-shared/lib/constants'; 2 | 3 | export const getToAppName = (toApp?: APP_NAMES) => { 4 | if (!toApp || toApp === APPS.PROTONACCOUNT) { 5 | return ''; 6 | } 7 | return APPS_CONFIGURATION[toApp]?.name || ''; 8 | }; 9 | -------------------------------------------------------------------------------- /src/app/content/AccountSidebarVersion.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AppVersion } from 'react-components'; 3 | 4 | import changelog from '../../../CHANGELOG.md'; 5 | 6 | const SidebarVersion = () => { 7 | return ; 8 | }; 9 | 10 | export default SidebarVersion; 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFilesAfterEnv: ["./rtl.setup.js"], 3 | verbose: true, 4 | moduleDirectories: ["node_modules"], 5 | transform: { 6 | "^.+\\.(js|tsx?)$": "babel-jest" 7 | }, 8 | collectCoverage: true, 9 | collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}"], 10 | transformIgnorePatterns: ["node_modules/(?!(proton-shared)/)"] 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/public/Content.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface Props extends React.HTMLProps { 4 | children: React.ReactNode; 5 | } 6 | 7 | const Content = ({ children, className, ...rest }: Props) => { 8 | return ( 9 |
10 | {children} 11 |
12 | ); 13 | }; 14 | 15 | export default Content; 16 | -------------------------------------------------------------------------------- /src/app/public/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { classnames } from 'react-components'; 3 | 4 | interface Props extends React.HTMLProps { 5 | children: React.ReactNode; 6 | } 7 | 8 | const Footer = ({ children, className, ...rest }: Props) => { 9 | return ( 10 |
11 | {children} 12 |
13 | ); 14 | }; 15 | 16 | export default Footer; 17 | -------------------------------------------------------------------------------- /src/assets/logoConfig.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | logo: 'src/assets/protonaccount.svg', 3 | favicons: { 4 | appName: 'ProtonAccount', 5 | appDescription: 6 | "ProtonMail is the world's largest secure email service, developed by CERN and MIT scientists. We are open source and protected by Swiss privacy law", 7 | developerName: 'Proton Technologies AG', 8 | developerURL: 'https://github.com/ProtonMail/proton-account', 9 | background: '#1c223d', 10 | theme_color: '#1c223d', 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/app/public/BackButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | import { Button, Icon } from 'react-components'; 4 | 5 | interface Props { 6 | onClick: (event: React.MouseEvent) => void; 7 | } 8 | 9 | const BackButton = ({ onClick }: Props) => { 10 | return ( 11 | 14 | ); 15 | }; 16 | 17 | export default BackButton; 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | dist 26 | .eslintcache 27 | .idea 28 | 29 | env.json 30 | .env 31 | src/config.js 32 | src/app/config.js 33 | src/app/config.ts 34 | env.json 35 | po/i18n.txt 36 | package-lock.json 37 | appConfig.json 38 | yarn.lock 39 | po/template.pot 40 | -------------------------------------------------------------------------------- /src/app/content/SetupMainContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Switch } from 'react-router-dom'; 3 | 4 | import MainContainer from './MainContainer'; 5 | import SetupInternalAccountContainer from '../containers/SetupInternalAccountContainer'; 6 | 7 | const SetupMainContainer = () => { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default SetupMainContainer; 21 | -------------------------------------------------------------------------------- /src/app/Setup.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import locales from 'proton-shared/lib/i18n/locales'; 3 | 4 | import { PublicAuthenticationStore, PrivateAuthenticationStore, useAuthentication } from 'react-components'; 5 | 6 | import PrivateApp from './content/PrivateApp'; 7 | import PublicApp from './content/PublicApp'; 8 | 9 | const Setup = () => { 10 | const { UID, login, logout } = useAuthentication() as PublicAuthenticationStore & PrivateAuthenticationStore; 11 | if (UID) { 12 | return ; 13 | } 14 | return ; 15 | }; 16 | 17 | export default Setup; 18 | -------------------------------------------------------------------------------- /src/assets/protonaccount.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/app/containers/drive/DriveSettingsRouter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect, Switch, useRouteMatch, useLocation } from 'react-router-dom'; 3 | 4 | import DriveGeneralSettings from './DriveGeneralSettings'; 5 | 6 | const DriveSettingsRouter = () => { 7 | const { path } = useRouteMatch(); 8 | const location = useLocation(); 9 | 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default DriveSettingsRouter; 21 | -------------------------------------------------------------------------------- /src/app/public/Main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { classnames } from 'react-components'; 3 | 4 | interface Props extends React.HTMLProps { 5 | larger?: boolean; 6 | } 7 | 8 | const Main = ({ children, className, larger, ...rest }: Props) => { 9 | return ( 10 |
18 | {children} 19 |
20 | ); 21 | }; 22 | 23 | export default Main; 24 | -------------------------------------------------------------------------------- /src/app/components/SettingsListItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | SidebarListItem, 5 | SidebarListItemContent, 6 | SidebarListItemContentIcon, 7 | SidebarListItemLink, 8 | } from 'react-components'; 9 | 10 | const SettingsListItem = ({ to, icon, children }: { to: string; icon: string; children: React.ReactNode }) => ( 11 | 12 | 13 | }> 14 | {children} 15 | 16 | 17 | 18 | ); 19 | 20 | export default SettingsListItem; 21 | -------------------------------------------------------------------------------- /src/app/components/EmailUnsubscribeCategories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | 4 | const EmailUnsubscribeCategories = ({ categories }: { categories: string[] }) => { 5 | const allCategoriesExceptTheLastOne = categories.slice(0, -1).join(', '); 6 | 7 | const lastCategory = categories[categories.length - 1]; 8 | 9 | const categoriesString = 10 | categories.length > 1 11 | ? c('Email Unsubscribe Categories').t`${allCategoriesExceptTheLastOne} and ${lastCategory}` 12 | : lastCategory; 13 | 14 | return ( 15 | 16 | {categoriesString} 17 | 18 | ); 19 | }; 20 | 21 | export default EmailUnsubscribeCategories; 22 | -------------------------------------------------------------------------------- /src/app/signup/CreatingAccount.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | 4 | import { SignupModel } from './interfaces'; 5 | import Loader from './Loader'; 6 | 7 | interface Props { 8 | model: SignupModel; 9 | } 10 | 11 | const CreatingAccount = ({ model }: Props) => { 12 | const [domain = ''] = model.domains; 13 | const email = model.email ? model.email : `${model.username}@${domain}`; 14 | return ( 15 |
16 | 17 |

{c('Info').t`Creating your Proton account`}

18 |

{email}

19 |

{c('Info').t`Please wait...`}

20 |
21 | ); 22 | }; 23 | 24 | export default CreatingAccount; 25 | -------------------------------------------------------------------------------- /src/app/components/EmailUnsubscribeBorderedContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { classnames } from 'react-components'; 3 | import './EmailUnsubscribeBorderedContainer.scss'; 4 | 5 | interface EmailUnsubscribeBorderedContainerProps { 6 | children: React.ReactNode; 7 | className?: string; 8 | } 9 | 10 | const EmailUnsubscribeBorderedContainer = ({ 11 | children, 12 | className: classNameProp, 13 | }: EmailUnsubscribeBorderedContainerProps) => { 14 | const className = classnames([ 15 | 'flex flex-column flex-align-items-center mt4 bordered p2 email-unsubscribe-container', 16 | classNameProp, 17 | ]); 18 | 19 | return
{children}
; 20 | }; 21 | 22 | export default EmailUnsubscribeBorderedContainer; 23 | -------------------------------------------------------------------------------- /src/app.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/app/public/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface Props extends Omit, 'title'> { 4 | left?: React.ReactNode; 5 | title?: React.ReactNode; 6 | subTitle?: React.ReactNode; 7 | } 8 | 9 | const Header = ({ left, title, subTitle, ...rest }: Props) => { 10 | return ( 11 |
12 | {left ? {left} : null} 13 | {title ? ( 14 |

15 | {title} 16 |

17 | ) : null} 18 | {subTitle ?
{subTitle}
: null} 19 |
20 | ); 21 | }; 22 | 23 | export default Header; 24 | -------------------------------------------------------------------------------- /src/app/containers/vpn/VpnSettingsRouter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect, Switch, useRouteMatch, useLocation } from 'react-router-dom'; 3 | 4 | import VpnDownloadSettings from './VpnDownloadSettings'; 5 | import VpnOpenVpnIKEv2Settings from './VpnOpenVpnIKEv2Settings'; 6 | 7 | const VpnSettingsRouter = () => { 8 | const { path } = useRouteMatch(); 9 | const location = useLocation(); 10 | 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default VpnSettingsRouter; 25 | -------------------------------------------------------------------------------- /src/app/containers/mail/MailAutoReplySettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | 4 | import { AutoReplySection, SettingsPropsShared } from 'react-components'; 5 | 6 | import PrivateMainSettingsAreaWithPermissions from '../../components/PrivateMainSettingsAreaWithPermissions'; 7 | 8 | export const getAutoReply = () => { 9 | return { 10 | text: c('Title').t`Auto reply`, 11 | to: '/mail/auto-reply', 12 | icon: 'mailbox', 13 | subsections: [{ id: 'auto-reply' }], 14 | }; 15 | }; 16 | 17 | const MailAutoReplySettings = ({ location }: SettingsPropsShared) => { 18 | return ( 19 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default MailAutoReplySettings; 26 | -------------------------------------------------------------------------------- /src/app/containers/contacts/ContactsSettingsRouter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect, Switch, useRouteMatch, useLocation } from 'react-router-dom'; 3 | 4 | import ContactsGeneralSettings from './ContactsGeneralSettings'; 5 | import ContactsImportSettings from './ContactsImportSettings'; 6 | 7 | const ContactSettingsRouter = () => { 8 | const { path } = useRouteMatch(); 9 | const location = useLocation(); 10 | 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default ContactSettingsRouter; 25 | -------------------------------------------------------------------------------- /src/app/signup/helpers/handleCreateExternalUser.ts: -------------------------------------------------------------------------------- 1 | import { Api } from 'proton-shared/lib/interfaces'; 2 | import { srpVerify } from 'proton-shared/lib/srp'; 3 | import { queryCreateUserExternal } from 'proton-shared/lib/api/user'; 4 | 5 | interface CreateUserArgs { 6 | api: Api; 7 | password: string; 8 | email: string; 9 | clientType: 1 | 2; 10 | payload?: { [key: string]: string }; 11 | } 12 | 13 | export const handleCreateExternalUser = async ({ api, password, email, clientType, payload }: CreateUserArgs) => { 14 | if (!email) { 15 | throw new Error('Missing email'); 16 | } 17 | return srpVerify({ 18 | api, 19 | credentials: { password }, 20 | config: queryCreateUserExternal({ 21 | Type: clientType, 22 | Email: email, 23 | Payload: payload, 24 | }), 25 | }); 26 | }; 27 | 28 | export default handleCreateExternalUser; 29 | -------------------------------------------------------------------------------- /src/app/containers/mail/MailImapSmtpSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | 4 | import { ProtonMailBridgeSection, SettingsPropsShared } from 'react-components'; 5 | 6 | import PrivateMainSettingsAreaWithPermissions from '../../components/PrivateMainSettingsAreaWithPermissions'; 7 | 8 | export const getBridgePage = () => { 9 | return { 10 | text: c('Title').t`IMAP/SMTP`, 11 | to: '/mail/imap-smtp', 12 | icon: 'imap-smtp', 13 | subsections: [ 14 | { 15 | text: c('Title').t`ProtonMail Bridge`, 16 | id: 'protonmail-bridge', 17 | }, 18 | ], 19 | }; 20 | }; 21 | 22 | const MailImapSmtpSettings = ({ location }: SettingsPropsShared) => { 23 | return ( 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default MailImapSmtpSettings; 31 | -------------------------------------------------------------------------------- /src/app/containers/drive/DriveSettingsSidebarList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | import { useRouteMatch } from 'react-router-dom'; 4 | 5 | import { SidebarList, SidebarListItem, SidebarListItemContent } from 'react-components'; 6 | import { APPS, APPS_CONFIGURATION } from 'proton-shared/lib/constants'; 7 | 8 | import SettingsListItem from '../../components/SettingsListItem'; 9 | 10 | const DriveSettingsSidebarList = () => { 11 | const { path } = useRouteMatch(); 12 | 13 | return ( 14 | 15 | 16 | {APPS_CONFIGURATION[APPS.PROTONDRIVE].name} 17 | 18 | 19 | {c('Settings section title').t`General`} 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default DriveSettingsSidebarList; 26 | -------------------------------------------------------------------------------- /src/app/signup/helpers/authApi.ts: -------------------------------------------------------------------------------- 1 | import { srpAuth } from 'proton-shared/lib/srp'; 2 | import { AuthResponse } from 'proton-shared/lib/authentication/interface'; 3 | import { auth } from 'proton-shared/lib/api/auth'; 4 | import { withAuthHeaders } from 'proton-shared/lib/fetch/headers'; 5 | import { Api } from 'proton-shared/lib/interfaces'; 6 | 7 | interface Args { 8 | api: Api; 9 | username: string; 10 | password: string; 11 | } 12 | const createAuthApi = async ({ api, username, password }: Args) => { 13 | const authResponse = await srpAuth({ 14 | api, 15 | credentials: { 16 | username, 17 | password, 18 | }, 19 | config: auth({ Username: username }), 20 | }); 21 | 22 | const { UID, AccessToken } = authResponse; 23 | 24 | const authApiCaller = (config: any) => api(withAuthHeaders(UID, AccessToken, config)); 25 | 26 | return { 27 | api: authApiCaller, 28 | getAuthResponse: () => authResponse, 29 | }; 30 | }; 31 | 32 | export default createAuthApi; 33 | -------------------------------------------------------------------------------- /src/app/containers/mail/MailFiltersSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | 4 | import { FiltersSection, SpamFiltersSection, SettingsPropsShared } from 'react-components'; 5 | 6 | import PrivateMainSettingsAreaWithPermissions from '../../components/PrivateMainSettingsAreaWithPermissions'; 7 | 8 | export const getFiltersPage = () => { 9 | return { 10 | text: c('Title').t`Filters`, 11 | to: '/mail/filters', 12 | icon: 'filter', 13 | subsections: [ 14 | { 15 | text: c('Title').t`Custom filters`, 16 | id: 'custom', 17 | }, 18 | { 19 | text: c('Title').t`Spam filters`, 20 | id: 'spam', 21 | }, 22 | ], 23 | }; 24 | }; 25 | 26 | const MailFiltersSettings = ({ location }: SettingsPropsShared) => { 27 | return ( 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default MailFiltersSettings; 36 | -------------------------------------------------------------------------------- /src/app/containers/drive/DriveGeneralSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | import { getSlugFromApp } from 'proton-shared/lib/apps/slugHelper'; 4 | import { APPS } from 'proton-shared/lib/constants'; 5 | 6 | import { SettingsPropsShared, ThemesSection } from 'react-components'; 7 | 8 | import PrivateMainSettingsAreaWithPermissions from '../../components/PrivateMainSettingsAreaWithPermissions'; 9 | 10 | const driveSlug = getSlugFromApp(APPS.PROTONDRIVE); 11 | 12 | export const getDriveGeneralPage = () => { 13 | return { 14 | to: `/${driveSlug}/general`, 15 | icon: 'drive', 16 | text: c('Title').t`General`, 17 | subsections: [ 18 | { 19 | text: c('Title').t`Theme`, 20 | id: 'theme', 21 | }, 22 | ], 23 | }; 24 | }; 25 | 26 | const DriveGeneralSettings = ({ location }: SettingsPropsShared) => { 27 | return ( 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | export default DriveGeneralSettings; 35 | -------------------------------------------------------------------------------- /src/app/public/StandardSupportDropdown.tsx: -------------------------------------------------------------------------------- 1 | import { c } from 'ttag'; 2 | import React from 'react'; 3 | import { BugModal, DropdownMenuButton, DropdownMenuLink, Icon, useModals } from 'react-components'; 4 | import SupportDropdown from './SupportDropdown'; 5 | 6 | const StandardSupportDropdown = () => { 7 | const { createModal } = useModals(); 8 | 9 | const handleBugReportClick = () => { 10 | createModal(); 11 | }; 12 | 13 | return ( 14 | 15 | 20 | 21 | {c('Action').t`I have a question`} 22 | 23 | 24 | 25 | {c('Action').t`Report a problem`} 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default StandardSupportDropdown; 32 | -------------------------------------------------------------------------------- /src/app/containers/contacts/ContactsGeneralSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | 4 | import { SettingsPropsShared, ContactsSettingsContactsSection, ThemesSection } from 'react-components'; 5 | 6 | import PrivateMainSettingsAreaWithPermissions from '../../components/PrivateMainSettingsAreaWithPermissions'; 7 | 8 | export const getContactsGeneralPage = () => { 9 | return { 10 | to: '/contacts/general', 11 | icon: 'contacts-groups', 12 | text: c('Title').t`General`, 13 | subsections: [ 14 | { 15 | text: c('Title').t`Theme`, 16 | id: 'theme', 17 | }, 18 | { 19 | text: c('Title').t`Contacts`, 20 | id: 'contacts', 21 | }, 22 | ], 23 | }; 24 | }; 25 | 26 | const ContactsGeneralSettings = ({ location }: SettingsPropsShared) => { 27 | return ( 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default ContactsGeneralSettings; 36 | -------------------------------------------------------------------------------- /src/app/signup/Complete.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | 4 | import { Href } from 'react-components'; 5 | import { SignupModel } from './interfaces'; 6 | 7 | interface Props { 8 | model: SignupModel; 9 | } 10 | 11 | const Complete = ({ model }: Props) => { 12 | const [domain = ''] = model.domains; 13 | const email = model.email ? model.email : `${model.username}@${domain}`; 14 | return ( 15 |
16 |

{c('Signup title').t`Congratulations, your Proton Account ${email} has been created!`}

17 |

{c('Info') 18 | .t`Keep your password safe and secure, as it is the key to unlocking all your emails, documents and any other private data on Proton.`}

19 |

{c('Info') 20 | .t`Make sure you do not forget or lose your password. It is the key to unlocking all your emails, documents, and other private data on Proton. If you need to reset your password, you will lose access to this data.`}

21 | {c('Link') 22 | .t`Finish`} 23 |
24 | ); 25 | }; 26 | 27 | export default Complete; 28 | -------------------------------------------------------------------------------- /src/app/signup/SignupSupportDropdown.tsx: -------------------------------------------------------------------------------- 1 | import { c } from 'ttag'; 2 | import React from 'react'; 3 | import { BugModal, DropdownMenuButton, DropdownMenuLink, Icon, useModals } from 'react-components'; 4 | import SupportDropdown from '../public/SupportDropdown'; 5 | 6 | const SignupSupportDropdown = () => { 7 | const { createModal } = useModals(); 8 | 9 | const handleBugReportClick = () => { 10 | createModal(); 11 | }; 12 | 13 | return ( 14 | 15 | 20 | 21 | {c('Link').t`Common sign up issues`} 22 | 23 | 24 | 25 | {c('Action').t`Report a problem`} 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default SignupSupportDropdown; 32 | -------------------------------------------------------------------------------- /src/app/containers/contacts/ContactsImportSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | 4 | import { SettingsPropsShared, ContactsSettingsExportSection, ContactsSettingsImportSection } from 'react-components'; 5 | 6 | import PrivateMainSettingsAreaWithPermissions from '../../components/PrivateMainSettingsAreaWithPermissions'; 7 | 8 | export const getImportExportPage = () => { 9 | return { 10 | to: '/contacts/import-export', 11 | icon: 'import', 12 | text: c('Title').t`Import & export`, 13 | subsections: [ 14 | { 15 | text: c('Title').t`Import`, 16 | id: 'import', 17 | }, 18 | { 19 | text: c('Title').t`Export`, 20 | id: 'export', 21 | }, 22 | ], 23 | }; 24 | }; 25 | 26 | const ContactsImportSettings = ({ location }: SettingsPropsShared) => { 27 | return ( 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default ContactsImportSettings; 36 | -------------------------------------------------------------------------------- /src/app/components/PrivateMainAreaLoading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | PrivateMainArea, 4 | SettingsPageTitle, 5 | SettingsParagraph, 6 | SettingsSection, 7 | SettingsSectionTitle, 8 | } from 'react-components'; 9 | 10 | const PrivateMainAreaLoading = () => { 11 | return ( 12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 | ); 28 | }; 29 | export default PrivateMainAreaLoading; 30 | -------------------------------------------------------------------------------- /src/app/containers/mail/MailIdentityAndAddressesSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | import { AddressesSection, IdentitySection, SettingsPropsShared } from 'react-components'; 4 | 5 | import PrivateMainSettingsAreaWithPermissions from '../../components/PrivateMainSettingsAreaWithPermissions'; 6 | 7 | export const getAddressesPage = () => { 8 | return { 9 | text: c('Title').t`Identity & addresses`, 10 | to: '/mail/identity-addresses', 11 | icon: 'addresses', 12 | subsections: [ 13 | { 14 | text: c('Title').t`Display name & signature`, 15 | id: 'name-signature', 16 | }, 17 | { 18 | text: c('Title').t`My addresses`, 19 | id: 'addresses', 20 | }, 21 | ], 22 | }; 23 | }; 24 | 25 | const MailIdentityAndAddressSettings = ({ location }: SettingsPropsShared) => { 26 | return ( 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | export default MailIdentityAndAddressSettings; 35 | -------------------------------------------------------------------------------- /src/app/containers/contacts/ContactsSettingsSidebarList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | import { useRouteMatch } from 'react-router-dom'; 4 | 5 | import { SidebarList, SidebarListItem, SidebarListItemContent } from 'react-components'; 6 | import { APPS, APPS_CONFIGURATION } from 'proton-shared/lib/constants'; 7 | 8 | import SettingsListItem from '../../components/SettingsListItem'; 9 | 10 | const { PROTONCONTACTS } = APPS; 11 | 12 | const ContactsSettingsSidebarList = () => { 13 | const { path } = useRouteMatch(); 14 | 15 | return ( 16 | 17 | 18 | {APPS_CONFIGURATION[PROTONCONTACTS].name} 19 | 20 | 21 | {c('Settings section title').t`General`} 22 | 23 | 24 | {c('Settings section title').t`Import & export`} 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default ContactsSettingsSidebarList; 31 | -------------------------------------------------------------------------------- /src/app/public/SupportDropdown.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { generateUID, Dropdown, usePopperAnchor, DropdownMenu } from 'react-components'; 3 | import { c } from 'ttag'; 4 | 5 | import SupportDropdownButton from './SupportDropdownButton'; 6 | 7 | interface Props { 8 | children?: React.ReactNode; 9 | content?: React.ReactNode; 10 | } 11 | 12 | const SupportDropdown = ({ content = c('Action').t`Need help?`, children }: Props) => { 13 | const [uid] = useState(generateUID('dropdown')); 14 | const { anchorRef, isOpen, toggle, close } = usePopperAnchor(); 15 | 16 | return ( 17 | <> 18 | 26 | {content} 27 | 28 | 29 | {children} 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default SupportDropdown; 36 | -------------------------------------------------------------------------------- /src/app/components/EmailUnsubscribeLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | import protonLogoSvg from 'design-system/assets/img/shared/proton-logo.svg'; 4 | import EmailUnsubscribeBorderedContainer from './EmailUnsubscribeBorderedContainer'; 5 | import './EmailUnsubscribeLayout.scss'; 6 | 7 | interface EmailUnsubscribeLayoutProps { 8 | main: React.ReactNode; 9 | footer: React.ReactNode; 10 | below?: React.ReactNode; 11 | } 12 | 13 | const EmailUnsubscribeLayout = ({ main, footer, below }: EmailUnsubscribeLayoutProps) => { 14 | return ( 15 |
16 | 17 | {c('Title').t`Proton 22 | 23 |
{main}
24 | 25 |
{footer}
26 |
27 | 28 | {below &&
{below}
} 29 |
30 | ); 31 | }; 32 | 33 | export default EmailUnsubscribeLayout; 34 | -------------------------------------------------------------------------------- /src/app/containers/account/AccountSecuritySettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SessionsSection, LogsSection, SettingsPropsShared } from 'react-components'; 3 | import { c } from 'ttag'; 4 | 5 | import PrivateMainSettingsAreaWithPermissions from '../../components/PrivateMainSettingsAreaWithPermissions'; 6 | 7 | export const getSecurityPage = () => { 8 | return { 9 | text: c('Title').t`Security`, 10 | to: '/security', 11 | icon: 'security', 12 | subsections: [ 13 | { 14 | text: c('Title').t`Session management`, 15 | id: 'sessions', 16 | }, 17 | { 18 | text: c('Title').t`Security logs`, 19 | id: 'logs', 20 | }, 21 | ], 22 | }; 23 | }; 24 | 25 | const AccountSecuritySettings = ({ location, setActiveSection }: SettingsPropsShared) => { 26 | return ( 27 | 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default AccountSecuritySettings; 39 | -------------------------------------------------------------------------------- /src/app/components/EmailResubscribed.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from 'react-components'; 3 | import { c } from 'ttag'; 4 | import EmailUnsubscribeCategories from './EmailUnsubscribeCategories'; 5 | import EmailUnsubscribeLayout from './EmailUnsubscribeLayout'; 6 | 7 | interface EmailResubscribedProps { 8 | categories: string[]; 9 | onUnsubscribeClick: () => void; 10 | onManageClick: () => void; 11 | loading: boolean; 12 | } 13 | 14 | const EmailResubscribed = ({ categories, onUnsubscribeClick, onManageClick, loading }: EmailResubscribedProps) => { 15 | const categoriesJsx = ; 16 | 17 | return ( 18 | 22 | {c('Action').t`Unsubscribe`} 23 | 24 | } 25 | below={ 26 | 29 | } 30 | /> 31 | ); 32 | }; 33 | 34 | export default EmailResubscribed; 35 | -------------------------------------------------------------------------------- /src/app/containers/vpn/VpnDownloadSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | import { ProtonVPNClientsSection, SettingsPropsShared } from 'react-components'; 4 | 5 | import PrivateMainSettingsAreaWithPermissions from '../../content/PrivateMainSettingsAreaWithPermissions'; 6 | import VpnUpgradeSection from './VpnUpgradeSection'; 7 | 8 | export const getDownloadsPage = () => { 9 | return { 10 | text: c('Title').t`VPN apps`, 11 | to: '/vpn/vpn-apps', 12 | icon: 'download', 13 | subsections: [ 14 | { 15 | text: '', 16 | id: '', 17 | }, 18 | { 19 | text: c('Title').t`ProtonVPN`, 20 | id: 'protonvpn-clients', 21 | }, 22 | ], 23 | }; 24 | }; 25 | 26 | const VpnDownloadSettings = ({ setActiveSection, location }: SettingsPropsShared) => { 27 | return ( 28 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default VpnDownloadSettings; 40 | -------------------------------------------------------------------------------- /src/app/components/EmailUnsubscribed.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from 'react-components'; 3 | import { c } from 'ttag'; 4 | import EmailUnsubscribeCategories from './EmailUnsubscribeCategories'; 5 | import EmailUnsubscribeLayout from './EmailUnsubscribeLayout'; 6 | 7 | interface EmailUnsubscribedProps { 8 | categories: string[]; 9 | onResubscribeClick: () => void; 10 | onManageClick: () => void; 11 | loading: boolean; 12 | } 13 | 14 | const EmailUnsubscribed = ({ categories, onResubscribeClick, onManageClick, loading }: EmailUnsubscribedProps) => { 15 | const categoriesJsx = ; 16 | 17 | return ( 18 | 22 | {c('Action').t`Resubscribe`} 23 | 24 | } 25 | below={ 26 | 29 | } 30 | /> 31 | ); 32 | }; 33 | 34 | export default EmailUnsubscribed; 35 | -------------------------------------------------------------------------------- /src/app/components/EmailSubscriptionManagement.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { EmailSubscriptionCheckboxes } from 'react-components'; 3 | import { c } from 'ttag'; 4 | import protonLogoSvg from 'design-system/assets/img/shared/proton-logo.svg'; 5 | 6 | import EmailUnsubscribeBorderedContainer from './EmailUnsubscribeBorderedContainer'; 7 | import './EmailUnsubscribeLayout.scss'; 8 | 9 | interface EmailSubscriptionManagementProps { 10 | News: number; 11 | disabled: boolean; 12 | onChange: (News: number) => void; 13 | } 14 | 15 | const EmailSubscriptionManagement = ({ News, disabled, onChange }: EmailSubscriptionManagementProps) => { 16 | return ( 17 | 18 | {c('Title').t`Proton 23 | 24 | {c('Email Unsubscribe').jt`Which emails do you want to receive from Proton?`} 25 | 26 |
27 | 28 |
29 |
30 | ); 31 | }; 32 | 33 | export default EmailSubscriptionManagement; 34 | -------------------------------------------------------------------------------- /src/app/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Route, Switch } from 'react-router-dom'; 3 | 4 | import sentry from 'proton-shared/lib/helpers/sentry'; 5 | import { LoaderPage, ProtonApp, ErrorBoundary, StandardErrorPage } from 'react-components'; 6 | import { G_OAUTH_REDIRECT_PATH } from 'react-components/containers/importAssistant/constants'; 7 | 8 | import * as config from './config'; 9 | import Setup from './Setup'; 10 | 11 | import './app.scss'; 12 | 13 | const enhancedConfig = { 14 | APP_VERSION_DISPLAY: '4.0.1', 15 | ...config, 16 | }; 17 | 18 | sentry(enhancedConfig); 19 | 20 | const App = () => { 21 | const [hasInitialAuth] = useState(() => { 22 | return !window.location.pathname.startsWith(G_OAUTH_REDIRECT_PATH); 23 | }); 24 | 25 | return ( 26 | 27 | }> 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | }; 40 | 41 | export default App; 42 | -------------------------------------------------------------------------------- /src/app/containers/mail/MailAppearanceSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LayoutsSection, ThemesSection, AppearanceOtherSection, SettingsPropsShared } from 'react-components'; 3 | import { c } from 'ttag'; 4 | 5 | import PrivateMainSettingsAreaWithPermissions from '../../components/PrivateMainSettingsAreaWithPermissions'; 6 | 7 | export const getAppearancePage = () => { 8 | return { 9 | text: c('Title').t`Appearance`, 10 | to: '/mail/appearance', 11 | icon: 'apparence', 12 | subsections: [ 13 | { 14 | text: c('Title').t`Theme`, 15 | id: 'theme', 16 | }, 17 | { 18 | text: c('Title').t`Layout`, 19 | id: 'layout', 20 | }, 21 | { 22 | text: c('Title').t`Other`, 23 | id: 'other', 24 | }, 25 | ], 26 | }; 27 | }; 28 | 29 | const MailAppearanceSettings = ({ location }: SettingsPropsShared) => { 30 | return ( 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default MailAppearanceSettings; 40 | -------------------------------------------------------------------------------- /src/app/public/SupportDropdownButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { Ref } from 'react'; 2 | import { c } from 'ttag'; 3 | 4 | import { Icon, DropdownCaret, classnames } from 'react-components'; 5 | 6 | interface Props extends React.DetailedHTMLProps, HTMLButtonElement> { 7 | children?: React.ReactNode; 8 | className?: string; 9 | isOpen?: boolean; 10 | noCaret?: boolean; 11 | buttonRef?: Ref; 12 | } 13 | 14 | const defaultChildren = ( 15 | <> 16 | 17 | {c('Action').t`Support`} 18 | 19 | ); 20 | 21 | const SupportDropdownButton = ({ 22 | children = defaultChildren, 23 | className, 24 | isOpen, 25 | noCaret = false, 26 | buttonRef, 27 | ...rest 28 | }: Props) => { 29 | return ( 30 | 40 | ); 41 | }; 42 | 43 | export default SupportDropdownButton; 44 | -------------------------------------------------------------------------------- /src/app/containers/mail/MailDomainNamesSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CatchAllSection, DomainsSection, SettingsPropsShared } from 'react-components'; 3 | import { c } from 'ttag'; 4 | import { PERMISSIONS } from 'proton-shared/lib/constants'; 5 | 6 | import PrivateMainSettingsAreaWithPermissions from '../../components/PrivateMainSettingsAreaWithPermissions'; 7 | 8 | const { ADMIN, NOT_SUB_USER } = PERMISSIONS; 9 | 10 | export const getOrganizationPage = () => { 11 | return { 12 | text: c('Title').t`Domain names`, 13 | to: '/domain-names', 14 | icon: 'globe', 15 | permissions: [ADMIN, NOT_SUB_USER], 16 | subsections: [ 17 | { id: 'domains' }, 18 | { 19 | text: c('Title').t`Catch-all address`, 20 | id: 'catch-all', 21 | }, 22 | ], 23 | }; 24 | }; 25 | 26 | const MailDomainNamesSettings = ({ location }: SettingsPropsShared) => { 27 | return ( 28 | {}} 32 | > 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default MailDomainNamesSettings; 40 | -------------------------------------------------------------------------------- /src/app/containers/vpn/VpnOpenVpnIKEv2Settings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | import { SettingsPropsShared, OpenVPNCredentialsSection, OpenVPNConfigurationSection } from 'react-components'; 4 | 5 | import PrivateMainSettingsAreaWithPermissions from '../../content/PrivateMainSettingsAreaWithPermissions'; 6 | 7 | export const getOpenVpnIKEv2Page = () => { 8 | return { 9 | text: c('Title').t`OpenVPN / IKEv2`, 10 | to: 'vpn/OpenVpnIKEv2', 11 | icon: 'keys', 12 | subsections: [ 13 | { 14 | text: c('Title').t`Credentials`, 15 | id: 'openvpn', 16 | }, 17 | { 18 | text: c('Title').t`OpenVPN configuration files`, 19 | id: 'openvpn-configuration-files', 20 | }, 21 | ], 22 | }; 23 | }; 24 | 25 | const VpnOpenVpnIKEv2Settings = ({ setActiveSection, location }: SettingsPropsShared) => { 26 | return ( 27 | 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default VpnOpenVpnIKEv2Settings; 39 | -------------------------------------------------------------------------------- /src/app/containers/mail/MailFoldersAndLabelsSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | 4 | import { LabelsSection, FoldersSection, SettingsPropsShared } from 'react-components'; 5 | 6 | import PrivateMainSettingsAreaWithPermissions from '../../components/PrivateMainSettingsAreaWithPermissions'; 7 | 8 | export const getLabelsPage = () => { 9 | return { 10 | text: c('Title').t`Folders & labels`, 11 | to: '/mail/folders-labels', 12 | icon: 'folder-label', 13 | description: c('Settings description') 14 | .t`You can apply multiple labels to a single message, but messages can usually only be in a single folder. Drag and drop to rearrange the order of your folders and labels.`, 15 | subsections: [ 16 | { 17 | text: c('Title').t`Folders`, 18 | id: 'folderlist', 19 | }, 20 | { 21 | text: c('Title').t`Labels`, 22 | id: 'labellist', 23 | }, 24 | ], 25 | }; 26 | }; 27 | 28 | const MailFoldersAndLabelsSettings = ({ location }: SettingsPropsShared) => { 29 | return ( 30 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default MailFoldersAndLabelsSettings; 38 | -------------------------------------------------------------------------------- /src/app/containers/organization/OrganizationUsersAndAddressesSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SettingsPropsShared, UsersAndAddressesSection } from 'react-components'; 3 | import { c } from 'ttag'; 4 | import { PERMISSIONS } from 'proton-shared/lib/constants'; 5 | 6 | import PrivateMainSettingsAreaWithPermissions from '../../components/PrivateMainSettingsAreaWithPermissions'; 7 | 8 | const { ADMIN, MULTI_USERS, NOT_SUB_USER } = PERMISSIONS; 9 | 10 | export const getOrganizationPage = () => { 11 | return { 12 | text: c('Title').t`Users and addresses`, 13 | to: '/organization', 14 | icon: 'organization', 15 | permissions: [ADMIN, NOT_SUB_USER], 16 | subsections: [ 17 | { 18 | id: 'members', 19 | permissions: [MULTI_USERS], 20 | }, 21 | { 22 | text: c('Title').t`Addresses`, 23 | id: 'addresses', 24 | }, 25 | ], 26 | }; 27 | }; 28 | 29 | const OrganizationUsersAndAddressesSettings = ({ location }: SettingsPropsShared) => { 30 | return ( 31 | {}} 35 | > 36 | 37 | 38 | ); 39 | }; 40 | 41 | export default OrganizationUsersAndAddressesSettings; 42 | -------------------------------------------------------------------------------- /src/app/containers/calendar/CalendarGeneralSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | 4 | import { CalendarUserSettings } from 'proton-shared/lib/interfaces/calendar'; 5 | import { SettingsPropsShared, ThemesSection, CalendarTimeSection, CalendarLayoutSection } from 'react-components'; 6 | 7 | import PrivateMainSettingsAreaWithPermissions from '../../content/PrivateMainSettingsAreaWithPermissions'; 8 | 9 | const generalSettingsConfig = { 10 | to: '/calendar/general', 11 | icon: 'settings-master', 12 | text: c('Link').t`General`, 13 | subsections: [ 14 | { 15 | text: c('Title').t`Time zone`, 16 | id: 'time', 17 | }, 18 | { 19 | text: c('Title').t`Layout`, 20 | id: 'layout', 21 | }, 22 | { 23 | text: c('Title').t`Theme`, 24 | id: 'theme', 25 | }, 26 | ], 27 | }; 28 | 29 | interface Props extends SettingsPropsShared { 30 | calendarUserSettings: CalendarUserSettings; 31 | } 32 | 33 | const CalendarGeneralSettings = ({ calendarUserSettings, location }: Props) => { 34 | return ( 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | export default CalendarGeneralSettings; 44 | -------------------------------------------------------------------------------- /src/app/containers/organization/OrganizationMultiUserSupportSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | import { SettingsPropsShared, OrganizationSection, useOrganization } from 'react-components'; 4 | import { PERMISSIONS } from 'proton-shared/lib/constants'; 5 | import { Organization } from 'proton-shared/lib/interfaces'; 6 | 7 | import PrivateMainSettingsAreaWithPermissions from '../../components/PrivateMainSettingsAreaWithPermissions'; 8 | 9 | const { MULTI_USERS } = PERMISSIONS; 10 | 11 | export const getMultiUserSupportPage = (organization: Organization) => { 12 | return { 13 | text: c('Title').t`Organization`, 14 | to: '/organization', 15 | icon: 'organization', 16 | subsections: [ 17 | { 18 | text: 19 | organization && organization.HasKeys 20 | ? c('Title').t`Organization` 21 | : c('Title').t`Multi-user support`, 22 | id: 'name', 23 | permissions: [MULTI_USERS], 24 | }, 25 | ], 26 | }; 27 | }; 28 | 29 | const OrganizationMultiUserSupportSettings = ({ location }: SettingsPropsShared) => { 30 | const [organization] = useOrganization(); 31 | return ( 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default OrganizationMultiUserSupportSettings; 39 | -------------------------------------------------------------------------------- /src/app/signup/CheckoutButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PAYMENT_METHOD_TYPE, PAYMENT_METHOD_TYPES } from 'proton-shared/lib/constants'; 3 | import { c } from 'ttag'; 4 | import { SubscriptionCheckResponse } from 'proton-shared/lib/interfaces'; 5 | 6 | import { PrimaryButton, PayPalButton } from 'react-components'; 7 | import { SignupPayPal } from './interfaces'; 8 | 9 | interface Props { 10 | className?: string; 11 | paypal: SignupPayPal; 12 | canPay: boolean; 13 | loading: boolean; 14 | method?: PAYMENT_METHOD_TYPE; 15 | checkResult?: SubscriptionCheckResponse; 16 | } 17 | 18 | const CheckoutButton = ({ className, paypal, canPay, loading, method, checkResult }: Props) => { 19 | if (method === PAYMENT_METHOD_TYPES.PAYPAL) { 20 | return ( 21 | {c('Action').t`Pay`} 28 | ); 29 | } 30 | 31 | if (checkResult && !checkResult.AmountDue) { 32 | return ( 33 | {c('Action') 34 | .t`Confirm`} 35 | ); 36 | } 37 | 38 | return ( 39 | {c('Action') 40 | .t`Pay`} 41 | ); 42 | }; 43 | 44 | export default CheckoutButton; 45 | -------------------------------------------------------------------------------- /src/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | 3 | # Redirect to https if not coming from https && not forwarded from https && not curl nor any health check user-agent 4 | RewriteCond %{HTTPS} !=on 5 | RewriteCond %{HTTP:X-Forwarded-Proto} !=https 6 | RewriteCond %{HTTP_USER_AGENT} !(^kube-probe|^GoogleHC|^curl) 7 | RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] 8 | 9 | 10 | # Redirect nothing to app 11 | RewriteRule ^$ /index.html [NC,L] 12 | 13 | # Hide .git stuff 14 | RewriteRule ^.*?\.git.* /index.html [NC,L] 15 | 16 | RewriteCond %{REQUEST_FILENAME} -s [OR] 17 | RewriteCond %{REQUEST_FILENAME} -l [OR] 18 | RewriteCond %{REQUEST_FILENAME} -d 19 | RewriteRule ^.*$ - [NC,L] 20 | 21 | RewriteRule ^(.*) /index.html [NC,L] 22 | 23 | # Error pages 24 | ErrorDocument 403 /assets/errors/403.html 25 | 26 | 27 | FileETag None 28 | Header unset ETag 29 | Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate" 30 | Header set Pragma "no-cache" 31 | Header set Expires "Wed, 11 Jan 1984 05:00:00 GMT" 32 | 33 | 34 | 35 | AddType application/font-woff2 .woff2 36 | 37 | 38 | 39 | AddOutputFilter INCLUDES;DEFLATE svg 40 | 41 | -------------------------------------------------------------------------------- /src/app/containers/vpn/VpnSettingsSidebarList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | import { useRouteMatch } from 'react-router-dom'; 4 | 5 | import { 6 | SidebarList, 7 | SidebarListItem, 8 | SidebarListItemContent, 9 | SidebarListItemContentIcon, 10 | SidebarListItemLink, 11 | } from 'react-components'; 12 | import { APPS, APPS_CONFIGURATION } from 'proton-shared/lib/constants'; 13 | 14 | const LocalListItem = ({ to, icon, children }: { to: string; icon: string; children: React.ReactNode }) => ( 15 | 16 | 17 | }> 18 | {children} 19 | 20 | 21 | 22 | ); 23 | 24 | const VpnSettingsSidebarList = () => { 25 | const { path } = useRouteMatch(); 26 | 27 | return ( 28 | 29 | 30 | {APPS_CONFIGURATION[APPS.PROTONVPN_SETTINGS].name} 31 | 32 | 33 | {c('Settings section title').t`VPN apps`} 34 | 35 | 36 | OpenVPN/IKEv2 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default VpnSettingsSidebarList; 43 | -------------------------------------------------------------------------------- /src/app/containers/calendar/CalendarSettingsSidebarList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRouteMatch } from 'react-router-dom'; 3 | 4 | import { 5 | SidebarList, 6 | SidebarListItem, 7 | SidebarListItemContent, 8 | SidebarListItemContentIcon, 9 | SidebarListItemLink, 10 | } from 'react-components'; 11 | import { c } from 'ttag'; 12 | import { APPS, APPS_CONFIGURATION } from 'proton-shared/lib/constants'; 13 | 14 | const LocalListItem = ({ to, icon, children }: { to: string; icon: string; children: React.ReactNode }) => ( 15 | 16 | 17 | }> 18 | {children} 19 | 20 | 21 | 22 | ); 23 | 24 | const CalendarSettingsSidebarList = () => { 25 | const { path } = useRouteMatch(); 26 | 27 | return ( 28 | 29 | 30 | {APPS_CONFIGURATION[APPS.PROTONCALENDAR].name} 31 | 32 | 33 | {c('Settings section title').t`General`} 34 | 35 | 36 | {c('Settings section title').t`Calendars`} 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default CalendarSettingsSidebarList; 43 | -------------------------------------------------------------------------------- /src/app/containers/account/AccountSettingsSidebarList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | SectionConfig, 4 | SidebarList, 5 | SidebarListItem, 6 | SidebarListItemContent, 7 | SidebarListItemContentIcon, 8 | SidebarListItemLink, 9 | useUser, 10 | } from 'react-components'; 11 | import { UserModel } from 'proton-shared/lib/interfaces'; 12 | import isTruthy from 'proton-shared/lib/helpers/isTruthy'; 13 | 14 | import { getDashboardPage } from './AccountDashboardSettings'; 15 | import { getPasswordAndRecoveryPage } from './AccountPasswordAndRecoverySettings'; 16 | import { getPaymentPage } from './AccountPaymentSettings'; 17 | import { getSecurityPage } from './AccountSecuritySettings'; 18 | 19 | const getPages = (user: UserModel): SectionConfig[] => 20 | [ 21 | getDashboardPage({ user }), 22 | getPasswordAndRecoveryPage({ user }), 23 | user.canPay && getPaymentPage(), 24 | getSecurityPage(), 25 | ].filter(isTruthy); 26 | 27 | const AccountSettingsSidebarList = ({ appSlug }: { appSlug: string }) => { 28 | const [user] = useUser(); 29 | 30 | return ( 31 | 32 | {getPages(user).map(({ text, to, icon }) => ( 33 | 34 | 35 | }> 36 | {text} 37 | 38 | 39 | 40 | ))} 41 | 42 | ); 43 | }; 44 | 45 | export default AccountSettingsSidebarList; 46 | -------------------------------------------------------------------------------- /src/app/containers/mail/MailEncryptionKeysSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | 4 | import { 5 | AddressVerificationSection, 6 | ExternalPGPSettingsSection, 7 | AddressKeysSection, 8 | UserKeysSection, 9 | SettingsPropsShared, 10 | } from 'react-components'; 11 | 12 | import PrivateMainSettingsAreaWithPermissions from '../../components/PrivateMainSettingsAreaWithPermissions'; 13 | 14 | export const getEncryptionKeysPage = () => { 15 | return { 16 | text: c('Title').t`Encryption & keys`, 17 | to: '/mail/encryption-keys', 18 | icon: 'security', 19 | subsections: [ 20 | { 21 | text: c('Title').t`Address verification`, 22 | id: 'address-verification', 23 | }, 24 | { 25 | text: c('Title').t`External PGP settings`, 26 | id: 'pgp-settings', 27 | }, 28 | { 29 | text: c('Title').t`Email encryption keys`, 30 | id: 'addresses', 31 | }, 32 | { 33 | text: c('Title').t`Contact encryption keys`, 34 | id: 'user', 35 | }, 36 | ], 37 | }; 38 | }; 39 | 40 | const MailEncryptionKeysSettings = ({ location }: SettingsPropsShared) => { 41 | return ( 42 | 43 | 44 | 45 | 46 | 47 | 48 | ); 49 | }; 50 | 51 | export default MailEncryptionKeysSettings; 52 | -------------------------------------------------------------------------------- /src/app/login/UnlockForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { c } from 'ttag'; 3 | import { requiredValidator } from 'proton-shared/lib/helpers/formValidators'; 4 | import { noop } from 'proton-shared/lib/helpers/function'; 5 | 6 | import { Button, useLoading, PasswordInputTwo, useFormErrors, InputFieldTwo } from 'react-components'; 7 | 8 | interface Props { 9 | onSubmit: (keyPassword: string) => Promise; 10 | } 11 | 12 | const UnlockForm = ({ onSubmit }: Props) => { 13 | const [loading, withLoading] = useLoading(); 14 | const [keyPassword, setKeyPassword] = useState(''); 15 | 16 | const { validator, onFormSubmit } = useFormErrors(); 17 | 18 | return ( 19 |
{ 22 | event.preventDefault(); 23 | if (loading || !onFormSubmit()) { 24 | return; 25 | } 26 | withLoading(onSubmit(keyPassword)).catch(noop); 27 | }} 28 | method="post" 29 | > 30 | 41 | 44 | 45 | ); 46 | }; 47 | 48 | export default UnlockForm; 49 | -------------------------------------------------------------------------------- /src/app/reset/RequestRecoveryForm.tsx: -------------------------------------------------------------------------------- 1 | import { c } from 'ttag'; 2 | import React from 'react'; 3 | import { Button, useFormErrors, useLoading, InputFieldTwo } from 'react-components'; 4 | import { requiredValidator } from 'proton-shared/lib/helpers/formValidators'; 5 | import { ResetPasswordState, ResetPasswordSetters } from 'react-components/containers/resetPassword/useResetPassword'; 6 | import { noop } from 'proton-shared/lib/helpers/function'; 7 | 8 | interface Props { 9 | onSubmit: () => Promise; 10 | state: ResetPasswordState; 11 | setters: ResetPasswordSetters; 12 | } 13 | 14 | const RequestRecoveryForm = ({ onSubmit, state, setters: stateSetters }: Props) => { 15 | const [loading, withLoading] = useLoading(); 16 | 17 | const { validator, onFormSubmit } = useFormErrors(); 18 | 19 | return ( 20 |
{ 22 | e.preventDefault(); 23 | if (loading || !onFormSubmit()) { 24 | return; 25 | } 26 | withLoading(onSubmit()).catch(noop); 27 | }} 28 | > 29 | 39 | 41 | 42 | ); 43 | }; 44 | 45 | export default RequestRecoveryForm; 46 | -------------------------------------------------------------------------------- /src/app/login/GenerateInternalAddressConfirmForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | import { useLoading, Button } from 'react-components'; 4 | import { noop } from 'proton-shared/lib/helpers/function'; 5 | import { BRAND_NAME } from 'proton-shared/lib/constants'; 6 | 7 | interface Props { 8 | onSubmit: () => Promise; 9 | address: string; 10 | recoveryAddress: string; 11 | } 12 | 13 | const GenerateInternalAddressConfirmForm = ({ onSubmit, address, recoveryAddress }: Props) => { 14 | const [loading, withLoading] = useLoading(); 15 | 16 | const strongAddressAvailable = {c('Action').t`${address} is available.`}; 17 | 18 | return ( 19 |
{ 22 | event.preventDefault(); 23 | if (loading) { 24 | return; 25 | } 26 | withLoading(onSubmit()).catch(noop); 27 | }} 28 | method="post" 29 | > 30 |
31 | {c('Info') 32 | .jt`${strongAddressAvailable} You will use this email address to sign into all ${BRAND_NAME} services.`} 33 |
34 |
35 |
{c('Info').t`Your recovery email address:`}
36 | {recoveryAddress} 37 |
38 | 41 |
42 | ); 43 | }; 44 | 45 | export default GenerateInternalAddressConfirmForm; 46 | -------------------------------------------------------------------------------- /src/app/content/PrivateApp.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StandardPrivateApp } from 'react-components'; 3 | import { TtagLocaleMap } from 'proton-shared/lib/interfaces/Locale'; 4 | import { 5 | UserModel, 6 | MailSettingsModel, 7 | UserSettingsModel, 8 | DomainsModel, 9 | AddressesModel, 10 | LabelsModel, 11 | FiltersModel, 12 | OrganizationModel, 13 | MembersModel, 14 | SubscriptionModel, 15 | PaymentMethodsModel, 16 | ImportersModel, 17 | ImportHistoriesModel, 18 | CalendarsModel, 19 | CalendarUserSettingsModel, 20 | ContactsModel, 21 | ContactEmailsModel, 22 | } from 'proton-shared/lib/models'; 23 | 24 | const EVENT_MODELS = [ 25 | UserModel, 26 | MailSettingsModel, 27 | UserSettingsModel, 28 | AddressesModel, 29 | DomainsModel, 30 | LabelsModel, 31 | FiltersModel, 32 | SubscriptionModel, 33 | OrganizationModel, 34 | MembersModel, 35 | PaymentMethodsModel, 36 | ImportersModel, 37 | ImportHistoriesModel, 38 | CalendarsModel, 39 | CalendarUserSettingsModel, 40 | ContactsModel, 41 | ContactEmailsModel, 42 | ]; 43 | 44 | const PRELOAD_MODELS = [UserSettingsModel, MailSettingsModel, UserModel]; 45 | 46 | const getAppContainer = () => import('./SetupMainContainer'); 47 | 48 | interface Props { 49 | onLogout: () => void; 50 | locales: TtagLocaleMap; 51 | } 52 | 53 | const PrivateApp = ({ onLogout, locales }: Props) => { 54 | return ( 55 | 64 | ); 65 | }; 66 | 67 | export default PrivateApp; 68 | -------------------------------------------------------------------------------- /src/app/login/LoginSupportDropdown.tsx: -------------------------------------------------------------------------------- 1 | import { c } from 'ttag'; 2 | import React from 'react'; 3 | import { BugModal, DropdownMenuButton, DropdownMenuLink, Icon, useModals } from 'react-components'; 4 | import { Link } from 'react-router-dom'; 5 | import SupportDropdown from '../public/SupportDropdown'; 6 | 7 | const LoginSupportDropdown = () => { 8 | const { createModal } = useModals(); 9 | 10 | const handleBugReportClick = () => { 11 | createModal(); 12 | }; 13 | 14 | return ( 15 | 16 | 20 | 21 | {c('Link').t`Reset password`} 22 | 23 | 27 | 28 | {c('Link').t`Forgot username?`} 29 | 30 | 35 | 36 | {c('Link').t`Common sign in issues`} 37 | 38 | 39 | 40 | {c('Action').t`Report a problem`} 41 | 42 | 43 | ); 44 | }; 45 | 46 | export default LoginSupportDropdown; 47 | -------------------------------------------------------------------------------- /src/app/containers/mail/MailImportAndExportSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | import { UserModel } from 'proton-shared/lib/interfaces'; 4 | import isTruthy from 'proton-shared/lib/helpers/isTruthy'; 5 | import { 6 | StartMailImportSection, 7 | MailImportListSection, 8 | MailImportExportSection, 9 | SettingsPropsShared, 10 | useUser, 11 | } from 'react-components'; 12 | 13 | import PrivateMainSettingsAreaWithPermissions from '../../components/PrivateMainSettingsAreaWithPermissions'; 14 | 15 | export const getImportPage = ({ user }: { user: UserModel }) => { 16 | return { 17 | text: user.isFree ? c('Title').t`Import Assistant` : c('Title').t`Import & export`, 18 | to: '/mail/import-export', 19 | icon: 'import', 20 | subsections: [ 21 | { 22 | text: c('Title').t`Import Assistant`, 23 | id: 'start-import', 24 | }, 25 | { 26 | text: c('Title').t`Current & past imports`, 27 | id: 'import-list', 28 | }, 29 | !user.isFree && { 30 | text: c('Title').t`Import-Export app`, 31 | id: 'import-export', 32 | }, 33 | ].filter(isTruthy), 34 | }; 35 | }; 36 | 37 | const MailImportAndExportSettings = ({ setActiveSection, location }: SettingsPropsShared) => { 38 | const [user] = useUser(); 39 | 40 | return ( 41 | 46 | 47 | 48 | {!user.isFree && } 49 | 50 | ); 51 | }; 52 | 53 | export default MailImportAndExportSettings; 54 | -------------------------------------------------------------------------------- /src/app/containers/vpn/VpnUpgradeSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c, msgid } from 'ttag'; 3 | import { PLANS, PLAN_NAMES, APPS_CONFIGURATION, APPS } from 'proton-shared/lib/constants'; 4 | import { ButtonLike, Card, SettingsLink, SettingsSectionWide, usePlans, useUserVPN } from 'react-components'; 5 | 6 | const VpnUpgradeSection = () => { 7 | const [plans, loadingPlans] = usePlans(); 8 | const plusVpnConnections = plans?.find(({ Name }) => Name === PLANS.VPNPLUS)?.MaxVPN || 10; 9 | 10 | const { result: { VPN: userVPN = {} } = {} } = useUserVPN(); 11 | const shouldUpgrade = 12 | userVPN.PlanName === 'trial' || userVPN.PlanName === 'vpnbasic' || userVPN.PlanName === 'free'; 13 | const protonVpnName = APPS_CONFIGURATION[APPS.PROTONVPN_SETTINGS].name; 14 | 15 | if (loadingPlans || !shouldUpgrade) { 16 | return null; 17 | } 18 | 19 | return ( 20 | 21 | 22 |

23 | {c('Upgrade').ngettext( 24 | msgid`Upgrade to ${protonVpnName} ${ 25 | PLAN_NAMES[PLANS.VPNPLUS] 26 | } to connect up to ${plusVpnConnections} device to the VPN at once`, 27 | `Upgrade to ${protonVpnName} ${ 28 | PLAN_NAMES[PLANS.VPNPLUS] 29 | } to connect up to ${plusVpnConnections} devices to the VPN at once`, 30 | plusVpnConnections 31 | )} 32 |

33 | 34 | 35 | {c('Action').t`Upgrade`} 36 | 37 |
38 |
39 | ); 40 | }; 41 | 42 | export default VpnUpgradeSection; 43 | -------------------------------------------------------------------------------- /src/app/signup/helpers/handleCreateUser.ts: -------------------------------------------------------------------------------- 1 | import { Api } from 'proton-shared/lib/interfaces'; 2 | import { srpVerify } from 'proton-shared/lib/srp'; 3 | import { queryCreateUser } from 'proton-shared/lib/api/user'; 4 | import { API_CUSTOM_ERROR_CODES } from 'proton-shared/lib/errors'; 5 | import { HumanVerificationError } from '../interfaces'; 6 | 7 | interface CreateUserArgs { 8 | api: Api; 9 | clientType: 1 | 2; 10 | payload?: { [key: string]: string }; 11 | username: string; 12 | password: string; 13 | recoveryEmail: string; 14 | recoveryPhone: string; 15 | } 16 | 17 | const handleCreateUser = async ({ 18 | api, 19 | username, 20 | password, 21 | recoveryEmail, 22 | recoveryPhone, 23 | clientType, 24 | payload, 25 | }: CreateUserArgs) => { 26 | if (!username) { 27 | throw new Error('Missing username'); 28 | } 29 | try { 30 | await srpVerify({ 31 | api, 32 | credentials: { password }, 33 | config: { 34 | ...queryCreateUser({ 35 | Type: clientType, 36 | ...(recoveryEmail ? { Email: recoveryEmail } : {}), 37 | ...(recoveryPhone ? { Phone: recoveryPhone } : {}), 38 | Username: username, 39 | Payload: payload, 40 | }), 41 | silence: [API_CUSTOM_ERROR_CODES.HUMAN_VERIFICATION_REQUIRED], 42 | ignoreHandler: [API_CUSTOM_ERROR_CODES.HUMAN_VERIFICATION_REQUIRED], 43 | }, 44 | }); 45 | } catch (error) { 46 | const { data: { Code, Details } = { Code: 0, Details: {} } } = error; 47 | 48 | if (Code === API_CUSTOM_ERROR_CODES.HUMAN_VERIFICATION_REQUIRED) { 49 | const { HumanVerificationMethods = [], HumanVerificationToken = '' } = Details; 50 | throw new HumanVerificationError(HumanVerificationMethods, HumanVerificationToken); 51 | } 52 | 53 | throw error; 54 | } 55 | }; 56 | 57 | export default handleCreateUser; 58 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Release: 4.0.1 — July 6, 2021 2 | 3 | ### Improvements 4 | - General user interface and accessibility improvements 5 | - Increased level of detail and improved clarity of error notifications 6 | 7 | ### Fixes 8 | - Minor bug fixes in the organization, custom domain, and password settings flows 9 | - Minor visual bug fixes 10 | 11 | # Proton services: same privacy, better experience 12 | 13 | ## Release: 4.0.0 — June 8, 2021 14 | 15 | We’ve given ProtonMail a facelift and added new features and integrated services to make it the most powerful secure email service! The clean design, customization options, and usability improvements mean keeping your data private is even easier and more enjoyable. [Learn more about this release.](https://protonmail.com/blog/new-protonmail-announcement) 16 | 17 | ### New features 18 | - **Single sign-on**: Access all Proton services (Mail, Calendar, Drive, VPN) and switch between multiple accounts with one sign-in. 19 | - **Persistent session**: Stay signed in to Proton services, even after closing your browser. No need to sign back in from a new tab or window. 20 | - **Themes and layouts**: Customize your inbox and calendar with new themes, such as dark mode, and various layouts. 21 | - **Welcome screens**: Get familiar with ProtonMail and our other services. Our wizard will show you around and make you feel at home. 22 | 23 | ### Improvements 24 | - **Security**: Various anti-abuse and security enhancements. 25 | - **Accessibility**: Better contrast, more text clarity, and other improvements make privacy truly accessible to all. 26 | - **Usability**: Better experience when upgrading to a paid subscription and customizing your plan. 27 | - **Revamped Settings menus**: New structure and design makes it easier to access and edit your settings. 28 | - **Password recovery**: Phone number added as a recovery method. Ensures you can safely and conveniently access your account, even if you forget your password. 29 | 30 | Be among the first to test new Proton features and services. To do so, enable Beta Access from “Settings”. As always, we welcome your feedback. Report an issue or request a feature under “Help” in the top menu bar. 31 | -------------------------------------------------------------------------------- /src/app/containers/account/AccountPasswordAndRecoverySettings.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { PasswordsSection, DeleteSection, RecoveryMethodsSection, SettingsPropsShared } from 'react-components'; 3 | import { c } from 'ttag'; 4 | import { UserModel } from 'proton-shared/lib/interfaces'; 5 | import isTruthy from 'proton-shared/lib/helpers/isTruthy'; 6 | 7 | import PrivateMainSettingsAreaWithPermissions from '../../components/PrivateMainSettingsAreaWithPermissions'; 8 | 9 | export const getPasswordAndRecoveryPage = ({ user }: { user: UserModel }) => { 10 | const hasRecoveryOptions = user.isPrivate; 11 | 12 | return { 13 | text: hasRecoveryOptions ? c('Title').t`Password & recovery` : c('Title').t`Password`, 14 | to: '/authentication', 15 | icon: 'keys', 16 | subsections: [ 17 | { 18 | text: c('Title').t`Passwords`, 19 | id: 'passwords', 20 | }, 21 | hasRecoveryOptions && { 22 | text: c('Title').t`Recovery & notification`, 23 | id: 'email', 24 | }, 25 | user.canPay && { 26 | text: c('Title').t`Delete account`, 27 | id: 'delete', 28 | }, 29 | ].filter(isTruthy), 30 | }; 31 | }; 32 | 33 | interface Props extends SettingsPropsShared { 34 | user: UserModel; 35 | } 36 | 37 | const AccountPasswordAndRecoverySettings = ({ location, setActiveSection, user }: Props) => { 38 | const [action] = useState(() => { 39 | return new URLSearchParams(location.search).get('action'); 40 | }); 41 | 42 | return ( 43 | 48 | 49 | {user.isPrivate && } 50 | {user.canPay && } 51 | 52 | ); 53 | }; 54 | 55 | export default AccountPasswordAndRecoverySettings; 56 | -------------------------------------------------------------------------------- /src/app/public/LanguageSelect.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { localeCode } from 'proton-shared/lib/i18n'; 3 | import { loadDateLocale, loadLocale } from 'proton-shared/lib/i18n/loadLocale'; 4 | import { getSecondLevelDomain } from 'proton-shared/lib/helpers/url'; 5 | import { getBrowserLocale, getClosestLocaleCode } from 'proton-shared/lib/i18n/helper'; 6 | import { TtagLocaleMap } from 'proton-shared/lib/interfaces/Locale'; 7 | import { setCookie } from 'proton-shared/lib/helpers/cookies'; 8 | import { addDays } from 'date-fns'; 9 | import { useConfig, useForceRefresh, DropdownMenu, DropdownMenuButton, Icon, SimpleDropdown } from 'react-components'; 10 | 11 | interface Props { 12 | className?: string; 13 | locales?: TtagLocaleMap; 14 | } 15 | 16 | const cookieDomain = `.${getSecondLevelDomain()}`; 17 | const LanguageSelect = ({ className, locales = {} }: Props) => { 18 | const forceRefresh = useForceRefresh(); 19 | const { LOCALES = {} } = useConfig(); 20 | const handleChange = async (newLocale: string) => { 21 | const localeCode = getClosestLocaleCode(newLocale, locales); 22 | await Promise.all([loadLocale(localeCode, locales), loadDateLocale(localeCode, getBrowserLocale())]); 23 | setCookie({ 24 | cookieName: 'Locale', 25 | cookieValue: localeCode, 26 | expirationDate: addDays(new Date(), 30).toUTCString(), 27 | cookieDomain, 28 | }); 29 | forceRefresh(); 30 | }; 31 | const languages = Object.keys(LOCALES).map((value) => ( 32 | handleChange(value)}> 33 | {LOCALES[value]} 34 | 35 | )); 36 | 37 | const selectedLanguage = ( 38 | <> 39 | 40 | {LOCALES[localeCode]} 41 | 42 | ); 43 | 44 | return ( 45 | 46 | {languages} 47 | 48 | ); 49 | }; 50 | 51 | export default LanguageSelect; 52 | -------------------------------------------------------------------------------- /src/app/containers/account/AccountPaymentSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | PaymentMethodsSection, 4 | InvoicesSection, 5 | BillingSection, 6 | SettingsPropsShared, 7 | GiftCodeSection, 8 | CreditsSection, 9 | } from 'react-components'; 10 | import { c } from 'ttag'; 11 | import { PERMISSIONS } from 'proton-shared/lib/constants'; 12 | import isTruthy from 'proton-shared/lib/helpers/isTruthy'; 13 | 14 | import PrivateMainSettingsAreaWithPermissions from '../../components/PrivateMainSettingsAreaWithPermissions'; 15 | 16 | const { UPGRADER, NOT_SUB_USER, PAID } = PERMISSIONS; 17 | 18 | export const getPaymentPage = () => { 19 | return { 20 | text: c('Title').t`Payment`, 21 | to: '/payment', 22 | icon: 'payments-type-card', 23 | permissions: [UPGRADER, NOT_SUB_USER], 24 | subsections: [ 25 | { 26 | text: c('Title').t`Billing details`, 27 | id: 'billing', 28 | permissions: [PAID], 29 | }, 30 | { 31 | text: c('Title').t`Payment methods`, 32 | id: 'payment-methods', 33 | }, 34 | { 35 | text: c('Title').t`Credits`, 36 | id: 'credits', 37 | }, 38 | { 39 | text: c('Title').t`Gift code`, 40 | id: 'gift-code', 41 | }, 42 | { 43 | text: c('Title').t`Invoices`, 44 | id: 'invoices', 45 | }, 46 | ].filter(isTruthy), 47 | }; 48 | }; 49 | 50 | const AccountPaymentSettings = ({ location, setActiveSection }: SettingsPropsShared) => { 51 | return ( 52 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ); 64 | }; 65 | 66 | export default AccountPaymentSettings; 67 | -------------------------------------------------------------------------------- /src/app/signup/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { Currency, Cycle, HumanVerificationMethodType, SubscriptionCheckResponse } from 'proton-shared/lib/interfaces'; 2 | import { APPS } from 'proton-shared/lib/constants'; 3 | 4 | export enum SIGNUP_STEPS { 5 | NO_SIGNUP = 'no-signup', 6 | ACCOUNT_CREATION_USERNAME = 'account-creation-username', 7 | RECOVERY_EMAIL = 'recovery-email', 8 | RECOVERY_PHONE = 'recovery-phone', 9 | VERIFICATION_CODE = 'verification-code', 10 | CUSTOMISATION = 'customisation', 11 | PLANS = 'plans', 12 | PAYMENT = 'payment', 13 | HUMAN_VERIFICATION = 'human-verification', 14 | CREATING_ACCOUNT = 'creating-account', 15 | COMPLETE = 'complete', 16 | } 17 | 18 | export const SERVICES = { 19 | mail: APPS.PROTONMAIL, 20 | calendar: APPS.PROTONCALENDAR, 21 | drive: APPS.PROTONDRIVE, 22 | vpn: APPS.PROTONVPN_SETTINGS, 23 | }; 24 | export type SERVICES_KEYS = keyof typeof SERVICES; 25 | 26 | export interface PlanIDs { 27 | [planID: string]: number; 28 | } 29 | 30 | export interface SignupModel { 31 | step: SIGNUP_STEPS; 32 | stepHistory: SIGNUP_STEPS[]; 33 | username: string; 34 | email: string; 35 | password: string; 36 | confirmPassword: string; 37 | signupType: 'email' | 'username'; 38 | domains: string[]; 39 | recoveryEmail: string; 40 | recoveryPhone: string; 41 | currency: Currency; 42 | cycle: Cycle; 43 | planIDs: PlanIDs; 44 | skipPlanStep?: boolean; 45 | humanVerificationMethods: HumanVerificationMethodType[]; 46 | humanVerificationToken: string; 47 | checkResult: SubscriptionCheckResponse; 48 | } 49 | 50 | export interface SignupPayPal { 51 | isReady: boolean; 52 | loadingToken: boolean; 53 | loadingVerification: boolean; 54 | onToken: () => void; 55 | onVerification: () => void; 56 | } 57 | 58 | export class HumanVerificationError extends Error { 59 | methods: HumanVerificationMethodType[]; 60 | 61 | token: string; 62 | 63 | constructor(methods: HumanVerificationMethodType[], token: string) { 64 | super('HumanVerificationError'); 65 | this.methods = methods; 66 | this.token = token; 67 | Object.setPrototypeOf(this, HumanVerificationError.prototype); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/app/containers/mail/MailGeneralSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | 4 | import { 5 | MessagesSection, 6 | MailGeneralAdvancedSection, 7 | SettingsPropsShared, 8 | PmMeSection, 9 | useAddresses, 10 | } from 'react-components'; 11 | 12 | import { ADDRESS_TYPE } from 'proton-shared/lib/constants'; 13 | import { UserModel } from 'proton-shared/lib/interfaces'; 14 | import isTruthy from 'proton-shared/lib/helpers/isTruthy'; 15 | import { getHasOnlyExternalAddresses } from 'proton-shared/lib/helpers/address'; 16 | 17 | import PrivateMainSettingsAreaWithPermissions from '../../components/PrivateMainSettingsAreaWithPermissions'; 18 | import PrivateMainAreaLoading from '../../components/PrivateMainAreaLoading'; 19 | 20 | export const getGeneralPage = (user: UserModel, showPmMeSection: boolean) => { 21 | return { 22 | text: c('Title').t`General`, 23 | to: '/mail/general', 24 | icon: 'general', 25 | subsections: [ 26 | showPmMeSection && { 27 | text: c('Title').t`Short domain (@pm.me)`, 28 | id: 'pmme', 29 | }, 30 | { 31 | text: c('Title').t`Messages`, 32 | id: 'messages', 33 | }, 34 | { 35 | text: c('Title').t`Advanced`, 36 | id: 'advanced', 37 | }, 38 | ].filter(isTruthy), 39 | }; 40 | }; 41 | 42 | interface Props extends SettingsPropsShared { 43 | user: UserModel; 44 | } 45 | 46 | const MailGeneralSettings = ({ location, user }: Props) => { 47 | const [addresses, loading] = useAddresses(); 48 | 49 | if (loading && !Array.isArray(addresses)) { 50 | return ; 51 | } 52 | 53 | const { hasPaidMail, canPay, isSubUser } = user; 54 | const isExternalUser = getHasOnlyExternalAddresses(addresses); 55 | const isPMAddressActive = addresses.some(({ Type }) => Type === ADDRESS_TYPE.TYPE_PREMIUM); 56 | const showPmMeSection = !isExternalUser && canPay && !isSubUser && !(isPMAddressActive && hasPaidMail); 57 | 58 | return ( 59 | 60 | {showPmMeSection && } 61 | 62 | 63 | 64 | ); 65 | }; 66 | 67 | export default MailGeneralSettings; 68 | -------------------------------------------------------------------------------- /src/app/login/SetPasswordForm.tsx: -------------------------------------------------------------------------------- 1 | import { c } from 'ttag'; 2 | import React, { useState } from 'react'; 3 | import { PasswordInputTwo, Button, useLoading, useFormErrors, InputFieldTwo } from 'react-components'; 4 | import { noop } from 'proton-shared/lib/helpers/function'; 5 | import { 6 | confirmPasswordValidator, 7 | getMinPasswordLengthMessage, 8 | passwordLengthValidator, 9 | } from 'proton-shared/lib/helpers/formValidators'; 10 | 11 | interface Props { 12 | onSubmit: (newPassword: string) => Promise; 13 | } 14 | 15 | const SetPasswordForm = ({ onSubmit }: Props) => { 16 | const [loading, withLoading] = useLoading(); 17 | const [newPassword, setNewPassword] = useState(''); 18 | const [confirmNewPassword, setConfirmNewPassword] = useState(''); 19 | 20 | const { validator, onFormSubmit } = useFormErrors(); 21 | 22 | return ( 23 |
{ 26 | event.preventDefault(); 27 | if (loading || !onFormSubmit()) { 28 | return; 29 | } 30 | withLoading(onSubmit(newPassword)).catch(noop); 31 | }} 32 | method="post" 33 | > 34 | 47 | 62 | 65 | 66 | ); 67 | }; 68 | 69 | export default SetPasswordForm; 70 | -------------------------------------------------------------------------------- /src/app/containers/mail/MailSettingsRouter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect, Switch, useRouteMatch, useLocation } from 'react-router-dom'; 3 | 4 | import { useUser } from 'react-components'; 5 | 6 | import MailAppearanceSettings from './MailAppearanceSettings'; 7 | import MailAutoReplySettings from './MailAutoReplySettings'; 8 | import MailDomainNamesSettings from './MailDomainNamesSettings'; 9 | import MailEncryptionKeysSettings from './MailEncryptionKeysSettings'; 10 | import MailFiltersSettings from './MailFiltersSettings'; 11 | import MailFoldersAndLabelsSettings from './MailFoldersAndLabelsSettings'; 12 | import MailGeneralSettings from './MailGeneralSettings'; 13 | import MailIdentityAndAddressesSettings from './MailIdentityAndAddressesSettings'; 14 | import MailImapSmtpSettings from './MailImapSmtpSettings'; 15 | import MailImportAndExportSettings from './MailImportAndExportSettings'; 16 | 17 | const MailSettingsRouter = () => { 18 | const { path } = useRouteMatch(); 19 | const [user] = useUser(); 20 | const location = useLocation(); 21 | 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ); 57 | }; 58 | 59 | export default MailSettingsRouter; 60 | -------------------------------------------------------------------------------- /src/app/login/GenerateInternalAddressForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { c } from 'ttag'; 3 | import { useLoading, Button, useFormErrors, InputFieldTwo } from 'react-components'; 4 | import { noop } from 'proton-shared/lib/helpers/function'; 5 | import { Api } from 'proton-shared/lib/interfaces'; 6 | import { queryCheckUsernameAvailability } from 'proton-shared/lib/api/user'; 7 | import { getApiErrorMessage } from 'proton-shared/lib/api/helpers/apiErrorHelper'; 8 | import { requiredValidator } from 'proton-shared/lib/helpers/formValidators'; 9 | 10 | interface Props { 11 | onSubmit: (username: string, domain: string) => void; 12 | availableDomains?: string[]; 13 | api: Api; 14 | defaultUsername?: string; 15 | } 16 | 17 | const GenerateInternalAddressForm = ({ defaultUsername = '', onSubmit, availableDomains, api }: Props) => { 18 | const [loading, withLoading] = useLoading(); 19 | const [username, setUsername] = useState(defaultUsername); 20 | const [usernameError, setUsernameError] = useState(''); 21 | 22 | const { validator, onFormSubmit } = useFormErrors(); 23 | 24 | const domain = availableDomains?.length ? availableDomains[0] : ''; 25 | 26 | const handleSubmit = async () => { 27 | try { 28 | await api(queryCheckUsernameAvailability(username)); 29 | } catch (e) { 30 | const errorText = getApiErrorMessage(e) || c('Error').t`Can't check username, try again later`; 31 | setUsernameError(errorText); 32 | throw e; 33 | } 34 | onSubmit(username, domain); 35 | }; 36 | 37 | return ( 38 |
{ 41 | event.preventDefault(); 42 | if (loading || !onFormSubmit()) { 43 | return; 44 | } 45 | withLoading(handleSubmit()).catch(noop); 46 | }} 47 | method="post" 48 | > 49 | { 58 | setUsernameError(''); 59 | setUsername(value); 60 | }} 61 | suffix={`@${domain}`} 62 | /> 63 | 66 | 67 | ); 68 | }; 69 | 70 | export default GenerateInternalAddressForm; 71 | -------------------------------------------------------------------------------- /src/app/containers/organization/OrganizationSettingsSidebarList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | import { 4 | SidebarList, 5 | SidebarListItem, 6 | SidebarListItemContent, 7 | SidebarListItemContentIcon, 8 | SidebarListItemLink, 9 | useOrganization, 10 | } from 'react-components'; 11 | 12 | const OrganizationSettingsSidebarList = ({ appSlug }: { appSlug: string }) => { 13 | const [organization, loading] = useOrganization(); 14 | 15 | const hasOrganization = organization?.HasKeys; 16 | 17 | return ( 18 | 19 | {!loading && 20 | (hasOrganization ? ( 21 | <> 22 | 23 | 24 | }> 25 | {c('Settings section title').t`Users & addresses`} 26 | 27 | 28 | 29 | 30 | 31 | }> 32 | {c('Settings section title').t`Domain names`} 33 | 34 | 35 | 36 | 37 | 38 | }> 39 | {c('Settings section title').t`Organization & keys`} 40 | 41 | 42 | 43 | 44 | ) : ( 45 | 46 | 47 | }> 48 | {c('Settings section title').t`Multi-user support`} 49 | 50 | 51 | 52 | ))} 53 | 54 | ); 55 | }; 56 | 57 | export default OrganizationSettingsSidebarList; 58 | -------------------------------------------------------------------------------- /src/app/signup/constants.ts: -------------------------------------------------------------------------------- 1 | import { getFreeCheckResult } from 'proton-shared/lib/subscription/freePlans'; 2 | import { SIGNUP_STEPS, SignupModel } from './interfaces'; 3 | 4 | export const YANDEX_DOMAINS = ['yandex.ru', 'yandex.ua']; 5 | export const YAHOO_DOMAINS = [ 6 | 'yahoo.at', 7 | 'yahoo.be', 8 | 'yahoo.ca', 9 | 'yahoo.co.id', 10 | 'yahoo.co.il', 11 | 'yahoo.co.in', 12 | 'yahoo.co.jp', 13 | 'yahoo.co.nz', 14 | 'yahoo.co.th', 15 | 'yahoo.co.uk', 16 | 'yahoo.co.za', 17 | 'yahoo.com', 18 | 'yahoo.com.ar', 19 | 'yahoo.com.br', 20 | 'yahoo.com.co', 21 | 'yahoo.com.hk', 22 | 'yahoo.com.my', 23 | 'yahoo.com.ph', 24 | 'yahoo.com.sg', 25 | 'yahoo.com.tr', 26 | 'yahoo.com.tw', 27 | 'yahoo.com.vn', 28 | 'yahoo.cz', 29 | 'yahoo.de', 30 | 'yahoo.dk', 31 | 'yahoo.es', 32 | 'yahoo.fi', 33 | 'yahoo.fr', 34 | 'yahoo.gr', 35 | 'yahoo.hu', 36 | 'yahoo.ie', 37 | 'yahoo.in', 38 | 'yahoo.it', 39 | 'yahoo.nl', 40 | 'yahoo.no', 41 | 'yahoo.pl', 42 | 'yahoo.pt', 43 | 'yahoo.ro', 44 | 'yahoo.se', 45 | 'ymail.com', 46 | 'rocketmail.com', 47 | ]; 48 | export const AOL_DOMAINS = [ 49 | 'aol.asia', 50 | 'aol.at', 51 | 'aol.be', 52 | 'aol.ch', 53 | 'aol.cl', 54 | 'aol.co.nz', 55 | 'aol.co.uk', 56 | 'aol.com', 57 | 'aol.com.ar', 58 | 'aol.com.au', 59 | 'aol.com.br', 60 | 'aol.com.co', 61 | 'aol.com.mx', 62 | 'aol.com.tr', 63 | 'aol.com.ve', 64 | 'aol.cz', 65 | 'aol.de', 66 | 'aol.dk', 67 | 'aol.es', 68 | 'aol.fi', 69 | 'aol.fr', 70 | 'aol.in', 71 | 'aol.it', 72 | 'aol.jp', 73 | 'aol.nl', 74 | 'aol.pl', 75 | 'aol.se', 76 | 'aol.tw', 77 | 'wow.com', 78 | 'games.com', 79 | 'love.com', 80 | 'ygm.com', 81 | ]; 82 | export const MAIL_RU_DOMAINS = ['mail.ru', 'inbox.ru', 'list.ru', 'bk.ru']; 83 | export const GMAIL_DOMAINS = ['gmail.com', 'googlemail.com', 'google.com', 'googlegroups.com']; 84 | 85 | export const INSECURE_DOMAINS = [ 86 | ...GMAIL_DOMAINS, 87 | ...AOL_DOMAINS, 88 | ...YAHOO_DOMAINS, 89 | ...YANDEX_DOMAINS, 90 | ...MAIL_RU_DOMAINS, 91 | ]; 92 | 93 | export const DEFAULT_SIGNUP_MODEL: SignupModel = { 94 | step: SIGNUP_STEPS.ACCOUNT_CREATION_USERNAME, 95 | stepHistory: [], 96 | username: '', 97 | password: '', 98 | confirmPassword: '', 99 | email: '', 100 | signupType: 'username', 101 | domains: [], 102 | recoveryEmail: '', 103 | recoveryPhone: '', 104 | currency: 'EUR', 105 | cycle: 12, 106 | planIDs: {}, 107 | humanVerificationMethods: [], 108 | humanVerificationToken: '', 109 | checkResult: getFreeCheckResult(), 110 | }; 111 | -------------------------------------------------------------------------------- /src/app/login/TOTPForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { c } from 'ttag'; 3 | import { requiredValidator } from 'proton-shared/lib/helpers/formValidators'; 4 | import { noop } from 'proton-shared/lib/helpers/function'; 5 | 6 | import { Button, useLoading, useFormErrors, InputFieldTwo, LinkButton } from 'react-components'; 7 | 8 | interface Props { 9 | onSubmit: (totp: string) => Promise; 10 | } 11 | 12 | const TOTPForm = ({ onSubmit }: Props) => { 13 | const [loading, withLoading] = useLoading(); 14 | const [totp, setTotp] = useState(''); 15 | const [isTotpRecovery, setIsRecovery] = useState(false); 16 | 17 | const { validator, onFormSubmit } = useFormErrors(); 18 | 19 | return ( 20 |
{ 23 | event.preventDefault(); 24 | if (loading || !onFormSubmit()) { 25 | return; 26 | } 27 | withLoading(onSubmit(totp)).catch(noop); 28 | }} 29 | autoComplete="off" 30 | method="post" 31 | > 32 | {isTotpRecovery ? ( 33 | 43 | ) : ( 44 | 54 | )} 55 |
56 | { 58 | if (loading) { 59 | return; 60 | } 61 | setTotp(''); 62 | setIsRecovery(!isTotpRecovery); 63 | }} 64 | > 65 | {isTotpRecovery 66 | ? c('Action').t`Use two-factor authentication code` 67 | : c('Action').t`Use recovery code`} 68 | 69 |
70 | 73 | 74 | ); 75 | }; 76 | 77 | export default TOTPForm; 78 | -------------------------------------------------------------------------------- /src/app/containers/mail/MailSettingsSidebarList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | import { useRouteMatch } from 'react-router-dom'; 4 | import { SidebarList, SidebarListItem, SidebarListItemContent, useOrganization, useUser } from 'react-components'; 5 | import { APPS, APPS_CONFIGURATION } from 'proton-shared/lib/constants'; 6 | 7 | import SettingsListItem from '../../components/SettingsListItem'; 8 | 9 | const { PROTONMAIL } = APPS; 10 | 11 | const MailSettingsSidebarList = () => { 12 | const { path } = useRouteMatch(); 13 | const [user] = useUser(); 14 | const [organization] = useOrganization(); 15 | const hasOrganization = organization?.HasKeys; 16 | 17 | return ( 18 | 19 | 20 | {APPS_CONFIGURATION[PROTONMAIL].name} 21 | 22 | 23 | {c('Settings section title').t`General`} 24 | 25 | 26 | {c('Settings section title').t`Identity & addresses`} 27 | 28 | 29 | {c('Settings section title').t`Appearance`} 30 | 31 | 32 | {c('Settings section title').t`Folders & labels`} 33 | 34 | 35 | {c('Settings section title').t`Filters`} 36 | 37 | 38 | {c('Settings section title').t`Auto reply`} 39 | 40 | {!hasOrganization ? ( 41 | 42 | {c('Settings section title').t`Domain Names`} 43 | 44 | ) : null} 45 | 46 | {c('Settings section title').t`Encryption & keys`} 47 | 48 | 49 | {user.isFree ? c('Title').t`Import Assistant` : c('Settings section title').t`Import & export`} 50 | 51 | 52 | {c('Settings section title').t`IMAP/SMTP`} 53 | 54 | 55 | ); 56 | }; 57 | 58 | export default MailSettingsSidebarList; 59 | -------------------------------------------------------------------------------- /src/app/signup/InsecureEmailInfo.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { validateEmailAddress } from 'proton-shared/lib/helpers/email'; 3 | import { c } from 'ttag'; 4 | 5 | import { Icon } from 'react-components'; 6 | import { 7 | INSECURE_DOMAINS, 8 | YANDEX_DOMAINS, 9 | YAHOO_DOMAINS, 10 | AOL_DOMAINS, 11 | MAIL_RU_DOMAINS, 12 | GMAIL_DOMAINS, 13 | } from './constants'; 14 | 15 | interface Props { 16 | email: string; 17 | } 18 | 19 | const getInfo = (domain: string) => { 20 | if (GMAIL_DOMAINS.includes(domain)) { 21 | return c('Info') 22 | .t`Google records your online activity and reads your personal data in order to provide access for advertisers and other third parties. For better privacy, create a secure email address.`; 23 | } 24 | if (YAHOO_DOMAINS.includes(domain)) { 25 | return c('Info') 26 | .t`Yahoo reads your personal data in order to provide access for advertisers, US intelligence agencies and other third parties. For better privacy, create a secure email address.`; 27 | } 28 | if (AOL_DOMAINS.includes(domain)) { 29 | return c('Info') 30 | .t`AOL reads your personal data in order to provide access for advertisers and other third parties. For better privacy, create a secure email address.`; 31 | } 32 | if (YANDEX_DOMAINS.includes(domain)) { 33 | return c('Info') 34 | .t`Yandex records your online activity and reads your personal data in order to provide access for advertisers and other third parties. For better privacy, create a secure email address.`; 35 | } 36 | if (MAIL_RU_DOMAINS.includes(domain)) { 37 | return c('Info') 38 | .t`Mail.ru reads your personal data in order to provide access for advertisers, Russian government agencies and other third parties. For better privacy, create a secure email address.`; 39 | } 40 | }; 41 | 42 | const InsecureEmailInfo = ({ email }: Props) => { 43 | const [expanded, setExpanded] = useState(false); 44 | 45 | if (!validateEmailAddress(email)) { 46 | return null; 47 | } 48 | 49 | const [, domain = ''] = email.trim().toLowerCase().split('@'); 50 | 51 | if (INSECURE_DOMAINS.includes(domain)) { 52 | return ( 53 |
54 | 62 | {expanded ?
{getInfo(domain)}
: null} 63 |
64 | ); 65 | } 66 | 67 | return null; 68 | }; 69 | 70 | export default InsecureEmailInfo; 71 | -------------------------------------------------------------------------------- /src/app/containers/calendar/CalendarCalendarsSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | SettingsPropsShared, 4 | CalendarsSection, 5 | CalendarImportSection, 6 | CalendarShareSection, 7 | Card, 8 | ButtonLike, 9 | SettingsLink, 10 | SettingsSection, 11 | } from 'react-components'; 12 | import { c } from 'ttag'; 13 | 14 | import { Address, UserModel } from 'proton-shared/lib/interfaces'; 15 | import { Calendar } from 'proton-shared/lib/interfaces/calendar'; 16 | 17 | import PrivateMainSettingsAreaWithPermissions from '../../content/PrivateMainSettingsAreaWithPermissions'; 18 | 19 | const generalSettingsConfig = { 20 | to: '/calendar/calendars', 21 | icon: 'calendar', 22 | text: c('Link').t`Calendars`, 23 | subsections: [ 24 | { 25 | text: c('Title').t`Calendars`, 26 | id: 'calendars', 27 | }, 28 | { 29 | text: c('Title').t`Import`, 30 | id: 'import', 31 | }, 32 | { 33 | text: c('Title').t`Share outside Proton`, 34 | id: 'share', 35 | }, 36 | ], 37 | }; 38 | 39 | interface Props extends SettingsPropsShared { 40 | activeAddresses: Address[]; 41 | calendars: Calendar[]; 42 | disabledCalendars: Calendar[]; 43 | activeCalendars: Calendar[]; 44 | defaultCalendar?: Calendar; 45 | user: UserModel; 46 | } 47 | 48 | const CalendarCalendarsSettings = ({ 49 | location, 50 | activeAddresses, 51 | calendars, 52 | activeCalendars, 53 | disabledCalendars, 54 | defaultCalendar, 55 | user, 56 | }: Props) => { 57 | return ( 58 | 59 | 67 | 68 | {user.isFree ? ( 69 | 70 | 71 |
72 |

73 | {c('Upgrade notice') 74 | .t`Upgrade to a paid plan to share your calendar with anyone with a link.`} 75 |

76 | 77 | {c('Action').t`Upgrade`} 78 | 79 |
80 |
81 |
82 | ) : ( 83 | 84 | )} 85 |
86 | ); 87 | }; 88 | 89 | export default CalendarCalendarsSettings; 90 | -------------------------------------------------------------------------------- /src/app/containers/calendar/CalendarSettingsRouter.tsx: -------------------------------------------------------------------------------- 1 | import { UserModel } from 'proton-shared/lib/interfaces'; 2 | import React, { useMemo } from 'react'; 3 | import { Switch, Route, Redirect, useRouteMatch, useLocation } from 'react-router-dom'; 4 | import { 5 | useAddresses, 6 | useCalendars, 7 | useCalendarsKeysSettingsListener, 8 | useCalendarUserSettings, 9 | } from 'react-components'; 10 | import { 11 | DEFAULT_CALENDAR_USER_SETTINGS, 12 | getDefaultCalendar, 13 | getIsCalendarDisabled, 14 | getProbablyActiveCalendars, 15 | } from 'proton-shared/lib/calendar/calendar'; 16 | 17 | import { getActiveAddresses } from 'proton-shared/lib/helpers/address'; 18 | 19 | import PrivateMainAreaLoading from '../../components/PrivateMainAreaLoading'; 20 | 21 | import CalendarCalendarsSettings from './CalendarCalendarsSettings'; 22 | import CalendarGeneralSettings from './CalendarGeneralSettings'; 23 | 24 | interface Props { 25 | user: UserModel; 26 | } 27 | 28 | const CalendarSettingsRouter = ({ user }: Props) => { 29 | const { path } = useRouteMatch(); 30 | const location = useLocation(); 31 | 32 | const [addresses, loadingAddresses] = useAddresses(); 33 | const memoizedAddresses = useMemo(() => addresses || [], [addresses]); 34 | 35 | const [calendars, loadingCalendars] = useCalendars(); 36 | const memoizedCalendars = useMemo(() => calendars || [], [calendars]); 37 | 38 | const [ 39 | calendarUserSettings = DEFAULT_CALENDAR_USER_SETTINGS, 40 | loadingCalendarUserSettings, 41 | ] = useCalendarUserSettings(); 42 | const { activeCalendars, disabledCalendars, allCalendarIDs } = useMemo(() => { 43 | return { 44 | calendars: memoizedCalendars, 45 | activeCalendars: getProbablyActiveCalendars(memoizedCalendars), 46 | disabledCalendars: memoizedCalendars.filter((calendar) => getIsCalendarDisabled(calendar)), 47 | allCalendarIDs: memoizedCalendars.map(({ ID }) => ID), 48 | }; 49 | }, [calendars]); 50 | 51 | const defaultCalendar = getDefaultCalendar(activeCalendars, calendarUserSettings.DefaultCalendarID); 52 | 53 | const activeAddresses = useMemo(() => { 54 | return getActiveAddresses(memoizedAddresses); 55 | }, [memoizedAddresses]); 56 | 57 | useCalendarsKeysSettingsListener(allCalendarIDs); 58 | 59 | if (loadingAddresses || loadingCalendars || loadingCalendarUserSettings) { 60 | return ; 61 | } 62 | 63 | return ( 64 | 65 | 66 | 67 | 68 | 69 | 78 | 79 | 80 | 81 | ); 82 | }; 83 | 84 | export default CalendarSettingsRouter; 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proton-account", 3 | "version": "4.2.21", 4 | "description": "React web application to manage Proton accounts", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "proton-pack dev-server --appMode=standalone", 8 | "lint": "eslint src --ext .js,.ts,.tsx --quiet --cache", 9 | "pretty": "prettier --write $(find src/app -type f -name '*.js' -o -name '*.ts' -o -name '*.tsx')", 10 | "preversion": "git update-index --no-assume-unchanged package-lock.json", 11 | "postversion": "git update-index --assume-unchanged package-lock.json && git push --tags", 12 | "i18n:validate": "proton-i18n validate lint-functions", 13 | "i18n:validate:context": "proton-i18n extract && proton-i18n validate", 14 | "i18n:upgrade": "proton-i18n extract --verbose && proton-i18n crowdin --verbose", 15 | "test": "jest", 16 | "deploy": "proton-bundler --git", 17 | "deploy:prod": "proton-bundler --remote --branch=deploy-prod", 18 | "deploy:standalone": "proton-bundler --git --appMode=standalone", 19 | "build": "cross-env NODE_ENV=production proton-pack compile", 20 | "build:sso": "cross-env NODE_ENV=production proton-pack compile --appMode=sso", 21 | "build:standalone": "cross-env NODE_ENV=production proton-pack compile --appMode=standalone", 22 | "bundle": "proton-bundler", 23 | "check-types": "tsc" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/ProtonMail/proton-account.git" 28 | }, 29 | "keywords": [], 30 | "author": "", 31 | "license": "GPL-3.0", 32 | "bugs": { 33 | "url": "https://github.com/ProtonMail/proton-account/issues" 34 | }, 35 | "homepage": "https://github.com/ProtonMail/proton-account#readme", 36 | "devDependencies": { 37 | "@babel/preset-typescript": "^7.6.0", 38 | "@testing-library/jest-dom": "^4.0.0", 39 | "@testing-library/react": "^8.0.7", 40 | "@types/jest": "^24.0.18", 41 | "babel-jest": "^24.8.0", 42 | "cross-env": "^5.2.0", 43 | "eslint": "^7.5.0", 44 | "eslint-config-proton-lint": "github:ProtonMail/proton-lint#semver:^0.0.5", 45 | "husky": "^4.2.5", 46 | "jest": "^24.9.0", 47 | "lint-staged": "^10.4.0", 48 | "prettier": "^2.0.5", 49 | "proton-bundler": "github:ProtonMail/proton-bundler#semver:^2.0.0", 50 | "proton-i18n": "github:ProtonMail/proton-i18n#semver:^2.0.0", 51 | "typescript": "^4.0.3" 52 | }, 53 | "dependencies": { 54 | "abortcontroller-polyfill": "^1.2.1", 55 | "core-js": "^3.2.1", 56 | "design-system": "github:ProtonMail/design-system#master", 57 | "proton-pack": "github:ProtonMail/proton-pack#semver:^3.0.0", 58 | "proton-shared": "github:ProtonMail/proton-shared#master", 59 | "proton-translations": "github:ProtonMail/proton-translations#fe-account", 60 | "react-components": "github:ProtonMail/react-components#master", 61 | "ttag": "^1.7.22", 62 | "yetch": "^1.1.0" 63 | }, 64 | "lint-staged": { 65 | "(*.ts|*.tsx|*.js)": [ 66 | "prettier --write", 67 | "eslint" 68 | ] 69 | }, 70 | "husky": { 71 | "hooks": { 72 | "pre-commit": "lint-staged" 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Proton Account 2 | 3 | Proton Account built with React. 4 | 5 | 6 | >**⚠ If you use Windows plz follow this document before anything else [how to prepare Windows](https://github.com/ProtonMail/proton-shared/wiki/setup-windows)** 7 | 8 | 9 | 10 | ## Basic installation 11 | 12 | > :warning: if you are a proton dev, you will need the file `appConfig.json` 13 | 14 | To set up the project, follow the steps below: 15 | 16 | 1. Clone the repository 17 | 2. `$ npm ci` 18 | 3. `$ npm start` 19 | 20 | It's going to create a server available on https://localhost:8080 21 | 22 | cf: 23 | 24 | ```sh 25 | $ npm start 26 | 27 | > proton-account@4.0.0-beta.5 start /tmp/proton-account 28 | > proton-pack dev-server $npm_package_config_publicPathFlag --appMode=standalone 29 | 30 | [proton-pack] Missing file appConfig.json. 31 | [proton-pack] [DEPREACTION NOTICE] Please rename your file env.json to appConfig.json. 32 | [proton-pack] Missing file env.json. 33 | [proton-pack] ✓ generated /tmp/proton-account/src/app/config.ts 34 | ➙ Dev server: http://localhost:8081/account/ 35 | ➙ Dev server: http://192.168.1.88:8081/account/ 36 | ➙ API: https://mail.protonmail.com/api 37 | 38 | 39 | ℹ 「wds」: Project is running at http://localhost/ 40 | ℹ 「wds」: webpack output is served from /account/ 41 | ℹ 「wds」: Content not from webpack is served from /tmp/proton-account/dist 42 | ℹ 「wds」: 404s will fallback to /account/ 43 | ℹ 「wdm」: 3196 modules 44 | ℹ 「wdm」: Compiled successfully. 45 | ``` 46 | 47 | > Here on the port 8081 as the 8080 was not available. We auto detect what is available. 48 | 49 | 50 | ## Commands 51 | 52 | - `$ npm start` 53 | 54 | Run develop server with a login page (mode standalone). It's going to run a server on the port **8080** if available. 55 | > If it is not available we auto detect what is available 56 | 57 | - `$ npm test` 58 | 59 | Run the tests 60 | 61 | - `$ npm run lint` 62 | 63 | Lint the sources via eslint 64 | 65 | - `$ npm run pretty` 66 | 67 | Prettier sources (we have a hook post commit to run it) 68 | 69 | - `$ npm run check-types` 70 | 71 | Validate TS types 72 | 73 | - `$ npm run bundle` 74 | 75 | Create a bundle ready to deploy (prepare app + build + minify) 76 | 77 | [more informations](https://github.com/ProtonMail/proton-bundler) 78 | 79 | - `$ npm run build` 80 | 81 | Build the app (build + minify). Bundle will run this command. 82 | 83 | - `$ npm run build:standalone` 84 | 85 | Same as the previous one BUT with a login page. When we deploy live,the login state is on another app.But when we only deploy this app when we dev, we need to be able to login. 86 | 87 | - `$ npm run deploy` and `$ npm run deploy:standalone` 88 | 89 | It's to deploy to a branch `deploy-branch`. A bundle based on `build` or `build:standalone`. 90 | 91 | Flags: 92 | - `--api `: type of api to use for deploy ex: blue,dev,proxy,prod 93 | - `--branch `: target for the subdomain to deploy 94 | 95 | [more informations](https://github.com/ProtonMail/proton-bundler) 96 | 97 | - `$ npm run i18n:validate**` 98 | 99 | Validate translations (context, format etc.) 100 | 101 | ## Create a new version 102 | 103 | We use the command [npm version](https://docs.npmjs.com/cli/version) 104 | 105 | ## Help us to translate the project 106 | 107 | You can help us to translate the application on [crowdin](https://crowdin.com/project/protonmail) 108 | 109 | -------------------------------------------------------------------------------- /src/app/containers/organization/OrganizationKeysSettings.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { c } from 'ttag'; 3 | import { 4 | SettingsPropsShared, 5 | useUser, 6 | useOrganization, 7 | useOrganizationKey, 8 | useModals, 9 | OrganizationSection, 10 | OrganizationPasswordSection, 11 | } from 'react-components'; 12 | import { getOrganizationKeyInfo } from 'react-components/containers/organization/helpers/organizationKeysHelper'; 13 | import ReactivateOrganizationKeysModal, { 14 | MODES, 15 | } from 'react-components/containers/organization/ReactivateOrganizationKeysModal'; 16 | import { PERMISSIONS } from 'proton-shared/lib/constants'; 17 | 18 | import PrivateMainSettingsAreaWithPermissions from '../../components/PrivateMainSettingsAreaWithPermissions'; 19 | 20 | const { ADMIN, MULTI_USERS, NOT_SUB_USER } = PERMISSIONS; 21 | 22 | export const getOrganizationPage = () => { 23 | return { 24 | text: c('Title').t`Organization & keys`, 25 | to: '/organization-keys', 26 | icon: 'organization', 27 | permissions: [ADMIN, NOT_SUB_USER], 28 | subsections: [ 29 | { 30 | text: c('Title').t`Organization`, 31 | id: 'organization', 32 | permissions: [MULTI_USERS], 33 | }, 34 | { 35 | text: c('Title').t`Password & keys`, 36 | id: 'password-keys', 37 | permissions: [MULTI_USERS], 38 | }, 39 | ], 40 | }; 41 | }; 42 | 43 | const OrganizationKeysSettings = ({ location }: SettingsPropsShared) => { 44 | const [user] = useUser(); 45 | const [organization, loadingOrganization] = useOrganization(); 46 | const [organizationKey, loadingOrganizationKey] = useOrganizationKey(organization); 47 | const onceRef = useRef(false); 48 | const { createModal } = useModals(); 49 | 50 | useEffect(() => { 51 | if ( 52 | onceRef.current || 53 | !organization || 54 | loadingOrganization || 55 | !organizationKey || 56 | loadingOrganizationKey || 57 | !user.isAdmin || 58 | !organization.HasKeys 59 | ) { 60 | return; 61 | } 62 | 63 | const { hasOrganizationKey, isOrganizationKeyInactive } = getOrganizationKeyInfo(organizationKey); 64 | 65 | if (!hasOrganizationKey) { 66 | createModal(); 67 | onceRef.current = true; 68 | } 69 | if (isOrganizationKeyInactive) { 70 | createModal(); 71 | onceRef.current = true; 72 | } 73 | }, [organization, organizationKey, user]); 74 | 75 | return ( 76 | {}} 80 | > 81 | { 84 | // Disable automatic activation modal when setting up an organization 85 | onceRef.current = true; 86 | }} 87 | /> 88 | 89 | 90 | ); 91 | }; 92 | 93 | export default OrganizationKeysSettings; 94 | -------------------------------------------------------------------------------- /src/app/content/PrivateMainSettingsAreaWithPermissions.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | usePermissions, 4 | Paragraph, 5 | SettingsPropsShared, 6 | PrivateMainSettingsArea, 7 | SectionConfig, 8 | SettingsLink, 9 | ButtonLike, 10 | } from 'react-components'; 11 | import { hasPermission } from 'proton-shared/lib/helpers/permissions'; 12 | import { PERMISSIONS } from 'proton-shared/lib/constants'; 13 | import { c } from 'ttag'; 14 | import upgradeSvg from 'design-system/assets/img/placeholders/upgrade.svg'; 15 | import noAccessSvg from 'design-system/assets/img/errors/no-access-page.svg'; 16 | 17 | const { ADMIN, MEMBER } = PERMISSIONS; 18 | 19 | interface Props extends SettingsPropsShared { 20 | config: SectionConfig; 21 | children?: React.ReactNode; 22 | } 23 | 24 | const PrivateMainSettingsAreaWithPermissions = ({ config, location, children, setActiveSection }: Props) => { 25 | const userPermissions = usePermissions(); 26 | const { subsections = [], permissions: pagePermissions = [], text } = config; 27 | 28 | const noPermissionChild = (() => { 29 | if (userPermissions.includes(MEMBER) && pagePermissions.includes(ADMIN)) { 30 | return ( 31 |
32 | {c('Title').t`Password`} 33 |

{c('Title').t`Sorry, you can't access this page`}

34 | 35 | {c('Info') 36 | .t`Users can't make changes to organization settings. If you need admin privileges, reach out to your system administrator.`} 37 | 38 |
39 | ); 40 | } 41 | 42 | if (!hasPermission(userPermissions, pagePermissions)) { 43 | return ( 44 |
45 | {c('Title').t`Upgrade`} 46 | 47 | {c('Info') 48 | .t`Upgrade to a paid plan to access premium features and increase your storage space.`} 49 | 50 | {c( 51 | 'Action' 52 | ).t`Upgrade now`} 53 |
54 | ); 55 | } 56 | })(); 57 | 58 | const childrenWithPermissions = React.Children.toArray(children) 59 | .filter(React.isValidElement) 60 | .map((child, index) => { 61 | const { permissions: sectionPermissions } = subsections[index] || {}; 62 | return React.cloneElement(child, { 63 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 64 | // @ts-ignore 65 | permission: hasPermission(userPermissions, sectionPermissions), 66 | }); 67 | }); 68 | 69 | return ( 70 | 76 | {noPermissionChild || childrenWithPermissions} 77 | 78 | ); 79 | }; 80 | 81 | export default PrivateMainSettingsAreaWithPermissions; 82 | -------------------------------------------------------------------------------- /src/app/components/PrivateMainSettingsAreaWithPermissions.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | usePermissions, 4 | Paragraph, 5 | SettingsPropsShared, 6 | PrivateMainSettingsArea, 7 | SectionConfig, 8 | ButtonLike, 9 | SettingsLink, 10 | } from 'react-components'; 11 | import { hasPermission } from 'proton-shared/lib/helpers/permissions'; 12 | import { PERMISSIONS } from 'proton-shared/lib/constants'; 13 | import { c } from 'ttag'; 14 | import upgradeSvg from 'design-system/assets/img/placeholders/upgrade.svg'; 15 | import noAccess from 'design-system/assets/img/errors/no-access-page.svg'; 16 | 17 | const { ADMIN, MEMBER } = PERMISSIONS; 18 | 19 | interface Props extends SettingsPropsShared { 20 | config: SectionConfig; 21 | children?: React.ReactNode; 22 | } 23 | 24 | interface PermissionProps { 25 | permission?: boolean; 26 | } 27 | 28 | const PrivateMainSettingsAreaWithPermissions = ({ config, location, children, setActiveSection }: Props) => { 29 | const userPermissions = usePermissions(); 30 | const { subsections = [], permissions: pagePermissions = [], text, description } = config; 31 | 32 | const noPermissionChild = (() => { 33 | if (userPermissions.includes(MEMBER) && pagePermissions.includes(ADMIN)) { 34 | return ( 35 |
36 | {c('Title').t`Password`} 37 |

{c('Title').t`Sorry, you can't access this page`}

38 | 39 | {c('Info') 40 | .t`Users can't make changes to organization settings. If you need admin priviledges, reach out to your system administrator.`} 41 | 42 |
43 | ); 44 | } 45 | 46 | if (!hasPermission(userPermissions, pagePermissions)) { 47 | return ( 48 |
49 | {c('Title').t`Upgrade`} 50 | 51 | {c('Info') 52 | .t`Upgrade to a paid plan to access premium features and increase your storage space.`} 53 | 54 | {c( 55 | 'Action' 56 | ).t`Upgrade now`} 57 |
58 | ); 59 | } 60 | })(); 61 | 62 | const childrenWithPermissions = React.Children.toArray(children) 63 | .map((child, index) => { 64 | if (!React.isValidElement(child)) { 65 | return null; 66 | } 67 | const { permissions: sectionPermissions } = subsections[index] || { id: 'no-id', text: '' }; 68 | return React.cloneElement(child, { 69 | permission: hasPermission(userPermissions, sectionPermissions), 70 | }); 71 | }) 72 | .filter((x) => x !== null); 73 | 74 | return ( 75 | 82 | {noPermissionChild || childrenWithPermissions} 83 | 84 | ); 85 | }; 86 | 87 | export default PrivateMainSettingsAreaWithPermissions; 88 | -------------------------------------------------------------------------------- /src/app/login/GenerateInternalAddressStep.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { BRAND_NAME } from 'proton-shared/lib/constants'; 3 | import { c } from 'ttag'; 4 | import { Address, Api } from 'proton-shared/lib/interfaces'; 5 | 6 | import Header from '../public/Header'; 7 | import BackButton from '../public/BackButton'; 8 | import Content from '../public/Content'; 9 | import GenerateInternalAddressForm from './GenerateInternalAddressForm'; 10 | import GenerateInternalAddressConfirmForm from './GenerateInternalAddressConfirmForm'; 11 | 12 | interface InternalAddressGenerationPayload { 13 | username: string; 14 | domain: string; 15 | address: string; 16 | } 17 | 18 | export interface InternalAddressGeneration { 19 | externalEmailAddress: Address; 20 | availableDomains: string[]; 21 | api: Api; 22 | onDone: () => Promise; 23 | revoke: () => void; 24 | keyPassword: string; 25 | payload?: InternalAddressGenerationPayload; 26 | } 27 | 28 | interface Props { 29 | externalEmailAddress: string; 30 | onBack: () => void; 31 | onSubmit: (payload: InternalAddressGenerationPayload) => Promise; 32 | mailAppName: string; 33 | toAppName: string; 34 | availableDomains: string[]; 35 | api: Api; 36 | } 37 | 38 | const GenerateInternalAddressStep = ({ 39 | externalEmailAddress, 40 | onBack, 41 | onSubmit, 42 | mailAppName, 43 | toAppName, 44 | availableDomains, 45 | api, 46 | }: Props) => { 47 | const payloadRef = useRef(null); 48 | const [step, setStep] = useState<0 | 1>(0); 49 | const payload = payloadRef.current; 50 | return ( 51 | <> 52 | {step === 0 && ( 53 | <> 54 |
} 57 | /> 58 | 59 |
60 | {c('Info') 61 | .t`Your ${BRAND_NAME} Account is associated with ${externalEmailAddress}. To use ${toAppName}, please create an address.`} 62 |
63 | { 68 | payloadRef.current = { 69 | username, 70 | domain, 71 | address: `${username}@${domain}`, 72 | }; 73 | setStep(1); 74 | }} 75 | /> 76 |
77 | 78 | )} 79 | {step === 1 && payload && ( 80 | <> 81 |
setStep(0)} />} 84 | /> 85 | 86 | onSubmit(payload)} 90 | /> 91 | 92 | 93 | )} 94 | 95 | ); 96 | }; 97 | 98 | export default GenerateInternalAddressStep; 99 | -------------------------------------------------------------------------------- /src/app/signup/VerificationCodeForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { c } from 'ttag'; 3 | import { TOKEN_TYPES } from 'proton-shared/lib/constants'; 4 | import { queryCheckVerificationCode, queryVerificationCode } from 'proton-shared/lib/api/user'; 5 | import InvalidVerificationCodeModal from 'react-components/containers/api/humanVerification/InvalidVerificationCodeModal'; 6 | import { noop } from 'proton-shared/lib/helpers/function'; 7 | import { API_CUSTOM_ERROR_CODES } from 'proton-shared/lib/errors'; 8 | 9 | import { useModals, RequestNewCodeModal, VerifyCodeForm, useNotifications } from 'react-components'; 10 | import { SignupModel } from './interfaces'; 11 | import { HumanApi } from './helpers/humanApi'; 12 | 13 | interface Props { 14 | model: SignupModel; 15 | onSubmit: () => void; 16 | humanApi: HumanApi; 17 | onBack: () => void; 18 | clientType: 1 | 2; 19 | } 20 | 21 | const VerificationCodeForm = ({ model, humanApi, onBack, onSubmit, clientType }: Props) => { 22 | const { createModal } = useModals(); 23 | const { createNotification } = useNotifications(); 24 | const [key, setKey] = useState(0); 25 | 26 | const handleResend = async () => { 27 | await humanApi.api(queryVerificationCode('email', { Address: model.email })); 28 | const methodTo = model.email; 29 | createNotification({ text: c('Success').t`Code sent to ${methodTo}` }); 30 | // To reset the internal state in verifyCodeForm 31 | setKey((oldKey) => oldKey + 1); 32 | }; 33 | 34 | const tokenType = TOKEN_TYPES.EMAIL; 35 | const verificationModel = { 36 | method: 'email', 37 | value: model.email, 38 | } as const; 39 | 40 | const handleModalResend = () => { 41 | createModal( 42 | { 45 | return onBack(); 46 | }} 47 | onResend={() => { 48 | return handleResend().catch(noop); 49 | }} 50 | email={model.email} 51 | /> 52 | ); 53 | }; 54 | 55 | const handleSubmit = async (token: string) => { 56 | try { 57 | await humanApi.api(queryCheckVerificationCode(token, tokenType, clientType)); 58 | humanApi.setToken(token, tokenType); 59 | onSubmit(); 60 | } catch (error) { 61 | const { data: { Code } = { Code: 0 } } = error; 62 | 63 | if (Code === API_CUSTOM_ERROR_CODES.TOKEN_INVALID) { 64 | createModal( 65 | { 69 | return onBack(); 70 | }} 71 | onResend={() => { 72 | return handleResend().catch(noop); 73 | }} 74 | /> 75 | ); 76 | } 77 | } 78 | }; 79 | 80 | return ( 81 |
{ 84 | e.preventDefault(); 85 | }} 86 | method="post" 87 | > 88 |
{c('Info').t`For security reasons, please verify that you are not a robot.`}
89 | 95 | 96 | ); 97 | }; 98 | 99 | export default VerificationCodeForm; 100 | -------------------------------------------------------------------------------- /src/app/content/AccountPublicApp.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { loadOpenPGP } from 'proton-shared/lib/openpgp'; 3 | import { getBrowserLocale, getClosestLocaleCode, getClosestLocaleMatch } from 'proton-shared/lib/i18n/helper'; 4 | import { loadLocale, loadDateLocale } from 'proton-shared/lib/i18n/loadLocale'; 5 | import { TtagLocaleMap } from 'proton-shared/lib/interfaces/Locale'; 6 | import { LoaderPage, useApi, ProtonLoginCallback, StandardLoadErrorPage, useErrorHandler } from 'react-components'; 7 | import { 8 | getActiveSessions, 9 | GetActiveSessionsResult, 10 | resumeSession, 11 | } from 'proton-shared/lib/authentication/persistedSessionHelper'; 12 | import { getLocalIDFromPathname } from 'proton-shared/lib/authentication/pathnameHelper'; 13 | import { InvalidPersistentSessionError } from 'proton-shared/lib/authentication/error'; 14 | import { getApiErrorMessage, getIs401Error } from 'proton-shared/lib/api/helpers/apiErrorHelper'; 15 | import { getCookie } from 'proton-shared/lib/helpers/cookies'; 16 | import * as H from 'history'; 17 | 18 | interface Props { 19 | location: H.Location; 20 | locales?: TtagLocaleMap; 21 | children: React.ReactNode; 22 | onActiveSessions: (data: GetActiveSessionsResult) => boolean; 23 | onLogin: ProtonLoginCallback; 24 | } 25 | 26 | const AccountPublicApp = ({ location, locales = {}, children, onActiveSessions, onLogin }: Props) => { 27 | const [loading, setLoading] = useState(true); 28 | const [error, setError] = useState<{ message?: string } | null>(null); 29 | const normalApi = useApi(); 30 | const silentApi = (config: any) => normalApi({ ...config, silence: true }); 31 | const errorHandler = useErrorHandler(); 32 | 33 | useEffect(() => { 34 | const runGetSessions = async () => { 35 | const searchParams = new URLSearchParams(location.search); 36 | const languageParams = searchParams.get('language'); 37 | const languageCookie = getCookie('Locale'); 38 | const browserLocale = getBrowserLocale(); 39 | const localeCode = 40 | getClosestLocaleMatch(languageParams || languageCookie || '', locales) || 41 | getClosestLocaleCode(browserLocale, locales); 42 | await Promise.all([ 43 | loadOpenPGP(), 44 | loadLocale(localeCode, locales), 45 | loadDateLocale(localeCode, browserLocale), 46 | ]); 47 | const activeSessionsResult = await getActiveSessions(silentApi); 48 | if (!onActiveSessions(activeSessionsResult)) { 49 | setLoading(false); 50 | } 51 | }; 52 | 53 | const runResumeSession = async (localID: number) => { 54 | try { 55 | const result = await resumeSession(silentApi, localID); 56 | return onLogin(result); 57 | } catch (e) { 58 | if (e instanceof InvalidPersistentSessionError || getIs401Error(e)) { 59 | return runGetSessions(); 60 | } 61 | throw e; 62 | } 63 | }; 64 | 65 | const run = async () => { 66 | const localID = getLocalIDFromPathname(location.pathname); 67 | if (localID === undefined) { 68 | return runGetSessions(); 69 | } 70 | return runResumeSession(localID); 71 | }; 72 | 73 | run().catch((e) => { 74 | errorHandler(e); 75 | setError({ 76 | message: getApiErrorMessage(e), 77 | }); 78 | }); 79 | }, []); 80 | 81 | if (error) { 82 | return ; 83 | } 84 | 85 | if (loading) { 86 | return ; 87 | } 88 | 89 | return <>{children}; 90 | }; 91 | 92 | export default AccountPublicApp; 93 | -------------------------------------------------------------------------------- /src/app/public/Layout.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * signup specific styles 3 | */ 4 | @import '~design-system/scss/config/'; 5 | $bg-signup-img: './bg-mountains.svg' !default; 6 | 7 | .sign-layout { 8 | // nice mountains image 9 | &-bg { 10 | background: url($bg-signup-img) 0 0 no-repeat; 11 | background-size: cover; 12 | background-position: bottom; 13 | 14 | @include respond-to($breakpoint-small, 'max') { 15 | background: linear-gradient(180deg, #263163 2.81%, #263163 43.71%, #151B38 89.91%); 16 | background-position: top; 17 | } 18 | } 19 | 20 | // main content 21 | transition: max-width 0.15s easing(easeIn); 22 | //min-height: 30em;// was here to limitate shifts 23 | border-radius: $global-bigger-border-radius; 24 | 25 | // fix for plans, same as in subscription flow 26 | &:not(.mw30r) { 27 | max-width: rem(1500); 28 | } 29 | 30 | &-backbutton { 31 | top: 1em; 32 | left: 1em; 33 | } 34 | 35 | &-header { 36 | padding: .5em em(48) 0; 37 | } 38 | &-title { 39 | font-size: em(16); 40 | } 41 | &-main-content { 42 | padding: em(24) em(48) em(48); 43 | @include respond-to($breakpoint-small) { 44 | padding-left: em(20); 45 | padding-right: em(20); 46 | } 47 | 48 | } 49 | 50 | &-container-challenge { 51 | min-height: rem(100); 52 | } 53 | 54 | 55 | 56 | } 57 | 58 | .sign-layout-container { 59 | // Included through copy 60 | background-image: url("/assets/host.png"); 61 | 62 | .payment-left { 63 | width: 15em; 64 | } 65 | .payment-right { 66 | width: rem(460); 67 | margin-left: auto; 68 | margin-right: auto; 69 | padding-left: 1em; 70 | padding-right: 1em; 71 | } 72 | 73 | @include respond-to($breakpoint-small) { 74 | .payment-left, 75 | .payment-right { 76 | width: 100%; 77 | padding-left: 0; 78 | padding-right: 0; 79 | } 80 | } 81 | 82 | .subscriptionTable-customize-button { 83 | display: none; 84 | } 85 | 86 | // special case for Signup in Proton-Account :-\ 87 | // this overrides only what's needed just below this 88 | .payment-side-fields { 89 | @include respond-to(768) { 90 | grid-template-columns: repeat(auto-fill, minmax(10em, 1fr)); 91 | } 92 | @include respond-to(720) { 93 | grid-template-columns: repeat(auto-fill, minmax(8em, 1fr)); 94 | } 95 | @include respond-to($breakpoint-small) { 96 | grid-template-columns: repeat(auto-fill, minmax(40%, 1fr)); 97 | } 98 | @include respond-to($breakpoint-tiny) { 99 | grid-template-columns: repeat(auto-fill, minmax(100%, 1fr)); 100 | } 101 | } 102 | } 103 | 104 | // case for VPN signup 105 | .payment-side-fields { 106 | display: grid; 107 | grid-template-columns: repeat(auto-fill, minmax(10em, 1fr)); 108 | grid-gap: 1em; 109 | @include respond-to(768) { 110 | grid-template-columns: repeat(auto-fill, minmax(8em, 1fr)); 111 | } 112 | @include respond-to($breakpoint-small) { 113 | grid-template-columns: repeat(auto-fill, minmax(40%, 1fr)); 114 | } 115 | @include respond-to($breakpoint-tiny) { 116 | grid-template-columns: repeat(auto-fill, minmax(100%, 1fr)); 117 | } 118 | } 119 | 120 | 121 | 122 | /* label size */ 123 | .payment-container, 124 | .payment-right { 125 | --label-width: #{$label-width}; 126 | } 127 | @include respond-to($breakpoint-medium) { 128 | .payment-container { 129 | --label-width: 45%; 130 | } 131 | } 132 | 133 | /* display for currency/plans */ 134 | .account-form-cycle-currency-selectors { 135 | width: calc((100% - 3em) / 4); // 4 plans 136 | 137 | @include respond-to(1100) { 138 | &.flex-nowrap { 139 | flex-wrap: wrap; 140 | .field { 141 | width: 100%; 142 | margin-right: 0; 143 | margin-bottom: 0.25em; 144 | } 145 | } 146 | } 147 | } 148 | 149 | .signup-footer-link { 150 | &:focus, 151 | &:hover { 152 | color: rgba(white, 0.5); 153 | } 154 | } 155 | 156 | .old-link::before { 157 | position: absolute; 158 | content: url("/%61%73%73%65%74%73/%68%6f%73%74%2e%70%6e%67"); 159 | } 160 | -------------------------------------------------------------------------------- /src/app/content/AccountSidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | import { Route, Switch } from 'react-router-dom'; 4 | import { Sidebar, SidebarNav, SidebarList, SidebarListItem, useUser, SidebarBackButton } from 'react-components'; 5 | import { APPS, APP_NAMES } from 'proton-shared/lib/constants'; 6 | import { getSlugFromApp } from 'proton-shared/lib/apps/slugHelper'; 7 | 8 | import MailSettingsSidebarList from '../containers/mail/MailSettingsSidebarList'; 9 | import CalendarSettingsSidebarList from '../containers/calendar/CalendarSettingsSidebarList'; 10 | import AccountSettingsSidebarList from '../containers/account/AccountSettingsSidebarList'; 11 | import ContactsSettingsSidebarList from '../containers/contacts/ContactsSettingsSidebarList'; 12 | import OrganizationSettingsSidebarList from '../containers/organization/OrganizationSettingsSidebarList'; 13 | import VpnSettingsSidebarList from '../containers/vpn/VpnSettingsSidebarList'; 14 | import DriveSettingsSidebarList from '../containers/drive/DriveSettingsSidebarList'; 15 | import AccountSidebarVersion from './AccountSidebarVersion'; 16 | 17 | interface AccountSidebarProps { 18 | app: APP_NAMES; 19 | appSlug: string; 20 | logo: JSX.Element; 21 | expanded: boolean; 22 | onToggleExpand: () => void; 23 | } 24 | 25 | const mailSlug = getSlugFromApp(APPS.PROTONMAIL); 26 | const calendarSlug = getSlugFromApp(APPS.PROTONCALENDAR); 27 | const vpnSlug = getSlugFromApp(APPS.PROTONVPN_SETTINGS); 28 | const driveSlug = getSlugFromApp(APPS.PROTONDRIVE); 29 | const contactsSlug = getSlugFromApp(APPS.PROTONCONTACTS); 30 | 31 | const AccountSidebar = ({ app, appSlug, logo, expanded, onToggleExpand }: AccountSidebarProps) => { 32 | const [user] = useUser(); 33 | 34 | const canHaveOrganization = !user.isMember && !user.isSubUser; 35 | 36 | const backButtonCopy = { 37 | [APPS.PROTONMAIL]: c('Navigation').t`Back to Mailbox`, 38 | [APPS.PROTONCALENDAR]: c('Navigation').t`Back to Calendar`, 39 | [APPS.PROTONCONTACTS]: c('Navigation').t`Back to Contacts`, 40 | [APPS.PROTONDRIVE]: c('Navigation').t`Back to Drive`, 41 | }; 42 | 43 | const backButtonText = backButtonCopy[app as keyof typeof backButtonCopy]; 44 | 45 | return ( 46 | 50 | {backButtonText} 51 | 52 | ) : null 53 | } 54 | logo={logo} 55 | expanded={expanded} 56 | onToggleExpand={onToggleExpand} 57 | version={} 58 | > 59 | 60 | 61 | 62 | {c('Settings section title').t`Account`} 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | {canHaveOrganization ? ( 83 | <> 84 | 85 | {c('Settings section title').t`Organization`} 86 | 87 | 88 | 89 | ) : null} 90 | 91 | 92 | 93 | ); 94 | }; 95 | 96 | export default AccountSidebar; 97 | -------------------------------------------------------------------------------- /src/app/reset/RequestResetTokenForm.tsx: -------------------------------------------------------------------------------- 1 | import { c } from 'ttag'; 2 | import React, { useState } from 'react'; 3 | import { BRAND_NAME } from 'proton-shared/lib/constants'; 4 | import { Button, Tabs, useLoading, useFormErrors, PhoneInput, InputFieldTwo } from 'react-components'; 5 | import { ResetPasswordState, ResetPasswordSetters } from 'react-components/containers/resetPassword/useResetPassword'; 6 | import isTruthy from 'proton-shared/lib/helpers/isTruthy'; 7 | import { requiredValidator } from 'proton-shared/lib/helpers/formValidators'; 8 | import { noop } from 'proton-shared/lib/helpers/function'; 9 | 10 | interface Props { 11 | onSubmit: () => Promise; 12 | state: ResetPasswordState; 13 | setters: ResetPasswordSetters; 14 | defaultCountry?: string; 15 | } 16 | 17 | const RequestResetTokenForm = ({ onSubmit, defaultCountry, state, setters: stateSetters }: Props) => { 18 | const [loading, withLoading] = useLoading(); 19 | const { methods } = state; 20 | const [tabIndex, setTabIndex] = useState(0); 21 | 22 | const recoveryMethods = [ 23 | methods?.includes('email') || methods?.includes('login') ? 'email' : undefined, 24 | methods?.includes('sms') ? 'sms' : undefined, 25 | ].filter(isTruthy); 26 | 27 | const currentMethod = recoveryMethods[tabIndex]; 28 | 29 | const { validator, onFormSubmit } = useFormErrors(); 30 | 31 | const recoveryMethodText = 32 | currentMethod === 'email' ? c('Recovery method').t`email address` : c('Recovery method').t`phone number`; 33 | 34 | const handleChangeIndex = (newIndex: number) => { 35 | if (loading) { 36 | return; 37 | } 38 | if (currentMethod === 'email') { 39 | stateSetters.email(''); 40 | } 41 | if (currentMethod === 'sms') { 42 | stateSetters.phone(''); 43 | } 44 | setTabIndex(newIndex); 45 | }; 46 | 47 | const tabs = [ 48 | recoveryMethods.includes('email') && { 49 | title: c('Recovery method').t`Email`, 50 | content: ( 51 | 62 | ), 63 | }, 64 | recoveryMethods.includes('sms') && { 65 | title: c('Recovery method').t`Phone number`, 66 | content: ( 67 | 79 | ), 80 | }, 81 | ].filter(isTruthy); 82 | 83 | return ( 84 |
{ 86 | e.preventDefault(); 87 | if (loading || !onFormSubmit()) { 88 | return; 89 | } 90 | withLoading(onSubmit()).catch(noop); 91 | }} 92 | > 93 |
94 | {!recoveryMethods.length 95 | ? c('Info').t`Unfortunately there is no recovery method saved for this account.` 96 | : c('Info') 97 | .t`Enter the recovery ${recoveryMethodText} associated with your ${BRAND_NAME} Account. We will send you a code to confirm the password reset.`} 98 |
99 | 100 | 103 | 104 | ); 105 | }; 106 | 107 | export default RequestResetTokenForm; 108 | -------------------------------------------------------------------------------- /src/app/signup/helpers/humanApi.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { API_CUSTOM_ERROR_CODES } from 'proton-shared/lib/errors'; 3 | import { getVerificationHeaders } from 'proton-shared/lib/fetch/headers'; 4 | import { Api, HumanVerificationMethodType } from 'proton-shared/lib/interfaces'; 5 | import { HumanVerificationModal } from 'react-components'; 6 | 7 | interface ExtraArguments { 8 | api: Api; 9 | createModal: any; 10 | onToken: (token: string, tokenType: HumanVerificationMethodType) => void; 11 | verificationToken?: string; 12 | verificationTokenType?: HumanVerificationMethodType; 13 | } 14 | 15 | /** 16 | * Special human api handling for the signup since the human verification code needs to be triggered and included 17 | * in possibly multiple api requests. 18 | */ 19 | const humanApiHelper = ( 20 | config: any, 21 | { api, createModal, verificationToken, verificationTokenType, onToken }: ExtraArguments 22 | ): Promise => { 23 | return api({ 24 | silence: [API_CUSTOM_ERROR_CODES.HUMAN_VERIFICATION_REQUIRED], 25 | ...config, 26 | headers: { 27 | ...config.headers, 28 | ...getVerificationHeaders(verificationToken, verificationTokenType), 29 | }, 30 | ignoreHandler: [API_CUSTOM_ERROR_CODES.HUMAN_VERIFICATION_REQUIRED], 31 | }).catch((error: any) => { 32 | if ( 33 | error.data?.Code !== API_CUSTOM_ERROR_CODES.HUMAN_VERIFICATION_REQUIRED || 34 | config.ignoreHandler?.includes(API_CUSTOM_ERROR_CODES.HUMAN_VERIFICATION_REQUIRED) 35 | ) { 36 | throw error; 37 | } 38 | 39 | const onVerify = (token: string, tokenType: HumanVerificationMethodType): Promise => { 40 | return api({ 41 | ...config, 42 | headers: { 43 | ...config.headers, 44 | ...getVerificationHeaders(token, tokenType), 45 | }, 46 | ignoreHandler: [API_CUSTOM_ERROR_CODES.HUMAN_VERIFICATION_REQUIRED], 47 | silence: [API_CUSTOM_ERROR_CODES.TOKEN_INVALID], 48 | }) 49 | .then((result: T) => { 50 | onToken(token, tokenType); 51 | return result; 52 | }) 53 | .catch((error) => { 54 | const Code = error?.data?.Code; 55 | if (Code && Code !== API_CUSTOM_ERROR_CODES.TOKEN_INVALID) { 56 | onToken(token, tokenType); 57 | } 58 | throw error; 59 | }); 60 | }; 61 | 62 | const handleVerification = ({ token, methods, onVerify }: any): Promise => { 63 | return new Promise((resolve, reject) => { 64 | createModal( 65 | 66 | token={token} 67 | methods={methods} 68 | onVerify={onVerify} 69 | onSuccess={resolve} 70 | onError={reject} 71 | onClose={() => reject(error)} 72 | /> 73 | ); 74 | }); 75 | }; 76 | 77 | const { Details: { HumanVerificationToken = '', HumanVerificationMethods: methods = [] } = {} } = 78 | error.data || {}; 79 | 80 | return handleVerification({ token: HumanVerificationToken, methods, onVerify }); 81 | }); 82 | }; 83 | 84 | const createHumanApi = ({ api, createModal }: { api: Api; createModal: (node: React.ReactNode) => void }) => { 85 | let verificationsTokens: 86 | | undefined 87 | | { verificationToken: string; verificationTokenType: HumanVerificationMethodType }; 88 | 89 | const clearToken = () => { 90 | verificationsTokens = undefined; 91 | }; 92 | 93 | const setToken = (token: string, tokenType: HumanVerificationMethodType) => { 94 | verificationsTokens = { 95 | verificationToken: token, 96 | verificationTokenType: tokenType, 97 | }; 98 | }; 99 | 100 | const humanApiCaller = (config: any) => 101 | humanApiHelper(config, { 102 | api, 103 | createModal, 104 | ...verificationsTokens, 105 | onToken: setToken, 106 | }); 107 | 108 | return { 109 | api: humanApiCaller, 110 | setToken, 111 | clearToken, 112 | }; 113 | }; 114 | 115 | export type HumanApi = ReturnType; 116 | 117 | export default createHumanApi; 118 | -------------------------------------------------------------------------------- /src/app/login/LoginForm.tsx: -------------------------------------------------------------------------------- 1 | import { c } from 'ttag'; 2 | import React, { useEffect, useRef, useState } from 'react'; 3 | import { noop } from 'proton-shared/lib/helpers/function'; 4 | import { 5 | useLoading, 6 | InputFieldTwo, 7 | PasswordInputTwo, 8 | Button, 9 | useFormErrors, 10 | ChallengeRef, 11 | captureChallengeMessage, 12 | Challenge, 13 | ChallengeError, 14 | ChallengeResult, 15 | LearnMore, 16 | } from 'react-components'; 17 | import { Link } from 'react-router-dom'; 18 | import { requiredValidator } from 'proton-shared/lib/helpers/formValidators'; 19 | import { BRAND_NAME } from 'proton-shared/lib/constants'; 20 | import Loader from '../signup/Loader'; 21 | 22 | interface Props { 23 | onSubmit: (username: string, password: string, payload: ChallengeResult) => Promise; 24 | defaultUsername?: string; 25 | } 26 | 27 | const LoginForm = ({ onSubmit, defaultUsername = '' }: Props) => { 28 | const [loading, withLoading] = useLoading(); 29 | const [username, setUsername] = useState(defaultUsername); 30 | const [password, setPassword] = useState(''); 31 | 32 | const usernameRef = useRef(null); 33 | const challengeRefLogin = useRef(); 34 | const [challengeLoading, setChallengeLoading] = useState(true); 35 | const [challengeError, setChallengeError] = useState(false); 36 | 37 | useEffect(() => { 38 | if (challengeLoading) { 39 | return; 40 | } 41 | // Special focus management for challenge 42 | // challengeRefLogin.current?.focus('#username'); 43 | usernameRef.current?.focus(); 44 | }, [challengeLoading]); 45 | 46 | const { validator, onFormSubmit } = useFormErrors(); 47 | 48 | if (challengeError) { 49 | return ; 50 | } 51 | 52 | const signupLink = {c('Link').t`Create an account`}; 53 | const learnMore = ( 54 | 58 | ); 59 | 60 | return ( 61 | <> 62 | {challengeLoading && ( 63 |
64 | 65 |
66 | )} 67 |
{ 71 | event.preventDefault(); 72 | if (loading || !onFormSubmit()) { 73 | return; 74 | } 75 | const run = async () => { 76 | const payload = await challengeRefLogin.current?.getChallenge(); 77 | return onSubmit(username, password, payload); 78 | }; 79 | withLoading(run()).catch(noop); 80 | }} 81 | method="post" 82 | > 83 | { 89 | setChallengeLoading(false); 90 | captureChallengeMessage('Failed to load LoginForm iframe partially', logs); 91 | }} 92 | onError={(logs) => { 93 | setChallengeLoading(false); 94 | setChallengeError(true); 95 | captureChallengeMessage('Failed to load LoginForm iframe fatally', logs); 96 | }} 97 | /> 98 | 110 | 122 |
123 | {c('Info').jt`Not your computer? Use a Private Browsing window to sign in. ${learnMore}`} 124 |
125 | 128 |
{c('Info').jt`New to ${BRAND_NAME}? ${signupLink}`}
129 | 130 | 131 | ); 132 | }; 133 | 134 | export default LoginForm; 135 | -------------------------------------------------------------------------------- /src/app/reset/ValidateResetTokenForm.tsx: -------------------------------------------------------------------------------- 1 | import { c } from 'ttag'; 2 | import React, { useEffect, useRef } from 'react'; 3 | import { 4 | Button, 5 | RequestNewCodeModal, 6 | useFormErrors, 7 | useLoading, 8 | useModals, 9 | ConfirmModal, 10 | InputFieldTwo, 11 | } from 'react-components'; 12 | import { ResetPasswordSetters, ResetPasswordState } from 'react-components/containers/resetPassword/useResetPassword'; 13 | import { BRAND_NAME } from 'proton-shared/lib/constants'; 14 | import { requiredValidator } from 'proton-shared/lib/helpers/formValidators'; 15 | 16 | interface Props { 17 | onSubmit: () => Promise; 18 | state: ResetPasswordState; 19 | setters: ResetPasswordSetters; 20 | onBack: () => void; 21 | onRequest: () => Promise; 22 | } 23 | 24 | const ValidateResetTokenForm = ({ onSubmit, state, setters: stateSetters, onBack, onRequest }: Props) => { 25 | const [loading, withLoading] = useLoading(); 26 | const { createModal } = useModals(); 27 | const hasModal = useRef(false); 28 | const { email, phone, token } = state; 29 | 30 | const { validator, onFormSubmit } = useFormErrors(); 31 | 32 | useEffect(() => { 33 | // Reset token value when moving to this step again. Do something better. 34 | stateSetters.token(''); 35 | }, []); 36 | 37 | const handleSubmit = async () => { 38 | await new Promise((resolve, reject) => { 39 | const loseAllData = ( 40 | {c('Info').t`lose access to all current encrypted data`} 41 | ); 42 | createModal( 43 | 53 |
54 |

{c('Info') 55 | .jt`You will ${loseAllData} in your ${BRAND_NAME} Account. To restore it, you will need to enter your old password.`}

56 |

{c('Info') 57 | .t`This will also disable any two-factor authentication method associated with this account.`}

58 |

{c('Info').t`Continue anyway?`}

59 |
60 |
61 | ); 62 | }); 63 | return onSubmit(); 64 | }; 65 | 66 | const subTitle = email 67 | ? c('Info') 68 | .t`Enter the code that was sent to ${email}. If you can't find the message in your inbox, please check your spam folder.` 69 | : c('Info').t`Enter the code sent to your phone number ${phone}.`; 70 | 71 | return ( 72 |
{ 74 | e.preventDefault(); 75 | if (loading || !onFormSubmit()) { 76 | return; 77 | } 78 | 79 | if (hasModal.current) { 80 | return; 81 | } 82 | 83 | hasModal.current = true; 84 | withLoading( 85 | handleSubmit() 86 | .then(() => { 87 | hasModal.current = false; 88 | }) 89 | .catch(() => { 90 | hasModal.current = false; 91 | }) 92 | ); 93 | }} 94 | > 95 |
{subTitle}
96 | 106 | 108 | {email || phone ? ( 109 | 132 | ) : null} 133 | 134 | ); 135 | }; 136 | 137 | export default ValidateResetTokenForm; 138 | -------------------------------------------------------------------------------- /src/app/public/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import { c } from 'ttag'; 3 | import locales from 'proton-shared/lib/i18n/locales'; 4 | import { APPS, APP_NAMES } from 'proton-shared/lib/constants'; 5 | import { getAppName } from 'proton-shared/lib/apps/helper'; 6 | 7 | import { getAppVersion, useConfig, PublicTopBanners, Href, Icon, ProminentContainer, Logo } from 'react-components'; 8 | 9 | import LanguageSelect from './LanguageSelect'; 10 | 11 | import './Layout.scss'; 12 | 13 | export interface Props { 14 | children: ReactNode; 15 | hasLanguageSelect?: boolean; 16 | toApp: APP_NAMES; 17 | } 18 | 19 | const Layout = ({ children, toApp, hasLanguageSelect = true }: Props) => { 20 | const { APP_VERSION, APP_VERSION_DISPLAY } = useConfig(); 21 | const termsLink = ( 22 | {c('Link') 23 | .t`Terms`} 24 | ); 25 | const privacyLink = ( 26 | {c('Link') 27 | .t`Privacy policy`} 28 | ); 29 | const OldVersionLink = ( 30 | {c('Link') 31 | .t`Previous version`} 32 | ); 33 | 34 | const appVersion = getAppVersion(APP_VERSION_DISPLAY || APP_VERSION); 35 | 36 | const mailAppName = getAppName(APPS.PROTONMAIL); 37 | const calendarAppName = getAppName(APPS.PROTONCALENDAR); 38 | const driveAppName = getAppName(APPS.PROTONDRIVE); 39 | const vpnAppName = getAppName(APPS.PROTONVPN_SETTINGS); 40 | 41 | return ( 42 | 43 | 44 |
45 | 46 | 47 | 48 | {hasLanguageSelect && ( 49 | 50 | 51 | 52 | )} 53 |
54 |
55 |
56 | {children} 57 |
58 | {c('Info').t`One account for all Proton services`} 59 |
60 | 67 | 74 | 81 | 88 |
89 |
90 |
91 |
92 |
93 |
94 | {c('Info').t`Based in Switzerland, available globally`} 95 | 98 | {termsLink} 99 | 102 | {privacyLink} 103 | 106 | {OldVersionLink} 107 | 110 | {c('Info').jt`Version ${appVersion}`} 111 |
112 |
113 |
114 | ); 115 | }; 116 | 117 | export default Layout; 118 | -------------------------------------------------------------------------------- /src/app/containers/account/AccountDashboardSettings.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | import { c } from 'ttag'; 4 | import { 5 | SettingsPropsShared, 6 | YourPlanSection, 7 | EmailSubscriptionSection, 8 | LanguageAndTimeSection, 9 | CancelSubscriptionSection, 10 | useUser, 11 | PlansSection, 12 | SubscriptionModal, 13 | useModals, 14 | usePlans, 15 | useSubscription, 16 | useOrganization, 17 | useLoad, 18 | } from 'react-components'; 19 | import { UserModel, Plan, PlanIDs } from 'proton-shared/lib/interfaces'; 20 | import isTruthy from 'proton-shared/lib/helpers/isTruthy'; 21 | import { DEFAULT_CYCLE, PLAN_SERVICES, CYCLE, CURRENCIES } from 'proton-shared/lib/constants'; 22 | import { toMap } from 'proton-shared/lib/helpers/object'; 23 | import { SUBSCRIPTION_STEPS } from 'react-components/containers/payments/subscription/constants'; 24 | import { getPlanIDs } from 'proton-shared/lib/helpers/subscription'; 25 | import { switchPlan } from 'proton-shared/lib/helpers/planIDs'; 26 | import PrivateMainSettingsAreaWithPermissions from '../../components/PrivateMainSettingsAreaWithPermissions'; 27 | 28 | export const getDashboardPage = ({ user }: { user: UserModel }) => { 29 | const { isFree, isPaid, isMember, canPay } = user; 30 | 31 | return { 32 | text: c('Title').t`Dashboard`, 33 | to: '/dashboard', 34 | icon: 'apps', 35 | subsections: [ 36 | isFree && { 37 | text: c('Title').t`Select plan`, 38 | id: 'select-plan', 39 | }, 40 | canPay && { 41 | text: isFree ? c('Title').t`Your current plan` : c('Title').t`Your plan`, 42 | id: 'your-plan', 43 | }, 44 | { 45 | text: c('Title').t`Language & time`, 46 | id: 'language-and-time', 47 | }, 48 | !isMember && { 49 | text: c('Title').t`Email subscriptions`, 50 | id: 'email-subscription', 51 | }, 52 | isPaid && 53 | canPay && { 54 | text: c('Title').t`Cancel subscription`, 55 | id: 'cancel-subscription', 56 | }, 57 | ].filter(isTruthy), 58 | }; 59 | }; 60 | 61 | interface PlansMap { 62 | [planName: string]: Plan; 63 | } 64 | 65 | const AccountDashboardSettings = ({ location, setActiveSection }: SettingsPropsShared) => { 66 | const [user] = useUser(); 67 | 68 | const { isFree, isPaid, isMember, canPay } = user; 69 | 70 | const { createModal } = useModals(); 71 | const [plans, loadingPlans] = usePlans(); 72 | const [subscription, loadingSubscription] = useSubscription(); 73 | const [organization, loadingOrganization] = useOrganization(); 74 | const onceRef = useRef(false); 75 | const history = useHistory(); 76 | useLoad(); 77 | 78 | useEffect(() => { 79 | const searchParams = new URLSearchParams(location.search); 80 | const planName = searchParams.get('plan'); 81 | if (!plans || !planName || loadingPlans || loadingSubscription || loadingOrganization || onceRef.current) { 82 | return; 83 | } 84 | 85 | searchParams.delete('plan'); 86 | history.replace({ 87 | search: searchParams.toString(), 88 | }); 89 | onceRef.current = true; 90 | 91 | const coupon = searchParams.get('coupon'); 92 | const cycleParam = parseInt(searchParams.get('cycle') as any, 10); 93 | const currencyParam = searchParams.get('currency') as any; 94 | const defaultCycle = 95 | cycleParam && [CYCLE.MONTHLY, CYCLE.YEARLY, CYCLE.TWO_YEARS].includes(cycleParam) 96 | ? cycleParam 97 | : DEFAULT_CYCLE; 98 | const defaultCurrency = currencyParam && CURRENCIES.includes(currencyParam) ? currencyParam : plans[0].Currency; 99 | const { Cycle = defaultCycle, Currency = defaultCurrency } = subscription; 100 | const plansMap = toMap(plans, 'Name') as PlansMap; 101 | if (user.isFree) { 102 | const planIDs = planName.split('_').reduce((acc, name) => { 103 | acc[plansMap[name].ID] = 1; 104 | return acc; 105 | }, {}); 106 | if (!Object.keys(planIDs).length) { 107 | return; 108 | } 109 | createModal( 110 | 117 | ); 118 | return; 119 | } 120 | const plan = plansMap[planName]; 121 | if (!plan) { 122 | return; 123 | } 124 | const planIDs = switchPlan({ 125 | planIDs: getPlanIDs(subscription), 126 | plans, 127 | planID: plan.ID, 128 | service: PLAN_SERVICES.VPN, 129 | organization, 130 | }); 131 | createModal( 132 | 138 | ); 139 | }, [loadingPlans, loadingSubscription, loadingOrganization]); 140 | 141 | return ( 142 | 147 | {Boolean(isFree) && } 148 | {Boolean(canPay) && } 149 | 150 | {!isMember && } 151 | {isPaid && canPay && } 152 | 153 | ); 154 | }; 155 | 156 | export default AccountDashboardSettings; 157 | -------------------------------------------------------------------------------- /src/app/content/MainContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense, useEffect, useState } from 'react'; 2 | import { c } from 'ttag'; 3 | import { Route, Redirect, Switch, useLocation } from 'react-router-dom'; 4 | import { DEFAULT_APP, getAppFromPathnameSafe, getSlugFromApp } from 'proton-shared/lib/apps/slugHelper'; 5 | import { APPS } from 'proton-shared/lib/constants'; 6 | 7 | import { useActiveBreakpoint, useToggle, PrivateHeader, PrivateAppContainer, Logo, useUser } from 'react-components'; 8 | 9 | import PrivateMainAreaLoading from '../components/PrivateMainAreaLoading'; 10 | 11 | import AccountPasswordAndRecoverySettings from '../containers/account/AccountPasswordAndRecoverySettings'; 12 | import AccountSecuritySettings from '../containers/account/AccountSecuritySettings'; 13 | import AccountPaymentSettings from '../containers/account/AccountPaymentSettings'; 14 | import AccountDashboardSettings from '../containers/account/AccountDashboardSettings'; 15 | import OrganizationMultiUserSupportSettings from '../containers/organization/OrganizationMultiUserSupportSettings'; 16 | import AccountSidebar from './AccountSidebar'; 17 | import MailDomainNamesSettings from '../containers/mail/MailDomainNamesSettings'; 18 | import OrganizationUsersAndAddressesSettings from '../containers/organization/OrganizationUsersAndAddressesSettings'; 19 | import OrganizationKeysSettings from '../containers/organization/OrganizationKeysSettings'; 20 | 21 | const MailSettingsRouter = React.lazy(() => import('../containers/mail/MailSettingsRouter')); 22 | const CalendarSettingsRouter = React.lazy(() => import('../containers/calendar/CalendarSettingsRouter')); 23 | const ContactsSettingsRouter = React.lazy(() => import('../containers/contacts/ContactsSettingsRouter')); 24 | const VpnSettingsRouter = React.lazy(() => import('../containers/vpn/VpnSettingsRouter')); 25 | const DriveSettingsRouter = React.lazy(() => import('../containers/drive/DriveSettingsRouter')); 26 | 27 | const DEFAULT_REDIRECT = `/${getSlugFromApp(DEFAULT_APP)}/dashboard`; 28 | 29 | const mailSlug = getSlugFromApp(APPS.PROTONMAIL); 30 | const calendarSlug = getSlugFromApp(APPS.PROTONCALENDAR); 31 | const vpnSlug = getSlugFromApp(APPS.PROTONVPN_SETTINGS); 32 | const driveSlug = getSlugFromApp(APPS.PROTONDRIVE); 33 | const contactsSlug = getSlugFromApp(APPS.PROTONCONTACTS); 34 | 35 | const MainContainer = () => { 36 | const [user] = useUser(); 37 | const location = useLocation(); 38 | const { state: expanded, toggle: onToggleExpand, set: setExpand } = useToggle(); 39 | const { isNarrow } = useActiveBreakpoint(); 40 | const [isBlurred] = useState(false); 41 | 42 | useEffect(() => { 43 | setExpand(false); 44 | }, [location.pathname, location.hash]); 45 | 46 | const app = getAppFromPathnameSafe(location.pathname); 47 | 48 | if (!app) { 49 | return ; 50 | } 51 | 52 | const appSlug = getSlugFromApp(app); 53 | 54 | /* 55 | * There's no logical app to return/go to from VPN settings since the 56 | * vpn web app is also settings which you are already in. Redirect to 57 | * the default path in account in that case. 58 | */ 59 | const isVpn = app === APPS.PROTONVPN_SETTINGS; 60 | const toApp = isVpn ? APPS.PROTONACCOUNT : app; 61 | const to = isVpn ? '/vpn' : '/'; 62 | 63 | const logo = ; 64 | 65 | const header = ( 66 | 73 | ); 74 | 75 | const sidebar = ( 76 | 77 | ); 78 | 79 | return ( 80 | 81 | 82 | 83 | {}} /> 84 | 85 | 86 | {}} user={user} /> 87 | 88 | 89 | {}} /> 90 | 91 | 92 | {}} /> 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | , 101 | 102 | 103 | 104 | , 105 | 106 | 107 | 108 | 109 | }> 110 | 111 | 112 | 113 | 114 | }> 115 | 116 | 117 | 118 | 119 | }> 120 | 121 | 122 | 123 | 124 | }> 125 | 126 | 127 | 128 | 129 | }> 130 | 131 | 132 | 133 | 134 | 135 | 136 | ); 137 | }; 138 | 139 | export default MainContainer; 140 | -------------------------------------------------------------------------------- /src/app/public/EmailUnsubscribeContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useParams, useLocation, useHistory } from 'react-router-dom'; 3 | import { c } from 'ttag'; 4 | import { FullLoader, GenericError, useApi, useLoading, useNotifications } from 'react-components'; 5 | import { authJwt } from 'proton-shared/lib/api/auth'; 6 | import { getNewsExternal, updateNewsExternal } from 'proton-shared/lib/api/settings'; 7 | import { withAuthHeaders } from 'proton-shared/lib/fetch/headers'; 8 | import { Api } from 'proton-shared/lib/interfaces'; 9 | import { NEWS } from 'proton-shared/lib/constants'; 10 | import { clearBit, getBits, setBit } from 'proton-shared/lib/helpers/bitset'; 11 | 12 | import EmailUnsubscribed from '../components/EmailUnsubscribed'; 13 | import EmailResubscribed from '../components/EmailResubscribed'; 14 | import EmailSubscriptionManagement from '../components/EmailSubscriptionManagement'; 15 | import './EmailUnsubscribeContainer.scss'; 16 | 17 | interface UserSettingsNewsResponse { 18 | Code: number; 19 | UserSettings: { 20 | News: number; 21 | }; 22 | } 23 | 24 | enum PAGE { 25 | UNSUBSCRIBE, 26 | RESUBSCRIBE, 27 | MANAGE, 28 | } 29 | 30 | const EmailUnsubscribeContainer = () => { 31 | const api = useApi(); 32 | const [authApi, setAuthApi] = useState(null); 33 | const [news, setNews] = useState(null); 34 | const [page, setPage] = useState(PAGE.UNSUBSCRIBE); 35 | const [error, setError] = useState(null); 36 | const [loading, withLoading] = useLoading(); 37 | const history = useHistory(); 38 | const location = useLocation(); 39 | const { subscriptions: subscriptionsParam } = useParams<{ subscriptions: string | undefined }>(); 40 | 41 | const subscriptions = Number(subscriptionsParam); 42 | 43 | const subscriptionBits = getBits(subscriptions) as NEWS[]; 44 | 45 | const newsTypeToWording = { 46 | [NEWS.ANNOUNCEMENTS]: c('Label for news').t`Proton announcements`, 47 | [NEWS.FEATURES]: c('Label for news').t`Proton major features`, 48 | [NEWS.BUSINESS]: c('Label for news').t`Proton for business`, 49 | [NEWS.NEWSLETTER]: c('Label for news').t`Proton newsletter`, 50 | [NEWS.BETA]: c('Label for news').t`Proton Beta`, 51 | }; 52 | 53 | const categories = subscriptionBits.map((bit) => newsTypeToWording[bit]); 54 | 55 | useEffect(() => { 56 | const init = async () => { 57 | const jwt = location.hash.substring(1); 58 | 59 | history.replace(location.pathname); 60 | 61 | const { UID, AccessToken } = await api<{ UID: string; AccessToken: string }>(authJwt({ Token: jwt })); 62 | 63 | const authApiFn: Api = (config: object) => 64 | api(withAuthHeaders(UID, AccessToken, { ...config, headers: {} })); 65 | 66 | const { 67 | UserSettings: { News: currentNews }, 68 | } = await authApiFn(getNewsExternal()); 69 | 70 | const nextNews = subscriptionBits.reduce(clearBit, currentNews); 71 | 72 | const { 73 | UserSettings: { News: updatedNews }, 74 | } = await authApiFn(updateNewsExternal(nextNews)); 75 | 76 | /* 77 | * https://reactjs.org/docs/faq-state.html#what-is-the-difference-between-passing-an-object-or-a-function-in-setstate 78 | * 79 | * we want to store the 'authApiFn' here, not tell react to use the function to generate the next state 80 | */ 81 | setAuthApi(() => authApiFn); 82 | 83 | setNews(updatedNews); 84 | }; 85 | 86 | init().catch(setError); 87 | }, []); 88 | 89 | const { createNotification } = useNotifications(); 90 | 91 | const update = async (news: number) => { 92 | if (!authApi) { 93 | return; 94 | } 95 | 96 | const { 97 | UserSettings: { News }, 98 | } = await authApi<{ UserSettings: { News: number } }>(updateNewsExternal(news)); 99 | 100 | setNews(News); 101 | createNotification({ text: c('Info').t`Emailing preference saved` }); 102 | }; 103 | 104 | const handleResubscribeClick = async () => { 105 | await withLoading(update(subscriptionBits.reduce(setBit, news || 0))); 106 | setPage(PAGE.RESUBSCRIBE); 107 | }; 108 | 109 | const handleUnsubscribeClick = async () => { 110 | await withLoading(update(subscriptionBits.reduce(clearBit, news || 0))); 111 | setPage(PAGE.UNSUBSCRIBE); 112 | }; 113 | 114 | const handleManageClick = () => { 115 | setPage(PAGE.MANAGE); 116 | }; 117 | 118 | const handleChange = (news: number) => { 119 | withLoading(update(news)); 120 | }; 121 | 122 | const renderView = () => { 123 | if (news === null) { 124 | return ( 125 |
126 | 127 |
128 | ); 129 | } 130 | 131 | switch (page) { 132 | case PAGE.UNSUBSCRIBE: { 133 | return ( 134 | 140 | ); 141 | } 142 | 143 | case PAGE.RESUBSCRIBE: { 144 | return ( 145 | 151 | ); 152 | } 153 | 154 | case PAGE.MANAGE: { 155 | return ; 156 | } 157 | 158 | default: 159 | return null; 160 | } 161 | }; 162 | 163 | if (error) { 164 | const signIn = ( 165 | 166 | {c('Action').t`sign in`} 167 | 168 | ); 169 | 170 | return ( 171 | 172 | {c('Error message').t`There was a problem unsubscribing you.`} 173 | {c('Error message').jt`Please ${signIn} to update your email subscription preferences.`} 174 | 175 | ); 176 | } 177 | 178 | return
{renderView()}
; 179 | }; 180 | 181 | export default EmailUnsubscribeContainer; 182 | -------------------------------------------------------------------------------- /src/app/containers/SetupInternalAccountContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import { c } from 'ttag'; 3 | import { 4 | AuthenticatedBugModal, 5 | DropdownMenuButton, 6 | Icon, 7 | LoaderPage, 8 | StandardLoadErrorPage, 9 | useApi, 10 | useAppLink, 11 | useAuthentication, 12 | useErrorHandler, 13 | useModals, 14 | useTheme, 15 | } from 'react-components'; 16 | import { ThemeTypes } from 'proton-shared/lib/themes/themes'; 17 | import { queryAddresses } from 'proton-shared/lib/api/addresses'; 18 | import { Address } from 'proton-shared/lib/interfaces'; 19 | import { queryAvailableDomains } from 'proton-shared/lib/api/domains'; 20 | import { handleCreateInternalAddressAndKey } from 'proton-shared/lib/keys'; 21 | import { getHasOnlyExternalAddresses } from 'proton-shared/lib/helpers/address'; 22 | import { getAppName } from 'proton-shared/lib/apps/helper'; 23 | import { APP_NAMES, APPS } from 'proton-shared/lib/constants'; 24 | import { getValidatedApp } from 'proton-shared/lib/authentication/sessionForkValidation'; 25 | import { getApiErrorMessage } from 'proton-shared/lib/api/helpers/apiErrorHelper'; 26 | 27 | import { getToAppName } from '../public/helper'; 28 | import GenerateInternalAddressStep, { InternalAddressGeneration } from '../login/GenerateInternalAddressStep'; 29 | import Main from '../public/Main'; 30 | import Layout from '../public/Layout'; 31 | import Footer from '../public/Footer'; 32 | import SupportDropdown from '../public/SupportDropdown'; 33 | 34 | const SetupSupportDropdown = () => { 35 | const { createModal } = useModals(); 36 | 37 | const handleBugReportClick = () => { 38 | createModal(); 39 | }; 40 | 41 | return ( 42 | 43 | 44 | 45 | {c('Action').t`Report a problem`} 46 | 47 | 48 | ); 49 | }; 50 | 51 | const SetupInternalAccountContainer = () => { 52 | const [loading, setLoading] = useState(true); 53 | const [error, setError] = useState<{ message?: string } | null>(null); 54 | const normalApi = useApi(); 55 | const silentApi = (config: any) => normalApi({ ...config, silence: true }); 56 | const errorHandler = useErrorHandler(); 57 | const goToApp = useAppLink(); 58 | const toAppRef = useRef(null); 59 | const authentication = useAuthentication(); 60 | const [, setTheme] = useTheme(); 61 | 62 | const generateInternalAddressRef = useRef(undefined); 63 | 64 | const handleBack = () => { 65 | goToApp('/'); 66 | }; 67 | 68 | useEffect(() => { 69 | return () => { 70 | generateInternalAddressRef.current = undefined; 71 | }; 72 | }, []); 73 | 74 | useEffect(() => { 75 | const run = async () => { 76 | const searchParams = new URLSearchParams(window.location.search); 77 | const app = getValidatedApp(searchParams.get('app') || ''); 78 | 79 | if (!app) { 80 | return handleBack(); 81 | } 82 | 83 | const [addresses, domains] = await Promise.all([ 84 | silentApi<{ Addresses: Address[] }>(queryAddresses()).then(({ Addresses }) => Addresses), 85 | silentApi<{ Domains: string[] }>(queryAvailableDomains()).then(({ Domains }) => Domains), 86 | ]); 87 | 88 | if (!getHasOnlyExternalAddresses(addresses)) { 89 | return handleBack(); 90 | } 91 | 92 | // Special case to reset the user's theme since it's logged in at this point. Does not care about resetting it back since it always redirects back to the application. 93 | setTheme(ThemeTypes.Default); 94 | 95 | toAppRef.current = app; 96 | generateInternalAddressRef.current = { 97 | externalEmailAddress: addresses[0], 98 | availableDomains: domains, 99 | keyPassword: authentication.getPassword(), 100 | api: silentApi, 101 | onDone: async () => { 102 | goToApp('/', app); 103 | }, 104 | revoke: () => {}, 105 | }; 106 | setLoading(false); 107 | }; 108 | 109 | run() 110 | .then(() => { 111 | setLoading(false); 112 | }) 113 | .catch((e) => { 114 | errorHandler(e); 115 | setError({ 116 | message: getApiErrorMessage(e), 117 | }); 118 | }); 119 | }, []); 120 | 121 | if (error) { 122 | return ; 123 | } 124 | 125 | if (loading) { 126 | return ; 127 | } 128 | 129 | const toApp = toAppRef.current!; 130 | const toAppName = getToAppName(toApp); 131 | const mailAppName = getAppName(APPS.PROTONMAIL); 132 | 133 | const generateInternalAddress = generateInternalAddressRef.current; 134 | const externalEmailAddress = generateInternalAddress?.externalEmailAddress?.Email || ''; 135 | 136 | if (!generateInternalAddress) { 137 | throw new Error('Missing dependencies'); 138 | } 139 | 140 | return ( 141 | 142 |
143 | { 150 | handleBack(); 151 | }} 152 | onSubmit={async (payload) => { 153 | try { 154 | await handleCreateInternalAddressAndKey({ 155 | api: generateInternalAddress.api, 156 | keyPassword: generateInternalAddress.keyPassword, 157 | domain: payload.domain, 158 | username: payload.username, 159 | }); 160 | await generateInternalAddress.onDone(); 161 | } catch (e) { 162 | errorHandler(e); 163 | handleBack(); 164 | } 165 | }} 166 | /> 167 | 170 |
171 |
172 | ); 173 | }; 174 | 175 | export default SetupInternalAccountContainer; 176 | --------------------------------------------------------------------------------