diff options
| author | Oskar <oskar@mullvad.net> | 2024-10-02 10:16:14 +0200 |
|---|---|---|
| committer | Oskar <oskar@mullvad.net> | 2024-10-02 10:16:14 +0200 |
| commit | 833d39b41af37616f15aaf124bf0b76e94f20e63 (patch) | |
| tree | a63a4704656328d05900a0b8d1f96e95290544af | |
| parent | 1217112350515255c1933fe2e8192147d660b7f2 (diff) | |
| parent | 62497011c88d52e80f9b36f84939600d56dfa6e6 (diff) | |
| download | mullvadvpn-833d39b41af37616f15aaf124bf0b76e94f20e63.tar.xz mullvadvpn-833d39b41af37616f15aaf124bf0b76e94f20e63.zip | |
Merge branch 'add-react-hooks-rules-of-hooks-linter-rule-des-1286'
| -rw-r--r-- | gui/eslint.config.mjs | 4 | ||||
| -rw-r--r-- | gui/package-lock.json | 20 | ||||
| -rw-r--r-- | gui/package.json | 1 | ||||
| -rw-r--r-- | gui/src/renderer/app.tsx | 4 | ||||
| -rw-r--r-- | gui/src/renderer/components/AppRouter.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/components/Login.tsx | 35 | ||||
| -rw-r--r-- | gui/src/renderer/containers/LoginPage.tsx | 42 | ||||
| -rw-r--r-- | gui/src/renderer/context.tsx | 19 |
8 files changed, 62 insertions, 65 deletions
diff --git a/gui/eslint.config.mjs b/gui/eslint.config.mjs index 5c2b5d848c..75f28f6091 100644 --- a/gui/eslint.config.mjs +++ b/gui/eslint.config.mjs @@ -1,6 +1,7 @@ import eslint from '@eslint/js'; import prettier from 'eslint-plugin-prettier/recommended'; import react from 'eslint-plugin-react'; +import reactHooks from 'eslint-plugin-react-hooks'; import simpleImportSort from 'eslint-plugin-simple-import-sort'; import globals from 'globals'; import tseslint from 'typescript-eslint'; @@ -128,6 +129,7 @@ export default tseslint.config( files: ['**/*.{js,mjs,ts,tsx}'], plugins: { 'simple-import-sort': simpleImportSort, + 'react-hooks': reactHooks, }, rules: { quotes: ['error', 'single', { avoidEscape: true }], @@ -144,6 +146,8 @@ export default tseslint.config( '@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': false }], 'simple-import-sort/imports': 'error', + 'react-hooks/rules-of-hooks': 'error', + '@typescript-eslint/no-use-before-define': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-non-null-assertion': 'off', diff --git a/gui/package-lock.json b/gui/package-lock.json index 6ff861c565..3f6bddbe34 100644 --- a/gui/package-lock.json +++ b/gui/package-lock.json @@ -56,6 +56,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-react": "^7.36.1", + "eslint-plugin-react-hooks": "^0.0.0-experimental-2d16326d-20240930", "eslint-plugin-simple-import-sort": "^12.1.1", "gettext-extractor": "^3.5.4", "globals": "^15.9.0", @@ -5216,6 +5217,18 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, + "node_modules/eslint-plugin-react-hooks": { + "version": "0.0.0-experimental-2d16326d-20240930", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-0.0.0-experimental-2d16326d-20240930.tgz", + "integrity": "sha512-6SrTdUGjhhQ51U6a695UlDFzR46pFlhz0yYyLrO5f4pXGHA0aE9X0TrqcB1gPyqB7LhSKjDSaoHC5/J48GG2UQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, "node_modules/eslint-plugin-react/node_modules/resolve": { "version": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", @@ -18032,6 +18045,13 @@ } } }, + "eslint-plugin-react-hooks": { + "version": "0.0.0-experimental-2d16326d-20240930", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-0.0.0-experimental-2d16326d-20240930.tgz", + "integrity": "sha512-6SrTdUGjhhQ51U6a695UlDFzR46pFlhz0yYyLrO5f4pXGHA0aE9X0TrqcB1gPyqB7LhSKjDSaoHC5/J48GG2UQ==", + "dev": true, + "requires": {} + }, "eslint-plugin-simple-import-sort": { "version": "12.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz", diff --git a/gui/package.json b/gui/package.json index 457a5a97d1..6298c01286 100644 --- a/gui/package.json +++ b/gui/package.json @@ -62,6 +62,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-react": "^7.36.1", + "eslint-plugin-react-hooks": "^0.0.0-experimental-2d16326d-20240930", "eslint-plugin-simple-import-sort": "^12.1.1", "gettext-extractor": "^3.5.4", "globals": "^15.9.0", diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index 940dcc000f..09c84ea673 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -437,7 +437,7 @@ export default class AppRenderer { await this.disconnectTunnel(); }; - public async createNewAccount() { + public createNewAccount = async () => { log.info('Creating account'); const actions = this.reduxActions; @@ -451,7 +451,7 @@ export default class AppRenderer { const error = e as Error; actions.account.createAccountFailed(error); } - } + }; public fetchDevices = async (accountNumber: AccountNumber): Promise<Array<IDevice>> => { const devices = await IpcRendererEventChannel.account.listDevices(accountNumber); diff --git a/gui/src/renderer/components/AppRouter.tsx b/gui/src/renderer/components/AppRouter.tsx index e1a3d2ab0f..e83b6f55cd 100644 --- a/gui/src/renderer/components/AppRouter.tsx +++ b/gui/src/renderer/components/AppRouter.tsx @@ -1,8 +1,8 @@ import { createRef, useCallback, useEffect, useState } from 'react'; import { Route, Switch } from 'react-router'; +import LoginPage from '../components/Login'; import SelectLocation from '../components/select-location/SelectLocationContainer'; -import LoginPage from '../containers/LoginPage'; import { useAppContext } from '../context'; import { ITransitionSpecification, transitions, useHistory } from '../lib/history'; import { RoutePath } from '../lib/routes'; diff --git a/gui/src/renderer/components/Login.tsx b/gui/src/renderer/components/Login.tsx index 8f17c35651..430772c2c7 100644 --- a/gui/src/renderer/components/Login.tsx +++ b/gui/src/renderer/components/Login.tsx @@ -6,7 +6,9 @@ import { AccountDataError, AccountNumber } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; import { useAppContext } from '../context'; import { formatAccountNumber } from '../lib/account'; +import useActions from '../lib/actionsHook'; import { formatHtml } from '../lib/html-formatter'; +import accountActions from '../redux/account/actions'; import { LoginState } from '../redux/account/reducers'; import { useSelector } from '../redux/store'; import Accordion from './Accordion'; @@ -40,6 +42,37 @@ import { StyledTopInfo, } from './LoginStyles'; +export default function LoginContainer() { + const { openUrl, login, clearAccountHistory, createNewAccount } = useAppContext(); + const { resetLoginError, updateAccountNumber } = useActions(accountActions); + + const { accountNumber, accountHistory, status } = useSelector((state) => state.account); + + const tunnelState = useSelector((state) => state.connection.status); + const blockWhenDisconnected = useSelector((state) => state.settings.blockWhenDisconnected); + const showBlockMessage = tunnelState.state === 'error' || blockWhenDisconnected; + + const isPerformingPostUpgrade = useSelector( + (state) => state.userInterface.isPerformingPostUpgrade, + ); + + return ( + <Login + accountNumber={accountNumber} + accountHistory={accountHistory} + loginState={status} + showBlockMessage={showBlockMessage} + openExternalLink={openUrl} + login={login} + resetLoginError={resetLoginError} + updateAccountNumber={updateAccountNumber} + clearAccountHistory={clearAccountHistory} + createNewAccount={createNewAccount} + isPerformingPostUpgrade={isPerformingPostUpgrade} + /> + ); +} + interface IProps { accountNumber?: AccountNumber; accountHistory?: AccountNumber; @@ -60,7 +93,7 @@ interface IState { const MIN_ACCOUNT_NUMBER_LENGTH = 10; -export default class Login extends React.Component<IProps, IState> { +class Login extends React.Component<IProps, IState> { public state: IState = { isActive: true, }; diff --git a/gui/src/renderer/containers/LoginPage.tsx b/gui/src/renderer/containers/LoginPage.tsx deleted file mode 100644 index 49fdbba839..0000000000 --- a/gui/src/renderer/containers/LoginPage.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; - -import Login from '../components/Login'; -import withAppContext, { IAppContext } from '../context'; -import accountActions from '../redux/account/actions'; -import { IReduxState, ReduxDispatch } from '../redux/store'; - -const mapStateToProps = (state: IReduxState) => { - const tunnelState = state.connection.status; - const blockWhenDisconnected = state.settings.blockWhenDisconnected; - const { accountNumber, accountHistory, status } = state.account; - - const showBlockMessage = tunnelState.state === 'error' || blockWhenDisconnected; - - const isPerformingPostUpgrade = state.userInterface.isPerformingPostUpgrade; - - return { - accountNumber, - accountHistory, - loginState: status, - showBlockMessage, - isPerformingPostUpgrade, - }; -}; -const mapDispatchToProps = (dispatch: ReduxDispatch, props: IAppContext) => { - const { resetLoginError, updateAccountNumber } = bindActionCreators(accountActions, dispatch); - return { - login: (account: string) => { - void props.app.login(account); - }, - resetLoginError: () => { - resetLoginError(); - }, - openExternalLink: (url: string) => props.app.openUrl(url), - updateAccountNumber, - clearAccountHistory: () => props.app.clearAccountHistory(), - createNewAccount: () => void props.app.createNewAccount(), - }; -}; - -export default withAppContext(connect(mapStateToProps, mapDispatchToProps)(Login)); diff --git a/gui/src/renderer/context.tsx b/gui/src/renderer/context.tsx index 3b5fcc14fb..eb1be7a556 100644 --- a/gui/src/renderer/context.tsx +++ b/gui/src/renderer/context.tsx @@ -15,25 +15,6 @@ const missingContextError = new Error( 'The context value is empty. Make sure to wrap the component in AppContext.Provider.', ); -type PropsWithoutAppContext<Props> = Omit<Props, 'app'>; - -export default function withAppContext<Props>( - BaseComponent: React.ComponentType<PropsWithoutAppContext<Props> & IAppContext>, -) { - // Exclude the IAppContext from props since those are injected props - const wrappedComponent = (props: PropsWithoutAppContext<Props>) => { - const appContext = useAppContext(); - return <BaseComponent app={appContext} {...props} />; - }; - - if (window.env.development) { - wrappedComponent.displayName = - 'withAppContext(' + (BaseComponent.displayName || BaseComponent.name) + ')'; - } - - return wrappedComponent; -} - export function useAppContext(): App { const appContext = useContext(AppContext); if (appContext) { |
