mirror of
https://github.com/elyby/accounts-frontend.git
synced 2024-12-23 05:29:56 +05:30
Change prettier rules
This commit is contained in:
parent
73f0c37a6a
commit
f85b9d8d35
328
.eslintrc.js
328
.eslintrc.js
@ -1,174 +1,168 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
},
|
||||
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:jsdoc/recommended',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'prettier/@typescript-eslint',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
|
||||
plugins: ['react'],
|
||||
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
commonjs: true,
|
||||
},
|
||||
|
||||
overrides: [
|
||||
{
|
||||
files: ['packages/webpack-utils/**', 'packages/scripts/**', 'jest/**'],
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
},
|
||||
{
|
||||
files: ['*.test.js'],
|
||||
env: {
|
||||
jest: true,
|
||||
},
|
||||
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:jsdoc/recommended',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'prettier/@typescript-eslint',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
|
||||
plugins: ['react'],
|
||||
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
commonjs: true,
|
||||
},
|
||||
{
|
||||
files: ['tests-e2e/**'],
|
||||
env: {
|
||||
mocha: true,
|
||||
},
|
||||
globals: {
|
||||
cy: 'readonly',
|
||||
Cypress: 'readonly',
|
||||
},
|
||||
rules: {
|
||||
'no-restricted-globals': 'off',
|
||||
},
|
||||
|
||||
overrides: [
|
||||
{
|
||||
files: ['packages/webpack-utils/**', 'packages/scripts/**', 'jest/**'],
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.test.js'],
|
||||
env: {
|
||||
jest: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['tests-e2e/**'],
|
||||
env: {
|
||||
mocha: true,
|
||||
},
|
||||
globals: {
|
||||
cy: 'readonly',
|
||||
Cypress: 'readonly',
|
||||
},
|
||||
rules: {
|
||||
'no-restricted-globals': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
// @see: http://eslint.org/docs/rules/
|
||||
rules: {
|
||||
'no-prototype-builtins': 'warn', // temporary set to warn
|
||||
'no-restricted-globals': [
|
||||
'error',
|
||||
'localStorage',
|
||||
'sessionStorage', // we have our own localStorage module
|
||||
'event',
|
||||
],
|
||||
'id-length': ['error', { min: 2, exceptions: ['x', 'y', 'i', 'k', 'l', 'm', 'n', '$', '_'] }],
|
||||
'guard-for-in': ['error'],
|
||||
'no-var': ['error'],
|
||||
'prefer-const': ['error'],
|
||||
'prefer-template': ['error'],
|
||||
'no-template-curly-in-string': ['error'],
|
||||
'no-multi-assign': ['error'],
|
||||
eqeqeq: ['error'],
|
||||
'prefer-rest-params': ['error'],
|
||||
'prefer-object-spread': 'warn',
|
||||
'prefer-destructuring': 'warn',
|
||||
'no-bitwise': 'warn',
|
||||
'no-negated-condition': 'warn',
|
||||
'no-nested-ternary': 'warn',
|
||||
'no-unneeded-ternary': 'warn',
|
||||
'no-shadow': 'warn',
|
||||
'no-else-return': 'warn',
|
||||
radix: 'warn',
|
||||
'prefer-promise-reject-errors': 'warn',
|
||||
'object-shorthand': 'warn',
|
||||
'require-atomic-updates': 'off',
|
||||
|
||||
// force extra lines around if, else, for, while, switch, return etc
|
||||
'padding-line-between-statements': [
|
||||
'error',
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: '*',
|
||||
next: ['if', 'for', 'while', 'switch', 'return'],
|
||||
},
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: ['if', 'for', 'while', 'switch', 'return'],
|
||||
next: '*',
|
||||
},
|
||||
{
|
||||
blankLine: 'never',
|
||||
prev: ['if', 'for', 'while', 'switch', 'return'],
|
||||
next: 'break',
|
||||
},
|
||||
],
|
||||
|
||||
'jsdoc/require-param-description': 'off',
|
||||
'jsdoc/require-returns-description': 'off',
|
||||
'jsdoc/require-jsdoc': 'off',
|
||||
'jsdoc/valid-types': 'off',
|
||||
'jsdoc/no-undefined-types': 'off',
|
||||
'jsdoc/require-returns': 'off',
|
||||
|
||||
// react
|
||||
'react/display-name': 'off',
|
||||
'react/react-in-jsx-scope': 'warn',
|
||||
'react/forbid-prop-types': 'warn',
|
||||
'react/jsx-boolean-value': 'warn',
|
||||
'react/jsx-closing-bracket-location': 'off', // can not configure for our code style
|
||||
'react/jsx-curly-spacing': 'warn',
|
||||
'react/jsx-handler-names': ['warn', { eventHandlerPrefix: 'on', eventHandlerPropPrefix: 'on' }],
|
||||
'react/jsx-key': 'warn',
|
||||
'react/jsx-max-props-per-line': 'off',
|
||||
'react/jsx-no-bind': 'off',
|
||||
'react/jsx-no-duplicate-props': 'warn',
|
||||
'react/jsx-no-literals': 'off',
|
||||
'react/jsx-no-undef': 'error',
|
||||
'react/jsx-pascal-case': 'warn',
|
||||
'react/jsx-uses-react': 'warn',
|
||||
'react/jsx-uses-vars': 'warn',
|
||||
'react/jsx-no-comment-textnodes': 'warn',
|
||||
'react/jsx-wrap-multilines': 'warn',
|
||||
'react/no-deprecated': 'error',
|
||||
'react/no-did-mount-set-state': 'warn',
|
||||
'react/no-did-update-set-state': 'warn',
|
||||
'react/no-direct-mutation-state': 'warn',
|
||||
'react/require-render-return': 'warn',
|
||||
'react/no-is-mounted': 'warn',
|
||||
'react/no-multi-comp': 'off',
|
||||
'react/no-string-refs': 'warn',
|
||||
'react/no-unknown-property': 'warn',
|
||||
'react/prefer-es6-class': 'warn',
|
||||
'react/prop-types': 'off', // using ts for this task
|
||||
'react/self-closing-comp': 'warn',
|
||||
'react/sort-comp': 'off',
|
||||
|
||||
// ts
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
'@typescript-eslint/ban-ts-ignore': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/ban-types': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-inferrable-types': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
vars: 'all',
|
||||
args: 'after-used',
|
||||
argsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
// @see: http://eslint.org/docs/rules/
|
||||
rules: {
|
||||
'no-prototype-builtins': 'warn', // temporary set to warn
|
||||
'no-restricted-globals': [
|
||||
'error',
|
||||
'localStorage',
|
||||
'sessionStorage', // we have our own localStorage module
|
||||
'event',
|
||||
],
|
||||
'id-length': [
|
||||
'error',
|
||||
{ min: 2, exceptions: ['x', 'y', 'i', 'k', 'l', 'm', 'n', '$', '_'] },
|
||||
],
|
||||
'guard-for-in': ['error'],
|
||||
'no-var': ['error'],
|
||||
'prefer-const': ['error'],
|
||||
'prefer-template': ['error'],
|
||||
'no-template-curly-in-string': ['error'],
|
||||
'no-multi-assign': ['error'],
|
||||
eqeqeq: ['error'],
|
||||
'prefer-rest-params': ['error'],
|
||||
'prefer-object-spread': 'warn',
|
||||
'prefer-destructuring': 'warn',
|
||||
'no-bitwise': 'warn',
|
||||
'no-negated-condition': 'warn',
|
||||
'no-nested-ternary': 'warn',
|
||||
'no-unneeded-ternary': 'warn',
|
||||
'no-shadow': 'warn',
|
||||
'no-else-return': 'warn',
|
||||
radix: 'warn',
|
||||
'prefer-promise-reject-errors': 'warn',
|
||||
'object-shorthand': 'warn',
|
||||
'require-atomic-updates': 'off',
|
||||
|
||||
// force extra lines around if, else, for, while, switch, return etc
|
||||
'padding-line-between-statements': [
|
||||
'error',
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: '*',
|
||||
next: ['if', 'for', 'while', 'switch', 'return'],
|
||||
},
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: ['if', 'for', 'while', 'switch', 'return'],
|
||||
next: '*',
|
||||
},
|
||||
{
|
||||
blankLine: 'never',
|
||||
prev: ['if', 'for', 'while', 'switch', 'return'],
|
||||
next: 'break',
|
||||
},
|
||||
],
|
||||
|
||||
'jsdoc/require-param-description': 'off',
|
||||
'jsdoc/require-returns-description': 'off',
|
||||
'jsdoc/require-jsdoc': 'off',
|
||||
'jsdoc/valid-types': 'off',
|
||||
'jsdoc/no-undefined-types': 'off',
|
||||
'jsdoc/require-returns': 'off',
|
||||
|
||||
// react
|
||||
'react/display-name': 'off',
|
||||
'react/react-in-jsx-scope': 'warn',
|
||||
'react/forbid-prop-types': 'warn',
|
||||
'react/jsx-boolean-value': 'warn',
|
||||
'react/jsx-closing-bracket-location': 'off', // can not configure for our code style
|
||||
'react/jsx-curly-spacing': 'warn',
|
||||
'react/jsx-handler-names': [
|
||||
'warn',
|
||||
{ eventHandlerPrefix: 'on', eventHandlerPropPrefix: 'on' },
|
||||
],
|
||||
'react/jsx-key': 'warn',
|
||||
'react/jsx-max-props-per-line': 'off',
|
||||
'react/jsx-no-bind': 'off',
|
||||
'react/jsx-no-duplicate-props': 'warn',
|
||||
'react/jsx-no-literals': 'off',
|
||||
'react/jsx-no-undef': 'error',
|
||||
'react/jsx-pascal-case': 'warn',
|
||||
'react/jsx-uses-react': 'warn',
|
||||
'react/jsx-uses-vars': 'warn',
|
||||
'react/jsx-no-comment-textnodes': 'warn',
|
||||
'react/jsx-wrap-multilines': 'warn',
|
||||
'react/no-deprecated': 'error',
|
||||
'react/no-did-mount-set-state': 'warn',
|
||||
'react/no-did-update-set-state': 'warn',
|
||||
'react/no-direct-mutation-state': 'warn',
|
||||
'react/require-render-return': 'warn',
|
||||
'react/no-is-mounted': 'warn',
|
||||
'react/no-multi-comp': 'off',
|
||||
'react/no-string-refs': 'warn',
|
||||
'react/no-unknown-property': 'warn',
|
||||
'react/prefer-es6-class': 'warn',
|
||||
'react/prop-types': 'off', // using ts for this task
|
||||
'react/self-closing-comp': 'warn',
|
||||
'react/sort-comp': 'off',
|
||||
|
||||
// ts
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
'@typescript-eslint/ban-ts-ignore': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/ban-types': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-inferrable-types': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
vars: 'all',
|
||||
args: 'after-used',
|
||||
argsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
10
.prettierrc
10
.prettierrc
@ -1,6 +1,8 @@
|
||||
{
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"proseWrap": "always",
|
||||
"endOfLine": "lf"
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"proseWrap": "always",
|
||||
"endOfLine": "lf",
|
||||
"tabWidth": 4,
|
||||
"printWidth": 120
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import storyDecorator from './storyDecorator';
|
||||
const req = require.context('../packages/app', true, /\.story\.[tj]sx?$/);
|
||||
|
||||
function loadStories() {
|
||||
req.keys().forEach((filename) => req(filename));
|
||||
req.keys().forEach((filename) => req(filename));
|
||||
}
|
||||
|
||||
addDecorator(storyDecorator);
|
||||
|
@ -2,40 +2,37 @@ import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Channel } from '@storybook/channels';
|
||||
import { setIntlConfig } from 'storybook-addon-intl';
|
||||
import {
|
||||
EVENT_SET_LOCALE_ID,
|
||||
EVENT_GET_LOCALE_ID,
|
||||
} from 'storybook-addon-intl/dist/shared';
|
||||
import { EVENT_SET_LOCALE_ID, EVENT_GET_LOCALE_ID } from 'storybook-addon-intl/dist/shared';
|
||||
import { SUPPORTED_LANGUAGES, DEFAULT_LANGUAGE } from 'app/services/i18n';
|
||||
import { setLocale } from 'app/components/i18n/actions';
|
||||
|
||||
setIntlConfig({
|
||||
locales: SUPPORTED_LANGUAGES,
|
||||
defaultLocale: DEFAULT_LANGUAGE,
|
||||
locales: SUPPORTED_LANGUAGES,
|
||||
defaultLocale: DEFAULT_LANGUAGE,
|
||||
});
|
||||
|
||||
const IntlDecorator: React.ComponentType<{
|
||||
channel: Channel;
|
||||
channel: Channel;
|
||||
}> = ({ channel, children }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
React.useEffect(() => {
|
||||
const onLocaleChange = (locale: string) => {
|
||||
dispatch(setLocale(locale));
|
||||
};
|
||||
React.useEffect(() => {
|
||||
const onLocaleChange = (locale: string) => {
|
||||
dispatch(setLocale(locale));
|
||||
};
|
||||
|
||||
// Listen for change of locale
|
||||
channel.on(EVENT_SET_LOCALE_ID, onLocaleChange);
|
||||
// Listen for change of locale
|
||||
channel.on(EVENT_SET_LOCALE_ID, onLocaleChange);
|
||||
|
||||
// Request the current locale
|
||||
channel.emit(EVENT_GET_LOCALE_ID);
|
||||
// Request the current locale
|
||||
channel.emit(EVENT_GET_LOCALE_ID);
|
||||
|
||||
return () => {
|
||||
channel.removeListener(EVENT_SET_LOCALE_ID, onLocaleChange);
|
||||
};
|
||||
}, [channel]);
|
||||
return () => {
|
||||
channel.removeListener(EVENT_SET_LOCALE_ID, onLocaleChange);
|
||||
};
|
||||
}, [channel]);
|
||||
|
||||
return children as React.ReactElement;
|
||||
return children as React.ReactElement;
|
||||
};
|
||||
|
||||
export default IntlDecorator;
|
||||
|
@ -10,11 +10,11 @@ import { IntlDecorator } from './decorators';
|
||||
const store = storeFactory();
|
||||
|
||||
export default ((story) => {
|
||||
const channel = addons.getChannel();
|
||||
const channel = addons.getChannel();
|
||||
|
||||
return (
|
||||
<ContextProvider store={store} history={browserHistory}>
|
||||
<IntlDecorator channel={channel}>{story()}</IntlDecorator>
|
||||
</ContextProvider>
|
||||
);
|
||||
return (
|
||||
<ContextProvider store={store} history={browserHistory}>
|
||||
<IntlDecorator channel={channel}>{story()}</IntlDecorator>
|
||||
</ContextProvider>
|
||||
);
|
||||
}) as DecoratorFunction<React.ReactElement>;
|
||||
|
@ -1,14 +1,14 @@
|
||||
const rootConfig = require('../webpack.config');
|
||||
|
||||
module.exports = async ({ config }) => ({
|
||||
...config,
|
||||
resolve: rootConfig.resolve,
|
||||
module: {
|
||||
...config.module,
|
||||
// our rules should satisfy all storybook needs,
|
||||
// so replace all storybook defaults with our rules
|
||||
rules: rootConfig.module.rules,
|
||||
},
|
||||
...config,
|
||||
resolve: rootConfig.resolve,
|
||||
module: {
|
||||
...config.module,
|
||||
// our rules should satisfy all storybook needs,
|
||||
// so replace all storybook defaults with our rules
|
||||
rules: rootConfig.module.rules,
|
||||
},
|
||||
|
||||
resolveLoader: rootConfig.resolveLoader,
|
||||
resolveLoader: rootConfig.resolveLoader,
|
||||
});
|
||||
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -1,3 +1,3 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
|
392
@types/chalk.d.ts
vendored
392
@types/chalk.d.ts
vendored
@ -4,141 +4,141 @@
|
||||
*/
|
||||
|
||||
declare module 'chalk' {
|
||||
const enum LevelEnum {
|
||||
/**
|
||||
const enum LevelEnum {
|
||||
/**
|
||||
All colors disabled.
|
||||
*/
|
||||
None = 0,
|
||||
None = 0,
|
||||
|
||||
/**
|
||||
/**
|
||||
Basic 16 colors support.
|
||||
*/
|
||||
Basic = 1,
|
||||
Basic = 1,
|
||||
|
||||
/**
|
||||
/**
|
||||
ANSI 256 colors support.
|
||||
*/
|
||||
Ansi256 = 2,
|
||||
Ansi256 = 2,
|
||||
|
||||
/**
|
||||
/**
|
||||
Truecolor 16 million colors support.
|
||||
*/
|
||||
TrueColor = 3,
|
||||
}
|
||||
TrueColor = 3,
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
Basic foreground colors.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/master/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
type ForegroundColor =
|
||||
| 'black'
|
||||
| 'red'
|
||||
| 'green'
|
||||
| 'yellow'
|
||||
| 'blue'
|
||||
| 'magenta'
|
||||
| 'cyan'
|
||||
| 'white'
|
||||
| 'gray'
|
||||
| 'grey'
|
||||
| 'blackBright'
|
||||
| 'redBright'
|
||||
| 'greenBright'
|
||||
| 'yellowBright'
|
||||
| 'blueBright'
|
||||
| 'magentaBright'
|
||||
| 'cyanBright'
|
||||
| 'whiteBright';
|
||||
type ForegroundColor =
|
||||
| 'black'
|
||||
| 'red'
|
||||
| 'green'
|
||||
| 'yellow'
|
||||
| 'blue'
|
||||
| 'magenta'
|
||||
| 'cyan'
|
||||
| 'white'
|
||||
| 'gray'
|
||||
| 'grey'
|
||||
| 'blackBright'
|
||||
| 'redBright'
|
||||
| 'greenBright'
|
||||
| 'yellowBright'
|
||||
| 'blueBright'
|
||||
| 'magentaBright'
|
||||
| 'cyanBright'
|
||||
| 'whiteBright';
|
||||
|
||||
/**
|
||||
/**
|
||||
Basic background colors.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/master/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
type BackgroundColor =
|
||||
| 'bgBlack'
|
||||
| 'bgRed'
|
||||
| 'bgGreen'
|
||||
| 'bgYellow'
|
||||
| 'bgBlue'
|
||||
| 'bgMagenta'
|
||||
| 'bgCyan'
|
||||
| 'bgWhite'
|
||||
| 'bgGray'
|
||||
| 'bgGrey'
|
||||
| 'bgBlackBright'
|
||||
| 'bgRedBright'
|
||||
| 'bgGreenBright'
|
||||
| 'bgYellowBright'
|
||||
| 'bgBlueBright'
|
||||
| 'bgMagentaBright'
|
||||
| 'bgCyanBright'
|
||||
| 'bgWhiteBright';
|
||||
type BackgroundColor =
|
||||
| 'bgBlack'
|
||||
| 'bgRed'
|
||||
| 'bgGreen'
|
||||
| 'bgYellow'
|
||||
| 'bgBlue'
|
||||
| 'bgMagenta'
|
||||
| 'bgCyan'
|
||||
| 'bgWhite'
|
||||
| 'bgGray'
|
||||
| 'bgGrey'
|
||||
| 'bgBlackBright'
|
||||
| 'bgRedBright'
|
||||
| 'bgGreenBright'
|
||||
| 'bgYellowBright'
|
||||
| 'bgBlueBright'
|
||||
| 'bgMagentaBright'
|
||||
| 'bgCyanBright'
|
||||
| 'bgWhiteBright';
|
||||
|
||||
/**
|
||||
/**
|
||||
Basic colors.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/master/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
type Color = ForegroundColor | BackgroundColor;
|
||||
type Color = ForegroundColor | BackgroundColor;
|
||||
|
||||
type Modifiers =
|
||||
| 'reset'
|
||||
| 'bold'
|
||||
| 'dim'
|
||||
| 'italic'
|
||||
| 'underline'
|
||||
| 'inverse'
|
||||
| 'hidden'
|
||||
| 'strikethrough'
|
||||
| 'visible';
|
||||
type Modifiers =
|
||||
| 'reset'
|
||||
| 'bold'
|
||||
| 'dim'
|
||||
| 'italic'
|
||||
| 'underline'
|
||||
| 'inverse'
|
||||
| 'hidden'
|
||||
| 'strikethrough'
|
||||
| 'visible';
|
||||
|
||||
namespace chalk {
|
||||
type Level = LevelEnum;
|
||||
namespace chalk {
|
||||
type Level = LevelEnum;
|
||||
|
||||
interface Options {
|
||||
/**
|
||||
interface Options {
|
||||
/**
|
||||
Specify the color support for Chalk.
|
||||
By default, color support is automatically detected based on the environment.
|
||||
*/
|
||||
level?: Level;
|
||||
}
|
||||
level?: Level;
|
||||
}
|
||||
|
||||
interface Instance {
|
||||
/**
|
||||
interface Instance {
|
||||
/**
|
||||
Return a new Chalk instance.
|
||||
*/
|
||||
new (options?: Options): Chalk;
|
||||
}
|
||||
new (options?: Options): Chalk;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
Detect whether the terminal supports color.
|
||||
*/
|
||||
interface ColorSupport {
|
||||
/**
|
||||
interface ColorSupport {
|
||||
/**
|
||||
The color level used by Chalk.
|
||||
*/
|
||||
level: Level;
|
||||
level: Level;
|
||||
|
||||
/**
|
||||
/**
|
||||
Return whether Chalk supports basic 16 colors.
|
||||
*/
|
||||
hasBasic: boolean;
|
||||
hasBasic: boolean;
|
||||
|
||||
/**
|
||||
/**
|
||||
Return whether Chalk supports ANSI 256 colors.
|
||||
*/
|
||||
has256: boolean;
|
||||
has256: boolean;
|
||||
|
||||
/**
|
||||
/**
|
||||
Return whether Chalk supports Truecolor 16 million colors.
|
||||
*/
|
||||
has16m: boolean;
|
||||
}
|
||||
has16m: boolean;
|
||||
}
|
||||
|
||||
interface ChalkFunction {
|
||||
/**
|
||||
interface ChalkFunction {
|
||||
/**
|
||||
Use a template string.
|
||||
|
||||
@remarks Template literals are unsupported for nested calls (see [issue #341](https://github.com/chalk/chalk/issues/341))
|
||||
@ -154,24 +154,24 @@ declare module 'chalk' {
|
||||
`);
|
||||
```
|
||||
*/
|
||||
(text: TemplateStringsArray, ...placeholders: unknown[]): string;
|
||||
(text: TemplateStringsArray, ...placeholders: unknown[]): string;
|
||||
|
||||
(...text: unknown[]): string;
|
||||
}
|
||||
(...text: unknown[]): string;
|
||||
}
|
||||
|
||||
interface Chalk extends ChalkFunction {
|
||||
/**
|
||||
interface Chalk extends ChalkFunction {
|
||||
/**
|
||||
Return a new Chalk instance.
|
||||
*/
|
||||
Instance: Instance;
|
||||
Instance: Instance;
|
||||
|
||||
/**
|
||||
/**
|
||||
The color support for Chalk.
|
||||
By default, color support is automatically detected based on the environment.
|
||||
*/
|
||||
level: Level;
|
||||
level: Level;
|
||||
|
||||
/**
|
||||
/**
|
||||
Use HEX value to set text color.
|
||||
|
||||
@param color - Hexadecimal value representing the desired color.
|
||||
@ -183,9 +183,9 @@ declare module 'chalk' {
|
||||
chalk.hex('#DEADED');
|
||||
```
|
||||
*/
|
||||
hex(color: string): Chalk;
|
||||
hex(color: string): Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Use keyword color value to set text color.
|
||||
|
||||
@param color - Keyword value representing the desired color.
|
||||
@ -197,42 +197,42 @@ declare module 'chalk' {
|
||||
chalk.keyword('orange');
|
||||
```
|
||||
*/
|
||||
keyword(color: string): Chalk;
|
||||
keyword(color: string): Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Use RGB values to set text color.
|
||||
*/
|
||||
rgb(red: number, green: number, blue: number): Chalk;
|
||||
rgb(red: number, green: number, blue: number): Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Use HSL values to set text color.
|
||||
*/
|
||||
hsl(hue: number, saturation: number, lightness: number): Chalk;
|
||||
hsl(hue: number, saturation: number, lightness: number): Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Use HSV values to set text color.
|
||||
*/
|
||||
hsv(hue: number, saturation: number, value: number): Chalk;
|
||||
hsv(hue: number, saturation: number, value: number): Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Use HWB values to set text color.
|
||||
*/
|
||||
hwb(hue: number, whiteness: number, blackness: number): Chalk;
|
||||
hwb(hue: number, whiteness: number, blackness: number): Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Use a [Select/Set Graphic Rendition](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters) (SGR) [color code number](https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit) to set text color.
|
||||
|
||||
30 <= code && code < 38 || 90 <= code && code < 98
|
||||
For example, 31 for red, 91 for redBright.
|
||||
*/
|
||||
ansi(code: number): Chalk;
|
||||
ansi(code: number): Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Use a [8-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) to set text color.
|
||||
*/
|
||||
ansi256(index: number): Chalk;
|
||||
ansi256(index: number): Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Use HEX value to set background color.
|
||||
|
||||
@param color - Hexadecimal value representing the desired color.
|
||||
@ -244,9 +244,9 @@ declare module 'chalk' {
|
||||
chalk.bgHex('#DEADED');
|
||||
```
|
||||
*/
|
||||
bgHex(color: string): Chalk;
|
||||
bgHex(color: string): Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Use keyword color value to set background color.
|
||||
|
||||
@param color - Keyword value representing the desired color.
|
||||
@ -258,162 +258,162 @@ declare module 'chalk' {
|
||||
chalk.bgKeyword('orange');
|
||||
```
|
||||
*/
|
||||
bgKeyword(color: string): Chalk;
|
||||
bgKeyword(color: string): Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Use RGB values to set background color.
|
||||
*/
|
||||
bgRgb(red: number, green: number, blue: number): Chalk;
|
||||
bgRgb(red: number, green: number, blue: number): Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Use HSL values to set background color.
|
||||
*/
|
||||
bgHsl(hue: number, saturation: number, lightness: number): Chalk;
|
||||
bgHsl(hue: number, saturation: number, lightness: number): Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Use HSV values to set background color.
|
||||
*/
|
||||
bgHsv(hue: number, saturation: number, value: number): Chalk;
|
||||
bgHsv(hue: number, saturation: number, value: number): Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Use HWB values to set background color.
|
||||
*/
|
||||
bgHwb(hue: number, whiteness: number, blackness: number): Chalk;
|
||||
bgHwb(hue: number, whiteness: number, blackness: number): Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Use a [Select/Set Graphic Rendition](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters) (SGR) [color code number](https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit) to set background color.
|
||||
|
||||
30 <= code && code < 38 || 90 <= code && code < 98
|
||||
For example, 31 for red, 91 for redBright.
|
||||
Use the foreground code, not the background code (for example, not 41, nor 101).
|
||||
*/
|
||||
bgAnsi(code: number): Chalk;
|
||||
bgAnsi(code: number): Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Use a [8-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) to set background color.
|
||||
*/
|
||||
bgAnsi256(index: number): Chalk;
|
||||
bgAnsi256(index: number): Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Modifier: Resets the current color chain.
|
||||
*/
|
||||
readonly reset: Chalk;
|
||||
readonly reset: Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Modifier: Make text bold.
|
||||
*/
|
||||
readonly bold: Chalk;
|
||||
readonly bold: Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Modifier: Emitting only a small amount of light.
|
||||
*/
|
||||
readonly dim: Chalk;
|
||||
readonly dim: Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Modifier: Make text italic. (Not widely supported)
|
||||
*/
|
||||
readonly italic: Chalk;
|
||||
readonly italic: Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Modifier: Make text underline. (Not widely supported)
|
||||
*/
|
||||
readonly underline: Chalk;
|
||||
readonly underline: Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Modifier: Inverse background and foreground colors.
|
||||
*/
|
||||
readonly inverse: Chalk;
|
||||
readonly inverse: Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Modifier: Prints the text, but makes it invisible.
|
||||
*/
|
||||
readonly hidden: Chalk;
|
||||
readonly hidden: Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Modifier: Puts a horizontal line through the center of the text. (Not widely supported)
|
||||
*/
|
||||
readonly strikethrough: Chalk;
|
||||
readonly strikethrough: Chalk;
|
||||
|
||||
/**
|
||||
/**
|
||||
Modifier: Prints the text only when Chalk has a color support level > 0.
|
||||
Can be useful for things that are purely cosmetic.
|
||||
*/
|
||||
readonly visible: Chalk;
|
||||
readonly visible: Chalk;
|
||||
|
||||
readonly black: Chalk;
|
||||
readonly red: Chalk;
|
||||
readonly green: Chalk;
|
||||
readonly yellow: Chalk;
|
||||
readonly blue: Chalk;
|
||||
readonly magenta: Chalk;
|
||||
readonly cyan: Chalk;
|
||||
readonly white: Chalk;
|
||||
readonly black: Chalk;
|
||||
readonly red: Chalk;
|
||||
readonly green: Chalk;
|
||||
readonly yellow: Chalk;
|
||||
readonly blue: Chalk;
|
||||
readonly magenta: Chalk;
|
||||
readonly cyan: Chalk;
|
||||
readonly white: Chalk;
|
||||
|
||||
/*
|
||||
/*
|
||||
Alias for `blackBright`.
|
||||
*/
|
||||
readonly gray: Chalk;
|
||||
readonly gray: Chalk;
|
||||
|
||||
/*
|
||||
/*
|
||||
Alias for `blackBright`.
|
||||
*/
|
||||
readonly grey: Chalk;
|
||||
readonly grey: Chalk;
|
||||
|
||||
readonly blackBright: Chalk;
|
||||
readonly redBright: Chalk;
|
||||
readonly greenBright: Chalk;
|
||||
readonly yellowBright: Chalk;
|
||||
readonly blueBright: Chalk;
|
||||
readonly magentaBright: Chalk;
|
||||
readonly cyanBright: Chalk;
|
||||
readonly whiteBright: Chalk;
|
||||
readonly blackBright: Chalk;
|
||||
readonly redBright: Chalk;
|
||||
readonly greenBright: Chalk;
|
||||
readonly yellowBright: Chalk;
|
||||
readonly blueBright: Chalk;
|
||||
readonly magentaBright: Chalk;
|
||||
readonly cyanBright: Chalk;
|
||||
readonly whiteBright: Chalk;
|
||||
|
||||
readonly bgBlack: Chalk;
|
||||
readonly bgRed: Chalk;
|
||||
readonly bgGreen: Chalk;
|
||||
readonly bgYellow: Chalk;
|
||||
readonly bgBlue: Chalk;
|
||||
readonly bgMagenta: Chalk;
|
||||
readonly bgCyan: Chalk;
|
||||
readonly bgWhite: Chalk;
|
||||
readonly bgBlack: Chalk;
|
||||
readonly bgRed: Chalk;
|
||||
readonly bgGreen: Chalk;
|
||||
readonly bgYellow: Chalk;
|
||||
readonly bgBlue: Chalk;
|
||||
readonly bgMagenta: Chalk;
|
||||
readonly bgCyan: Chalk;
|
||||
readonly bgWhite: Chalk;
|
||||
|
||||
/*
|
||||
/*
|
||||
Alias for `bgBlackBright`.
|
||||
*/
|
||||
readonly bgGray: Chalk;
|
||||
readonly bgGray: Chalk;
|
||||
|
||||
/*
|
||||
/*
|
||||
Alias for `bgBlackBright`.
|
||||
*/
|
||||
readonly bgGrey: Chalk;
|
||||
readonly bgGrey: Chalk;
|
||||
|
||||
readonly bgBlackBright: Chalk;
|
||||
readonly bgRedBright: Chalk;
|
||||
readonly bgGreenBright: Chalk;
|
||||
readonly bgYellowBright: Chalk;
|
||||
readonly bgBlueBright: Chalk;
|
||||
readonly bgMagentaBright: Chalk;
|
||||
readonly bgCyanBright: Chalk;
|
||||
readonly bgWhiteBright: Chalk;
|
||||
readonly bgBlackBright: Chalk;
|
||||
readonly bgRedBright: Chalk;
|
||||
readonly bgGreenBright: Chalk;
|
||||
readonly bgYellowBright: Chalk;
|
||||
readonly bgBlueBright: Chalk;
|
||||
readonly bgMagentaBright: Chalk;
|
||||
readonly bgCyanBright: Chalk;
|
||||
readonly bgWhiteBright: Chalk;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
Main Chalk object that allows to chain styles together.
|
||||
Call the last one as a method with a string argument.
|
||||
Order doesn't matter, and later styles take precedent in case of a conflict.
|
||||
This simply means that `chalk.red.yellow.green` is equivalent to `chalk.green`.
|
||||
*/
|
||||
const chalk: chalk.Chalk &
|
||||
chalk.ChalkFunction & {
|
||||
supportsColor: chalk.ColorSupport | false;
|
||||
Level: LevelEnum;
|
||||
Color: Color;
|
||||
ForegroundColor: ForegroundColor;
|
||||
BackgroundColor: BackgroundColor;
|
||||
Modifiers: Modifiers;
|
||||
stderr: chalk.Chalk & { supportsColor: chalk.ColorSupport | false };
|
||||
};
|
||||
const chalk: chalk.Chalk &
|
||||
chalk.ChalkFunction & {
|
||||
supportsColor: chalk.ColorSupport | false;
|
||||
Level: LevelEnum;
|
||||
Color: Color;
|
||||
ForegroundColor: ForegroundColor;
|
||||
BackgroundColor: BackgroundColor;
|
||||
Modifiers: Modifiers;
|
||||
stderr: chalk.Chalk & { supportsColor: chalk.ColorSupport | false };
|
||||
};
|
||||
|
||||
export = chalk;
|
||||
export = chalk;
|
||||
}
|
||||
|
186
@types/cwordin-api.d.ts
vendored
186
@types/cwordin-api.d.ts
vendored
@ -1,102 +1,98 @@
|
||||
declare module 'crowdin-api' {
|
||||
export interface ProjectInfoFile {
|
||||
node_type: 'file';
|
||||
id: number;
|
||||
name: string;
|
||||
created: string;
|
||||
last_updated: string;
|
||||
last_accessed: string;
|
||||
last_revision: string;
|
||||
}
|
||||
|
||||
export interface ProjectInfoDirectory {
|
||||
node_type: 'directory';
|
||||
id: number;
|
||||
name: string;
|
||||
files: Array<ProjectInfoFile | ProjectInfoDirectory>;
|
||||
}
|
||||
|
||||
export interface ProjectInfoResponse {
|
||||
details: {
|
||||
source_language: {
|
||||
export interface ProjectInfoFile {
|
||||
node_type: 'file';
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
};
|
||||
name: string;
|
||||
identifier: string;
|
||||
created: string;
|
||||
description: string;
|
||||
join_policy: string;
|
||||
last_build: string | null;
|
||||
last_activity: string;
|
||||
participants_count: string; // it's number, but string in the response
|
||||
logo_url: string | null;
|
||||
total_strings_count: string; // it's number, but string in the response
|
||||
total_words_count: string; // it's number, but string in the response
|
||||
duplicate_strings_count: number;
|
||||
duplicate_words_count: number;
|
||||
invite_url: {
|
||||
translator: string;
|
||||
proofreader: string;
|
||||
};
|
||||
};
|
||||
languages: Array<{
|
||||
name: string; // English language name
|
||||
code: string;
|
||||
can_translate: 0 | 1;
|
||||
can_approve: 0 | 1;
|
||||
}>;
|
||||
files: Array<ProjectInfoFile | ProjectInfoDirectory>;
|
||||
}
|
||||
created: string;
|
||||
last_updated: string;
|
||||
last_accessed: string;
|
||||
last_revision: string;
|
||||
}
|
||||
|
||||
export interface LanguageStatusNode {
|
||||
node_type: 'directory' | 'file';
|
||||
id: number;
|
||||
name: string;
|
||||
phrases: number;
|
||||
translated: number;
|
||||
approved: number;
|
||||
words: number;
|
||||
words_translated: number;
|
||||
words_approved: number;
|
||||
files: Array<LanguageStatusNode>;
|
||||
}
|
||||
export interface ProjectInfoDirectory {
|
||||
node_type: 'directory';
|
||||
id: number;
|
||||
name: string;
|
||||
files: Array<ProjectInfoFile | ProjectInfoDirectory>;
|
||||
}
|
||||
|
||||
export interface LanguageStatusResponse {
|
||||
files: Array<LanguageStatusNode>;
|
||||
}
|
||||
export interface ProjectInfoResponse {
|
||||
details: {
|
||||
source_language: {
|
||||
name: string;
|
||||
code: string;
|
||||
};
|
||||
name: string;
|
||||
identifier: string;
|
||||
created: string;
|
||||
description: string;
|
||||
join_policy: string;
|
||||
last_build: string | null;
|
||||
last_activity: string;
|
||||
participants_count: string; // it's number, but string in the response
|
||||
logo_url: string | null;
|
||||
total_strings_count: string; // it's number, but string in the response
|
||||
total_words_count: string; // it's number, but string in the response
|
||||
duplicate_strings_count: number;
|
||||
duplicate_words_count: number;
|
||||
invite_url: {
|
||||
translator: string;
|
||||
proofreader: string;
|
||||
};
|
||||
};
|
||||
languages: Array<{
|
||||
name: string; // English language name
|
||||
code: string;
|
||||
can_translate: 0 | 1;
|
||||
can_approve: 0 | 1;
|
||||
}>;
|
||||
files: Array<ProjectInfoFile | ProjectInfoDirectory>;
|
||||
}
|
||||
|
||||
type FilesList = Record<string, string | ReadableStream>;
|
||||
export interface LanguageStatusNode {
|
||||
node_type: 'directory' | 'file';
|
||||
id: number;
|
||||
name: string;
|
||||
phrases: number;
|
||||
translated: number;
|
||||
approved: number;
|
||||
words: number;
|
||||
words_translated: number;
|
||||
words_approved: number;
|
||||
files: Array<LanguageStatusNode>;
|
||||
}
|
||||
|
||||
export default class CrowdinApi {
|
||||
constructor(params: {
|
||||
apiKey: string;
|
||||
projectName: string;
|
||||
baseUrl?: string;
|
||||
});
|
||||
projectInfo(): Promise<ProjectInfoResponse>;
|
||||
languageStatus(language: string): Promise<LanguageStatusResponse>;
|
||||
exportFile(
|
||||
file: string,
|
||||
language: string,
|
||||
params?: {
|
||||
branch?: string;
|
||||
format?: 'xliff';
|
||||
export_translated_only?: boolean;
|
||||
export_approved_only?: boolean;
|
||||
},
|
||||
): Promise<string>; // TODO: not sure about Promise return type
|
||||
updateFile(
|
||||
files: FilesList,
|
||||
params: {
|
||||
titles?: Record<string, string>;
|
||||
export_patterns?: Record<string, string>;
|
||||
new_names?: Record<string, string>;
|
||||
first_line_contains_header?: string;
|
||||
scheme?: string;
|
||||
update_option?: 'update_as_unapproved' | 'update_without_changes';
|
||||
branch?: string;
|
||||
},
|
||||
): Promise<void>;
|
||||
}
|
||||
export interface LanguageStatusResponse {
|
||||
files: Array<LanguageStatusNode>;
|
||||
}
|
||||
|
||||
type FilesList = Record<string, string | ReadableStream>;
|
||||
|
||||
export default class CrowdinApi {
|
||||
constructor(params: { apiKey: string; projectName: string; baseUrl?: string });
|
||||
projectInfo(): Promise<ProjectInfoResponse>;
|
||||
languageStatus(language: string): Promise<LanguageStatusResponse>;
|
||||
exportFile(
|
||||
file: string,
|
||||
language: string,
|
||||
params?: {
|
||||
branch?: string;
|
||||
format?: 'xliff';
|
||||
export_translated_only?: boolean;
|
||||
export_approved_only?: boolean;
|
||||
},
|
||||
): Promise<string>; // TODO: not sure about Promise return type
|
||||
updateFile(
|
||||
files: FilesList,
|
||||
params: {
|
||||
titles?: Record<string, string>;
|
||||
export_patterns?: Record<string, string>;
|
||||
new_names?: Record<string, string>;
|
||||
first_line_contains_header?: string;
|
||||
scheme?: string;
|
||||
update_option?: 'update_as_unapproved' | 'update_without_changes';
|
||||
branch?: string;
|
||||
},
|
||||
): Promise<void>;
|
||||
}
|
||||
}
|
||||
|
19
@types/multi-progress.d.ts
vendored
19
@types/multi-progress.d.ts
vendored
@ -1,13 +1,10 @@
|
||||
declare module 'multi-progress' {
|
||||
export default class MultiProgress {
|
||||
constructor(stream?: string);
|
||||
newBar(
|
||||
schema: string,
|
||||
options: ProgressBar.ProgressBarOptions,
|
||||
): ProgressBar;
|
||||
terminate(): void;
|
||||
move(index: number): void;
|
||||
tick(index: number, value?: number, options?: any): void;
|
||||
update(index: number, value?: number, options?: any): void;
|
||||
}
|
||||
export default class MultiProgress {
|
||||
constructor(stream?: string);
|
||||
newBar(schema: string, options: ProgressBar.ProgressBarOptions): ProgressBar;
|
||||
terminate(): void;
|
||||
move(index: number): void;
|
||||
tick(index: number, value?: number, options?: any): void;
|
||||
update(index: number, value?: number, options?: any): void;
|
||||
}
|
||||
}
|
||||
|
106
@types/prompt.d.ts
vendored
106
@types/prompt.d.ts
vendored
@ -2,65 +2,55 @@
|
||||
// Project: https://github.com/flatiron/prompt
|
||||
|
||||
declare module 'prompt' {
|
||||
type PropertiesType =
|
||||
| Array<string>
|
||||
| prompt.PromptSchema
|
||||
| Array<prompt.PromptPropertyOptions>;
|
||||
type PropertiesType = Array<string> | prompt.PromptSchema | Array<prompt.PromptPropertyOptions>;
|
||||
|
||||
namespace prompt {
|
||||
interface PromptSchema {
|
||||
properties: PromptProperties;
|
||||
namespace prompt {
|
||||
interface PromptSchema {
|
||||
properties: PromptProperties;
|
||||
}
|
||||
|
||||
interface PromptProperties {
|
||||
[propName: string]: PromptPropertyOptions;
|
||||
}
|
||||
|
||||
interface PromptPropertyOptions {
|
||||
name?: string;
|
||||
pattern?: RegExp;
|
||||
message?: string;
|
||||
required?: boolean;
|
||||
hidden?: boolean;
|
||||
description?: string;
|
||||
type?: string;
|
||||
default?: string;
|
||||
before?: (value: any) => any;
|
||||
conform?: (result: any) => boolean;
|
||||
}
|
||||
|
||||
export function start(): void;
|
||||
|
||||
export function get<T extends PropertiesType>(
|
||||
properties: T,
|
||||
callback: (
|
||||
err: Error,
|
||||
result: T extends Array<string>
|
||||
? Record<T[number], string>
|
||||
: T extends PromptSchema
|
||||
? Record<keyof T['properties'], string>
|
||||
: T extends Array<PromptPropertyOptions>
|
||||
? Record<T[number]['name'] extends string ? T[number]['name'] : number, string>
|
||||
: never,
|
||||
) => void,
|
||||
): void;
|
||||
|
||||
export function addProperties(obj: any, properties: PropertiesType, callback: (err: Error) => void): void;
|
||||
|
||||
export function history(propertyName: string): any;
|
||||
|
||||
export let override: any;
|
||||
export let colors: boolean;
|
||||
export let message: string;
|
||||
export let delimiter: string;
|
||||
}
|
||||
|
||||
interface PromptProperties {
|
||||
[propName: string]: PromptPropertyOptions;
|
||||
}
|
||||
|
||||
interface PromptPropertyOptions {
|
||||
name?: string;
|
||||
pattern?: RegExp;
|
||||
message?: string;
|
||||
required?: boolean;
|
||||
hidden?: boolean;
|
||||
description?: string;
|
||||
type?: string;
|
||||
default?: string;
|
||||
before?: (value: any) => any;
|
||||
conform?: (result: any) => boolean;
|
||||
}
|
||||
|
||||
export function start(): void;
|
||||
|
||||
export function get<T extends PropertiesType>(
|
||||
properties: T,
|
||||
callback: (
|
||||
err: Error,
|
||||
result: T extends Array<string>
|
||||
? Record<T[number], string>
|
||||
: T extends PromptSchema
|
||||
? Record<keyof T['properties'], string>
|
||||
: T extends Array<PromptPropertyOptions>
|
||||
? Record<
|
||||
T[number]['name'] extends string ? T[number]['name'] : number,
|
||||
string
|
||||
>
|
||||
: never,
|
||||
) => void,
|
||||
): void;
|
||||
|
||||
export function addProperties(
|
||||
obj: any,
|
||||
properties: PropertiesType,
|
||||
callback: (err: Error) => void,
|
||||
): void;
|
||||
|
||||
export function history(propertyName: string): any;
|
||||
|
||||
export let override: any;
|
||||
export let colors: boolean;
|
||||
export let message: string;
|
||||
export let delimiter: string;
|
||||
}
|
||||
|
||||
export = prompt;
|
||||
export = prompt;
|
||||
}
|
||||
|
26
@types/redux-localstorage.d.ts
vendored
26
@types/redux-localstorage.d.ts
vendored
@ -3,23 +3,13 @@
|
||||
// import * as Redux from 'redux';
|
||||
|
||||
declare module 'redux-localstorage' {
|
||||
export interface ConfigRS {
|
||||
key: string;
|
||||
merge?: any;
|
||||
slicer?: any;
|
||||
serialize?: (
|
||||
value: any,
|
||||
replacer?: (key: string, value: any) => any,
|
||||
space?: string | number,
|
||||
) => string;
|
||||
deserialize?: (
|
||||
text: string,
|
||||
reviver?: (key: any, value: any) => any,
|
||||
) => any;
|
||||
}
|
||||
export interface ConfigRS {
|
||||
key: string;
|
||||
merge?: any;
|
||||
slicer?: any;
|
||||
serialize?: (value: any, replacer?: (key: string, value: any) => any, space?: string | number) => string;
|
||||
deserialize?: (text: string, reviver?: (key: any, value: any) => any) => any;
|
||||
}
|
||||
|
||||
export default function persistState(
|
||||
paths: string | string[],
|
||||
config: ConfigRS,
|
||||
): () => any;
|
||||
export default function persistState(paths: string | string[], config: ConfigRS): () => any;
|
||||
}
|
||||
|
153
@types/unexpected.d.ts
vendored
153
@types/unexpected.d.ts
vendored
@ -1,86 +1,77 @@
|
||||
declare module 'unexpected' {
|
||||
namespace unexpected {
|
||||
interface EnchantedPromise<T> extends Promise<T> {
|
||||
and<A extends Array<unknown> = []>(
|
||||
assertionName: string,
|
||||
subject: unknown,
|
||||
...args: A
|
||||
): EnchantedPromise<any>;
|
||||
namespace unexpected {
|
||||
interface EnchantedPromise<T> extends Promise<T> {
|
||||
and<A extends Array<unknown> = []>(
|
||||
assertionName: string,
|
||||
subject: unknown,
|
||||
...args: A
|
||||
): EnchantedPromise<any>;
|
||||
}
|
||||
|
||||
interface Expect {
|
||||
/**
|
||||
* @see http://unexpected.js.org/api/expect/
|
||||
*/
|
||||
<A extends Array<unknown> = []>(subject: unknown, assertionName: string, ...args: A): EnchantedPromise<any>;
|
||||
|
||||
it<A extends Array<unknown> = []>(
|
||||
assertionName: string,
|
||||
subject?: unknown,
|
||||
...args: A
|
||||
): EnchantedPromise<any>;
|
||||
|
||||
/**
|
||||
* @see http://unexpected.js.org/api/clone/
|
||||
*/
|
||||
clone(): this;
|
||||
|
||||
/**
|
||||
* @see http://unexpected.js.org/api/addAssertion/
|
||||
*/
|
||||
addAssertion<T, A extends Array<unknown> = []>(
|
||||
pattern: string,
|
||||
handler: (expect: Expect, subject: T, ...args: A) => void,
|
||||
): this;
|
||||
|
||||
/**
|
||||
* @see http://unexpected.js.org/api/addType/
|
||||
*/
|
||||
addType<T>(typeDefinition: unexpected.TypeDefinition<T>): this;
|
||||
|
||||
/**
|
||||
* @see http://unexpected.js.org/api/fail/
|
||||
*/
|
||||
fail<A extends Array<unknown> = []>(format: string, ...args: A): void;
|
||||
fail<E extends Error>(error: E): void;
|
||||
|
||||
/**
|
||||
* @see http://unexpected.js.org/api/freeze/
|
||||
*/
|
||||
freeze(): this;
|
||||
|
||||
/**
|
||||
* @see http://unexpected.js.org/api/use/
|
||||
*/
|
||||
use(plugin: unexpected.PluginDefinition): this;
|
||||
}
|
||||
|
||||
interface PluginDefinition {
|
||||
name?: string;
|
||||
version?: string;
|
||||
dependencies?: Array<string>;
|
||||
installInto(expect: Expect): void;
|
||||
}
|
||||
|
||||
interface TypeDefinition<T> {
|
||||
name: string;
|
||||
identify(value: unknown): value is T;
|
||||
base?: string;
|
||||
equal?(a: T, b: T, equal: (a: unknown, b: unknown) => boolean): boolean;
|
||||
inspect?(value: T, depth: number, output: any, inspect: (value: unknown, depth: number) => any): void;
|
||||
}
|
||||
}
|
||||
|
||||
interface Expect {
|
||||
/**
|
||||
* @see http://unexpected.js.org/api/expect/
|
||||
*/
|
||||
<A extends Array<unknown> = []>(
|
||||
subject: unknown,
|
||||
assertionName: string,
|
||||
...args: A
|
||||
): EnchantedPromise<any>;
|
||||
const unexpected: unexpected.Expect;
|
||||
|
||||
it<A extends Array<unknown> = []>(
|
||||
assertionName: string,
|
||||
subject?: unknown,
|
||||
...args: A
|
||||
): EnchantedPromise<any>;
|
||||
|
||||
/**
|
||||
* @see http://unexpected.js.org/api/clone/
|
||||
*/
|
||||
clone(): this;
|
||||
|
||||
/**
|
||||
* @see http://unexpected.js.org/api/addAssertion/
|
||||
*/
|
||||
addAssertion<T, A extends Array<unknown> = []>(
|
||||
pattern: string,
|
||||
handler: (expect: Expect, subject: T, ...args: A) => void,
|
||||
): this;
|
||||
|
||||
/**
|
||||
* @see http://unexpected.js.org/api/addType/
|
||||
*/
|
||||
addType<T>(typeDefinition: unexpected.TypeDefinition<T>): this;
|
||||
|
||||
/**
|
||||
* @see http://unexpected.js.org/api/fail/
|
||||
*/
|
||||
fail<A extends Array<unknown> = []>(format: string, ...args: A): void;
|
||||
fail<E extends Error>(error: E): void;
|
||||
|
||||
/**
|
||||
* @see http://unexpected.js.org/api/freeze/
|
||||
*/
|
||||
freeze(): this;
|
||||
|
||||
/**
|
||||
* @see http://unexpected.js.org/api/use/
|
||||
*/
|
||||
use(plugin: unexpected.PluginDefinition): this;
|
||||
}
|
||||
|
||||
interface PluginDefinition {
|
||||
name?: string;
|
||||
version?: string;
|
||||
dependencies?: Array<string>;
|
||||
installInto(expect: Expect): void;
|
||||
}
|
||||
|
||||
interface TypeDefinition<T> {
|
||||
name: string;
|
||||
identify(value: unknown): value is T;
|
||||
base?: string;
|
||||
equal?(a: T, b: T, equal: (a: unknown, b: unknown) => boolean): boolean;
|
||||
inspect?(
|
||||
value: T,
|
||||
depth: number,
|
||||
output: any,
|
||||
inspect: (value: unknown, depth: number) => any,
|
||||
): void;
|
||||
}
|
||||
}
|
||||
|
||||
const unexpected: unexpected.Expect;
|
||||
|
||||
export = unexpected;
|
||||
export = unexpected;
|
||||
}
|
||||
|
56
@types/webpack-loaders.d.ts
vendored
56
@types/webpack-loaders.d.ts
vendored
@ -1,59 +1,59 @@
|
||||
declare module '*.html' {
|
||||
const url: string;
|
||||
export = url;
|
||||
const url: string;
|
||||
export = url;
|
||||
}
|
||||
|
||||
declare module '*.svg' {
|
||||
const url: string;
|
||||
export = url;
|
||||
const url: string;
|
||||
export = url;
|
||||
}
|
||||
|
||||
declare module '*.png' {
|
||||
const url: string;
|
||||
export = url;
|
||||
const url: string;
|
||||
export = url;
|
||||
}
|
||||
|
||||
declare module '*.gif' {
|
||||
const url: string;
|
||||
export = url;
|
||||
const url: string;
|
||||
export = url;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const url: string;
|
||||
export = url;
|
||||
const url: string;
|
||||
export = url;
|
||||
}
|
||||
|
||||
declare module '*.intl.json' {
|
||||
import { MessageDescriptor } from 'react-intl';
|
||||
import { MessageDescriptor } from 'react-intl';
|
||||
|
||||
const descriptor: Record<string, MessageDescriptor>;
|
||||
const descriptor: Record<string, MessageDescriptor>;
|
||||
|
||||
export = descriptor;
|
||||
export = descriptor;
|
||||
}
|
||||
|
||||
declare module '*.json' {
|
||||
const jsonContents: {
|
||||
[key: string]: any;
|
||||
};
|
||||
const jsonContents: {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export = jsonContents;
|
||||
export = jsonContents;
|
||||
}
|
||||
|
||||
declare module '*.scss' {
|
||||
// TODO: replace with:
|
||||
// https://www.npmjs.com/package/css-modules-typescript-loader
|
||||
// https://github.com/Jimdo/typings-for-css-modules-loader
|
||||
const classNames: {
|
||||
[className: string]: string;
|
||||
};
|
||||
// TODO: replace with:
|
||||
// https://www.npmjs.com/package/css-modules-typescript-loader
|
||||
// https://github.com/Jimdo/typings-for-css-modules-loader
|
||||
const classNames: {
|
||||
[className: string]: string;
|
||||
};
|
||||
|
||||
export = classNames;
|
||||
export = classNames;
|
||||
}
|
||||
|
||||
declare module '*.css' {
|
||||
const classNames: {
|
||||
[className: string]: string;
|
||||
};
|
||||
const classNames: {
|
||||
[className: string]: string;
|
||||
};
|
||||
|
||||
export = classNames;
|
||||
export = classNames;
|
||||
}
|
||||
|
18
README.md
18
README.md
@ -3,8 +3,7 @@
|
||||
[![Build Status](https://travis-ci.org/elyby/accounts-frontend.svg?branch=master)](https://travis-ci.org/elyby/accounts-frontend)
|
||||
[![Ely.by translation on Crowdin](https://d322cqt584bo4o.cloudfront.net/elyby/localized.svg)](https://translate.ely.by/project/elyby)
|
||||
|
||||
Web interface for Ely.by Accounts service. Developed using ReactJS and Flow
|
||||
typing.
|
||||
Web interface for Ely.by Accounts service. Developed using ReactJS and Flow typing.
|
||||
|
||||
## Development
|
||||
|
||||
@ -19,16 +18,15 @@ cd accounts-frontend
|
||||
yarn install
|
||||
```
|
||||
|
||||
After that you need to copy `config/template.env.js` into `config/env.js` and
|
||||
adjust it for yourself. Then you can start the application in dev mode:
|
||||
After that you need to copy `config/template.env.js` into `config/env.js` and adjust it for yourself. Then you can start
|
||||
the application in dev mode:
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
```
|
||||
|
||||
This will start the dev server on port 8080, which will automatically apply all
|
||||
changes in project files, as well as proxy all requests to the backend on the
|
||||
domain specified in `env.js`.
|
||||
This will start the dev server on port 8080, which will automatically apply all changes in project files, as well as
|
||||
proxy all requests to the backend on the domain specified in `env.js`.
|
||||
|
||||
To run the tests execute:
|
||||
|
||||
@ -42,8 +40,7 @@ yarn test
|
||||
|
||||
2. Place your code in a separate branch `git checkout -b <your_branch_name>`.
|
||||
|
||||
3. Add your fork as a remote
|
||||
`git remote add fork https://github.com/<your_username>/accounts-frontend.git`.
|
||||
3. Add your fork as a remote `git remote add fork https://github.com/<your_username>/accounts-frontend.git`.
|
||||
|
||||
4. Push to your fork repository `git push -u fork <your_branch_name>`.
|
||||
|
||||
@ -52,5 +49,4 @@ yarn test
|
||||
## Translating
|
||||
|
||||
Ely.by translation is done through the [Crowdin](https://crowdin.com) service.
|
||||
[Click here](https://translate.ely.by/project/elyby/invite) to participate in
|
||||
the translation of the project.
|
||||
[Click here](https://translate.ely.by/project/elyby/invite) to participate in the translation of the project.
|
||||
|
@ -1,40 +1,40 @@
|
||||
/* eslint-env node */
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-typescript',
|
||||
{
|
||||
allowDeclareFields: true,
|
||||
},
|
||||
],
|
||||
'@babel/preset-react',
|
||||
'@babel/preset-env',
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
'@babel/plugin-proposal-function-bind',
|
||||
'@babel/plugin-proposal-class-properties',
|
||||
[
|
||||
'@babel/plugin-transform-runtime',
|
||||
{
|
||||
corejs: 3,
|
||||
},
|
||||
],
|
||||
['react-intl', { messagesDir: './build/messages/' }],
|
||||
],
|
||||
env: {
|
||||
webpack: {
|
||||
plugins: ['react-hot-loader/babel'],
|
||||
presets: [
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
modules: false,
|
||||
useBuiltIns: 'usage', // or "entry"
|
||||
corejs: 3,
|
||||
},
|
||||
'@babel/preset-typescript',
|
||||
{
|
||||
allowDeclareFields: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
'@babel/preset-react',
|
||||
'@babel/preset-env',
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
'@babel/plugin-proposal-function-bind',
|
||||
'@babel/plugin-proposal-class-properties',
|
||||
[
|
||||
'@babel/plugin-transform-runtime',
|
||||
{
|
||||
corejs: 3,
|
||||
},
|
||||
],
|
||||
['react-intl', { messagesDir: './build/messages/' }],
|
||||
],
|
||||
env: {
|
||||
webpack: {
|
||||
plugins: ['react-hot-loader/babel'],
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
modules: false,
|
||||
useBuiltIns: 'usage', // or "entry"
|
||||
corejs: 3,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
10
config.js
10
config.js
@ -5,9 +5,9 @@ require('dotenv').config();
|
||||
const { env } = process;
|
||||
|
||||
module.exports = {
|
||||
version: env.VERSION || env.NODE_ENV,
|
||||
apiHost: env.API_HOST || 'https://dev.account.ely.by',
|
||||
ga: env.GA_ID && { id: env.GA_ID },
|
||||
sentryDSN: env.SENTRY_DSN,
|
||||
crowdinApiKey: env.CROWDIN_API_KEY,
|
||||
version: env.VERSION || env.NODE_ENV,
|
||||
apiHost: env.API_HOST || 'https://dev.account.ely.by',
|
||||
ga: env.GA_ID && { id: env.GA_ID },
|
||||
sentryDSN: env.SENTRY_DSN,
|
||||
crowdinApiKey: env.CROWDIN_API_KEY,
|
||||
};
|
||||
|
@ -5,15 +5,15 @@ const path = require('path');
|
||||
const { transform } = require('webpack-utils/intl-loader');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* @param {string} src - transformed module source code
|
||||
* @param {string} filename - transformed module file path
|
||||
* @param {{[key: string]: any}} config - jest config
|
||||
* @param {{instrument: boolean}} options - additional options
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
process(src, filename, config, options) {
|
||||
return transform(src, filename, path.resolve(`${__dirname}/../../..`));
|
||||
},
|
||||
/**
|
||||
* @param {string} src - transformed module source code
|
||||
* @param {string} filename - transformed module file path
|
||||
* @param {{[key: string]: any}} config - jest config
|
||||
* @param {{instrument: boolean}} options - additional options
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
process(src, filename, config, options) {
|
||||
return transform(src, filename, path.resolve(`${__dirname}/../../..`));
|
||||
},
|
||||
};
|
||||
|
@ -2,19 +2,19 @@ import 'app/polyfills';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
if (!window.localStorage) {
|
||||
window.localStorage = {
|
||||
getItem(key) {
|
||||
return this[key] || null;
|
||||
},
|
||||
setItem(key, value) {
|
||||
this[key] = value;
|
||||
},
|
||||
removeItem(key) {
|
||||
delete this[key];
|
||||
},
|
||||
};
|
||||
window.localStorage = {
|
||||
getItem(key) {
|
||||
return this[key] || null;
|
||||
},
|
||||
setItem(key, value) {
|
||||
this[key] = value;
|
||||
},
|
||||
removeItem(key) {
|
||||
delete this[key];
|
||||
},
|
||||
};
|
||||
|
||||
window.sessionStorage = {
|
||||
...window.localStorage,
|
||||
};
|
||||
window.sessionStorage = {
|
||||
...window.localStorage,
|
||||
};
|
||||
}
|
||||
|
336
package.json
336
package.json
@ -1,174 +1,174 @@
|
||||
{
|
||||
"name": "@elyby/accounts-frontend",
|
||||
"description": "",
|
||||
"author": "SleepWalker <mybox@udf.su>",
|
||||
"private": true,
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "ErickSkrauch",
|
||||
"email": "erickskrauch@ely.by"
|
||||
"name": "@elyby/accounts-frontend",
|
||||
"description": "",
|
||||
"author": "SleepWalker <mybox@udf.su>",
|
||||
"private": true,
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "ErickSkrauch",
|
||||
"email": "erickskrauch@ely.by"
|
||||
},
|
||||
{
|
||||
"name": "SleepWalker",
|
||||
"email": "mybox@udf.su"
|
||||
}
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"repository": "https://github.com/elyby/accounts-frontend",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
{
|
||||
"name": "SleepWalker",
|
||||
"email": "mybox@udf.su"
|
||||
}
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"repository": "https://github.com/elyby/accounts-frontend",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"tests-e2e"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "yarn run clean && yarn run build:dll && webpack-dev-server --colors",
|
||||
"clean": "rm -rf ./build && mkdir ./build",
|
||||
"e2e": "yarn --cwd ./tests-e2e test",
|
||||
"test": "jest",
|
||||
"test:watch": "yarn test --watch",
|
||||
"lint": "eslint --ext js,ts,tsx --fix .",
|
||||
"lint:check": "eslint --ext js,ts,tsx --quiet .",
|
||||
"prettier": "prettier --write .",
|
||||
"prettier:check": "prettier --check .",
|
||||
"ts:check": "tsc",
|
||||
"ci:check": "yarn lint:check && yarn ts:check && yarn test",
|
||||
"analyze": "yarn run clean && yarn run build:webpack --analyze",
|
||||
"i18n:collect": "babel-node --extensions \".ts\" ./packages/scripts/i18n-collect.ts",
|
||||
"i18n:push": "babel-node --extensions \".ts\" ./packages/scripts/i18n-crowdin.ts push",
|
||||
"i18n:pull": "babel-node --extensions \".ts\" ./packages/scripts/i18n-crowdin.ts pull",
|
||||
"build": "yarn run clean && yarn run build:webpack",
|
||||
"build:install": "yarn install",
|
||||
"build:webpack": "NODE_ENV=production webpack --colors -p --bail",
|
||||
"build:quiet": "yarn run clean && yarn run build:webpack --quiet",
|
||||
"build:dll": "babel-node --extensions '.ts,.d.ts' ./packages/scripts/build-dll.ts",
|
||||
"build:serve": "http-server --proxy https://dev.account.ely.by ./build",
|
||||
"sb": "APP_ENV=storybook start-storybook -p 9009 --ci",
|
||||
"sb:build": "APP_ENV=storybook build-storybook"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged",
|
||||
"pre-push": "yarn ci:check"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{json,scss,css,md}": [
|
||||
"prettier --write"
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"tests-e2e"
|
||||
],
|
||||
"*.{js,ts,tsx}": [
|
||||
"eslint --fix"
|
||||
]
|
||||
},
|
||||
"jest": {
|
||||
"roots": [
|
||||
"<rootDir>/packages/app"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/jest/setupAfterEnv.js"
|
||||
],
|
||||
"resetMocks": true,
|
||||
"restoreMocks": true,
|
||||
"watchPlugins": [
|
||||
"jest-watch-typeahead/filename",
|
||||
"jest-watch-typeahead/testname"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/jest/__mocks__/mockStrExport.js",
|
||||
"\\.(css|less|scss)$": "identity-obj-proxy"
|
||||
"scripts": {
|
||||
"start": "yarn run clean && yarn run build:dll && webpack-dev-server --colors",
|
||||
"clean": "rm -rf ./build && mkdir ./build",
|
||||
"e2e": "yarn --cwd ./tests-e2e test",
|
||||
"test": "jest",
|
||||
"test:watch": "yarn test --watch",
|
||||
"lint": "eslint --ext js,ts,tsx --fix .",
|
||||
"lint:check": "eslint --ext js,ts,tsx --quiet .",
|
||||
"prettier": "prettier --write .",
|
||||
"prettier:check": "prettier --check .",
|
||||
"ts:check": "tsc",
|
||||
"ci:check": "yarn lint:check && yarn ts:check && yarn test",
|
||||
"analyze": "yarn run clean && yarn run build:webpack --analyze",
|
||||
"i18n:collect": "babel-node --extensions \".ts\" ./packages/scripts/i18n-collect.ts",
|
||||
"i18n:push": "babel-node --extensions \".ts\" ./packages/scripts/i18n-crowdin.ts push",
|
||||
"i18n:pull": "babel-node --extensions \".ts\" ./packages/scripts/i18n-crowdin.ts pull",
|
||||
"build": "yarn run clean && yarn run build:webpack",
|
||||
"build:install": "yarn install",
|
||||
"build:webpack": "NODE_ENV=production webpack --colors -p --bail",
|
||||
"build:quiet": "yarn run clean && yarn run build:webpack --quiet",
|
||||
"build:dll": "babel-node --extensions '.ts,.d.ts' ./packages/scripts/build-dll.ts",
|
||||
"build:serve": "http-server --proxy https://dev.account.ely.by ./build",
|
||||
"sb": "APP_ENV=storybook start-storybook -p 9009 --ci",
|
||||
"sb:build": "APP_ENV=storybook build-storybook"
|
||||
},
|
||||
"transform": {
|
||||
"\\.intl\\.json$": "<rootDir>/jest/__mocks__/intlMock.js",
|
||||
"^.+\\.[tj]sx?$": "babel-jest"
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged",
|
||||
"pre-push": "yarn ci:check"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{json,scss,css,md}": [
|
||||
"prettier --write"
|
||||
],
|
||||
"*.{js,ts,tsx}": [
|
||||
"eslint --fix"
|
||||
]
|
||||
},
|
||||
"jest": {
|
||||
"roots": [
|
||||
"<rootDir>/packages/app"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/jest/setupAfterEnv.js"
|
||||
],
|
||||
"resetMocks": true,
|
||||
"restoreMocks": true,
|
||||
"watchPlugins": [
|
||||
"jest-watch-typeahead/filename",
|
||||
"jest-watch-typeahead/testname"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/jest/__mocks__/mockStrExport.js",
|
||||
"\\.(css|less|scss)$": "identity-obj-proxy"
|
||||
},
|
||||
"transform": {
|
||||
"\\.intl\\.json$": "<rootDir>/jest/__mocks__/intlMock.js",
|
||||
"^.+\\.[tj]sx?$": "babel-jest"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-hot-loader": "^4.12.18",
|
||||
"react-intl": "^4.5.7",
|
||||
"regenerator-runtime": "^0.13.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.8.3",
|
||||
"@babel/core": "^7.8.3",
|
||||
"@babel/node": "^7.8.3",
|
||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||
"@babel/plugin-proposal-decorators": "^7.8.3",
|
||||
"@babel/plugin-proposal-do-expressions": "^7.8.3",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.8.3",
|
||||
"@babel/plugin-proposal-export-namespace-from": "^7.8.3",
|
||||
"@babel/plugin-proposal-function-bind": "^7.8.3",
|
||||
"@babel/plugin-proposal-function-sent": "^7.8.3",
|
||||
"@babel/plugin-proposal-json-strings": "^7.8.3",
|
||||
"@babel/plugin-proposal-logical-assignment-operators": "^7.8.3",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.8.3",
|
||||
"@babel/plugin-proposal-pipeline-operator": "^7.8.3",
|
||||
"@babel/plugin-proposal-throw-expressions": "^7.8.3",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-syntax-import-meta": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.8.3",
|
||||
"@babel/preset-env": "^7.8.3",
|
||||
"@babel/preset-react": "^7.8.3",
|
||||
"@babel/preset-typescript": "^7.8.3",
|
||||
"@babel/runtime-corejs3": "^7.8.3",
|
||||
"@storybook/addon-actions": "^5.3.18",
|
||||
"@storybook/addon-links": "^5.3.18",
|
||||
"@storybook/addon-viewport": "^5.3.18",
|
||||
"@storybook/addons": "^5.3.18",
|
||||
"@storybook/react": "^5.3.18",
|
||||
"@types/jest": "^25.2.3",
|
||||
"@types/sinon": "^9.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^3.0.0",
|
||||
"@typescript-eslint/parser": "^3.0.0",
|
||||
"babel-loader": "^8.0.0",
|
||||
"babel-plugin-react-intl": "^7.5.10",
|
||||
"core-js": "3.6.5",
|
||||
"csp-webpack-plugin": "^2.0.2",
|
||||
"css-loader": "^3.5.3",
|
||||
"cssnano": "^4.1.10",
|
||||
"dotenv": "^8.2.0",
|
||||
"eager-imports-webpack-plugin": "^1.0.0",
|
||||
"eslint": "^7.0.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-jsdoc": "^25.4.2",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-react": "^7.20.0",
|
||||
"exports-loader": "^0.7.0",
|
||||
"file-loader": "^6.0.0",
|
||||
"html-loader": "^1.1.0",
|
||||
"html-webpack-plugin": "^4.3.0",
|
||||
"husky": "^4.2.5",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"imports-loader": "^0.8.0",
|
||||
"jest": "^26.0.1",
|
||||
"jest-watch-typeahead": "^0.6.0",
|
||||
"json-loader": "^0.5.4",
|
||||
"lint-staged": "^10.2.4",
|
||||
"loader-utils": "^2.0.0",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"postcss-scss": "^2.1.1",
|
||||
"prettier": "^2.0.5",
|
||||
"raw-loader": "^4.0.1",
|
||||
"sass-loader": "^8.0.2",
|
||||
"sinon": "^9.0.2",
|
||||
"sitemap-webpack-plugin": "^0.8.0",
|
||||
"speed-measure-webpack-plugin": "^1.3.3",
|
||||
"storybook-addon-intl": "^2.4.1",
|
||||
"style-loader": "~1.2.1",
|
||||
"typescript": "^3.9.3",
|
||||
"unexpected": "^11.14.0",
|
||||
"unexpected-sinon": "^10.5.1",
|
||||
"url-loader": "^4.1.0",
|
||||
"wait-on": "^5.0.0",
|
||||
"webpack": "^4.41.5",
|
||||
"webpack-bundle-analyzer": "^3.8.0",
|
||||
"webpack-cli": "^3.3.10",
|
||||
"webpack-dev-server": "^3.10.1",
|
||||
"webpackbar": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-hot-loader": "^4.12.18",
|
||||
"react-intl": "^4.5.7",
|
||||
"regenerator-runtime": "^0.13.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.8.3",
|
||||
"@babel/core": "^7.8.3",
|
||||
"@babel/node": "^7.8.3",
|
||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||
"@babel/plugin-proposal-decorators": "^7.8.3",
|
||||
"@babel/plugin-proposal-do-expressions": "^7.8.3",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.8.3",
|
||||
"@babel/plugin-proposal-export-namespace-from": "^7.8.3",
|
||||
"@babel/plugin-proposal-function-bind": "^7.8.3",
|
||||
"@babel/plugin-proposal-function-sent": "^7.8.3",
|
||||
"@babel/plugin-proposal-json-strings": "^7.8.3",
|
||||
"@babel/plugin-proposal-logical-assignment-operators": "^7.8.3",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.8.3",
|
||||
"@babel/plugin-proposal-pipeline-operator": "^7.8.3",
|
||||
"@babel/plugin-proposal-throw-expressions": "^7.8.3",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-syntax-import-meta": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.8.3",
|
||||
"@babel/preset-env": "^7.8.3",
|
||||
"@babel/preset-react": "^7.8.3",
|
||||
"@babel/preset-typescript": "^7.8.3",
|
||||
"@babel/runtime-corejs3": "^7.8.3",
|
||||
"@storybook/addon-actions": "^5.3.18",
|
||||
"@storybook/addon-links": "^5.3.18",
|
||||
"@storybook/addon-viewport": "^5.3.18",
|
||||
"@storybook/addons": "^5.3.18",
|
||||
"@storybook/react": "^5.3.18",
|
||||
"@types/jest": "^25.2.3",
|
||||
"@types/sinon": "^9.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^3.0.0",
|
||||
"@typescript-eslint/parser": "^3.0.0",
|
||||
"babel-loader": "^8.0.0",
|
||||
"babel-plugin-react-intl": "^7.5.10",
|
||||
"core-js": "3.6.5",
|
||||
"csp-webpack-plugin": "^2.0.2",
|
||||
"css-loader": "^3.5.3",
|
||||
"cssnano": "^4.1.10",
|
||||
"dotenv": "^8.2.0",
|
||||
"eager-imports-webpack-plugin": "^1.0.0",
|
||||
"eslint": "^7.0.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-jsdoc": "^25.4.2",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-react": "^7.20.0",
|
||||
"exports-loader": "^0.7.0",
|
||||
"file-loader": "^6.0.0",
|
||||
"html-loader": "^1.1.0",
|
||||
"html-webpack-plugin": "^4.3.0",
|
||||
"husky": "^4.2.5",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"imports-loader": "^0.8.0",
|
||||
"jest": "^26.0.1",
|
||||
"jest-watch-typeahead": "^0.6.0",
|
||||
"json-loader": "^0.5.4",
|
||||
"lint-staged": "^10.2.4",
|
||||
"loader-utils": "^2.0.0",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"postcss-scss": "^2.1.1",
|
||||
"prettier": "^2.0.5",
|
||||
"raw-loader": "^4.0.1",
|
||||
"sass-loader": "^8.0.2",
|
||||
"sinon": "^9.0.2",
|
||||
"sitemap-webpack-plugin": "^0.8.0",
|
||||
"speed-measure-webpack-plugin": "^1.3.3",
|
||||
"storybook-addon-intl": "^2.4.1",
|
||||
"style-loader": "~1.2.1",
|
||||
"typescript": "^3.9.3",
|
||||
"unexpected": "^11.14.0",
|
||||
"unexpected-sinon": "^10.5.1",
|
||||
"url-loader": "^4.1.0",
|
||||
"wait-on": "^5.0.0",
|
||||
"webpack": "^4.41.5",
|
||||
"webpack-bundle-analyzer": "^3.8.0",
|
||||
"webpack-cli": "^3.3.10",
|
||||
"webpack-dev-server": "^3.10.1",
|
||||
"webpackbar": "^4.0.0"
|
||||
}
|
||||
}
|
||||
|
@ -26,47 +26,46 @@ type ChildState = any;
|
||||
// TODO: this may be rewritten in more efficient way using resize/mutation observer
|
||||
|
||||
export default class MeasureHeight extends React.PureComponent<
|
||||
{
|
||||
shouldMeasure: (prevState: ChildState, newState: ChildState) => boolean;
|
||||
onMeasure: (height: number) => void;
|
||||
state: ChildState;
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
{
|
||||
shouldMeasure: (prevState: ChildState, newState: ChildState) => boolean;
|
||||
onMeasure: (height: number) => void;
|
||||
state: ChildState;
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
> {
|
||||
static defaultProps = {
|
||||
shouldMeasure: (prevState: ChildState, newState: ChildState) =>
|
||||
prevState !== newState,
|
||||
onMeasure: () => {},
|
||||
};
|
||||
static defaultProps = {
|
||||
shouldMeasure: (prevState: ChildState, newState: ChildState) => prevState !== newState,
|
||||
onMeasure: () => {},
|
||||
};
|
||||
|
||||
el: HTMLDivElement | null = null;
|
||||
el: HTMLDivElement | null = null;
|
||||
|
||||
componentDidMount() {
|
||||
// we want to measure height immediately on first mount to avoid ui laggs
|
||||
this.measure();
|
||||
window.addEventListener('resize', this.enqueueMeasurement);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: typeof MeasureHeight.prototype.props) {
|
||||
if (this.props.shouldMeasure(prevProps.state, this.props.state)) {
|
||||
this.enqueueMeasurement();
|
||||
componentDidMount() {
|
||||
// we want to measure height immediately on first mount to avoid ui laggs
|
||||
this.measure();
|
||||
window.addEventListener('resize', this.enqueueMeasurement);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.enqueueMeasurement);
|
||||
}
|
||||
componentDidUpdate(prevProps: typeof MeasureHeight.prototype.props) {
|
||||
if (this.props.shouldMeasure(prevProps.state, this.props.state)) {
|
||||
this.enqueueMeasurement();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = omit(this.props, ['shouldMeasure', 'onMeasure', 'state']);
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.enqueueMeasurement);
|
||||
}
|
||||
|
||||
return <div {...props} ref={(el: HTMLDivElement) => (this.el = el)} />;
|
||||
}
|
||||
render() {
|
||||
const props = omit(this.props, ['shouldMeasure', 'onMeasure', 'state']);
|
||||
|
||||
measure = () => {
|
||||
requestAnimationFrame(() => {
|
||||
this.el && this.props.onMeasure(this.el.offsetHeight);
|
||||
});
|
||||
};
|
||||
return <div {...props} ref={(el: HTMLDivElement) => (this.el = el)} />;
|
||||
}
|
||||
|
||||
enqueueMeasurement = debounce(this.measure);
|
||||
measure = () => {
|
||||
requestAnimationFrame(() => {
|
||||
this.el && this.props.onMeasure(this.el.offsetHeight);
|
||||
});
|
||||
};
|
||||
|
||||
enqueueMeasurement = debounce(this.measure);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"addAccount": "Add account",
|
||||
"goToEly": "Go to Ely.by profile",
|
||||
"logout": "Log out"
|
||||
"addAccount": "Add account",
|
||||
"goToEly": "Go to Ely.by profile",
|
||||
"logout": "Log out"
|
||||
}
|
||||
|
@ -14,190 +14,164 @@ import styles from './accountSwitcher.scss';
|
||||
import messages from './AccountSwitcher.intl.json';
|
||||
|
||||
interface Props {
|
||||
switchAccount: (account: Account) => Promise<Account>;
|
||||
removeAccount: (account: Account) => Promise<void>;
|
||||
// called after each action performed
|
||||
onAfterAction: () => void;
|
||||
// called after switching an account. The active account will be passed as arg
|
||||
onSwitch: (account: Account) => void;
|
||||
accounts: RootState['accounts'];
|
||||
skin: Skin;
|
||||
// whether active account should be expanded and shown on the top
|
||||
highlightActiveAccount: boolean;
|
||||
// whether to show logout icon near each account
|
||||
allowLogout: boolean;
|
||||
// whether to show add account button
|
||||
allowAdd: boolean;
|
||||
switchAccount: (account: Account) => Promise<Account>;
|
||||
removeAccount: (account: Account) => Promise<void>;
|
||||
// called after each action performed
|
||||
onAfterAction: () => void;
|
||||
// called after switching an account. The active account will be passed as arg
|
||||
onSwitch: (account: Account) => void;
|
||||
accounts: RootState['accounts'];
|
||||
skin: Skin;
|
||||
// whether active account should be expanded and shown on the top
|
||||
highlightActiveAccount: boolean;
|
||||
// whether to show logout icon near each account
|
||||
allowLogout: boolean;
|
||||
// whether to show add account button
|
||||
allowAdd: boolean;
|
||||
}
|
||||
|
||||
export class AccountSwitcher extends React.Component<Props> {
|
||||
static defaultProps: Partial<Props> = {
|
||||
skin: SKIN_DARK,
|
||||
highlightActiveAccount: true,
|
||||
allowLogout: true,
|
||||
allowAdd: true,
|
||||
onAfterAction() {},
|
||||
onSwitch() {},
|
||||
};
|
||||
static defaultProps: Partial<Props> = {
|
||||
skin: SKIN_DARK,
|
||||
highlightActiveAccount: true,
|
||||
allowLogout: true,
|
||||
allowAdd: true,
|
||||
onAfterAction() {},
|
||||
onSwitch() {},
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
accounts,
|
||||
skin,
|
||||
allowAdd,
|
||||
allowLogout,
|
||||
highlightActiveAccount,
|
||||
} = this.props;
|
||||
const activeAccount = getActiveAccount({ accounts });
|
||||
render() {
|
||||
const { accounts, skin, allowAdd, allowLogout, highlightActiveAccount } = this.props;
|
||||
const activeAccount = getActiveAccount({ accounts });
|
||||
|
||||
if (!activeAccount) {
|
||||
return null;
|
||||
if (!activeAccount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let { available } = accounts;
|
||||
|
||||
if (highlightActiveAccount) {
|
||||
available = available.filter((account) => account.id !== activeAccount.id);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.accountSwitcher, styles[`${skin}AccountSwitcher`])}
|
||||
data-testid="account-switcher"
|
||||
>
|
||||
{highlightActiveAccount && (
|
||||
<div className={styles.item} data-testid="active-account">
|
||||
<div className={clsx(styles.accountIcon, styles.activeAccountIcon, styles.accountIcon1)} />
|
||||
<div className={styles.activeAccountInfo}>
|
||||
<div className={styles.activeAccountUsername}>{activeAccount.username}</div>
|
||||
<div className={clsx(styles.accountEmail, styles.activeAccountEmail)}>
|
||||
{activeAccount.email}
|
||||
</div>
|
||||
<div className={styles.links}>
|
||||
<div className={styles.link}>
|
||||
<a href={`http://ely.by/u${activeAccount.id}`} target="_blank">
|
||||
<Message {...messages.goToEly} />
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.link}>
|
||||
<a
|
||||
className={styles.link}
|
||||
data-testid="logout-account"
|
||||
onClick={this.onRemove(activeAccount)}
|
||||
href="#"
|
||||
>
|
||||
<Message {...messages.logout} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{available.map((account, index) => (
|
||||
<div
|
||||
className={clsx(styles.item, styles.accountSwitchItem)}
|
||||
key={account.id}
|
||||
data-e2e-account-id={account.id}
|
||||
onClick={this.onSwitch(account)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
styles.accountIcon,
|
||||
styles[`accountIcon${(index % 7) + (highlightActiveAccount ? 2 : 1)}`],
|
||||
)}
|
||||
/>
|
||||
|
||||
{allowLogout ? (
|
||||
<div
|
||||
className={styles.logoutIcon}
|
||||
data-testid="logout-account"
|
||||
onClick={this.onRemove(account)}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.nextIcon} />
|
||||
)}
|
||||
|
||||
<div className={styles.accountInfo}>
|
||||
<div className={styles.accountUsername}>{account.username}</div>
|
||||
<div className={styles.accountEmail}>{account.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{allowAdd ? (
|
||||
<Link to="/login" onClick={this.props.onAfterAction}>
|
||||
<Button
|
||||
color={COLOR_WHITE}
|
||||
data-testid="add-account"
|
||||
block
|
||||
small
|
||||
className={styles.addAccount}
|
||||
label={
|
||||
<Message {...messages.addAccount}>
|
||||
{(message) => (
|
||||
<span>
|
||||
<div className={styles.addIcon} />
|
||||
{message}
|
||||
</span>
|
||||
)}
|
||||
</Message>
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let { available } = accounts;
|
||||
onSwitch = (account: Account) => (event: React.MouseEvent<any>) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (highlightActiveAccount) {
|
||||
available = available.filter(
|
||||
(account) => account.id !== activeAccount.id,
|
||||
);
|
||||
}
|
||||
loader.show();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
styles.accountSwitcher,
|
||||
styles[`${skin}AccountSwitcher`],
|
||||
)}
|
||||
data-testid="account-switcher"
|
||||
>
|
||||
{highlightActiveAccount && (
|
||||
<div className={styles.item} data-testid="active-account">
|
||||
<div
|
||||
className={clsx(
|
||||
styles.accountIcon,
|
||||
styles.activeAccountIcon,
|
||||
styles.accountIcon1,
|
||||
)}
|
||||
/>
|
||||
<div className={styles.activeAccountInfo}>
|
||||
<div className={styles.activeAccountUsername}>
|
||||
{activeAccount.username}
|
||||
</div>
|
||||
<div
|
||||
className={clsx(styles.accountEmail, styles.activeAccountEmail)}
|
||||
>
|
||||
{activeAccount.email}
|
||||
</div>
|
||||
<div className={styles.links}>
|
||||
<div className={styles.link}>
|
||||
<a
|
||||
href={`http://ely.by/u${activeAccount.id}`}
|
||||
target="_blank"
|
||||
>
|
||||
<Message {...messages.goToEly} />
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.link}>
|
||||
<a
|
||||
className={styles.link}
|
||||
data-testid="logout-account"
|
||||
onClick={this.onRemove(activeAccount)}
|
||||
href="#"
|
||||
>
|
||||
<Message {...messages.logout} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
this.props
|
||||
.switchAccount(account)
|
||||
.finally(() => this.props.onAfterAction())
|
||||
.then(() => this.props.onSwitch(account))
|
||||
// we won't sent any logs to sentry, because an error should be already
|
||||
// handled by external logic
|
||||
.catch((error) => console.warn('Error switching account', { error }))
|
||||
.finally(() => loader.hide());
|
||||
};
|
||||
|
||||
{available.map((account, index) => (
|
||||
<div
|
||||
className={clsx(styles.item, styles.accountSwitchItem)}
|
||||
key={account.id}
|
||||
data-e2e-account-id={account.id}
|
||||
onClick={this.onSwitch(account)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
styles.accountIcon,
|
||||
styles[
|
||||
`accountIcon${(index % 7) + (highlightActiveAccount ? 2 : 1)}`
|
||||
],
|
||||
)}
|
||||
/>
|
||||
onRemove = (account: Account) => (event: React.MouseEvent<any>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
{allowLogout ? (
|
||||
<div
|
||||
className={styles.logoutIcon}
|
||||
data-testid="logout-account"
|
||||
onClick={this.onRemove(account)}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.nextIcon} />
|
||||
)}
|
||||
|
||||
<div className={styles.accountInfo}>
|
||||
<div className={styles.accountUsername}>{account.username}</div>
|
||||
<div className={styles.accountEmail}>{account.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{allowAdd ? (
|
||||
<Link to="/login" onClick={this.props.onAfterAction}>
|
||||
<Button
|
||||
color={COLOR_WHITE}
|
||||
data-testid="add-account"
|
||||
block
|
||||
small
|
||||
className={styles.addAccount}
|
||||
label={
|
||||
<Message {...messages.addAccount}>
|
||||
{(message) => (
|
||||
<span>
|
||||
<div className={styles.addIcon} />
|
||||
{message}
|
||||
</span>
|
||||
)}
|
||||
</Message>
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onSwitch = (account: Account) => (event: React.MouseEvent<any>) => {
|
||||
event.preventDefault();
|
||||
|
||||
loader.show();
|
||||
|
||||
this.props
|
||||
.switchAccount(account)
|
||||
.finally(() => this.props.onAfterAction())
|
||||
.then(() => this.props.onSwitch(account))
|
||||
// we won't sent any logs to sentry, because an error should be already
|
||||
// handled by external logic
|
||||
.catch((error) => console.warn('Error switching account', { error }))
|
||||
.finally(() => loader.hide());
|
||||
};
|
||||
|
||||
onRemove = (account: Account) => (event: React.MouseEvent<any>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.props.removeAccount(account).then(() => this.props.onAfterAction());
|
||||
};
|
||||
this.props.removeAccount(account).then(() => this.props.onAfterAction());
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
({ accounts }: RootState) => ({
|
||||
accounts,
|
||||
}),
|
||||
{
|
||||
switchAccount: authenticate,
|
||||
removeAccount: revoke,
|
||||
},
|
||||
({ accounts }: RootState) => ({
|
||||
accounts,
|
||||
}),
|
||||
{
|
||||
switchAccount: authenticate,
|
||||
removeAccount: revoke,
|
||||
},
|
||||
)(AccountSwitcher);
|
||||
|
@ -8,7 +8,7 @@ $bodyLeftRightPadding: 20px;
|
||||
$lightBorderColor: #eee;
|
||||
|
||||
.accountSwitcher {
|
||||
text-align: left;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.accountInfo {
|
||||
@ -16,210 +16,210 @@ $lightBorderColor: #eee;
|
||||
|
||||
.accountUsername,
|
||||
.accountEmail {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.lightAccountSwitcher {
|
||||
background: #fff;
|
||||
color: #444;
|
||||
min-width: 205px;
|
||||
background: #fff;
|
||||
color: #444;
|
||||
min-width: 205px;
|
||||
|
||||
$border: 1px solid $lightBorderColor;
|
||||
border-left: $border;
|
||||
border-right: $border;
|
||||
border-bottom: 7px solid darker($green);
|
||||
$border: 1px solid $lightBorderColor;
|
||||
border-left: $border;
|
||||
border-right: $border;
|
||||
border-bottom: 7px solid darker($green);
|
||||
|
||||
.item {
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid $lightBorderColor;
|
||||
}
|
||||
|
||||
.accountSwitchItem {
|
||||
cursor: pointer;
|
||||
transition: 0.25s;
|
||||
|
||||
&:hover {
|
||||
background-color: $whiteButtonLight;
|
||||
.item {
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid $lightBorderColor;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $whiteButtonDark;
|
||||
.accountSwitchItem {
|
||||
cursor: pointer;
|
||||
transition: 0.25s;
|
||||
|
||||
&:hover {
|
||||
background-color: $whiteButtonLight;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $whiteButtonDark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.accountIcon {
|
||||
font-size: 27px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.activeAccountIcon {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.activeAccountInfo {
|
||||
margin-left: 29px;
|
||||
}
|
||||
|
||||
.activeAccountUsername {
|
||||
font-family: $font-family-title;
|
||||
font-size: 20px;
|
||||
color: $green;
|
||||
}
|
||||
|
||||
.activeAccountEmail {
|
||||
}
|
||||
|
||||
.links {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.link {
|
||||
font-size: 12px;
|
||||
margin-bottom: 3px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
.accountIcon {
|
||||
font-size: 27px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.accountInfo {
|
||||
margin-left: 29px;
|
||||
margin-right: 25px;
|
||||
}
|
||||
.activeAccountIcon {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.accountUsername {
|
||||
font-family: $font-family-title;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
.activeAccountInfo {
|
||||
margin-left: 29px;
|
||||
}
|
||||
|
||||
.accountEmail {
|
||||
font-size: 10px;
|
||||
color: #999;
|
||||
}
|
||||
.activeAccountUsername {
|
||||
font-family: $font-family-title;
|
||||
font-size: 20px;
|
||||
color: $green;
|
||||
}
|
||||
|
||||
.addAccount {
|
||||
}
|
||||
.activeAccountEmail {
|
||||
}
|
||||
|
||||
.links {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.link {
|
||||
font-size: 12px;
|
||||
margin-bottom: 3px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.accountInfo {
|
||||
margin-left: 29px;
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
.accountUsername {
|
||||
font-family: $font-family-title;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.accountEmail {
|
||||
font-size: 10px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.addAccount {
|
||||
}
|
||||
}
|
||||
|
||||
.darkAccountSwitcher {
|
||||
background: $black;
|
||||
background: $black;
|
||||
|
||||
$border: 1px solid lighter($black);
|
||||
$border: 1px solid lighter($black);
|
||||
|
||||
.item {
|
||||
padding: 15px 20px;
|
||||
border-top: 1px solid lighter($black);
|
||||
transition: 0.25s;
|
||||
cursor: pointer;
|
||||
.item {
|
||||
padding: 15px 20px;
|
||||
border-top: 1px solid lighter($black);
|
||||
transition: 0.25s;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: lighter($black);
|
||||
&:hover {
|
||||
background-color: lighter($black);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: darker($black);
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: $border;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: darker($black);
|
||||
.accountIcon {
|
||||
font-size: 35px;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: $border;
|
||||
.accountInfo {
|
||||
margin-left: 30px;
|
||||
margin-right: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.accountIcon {
|
||||
font-size: 35px;
|
||||
}
|
||||
.accountUsername {
|
||||
font-family: $font-family-title;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.accountInfo {
|
||||
margin-left: 30px;
|
||||
margin-right: 26px;
|
||||
}
|
||||
|
||||
.accountUsername {
|
||||
font-family: $font-family-title;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.accountEmail {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
.accountEmail {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.accountIcon {
|
||||
composes: minecraft-character from '~app/components/ui/icons.scss';
|
||||
composes: minecraft-character from '~app/components/ui/icons.scss';
|
||||
|
||||
float: left;
|
||||
float: left;
|
||||
|
||||
&1 {
|
||||
color: $green;
|
||||
}
|
||||
&1 {
|
||||
color: $green;
|
||||
}
|
||||
|
||||
&2 {
|
||||
color: $blue;
|
||||
}
|
||||
&2 {
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
&3 {
|
||||
color: $violet;
|
||||
}
|
||||
&3 {
|
||||
color: $violet;
|
||||
}
|
||||
|
||||
&4 {
|
||||
color: $orange;
|
||||
}
|
||||
&4 {
|
||||
color: $orange;
|
||||
}
|
||||
|
||||
&5 {
|
||||
color: $dark_blue;
|
||||
}
|
||||
&5 {
|
||||
color: $dark_blue;
|
||||
}
|
||||
|
||||
&6 {
|
||||
color: $light_violet;
|
||||
}
|
||||
&6 {
|
||||
color: $light_violet;
|
||||
}
|
||||
|
||||
&7 {
|
||||
color: $red;
|
||||
}
|
||||
&7 {
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
|
||||
.addIcon {
|
||||
composes: plus from '~app/components/ui/icons.scss';
|
||||
composes: plus from '~app/components/ui/icons.scss';
|
||||
|
||||
color: $green;
|
||||
position: relative;
|
||||
bottom: 1px;
|
||||
margin-right: 3px;
|
||||
color: $green;
|
||||
position: relative;
|
||||
bottom: 1px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.nextIcon {
|
||||
composes: arrowRight from '~app/components/ui/icons.scss';
|
||||
composes: arrowRight from '~app/components/ui/icons.scss';
|
||||
|
||||
position: relative;
|
||||
float: right;
|
||||
position: relative;
|
||||
float: right;
|
||||
|
||||
font-size: 24px;
|
||||
color: #4e4e4e;
|
||||
line-height: 35px;
|
||||
left: 0;
|
||||
font-size: 24px;
|
||||
color: #4e4e4e;
|
||||
line-height: 35px;
|
||||
left: 0;
|
||||
|
||||
transition: color 0.25s, left 0.5s;
|
||||
transition: color 0.25s, left 0.5s;
|
||||
|
||||
.item:hover & {
|
||||
color: #aaa;
|
||||
left: 5px;
|
||||
}
|
||||
.item:hover & {
|
||||
color: #aaa;
|
||||
left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.logoutIcon {
|
||||
composes: exit from '~app/components/ui/icons.scss';
|
||||
composes: exit from '~app/components/ui/icons.scss';
|
||||
|
||||
color: #cdcdcd;
|
||||
float: right;
|
||||
line-height: 27px;
|
||||
transition: 0.25s;
|
||||
color: #cdcdcd;
|
||||
float: right;
|
||||
line-height: 27px;
|
||||
transition: 0.25s;
|
||||
|
||||
&:hover {
|
||||
color: #777;
|
||||
}
|
||||
&:hover {
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
@ -4,491 +4,420 @@ import { browserHistory } from 'app/services/history';
|
||||
import { InternalServerError } from 'app/services/request';
|
||||
import { sessionStorage } from 'app/services/localStorage';
|
||||
import * as authentication from 'app/services/api/authentication';
|
||||
import {
|
||||
authenticate,
|
||||
revoke,
|
||||
logoutAll,
|
||||
logoutStrangers,
|
||||
} from 'app/components/accounts/actions';
|
||||
import {
|
||||
add,
|
||||
activate,
|
||||
remove,
|
||||
reset,
|
||||
} from 'app/components/accounts/actions/pure-actions';
|
||||
import { authenticate, revoke, logoutAll, logoutStrangers } from 'app/components/accounts/actions';
|
||||
import { add, activate, remove, reset } from 'app/components/accounts/actions/pure-actions';
|
||||
import { updateUser, setUser } from 'app/components/user/actions';
|
||||
import { setLogin, setAccountSwitcher } from 'app/components/auth/actions';
|
||||
import { Dispatch, RootState } from 'app/reducers';
|
||||
|
||||
import { Account } from './reducer';
|
||||
|
||||
const token =
|
||||
'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJlbHl8MSJ9.pRJ7vakt2eIscjqwG__KhSxKb3qwGsdBBeDbBffJs_I';
|
||||
const legacyToken =
|
||||
'eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOjF9.cRF-sQNrwWQ94xCb3vWioVdjxAZeefEE7GMGwh7708o';
|
||||
const token = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJlbHl8MSJ9.pRJ7vakt2eIscjqwG__KhSxKb3qwGsdBBeDbBffJs_I';
|
||||
const legacyToken = 'eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOjF9.cRF-sQNrwWQ94xCb3vWioVdjxAZeefEE7GMGwh7708o';
|
||||
|
||||
const account = {
|
||||
id: 1,
|
||||
username: 'username',
|
||||
email: 'email@test.com',
|
||||
token,
|
||||
refreshToken: 'bar',
|
||||
id: 1,
|
||||
username: 'username',
|
||||
email: 'email@test.com',
|
||||
token,
|
||||
refreshToken: 'bar',
|
||||
};
|
||||
|
||||
const user = {
|
||||
id: 1,
|
||||
username: 'username',
|
||||
email: 'email@test.com',
|
||||
lang: 'be',
|
||||
id: 1,
|
||||
username: 'username',
|
||||
email: 'email@test.com',
|
||||
lang: 'be',
|
||||
};
|
||||
|
||||
describe('components/accounts/actions', () => {
|
||||
let dispatch: Dispatch;
|
||||
let getState: () => RootState;
|
||||
let dispatch: Dispatch;
|
||||
let getState: () => RootState;
|
||||
|
||||
beforeEach(() => {
|
||||
dispatch = sinon
|
||||
.spy((arg) => (typeof arg === 'function' ? arg(dispatch, getState) : arg))
|
||||
.named('store.dispatch');
|
||||
getState = sinon.stub().named('store.getState');
|
||||
beforeEach(() => {
|
||||
dispatch = sinon
|
||||
.spy((arg) => (typeof arg === 'function' ? arg(dispatch, getState) : arg))
|
||||
.named('store.dispatch');
|
||||
getState = sinon.stub().named('store.getState');
|
||||
|
||||
(getState as any).returns({
|
||||
accounts: {
|
||||
available: [],
|
||||
active: null,
|
||||
},
|
||||
auth: {
|
||||
credentials: {},
|
||||
},
|
||||
user: {},
|
||||
});
|
||||
|
||||
sinon
|
||||
.stub(authentication, 'validateToken')
|
||||
.named('authentication.validateToken');
|
||||
sinon.stub(browserHistory, 'push').named('browserHistory.push');
|
||||
sinon.stub(authentication, 'logout').named('authentication.logout');
|
||||
|
||||
(authentication.logout as any).returns(Promise.resolve());
|
||||
(authentication.validateToken as any).returns(
|
||||
Promise.resolve({
|
||||
token: account.token,
|
||||
refreshToken: account.refreshToken,
|
||||
user,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(authentication.validateToken as any).restore();
|
||||
(authentication.logout as any).restore();
|
||||
(browserHistory.push as any).restore();
|
||||
});
|
||||
|
||||
describe('#authenticate()', () => {
|
||||
it('should request user state using token', () =>
|
||||
authenticate(account)(dispatch, getState, undefined).then(() =>
|
||||
expect(authentication.validateToken, 'to have a call satisfying', [
|
||||
account.id,
|
||||
account.token,
|
||||
account.refreshToken,
|
||||
]),
|
||||
));
|
||||
|
||||
it('should request user by extracting id from token', () =>
|
||||
authenticate({ token } as Account)(
|
||||
dispatch,
|
||||
getState,
|
||||
undefined,
|
||||
).then(() =>
|
||||
expect(authentication.validateToken, 'to have a call satisfying', [
|
||||
1,
|
||||
token,
|
||||
undefined,
|
||||
]),
|
||||
));
|
||||
|
||||
it('should request user by extracting id from legacy token', () =>
|
||||
authenticate({ token: legacyToken } as Account)(
|
||||
dispatch,
|
||||
getState,
|
||||
undefined,
|
||||
).then(() =>
|
||||
expect(authentication.validateToken, 'to have a call satisfying', [
|
||||
1,
|
||||
legacyToken,
|
||||
undefined,
|
||||
]),
|
||||
));
|
||||
|
||||
it(`dispatches accounts:add action`, () =>
|
||||
authenticate(account)(dispatch, getState, undefined).then(() =>
|
||||
expect(dispatch, 'to have a call satisfying', [add(account)]),
|
||||
));
|
||||
|
||||
it(`dispatches accounts:activate action`, () =>
|
||||
authenticate(account)(dispatch, getState, undefined).then(() =>
|
||||
expect(dispatch, 'to have a call satisfying', [activate(account)]),
|
||||
));
|
||||
|
||||
it(`dispatches i18n:setLocale action`, () =>
|
||||
authenticate(account)(dispatch, getState, undefined).then(() =>
|
||||
expect(dispatch, 'to have a call satisfying', [
|
||||
{ type: 'i18n:setLocale', payload: { locale: 'be' } },
|
||||
]),
|
||||
));
|
||||
|
||||
it('should update user state', () =>
|
||||
authenticate(account)(dispatch, getState, undefined).then(() =>
|
||||
expect(dispatch, 'to have a call satisfying', [
|
||||
updateUser({ ...user, isGuest: false }),
|
||||
]),
|
||||
));
|
||||
|
||||
it('resolves with account', () =>
|
||||
authenticate(account)(dispatch, getState, undefined).then((resp) =>
|
||||
expect(resp, 'to equal', account),
|
||||
));
|
||||
|
||||
it('rejects when bad auth data', () => {
|
||||
(authentication.validateToken as any).returns(Promise.reject({}));
|
||||
|
||||
return expect(
|
||||
authenticate(account)(dispatch, getState, undefined),
|
||||
'to be rejected',
|
||||
).then(() => {
|
||||
expect(dispatch, 'to have a call satisfying', [
|
||||
setLogin(account.email),
|
||||
]);
|
||||
|
||||
expect(browserHistory.push, 'to have a call satisfying', ['/login']);
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects when 5xx without logouting', () => {
|
||||
const resp = new InternalServerError('500', { status: 500 });
|
||||
|
||||
(authentication.validateToken as any).rejects(resp);
|
||||
|
||||
return expect(
|
||||
authenticate(account)(dispatch, getState, undefined),
|
||||
'to be rejected with',
|
||||
resp,
|
||||
).then(() =>
|
||||
expect(dispatch, 'to have no calls satisfying', [
|
||||
{ payload: { isGuest: true } },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('marks user as stranger, if there is no refreshToken', () => {
|
||||
const expectedKey = `stranger${account.id}`;
|
||||
(authentication.validateToken as any).resolves({
|
||||
token: account.token,
|
||||
user,
|
||||
});
|
||||
|
||||
sessionStorage.removeItem(expectedKey);
|
||||
|
||||
return authenticate(account)(dispatch, getState, undefined).then(() => {
|
||||
expect(sessionStorage.getItem(expectedKey), 'not to be null');
|
||||
sessionStorage.removeItem(expectedKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user authenticated during oauth', () => {
|
||||
beforeEach(() => {
|
||||
(getState as any).returns({
|
||||
accounts: {
|
||||
available: [],
|
||||
active: null,
|
||||
},
|
||||
user: {},
|
||||
auth: {
|
||||
oauth: {
|
||||
clientId: 'ely.by',
|
||||
prompt: [],
|
||||
accounts: {
|
||||
available: [],
|
||||
active: null,
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
credentials: {},
|
||||
},
|
||||
user: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch setAccountSwitcher', () =>
|
||||
authenticate(account)(dispatch, getState, undefined).then(() =>
|
||||
expect(dispatch, 'to have a call satisfying', [
|
||||
setAccountSwitcher(false),
|
||||
]),
|
||||
));
|
||||
});
|
||||
sinon.stub(authentication, 'validateToken').named('authentication.validateToken');
|
||||
sinon.stub(browserHistory, 'push').named('browserHistory.push');
|
||||
sinon.stub(authentication, 'logout').named('authentication.logout');
|
||||
|
||||
describe('when one account available', () => {
|
||||
beforeEach(() => {
|
||||
(getState as any).returns({
|
||||
accounts: {
|
||||
active: account.id,
|
||||
available: [account],
|
||||
},
|
||||
auth: {
|
||||
credentials: {},
|
||||
},
|
||||
user,
|
||||
});
|
||||
});
|
||||
|
||||
it('should activate account before auth api call', () => {
|
||||
(authentication.logout as any).returns(Promise.resolve());
|
||||
(authentication.validateToken as any).returns(
|
||||
Promise.reject({ error: 'foo' }),
|
||||
Promise.resolve({
|
||||
token: account.token,
|
||||
refreshToken: account.refreshToken,
|
||||
user,
|
||||
}),
|
||||
);
|
||||
|
||||
return expect(
|
||||
authenticate(account)(dispatch, getState, undefined),
|
||||
'to be rejected with',
|
||||
{ error: 'foo' },
|
||||
).then(() =>
|
||||
expect(dispatch, 'to have a call satisfying', [activate(account)]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#revoke()', () => {
|
||||
describe('when one account available', () => {
|
||||
beforeEach(() => {
|
||||
(getState as any).returns({
|
||||
accounts: {
|
||||
active: account.id,
|
||||
available: [account],
|
||||
},
|
||||
auth: {
|
||||
credentials: {},
|
||||
},
|
||||
user,
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch reset action', () =>
|
||||
revoke(account)(dispatch, getState, undefined).then(() =>
|
||||
expect(dispatch, 'to have a call satisfying', [reset()]),
|
||||
));
|
||||
|
||||
it('should call logout api method in background', () =>
|
||||
revoke(account)(dispatch, getState, undefined).then(() =>
|
||||
expect(authentication.logout, 'to have a call satisfying', [
|
||||
account.token,
|
||||
]),
|
||||
));
|
||||
|
||||
it('should update user state', () =>
|
||||
revoke(account)(dispatch, getState, undefined).then(
|
||||
() =>
|
||||
expect(dispatch, 'to have a call satisfying', [
|
||||
setUser({ isGuest: true }),
|
||||
]),
|
||||
// expect(dispatch, 'to have calls satisfying', [
|
||||
// [remove(account)],
|
||||
// [expect.it('to be a function')]
|
||||
// // [logout()] // TODO: this is not a plain action. How should we simplify its testing?
|
||||
// ])
|
||||
));
|
||||
});
|
||||
|
||||
describe('when multiple accounts available', () => {
|
||||
const account2 = { ...account, id: 2 };
|
||||
|
||||
beforeEach(() => {
|
||||
(getState as any).returns({
|
||||
accounts: {
|
||||
active: account2.id,
|
||||
available: [account, account2],
|
||||
},
|
||||
user,
|
||||
});
|
||||
});
|
||||
|
||||
it('should switch to the next account', () =>
|
||||
revoke(account2)(dispatch, getState, undefined).then(() =>
|
||||
expect(dispatch, 'to have a call satisfying', [activate(account)]),
|
||||
));
|
||||
|
||||
it('should remove current account', () =>
|
||||
revoke(account2)(dispatch, getState, undefined).then(() =>
|
||||
expect(dispatch, 'to have a call satisfying', [remove(account2)]),
|
||||
));
|
||||
|
||||
it('should call logout api method in background', () =>
|
||||
revoke(account2)(dispatch, getState, undefined).then(() =>
|
||||
expect(authentication.logout, 'to have a call satisfying', [
|
||||
account2.token,
|
||||
]),
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#logoutAll()', () => {
|
||||
const account2 = { ...account, id: 2 };
|
||||
|
||||
beforeEach(() => {
|
||||
(getState as any).returns({
|
||||
accounts: {
|
||||
active: account2.id,
|
||||
available: [account, account2],
|
||||
},
|
||||
auth: {
|
||||
credentials: {},
|
||||
},
|
||||
user,
|
||||
});
|
||||
afterEach(() => {
|
||||
(authentication.validateToken as any).restore();
|
||||
(authentication.logout as any).restore();
|
||||
(browserHistory.push as any).restore();
|
||||
});
|
||||
|
||||
it('should call logout api method for each account', () => {
|
||||
logoutAll()(dispatch, getState, undefined);
|
||||
describe('#authenticate()', () => {
|
||||
it('should request user state using token', () =>
|
||||
authenticate(account)(dispatch, getState, undefined).then(() =>
|
||||
expect(authentication.validateToken, 'to have a call satisfying', [
|
||||
account.id,
|
||||
account.token,
|
||||
account.refreshToken,
|
||||
]),
|
||||
));
|
||||
|
||||
expect(authentication.logout, 'to have calls satisfying', [
|
||||
[account.token],
|
||||
[account2.token],
|
||||
]);
|
||||
});
|
||||
it('should request user by extracting id from token', () =>
|
||||
authenticate({ token } as Account)(dispatch, getState, undefined).then(() =>
|
||||
expect(authentication.validateToken, 'to have a call satisfying', [1, token, undefined]),
|
||||
));
|
||||
|
||||
it('should dispatch reset', () => {
|
||||
logoutAll()(dispatch, getState, undefined);
|
||||
it('should request user by extracting id from legacy token', () =>
|
||||
authenticate({ token: legacyToken } as Account)(dispatch, getState, undefined).then(() =>
|
||||
expect(authentication.validateToken, 'to have a call satisfying', [1, legacyToken, undefined]),
|
||||
));
|
||||
|
||||
expect(dispatch, 'to have a call satisfying', [reset()]);
|
||||
});
|
||||
it(`dispatches accounts:add action`, () =>
|
||||
authenticate(account)(dispatch, getState, undefined).then(() =>
|
||||
expect(dispatch, 'to have a call satisfying', [add(account)]),
|
||||
));
|
||||
|
||||
it('should redirect to /login', () =>
|
||||
logoutAll()(dispatch, getState, undefined).then(() => {
|
||||
expect(browserHistory.push, 'to have a call satisfying', ['/login']);
|
||||
}));
|
||||
it(`dispatches accounts:activate action`, () =>
|
||||
authenticate(account)(dispatch, getState, undefined).then(() =>
|
||||
expect(dispatch, 'to have a call satisfying', [activate(account)]),
|
||||
));
|
||||
|
||||
it('should change user to guest', () =>
|
||||
logoutAll()(dispatch, getState, undefined).then(() => {
|
||||
expect(dispatch, 'to have a call satisfying', [
|
||||
setUser({
|
||||
lang: user.lang,
|
||||
isGuest: true,
|
||||
}),
|
||||
]);
|
||||
}));
|
||||
});
|
||||
it(`dispatches i18n:setLocale action`, () =>
|
||||
authenticate(account)(dispatch, getState, undefined).then(() =>
|
||||
expect(dispatch, 'to have a call satisfying', [{ type: 'i18n:setLocale', payload: { locale: 'be' } }]),
|
||||
));
|
||||
|
||||
describe('#logoutStrangers', () => {
|
||||
const foreignAccount = {
|
||||
...account,
|
||||
id: 2,
|
||||
refreshToken: null,
|
||||
};
|
||||
it('should update user state', () =>
|
||||
authenticate(account)(dispatch, getState, undefined).then(() =>
|
||||
expect(dispatch, 'to have a call satisfying', [updateUser({ ...user, isGuest: false })]),
|
||||
));
|
||||
|
||||
const foreignAccount2 = {
|
||||
...foreignAccount,
|
||||
id: 3,
|
||||
};
|
||||
it('resolves with account', () =>
|
||||
authenticate(account)(dispatch, getState, undefined).then((resp) => expect(resp, 'to equal', account)));
|
||||
|
||||
beforeEach(() => {
|
||||
(getState as any).returns({
|
||||
accounts: {
|
||||
active: foreignAccount.id,
|
||||
available: [account, foreignAccount, foreignAccount2],
|
||||
},
|
||||
user,
|
||||
});
|
||||
});
|
||||
it('rejects when bad auth data', () => {
|
||||
(authentication.validateToken as any).returns(Promise.reject({}));
|
||||
|
||||
it('should remove stranger accounts', () => {
|
||||
logoutStrangers()(dispatch, getState, undefined);
|
||||
return expect(authenticate(account)(dispatch, getState, undefined), 'to be rejected').then(() => {
|
||||
expect(dispatch, 'to have a call satisfying', [setLogin(account.email)]);
|
||||
|
||||
expect(dispatch, 'to have a call satisfying', [remove(foreignAccount)]);
|
||||
expect(dispatch, 'to have a call satisfying', [remove(foreignAccount2)]);
|
||||
});
|
||||
|
||||
it('should logout stranger accounts', () => {
|
||||
logoutStrangers()(dispatch, getState, undefined);
|
||||
|
||||
expect(authentication.logout, 'to have calls satisfying', [
|
||||
[foreignAccount.token],
|
||||
[foreignAccount2.token],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should activate another account if available', () =>
|
||||
logoutStrangers()(dispatch, getState, undefined).then(() =>
|
||||
expect(dispatch, 'to have a call satisfying', [activate(account)]),
|
||||
));
|
||||
|
||||
it('should not activate another account if active account is already not a stranger', () => {
|
||||
(getState as any).returns({
|
||||
accounts: {
|
||||
active: account.id,
|
||||
available: [account, foreignAccount],
|
||||
},
|
||||
user,
|
||||
});
|
||||
|
||||
return logoutStrangers()(dispatch, getState, undefined).then(() =>
|
||||
expect(dispatch, 'not to have calls satisfying', [activate(account)]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not dispatch if no strangers', () => {
|
||||
(getState as any).returns({
|
||||
accounts: {
|
||||
active: account.id,
|
||||
available: [account],
|
||||
},
|
||||
user,
|
||||
});
|
||||
|
||||
return logoutStrangers()(dispatch, getState, undefined).then(() =>
|
||||
expect(dispatch, 'was not called'),
|
||||
);
|
||||
});
|
||||
|
||||
describe('when all accounts are strangers', () => {
|
||||
beforeEach(() => {
|
||||
(getState as any).returns({
|
||||
accounts: {
|
||||
active: foreignAccount.id,
|
||||
available: [foreignAccount, foreignAccount2],
|
||||
},
|
||||
auth: {
|
||||
credentials: {},
|
||||
},
|
||||
user,
|
||||
expect(browserHistory.push, 'to have a call satisfying', ['/login']);
|
||||
});
|
||||
});
|
||||
|
||||
logoutStrangers()(dispatch, getState, undefined);
|
||||
});
|
||||
it('rejects when 5xx without logouting', () => {
|
||||
const resp = new InternalServerError('500', { status: 500 });
|
||||
|
||||
it('logouts all accounts', () => {
|
||||
expect(authentication.logout, 'to have calls satisfying', [
|
||||
[foreignAccount.token],
|
||||
[foreignAccount2.token],
|
||||
]);
|
||||
(authentication.validateToken as any).rejects(resp);
|
||||
|
||||
expect(dispatch, 'to have a call satisfying', [
|
||||
setUser({ isGuest: true }),
|
||||
]);
|
||||
return expect(authenticate(account)(dispatch, getState, undefined), 'to be rejected with', resp).then(() =>
|
||||
expect(dispatch, 'to have no calls satisfying', [{ payload: { isGuest: true } }]),
|
||||
);
|
||||
});
|
||||
|
||||
expect(dispatch, 'to have a call satisfying', [reset()]);
|
||||
});
|
||||
it('marks user as stranger, if there is no refreshToken', () => {
|
||||
const expectedKey = `stranger${account.id}`;
|
||||
(authentication.validateToken as any).resolves({
|
||||
token: account.token,
|
||||
user,
|
||||
});
|
||||
|
||||
sessionStorage.removeItem(expectedKey);
|
||||
|
||||
return authenticate(account)(dispatch, getState, undefined).then(() => {
|
||||
expect(sessionStorage.getItem(expectedKey), 'not to be null');
|
||||
sessionStorage.removeItem(expectedKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user authenticated during oauth', () => {
|
||||
beforeEach(() => {
|
||||
(getState as any).returns({
|
||||
accounts: {
|
||||
available: [],
|
||||
active: null,
|
||||
},
|
||||
user: {},
|
||||
auth: {
|
||||
oauth: {
|
||||
clientId: 'ely.by',
|
||||
prompt: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch setAccountSwitcher', () =>
|
||||
authenticate(account)(dispatch, getState, undefined).then(() =>
|
||||
expect(dispatch, 'to have a call satisfying', [setAccountSwitcher(false)]),
|
||||
));
|
||||
});
|
||||
|
||||
describe('when one account available', () => {
|
||||
beforeEach(() => {
|
||||
(getState as any).returns({
|
||||
accounts: {
|
||||
active: account.id,
|
||||
available: [account],
|
||||
},
|
||||
auth: {
|
||||
credentials: {},
|
||||
},
|
||||
user,
|
||||
});
|
||||
});
|
||||
|
||||
it('should activate account before auth api call', () => {
|
||||
(authentication.validateToken as any).returns(Promise.reject({ error: 'foo' }));
|
||||
|
||||
return expect(authenticate(account)(dispatch, getState, undefined), 'to be rejected with', {
|
||||
error: 'foo',
|
||||
}).then(() => expect(dispatch, 'to have a call satisfying', [activate(account)]));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a stranger has a mark in sessionStorage', () => {
|
||||
const key = `stranger${foreignAccount.id}`;
|
||||
describe('#revoke()', () => {
|
||||
describe('when one account available', () => {
|
||||
beforeEach(() => {
|
||||
(getState as any).returns({
|
||||
accounts: {
|
||||
active: account.id,
|
||||
available: [account],
|
||||
},
|
||||
auth: {
|
||||
credentials: {},
|
||||
},
|
||||
user,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
sessionStorage.setItem(key, '1');
|
||||
it('should dispatch reset action', () =>
|
||||
revoke(account)(dispatch, getState, undefined).then(() =>
|
||||
expect(dispatch, 'to have a call satisfying', [reset()]),
|
||||
));
|
||||
|
||||
logoutStrangers()(dispatch, getState, undefined);
|
||||
});
|
||||
it('should call logout api method in background', () =>
|
||||
revoke(account)(dispatch, getState, undefined).then(() =>
|
||||
expect(authentication.logout, 'to have a call satisfying', [account.token]),
|
||||
));
|
||||
|
||||
afterEach(() => {
|
||||
sessionStorage.removeItem(key);
|
||||
});
|
||||
it('should update user state', () =>
|
||||
revoke(account)(dispatch, getState, undefined).then(
|
||||
() => expect(dispatch, 'to have a call satisfying', [setUser({ isGuest: true })]),
|
||||
// expect(dispatch, 'to have calls satisfying', [
|
||||
// [remove(account)],
|
||||
// [expect.it('to be a function')]
|
||||
// // [logout()] // TODO: this is not a plain action. How should we simplify its testing?
|
||||
// ])
|
||||
));
|
||||
});
|
||||
|
||||
it('should not log out', () =>
|
||||
expect(dispatch, 'not to have calls satisfying', [
|
||||
{ payload: foreignAccount },
|
||||
]));
|
||||
describe('when multiple accounts available', () => {
|
||||
const account2 = { ...account, id: 2 };
|
||||
|
||||
beforeEach(() => {
|
||||
(getState as any).returns({
|
||||
accounts: {
|
||||
active: account2.id,
|
||||
available: [account, account2],
|
||||
},
|
||||
user,
|
||||
});
|
||||
});
|
||||
|
||||
it('should switch to the next account', () =>
|
||||
revoke(account2)(dispatch, getState, undefined).then(() =>
|
||||
expect(dispatch, 'to have a call satisfying', [activate(account)]),
|
||||
));
|
||||
|
||||
it('should remove current account', () =>
|
||||
revoke(account2)(dispatch, getState, undefined).then(() =>
|
||||
expect(dispatch, 'to have a call satisfying', [remove(account2)]),
|
||||
));
|
||||
|
||||
it('should call logout api method in background', () =>
|
||||
revoke(account2)(dispatch, getState, undefined).then(() =>
|
||||
expect(authentication.logout, 'to have a call satisfying', [account2.token]),
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#logoutAll()', () => {
|
||||
const account2 = { ...account, id: 2 };
|
||||
|
||||
beforeEach(() => {
|
||||
(getState as any).returns({
|
||||
accounts: {
|
||||
active: account2.id,
|
||||
available: [account, account2],
|
||||
},
|
||||
auth: {
|
||||
credentials: {},
|
||||
},
|
||||
user,
|
||||
});
|
||||
});
|
||||
|
||||
it('should call logout api method for each account', () => {
|
||||
logoutAll()(dispatch, getState, undefined);
|
||||
|
||||
expect(authentication.logout, 'to have calls satisfying', [[account.token], [account2.token]]);
|
||||
});
|
||||
|
||||
it('should dispatch reset', () => {
|
||||
logoutAll()(dispatch, getState, undefined);
|
||||
|
||||
expect(dispatch, 'to have a call satisfying', [reset()]);
|
||||
});
|
||||
|
||||
it('should redirect to /login', () =>
|
||||
logoutAll()(dispatch, getState, undefined).then(() => {
|
||||
expect(browserHistory.push, 'to have a call satisfying', ['/login']);
|
||||
}));
|
||||
|
||||
it('should change user to guest', () =>
|
||||
logoutAll()(dispatch, getState, undefined).then(() => {
|
||||
expect(dispatch, 'to have a call satisfying', [
|
||||
setUser({
|
||||
lang: user.lang,
|
||||
isGuest: true,
|
||||
}),
|
||||
]);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('#logoutStrangers', () => {
|
||||
const foreignAccount = {
|
||||
...account,
|
||||
id: 2,
|
||||
refreshToken: null,
|
||||
};
|
||||
|
||||
const foreignAccount2 = {
|
||||
...foreignAccount,
|
||||
id: 3,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
(getState as any).returns({
|
||||
accounts: {
|
||||
active: foreignAccount.id,
|
||||
available: [account, foreignAccount, foreignAccount2],
|
||||
},
|
||||
user,
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove stranger accounts', () => {
|
||||
logoutStrangers()(dispatch, getState, undefined);
|
||||
|
||||
expect(dispatch, 'to have a call satisfying', [remove(foreignAccount)]);
|
||||
expect(dispatch, 'to have a call satisfying', [remove(foreignAccount2)]);
|
||||
});
|
||||
|
||||
it('should logout stranger accounts', () => {
|
||||
logoutStrangers()(dispatch, getState, undefined);
|
||||
|
||||
expect(authentication.logout, 'to have calls satisfying', [
|
||||
[foreignAccount.token],
|
||||
[foreignAccount2.token],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should activate another account if available', () =>
|
||||
logoutStrangers()(dispatch, getState, undefined).then(() =>
|
||||
expect(dispatch, 'to have a call satisfying', [activate(account)]),
|
||||
));
|
||||
|
||||
it('should not activate another account if active account is already not a stranger', () => {
|
||||
(getState as any).returns({
|
||||
accounts: {
|
||||
active: account.id,
|
||||
available: [account, foreignAccount],
|
||||
},
|
||||
user,
|
||||
});
|
||||
|
||||
return logoutStrangers()(dispatch, getState, undefined).then(() =>
|
||||
expect(dispatch, 'not to have calls satisfying', [activate(account)]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not dispatch if no strangers', () => {
|
||||
(getState as any).returns({
|
||||
accounts: {
|
||||
active: account.id,
|
||||
available: [account],
|
||||
},
|
||||
user,
|
||||
});
|
||||
|
||||
return logoutStrangers()(dispatch, getState, undefined).then(() => expect(dispatch, 'was not called'));
|
||||
});
|
||||
|
||||
describe('when all accounts are strangers', () => {
|
||||
beforeEach(() => {
|
||||
(getState as any).returns({
|
||||
accounts: {
|
||||
active: foreignAccount.id,
|
||||
available: [foreignAccount, foreignAccount2],
|
||||
},
|
||||
auth: {
|
||||
credentials: {},
|
||||
},
|
||||
user,
|
||||
});
|
||||
|
||||
logoutStrangers()(dispatch, getState, undefined);
|
||||
});
|
||||
|
||||
it('logouts all accounts', () => {
|
||||
expect(authentication.logout, 'to have calls satisfying', [
|
||||
[foreignAccount.token],
|
||||
[foreignAccount2.token],
|
||||
]);
|
||||
|
||||
expect(dispatch, 'to have a call satisfying', [setUser({ isGuest: true })]);
|
||||
|
||||
expect(dispatch, 'to have a call satisfying', [reset()]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a stranger has a mark in sessionStorage', () => {
|
||||
const key = `stranger${foreignAccount.id}`;
|
||||
|
||||
beforeEach(() => {
|
||||
sessionStorage.setItem(key, '1');
|
||||
|
||||
logoutStrangers()(dispatch, getState, undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sessionStorage.removeItem(key);
|
||||
});
|
||||
|
||||
it('should not log out', () =>
|
||||
expect(dispatch, 'not to have calls satisfying', [{ payload: foreignAccount }]));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,27 +1,14 @@
|
||||
import { getJwtPayloads } from 'app/functions';
|
||||
import { sessionStorage } from 'app/services/localStorage';
|
||||
import {
|
||||
validateToken,
|
||||
requestToken,
|
||||
logout,
|
||||
} from 'app/services/api/authentication';
|
||||
import {
|
||||
relogin as navigateToLogin,
|
||||
setAccountSwitcher,
|
||||
} from 'app/components/auth/actions';
|
||||
import { validateToken, requestToken, logout } from 'app/services/api/authentication';
|
||||
import { relogin as navigateToLogin, setAccountSwitcher } from 'app/components/auth/actions';
|
||||
import { updateUser, setGuest } from 'app/components/user/actions';
|
||||
import { setLocale } from 'app/components/i18n/actions';
|
||||
import logger from 'app/services/logger';
|
||||
import { ThunkAction } from 'app/reducers';
|
||||
|
||||
import { getActiveAccount, Account } from './reducer';
|
||||
import {
|
||||
add,
|
||||
remove,
|
||||
activate,
|
||||
reset,
|
||||
updateToken,
|
||||
} from './actions/pure-actions';
|
||||
import { add, remove, activate, reset, updateToken } from './actions/pure-actions';
|
||||
|
||||
export { updateToken, activate, remove };
|
||||
|
||||
@ -33,120 +20,118 @@ export { updateToken, activate, remove };
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function authenticate(
|
||||
account:
|
||||
| Account
|
||||
| {
|
||||
token: string;
|
||||
refreshToken: string | null;
|
||||
},
|
||||
account:
|
||||
| Account
|
||||
| {
|
||||
token: string;
|
||||
refreshToken: string | null;
|
||||
},
|
||||
): ThunkAction<Promise<Account>> {
|
||||
const { token, refreshToken } = account;
|
||||
const email = 'email' in account ? account.email : null;
|
||||
const { token, refreshToken } = account;
|
||||
const email = 'email' in account ? account.email : null;
|
||||
|
||||
return async (dispatch, getState) => {
|
||||
let accountId: number;
|
||||
return async (dispatch, getState) => {
|
||||
let accountId: number;
|
||||
|
||||
if ('id' in account && typeof account.id === 'number') {
|
||||
accountId = account.id;
|
||||
} else {
|
||||
accountId = findAccountIdFromToken(token);
|
||||
}
|
||||
if ('id' in account && typeof account.id === 'number') {
|
||||
accountId = account.id;
|
||||
} else {
|
||||
accountId = findAccountIdFromToken(token);
|
||||
}
|
||||
|
||||
const knownAccount = getState().accounts.available.find(
|
||||
(item) => item.id === accountId,
|
||||
);
|
||||
const knownAccount = getState().accounts.available.find((item) => item.id === accountId);
|
||||
|
||||
if (knownAccount) {
|
||||
// this account is already available
|
||||
// activate it before validation
|
||||
dispatch(activate(knownAccount));
|
||||
}
|
||||
if (knownAccount) {
|
||||
// this account is already available
|
||||
// activate it before validation
|
||||
dispatch(activate(knownAccount));
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
token: newToken,
|
||||
refreshToken: newRefreshToken,
|
||||
user,
|
||||
} = await validateToken(accountId, token, refreshToken);
|
||||
const { auth } = getState();
|
||||
const newAccount: Account = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
token: newToken,
|
||||
refreshToken: newRefreshToken,
|
||||
};
|
||||
dispatch(add(newAccount));
|
||||
dispatch(activate(newAccount));
|
||||
dispatch(
|
||||
updateUser({
|
||||
isGuest: false,
|
||||
...user,
|
||||
}),
|
||||
);
|
||||
try {
|
||||
const { token: newToken, refreshToken: newRefreshToken, user } = await validateToken(
|
||||
accountId,
|
||||
token,
|
||||
refreshToken,
|
||||
);
|
||||
const { auth } = getState();
|
||||
const newAccount: Account = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
token: newToken,
|
||||
refreshToken: newRefreshToken,
|
||||
};
|
||||
dispatch(add(newAccount));
|
||||
dispatch(activate(newAccount));
|
||||
dispatch(
|
||||
updateUser({
|
||||
isGuest: false,
|
||||
...user,
|
||||
}),
|
||||
);
|
||||
|
||||
// TODO: probably should be moved from here, because it is a side effect
|
||||
logger.setUser(user);
|
||||
// TODO: probably should be moved from here, because it is a side effect
|
||||
logger.setUser(user);
|
||||
|
||||
if (!newRefreshToken) {
|
||||
// mark user as stranger (user does not want us to remember his account)
|
||||
sessionStorage.setItem(`stranger${newAccount.id}`, '1');
|
||||
}
|
||||
if (!newRefreshToken) {
|
||||
// mark user as stranger (user does not want us to remember his account)
|
||||
sessionStorage.setItem(`stranger${newAccount.id}`, '1');
|
||||
}
|
||||
|
||||
if (auth && auth.oauth && auth.oauth.clientId) {
|
||||
// if we authenticating during oauth, we disable account chooser
|
||||
// because user probably has made his choise now
|
||||
// this may happen, when user registers, logs in or uses account
|
||||
// chooser panel during oauth
|
||||
dispatch(setAccountSwitcher(false));
|
||||
}
|
||||
if (auth && auth.oauth && auth.oauth.clientId) {
|
||||
// if we authenticating during oauth, we disable account chooser
|
||||
// because user probably has made his choise now
|
||||
// this may happen, when user registers, logs in or uses account
|
||||
// chooser panel during oauth
|
||||
dispatch(setAccountSwitcher(false));
|
||||
}
|
||||
|
||||
await dispatch(setLocale(user.lang));
|
||||
await dispatch(setLocale(user.lang));
|
||||
|
||||
return newAccount;
|
||||
} catch (resp) {
|
||||
// all the logic to get the valid token was failed,
|
||||
// looks like we have some problems with token
|
||||
// lets redirect to login page
|
||||
if (typeof email === 'string') {
|
||||
// TODO: we should somehow try to find email by token
|
||||
dispatch(relogin(email));
|
||||
}
|
||||
return newAccount;
|
||||
} catch (resp) {
|
||||
// all the logic to get the valid token was failed,
|
||||
// looks like we have some problems with token
|
||||
// lets redirect to login page
|
||||
if (typeof email === 'string') {
|
||||
// TODO: we should somehow try to find email by token
|
||||
dispatch(relogin(email));
|
||||
}
|
||||
|
||||
throw resp;
|
||||
}
|
||||
};
|
||||
throw resp;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-fetch user data for currently active account
|
||||
*/
|
||||
export function refreshUserData(): ThunkAction<Promise<void>> {
|
||||
return async (dispatch, getState) => {
|
||||
const activeAccount = getActiveAccount(getState());
|
||||
return async (dispatch, getState) => {
|
||||
const activeAccount = getActiveAccount(getState());
|
||||
|
||||
if (!activeAccount) {
|
||||
throw new Error('Can not fetch user data. No user.id available');
|
||||
}
|
||||
if (!activeAccount) {
|
||||
throw new Error('Can not fetch user data. No user.id available');
|
||||
}
|
||||
|
||||
await dispatch(authenticate(activeAccount));
|
||||
};
|
||||
await dispatch(authenticate(activeAccount));
|
||||
};
|
||||
}
|
||||
|
||||
function findAccountIdFromToken(token: string): number {
|
||||
const { sub, jti } = getJwtPayloads(token);
|
||||
const { sub, jti } = getJwtPayloads(token);
|
||||
|
||||
// sub has the format "ely|{accountId}", so we must trim "ely|" part
|
||||
if (sub) {
|
||||
return parseInt(sub.substr(4), 10);
|
||||
}
|
||||
// sub has the format "ely|{accountId}", so we must trim "ely|" part
|
||||
if (sub) {
|
||||
return parseInt(sub.substr(4), 10);
|
||||
}
|
||||
|
||||
// In older backend versions identity was stored in jti claim. Some users still have such tokens
|
||||
if (jti) {
|
||||
return jti;
|
||||
}
|
||||
// In older backend versions identity was stored in jti claim. Some users still have such tokens
|
||||
if (jti) {
|
||||
return jti;
|
||||
}
|
||||
|
||||
throw new Error('payloads is not contains any identity claim');
|
||||
throw new Error('payloads is not contains any identity claim');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -158,28 +143,28 @@ function findAccountIdFromToken(token: string): number {
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function ensureToken(): ThunkAction<Promise<void>> {
|
||||
return (dispatch, getState) => {
|
||||
const { token } = getActiveAccount(getState()) || {};
|
||||
return (dispatch, getState) => {
|
||||
const { token } = getActiveAccount(getState()) || {};
|
||||
|
||||
try {
|
||||
const SAFETY_FACTOR = 300; // ask new token earlier to overcome time desynchronization problem
|
||||
const { exp } = getJwtPayloads(token as any);
|
||||
try {
|
||||
const SAFETY_FACTOR = 300; // ask new token earlier to overcome time desynchronization problem
|
||||
const { exp } = getJwtPayloads(token as any);
|
||||
|
||||
if (exp - SAFETY_FACTOR < Date.now() / 1000) {
|
||||
return dispatch(requestNewToken());
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn('Refresh token error: bad token', {
|
||||
token,
|
||||
});
|
||||
if (exp - SAFETY_FACTOR < Date.now() / 1000) {
|
||||
return dispatch(requestNewToken());
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn('Refresh token error: bad token', {
|
||||
token,
|
||||
});
|
||||
|
||||
dispatch(relogin());
|
||||
dispatch(relogin());
|
||||
|
||||
return Promise.reject(new Error('Invalid token'));
|
||||
}
|
||||
return Promise.reject(new Error('Invalid token'));
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
return Promise.resolve();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -193,40 +178,38 @@ export function ensureToken(): ThunkAction<Promise<void>> {
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function recoverFromTokenError(
|
||||
error: {
|
||||
status: number;
|
||||
message: string;
|
||||
} | void,
|
||||
error: {
|
||||
status: number;
|
||||
message: string;
|
||||
} | void,
|
||||
): ThunkAction<Promise<void>> {
|
||||
return (dispatch, getState) => {
|
||||
if (error && error.status === 401) {
|
||||
const activeAccount = getActiveAccount(getState());
|
||||
return (dispatch, getState) => {
|
||||
if (error && error.status === 401) {
|
||||
const activeAccount = getActiveAccount(getState());
|
||||
|
||||
if (activeAccount && activeAccount.refreshToken) {
|
||||
if (
|
||||
[
|
||||
'Token expired',
|
||||
'Incorrect token',
|
||||
'You are requesting with an invalid credential.',
|
||||
].includes(error.message)
|
||||
) {
|
||||
// request token and retry
|
||||
return dispatch(requestNewToken());
|
||||
if (activeAccount && activeAccount.refreshToken) {
|
||||
if (
|
||||
['Token expired', 'Incorrect token', 'You are requesting with an invalid credential.'].includes(
|
||||
error.message,
|
||||
)
|
||||
) {
|
||||
// request token and retry
|
||||
return dispatch(requestNewToken());
|
||||
}
|
||||
|
||||
logger.error('Unknown unauthorized response', {
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
// user's access token is outdated and we have no refreshToken
|
||||
// or something unexpected happend
|
||||
// in both cases we resetting all the user's state
|
||||
dispatch(relogin());
|
||||
}
|
||||
|
||||
logger.error('Unknown unauthorized response', {
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
// user's access token is outdated and we have no refreshToken
|
||||
// or something unexpected happend
|
||||
// in both cases we resetting all the user's state
|
||||
dispatch(relogin());
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
};
|
||||
return Promise.reject(error);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -236,28 +219,28 @@ export function recoverFromTokenError(
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function requestNewToken(): ThunkAction<Promise<void>> {
|
||||
return (dispatch, getState) => {
|
||||
const { refreshToken } = getActiveAccount(getState()) || {};
|
||||
return (dispatch, getState) => {
|
||||
const { refreshToken } = getActiveAccount(getState()) || {};
|
||||
|
||||
if (!refreshToken) {
|
||||
dispatch(relogin());
|
||||
if (!refreshToken) {
|
||||
dispatch(relogin());
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return requestToken(refreshToken)
|
||||
.then((token) => {
|
||||
dispatch(updateToken(token));
|
||||
})
|
||||
.catch((resp) => {
|
||||
// all the logic to get the valid token was failed,
|
||||
// looks like we have some problems with token
|
||||
// lets redirect to login page
|
||||
dispatch(relogin());
|
||||
return requestToken(refreshToken)
|
||||
.then((token) => {
|
||||
dispatch(updateToken(token));
|
||||
})
|
||||
.catch((resp) => {
|
||||
// all the logic to get the valid token was failed,
|
||||
// looks like we have some problems with token
|
||||
// lets redirect to login page
|
||||
dispatch(relogin());
|
||||
|
||||
return Promise.reject(resp);
|
||||
});
|
||||
};
|
||||
return Promise.reject(resp);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -268,62 +251,62 @@ export function requestNewToken(): ThunkAction<Promise<void>> {
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function revoke(account: Account): ThunkAction<Promise<void>> {
|
||||
return async (dispatch, getState) => {
|
||||
const accountToReplace: Account | null =
|
||||
getState().accounts.available.find(({ id }) => id !== account.id) || null;
|
||||
return async (dispatch, getState) => {
|
||||
const accountToReplace: Account | null =
|
||||
getState().accounts.available.find(({ id }) => id !== account.id) || null;
|
||||
|
||||
if (accountToReplace) {
|
||||
await dispatch(authenticate(accountToReplace))
|
||||
.finally(() => {
|
||||
// we need to logout user, even in case, when we can
|
||||
// not authenticate him with new account
|
||||
logout(account.token).catch(() => {
|
||||
// we don't care
|
||||
});
|
||||
dispatch(remove(account));
|
||||
})
|
||||
.catch(() => {
|
||||
// we don't care
|
||||
});
|
||||
if (accountToReplace) {
|
||||
await dispatch(authenticate(accountToReplace))
|
||||
.finally(() => {
|
||||
// we need to logout user, even in case, when we can
|
||||
// not authenticate him with new account
|
||||
logout(account.token).catch(() => {
|
||||
// we don't care
|
||||
});
|
||||
dispatch(remove(account));
|
||||
})
|
||||
.catch(() => {
|
||||
// we don't care
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return dispatch(logoutAll());
|
||||
};
|
||||
return dispatch(logoutAll());
|
||||
};
|
||||
}
|
||||
|
||||
export function relogin(email?: string): ThunkAction<Promise<void>> {
|
||||
return async (dispatch, getState) => {
|
||||
const activeAccount = getActiveAccount(getState());
|
||||
return async (dispatch, getState) => {
|
||||
const activeAccount = getActiveAccount(getState());
|
||||
|
||||
if (!email && activeAccount) {
|
||||
email = activeAccount.email;
|
||||
}
|
||||
if (!email && activeAccount) {
|
||||
email = activeAccount.email;
|
||||
}
|
||||
|
||||
dispatch(navigateToLogin(email || null));
|
||||
};
|
||||
dispatch(navigateToLogin(email || null));
|
||||
};
|
||||
}
|
||||
|
||||
export function logoutAll(): ThunkAction<Promise<void>> {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(setGuest());
|
||||
return (dispatch, getState) => {
|
||||
dispatch(setGuest());
|
||||
|
||||
const {
|
||||
accounts: { available },
|
||||
} = getState();
|
||||
const {
|
||||
accounts: { available },
|
||||
} = getState();
|
||||
|
||||
available.forEach((account) =>
|
||||
logout(account.token).catch(() => {
|
||||
// we don't care
|
||||
}),
|
||||
);
|
||||
available.forEach((account) =>
|
||||
logout(account.token).catch(() => {
|
||||
// we don't care
|
||||
}),
|
||||
);
|
||||
|
||||
dispatch(reset());
|
||||
dispatch(relogin());
|
||||
dispatch(reset());
|
||||
dispatch(relogin());
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
return Promise.resolve();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -335,38 +318,35 @@ export function logoutAll(): ThunkAction<Promise<void>> {
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function logoutStrangers(): ThunkAction<Promise<void>> {
|
||||
return async (dispatch, getState) => {
|
||||
const {
|
||||
accounts: { available },
|
||||
} = getState();
|
||||
const activeAccount = getActiveAccount(getState());
|
||||
return async (dispatch, getState) => {
|
||||
const {
|
||||
accounts: { available },
|
||||
} = getState();
|
||||
const activeAccount = getActiveAccount(getState());
|
||||
|
||||
const isStranger = ({ refreshToken, id }: Account) =>
|
||||
!refreshToken && !sessionStorage.getItem(`stranger${id}`);
|
||||
const isStranger = ({ refreshToken, id }: Account) => !refreshToken && !sessionStorage.getItem(`stranger${id}`);
|
||||
|
||||
if (available.some(isStranger)) {
|
||||
const accountToReplace = available.find(
|
||||
(account) => !isStranger(account),
|
||||
);
|
||||
if (available.some(isStranger)) {
|
||||
const accountToReplace = available.find((account) => !isStranger(account));
|
||||
|
||||
if (accountToReplace) {
|
||||
available.filter(isStranger).forEach((account) => {
|
||||
dispatch(remove(account));
|
||||
logout(account.token);
|
||||
});
|
||||
if (accountToReplace) {
|
||||
available.filter(isStranger).forEach((account) => {
|
||||
dispatch(remove(account));
|
||||
logout(account.token);
|
||||
});
|
||||
|
||||
if (activeAccount && isStranger(activeAccount)) {
|
||||
await dispatch(authenticate(accountToReplace));
|
||||
if (activeAccount && isStranger(activeAccount)) {
|
||||
await dispatch(authenticate(accountToReplace));
|
||||
|
||||
return;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
await dispatch(logoutAll());
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await dispatch(logoutAll());
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
return Promise.resolve();
|
||||
};
|
||||
}
|
||||
|
@ -2,66 +2,61 @@ import { Action as ReduxAction } from 'redux';
|
||||
import { Account } from 'app/components/accounts/reducer';
|
||||
|
||||
interface AddAction extends ReduxAction {
|
||||
type: 'accounts:add';
|
||||
payload: Account;
|
||||
type: 'accounts:add';
|
||||
payload: Account;
|
||||
}
|
||||
|
||||
export function add(account: Account): AddAction {
|
||||
return {
|
||||
type: 'accounts:add',
|
||||
payload: account,
|
||||
};
|
||||
return {
|
||||
type: 'accounts:add',
|
||||
payload: account,
|
||||
};
|
||||
}
|
||||
|
||||
interface RemoveAction extends ReduxAction {
|
||||
type: 'accounts:remove';
|
||||
payload: Account;
|
||||
type: 'accounts:remove';
|
||||
payload: Account;
|
||||
}
|
||||
|
||||
export function remove(account: Account): RemoveAction {
|
||||
return {
|
||||
type: 'accounts:remove',
|
||||
payload: account,
|
||||
};
|
||||
return {
|
||||
type: 'accounts:remove',
|
||||
payload: account,
|
||||
};
|
||||
}
|
||||
|
||||
interface ActivateAction extends ReduxAction {
|
||||
type: 'accounts:activate';
|
||||
payload: Account;
|
||||
type: 'accounts:activate';
|
||||
payload: Account;
|
||||
}
|
||||
|
||||
export function activate(account: Account): ActivateAction {
|
||||
return {
|
||||
type: 'accounts:activate',
|
||||
payload: account,
|
||||
};
|
||||
return {
|
||||
type: 'accounts:activate',
|
||||
payload: account,
|
||||
};
|
||||
}
|
||||
|
||||
interface ResetAction extends ReduxAction {
|
||||
type: 'accounts:reset';
|
||||
type: 'accounts:reset';
|
||||
}
|
||||
|
||||
export function reset(): ResetAction {
|
||||
return {
|
||||
type: 'accounts:reset',
|
||||
};
|
||||
return {
|
||||
type: 'accounts:reset',
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateTokenAction extends ReduxAction {
|
||||
type: 'accounts:updateToken';
|
||||
payload: string;
|
||||
type: 'accounts:updateToken';
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export function updateToken(token: string): UpdateTokenAction {
|
||||
return {
|
||||
type: 'accounts:updateToken',
|
||||
payload: token,
|
||||
};
|
||||
return {
|
||||
type: 'accounts:updateToken',
|
||||
payload: token,
|
||||
};
|
||||
}
|
||||
|
||||
export type Action =
|
||||
| AddAction
|
||||
| RemoveAction
|
||||
| ActivateAction
|
||||
| ResetAction
|
||||
| UpdateTokenAction;
|
||||
export type Action = AddAction | RemoveAction | ActivateAction | ResetAction | UpdateTokenAction;
|
||||
|
@ -6,148 +6,122 @@ import { AccountsState } from './index';
|
||||
import accounts, { Account } from './reducer';
|
||||
|
||||
const account: Account = {
|
||||
id: 1,
|
||||
username: 'username',
|
||||
email: 'email@test.com',
|
||||
token: 'foo',
|
||||
id: 1,
|
||||
username: 'username',
|
||||
email: 'email@test.com',
|
||||
token: 'foo',
|
||||
} as Account;
|
||||
|
||||
describe('Accounts reducer', () => {
|
||||
let initial: AccountsState;
|
||||
let initial: AccountsState;
|
||||
|
||||
beforeEach(() => {
|
||||
initial = accounts(undefined, {} as any);
|
||||
});
|
||||
|
||||
it('should be empty', () =>
|
||||
expect(accounts(undefined, {} as any), 'to equal', {
|
||||
active: null,
|
||||
available: [],
|
||||
}));
|
||||
|
||||
it('should return last state if unsupported action', () =>
|
||||
expect(accounts({ state: 'foo' } as any, {} as any), 'to equal', {
|
||||
state: 'foo',
|
||||
}));
|
||||
|
||||
describe('accounts:activate', () => {
|
||||
it('sets active account', () => {
|
||||
expect(accounts(initial, activate(account)), 'to satisfy', {
|
||||
active: account.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('accounts:add', () => {
|
||||
it('adds an account', () =>
|
||||
expect(accounts(initial, add(account)), 'to satisfy', {
|
||||
available: [account],
|
||||
}));
|
||||
|
||||
it('should replace if account was added for the second time', () => {
|
||||
const outdatedAccount = {
|
||||
...account,
|
||||
someShit: true,
|
||||
};
|
||||
|
||||
const updatedAccount = {
|
||||
...account,
|
||||
token: 'newToken',
|
||||
};
|
||||
|
||||
expect(
|
||||
accounts(
|
||||
{ ...initial, available: [outdatedAccount] },
|
||||
add(updatedAccount),
|
||||
),
|
||||
'to satisfy',
|
||||
{
|
||||
available: [updatedAccount],
|
||||
},
|
||||
);
|
||||
beforeEach(() => {
|
||||
initial = accounts(undefined, {} as any);
|
||||
});
|
||||
|
||||
it('should sort accounts by username', () => {
|
||||
const newAccount = {
|
||||
...account,
|
||||
id: 2,
|
||||
username: 'abc',
|
||||
};
|
||||
it('should be empty', () =>
|
||||
expect(accounts(undefined, {} as any), 'to equal', {
|
||||
active: null,
|
||||
available: [],
|
||||
}));
|
||||
|
||||
expect(
|
||||
accounts({ ...initial, available: [account] }, add(newAccount)),
|
||||
'to satisfy',
|
||||
{
|
||||
available: [newAccount, account],
|
||||
},
|
||||
);
|
||||
it('should return last state if unsupported action', () =>
|
||||
expect(accounts({ state: 'foo' } as any, {} as any), 'to equal', {
|
||||
state: 'foo',
|
||||
}));
|
||||
|
||||
describe('accounts:activate', () => {
|
||||
it('sets active account', () => {
|
||||
expect(accounts(initial, activate(account)), 'to satisfy', {
|
||||
active: account.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('throws, when account is invalid', () => {
|
||||
expect(
|
||||
() =>
|
||||
accounts(
|
||||
initial,
|
||||
// @ts-ignore
|
||||
add(),
|
||||
),
|
||||
'to throw',
|
||||
'Invalid or empty payload passed for accounts.add',
|
||||
);
|
||||
describe('accounts:add', () => {
|
||||
it('adds an account', () =>
|
||||
expect(accounts(initial, add(account)), 'to satisfy', {
|
||||
available: [account],
|
||||
}));
|
||||
|
||||
it('should replace if account was added for the second time', () => {
|
||||
const outdatedAccount = {
|
||||
...account,
|
||||
someShit: true,
|
||||
};
|
||||
|
||||
const updatedAccount = {
|
||||
...account,
|
||||
token: 'newToken',
|
||||
};
|
||||
|
||||
expect(accounts({ ...initial, available: [outdatedAccount] }, add(updatedAccount)), 'to satisfy', {
|
||||
available: [updatedAccount],
|
||||
});
|
||||
});
|
||||
|
||||
it('should sort accounts by username', () => {
|
||||
const newAccount = {
|
||||
...account,
|
||||
id: 2,
|
||||
username: 'abc',
|
||||
};
|
||||
|
||||
expect(accounts({ ...initial, available: [account] }, add(newAccount)), 'to satisfy', {
|
||||
available: [newAccount, account],
|
||||
});
|
||||
});
|
||||
|
||||
it('throws, when account is invalid', () => {
|
||||
expect(
|
||||
() =>
|
||||
accounts(
|
||||
initial,
|
||||
// @ts-ignore
|
||||
add(),
|
||||
),
|
||||
'to throw',
|
||||
'Invalid or empty payload passed for accounts.add',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('accounts:remove', () => {
|
||||
it('should remove an account', () =>
|
||||
expect(
|
||||
accounts({ ...initial, available: [account] }, remove(account)),
|
||||
'to equal',
|
||||
initial,
|
||||
));
|
||||
describe('accounts:remove', () => {
|
||||
it('should remove an account', () =>
|
||||
expect(accounts({ ...initial, available: [account] }, remove(account)), 'to equal', initial));
|
||||
|
||||
it('throws, when account is invalid', () => {
|
||||
expect(
|
||||
() =>
|
||||
accounts(
|
||||
initial,
|
||||
// @ts-ignore
|
||||
remove(),
|
||||
),
|
||||
'to throw',
|
||||
'Invalid or empty payload passed for accounts.remove',
|
||||
);
|
||||
it('throws, when account is invalid', () => {
|
||||
expect(
|
||||
() =>
|
||||
accounts(
|
||||
initial,
|
||||
// @ts-ignore
|
||||
remove(),
|
||||
),
|
||||
'to throw',
|
||||
'Invalid or empty payload passed for accounts.remove',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('actions:reset', () => {
|
||||
it('should reset accounts state', () =>
|
||||
expect(
|
||||
accounts({ ...initial, available: [account] }, reset()),
|
||||
'to equal',
|
||||
initial,
|
||||
));
|
||||
});
|
||||
|
||||
describe('accounts:updateToken', () => {
|
||||
it('should update token', () => {
|
||||
const newToken = 'newToken';
|
||||
|
||||
expect(
|
||||
accounts(
|
||||
{ active: account.id, available: [account] },
|
||||
updateToken(newToken),
|
||||
),
|
||||
'to satisfy',
|
||||
{
|
||||
active: account.id,
|
||||
available: [
|
||||
{
|
||||
...account,
|
||||
token: newToken,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
describe('actions:reset', () => {
|
||||
it('should reset accounts state', () =>
|
||||
expect(accounts({ ...initial, available: [account] }, reset()), 'to equal', initial));
|
||||
});
|
||||
|
||||
describe('accounts:updateToken', () => {
|
||||
it('should update token', () => {
|
||||
const newToken = 'newToken';
|
||||
|
||||
expect(accounts({ active: account.id, available: [account] }, updateToken(newToken)), 'to satisfy', {
|
||||
active: account.id,
|
||||
available: [
|
||||
{
|
||||
...account,
|
||||
token: newToken,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,124 +1,116 @@
|
||||
import { Action } from './actions/pure-actions';
|
||||
|
||||
export type Account = {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
token: string;
|
||||
refreshToken: string | null;
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
token: string;
|
||||
refreshToken: string | null;
|
||||
};
|
||||
|
||||
export type State = {
|
||||
active: number | null;
|
||||
available: Array<Account>;
|
||||
active: number | null;
|
||||
available: Array<Account>;
|
||||
};
|
||||
|
||||
export function getActiveAccount(state: { accounts: State }): Account | null {
|
||||
const accountId = state.accounts.active;
|
||||
const accountId = state.accounts.active;
|
||||
|
||||
return (
|
||||
state.accounts.available.find((account) => account.id === accountId) || null
|
||||
);
|
||||
return state.accounts.available.find((account) => account.id === accountId) || null;
|
||||
}
|
||||
|
||||
export function getAvailableAccounts(state: {
|
||||
accounts: State;
|
||||
}): Array<Account> {
|
||||
return state.accounts.available;
|
||||
export function getAvailableAccounts(state: { accounts: State }): Array<Account> {
|
||||
return state.accounts.available;
|
||||
}
|
||||
|
||||
export default function accounts(
|
||||
state: State = {
|
||||
active: null,
|
||||
available: [],
|
||||
},
|
||||
action: Action,
|
||||
): State {
|
||||
switch (action.type) {
|
||||
case 'accounts:add': {
|
||||
if (!action.payload || !action.payload.id || !action.payload.token) {
|
||||
throw new Error('Invalid or empty payload passed for accounts.add');
|
||||
}
|
||||
|
||||
const { payload } = action;
|
||||
|
||||
state.available = state.available
|
||||
.filter((account) => account.id !== payload.id)
|
||||
.concat(payload);
|
||||
|
||||
state.available.sort((account1, account2) => {
|
||||
if (account1.username === account2.username) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return account1.username > account2.username ? 1 : -1;
|
||||
});
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
case 'accounts:activate': {
|
||||
if (!action.payload || !action.payload.id || !action.payload.token) {
|
||||
throw new Error('Invalid or empty payload passed for accounts.add');
|
||||
}
|
||||
|
||||
const { payload } = action;
|
||||
|
||||
return {
|
||||
available: state.available.map((account) => {
|
||||
if (account.id === payload.id) {
|
||||
return { ...payload };
|
||||
}
|
||||
|
||||
return { ...account };
|
||||
}),
|
||||
active: payload.id,
|
||||
};
|
||||
}
|
||||
|
||||
case 'accounts:reset':
|
||||
return {
|
||||
state: State = {
|
||||
active: null,
|
||||
available: [],
|
||||
};
|
||||
},
|
||||
action: Action,
|
||||
): State {
|
||||
switch (action.type) {
|
||||
case 'accounts:add': {
|
||||
if (!action.payload || !action.payload.id || !action.payload.token) {
|
||||
throw new Error('Invalid or empty payload passed for accounts.add');
|
||||
}
|
||||
|
||||
case 'accounts:remove': {
|
||||
if (!action.payload || !action.payload.id) {
|
||||
throw new Error('Invalid or empty payload passed for accounts.remove');
|
||||
}
|
||||
const { payload } = action;
|
||||
|
||||
const { payload } = action;
|
||||
state.available = state.available.filter((account) => account.id !== payload.id).concat(payload);
|
||||
|
||||
return {
|
||||
...state,
|
||||
available: state.available.filter(
|
||||
(account) => account.id !== payload.id,
|
||||
),
|
||||
};
|
||||
}
|
||||
state.available.sort((account1, account2) => {
|
||||
if (account1.username === account2.username) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
case 'accounts:updateToken': {
|
||||
if (typeof action.payload !== 'string') {
|
||||
throw new Error('payload must be a jwt token');
|
||||
}
|
||||
return account1.username > account2.username ? 1 : -1;
|
||||
});
|
||||
|
||||
const { payload } = action;
|
||||
return state;
|
||||
}
|
||||
|
||||
case 'accounts:activate': {
|
||||
if (!action.payload || !action.payload.id || !action.payload.token) {
|
||||
throw new Error('Invalid or empty payload passed for accounts.add');
|
||||
}
|
||||
|
||||
const { payload } = action;
|
||||
|
||||
return {
|
||||
...state,
|
||||
available: state.available.map((account) => {
|
||||
if (account.id === state.active) {
|
||||
return {
|
||||
...account,
|
||||
token: payload,
|
||||
available: state.available.map((account) => {
|
||||
if (account.id === payload.id) {
|
||||
return { ...payload };
|
||||
}
|
||||
|
||||
return { ...account };
|
||||
}),
|
||||
active: payload.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { ...account };
|
||||
}),
|
||||
};
|
||||
case 'accounts:reset':
|
||||
return {
|
||||
active: null,
|
||||
available: [],
|
||||
};
|
||||
|
||||
case 'accounts:remove': {
|
||||
if (!action.payload || !action.payload.id) {
|
||||
throw new Error('Invalid or empty payload passed for accounts.remove');
|
||||
}
|
||||
|
||||
const { payload } = action;
|
||||
|
||||
return {
|
||||
...state,
|
||||
available: state.available.filter((account) => account.id !== payload.id),
|
||||
};
|
||||
}
|
||||
|
||||
case 'accounts:updateToken': {
|
||||
if (typeof action.payload !== 'string') {
|
||||
throw new Error('payload must be a jwt token');
|
||||
}
|
||||
|
||||
const { payload } = action;
|
||||
|
||||
return {
|
||||
...state,
|
||||
available: state.available.map((account) => {
|
||||
if (account.id === state.active) {
|
||||
return {
|
||||
...account,
|
||||
token: payload,
|
||||
};
|
||||
}
|
||||
|
||||
return { ...account };
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
return state;
|
||||
}
|
||||
|
@ -3,14 +3,14 @@ import { Helmet } from 'react-helmet-async';
|
||||
import { FormattedMessage as Message, MessageDescriptor } from 'react-intl';
|
||||
|
||||
export default function AuthTitle({ title }: { title: MessageDescriptor }) {
|
||||
return (
|
||||
<Message {...title}>
|
||||
{(msg) => (
|
||||
<span>
|
||||
{msg}
|
||||
<Helmet title={msg as string} />
|
||||
</span>
|
||||
)}
|
||||
</Message>
|
||||
);
|
||||
return (
|
||||
<Message {...title}>
|
||||
{(msg) => (
|
||||
<span>
|
||||
{msg}
|
||||
<Helmet title={msg as string} />
|
||||
</span>
|
||||
)}
|
||||
</Message>
|
||||
);
|
||||
}
|
||||
|
@ -11,63 +11,63 @@ import Context, { AuthContext } from './Context';
|
||||
*/
|
||||
|
||||
class BaseAuthBody extends React.Component<
|
||||
// TODO: this may be converted to generic type RouteComponentProps<T>
|
||||
RouteComponentProps<Record<string, any>>
|
||||
// TODO: this may be converted to generic type RouteComponentProps<T>
|
||||
RouteComponentProps<Record<string, any>>
|
||||
> {
|
||||
static contextType = Context;
|
||||
/* TODO: use declare */ context: React.ContextType<typeof Context>;
|
||||
prevErrors: AuthContext['auth']['error'];
|
||||
static contextType = Context;
|
||||
/* TODO: use declare */ context: React.ContextType<typeof Context>;
|
||||
prevErrors: AuthContext['auth']['error'];
|
||||
|
||||
autoFocusField: string | null = '';
|
||||
autoFocusField: string | null = '';
|
||||
|
||||
componentDidMount() {
|
||||
this.prevErrors = this.context.auth.error;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.context.auth.error !== this.prevErrors) {
|
||||
this.form.setErrors(this.context.auth.error || {});
|
||||
this.context.requestRedraw();
|
||||
componentDidMount() {
|
||||
this.prevErrors = this.context.auth.error;
|
||||
}
|
||||
|
||||
this.prevErrors = this.context.auth.error;
|
||||
}
|
||||
componentDidUpdate() {
|
||||
if (this.context.auth.error !== this.prevErrors) {
|
||||
this.form.setErrors(this.context.auth.error || {});
|
||||
this.context.requestRedraw();
|
||||
}
|
||||
|
||||
renderErrors(): ReactNode {
|
||||
const error = this.form.getFirstError();
|
||||
|
||||
if (error === null) {
|
||||
return null;
|
||||
this.prevErrors = this.context.auth.error;
|
||||
}
|
||||
|
||||
return <AuthError error={error} onClose={this.onClearErrors} />;
|
||||
}
|
||||
renderErrors(): ReactNode {
|
||||
const error = this.form.getFirstError();
|
||||
|
||||
onFormSubmit() {
|
||||
this.context.resolve(this.serialize());
|
||||
}
|
||||
if (error === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
onClearErrors = () => this.context.clearErrors();
|
||||
|
||||
form = new FormModel({
|
||||
renderErrors: false,
|
||||
});
|
||||
|
||||
bindField(name: string) {
|
||||
return this.form.bindField(name);
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return this.form.serialize();
|
||||
}
|
||||
|
||||
autoFocus() {
|
||||
const fieldId = this.autoFocusField;
|
||||
|
||||
if (fieldId && this.form.hasField(fieldId)) {
|
||||
this.form.focus(fieldId);
|
||||
return <AuthError error={error} onClose={this.onClearErrors} />;
|
||||
}
|
||||
|
||||
onFormSubmit() {
|
||||
this.context.resolve(this.serialize());
|
||||
}
|
||||
|
||||
onClearErrors = () => this.context.clearErrors();
|
||||
|
||||
form = new FormModel({
|
||||
renderErrors: false,
|
||||
});
|
||||
|
||||
bindField(name: string) {
|
||||
return this.form.bindField(name);
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return this.form.serialize();
|
||||
}
|
||||
|
||||
autoFocus() {
|
||||
const fieldId = this.autoFocusField;
|
||||
|
||||
if (fieldId && this.form.hasField(fieldId)) {
|
||||
this.form.focus(fieldId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseAuthBody;
|
||||
|
@ -4,28 +4,28 @@ import { User } from 'app/components/user';
|
||||
import { State as AuthState } from './reducer';
|
||||
|
||||
export interface AuthContext {
|
||||
auth: AuthState;
|
||||
user: User;
|
||||
requestRedraw: () => Promise<void>;
|
||||
clearErrors: () => void;
|
||||
resolve: (payload: { [key: string]: any } | undefined) => void;
|
||||
reject: (payload: { [key: string]: any } | undefined) => void;
|
||||
auth: AuthState;
|
||||
user: User;
|
||||
requestRedraw: () => Promise<void>;
|
||||
clearErrors: () => void;
|
||||
resolve: (payload: { [key: string]: any } | undefined) => void;
|
||||
reject: (payload: { [key: string]: any } | undefined) => void;
|
||||
}
|
||||
|
||||
const Context = React.createContext<AuthContext>({
|
||||
auth: {
|
||||
error: null,
|
||||
login: '',
|
||||
scopes: [],
|
||||
} as any,
|
||||
user: {
|
||||
id: null,
|
||||
isGuest: true,
|
||||
} as any,
|
||||
async requestRedraw() {},
|
||||
clearErrors() {},
|
||||
resolve() {},
|
||||
reject() {},
|
||||
auth: {
|
||||
error: null,
|
||||
login: '',
|
||||
scopes: [],
|
||||
} as any,
|
||||
user: {
|
||||
id: null,
|
||||
isGuest: true,
|
||||
} as any,
|
||||
async requestRedraw() {},
|
||||
clearErrors() {},
|
||||
resolve() {},
|
||||
reject() {},
|
||||
});
|
||||
Context.displayName = 'AuthContext';
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -2,16 +2,13 @@
|
||||
|
||||
To add new panel you need to:
|
||||
|
||||
- create panel component at `components/auth/[panelId]`
|
||||
- add new context in `components/auth/PanelTransition`
|
||||
- connect component to router in `pages/auth/AuthPage`
|
||||
- add new state to `services/authFlow` and coresponding test to
|
||||
`tests/services/authFlow`
|
||||
- connect state to `authFlow`. Update `services/authFlow/AuthFlow.test` and
|
||||
`services/authFlow/AuthFlow.functional.test` (the last one for some complex
|
||||
flow)
|
||||
- add new actions to `components/auth/actions` and api endpoints to
|
||||
`services/api`
|
||||
- whatever else you need
|
||||
- create panel component at `components/auth/[panelId]`
|
||||
- add new context in `components/auth/PanelTransition`
|
||||
- connect component to router in `pages/auth/AuthPage`
|
||||
- add new state to `services/authFlow` and coresponding test to `tests/services/authFlow`
|
||||
- connect state to `authFlow`. Update `services/authFlow/AuthFlow.test` and
|
||||
`services/authFlow/AuthFlow.functional.test` (the last one for some complex flow)
|
||||
- add new actions to `components/auth/actions` and api endpoints to `services/api`
|
||||
- whatever else you need
|
||||
|
||||
Commit id with example implementation: f4d315c
|
||||
|
@ -4,36 +4,32 @@ import { FormattedMessage as Message, MessageDescriptor } from 'react-intl';
|
||||
import Context, { AuthContext } from './Context';
|
||||
|
||||
interface Props {
|
||||
isAvailable?: (context: AuthContext) => boolean;
|
||||
payload?: Record<string, any>;
|
||||
label: MessageDescriptor;
|
||||
isAvailable?: (context: AuthContext) => boolean;
|
||||
payload?: Record<string, any>;
|
||||
label: MessageDescriptor;
|
||||
}
|
||||
|
||||
const RejectionLink: ComponentType<Props> = ({
|
||||
isAvailable,
|
||||
payload,
|
||||
label,
|
||||
}) => {
|
||||
const context = useContext(Context);
|
||||
const RejectionLink: ComponentType<Props> = ({ isAvailable, payload, label }) => {
|
||||
const context = useContext(Context);
|
||||
|
||||
if (isAvailable && !isAvailable(context)) {
|
||||
// TODO: if want to properly support multiple links, we should control
|
||||
// the dividers ' | ' rendered from factory too
|
||||
return null;
|
||||
}
|
||||
if (isAvailable && !isAvailable(context)) {
|
||||
// TODO: if want to properly support multiple links, we should control
|
||||
// the dividers ' | ' rendered from factory too
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
context.reject(payload);
|
||||
}}
|
||||
>
|
||||
<Message {...label} />
|
||||
</a>
|
||||
);
|
||||
context.reject(payload);
|
||||
}}
|
||||
>
|
||||
<Message {...label} />
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default RejectionLink;
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"title": "User Agreement",
|
||||
"accept": "Accept",
|
||||
"declineAndLogout": "Decline and logout",
|
||||
"description1": "We have updated our {link}.",
|
||||
"termsOfService": "terms of service",
|
||||
"description2": "In order to continue using {name} service, you need to accept them."
|
||||
"title": "User Agreement",
|
||||
"accept": "Accept",
|
||||
"declineAndLogout": "Decline and logout",
|
||||
"description1": "We have updated our {link}.",
|
||||
"termsOfService": "terms of service",
|
||||
"description2": "In order to continue using {name} service, you need to accept them."
|
||||
}
|
||||
|
@ -3,14 +3,14 @@ import Body from './AcceptRulesBody';
|
||||
import messages from './AcceptRules.intl.json';
|
||||
|
||||
export default factory({
|
||||
title: messages.title,
|
||||
body: Body,
|
||||
footer: {
|
||||
color: 'darkBlue',
|
||||
autoFocus: true,
|
||||
label: messages.accept,
|
||||
},
|
||||
links: {
|
||||
label: messages.declineAndLogout,
|
||||
},
|
||||
title: messages.title,
|
||||
body: Body,
|
||||
footer: {
|
||||
color: 'darkBlue',
|
||||
autoFocus: true,
|
||||
label: messages.accept,
|
||||
},
|
||||
links: {
|
||||
label: messages.declineAndLogout,
|
||||
},
|
||||
});
|
||||
|
@ -11,38 +11,38 @@ import styles from './acceptRules.scss';
|
||||
import messages from './AcceptRules.intl.json';
|
||||
|
||||
export default class AcceptRulesBody extends BaseAuthBody {
|
||||
static displayName = 'AcceptRulesBody';
|
||||
static panelId = 'acceptRules';
|
||||
static displayName = 'AcceptRulesBody';
|
||||
static panelId = 'acceptRules';
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.renderErrors()}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.renderErrors()}
|
||||
|
||||
<div className={styles.security}>
|
||||
<span className={icons.lock} />
|
||||
</div>
|
||||
<div className={styles.security}>
|
||||
<span className={icons.lock} />
|
||||
</div>
|
||||
|
||||
<p className={styles.descriptionText}>
|
||||
<Message
|
||||
{...messages.description1}
|
||||
values={{
|
||||
link: (
|
||||
<Link to="/rules" target="_blank">
|
||||
<Message {...messages.termsOfService} />
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<br />
|
||||
<Message
|
||||
{...messages.description2}
|
||||
values={{
|
||||
name: <Message {...appInfo.appName} />,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<p className={styles.descriptionText}>
|
||||
<Message
|
||||
{...messages.description1}
|
||||
values={{
|
||||
link: (
|
||||
<Link to="/rules" target="_blank">
|
||||
<Message {...messages.termsOfService} />
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<br />
|
||||
<Message
|
||||
{...messages.description2}
|
||||
values={{
|
||||
name: <Message {...appInfo.appName} />,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
@import '~app/components/ui/colors.scss';
|
||||
|
||||
.descriptionText {
|
||||
font-size: 15px;
|
||||
line-height: 1.4;
|
||||
padding-bottom: 8px;
|
||||
color: #aaa;
|
||||
font-size: 15px;
|
||||
line-height: 1.4;
|
||||
padding-bottom: 8px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
// TODO: вынести иконки такого типа в какую-то внешнюю структуру?
|
||||
.security {
|
||||
color: #fff;
|
||||
font-size: 90px;
|
||||
line-height: 1;
|
||||
margin-bottom: 15px;
|
||||
color: #fff;
|
||||
font-size: 90px;
|
||||
line-height: 1;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
@ -5,200 +5,190 @@ import expect from 'app/test/unexpected';
|
||||
import request from 'app/services/request';
|
||||
|
||||
import {
|
||||
setLoadingState,
|
||||
oAuthValidate,
|
||||
oAuthComplete,
|
||||
setClient,
|
||||
setOAuthRequest,
|
||||
setScopes,
|
||||
setOAuthCode,
|
||||
requirePermissionsAccept,
|
||||
login,
|
||||
setLogin,
|
||||
setLoadingState,
|
||||
oAuthValidate,
|
||||
oAuthComplete,
|
||||
setClient,
|
||||
setOAuthRequest,
|
||||
setScopes,
|
||||
setOAuthCode,
|
||||
requirePermissionsAccept,
|
||||
login,
|
||||
setLogin,
|
||||
} from 'app/components/auth/actions';
|
||||
import { OauthData, OAuthValidateResponse } from '../../services/api/oauth';
|
||||
|
||||
const oauthData: OauthData = {
|
||||
clientId: '',
|
||||
redirectUrl: '',
|
||||
responseType: '',
|
||||
scope: '',
|
||||
state: '',
|
||||
prompt: 'none',
|
||||
clientId: '',
|
||||
redirectUrl: '',
|
||||
responseType: '',
|
||||
scope: '',
|
||||
state: '',
|
||||
prompt: 'none',
|
||||
};
|
||||
|
||||
describe('components/auth/actions', () => {
|
||||
const dispatch = sinon.stub().named('store.dispatch');
|
||||
const getState = sinon.stub().named('store.getState');
|
||||
const dispatch = sinon.stub().named('store.dispatch');
|
||||
const getState = sinon.stub().named('store.getState');
|
||||
|
||||
function callThunk<A extends Array<any>, F extends (...args: A) => any>(
|
||||
fn: F,
|
||||
...args: A
|
||||
): Promise<void> {
|
||||
const thunk = fn(...args);
|
||||
function callThunk<A extends Array<any>, F extends (...args: A) => any>(fn: F, ...args: A): Promise<void> {
|
||||
const thunk = fn(...args);
|
||||
|
||||
return thunk(dispatch, getState);
|
||||
}
|
||||
return thunk(dispatch, getState);
|
||||
}
|
||||
|
||||
function expectDispatchCalls(calls: Array<Array<ReduxAction>>) {
|
||||
expect(dispatch, 'to have calls satisfying', [
|
||||
[setLoadingState(true)],
|
||||
...calls,
|
||||
[setLoadingState(false)],
|
||||
]);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
dispatch.reset();
|
||||
getState.reset();
|
||||
getState.returns({});
|
||||
sinon.stub(request, 'get').named('request.get');
|
||||
sinon.stub(request, 'post').named('request.post');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(request.get as any).restore();
|
||||
(request.post as any).restore();
|
||||
});
|
||||
|
||||
describe('#oAuthValidate()', () => {
|
||||
let resp: OAuthValidateResponse;
|
||||
function expectDispatchCalls(calls: Array<Array<ReduxAction>>) {
|
||||
expect(dispatch, 'to have calls satisfying', [[setLoadingState(true)], ...calls, [setLoadingState(false)]]);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
resp = {
|
||||
client: {
|
||||
id: '123',
|
||||
name: '',
|
||||
description: '',
|
||||
},
|
||||
oAuth: {
|
||||
state: 123,
|
||||
},
|
||||
session: {
|
||||
scopes: ['account_info'],
|
||||
},
|
||||
};
|
||||
|
||||
(request.get as any).returns(Promise.resolve(resp));
|
||||
dispatch.reset();
|
||||
getState.reset();
|
||||
getState.returns({});
|
||||
sinon.stub(request, 'get').named('request.get');
|
||||
sinon.stub(request, 'post').named('request.post');
|
||||
});
|
||||
|
||||
it('should send get request to an api', () =>
|
||||
callThunk(oAuthValidate, oauthData).then(() => {
|
||||
expect(request.get, 'to have a call satisfying', [
|
||||
'/api/oauth2/v1/validate',
|
||||
{},
|
||||
]);
|
||||
}));
|
||||
|
||||
it('should dispatch setClient, setOAuthRequest and setScopes', () =>
|
||||
callThunk(oAuthValidate, oauthData).then(() => {
|
||||
expectDispatchCalls([
|
||||
[setClient(resp.client)],
|
||||
[
|
||||
setOAuthRequest({
|
||||
...resp.oAuth,
|
||||
prompt: 'none',
|
||||
loginHint: undefined,
|
||||
}),
|
||||
],
|
||||
[setScopes(resp.session.scopes)],
|
||||
]);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('#oAuthComplete()', () => {
|
||||
beforeEach(() => {
|
||||
getState.returns({
|
||||
auth: {
|
||||
oauth: oauthData,
|
||||
},
|
||||
});
|
||||
afterEach(() => {
|
||||
(request.get as any).restore();
|
||||
(request.post as any).restore();
|
||||
});
|
||||
|
||||
it('should post to api/oauth2/complete', () => {
|
||||
(request.post as any).returns(
|
||||
Promise.resolve({
|
||||
redirectUri: '',
|
||||
}),
|
||||
);
|
||||
describe('#oAuthValidate()', () => {
|
||||
let resp: OAuthValidateResponse;
|
||||
|
||||
return callThunk(oAuthComplete).then(() => {
|
||||
expect(request.post, 'to have a call satisfying', [
|
||||
'/api/oauth2/v1/complete?client_id=&redirect_uri=&response_type=&description=&scope=&prompt=none&login_hint=&state=',
|
||||
{},
|
||||
]);
|
||||
});
|
||||
beforeEach(() => {
|
||||
resp = {
|
||||
client: {
|
||||
id: '123',
|
||||
name: '',
|
||||
description: '',
|
||||
},
|
||||
oAuth: {
|
||||
state: 123,
|
||||
},
|
||||
session: {
|
||||
scopes: ['account_info'],
|
||||
},
|
||||
};
|
||||
|
||||
(request.get as any).returns(Promise.resolve(resp));
|
||||
});
|
||||
|
||||
it('should send get request to an api', () =>
|
||||
callThunk(oAuthValidate, oauthData).then(() => {
|
||||
expect(request.get, 'to have a call satisfying', ['/api/oauth2/v1/validate', {}]);
|
||||
}));
|
||||
|
||||
it('should dispatch setClient, setOAuthRequest and setScopes', () =>
|
||||
callThunk(oAuthValidate, oauthData).then(() => {
|
||||
expectDispatchCalls([
|
||||
[setClient(resp.client)],
|
||||
[
|
||||
setOAuthRequest({
|
||||
...resp.oAuth,
|
||||
prompt: 'none',
|
||||
loginHint: undefined,
|
||||
}),
|
||||
],
|
||||
[setScopes(resp.session.scopes)],
|
||||
]);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should dispatch setOAuthCode for static_page redirect', () => {
|
||||
const resp = {
|
||||
success: true,
|
||||
redirectUri: 'static_page?code=123&state=',
|
||||
};
|
||||
describe('#oAuthComplete()', () => {
|
||||
beforeEach(() => {
|
||||
getState.returns({
|
||||
auth: {
|
||||
oauth: oauthData,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
(request.post as any).returns(Promise.resolve(resp));
|
||||
it('should post to api/oauth2/complete', () => {
|
||||
(request.post as any).returns(
|
||||
Promise.resolve({
|
||||
redirectUri: '',
|
||||
}),
|
||||
);
|
||||
|
||||
return callThunk(oAuthComplete).then(() => {
|
||||
expectDispatchCalls([
|
||||
[
|
||||
setOAuthCode({
|
||||
success: true,
|
||||
code: '123',
|
||||
displayCode: false,
|
||||
}),
|
||||
],
|
||||
]);
|
||||
});
|
||||
return callThunk(oAuthComplete).then(() => {
|
||||
expect(request.post, 'to have a call satisfying', [
|
||||
'/api/oauth2/v1/complete?client_id=&redirect_uri=&response_type=&description=&scope=&prompt=none&login_hint=&state=',
|
||||
{},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch setOAuthCode for static_page redirect', () => {
|
||||
const resp = {
|
||||
success: true,
|
||||
redirectUri: 'static_page?code=123&state=',
|
||||
};
|
||||
|
||||
(request.post as any).returns(Promise.resolve(resp));
|
||||
|
||||
return callThunk(oAuthComplete).then(() => {
|
||||
expectDispatchCalls([
|
||||
[
|
||||
setOAuthCode({
|
||||
success: true,
|
||||
code: '123',
|
||||
displayCode: false,
|
||||
}),
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve to with success false and redirectUri for access_denied', async () => {
|
||||
const resp = {
|
||||
statusCode: 401,
|
||||
error: 'access_denied',
|
||||
redirectUri: 'redirectUri',
|
||||
};
|
||||
|
||||
(request.post as any).returns(Promise.reject(resp));
|
||||
|
||||
const data = await callThunk(oAuthComplete);
|
||||
|
||||
expect(data, 'to equal', {
|
||||
success: false,
|
||||
redirectUri: 'redirectUri',
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch requirePermissionsAccept if accept_required', () => {
|
||||
const resp = {
|
||||
statusCode: 401,
|
||||
error: 'accept_required',
|
||||
};
|
||||
|
||||
(request.post as any).returns(Promise.reject(resp));
|
||||
|
||||
return callThunk(oAuthComplete).catch((error) => {
|
||||
expect(error.acceptRequired, 'to be true');
|
||||
expectDispatchCalls([[requirePermissionsAccept()]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve to with success false and redirectUri for access_denied', async () => {
|
||||
const resp = {
|
||||
statusCode: 401,
|
||||
error: 'access_denied',
|
||||
redirectUri: 'redirectUri',
|
||||
};
|
||||
describe('#login()', () => {
|
||||
describe('when correct login was entered', () => {
|
||||
beforeEach(() => {
|
||||
(request.post as any).returns(
|
||||
Promise.reject({
|
||||
errors: {
|
||||
password: 'error.password_required',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
(request.post as any).returns(Promise.reject(resp));
|
||||
|
||||
const data = await callThunk(oAuthComplete);
|
||||
|
||||
expect(data, 'to equal', {
|
||||
success: false,
|
||||
redirectUri: 'redirectUri',
|
||||
});
|
||||
it('should set login', () =>
|
||||
callThunk(login, { login: 'foo' }).then(() => {
|
||||
expectDispatchCalls([[setLogin('foo')]]);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch requirePermissionsAccept if accept_required', () => {
|
||||
const resp = {
|
||||
statusCode: 401,
|
||||
error: 'accept_required',
|
||||
};
|
||||
|
||||
(request.post as any).returns(Promise.reject(resp));
|
||||
|
||||
return callThunk(oAuthComplete).catch((error) => {
|
||||
expect(error.acceptRequired, 'to be true');
|
||||
expectDispatchCalls([[requirePermissionsAccept()]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#login()', () => {
|
||||
describe('when correct login was entered', () => {
|
||||
beforeEach(() => {
|
||||
(request.post as any).returns(
|
||||
Promise.reject({
|
||||
errors: {
|
||||
password: 'error.password_required',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should set login', () =>
|
||||
callThunk(login, { login: 'foo' }).then(() => {
|
||||
expectDispatchCalls([[setLogin('foo')]]);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,8 @@
|
||||
{
|
||||
"accountActivationTitle": "Account activation",
|
||||
"activationMailWasSent": "Please check {email} for the message with further instructions",
|
||||
"activationMailWasSentNoEmail": "Please check your E‑mail for the message with further instructions",
|
||||
"confirmEmail": "Confirm E‑mail",
|
||||
"didNotReceivedEmail": "Did not received E‑mail?",
|
||||
"enterTheCode": "Enter the code from E‑mail here"
|
||||
"accountActivationTitle": "Account activation",
|
||||
"activationMailWasSent": "Please check {email} for the message with further instructions",
|
||||
"activationMailWasSentNoEmail": "Please check your E‑mail for the message with further instructions",
|
||||
"confirmEmail": "Confirm E‑mail",
|
||||
"didNotReceivedEmail": "Did not received E‑mail?",
|
||||
"enterTheCode": "Enter the code from E‑mail here"
|
||||
}
|
||||
|
@ -3,13 +3,13 @@ import messages from './Activation.intl.json';
|
||||
import Body from './ActivationBody';
|
||||
|
||||
export default factory({
|
||||
title: messages.accountActivationTitle,
|
||||
body: Body,
|
||||
footer: {
|
||||
color: 'blue',
|
||||
label: messages.confirmEmail,
|
||||
},
|
||||
links: {
|
||||
label: messages.didNotReceivedEmail,
|
||||
},
|
||||
title: messages.accountActivationTitle,
|
||||
body: Body,
|
||||
footer: {
|
||||
color: 'blue',
|
||||
label: messages.confirmEmail,
|
||||
},
|
||||
links: {
|
||||
label: messages.didNotReceivedEmail,
|
||||
},
|
||||
});
|
||||
|
@ -9,49 +9,48 @@ import styles from './activation.scss';
|
||||
import messages from './Activation.intl.json';
|
||||
|
||||
export default class ActivationBody extends BaseAuthBody {
|
||||
static displayName = 'ActivationBody';
|
||||
static panelId = 'activation';
|
||||
static displayName = 'ActivationBody';
|
||||
static panelId = 'activation';
|
||||
|
||||
autoFocusField =
|
||||
this.props.match.params && this.props.match.params.key ? null : 'key';
|
||||
autoFocusField = this.props.match.params && this.props.match.params.key ? null : 'key';
|
||||
|
||||
render() {
|
||||
const { key } = this.props.match.params;
|
||||
const { email } = this.context.user;
|
||||
render() {
|
||||
const { key } = this.props.match.params;
|
||||
const { email } = this.context.user;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{this.renderErrors()}
|
||||
return (
|
||||
<div>
|
||||
{this.renderErrors()}
|
||||
|
||||
<div className={styles.description}>
|
||||
<div className={styles.descriptionImage} />
|
||||
<div className={styles.description}>
|
||||
<div className={styles.descriptionImage} />
|
||||
|
||||
<div className={styles.descriptionText}>
|
||||
{email ? (
|
||||
<Message
|
||||
{...messages.activationMailWasSent}
|
||||
values={{
|
||||
email: <b>{email}</b>,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Message {...messages.activationMailWasSentNoEmail} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.formRow}>
|
||||
<Input
|
||||
{...this.bindField('key')}
|
||||
color="blue"
|
||||
center
|
||||
required
|
||||
value={key}
|
||||
readOnly={!!key}
|
||||
autoComplete="off"
|
||||
placeholder={messages.enterTheCode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<div className={styles.descriptionText}>
|
||||
{email ? (
|
||||
<Message
|
||||
{...messages.activationMailWasSent}
|
||||
values={{
|
||||
email: <b>{email}</b>,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Message {...messages.activationMailWasSentNoEmail} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.formRow}>
|
||||
<Input
|
||||
{...this.bindField('key')}
|
||||
color="blue"
|
||||
center
|
||||
required
|
||||
value={key}
|
||||
readOnly={!!key}
|
||||
autoComplete="off"
|
||||
placeholder={messages.enterTheCode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -5,15 +5,15 @@
|
||||
}
|
||||
|
||||
.descriptionImage {
|
||||
composes: envelope from '~app/components/ui/icons.scss';
|
||||
composes: envelope from '~app/components/ui/icons.scss';
|
||||
|
||||
font-size: 100px;
|
||||
color: $blue;
|
||||
font-size: 100px;
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.descriptionText {
|
||||
font-family: $font-family-title;
|
||||
margin: 5px 0 19px;
|
||||
line-height: 1.4;
|
||||
font-size: 16px;
|
||||
font-family: $font-family-title;
|
||||
margin: 5px 0 19px;
|
||||
line-height: 1.4;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"appName": "Ely Accounts",
|
||||
"goToAuth": "Go to auth",
|
||||
"appDescription": "You are on the Ely.by authorization service, that allows you to safely perform any operations on your account. This single entry point for websites and desktop software, including game launchers.",
|
||||
"useItYourself": "Visit our {link}, to learn how to use this service in you projects.",
|
||||
"documentation": "documentation"
|
||||
"appName": "Ely Accounts",
|
||||
"goToAuth": "Go to auth",
|
||||
"appDescription": "You are on the Ely.by authorization service, that allows you to safely perform any operations on your account. This single entry point for websites and desktop software, including game launchers.",
|
||||
"useItYourself": "Visit our {link}, to learn how to use this service in you projects.",
|
||||
"documentation": "documentation"
|
||||
}
|
||||
|
@ -7,51 +7,49 @@ import styles from './appInfo.scss';
|
||||
import messages from './AppInfo.intl.json';
|
||||
|
||||
export default class AppInfo extends React.Component<{
|
||||
name?: string;
|
||||
description?: string;
|
||||
onGoToAuth: () => void;
|
||||
name?: string;
|
||||
description?: string;
|
||||
onGoToAuth: () => void;
|
||||
}> {
|
||||
render() {
|
||||
const { name, description, onGoToAuth } = this.props;
|
||||
render() {
|
||||
const { name, description, onGoToAuth } = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.appInfo}>
|
||||
<div className={styles.logoContainer}>
|
||||
<h2 className={styles.logo}>
|
||||
{name ? name : <Message {...messages.appName} />}
|
||||
</h2>
|
||||
</div>
|
||||
<div className={styles.descriptionContainer}>
|
||||
{description ? (
|
||||
<p className={styles.description}>{description}</p>
|
||||
) : (
|
||||
<div>
|
||||
<p className={styles.description}>
|
||||
<Message {...messages.appDescription} />
|
||||
</p>
|
||||
<p className={styles.description}>
|
||||
<Message
|
||||
{...messages.useItYourself}
|
||||
values={{
|
||||
link: (
|
||||
<a href="http://docs.ely.by/oauth.html">
|
||||
<Message {...messages.documentation} />
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
return (
|
||||
<div className={styles.appInfo}>
|
||||
<div className={styles.logoContainer}>
|
||||
<h2 className={styles.logo}>{name ? name : <Message {...messages.appName} />}</h2>
|
||||
</div>
|
||||
<div className={styles.descriptionContainer}>
|
||||
{description ? (
|
||||
<p className={styles.description}>{description}</p>
|
||||
) : (
|
||||
<div>
|
||||
<p className={styles.description}>
|
||||
<Message {...messages.appDescription} />
|
||||
</p>
|
||||
<p className={styles.description}>
|
||||
<Message
|
||||
{...messages.useItYourself}
|
||||
values={{
|
||||
link: (
|
||||
<a href="http://docs.ely.by/oauth.html">
|
||||
<Message {...messages.documentation} />
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.goToAuth}>
|
||||
<Button onClick={onGoToAuth} label={messages.goToAuth} />
|
||||
</div>
|
||||
|
||||
<div className={styles.footer}>
|
||||
<FooterMenu />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.goToAuth}>
|
||||
<Button onClick={onGoToAuth} label={messages.goToAuth} />
|
||||
</div>
|
||||
|
||||
<div className={styles.footer}>
|
||||
<FooterMenu />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,71 +2,71 @@
|
||||
@import '~app/components/ui/fonts.scss';
|
||||
|
||||
.appInfo {
|
||||
max-width: 270px;
|
||||
margin: 0 auto;
|
||||
padding: 55px 25px;
|
||||
max-width: 270px;
|
||||
margin: 0 auto;
|
||||
padding: 55px 25px;
|
||||
}
|
||||
|
||||
.logoContainer {
|
||||
position: relative;
|
||||
padding: 15px 0;
|
||||
position: relative;
|
||||
padding: 15px 0;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
display: block;
|
||||
&:after {
|
||||
content: '';
|
||||
display: block;
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
height: 3px;
|
||||
width: 40px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
height: 3px;
|
||||
width: 40px;
|
||||
|
||||
background: $green;
|
||||
}
|
||||
background: $green;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-family: $font-family-title;
|
||||
color: #fff;
|
||||
font-size: 36px;
|
||||
font-family: $font-family-title;
|
||||
color: #fff;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.descriptionContainer {
|
||||
margin: 20px 0;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
$font-color: #ccc;
|
||||
font-family: $font-family-base;
|
||||
color: $font-color;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
margin-top: 7px;
|
||||
$font-color: #ccc;
|
||||
font-family: $font-family-base;
|
||||
color: $font-color;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
margin-top: 7px;
|
||||
|
||||
a {
|
||||
color: lighten($font-color, 10%);
|
||||
border-bottom-color: #666;
|
||||
a {
|
||||
color: lighten($font-color, 10%);
|
||||
border-bottom-color: #666;
|
||||
|
||||
&:hover {
|
||||
color: $font-color;
|
||||
&:hover {
|
||||
color: $font-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goToAuth {
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.goToAuth {
|
||||
display: none;
|
||||
}
|
||||
.goToAuth {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
.checkboxInput {
|
||||
margin-top: 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
@ -5,41 +5,36 @@ import { PanelBodyHeader } from 'app/components/ui/Panel';
|
||||
import { ValidationError } from 'app/components/ui/form/FormModel';
|
||||
|
||||
interface Props {
|
||||
error: ValidationError;
|
||||
onClose?: () => void;
|
||||
error: ValidationError;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
let autoHideTimer: number | null = null;
|
||||
function resetTimeout(): void {
|
||||
if (autoHideTimer) {
|
||||
clearTimeout(autoHideTimer);
|
||||
autoHideTimer = null;
|
||||
}
|
||||
if (autoHideTimer) {
|
||||
clearTimeout(autoHideTimer);
|
||||
autoHideTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
const AuthError: ComponentType<Props> = ({ error, onClose }) => {
|
||||
useEffect(() => {
|
||||
resetTimeout();
|
||||
useEffect(() => {
|
||||
resetTimeout();
|
||||
|
||||
if (
|
||||
onClose &&
|
||||
typeof error !== 'string' &&
|
||||
error.payload &&
|
||||
error.payload.canRepeatIn
|
||||
) {
|
||||
const msLeft = error.payload.canRepeatIn * 1000;
|
||||
// 1500 to let the user see, that time is elapsed
|
||||
setTimeout(onClose, msLeft - Date.now() + 1500);
|
||||
}
|
||||
if (onClose && typeof error !== 'string' && error.payload && error.payload.canRepeatIn) {
|
||||
const msLeft = error.payload.canRepeatIn * 1000;
|
||||
// 1500 to let the user see, that time is elapsed
|
||||
setTimeout(onClose, msLeft - Date.now() + 1500);
|
||||
}
|
||||
|
||||
return resetTimeout;
|
||||
}, [error, onClose]);
|
||||
return resetTimeout;
|
||||
}, [error, onClose]);
|
||||
|
||||
return (
|
||||
<PanelBodyHeader type="error" onClose={onClose}>
|
||||
{resolveError(error)}
|
||||
</PanelBodyHeader>
|
||||
);
|
||||
return (
|
||||
<PanelBodyHeader type="error" onClose={onClose}>
|
||||
{resolveError(error)}
|
||||
</PanelBodyHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthError;
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"chooseAccountTitle": "Choose an account",
|
||||
"addAccount": "Log into another account",
|
||||
"logoutAll": "Log out from all accounts",
|
||||
"pleaseChooseAccount": "Please select an account you're willing to use",
|
||||
"pleaseChooseAccountForApp": "Please select an account that you want to use to authorize {appName}"
|
||||
"chooseAccountTitle": "Choose an account",
|
||||
"addAccount": "Log into another account",
|
||||
"logoutAll": "Log out from all accounts",
|
||||
"pleaseChooseAccount": "Please select an account you're willing to use",
|
||||
"pleaseChooseAccountForApp": "Please select an account that you want to use to authorize {appName}"
|
||||
}
|
||||
|
@ -3,14 +3,14 @@ import messages from './ChooseAccount.intl.json';
|
||||
import Body from './ChooseAccountBody';
|
||||
|
||||
export default factory({
|
||||
title: messages.chooseAccountTitle,
|
||||
body: Body,
|
||||
footer: {
|
||||
label: messages.addAccount,
|
||||
},
|
||||
links: [
|
||||
{
|
||||
label: messages.logoutAll,
|
||||
title: messages.chooseAccountTitle,
|
||||
body: Body,
|
||||
footer: {
|
||||
label: messages.addAccount,
|
||||
},
|
||||
],
|
||||
links: [
|
||||
{
|
||||
label: messages.logoutAll,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -10,44 +10,44 @@ import styles from './chooseAccount.scss';
|
||||
import messages from './ChooseAccount.intl.json';
|
||||
|
||||
export default class ChooseAccountBody extends BaseAuthBody {
|
||||
static displayName = 'ChooseAccountBody';
|
||||
static panelId = 'chooseAccount';
|
||||
static displayName = 'ChooseAccountBody';
|
||||
static panelId = 'chooseAccount';
|
||||
|
||||
render() {
|
||||
const { client } = this.context.auth;
|
||||
render() {
|
||||
const { client } = this.context.auth;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{this.renderErrors()}
|
||||
return (
|
||||
<div>
|
||||
{this.renderErrors()}
|
||||
|
||||
<div className={styles.description}>
|
||||
{client ? (
|
||||
<Message
|
||||
{...messages.pleaseChooseAccountForApp}
|
||||
values={{
|
||||
appName: <span className={styles.appName}>{client.name}</span>,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.description}>
|
||||
<Message {...messages.pleaseChooseAccount} />
|
||||
<div className={styles.description}>
|
||||
{client ? (
|
||||
<Message
|
||||
{...messages.pleaseChooseAccountForApp}
|
||||
values={{
|
||||
appName: <span className={styles.appName}>{client.name}</span>,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.description}>
|
||||
<Message {...messages.pleaseChooseAccount} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.accountSwitcherContainer}>
|
||||
<AccountSwitcher
|
||||
allowAdd={false}
|
||||
allowLogout={false}
|
||||
highlightActiveAccount={false}
|
||||
onSwitch={this.onSwitch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<div className={styles.accountSwitcherContainer}>
|
||||
<AccountSwitcher
|
||||
allowAdd={false}
|
||||
allowLogout={false}
|
||||
highlightActiveAccount={false}
|
||||
onSwitch={this.onSwitch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onSwitch = (account: Account): void => {
|
||||
this.context.resolve(account);
|
||||
};
|
||||
onSwitch = (account: Account): void => {
|
||||
this.context.resolve(account);
|
||||
};
|
||||
}
|
||||
|
@ -2,17 +2,17 @@
|
||||
@import '~app/components/ui/fonts.scss';
|
||||
|
||||
.accountSwitcherContainer {
|
||||
margin-left: -$bodyLeftRightPadding;
|
||||
margin-right: -$bodyLeftRightPadding;
|
||||
margin-left: -$bodyLeftRightPadding;
|
||||
margin-right: -$bodyLeftRightPadding;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-family: $font-family-title;
|
||||
margin: 5px 0 19px;
|
||||
line-height: 1.4;
|
||||
font-size: 16px;
|
||||
font-family: $font-family-title;
|
||||
margin: 5px 0 19px;
|
||||
line-height: 1.4;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.appName {
|
||||
color: #fff;
|
||||
color: #fff;
|
||||
}
|
||||
|
@ -7,44 +7,36 @@ import { Color } from 'app/components/ui';
|
||||
import BaseAuthBody from './BaseAuthBody';
|
||||
|
||||
export type Factory = () => {
|
||||
Title: ComponentType;
|
||||
Body: typeof BaseAuthBody;
|
||||
Footer: ComponentType;
|
||||
Links: ComponentType;
|
||||
Title: ComponentType;
|
||||
Body: typeof BaseAuthBody;
|
||||
Footer: ComponentType;
|
||||
Links: ComponentType;
|
||||
};
|
||||
|
||||
type RejectionLinkProps = ComponentProps<typeof RejectionLink>;
|
||||
interface FactoryParams {
|
||||
title: MessageDescriptor;
|
||||
body: typeof BaseAuthBody;
|
||||
footer: {
|
||||
color?: Color;
|
||||
label: string | MessageDescriptor;
|
||||
autoFocus?: boolean;
|
||||
};
|
||||
links?: RejectionLinkProps | Array<RejectionLinkProps>;
|
||||
title: MessageDescriptor;
|
||||
body: typeof BaseAuthBody;
|
||||
footer: {
|
||||
color?: Color;
|
||||
label: string | MessageDescriptor;
|
||||
autoFocus?: boolean;
|
||||
};
|
||||
links?: RejectionLinkProps | Array<RejectionLinkProps>;
|
||||
}
|
||||
|
||||
export default function ({
|
||||
title,
|
||||
body,
|
||||
footer,
|
||||
links,
|
||||
}: FactoryParams): Factory {
|
||||
return () => ({
|
||||
Title: () => <AuthTitle title={title} />,
|
||||
Body: body,
|
||||
Footer: () => <Button type="submit" {...footer} />,
|
||||
Links: () =>
|
||||
links ? (
|
||||
<span>
|
||||
{([] as Array<RejectionLinkProps>)
|
||||
.concat(links)
|
||||
.map((link, index) => [
|
||||
index ? ' | ' : '',
|
||||
<RejectionLink {...link} key={index} />,
|
||||
])}
|
||||
</span>
|
||||
) : null,
|
||||
});
|
||||
export default function ({ title, body, footer, links }: FactoryParams): Factory {
|
||||
return () => ({
|
||||
Title: () => <AuthTitle title={title} />,
|
||||
Body: body,
|
||||
Footer: () => <Button type="submit" {...footer} />,
|
||||
Links: () =>
|
||||
links ? (
|
||||
<span>
|
||||
{([] as Array<RejectionLinkProps>)
|
||||
.concat(links)
|
||||
.map((link, index) => [index ? ' | ' : '', <RejectionLink {...link} key={index} />])}
|
||||
</span>
|
||||
) : null,
|
||||
});
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"authForAppSuccessful": "Authorization for {appName} was successfully completed",
|
||||
"authForAppFailed": "Authorization for {appName} was failed",
|
||||
"waitAppReaction": "Please, wait till your application response",
|
||||
"passCodeToApp": "To complete authorization process, please, provide the following code to {appName}",
|
||||
"copy": "Copy"
|
||||
"authForAppSuccessful": "Authorization for {appName} was successfully completed",
|
||||
"authForAppFailed": "Authorization for {appName} was failed",
|
||||
"waitAppReaction": "Please, wait till your application response",
|
||||
"passCodeToApp": "To complete authorization process, please, provide the following code to {appName}",
|
||||
"copy": "Copy"
|
||||
}
|
||||
|
@ -10,100 +10,95 @@ import messages from './Finish.intl.json';
|
||||
import styles from './finish.scss';
|
||||
|
||||
interface Props {
|
||||
appName: string;
|
||||
code?: string;
|
||||
state: string;
|
||||
displayCode?: boolean;
|
||||
success?: boolean;
|
||||
appName: string;
|
||||
code?: string;
|
||||
state: string;
|
||||
displayCode?: boolean;
|
||||
success?: boolean;
|
||||
}
|
||||
|
||||
class Finish extends React.Component<Props> {
|
||||
render() {
|
||||
const { appName, code, state, displayCode, success } = this.props;
|
||||
const authData = JSON.stringify({
|
||||
auth_code: code,
|
||||
state,
|
||||
});
|
||||
render() {
|
||||
const { appName, code, state, displayCode, success } = this.props;
|
||||
const authData = JSON.stringify({
|
||||
auth_code: code,
|
||||
state,
|
||||
});
|
||||
|
||||
history.pushState(null, document.title, `#${authData}`);
|
||||
history.pushState(null, document.title, `#${authData}`);
|
||||
|
||||
return (
|
||||
<div className={styles.finishPage}>
|
||||
<Helmet title={authData} />
|
||||
return (
|
||||
<div className={styles.finishPage}>
|
||||
<Helmet title={authData} />
|
||||
|
||||
{success ? (
|
||||
<div>
|
||||
<div className={styles.successBackground} />
|
||||
<div className={styles.greenTitle}>
|
||||
<Message
|
||||
{...messages.authForAppSuccessful}
|
||||
values={{
|
||||
appName: <span className={styles.appName}>{appName}</span>,
|
||||
}}
|
||||
/>
|
||||
{success ? (
|
||||
<div>
|
||||
<div className={styles.successBackground} />
|
||||
<div className={styles.greenTitle}>
|
||||
<Message
|
||||
{...messages.authForAppSuccessful}
|
||||
values={{
|
||||
appName: <span className={styles.appName}>{appName}</span>,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{displayCode ? (
|
||||
<div data-testid="oauth-code-container">
|
||||
<div className={styles.description}>
|
||||
<Message {...messages.passCodeToApp} values={{ appName }} />
|
||||
</div>
|
||||
<div className={styles.codeContainer}>
|
||||
<div className={styles.code}>{code}</div>
|
||||
</div>
|
||||
<Button color="green" small label={messages.copy} onClick={this.onCopyClick} />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.description}>
|
||||
<Message {...messages.waitAppReaction} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className={styles.failBackground} />
|
||||
<div className={styles.redTitle}>
|
||||
<Message
|
||||
{...messages.authForAppFailed}
|
||||
values={{
|
||||
appName: <span className={styles.appName}>{appName}</span>,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.description}>
|
||||
<Message {...messages.waitAppReaction} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{displayCode ? (
|
||||
<div data-testid="oauth-code-container">
|
||||
<div className={styles.description}>
|
||||
<Message {...messages.passCodeToApp} values={{ appName }} />
|
||||
</div>
|
||||
<div className={styles.codeContainer}>
|
||||
<div className={styles.code}>{code}</div>
|
||||
</div>
|
||||
<Button
|
||||
color="green"
|
||||
small
|
||||
label={messages.copy}
|
||||
onClick={this.onCopyClick}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.description}>
|
||||
<Message {...messages.waitAppReaction} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className={styles.failBackground} />
|
||||
<div className={styles.redTitle}>
|
||||
<Message
|
||||
{...messages.authForAppFailed}
|
||||
values={{
|
||||
appName: <span className={styles.appName}>{appName}</span>,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.description}>
|
||||
<Message {...messages.waitAppReaction} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onCopyClick: MouseEventHandler = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const { code } = this.props;
|
||||
|
||||
if (code) {
|
||||
copy(code);
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
onCopyClick: MouseEventHandler = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const { code } = this.props;
|
||||
|
||||
if (code) {
|
||||
copy(code);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(({ auth }: RootState) => {
|
||||
if (!auth || !auth.client || !auth.oauth) {
|
||||
throw new Error('Can not connect Finish component. No auth data in state');
|
||||
}
|
||||
if (!auth || !auth.client || !auth.oauth) {
|
||||
throw new Error('Can not connect Finish component. No auth data in state');
|
||||
}
|
||||
|
||||
return {
|
||||
appName: auth.client.name,
|
||||
code: auth.oauth.code,
|
||||
displayCode: auth.oauth.displayCode,
|
||||
state: auth.oauth.state,
|
||||
success: auth.oauth.success,
|
||||
};
|
||||
return {
|
||||
appName: auth.client.name,
|
||||
code: auth.oauth.code,
|
||||
displayCode: auth.oauth.displayCode,
|
||||
state: auth.oauth.state,
|
||||
success: auth.oauth.success,
|
||||
};
|
||||
})(Finish);
|
||||
|
@ -2,75 +2,75 @@
|
||||
@import '~app/components/ui/fonts.scss';
|
||||
|
||||
.finishPage {
|
||||
font-family: $font-family-title;
|
||||
position: relative;
|
||||
max-width: 515px;
|
||||
padding-top: 40px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
font-family: $font-family-title;
|
||||
position: relative;
|
||||
max-width: 515px;
|
||||
padding-top: 40px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.iconBackground {
|
||||
position: absolute;
|
||||
top: -15px;
|
||||
transform: translateX(-50%);
|
||||
font-size: 200px;
|
||||
color: #e0d9cf;
|
||||
z-index: -1;
|
||||
position: absolute;
|
||||
top: -15px;
|
||||
transform: translateX(-50%);
|
||||
font-size: 200px;
|
||||
color: #e0d9cf;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.successBackground {
|
||||
composes: checkmark from '~app/components/ui/icons.scss';
|
||||
@extend .iconBackground;
|
||||
composes: checkmark from '~app/components/ui/icons.scss';
|
||||
@extend .iconBackground;
|
||||
}
|
||||
|
||||
.failBackground {
|
||||
composes: close from '~app/components/ui/icons.scss';
|
||||
@extend .iconBackground;
|
||||
composes: close from '~app/components/ui/icons.scss';
|
||||
@extend .iconBackground;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 22px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 22px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.greenTitle {
|
||||
composes: title;
|
||||
composes: title;
|
||||
|
||||
color: $green;
|
||||
color: $green;
|
||||
|
||||
.appName {
|
||||
color: darker($green);
|
||||
}
|
||||
.appName {
|
||||
color: darker($green);
|
||||
}
|
||||
}
|
||||
|
||||
.redTitle {
|
||||
composes: title;
|
||||
composes: title;
|
||||
|
||||
color: $red;
|
||||
color: $red;
|
||||
|
||||
.appName {
|
||||
color: darker($red);
|
||||
}
|
||||
.appName {
|
||||
color: darker($red);
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 18px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 18px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.codeContainer {
|
||||
margin-bottom: 5px;
|
||||
margin-top: 35px;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 35px;
|
||||
}
|
||||
|
||||
.code {
|
||||
$border: 5px solid darker($green);
|
||||
$border: 5px solid darker($green);
|
||||
|
||||
display: inline-block;
|
||||
border-right: $border;
|
||||
border-left: $border;
|
||||
padding: 5px 10px;
|
||||
word-break: break-all;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
border-right: $border;
|
||||
border-left: $border;
|
||||
padding: 5px 10px;
|
||||
word-break: break-all;
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"title": "Forgot password",
|
||||
"sendMail": "Send mail",
|
||||
"specifyEmail": "Specify the registration E‑mail address or last used username for your account and we will send an E‑mail with instructions for further password recovery.",
|
||||
"pleasePressButton": "Please press the button bellow to get an E‑mail with password recovery code.",
|
||||
"alreadyHaveCode": "Already have a code"
|
||||
"title": "Forgot password",
|
||||
"sendMail": "Send mail",
|
||||
"specifyEmail": "Specify the registration E‑mail address or last used username for your account and we will send an E‑mail with instructions for further password recovery.",
|
||||
"pleasePressButton": "Please press the button bellow to get an E‑mail with password recovery code.",
|
||||
"alreadyHaveCode": "Already have a code"
|
||||
}
|
||||
|
@ -3,14 +3,14 @@ import messages from './ForgotPassword.intl.json';
|
||||
import Body from './ForgotPasswordBody';
|
||||
|
||||
export default factory({
|
||||
title: messages.title,
|
||||
body: Body,
|
||||
footer: {
|
||||
color: 'lightViolet',
|
||||
autoFocus: true,
|
||||
label: messages.sendMail,
|
||||
},
|
||||
links: {
|
||||
label: messages.alreadyHaveCode,
|
||||
},
|
||||
title: messages.title,
|
||||
body: Body,
|
||||
footer: {
|
||||
color: 'lightViolet',
|
||||
autoFocus: true,
|
||||
label: messages.sendMail,
|
||||
},
|
||||
links: {
|
||||
label: messages.alreadyHaveCode,
|
||||
},
|
||||
});
|
||||
|
@ -11,87 +11,83 @@ import styles from './forgotPassword.scss';
|
||||
import messages from './ForgotPassword.intl.json';
|
||||
|
||||
export default class ForgotPasswordBody extends BaseAuthBody {
|
||||
static displayName = 'ForgotPasswordBody';
|
||||
static panelId = 'forgotPassword';
|
||||
static hasGoBack = true;
|
||||
static displayName = 'ForgotPasswordBody';
|
||||
static panelId = 'forgotPassword';
|
||||
static hasGoBack = true;
|
||||
|
||||
state = {
|
||||
isLoginEdit: false,
|
||||
};
|
||||
state = {
|
||||
isLoginEdit: false,
|
||||
};
|
||||
|
||||
autoFocusField = 'login';
|
||||
autoFocusField = 'login';
|
||||
|
||||
render() {
|
||||
const { isLoginEdit } = this.state;
|
||||
render() {
|
||||
const { isLoginEdit } = this.state;
|
||||
|
||||
const login = this.getLogin();
|
||||
const isLoginEditShown = isLoginEdit || !login;
|
||||
const login = this.getLogin();
|
||||
const isLoginEditShown = isLoginEdit || !login;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{this.renderErrors()}
|
||||
return (
|
||||
<div>
|
||||
{this.renderErrors()}
|
||||
|
||||
<PanelIcon icon="lock" />
|
||||
<PanelIcon icon="lock" />
|
||||
|
||||
{isLoginEditShown ? (
|
||||
<div>
|
||||
<p className={styles.descriptionText}>
|
||||
<Message {...messages.specifyEmail} />
|
||||
</p>
|
||||
<Input
|
||||
{...this.bindField('login')}
|
||||
icon="envelope"
|
||||
color="lightViolet"
|
||||
required
|
||||
placeholder={messages.accountEmail}
|
||||
defaultValue={login}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div data-testid="forgot-password-login">
|
||||
<div className={styles.login}>
|
||||
{login}
|
||||
<span
|
||||
className={styles.editLogin}
|
||||
onClick={this.onClickEdit}
|
||||
data-testid="edit-login"
|
||||
/>
|
||||
{isLoginEditShown ? (
|
||||
<div>
|
||||
<p className={styles.descriptionText}>
|
||||
<Message {...messages.specifyEmail} />
|
||||
</p>
|
||||
<Input
|
||||
{...this.bindField('login')}
|
||||
icon="envelope"
|
||||
color="lightViolet"
|
||||
required
|
||||
placeholder={messages.accountEmail}
|
||||
defaultValue={login}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div data-testid="forgot-password-login">
|
||||
<div className={styles.login}>
|
||||
{login}
|
||||
<span className={styles.editLogin} onClick={this.onClickEdit} data-testid="edit-login" />
|
||||
</div>
|
||||
<p className={styles.descriptionText}>
|
||||
<Message {...messages.pleasePressButton} />
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Captcha {...this.bindField('captcha')} delay={600} />
|
||||
</div>
|
||||
<p className={styles.descriptionText}>
|
||||
<Message {...messages.pleasePressButton} />
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Captcha {...this.bindField('captcha')} delay={600} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
serialize() {
|
||||
const data = super.serialize();
|
||||
|
||||
if (!data.login) {
|
||||
data.login = this.getLogin();
|
||||
);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
serialize() {
|
||||
const data = super.serialize();
|
||||
|
||||
getLogin() {
|
||||
const login = getLogin(this.context);
|
||||
const { user } = this.context;
|
||||
if (!data.login) {
|
||||
data.login = this.getLogin();
|
||||
}
|
||||
|
||||
return login || user.username || user.email || '';
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
onClickEdit = async () => {
|
||||
this.setState({
|
||||
isLoginEdit: true,
|
||||
});
|
||||
getLogin() {
|
||||
const login = getLogin(this.context);
|
||||
const { user } = this.context;
|
||||
|
||||
await this.context.requestRedraw();
|
||||
return login || user.username || user.email || '';
|
||||
}
|
||||
|
||||
this.form.focus('login');
|
||||
};
|
||||
onClickEdit = async () => {
|
||||
this.setState({
|
||||
isLoginEdit: true,
|
||||
});
|
||||
|
||||
await this.context.requestRedraw();
|
||||
|
||||
this.form.focus('login');
|
||||
};
|
||||
}
|
||||
|
@ -1,31 +1,31 @@
|
||||
@import '~app/components/ui/colors.scss';
|
||||
|
||||
.descriptionText {
|
||||
font-size: 15px;
|
||||
line-height: 1.4;
|
||||
padding-bottom: 8px;
|
||||
color: #aaa;
|
||||
font-size: 15px;
|
||||
line-height: 1.4;
|
||||
padding-bottom: 8px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.login {
|
||||
composes: email from '~app/components/auth/password/password.scss';
|
||||
composes: email from '~app/components/auth/password/password.scss';
|
||||
}
|
||||
|
||||
.editLogin {
|
||||
composes: pencil from '~app/components/ui/icons.scss';
|
||||
composes: pencil from '~app/components/ui/icons.scss';
|
||||
|
||||
position: relative;
|
||||
bottom: 1px;
|
||||
padding-left: 3px;
|
||||
position: relative;
|
||||
bottom: 1px;
|
||||
padding-left: 3px;
|
||||
|
||||
color: #666666;
|
||||
font-size: 10px;
|
||||
color: #666666;
|
||||
font-size: 10px;
|
||||
|
||||
transition: color 0.3s;
|
||||
transition: color 0.3s;
|
||||
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #ccc;
|
||||
}
|
||||
&:hover {
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
.helpLinks {
|
||||
margin: 8px 0;
|
||||
position: relative;
|
||||
height: 20px;
|
||||
margin: 8px 0;
|
||||
position: relative;
|
||||
height: 20px;
|
||||
|
||||
color: #444;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
color: #444;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"createNewAccount": "Create new account",
|
||||
"loginTitle": "Sign in",
|
||||
"emailOrUsername": "E‑mail or username",
|
||||
"next": "Next"
|
||||
"createNewAccount": "Create new account",
|
||||
"loginTitle": "Sign in",
|
||||
"emailOrUsername": "E‑mail or username",
|
||||
"next": "Next"
|
||||
}
|
||||
|
@ -3,14 +3,14 @@ import Body from './LoginBody';
|
||||
import messages from './Login.intl.json';
|
||||
|
||||
export default factory({
|
||||
title: messages.loginTitle,
|
||||
body: Body,
|
||||
footer: {
|
||||
color: 'green',
|
||||
label: messages.next,
|
||||
},
|
||||
links: {
|
||||
isAvailable: (context) => !context.user.isGuest,
|
||||
label: messages.createNewAccount,
|
||||
},
|
||||
title: messages.loginTitle,
|
||||
body: Body,
|
||||
footer: {
|
||||
color: 'green',
|
||||
label: messages.next,
|
||||
},
|
||||
links: {
|
||||
isAvailable: (context) => !context.user.isGuest,
|
||||
label: messages.createNewAccount,
|
||||
},
|
||||
});
|
||||
|
@ -7,26 +7,21 @@ import { User } from 'app/components/user/reducer';
|
||||
import messages from './Login.intl.json';
|
||||
|
||||
export default class LoginBody extends BaseAuthBody {
|
||||
static displayName = 'LoginBody';
|
||||
static panelId = 'login';
|
||||
static hasGoBack = (state: { user: User }) => {
|
||||
return !state.user.isGuest;
|
||||
};
|
||||
static displayName = 'LoginBody';
|
||||
static panelId = 'login';
|
||||
static hasGoBack = (state: { user: User }) => {
|
||||
return !state.user.isGuest;
|
||||
};
|
||||
|
||||
autoFocusField = 'login';
|
||||
autoFocusField = 'login';
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.renderErrors()}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.renderErrors()}
|
||||
|
||||
<Input
|
||||
{...this.bindField('login')}
|
||||
icon="envelope"
|
||||
required
|
||||
placeholder={messages.emailOrUsername}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<Input {...this.bindField('login')} icon="envelope" required placeholder={messages.emailOrUsername} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
{
|
||||
"enterTotp": "Enter code",
|
||||
"description": "In order to sign in this account, you need to enter a one-time password from mobile application"
|
||||
"enterTotp": "Enter code",
|
||||
"description": "In order to sign in this account, you need to enter a one-time password from mobile application"
|
||||
}
|
||||
|
@ -4,10 +4,10 @@ import messages from './Mfa.intl.json';
|
||||
import passwordMessages from '../password/Password.intl.json';
|
||||
|
||||
export default factory({
|
||||
title: messages.enterTotp,
|
||||
body: Body,
|
||||
footer: {
|
||||
color: 'green',
|
||||
label: passwordMessages.signInButton,
|
||||
},
|
||||
title: messages.enterTotp,
|
||||
body: Body,
|
||||
footer: {
|
||||
color: 'green',
|
||||
label: passwordMessages.signInButton,
|
||||
},
|
||||
});
|
||||
|
@ -8,30 +8,30 @@ import styles from './mfa.scss';
|
||||
import messages from './Mfa.intl.json';
|
||||
|
||||
export default class MfaBody extends BaseAuthBody {
|
||||
static panelId = 'mfa';
|
||||
static hasGoBack = true;
|
||||
static panelId = 'mfa';
|
||||
static hasGoBack = true;
|
||||
|
||||
autoFocusField = 'totp';
|
||||
autoFocusField = 'totp';
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.renderErrors()}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.renderErrors()}
|
||||
|
||||
<PanelIcon icon="lock" />
|
||||
<PanelIcon icon="lock" />
|
||||
|
||||
<p className={styles.descriptionText}>
|
||||
<Message {...messages.description} />
|
||||
</p>
|
||||
<p className={styles.descriptionText}>
|
||||
<Message {...messages.description} />
|
||||
</p>
|
||||
|
||||
<Input
|
||||
{...this.bindField('totp')}
|
||||
icon="key"
|
||||
required
|
||||
placeholder={messages.enterTotp}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<Input
|
||||
{...this.bindField('totp')}
|
||||
icon="key"
|
||||
required
|
||||
placeholder={messages.enterTotp}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
.descriptionText {
|
||||
font-size: 15px;
|
||||
line-height: 1.4;
|
||||
padding-bottom: 8px;
|
||||
color: #aaa;
|
||||
font-size: 15px;
|
||||
line-height: 1.4;
|
||||
padding-bottom: 8px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"passwordTitle": "Enter password",
|
||||
"signInButton": "Sign in",
|
||||
"forgotPassword": "Forgot password",
|
||||
"accountPassword": "Account password",
|
||||
"rememberMe": "Remember me on this device"
|
||||
"passwordTitle": "Enter password",
|
||||
"signInButton": "Sign in",
|
||||
"forgotPassword": "Forgot password",
|
||||
"accountPassword": "Account password",
|
||||
"rememberMe": "Remember me on this device"
|
||||
}
|
||||
|
@ -3,13 +3,13 @@ import Body from './PasswordBody';
|
||||
import messages from './Password.intl.json';
|
||||
|
||||
export default factory({
|
||||
title: messages.passwordTitle,
|
||||
body: Body,
|
||||
footer: {
|
||||
color: 'green',
|
||||
label: messages.signInButton,
|
||||
},
|
||||
links: {
|
||||
label: messages.forgotPassword,
|
||||
},
|
||||
title: messages.passwordTitle,
|
||||
body: Body,
|
||||
footer: {
|
||||
color: 'green',
|
||||
label: messages.signInButton,
|
||||
},
|
||||
links: {
|
||||
label: messages.forgotPassword,
|
||||
},
|
||||
});
|
||||
|
@ -8,46 +8,38 @@ import styles from './password.scss';
|
||||
import messages from './Password.intl.json';
|
||||
|
||||
export default class PasswordBody extends BaseAuthBody {
|
||||
static displayName = 'PasswordBody';
|
||||
static panelId = 'password';
|
||||
static hasGoBack = true;
|
||||
static displayName = 'PasswordBody';
|
||||
static panelId = 'password';
|
||||
static hasGoBack = true;
|
||||
|
||||
autoFocusField = 'password';
|
||||
autoFocusField = 'password';
|
||||
|
||||
render() {
|
||||
const { user } = this.context;
|
||||
render() {
|
||||
const { user } = this.context;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{this.renderErrors()}
|
||||
return (
|
||||
<div>
|
||||
{this.renderErrors()}
|
||||
|
||||
<div className={styles.miniProfile}>
|
||||
<div className={styles.avatar}>
|
||||
{user.avatar ? (
|
||||
<img src={user.avatar} />
|
||||
) : (
|
||||
<span className={icons.user} />
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.email}>{user.email || user.username}</div>
|
||||
</div>
|
||||
<div className={styles.miniProfile}>
|
||||
<div className={styles.avatar}>
|
||||
{user.avatar ? <img src={user.avatar} /> : <span className={icons.user} />}
|
||||
</div>
|
||||
<div className={styles.email}>{user.email || user.username}</div>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
{...this.bindField('password')}
|
||||
icon="key"
|
||||
type="password"
|
||||
required
|
||||
placeholder={messages.accountPassword}
|
||||
/>
|
||||
<Input
|
||||
{...this.bindField('password')}
|
||||
icon="key"
|
||||
type="password"
|
||||
required
|
||||
placeholder={messages.accountPassword}
|
||||
/>
|
||||
|
||||
<div className={authStyles.checkboxInput}>
|
||||
<Checkbox
|
||||
{...this.bindField('rememberMe')}
|
||||
defaultChecked
|
||||
label={messages.rememberMe}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<div className={authStyles.checkboxInput}>
|
||||
<Checkbox {...this.bindField('rememberMe')} defaultChecked label={messages.rememberMe} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +1,22 @@
|
||||
@import '~app/components/ui/fonts.scss';
|
||||
|
||||
.avatar {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
font-size: 90px;
|
||||
line-height: 1;
|
||||
margin: 0 auto;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
font-size: 90px;
|
||||
line-height: 1;
|
||||
margin: 0 auto;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.email {
|
||||
font-family: $font-family-title;
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
font-family: $font-family-title;
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
|
||||
margin-bottom: 15px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 15px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
{
|
||||
"permissionsTitle": "Application permissions",
|
||||
"youAuthorizedAs": "You authorized as:",
|
||||
"theAppNeedsAccess1": "This application needs access",
|
||||
"theAppNeedsAccess2": "to your data",
|
||||
"decline": "Decline",
|
||||
"approve": "Approve",
|
||||
"scope_minecraft_server_session": "Authorization data for minecraft server",
|
||||
"scope_offline_access": "Access to your profile data, when you offline",
|
||||
"scope_account_info": "Access to your profile data (except E‑mail)",
|
||||
"scope_account_email": "Access to your E‑mail address"
|
||||
"permissionsTitle": "Application permissions",
|
||||
"youAuthorizedAs": "You authorized as:",
|
||||
"theAppNeedsAccess1": "This application needs access",
|
||||
"theAppNeedsAccess2": "to your data",
|
||||
"decline": "Decline",
|
||||
"approve": "Approve",
|
||||
"scope_minecraft_server_session": "Authorization data for minecraft server",
|
||||
"scope_offline_access": "Access to your profile data, when you offline",
|
||||
"scope_account_info": "Access to your profile data (except E‑mail)",
|
||||
"scope_account_email": "Access to your E‑mail address"
|
||||
}
|
||||
|
@ -3,14 +3,14 @@ import messages from './Permissions.intl.json';
|
||||
import Body from './PermissionsBody';
|
||||
|
||||
export default factory({
|
||||
title: messages.permissionsTitle,
|
||||
body: Body,
|
||||
footer: {
|
||||
color: 'orange',
|
||||
autoFocus: true,
|
||||
label: messages.approve,
|
||||
},
|
||||
links: {
|
||||
label: messages.decline,
|
||||
},
|
||||
title: messages.permissionsTitle,
|
||||
body: Body,
|
||||
footer: {
|
||||
color: 'orange',
|
||||
autoFocus: true,
|
||||
label: messages.approve,
|
||||
},
|
||||
links: {
|
||||
label: messages.decline,
|
||||
},
|
||||
});
|
||||
|
@ -8,58 +8,52 @@ import styles from './permissions.scss';
|
||||
import messages from './Permissions.intl.json';
|
||||
|
||||
export default class PermissionsBody extends BaseAuthBody {
|
||||
static displayName = 'PermissionsBody';
|
||||
static panelId = 'permissions';
|
||||
static displayName = 'PermissionsBody';
|
||||
static panelId = 'permissions';
|
||||
|
||||
render() {
|
||||
const { user } = this.context;
|
||||
const { scopes } = this.context.auth;
|
||||
render() {
|
||||
const { user } = this.context;
|
||||
const { scopes } = this.context.auth;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{this.renderErrors()}
|
||||
return (
|
||||
<div>
|
||||
{this.renderErrors()}
|
||||
|
||||
<PanelBodyHeader>
|
||||
<div className={styles.authInfo}>
|
||||
<div className={styles.authInfoAvatar}>
|
||||
{user.avatar ? (
|
||||
<img src={user.avatar} />
|
||||
) : (
|
||||
<span className={icons.user} />
|
||||
)}
|
||||
<PanelBodyHeader>
|
||||
<div className={styles.authInfo}>
|
||||
<div className={styles.authInfoAvatar}>
|
||||
{user.avatar ? <img src={user.avatar} /> : <span className={icons.user} />}
|
||||
</div>
|
||||
<div className={styles.authInfoTitle}>
|
||||
<Message {...messages.youAuthorizedAs} />
|
||||
</div>
|
||||
<div className={styles.authInfoEmail}>{user.username}</div>
|
||||
</div>
|
||||
</PanelBodyHeader>
|
||||
<div className={styles.permissionsContainer}>
|
||||
<div className={styles.permissionsTitle}>
|
||||
<Message {...messages.theAppNeedsAccess1} />
|
||||
<br />
|
||||
<Message {...messages.theAppNeedsAccess2} />
|
||||
</div>
|
||||
<ul className={styles.permissionsList}>
|
||||
{scopes.map((scope) => {
|
||||
const key = `scope_${scope}`;
|
||||
const message = messages[key];
|
||||
|
||||
return (
|
||||
<li key={key}>
|
||||
{message ? (
|
||||
<Message {...message} />
|
||||
) : (
|
||||
scope.replace(/^\w|_/g, (match) => match.replace('_', ' ').toUpperCase())
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.authInfoTitle}>
|
||||
<Message {...messages.youAuthorizedAs} />
|
||||
</div>
|
||||
<div className={styles.authInfoEmail}>{user.username}</div>
|
||||
</div>
|
||||
</PanelBodyHeader>
|
||||
<div className={styles.permissionsContainer}>
|
||||
<div className={styles.permissionsTitle}>
|
||||
<Message {...messages.theAppNeedsAccess1} />
|
||||
<br />
|
||||
<Message {...messages.theAppNeedsAccess2} />
|
||||
</div>
|
||||
<ul className={styles.permissionsList}>
|
||||
{scopes.map((scope) => {
|
||||
const key = `scope_${scope}`;
|
||||
const message = messages[key];
|
||||
|
||||
return (
|
||||
<li key={key}>
|
||||
{message ? (
|
||||
<Message {...message} />
|
||||
) : (
|
||||
scope.replace(/^\w|_/g, (match) =>
|
||||
match.replace('_', ' ').toUpperCase(),
|
||||
)
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,76 +2,76 @@
|
||||
@import '~app/components/ui/fonts.scss';
|
||||
|
||||
.authInfo {
|
||||
// Отступы сверху и снизу разные т.к. мы ужимаем высоту линии строки с логином на 2 пикселя и из-за этого теряем отступ снизу
|
||||
padding: 5px 20px 7px;
|
||||
text-align: left;
|
||||
// Отступы сверху и снизу разные т.к. мы ужимаем высоту линии строки с логином на 2 пикселя и из-за этого теряем отступ снизу
|
||||
padding: 5px 20px 7px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.authInfoAvatar {
|
||||
$size: 30px;
|
||||
$size: 30px;
|
||||
|
||||
float: left;
|
||||
height: $size;
|
||||
width: $size;
|
||||
font-size: $size;
|
||||
line-height: 1;
|
||||
margin-right: 10px;
|
||||
margin-top: 2px;
|
||||
color: #aaa;
|
||||
float: left;
|
||||
height: $size;
|
||||
width: $size;
|
||||
font-size: $size;
|
||||
line-height: 1;
|
||||
margin-right: 10px;
|
||||
margin-top: 2px;
|
||||
color: #aaa;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.authInfoTitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.authInfoEmail {
|
||||
font-family: $font-family-title;
|
||||
font-size: 20px;
|
||||
line-height: 16px;
|
||||
color: #fff;
|
||||
font-family: $font-family-title;
|
||||
font-size: 20px;
|
||||
line-height: 16px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.permissionsContainer {
|
||||
padding: 15px 12px;
|
||||
text-align: left;
|
||||
padding: 15px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.permissionsTitle {
|
||||
font-family: $font-family-title;
|
||||
font-size: 18px;
|
||||
color: #dd8650;
|
||||
padding-bottom: 6px;
|
||||
font-family: $font-family-title;
|
||||
font-size: 18px;
|
||||
color: #dd8650;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.permissionsList {
|
||||
list-style: none;
|
||||
margin-top: 10px;
|
||||
list-style: none;
|
||||
margin-top: 10px;
|
||||
|
||||
li {
|
||||
color: #a9a9a9;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
padding-bottom: 4px;
|
||||
padding-left: 17px;
|
||||
position: relative;
|
||||
li {
|
||||
color: #a9a9a9;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
padding-bottom: 4px;
|
||||
padding-left: 17px;
|
||||
position: relative;
|
||||
|
||||
&:last-of-type {
|
||||
padding-bottom: 0;
|
||||
&:last-of-type {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '• ';
|
||||
color: lighter($light_violet);
|
||||
font-size: 39px; // ~ 9px
|
||||
line-height: 9px;
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: -4px;
|
||||
}
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '• ';
|
||||
color: lighter($light_violet);
|
||||
font-size: 39px; // ~ 9px
|
||||
line-height: 9px;
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: -4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
{
|
||||
"title": "Restore password",
|
||||
"contactSupport": "Contact support",
|
||||
"messageWasSent": "The recovery code was sent to your account E‑mail.",
|
||||
"messageWasSentTo": "The recovery code was sent to your E‑mail {email}.",
|
||||
"enterCodeBelow": "Please enter the code received into the field below:",
|
||||
"enterNewPasswordBelow": "Enter and repeat new password below:",
|
||||
"change": "Change password",
|
||||
"newPassword": "Enter new password",
|
||||
"newRePassword": "Repeat new password",
|
||||
"enterTheCode": "Enter confirmation code"
|
||||
"title": "Restore password",
|
||||
"contactSupport": "Contact support",
|
||||
"messageWasSent": "The recovery code was sent to your account E‑mail.",
|
||||
"messageWasSentTo": "The recovery code was sent to your E‑mail {email}.",
|
||||
"enterCodeBelow": "Please enter the code received into the field below:",
|
||||
"enterNewPasswordBelow": "Enter and repeat new password below:",
|
||||
"change": "Change password",
|
||||
"newPassword": "Enter new password",
|
||||
"newRePassword": "Repeat new password",
|
||||
"enterTheCode": "Enter confirmation code"
|
||||
}
|
||||
|
@ -3,13 +3,13 @@ import messages from './RecoverPassword.intl.json';
|
||||
import Body from './RecoverPasswordBody';
|
||||
|
||||
export default factory({
|
||||
title: messages.title,
|
||||
body: Body,
|
||||
footer: {
|
||||
color: 'lightViolet',
|
||||
label: messages.change,
|
||||
},
|
||||
links: {
|
||||
label: messages.contactSupport,
|
||||
},
|
||||
title: messages.title,
|
||||
body: Body,
|
||||
footer: {
|
||||
color: 'lightViolet',
|
||||
label: messages.change,
|
||||
},
|
||||
links: {
|
||||
label: messages.contactSupport,
|
||||
},
|
||||
});
|
||||
|
@ -11,70 +11,67 @@ import messages from './RecoverPassword.intl.json';
|
||||
// TODO: activation code field may be decoupled into common component and reused here and in activation panel
|
||||
|
||||
export default class RecoverPasswordBody extends BaseAuthBody {
|
||||
static displayName = 'RecoverPasswordBody';
|
||||
static panelId = 'recoverPassword';
|
||||
static hasGoBack = true;
|
||||
static displayName = 'RecoverPasswordBody';
|
||||
static panelId = 'recoverPassword';
|
||||
static hasGoBack = true;
|
||||
|
||||
autoFocusField =
|
||||
this.props.match.params && this.props.match.params.key
|
||||
? 'newPassword'
|
||||
: 'key';
|
||||
autoFocusField = this.props.match.params && this.props.match.params.key ? 'newPassword' : 'key';
|
||||
|
||||
render() {
|
||||
const { user } = this.context;
|
||||
const { key } = this.props.match.params;
|
||||
render() {
|
||||
const { user } = this.context;
|
||||
const { key } = this.props.match.params;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{this.renderErrors()}
|
||||
return (
|
||||
<div>
|
||||
{this.renderErrors()}
|
||||
|
||||
<p className={styles.descriptionText}>
|
||||
{user.maskedEmail ? (
|
||||
<Message
|
||||
{...messages.messageWasSentTo}
|
||||
values={{
|
||||
email: <b>{user.maskedEmail}</b>,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Message {...messages.messageWasSent} />
|
||||
)}{' '}
|
||||
<Message {...messages.enterCodeBelow} />
|
||||
</p>
|
||||
<p className={styles.descriptionText}>
|
||||
{user.maskedEmail ? (
|
||||
<Message
|
||||
{...messages.messageWasSentTo}
|
||||
values={{
|
||||
email: <b>{user.maskedEmail}</b>,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Message {...messages.messageWasSent} />
|
||||
)}{' '}
|
||||
<Message {...messages.enterCodeBelow} />
|
||||
</p>
|
||||
|
||||
<Input
|
||||
{...this.bindField('key')}
|
||||
color="lightViolet"
|
||||
center
|
||||
required
|
||||
value={key}
|
||||
readOnly={!!key}
|
||||
autoComplete="off"
|
||||
placeholder={messages.enterTheCode}
|
||||
/>
|
||||
<Input
|
||||
{...this.bindField('key')}
|
||||
color="lightViolet"
|
||||
center
|
||||
required
|
||||
value={key}
|
||||
readOnly={!!key}
|
||||
autoComplete="off"
|
||||
placeholder={messages.enterTheCode}
|
||||
/>
|
||||
|
||||
<p className={styles.descriptionText}>
|
||||
<Message {...messages.enterNewPasswordBelow} />
|
||||
</p>
|
||||
<p className={styles.descriptionText}>
|
||||
<Message {...messages.enterNewPasswordBelow} />
|
||||
</p>
|
||||
|
||||
<Input
|
||||
{...this.bindField('newPassword')}
|
||||
icon="key"
|
||||
color="lightViolet"
|
||||
type="password"
|
||||
required
|
||||
placeholder={messages.newPassword}
|
||||
/>
|
||||
<Input
|
||||
{...this.bindField('newPassword')}
|
||||
icon="key"
|
||||
color="lightViolet"
|
||||
type="password"
|
||||
required
|
||||
placeholder={messages.newPassword}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...this.bindField('newRePassword')}
|
||||
icon="key"
|
||||
color="lightViolet"
|
||||
type="password"
|
||||
required
|
||||
placeholder={messages.newRePassword}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<Input
|
||||
{...this.bindField('newRePassword')}
|
||||
icon="key"
|
||||
color="lightViolet"
|
||||
type="password"
|
||||
required
|
||||
placeholder={messages.newRePassword}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
@import '~app/components/ui/colors.scss';
|
||||
|
||||
.descriptionText {
|
||||
font-size: 15px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 8px;
|
||||
color: #aaa;
|
||||
font-size: 15px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 8px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
@ -3,40 +3,36 @@ import auth from './reducer';
|
||||
import { setLogin, setAccountSwitcher } from './actions';
|
||||
|
||||
describe('components/auth/reducer', () => {
|
||||
describe('auth:setCredentials', () => {
|
||||
it('should set login', () => {
|
||||
const expectedLogin = 'foo';
|
||||
describe('auth:setCredentials', () => {
|
||||
it('should set login', () => {
|
||||
const expectedLogin = 'foo';
|
||||
|
||||
expect(
|
||||
auth(undefined, setLogin(expectedLogin)).credentials,
|
||||
'to satisfy',
|
||||
{
|
||||
login: expectedLogin,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('auth:setAccountSwitcher', () => {
|
||||
it('should be enabled by default', () =>
|
||||
expect(auth(undefined, {} as any), 'to satisfy', {
|
||||
isSwitcherEnabled: true,
|
||||
}));
|
||||
|
||||
it('should enable switcher', () => {
|
||||
const expectedValue = true;
|
||||
|
||||
expect(auth(undefined, setAccountSwitcher(expectedValue)), 'to satisfy', {
|
||||
isSwitcherEnabled: expectedValue,
|
||||
});
|
||||
expect(auth(undefined, setLogin(expectedLogin)).credentials, 'to satisfy', {
|
||||
login: expectedLogin,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable switcher', () => {
|
||||
const expectedValue = false;
|
||||
describe('auth:setAccountSwitcher', () => {
|
||||
it('should be enabled by default', () =>
|
||||
expect(auth(undefined, {} as any), 'to satisfy', {
|
||||
isSwitcherEnabled: true,
|
||||
}));
|
||||
|
||||
expect(auth(undefined, setAccountSwitcher(expectedValue)), 'to satisfy', {
|
||||
isSwitcherEnabled: expectedValue,
|
||||
});
|
||||
it('should enable switcher', () => {
|
||||
const expectedValue = true;
|
||||
|
||||
expect(auth(undefined, setAccountSwitcher(expectedValue)), 'to satisfy', {
|
||||
isSwitcherEnabled: expectedValue,
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable switcher', () => {
|
||||
const expectedValue = false;
|
||||
|
||||
expect(auth(undefined, setAccountSwitcher(expectedValue)), 'to satisfy', {
|
||||
isSwitcherEnabled: expectedValue,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -3,173 +3,156 @@ import { RootState } from 'app/reducers';
|
||||
import { Scope } from '../../services/api/oauth';
|
||||
|
||||
import {
|
||||
ErrorAction,
|
||||
CredentialsAction,
|
||||
AccountSwitcherAction,
|
||||
LoadingAction,
|
||||
ClientAction,
|
||||
OAuthAction,
|
||||
ScopesAction,
|
||||
ErrorAction,
|
||||
CredentialsAction,
|
||||
AccountSwitcherAction,
|
||||
LoadingAction,
|
||||
ClientAction,
|
||||
OAuthAction,
|
||||
ScopesAction,
|
||||
} from './actions';
|
||||
|
||||
export interface Credentials {
|
||||
login?: string | null; // By some reasons there is can be null value. Need to investigate.
|
||||
password?: string;
|
||||
rememberMe?: boolean;
|
||||
returnUrl?: string;
|
||||
isRelogin?: boolean;
|
||||
isTotpRequired?: boolean;
|
||||
login?: string | null; // By some reasons there is can be null value. Need to investigate.
|
||||
password?: string;
|
||||
rememberMe?: boolean;
|
||||
returnUrl?: string;
|
||||
isRelogin?: boolean;
|
||||
isTotpRequired?: boolean;
|
||||
}
|
||||
|
||||
type Error = Record<
|
||||
string,
|
||||
| string
|
||||
| {
|
||||
type: string;
|
||||
payload: Record<string, any>;
|
||||
}
|
||||
string,
|
||||
| string
|
||||
| {
|
||||
type: string;
|
||||
payload: Record<string, any>;
|
||||
}
|
||||
> | null;
|
||||
|
||||
export interface Client {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface OAuthState {
|
||||
clientId: string;
|
||||
redirectUrl: string;
|
||||
responseType: string;
|
||||
description?: string;
|
||||
scope: string;
|
||||
prompt: string;
|
||||
loginHint: string;
|
||||
state: string;
|
||||
success?: boolean;
|
||||
code?: string;
|
||||
displayCode?: boolean;
|
||||
acceptRequired?: boolean;
|
||||
clientId: string;
|
||||
redirectUrl: string;
|
||||
responseType: string;
|
||||
description?: string;
|
||||
scope: string;
|
||||
prompt: string;
|
||||
loginHint: string;
|
||||
state: string;
|
||||
success?: boolean;
|
||||
code?: string;
|
||||
displayCode?: boolean;
|
||||
acceptRequired?: boolean;
|
||||
}
|
||||
|
||||
type Scopes = Array<Scope>;
|
||||
|
||||
export interface State {
|
||||
credentials: Credentials;
|
||||
error: Error;
|
||||
isLoading: boolean;
|
||||
isSwitcherEnabled: boolean;
|
||||
client: Client | null;
|
||||
oauth: OAuthState | null;
|
||||
scopes: Scopes;
|
||||
credentials: Credentials;
|
||||
error: Error;
|
||||
isLoading: boolean;
|
||||
isSwitcherEnabled: boolean;
|
||||
client: Client | null;
|
||||
oauth: OAuthState | null;
|
||||
scopes: Scopes;
|
||||
}
|
||||
|
||||
const error: Reducer<State['error'], ErrorAction> = (
|
||||
state = null,
|
||||
{ type, payload },
|
||||
) => {
|
||||
if (type === 'auth:error') {
|
||||
return payload;
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
const credentials: Reducer<State['credentials'], CredentialsAction> = (
|
||||
state = {},
|
||||
{ type, payload },
|
||||
) => {
|
||||
if (type === 'auth:setCredentials') {
|
||||
if (payload) {
|
||||
return {
|
||||
...payload,
|
||||
};
|
||||
const error: Reducer<State['error'], ErrorAction> = (state = null, { type, payload }) => {
|
||||
if (type === 'auth:error') {
|
||||
return payload;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
return state;
|
||||
return state;
|
||||
};
|
||||
|
||||
const isSwitcherEnabled: Reducer<
|
||||
State['isSwitcherEnabled'],
|
||||
AccountSwitcherAction
|
||||
> = (state = true, { type, payload }) => {
|
||||
if (type === 'auth:setAccountSwitcher') {
|
||||
return payload;
|
||||
}
|
||||
const credentials: Reducer<State['credentials'], CredentialsAction> = (state = {}, { type, payload }) => {
|
||||
if (type === 'auth:setCredentials') {
|
||||
if (payload) {
|
||||
return {
|
||||
...payload,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
return {};
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
const isLoading: Reducer<State['isLoading'], LoadingAction> = (
|
||||
state = false,
|
||||
{ type, payload },
|
||||
const isSwitcherEnabled: Reducer<State['isSwitcherEnabled'], AccountSwitcherAction> = (
|
||||
state = true,
|
||||
{ type, payload },
|
||||
) => {
|
||||
if (type === 'set_loading_state') {
|
||||
return payload;
|
||||
}
|
||||
if (type === 'auth:setAccountSwitcher') {
|
||||
return payload;
|
||||
}
|
||||
|
||||
return state;
|
||||
return state;
|
||||
};
|
||||
|
||||
const client: Reducer<State['client'], ClientAction> = (
|
||||
state = null,
|
||||
{ type, payload },
|
||||
) => {
|
||||
if (type === 'set_client') {
|
||||
return payload;
|
||||
}
|
||||
const isLoading: Reducer<State['isLoading'], LoadingAction> = (state = false, { type, payload }) => {
|
||||
if (type === 'set_loading_state') {
|
||||
return payload;
|
||||
}
|
||||
|
||||
return state;
|
||||
return state;
|
||||
};
|
||||
|
||||
const client: Reducer<State['client'], ClientAction> = (state = null, { type, payload }) => {
|
||||
if (type === 'set_client') {
|
||||
return payload;
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
const oauth: Reducer<State['oauth'], OAuthAction> = (state = null, action) => {
|
||||
switch (action.type) {
|
||||
case 'set_oauth':
|
||||
return action.payload;
|
||||
case 'set_oauth_result':
|
||||
return {
|
||||
...(state as OAuthState),
|
||||
...action.payload,
|
||||
};
|
||||
case 'require_permissions_accept':
|
||||
return {
|
||||
...(state as OAuthState),
|
||||
acceptRequired: true,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
switch (action.type) {
|
||||
case 'set_oauth':
|
||||
return action.payload;
|
||||
case 'set_oauth_result':
|
||||
return {
|
||||
...(state as OAuthState),
|
||||
...action.payload,
|
||||
};
|
||||
case 'require_permissions_accept':
|
||||
return {
|
||||
...(state as OAuthState),
|
||||
acceptRequired: true,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const scopes: Reducer<State['scopes'], ScopesAction> = (
|
||||
state = [],
|
||||
{ type, payload },
|
||||
) => {
|
||||
if (type === 'set_scopes') {
|
||||
return payload;
|
||||
}
|
||||
const scopes: Reducer<State['scopes'], ScopesAction> = (state = [], { type, payload }) => {
|
||||
if (type === 'set_scopes') {
|
||||
return payload;
|
||||
}
|
||||
|
||||
return state;
|
||||
return state;
|
||||
};
|
||||
|
||||
export default combineReducers<State>({
|
||||
credentials,
|
||||
error,
|
||||
isLoading,
|
||||
isSwitcherEnabled,
|
||||
client,
|
||||
oauth,
|
||||
scopes,
|
||||
credentials,
|
||||
error,
|
||||
isLoading,
|
||||
isSwitcherEnabled,
|
||||
client,
|
||||
oauth,
|
||||
scopes,
|
||||
});
|
||||
|
||||
export function getLogin(
|
||||
state: RootState | Pick<RootState, 'auth'>,
|
||||
): string | null {
|
||||
return state.auth.credentials.login || null;
|
||||
export function getLogin(state: RootState | Pick<RootState, 'auth'>): string | null {
|
||||
return state.auth.credentials.login || null;
|
||||
}
|
||||
|
||||
export function getCredentials(state: RootState): Credentials {
|
||||
return state.auth.credentials;
|
||||
return state.auth.credentials;
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
{
|
||||
"registerTitle": "Sign Up",
|
||||
"yourNickname": "Your nickname",
|
||||
"yourEmail": "Your E‑mail",
|
||||
"accountPassword": "Account password",
|
||||
"repeatPassword": "Repeat password",
|
||||
"signUpButton": "Register",
|
||||
"acceptRules": "I agree with {link}",
|
||||
"termsOfService": "terms of service"
|
||||
"registerTitle": "Sign Up",
|
||||
"yourNickname": "Your nickname",
|
||||
"yourEmail": "Your E‑mail",
|
||||
"accountPassword": "Account password",
|
||||
"repeatPassword": "Repeat password",
|
||||
"signUpButton": "Register",
|
||||
"acceptRules": "I agree with {link}",
|
||||
"termsOfService": "terms of service"
|
||||
}
|
||||
|
@ -5,19 +5,19 @@ import messages from './Register.intl.json';
|
||||
import Body from './RegisterBody';
|
||||
|
||||
export default factory({
|
||||
title: messages.registerTitle,
|
||||
body: Body,
|
||||
footer: {
|
||||
color: 'blue',
|
||||
label: messages.signUpButton,
|
||||
},
|
||||
links: [
|
||||
{
|
||||
label: activationMessages.didNotReceivedEmail,
|
||||
payload: { requestEmail: true },
|
||||
title: messages.registerTitle,
|
||||
body: Body,
|
||||
footer: {
|
||||
color: 'blue',
|
||||
label: messages.signUpButton,
|
||||
},
|
||||
{
|
||||
label: forgotPasswordMessages.alreadyHaveCode,
|
||||
},
|
||||
],
|
||||
links: [
|
||||
{
|
||||
label: activationMessages.didNotReceivedEmail,
|
||||
payload: { requestEmail: true },
|
||||
},
|
||||
{
|
||||
label: forgotPasswordMessages.alreadyHaveCode,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -11,73 +11,73 @@ import messages from './Register.intl.json';
|
||||
// TODO: password and username can be validate for length and sameness
|
||||
|
||||
export default class RegisterBody extends BaseAuthBody {
|
||||
static panelId = 'register';
|
||||
static panelId = 'register';
|
||||
|
||||
autoFocusField = 'username';
|
||||
autoFocusField = 'username';
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.renderErrors()}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.renderErrors()}
|
||||
|
||||
<Input
|
||||
{...this.bindField('username')}
|
||||
icon="user"
|
||||
color="blue"
|
||||
type="text"
|
||||
required
|
||||
placeholder={messages.yourNickname}
|
||||
/>
|
||||
<Input
|
||||
{...this.bindField('username')}
|
||||
icon="user"
|
||||
color="blue"
|
||||
type="text"
|
||||
required
|
||||
placeholder={messages.yourNickname}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...this.bindField('email')}
|
||||
icon="envelope"
|
||||
color="blue"
|
||||
type="email"
|
||||
required
|
||||
placeholder={messages.yourEmail}
|
||||
/>
|
||||
<Input
|
||||
{...this.bindField('email')}
|
||||
icon="envelope"
|
||||
color="blue"
|
||||
type="email"
|
||||
required
|
||||
placeholder={messages.yourEmail}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...this.bindField('password')}
|
||||
icon="key"
|
||||
color="blue"
|
||||
type="password"
|
||||
required
|
||||
placeholder={passwordMessages.accountPassword}
|
||||
/>
|
||||
<Input
|
||||
{...this.bindField('password')}
|
||||
icon="key"
|
||||
color="blue"
|
||||
type="password"
|
||||
required
|
||||
placeholder={passwordMessages.accountPassword}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...this.bindField('rePassword')}
|
||||
icon="key"
|
||||
color="blue"
|
||||
type="password"
|
||||
required
|
||||
placeholder={messages.repeatPassword}
|
||||
/>
|
||||
<Input
|
||||
{...this.bindField('rePassword')}
|
||||
icon="key"
|
||||
color="blue"
|
||||
type="password"
|
||||
required
|
||||
placeholder={messages.repeatPassword}
|
||||
/>
|
||||
|
||||
<Captcha {...this.bindField('captcha')} delay={600} />
|
||||
<Captcha {...this.bindField('captcha')} delay={600} />
|
||||
|
||||
<div className={styles.checkboxInput}>
|
||||
<Checkbox
|
||||
{...this.bindField('rulesAgreement')}
|
||||
color="blue"
|
||||
required
|
||||
label={
|
||||
<Message
|
||||
{...messages.acceptRules}
|
||||
values={{
|
||||
link: (
|
||||
<Link to="/rules" target="_blank">
|
||||
<Message {...messages.termsOfService} />
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<div className={styles.checkboxInput}>
|
||||
<Checkbox
|
||||
{...this.bindField('rulesAgreement')}
|
||||
color="blue"
|
||||
required
|
||||
label={
|
||||
<Message
|
||||
{...messages.acceptRules}
|
||||
values={{
|
||||
link: (
|
||||
<Link to="/rules" target="_blank">
|
||||
<Message {...messages.termsOfService} />
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"title": "Did not received an E‑mail",
|
||||
"specifyYourEmail": "Please, enter an E‑mail you've registered with and we will send you new activation code",
|
||||
"sendNewEmail": "Send new E‑mail"
|
||||
"title": "Did not received an E‑mail",
|
||||
"specifyYourEmail": "Please, enter an E‑mail you've registered with and we will send you new activation code",
|
||||
"sendNewEmail": "Send new E‑mail"
|
||||
}
|
||||
|
@ -4,13 +4,13 @@ import messages from './ResendActivation.intl.json';
|
||||
import Body from './ResendActivationBody';
|
||||
|
||||
export default factory({
|
||||
title: messages.title,
|
||||
body: Body,
|
||||
footer: {
|
||||
color: 'blue',
|
||||
label: messages.sendNewEmail,
|
||||
},
|
||||
links: {
|
||||
label: forgotPasswordMessages.alreadyHaveCode,
|
||||
},
|
||||
title: messages.title,
|
||||
body: Body,
|
||||
footer: {
|
||||
color: 'blue',
|
||||
label: messages.sendNewEmail,
|
||||
},
|
||||
links: {
|
||||
label: forgotPasswordMessages.alreadyHaveCode,
|
||||
},
|
||||
});
|
||||
|
@ -8,33 +8,33 @@ import styles from './resendActivation.scss';
|
||||
import messages from './ResendActivation.intl.json';
|
||||
|
||||
export default class ResendActivation extends BaseAuthBody {
|
||||
static displayName = 'ResendActivation';
|
||||
static panelId = 'resendActivation';
|
||||
static hasGoBack = true;
|
||||
static displayName = 'ResendActivation';
|
||||
static panelId = 'resendActivation';
|
||||
static hasGoBack = true;
|
||||
|
||||
autoFocusField = 'email';
|
||||
autoFocusField = 'email';
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.renderErrors()}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.renderErrors()}
|
||||
|
||||
<div className={styles.description}>
|
||||
<Message {...messages.specifyYourEmail} />
|
||||
</div>
|
||||
<div className={styles.description}>
|
||||
<Message {...messages.specifyYourEmail} />
|
||||
</div>
|
||||
|
||||
<Input
|
||||
{...this.bindField('email')}
|
||||
icon="envelope"
|
||||
color="blue"
|
||||
type="email"
|
||||
required
|
||||
placeholder={registerMessages.yourEmail}
|
||||
defaultValue={this.context.user.email}
|
||||
/>
|
||||
<Input
|
||||
{...this.bindField('email')}
|
||||
icon="envelope"
|
||||
color="blue"
|
||||
type="email"
|
||||
required
|
||||
placeholder={registerMessages.yourEmail}
|
||||
defaultValue={this.context.user.email}
|
||||
/>
|
||||
|
||||
<Captcha {...this.bindField('captcha')} delay={600} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<Captcha {...this.bindField('captcha')} delay={600} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
@import '~app/components/ui/fonts.scss';
|
||||
|
||||
.description {
|
||||
font-family: $font-family-title;
|
||||
margin: 5px 0 19px;
|
||||
line-height: 1.4;
|
||||
font-size: 16px;
|
||||
font-family: $font-family-title;
|
||||
margin: 5px 0 19px;
|
||||
line-height: 1.4;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
@ -9,162 +9,143 @@ import { TestContextProvider } from 'app/shell';
|
||||
import { ContactForm } from './ContactForm';
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.stub(feedback, 'send').returns(Promise.resolve() as any);
|
||||
sinon.stub(feedback, 'send').returns(Promise.resolve() as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(feedback.send as any).restore();
|
||||
(feedback.send as any).restore();
|
||||
});
|
||||
|
||||
describe('ContactForm', () => {
|
||||
it('should contain Form', () => {
|
||||
const user = {} as User;
|
||||
it('should contain Form', () => {
|
||||
const user = {} as User;
|
||||
|
||||
render(
|
||||
<TestContextProvider>
|
||||
<ContactForm user={user} />
|
||||
</TestContextProvider>,
|
||||
);
|
||||
render(
|
||||
<TestContextProvider>
|
||||
<ContactForm user={user} />
|
||||
</TestContextProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getAllByRole('textbox').length, 'to be greater than', 1);
|
||||
expect(screen.getAllByRole('textbox').length, 'to be greater than', 1);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /Send/ }),
|
||||
'to have property',
|
||||
'type',
|
||||
'submit',
|
||||
);
|
||||
expect(screen.getByRole('button', { name: /Send/ }), 'to have property', 'type', 'submit');
|
||||
|
||||
[
|
||||
{
|
||||
label: 'subject',
|
||||
name: 'subject',
|
||||
},
|
||||
{
|
||||
label: 'E‑mail',
|
||||
name: 'email',
|
||||
},
|
||||
{
|
||||
label: 'message',
|
||||
name: 'message',
|
||||
},
|
||||
].forEach((el) => {
|
||||
expect(
|
||||
screen.getByLabelText(el.label, { exact: false }),
|
||||
'to have property',
|
||||
'name',
|
||||
el.name,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when rendered with user', () => {
|
||||
const user = {
|
||||
email: 'foo@bar.com',
|
||||
} as User;
|
||||
|
||||
it('should render email field with user email', () => {
|
||||
render(
|
||||
<TestContextProvider>
|
||||
<ContactForm user={user} />
|
||||
</TestContextProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByDisplayValue(user.email), 'to be a', HTMLInputElement);
|
||||
});
|
||||
});
|
||||
|
||||
it('should submit and then hide form and display success message', async () => {
|
||||
const user = {
|
||||
email: 'foo@bar.com',
|
||||
} as User;
|
||||
|
||||
render(
|
||||
<TestContextProvider>
|
||||
<ContactForm user={user} />
|
||||
</TestContextProvider>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/subject/i), {
|
||||
target: {
|
||||
value: 'subject',
|
||||
},
|
||||
[
|
||||
{
|
||||
label: 'subject',
|
||||
name: 'subject',
|
||||
},
|
||||
{
|
||||
label: 'E‑mail',
|
||||
name: 'email',
|
||||
},
|
||||
{
|
||||
label: 'message',
|
||||
name: 'message',
|
||||
},
|
||||
].forEach((el) => {
|
||||
expect(screen.getByLabelText(el.label, { exact: false }), 'to have property', 'name', el.name);
|
||||
});
|
||||
});
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message/i), {
|
||||
target: {
|
||||
value: 'the message',
|
||||
},
|
||||
describe('when rendered with user', () => {
|
||||
const user = {
|
||||
email: 'foo@bar.com',
|
||||
} as User;
|
||||
|
||||
it('should render email field with user email', () => {
|
||||
render(
|
||||
<TestContextProvider>
|
||||
<ContactForm user={user} />
|
||||
</TestContextProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByDisplayValue(user.email), 'to be a', HTMLInputElement);
|
||||
});
|
||||
});
|
||||
|
||||
const button = screen.getByRole('button', { name: 'Send' });
|
||||
it('should submit and then hide form and display success message', async () => {
|
||||
const user = {
|
||||
email: 'foo@bar.com',
|
||||
} as User;
|
||||
|
||||
expect(button, 'to have property', 'disabled', false);
|
||||
render(
|
||||
<TestContextProvider>
|
||||
<ContactForm user={user} />
|
||||
</TestContextProvider>,
|
||||
);
|
||||
|
||||
fireEvent.click(button);
|
||||
fireEvent.change(screen.getByLabelText(/subject/i), {
|
||||
target: {
|
||||
value: 'subject',
|
||||
},
|
||||
});
|
||||
|
||||
expect(button, 'to have property', 'disabled', true);
|
||||
expect(feedback.send, 'to have a call exhaustively satisfying', [
|
||||
{
|
||||
subject: 'subject',
|
||||
email: user.email,
|
||||
category: '',
|
||||
message: 'the message',
|
||||
},
|
||||
]);
|
||||
fireEvent.change(screen.getByLabelText(/message/i), {
|
||||
target: {
|
||||
value: 'the message',
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('Your message was received', { exact: false }),
|
||||
'to be a',
|
||||
HTMLElement,
|
||||
);
|
||||
const button = screen.getByRole('button', { name: 'Send' });
|
||||
|
||||
expect(button, 'to have property', 'disabled', false);
|
||||
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(button, 'to have property', 'disabled', true);
|
||||
expect(feedback.send, 'to have a call exhaustively satisfying', [
|
||||
{
|
||||
subject: 'subject',
|
||||
email: user.email,
|
||||
category: '',
|
||||
message: 'the message',
|
||||
},
|
||||
]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Your message was received', { exact: false }), 'to be a', HTMLElement);
|
||||
});
|
||||
|
||||
expect(screen.getByText(user.email), 'to be a', HTMLElement);
|
||||
|
||||
expect(screen.queryByRole('button', { name: /Send/ }), 'to be null');
|
||||
});
|
||||
|
||||
expect(screen.getByText(user.email), 'to be a', HTMLElement);
|
||||
it('should show validation messages', async () => {
|
||||
const user = {
|
||||
email: 'foo@bar.com',
|
||||
} as User;
|
||||
|
||||
expect(screen.queryByRole('button', { name: /Send/ }), 'to be null');
|
||||
});
|
||||
(feedback.send as any).callsFake(() =>
|
||||
Promise.reject({
|
||||
success: false,
|
||||
errors: { email: 'error.email_invalid' },
|
||||
}),
|
||||
);
|
||||
|
||||
it('should show validation messages', async () => {
|
||||
const user = {
|
||||
email: 'foo@bar.com',
|
||||
} as User;
|
||||
render(
|
||||
<TestContextProvider>
|
||||
<ContactForm user={user} />
|
||||
</TestContextProvider>,
|
||||
);
|
||||
|
||||
(feedback.send as any).callsFake(() =>
|
||||
Promise.reject({
|
||||
success: false,
|
||||
errors: { email: 'error.email_invalid' },
|
||||
}),
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText(/subject/i), {
|
||||
target: {
|
||||
value: 'subject',
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<TestContextProvider>
|
||||
<ContactForm user={user} />
|
||||
</TestContextProvider>,
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText(/message/i), {
|
||||
target: {
|
||||
value: 'the message',
|
||||
},
|
||||
});
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/subject/i), {
|
||||
target: {
|
||||
value: 'subject',
|
||||
},
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Send' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('alert'), 'to have property', 'innerHTML', 'E‑mail is invalid');
|
||||
});
|
||||
});
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/message/i), {
|
||||
target: {
|
||||
value: 'the message',
|
||||
},
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Send' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('alert'),
|
||||
'to have property',
|
||||
'innerHTML',
|
||||
'E‑mail is invalid',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -2,14 +2,7 @@ import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import clsx from 'clsx';
|
||||
import { FormattedMessage as Message } from 'react-intl';
|
||||
import {
|
||||
Input,
|
||||
TextArea,
|
||||
Button,
|
||||
Form,
|
||||
FormModel,
|
||||
Dropdown,
|
||||
} from 'app/components/ui/form';
|
||||
import { Input, TextArea, Button, Form, FormModel, Dropdown } from 'app/components/ui/form';
|
||||
import feedback from 'app/services/api/feedback';
|
||||
import icons from 'app/components/ui/icons.scss';
|
||||
import popupStyles from 'app/components/ui/popup/popup.scss';
|
||||
@ -21,190 +14,170 @@ import styles from './contactForm.scss';
|
||||
import messages from './contactForm.intl.json';
|
||||
|
||||
const CONTACT_CATEGORIES = {
|
||||
// TODO: сюда позже проставить реальные id категорий с backend
|
||||
0: <Message {...messages.cannotAccessMyAccount} />,
|
||||
1: <Message {...messages.foundBugOnSite} />,
|
||||
2: <Message {...messages.improvementsSuggestion} />,
|
||||
3: <Message {...messages.integrationQuestion} />,
|
||||
4: <Message {...messages.other} />,
|
||||
// TODO: сюда позже проставить реальные id категорий с backend
|
||||
0: <Message {...messages.cannotAccessMyAccount} />,
|
||||
1: <Message {...messages.foundBugOnSite} />,
|
||||
2: <Message {...messages.improvementsSuggestion} />,
|
||||
3: <Message {...messages.integrationQuestion} />,
|
||||
4: <Message {...messages.other} />,
|
||||
};
|
||||
|
||||
export class ContactForm extends React.Component<
|
||||
{
|
||||
onClose: () => void;
|
||||
user: User;
|
||||
},
|
||||
{
|
||||
isLoading: boolean;
|
||||
isSuccessfullySent: boolean;
|
||||
lastEmail: string | null;
|
||||
}
|
||||
{
|
||||
onClose: () => void;
|
||||
user: User;
|
||||
},
|
||||
{
|
||||
isLoading: boolean;
|
||||
isSuccessfullySent: boolean;
|
||||
lastEmail: string | null;
|
||||
}
|
||||
> {
|
||||
static defaultProps = {
|
||||
onClose() {},
|
||||
};
|
||||
static defaultProps = {
|
||||
onClose() {},
|
||||
};
|
||||
|
||||
state = {
|
||||
isLoading: false,
|
||||
isSuccessfullySent: false,
|
||||
lastEmail: null,
|
||||
};
|
||||
state = {
|
||||
isLoading: false,
|
||||
isSuccessfullySent: false,
|
||||
lastEmail: null,
|
||||
};
|
||||
|
||||
form = new FormModel();
|
||||
form = new FormModel();
|
||||
|
||||
render() {
|
||||
const { isSuccessfullySent } = this.state || {};
|
||||
const { onClose } = this.props;
|
||||
render() {
|
||||
const { isSuccessfullySent } = this.state || {};
|
||||
const { onClose } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="feedbackPopup"
|
||||
className={
|
||||
isSuccessfullySent ? styles.successState : styles.contactForm
|
||||
}
|
||||
>
|
||||
<div className={popupStyles.popup}>
|
||||
<div className={popupStyles.header}>
|
||||
<h2 className={popupStyles.headerTitle}>
|
||||
<Message {...messages.title} />
|
||||
</h2>
|
||||
<span
|
||||
className={clsx(icons.close, popupStyles.close)}
|
||||
onClick={onClose}
|
||||
data-testid="feedback-popup-close"
|
||||
/>
|
||||
</div>
|
||||
return (
|
||||
<div data-testid="feedbackPopup" className={isSuccessfullySent ? styles.successState : styles.contactForm}>
|
||||
<div className={popupStyles.popup}>
|
||||
<div className={popupStyles.header}>
|
||||
<h2 className={popupStyles.headerTitle}>
|
||||
<Message {...messages.title} />
|
||||
</h2>
|
||||
<span
|
||||
className={clsx(icons.close, popupStyles.close)}
|
||||
onClick={onClose}
|
||||
data-testid="feedback-popup-close"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isSuccessfullySent ? this.renderSuccess() : this.renderForm()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderForm() {
|
||||
const { form } = this;
|
||||
const { user } = this.props;
|
||||
const { isLoading } = this.state;
|
||||
|
||||
return (
|
||||
<Form form={form} onSubmit={this.onSubmit} isLoading={isLoading}>
|
||||
<div className={popupStyles.body}>
|
||||
<div className={styles.philosophicalThought}>
|
||||
<Message {...messages.philosophicalThought} />
|
||||
</div>
|
||||
|
||||
<div className={styles.formDisclaimer}>
|
||||
<Message {...messages.disclaimer} />
|
||||
<br />
|
||||
</div>
|
||||
|
||||
<div className={styles.pairInputRow}>
|
||||
<div className={styles.pairInput}>
|
||||
<Input
|
||||
{...form.bindField('subject')}
|
||||
required
|
||||
label={messages.subject}
|
||||
skin="light"
|
||||
/>
|
||||
{isSuccessfullySent ? this.renderSuccess() : this.renderForm()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.pairInput}>
|
||||
<Input
|
||||
{...form.bindField('email')}
|
||||
required
|
||||
label={messages.email}
|
||||
type="email"
|
||||
skin="light"
|
||||
defaultValue={user.email}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formMargin}>
|
||||
<Dropdown
|
||||
{...form.bindField('category')}
|
||||
label={messages.whichQuestion}
|
||||
items={CONTACT_CATEGORIES}
|
||||
block
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TextArea
|
||||
{...form.bindField('message')}
|
||||
required
|
||||
label={messages.message}
|
||||
skin="light"
|
||||
minRows={6}
|
||||
maxRows={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
label={messages.send}
|
||||
block
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
renderSuccess() {
|
||||
const { lastEmail: email } = this.state;
|
||||
const { onClose } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.successBody}>
|
||||
<span className={styles.successIcon} />
|
||||
<div className={styles.successDescription}>
|
||||
<Message {...messages.youMessageReceived} />
|
||||
</div>
|
||||
<div className={styles.sentToEmail}>{email}</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
label={messages.close}
|
||||
block
|
||||
onClick={onClose}
|
||||
data-testid="feedback-popup-close-button"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onSubmit = (): Promise<void> => {
|
||||
if (this.state.isLoading) {
|
||||
return Promise.resolve();
|
||||
);
|
||||
}
|
||||
|
||||
this.setState({ isLoading: true });
|
||||
renderForm() {
|
||||
const { form } = this;
|
||||
const { user } = this.props;
|
||||
const { isLoading } = this.state;
|
||||
|
||||
return feedback
|
||||
.send(this.form.serialize())
|
||||
.then(() =>
|
||||
this.setState({
|
||||
isSuccessfullySent: true,
|
||||
lastEmail: this.form.value('email'),
|
||||
}),
|
||||
)
|
||||
.catch((resp) => {
|
||||
if (resp.errors) {
|
||||
this.form.setErrors(resp.errors);
|
||||
return (
|
||||
<Form form={form} onSubmit={this.onSubmit} isLoading={isLoading}>
|
||||
<div className={popupStyles.body}>
|
||||
<div className={styles.philosophicalThought}>
|
||||
<Message {...messages.philosophicalThought} />
|
||||
</div>
|
||||
|
||||
return;
|
||||
<div className={styles.formDisclaimer}>
|
||||
<Message {...messages.disclaimer} />
|
||||
<br />
|
||||
</div>
|
||||
|
||||
<div className={styles.pairInputRow}>
|
||||
<div className={styles.pairInput}>
|
||||
<Input {...form.bindField('subject')} required label={messages.subject} skin="light" />
|
||||
</div>
|
||||
|
||||
<div className={styles.pairInput}>
|
||||
<Input
|
||||
{...form.bindField('email')}
|
||||
required
|
||||
label={messages.email}
|
||||
type="email"
|
||||
skin="light"
|
||||
defaultValue={user.email}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formMargin}>
|
||||
<Dropdown
|
||||
{...form.bindField('category')}
|
||||
label={messages.whichQuestion}
|
||||
items={CONTACT_CATEGORIES}
|
||||
block
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TextArea
|
||||
{...form.bindField('message')}
|
||||
required
|
||||
label={messages.message}
|
||||
skin="light"
|
||||
minRows={6}
|
||||
maxRows={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.footer}>
|
||||
<Button label={messages.send} block type="submit" disabled={isLoading} />
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
renderSuccess() {
|
||||
const { lastEmail: email } = this.state;
|
||||
const { onClose } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.successBody}>
|
||||
<span className={styles.successIcon} />
|
||||
<div className={styles.successDescription}>
|
||||
<Message {...messages.youMessageReceived} />
|
||||
</div>
|
||||
<div className={styles.sentToEmail}>{email}</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.footer}>
|
||||
<Button label={messages.close} block onClick={onClose} data-testid="feedback-popup-close-button" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onSubmit = (): Promise<void> => {
|
||||
if (this.state.isLoading) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
logger.warn('Error sending feedback', resp);
|
||||
})
|
||||
.finally(() => this.setState({ isLoading: false }));
|
||||
};
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
return feedback
|
||||
.send(this.form.serialize())
|
||||
.then(() =>
|
||||
this.setState({
|
||||
isSuccessfullySent: true,
|
||||
lastEmail: this.form.value('email'),
|
||||
}),
|
||||
)
|
||||
.catch((resp) => {
|
||||
if (resp.errors) {
|
||||
this.form.setErrors(resp.errors);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
logger.warn('Error sending feedback', resp);
|
||||
})
|
||||
.finally(() => this.setState({ isLoading: false }));
|
||||
};
|
||||
}
|
||||
|
||||
export default connect((state: RootState) => ({
|
||||
user: state.user,
|
||||
user: state.user,
|
||||
}))(ContactForm);
|
||||
|
@ -4,24 +4,24 @@ import { create as createPopup } from 'app/components/ui/popup/actions';
|
||||
import ContactForm from './ContactForm';
|
||||
|
||||
type Props = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
|
||||
createContactPopup: () => void;
|
||||
createContactPopup: () => void;
|
||||
};
|
||||
|
||||
function ContactLink({ createContactPopup, ...props }: Props) {
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
data-e2e-button="feedbackPopup"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
data-e2e-button="feedbackPopup"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
createContactPopup();
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
createContactPopup();
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(null, {
|
||||
createContactPopup: () => createPopup({ Popup: ContactForm }),
|
||||
createContactPopup: () => createPopup({ Popup: ContactForm }),
|
||||
})(ContactLink);
|
||||
|
@ -1,19 +1,19 @@
|
||||
{
|
||||
"title": "Feedback form",
|
||||
"subject": "Subject",
|
||||
"email": "E‑mail",
|
||||
"message": "Message",
|
||||
"send": "Send",
|
||||
"philosophicalThought": "Properly formulated question — half of the answer",
|
||||
"disclaimer": "Please formulate your feedback providing as much useful information, as possible to help us understand your problem and solve it",
|
||||
"whichQuestion": "What are you interested in?",
|
||||
"title": "Feedback form",
|
||||
"subject": "Subject",
|
||||
"email": "E‑mail",
|
||||
"message": "Message",
|
||||
"send": "Send",
|
||||
"philosophicalThought": "Properly formulated question — half of the answer",
|
||||
"disclaimer": "Please formulate your feedback providing as much useful information, as possible to help us understand your problem and solve it",
|
||||
"whichQuestion": "What are you interested in?",
|
||||
|
||||
"cannotAccessMyAccount": "Can not access my account",
|
||||
"foundBugOnSite": "I found a bug on the site",
|
||||
"improvementsSuggestion": "I have a suggestion for improving the functional",
|
||||
"integrationQuestion": "Service integration question",
|
||||
"other": "Other",
|
||||
"cannotAccessMyAccount": "Can not access my account",
|
||||
"foundBugOnSite": "I found a bug on the site",
|
||||
"improvementsSuggestion": "I have a suggestion for improving the functional",
|
||||
"integrationQuestion": "Service integration question",
|
||||
"other": "Other",
|
||||
|
||||
"youMessageReceived": "Your message was received. We will respond to you shortly. The answer will come to your E‑mail:",
|
||||
"close": "Close"
|
||||
"youMessageReceived": "Your message was received. We will respond to you shortly. The answer will come to your E‑mail:",
|
||||
"close": "Close"
|
||||
}
|
||||
|
@ -5,81 +5,81 @@
|
||||
/* Form state */
|
||||
|
||||
.contactForm {
|
||||
composes: popupWrapper from '~app/components/ui/popup/popup.scss';
|
||||
composes: popupWrapper from '~app/components/ui/popup/popup.scss';
|
||||
|
||||
@include popupBounding(500px);
|
||||
@include popupBounding(500px);
|
||||
}
|
||||
|
||||
.philosophicalThought {
|
||||
font-family: $font-family-title;
|
||||
font-size: 19px;
|
||||
color: $green;
|
||||
text-align: center;
|
||||
margin-bottom: 5px;
|
||||
font-family: $font-family-title;
|
||||
font-size: 19px;
|
||||
color: $green;
|
||||
text-align: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.formDisclaimer {
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
margin: 0 auto 10px;
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
margin: 0 auto 10px;
|
||||
}
|
||||
|
||||
.pairInputRow {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.pairInput {
|
||||
width: 50%;
|
||||
width: 50%;
|
||||
|
||||
&:first-of-type {
|
||||
margin-right: $popupPadding;
|
||||
}
|
||||
&:first-of-type {
|
||||
margin-right: $popupPadding;
|
||||
}
|
||||
}
|
||||
|
||||
.formMargin {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Success State */
|
||||
|
||||
.successState {
|
||||
composes: popupWrapper from '~app/components/ui/popup/popup.scss';
|
||||
composes: popupWrapper from '~app/components/ui/popup/popup.scss';
|
||||
|
||||
@include popupBounding(320px);
|
||||
@include popupBounding(320px);
|
||||
}
|
||||
|
||||
.successBody {
|
||||
composes: body from '~app/components/ui/popup/popup.scss';
|
||||
composes: body from '~app/components/ui/popup/popup.scss';
|
||||
|
||||
text-align: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.successDescription {
|
||||
@extend .formDisclaimer;
|
||||
@extend .formDisclaimer;
|
||||
|
||||
margin-bottom: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.successIcon {
|
||||
composes: checkmark from '~app/components/ui/icons.scss';
|
||||
composes: checkmark from '~app/components/ui/icons.scss';
|
||||
|
||||
font-size: 90px;
|
||||
color: #aaa;
|
||||
margin-bottom: 20px;
|
||||
line-height: 71px;
|
||||
font-size: 90px;
|
||||
color: #aaa;
|
||||
margin-bottom: 20px;
|
||||
line-height: 71px;
|
||||
}
|
||||
|
||||
.sentToEmail {
|
||||
font-family: $font-family-title;
|
||||
color: #444;
|
||||
font-size: 18px;
|
||||
font-family: $font-family-title;
|
||||
color: #444;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* Common */
|
||||
|
||||
.footer {
|
||||
margin-top: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
@ -1,26 +1,26 @@
|
||||
{
|
||||
"accountsForDevelopers": "Ely.by Accounts for developers",
|
||||
"accountsAllowsYouYoUseOauth2": "Ely.by Accounts service provides users with a quick and easy-to-use way to login to your site, launcher or Minecraft server via OAuth2 authorization protocol. You can find more information about integration with Ely.by Accounts in {ourDocumentation}.",
|
||||
"ourDocumentation": "our documentation",
|
||||
"ifYouHaveAnyTroubles": "If you are experiencing difficulties, you can always use {feedback}. We'll surely help you.",
|
||||
"feedback": "feedback",
|
||||
"weDontKnowAnythingAboutYou": "We don't know anything about you yet.",
|
||||
"youMustAuthToBegin": "You have to authorize to start.",
|
||||
"authorization": "Authorization",
|
||||
"youDontHaveAnyApplication": "You don't have any app registered yet.",
|
||||
"shallWeStart": "Shall we start?",
|
||||
"addNew": "Add new",
|
||||
"yourApplications": "Your applications:",
|
||||
"countUsers": "{count, plural, =0 {No users} one {# user} other {# users}}",
|
||||
"ifYouSuspectingThatSecretHasBeenCompromised": "If you are suspecting that your Client Secret has been compromised, then you may want to reset it value. It'll cause recall of the all \"access\" and \"refresh\" tokens that have been issued. You can also recall all issued tokens without changing Client Secret.",
|
||||
"revokeAllTokens": "Revoke all tokens",
|
||||
"resetClientSecret": "Reset Client Secret",
|
||||
"delete": "Delete",
|
||||
"editDescription": "{icon} Edit description",
|
||||
"allRefreshTokensWillBecomeInvalid": "All \"refresh\" tokens will become invalid and after next authorization the user will get permissions prompt.",
|
||||
"appAndAllTokenWillBeDeleted": "Application and all associated tokens will be deleted.",
|
||||
"takeCareAccessTokensInvalidation": "Take care because \"access\" tokens won't be invalidated immediately.",
|
||||
"cancel": "Cancel",
|
||||
"continue": "Continue",
|
||||
"performing": "Performing…"
|
||||
"accountsForDevelopers": "Ely.by Accounts for developers",
|
||||
"accountsAllowsYouYoUseOauth2": "Ely.by Accounts service provides users with a quick and easy-to-use way to login to your site, launcher or Minecraft server via OAuth2 authorization protocol. You can find more information about integration with Ely.by Accounts in {ourDocumentation}.",
|
||||
"ourDocumentation": "our documentation",
|
||||
"ifYouHaveAnyTroubles": "If you are experiencing difficulties, you can always use {feedback}. We'll surely help you.",
|
||||
"feedback": "feedback",
|
||||
"weDontKnowAnythingAboutYou": "We don't know anything about you yet.",
|
||||
"youMustAuthToBegin": "You have to authorize to start.",
|
||||
"authorization": "Authorization",
|
||||
"youDontHaveAnyApplication": "You don't have any app registered yet.",
|
||||
"shallWeStart": "Shall we start?",
|
||||
"addNew": "Add new",
|
||||
"yourApplications": "Your applications:",
|
||||
"countUsers": "{count, plural, =0 {No users} one {# user} other {# users}}",
|
||||
"ifYouSuspectingThatSecretHasBeenCompromised": "If you are suspecting that your Client Secret has been compromised, then you may want to reset it value. It'll cause recall of the all \"access\" and \"refresh\" tokens that have been issued. You can also recall all issued tokens without changing Client Secret.",
|
||||
"revokeAllTokens": "Revoke all tokens",
|
||||
"resetClientSecret": "Reset Client Secret",
|
||||
"delete": "Delete",
|
||||
"editDescription": "{icon} Edit description",
|
||||
"allRefreshTokensWillBecomeInvalid": "All \"refresh\" tokens will become invalid and after next authorization the user will get permissions prompt.",
|
||||
"appAndAllTokenWillBeDeleted": "Application and all associated tokens will be deleted.",
|
||||
"takeCareAccessTokensInvalidation": "Take care because \"access\" tokens won't be invalidated immediately.",
|
||||
"cancel": "Cancel",
|
||||
"continue": "Continue",
|
||||
"performing": "Performing…"
|
||||
}
|
||||
|
@ -15,144 +15,133 @@ import toolsIcon from './icons/tools.svg';
|
||||
import ApplicationsList from './list';
|
||||
|
||||
type Props = {
|
||||
clientId: string | null;
|
||||
resetClientId: () => void; // notify parent to remove clientId from current location.href
|
||||
displayForGuest: boolean;
|
||||
applications: Array<OauthAppResponse>;
|
||||
isLoading: boolean;
|
||||
deleteApp: (clientId: string) => Promise<any>;
|
||||
resetApp: (clientId: string, resetClientSecret: boolean) => Promise<any>;
|
||||
clientId: string | null;
|
||||
resetClientId: () => void; // notify parent to remove clientId from current location.href
|
||||
displayForGuest: boolean;
|
||||
applications: Array<OauthAppResponse>;
|
||||
isLoading: boolean;
|
||||
deleteApp: (clientId: string) => Promise<any>;
|
||||
resetApp: (clientId: string, resetClientSecret: boolean) => Promise<any>;
|
||||
};
|
||||
|
||||
export default class ApplicationsIndex extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.welcomeContainer}>
|
||||
<Message {...messages.accountsForDevelopers}>
|
||||
{(pageTitle: string) => (
|
||||
<h2 className={styles.welcomeTitle}>
|
||||
<Helmet title={pageTitle} />
|
||||
{pageTitle}
|
||||
</h2>
|
||||
)}
|
||||
</Message>
|
||||
<div className={styles.welcomeTitleDelimiter} />
|
||||
<div className={styles.welcomeParagraph}>
|
||||
<Message
|
||||
{...messages.accountsAllowsYouYoUseOauth2}
|
||||
values={{
|
||||
ourDocumentation: (
|
||||
<a href="https://docs.ely.by/en/oauth.html" target="_blank">
|
||||
<Message {...messages.ourDocumentation} />
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.welcomeParagraph}>
|
||||
<Message
|
||||
{...messages.ifYouHaveAnyTroubles}
|
||||
values={{
|
||||
feedback: (
|
||||
<ContactLink>
|
||||
<Message {...messages.feedback} />
|
||||
</ContactLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.welcomeContainer}>
|
||||
<Message {...messages.accountsForDevelopers}>
|
||||
{(pageTitle: string) => (
|
||||
<h2 className={styles.welcomeTitle}>
|
||||
<Helmet title={pageTitle} />
|
||||
{pageTitle}
|
||||
</h2>
|
||||
)}
|
||||
</Message>
|
||||
<div className={styles.welcomeTitleDelimiter} />
|
||||
<div className={styles.welcomeParagraph}>
|
||||
<Message
|
||||
{...messages.accountsAllowsYouYoUseOauth2}
|
||||
values={{
|
||||
ourDocumentation: (
|
||||
<a href="https://docs.ely.by/en/oauth.html" target="_blank">
|
||||
<Message {...messages.ourDocumentation} />
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.welcomeParagraph}>
|
||||
<Message
|
||||
{...messages.ifYouHaveAnyTroubles}
|
||||
values={{
|
||||
feedback: (
|
||||
<ContactLink>
|
||||
<Message {...messages.feedback} />
|
||||
</ContactLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.getContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getContent() {
|
||||
const {
|
||||
displayForGuest,
|
||||
applications,
|
||||
isLoading,
|
||||
resetApp,
|
||||
deleteApp,
|
||||
clientId,
|
||||
resetClientId,
|
||||
} = this.props;
|
||||
|
||||
if (applications.length > 0) {
|
||||
return (
|
||||
<ApplicationsList
|
||||
applications={applications}
|
||||
resetApp={resetApp}
|
||||
deleteApp={deleteApp}
|
||||
clientId={clientId}
|
||||
resetClientId={resetClientId}
|
||||
/>
|
||||
);
|
||||
{this.getContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (displayForGuest) {
|
||||
return <Guest />;
|
||||
}
|
||||
getContent() {
|
||||
const { displayForGuest, applications, isLoading, resetApp, deleteApp, clientId, resetClientId } = this.props;
|
||||
|
||||
return <Loader noApps={!isLoading} />;
|
||||
}
|
||||
if (applications.length > 0) {
|
||||
return (
|
||||
<ApplicationsList
|
||||
applications={applications}
|
||||
resetApp={resetApp}
|
||||
deleteApp={deleteApp}
|
||||
clientId={clientId}
|
||||
resetClientId={resetClientId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (displayForGuest) {
|
||||
return <Guest />;
|
||||
}
|
||||
|
||||
return <Loader noApps={!isLoading} />;
|
||||
}
|
||||
}
|
||||
|
||||
function Loader({ noApps }: { noApps: boolean }) {
|
||||
return (
|
||||
<div className={styles.emptyState} data-e2e={noApps ? 'noApps' : 'loading'}>
|
||||
<img
|
||||
src={noApps ? cubeIcon : loadingCubeIcon}
|
||||
className={styles.emptyStateIcon}
|
||||
/>
|
||||
return (
|
||||
<div className={styles.emptyState} data-e2e={noApps ? 'noApps' : 'loading'}>
|
||||
<img src={noApps ? cubeIcon : loadingCubeIcon} className={styles.emptyStateIcon} />
|
||||
|
||||
<div
|
||||
className={clsx(styles.noAppsContainer, {
|
||||
[styles.noAppsAnimating]: noApps,
|
||||
})}
|
||||
>
|
||||
<div className={styles.emptyStateText}>
|
||||
<div>
|
||||
<Message {...messages.youDontHaveAnyApplication} />
|
||||
</div>
|
||||
<div>
|
||||
<Message {...messages.shallWeStart} />
|
||||
</div>
|
||||
<div
|
||||
className={clsx(styles.noAppsContainer, {
|
||||
[styles.noAppsAnimating]: noApps,
|
||||
})}
|
||||
>
|
||||
<div className={styles.emptyStateText}>
|
||||
<div>
|
||||
<Message {...messages.youDontHaveAnyApplication} />
|
||||
</div>
|
||||
<div>
|
||||
<Message {...messages.shallWeStart} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LinkButton
|
||||
to="/dev/applications/new"
|
||||
data-e2e="newApp"
|
||||
label={messages.addNew}
|
||||
color={COLOR_GREEN}
|
||||
className={styles.emptyStateActionButton}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LinkButton
|
||||
to="/dev/applications/new"
|
||||
data-e2e="newApp"
|
||||
label={messages.addNew}
|
||||
color={COLOR_GREEN}
|
||||
className={styles.emptyStateActionButton}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
function Guest() {
|
||||
return (
|
||||
<div className={styles.emptyState}>
|
||||
<img src={toolsIcon} className={styles.emptyStateIcon} />
|
||||
<div className={styles.emptyStateText}>
|
||||
<div>
|
||||
<Message {...messages.weDontKnowAnythingAboutYou} />
|
||||
</div>
|
||||
<div>
|
||||
<Message {...messages.youMustAuthToBegin} />
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div className={styles.emptyState}>
|
||||
<img src={toolsIcon} className={styles.emptyStateIcon} />
|
||||
<div className={styles.emptyStateText}>
|
||||
<div>
|
||||
<Message {...messages.weDontKnowAnythingAboutYou} />
|
||||
</div>
|
||||
<div>
|
||||
<Message {...messages.youMustAuthToBegin} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LinkButton
|
||||
to="/login"
|
||||
label={messages.authorization}
|
||||
color={COLOR_BLUE}
|
||||
className={styles.emptyStateActionButton}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
<LinkButton
|
||||
to="/login"
|
||||
label={messages.authorization}
|
||||
color={COLOR_BLUE}
|
||||
className={styles.emptyStateActionButton}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -7,94 +7,85 @@ import { ThunkAction } from 'app/reducers';
|
||||
import { Apps } from './reducer';
|
||||
|
||||
interface SetAvailableAction extends ReduxAction {
|
||||
type: 'apps:setAvailable';
|
||||
payload: Array<OauthAppResponse>;
|
||||
type: 'apps:setAvailable';
|
||||
payload: Array<OauthAppResponse>;
|
||||
}
|
||||
|
||||
export function setAppsList(apps: Array<OauthAppResponse>): SetAvailableAction {
|
||||
return {
|
||||
type: 'apps:setAvailable',
|
||||
payload: apps,
|
||||
};
|
||||
return {
|
||||
type: 'apps:setAvailable',
|
||||
payload: apps,
|
||||
};
|
||||
}
|
||||
|
||||
export function getApp(
|
||||
state: { apps: Apps },
|
||||
clientId: string,
|
||||
): OauthAppResponse | null {
|
||||
return state.apps.available.find((app) => app.clientId === clientId) || null;
|
||||
export function getApp(state: { apps: Apps }, clientId: string): OauthAppResponse | null {
|
||||
return state.apps.available.find((app) => app.clientId === clientId) || null;
|
||||
}
|
||||
|
||||
export function fetchApp(clientId: string): ThunkAction<Promise<void>> {
|
||||
return async (dispatch) => {
|
||||
const app = await oauth.getApp(clientId);
|
||||
return async (dispatch) => {
|
||||
const app = await oauth.getApp(clientId);
|
||||
|
||||
dispatch(addApp(app));
|
||||
};
|
||||
dispatch(addApp(app));
|
||||
};
|
||||
}
|
||||
|
||||
interface AddAppAction extends ReduxAction {
|
||||
type: 'apps:addApp';
|
||||
payload: OauthAppResponse;
|
||||
type: 'apps:addApp';
|
||||
payload: OauthAppResponse;
|
||||
}
|
||||
|
||||
function addApp(app: OauthAppResponse): AddAppAction {
|
||||
return {
|
||||
type: 'apps:addApp',
|
||||
payload: app,
|
||||
};
|
||||
return {
|
||||
type: 'apps:addApp',
|
||||
payload: app,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchAvailableApps() {
|
||||
return async (
|
||||
dispatch: Dispatch<any>,
|
||||
getState: () => { user: User },
|
||||
): Promise<void> => {
|
||||
const { id } = getState().user;
|
||||
return async (dispatch: Dispatch<any>, getState: () => { user: User }): Promise<void> => {
|
||||
const { id } = getState().user;
|
||||
|
||||
if (!id) {
|
||||
dispatch(setAppsList([]));
|
||||
if (!id) {
|
||||
dispatch(setAppsList([]));
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const apps = await oauth.getAppsByUser(id);
|
||||
const apps = await oauth.getAppsByUser(id);
|
||||
|
||||
dispatch(setAppsList(apps));
|
||||
};
|
||||
dispatch(setAppsList(apps));
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteApp(clientId: string) {
|
||||
return async (dispatch: Dispatch<any>): Promise<void> => {
|
||||
await oauth.delete(clientId);
|
||||
return async (dispatch: Dispatch<any>): Promise<void> => {
|
||||
await oauth.delete(clientId);
|
||||
|
||||
dispatch(createDeleteAppAction(clientId));
|
||||
};
|
||||
dispatch(createDeleteAppAction(clientId));
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteAppAction extends ReduxAction {
|
||||
type: 'apps:deleteApp';
|
||||
payload: string;
|
||||
type: 'apps:deleteApp';
|
||||
payload: string;
|
||||
}
|
||||
|
||||
function createDeleteAppAction(clientId: string): DeleteAppAction {
|
||||
return {
|
||||
type: 'apps:deleteApp',
|
||||
payload: clientId,
|
||||
};
|
||||
return {
|
||||
type: 'apps:deleteApp',
|
||||
payload: clientId,
|
||||
};
|
||||
}
|
||||
|
||||
export function resetApp(
|
||||
clientId: string,
|
||||
resetSecret: boolean,
|
||||
): ThunkAction<Promise<void>> {
|
||||
return async (dispatch) => {
|
||||
const { data: app } = await oauth.reset(clientId, resetSecret);
|
||||
export function resetApp(clientId: string, resetSecret: boolean): ThunkAction<Promise<void>> {
|
||||
return async (dispatch) => {
|
||||
const { data: app } = await oauth.reset(clientId, resetSecret);
|
||||
|
||||
if (resetSecret) {
|
||||
dispatch(addApp(app));
|
||||
}
|
||||
};
|
||||
if (resetSecret) {
|
||||
dispatch(addApp(app));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type Action = SetAvailableAction | DeleteAppAction | AddAppAction;
|
||||
|
@ -1,20 +1,20 @@
|
||||
{
|
||||
"creatingApplication": "Creating an application",
|
||||
"website": "Web site",
|
||||
"minecraftServer": "Minecraft server",
|
||||
"toDisplayRegistrationFormChooseType": "To display registration form for a new application choose necessary type.",
|
||||
"applicationName": "Application name:",
|
||||
"appDescriptionWillBeAlsoVisibleOnOauthPage": "Application's description will be displayed at the authorization page too. It isn't a required field. In authorization process the value may be overridden.",
|
||||
"description": "Description:",
|
||||
"websiteLinkWillBeUsedAsAdditionalId": "Site's link is optional, but it can be used as an additional identifier of the application.",
|
||||
"websiteLink": "Website link:",
|
||||
"redirectUriLimitsAllowableBaseAddress": "Redirection URI (redirectUri) determines a base address, that user will be allowed to be redirected to after authorization. In order to improve security it's better to use the whole path instead of just a domain name. For example: https://example.com/oauth/ely.",
|
||||
"redirectUri": "Redirect URI:",
|
||||
"createApplication": "Create application",
|
||||
"serverName": "Server name:",
|
||||
"ipAddressIsOptionButPreferable": "IP address is optional, but is very preferable. It might become handy in case of we suddenly decide to play on your server with the entire band (=",
|
||||
"serverIp": "Server IP:",
|
||||
"youCanAlsoSpecifyServerSite": "You also can specify either server's site URL or its community in a social network.",
|
||||
"updatingApplication": "Updating an application",
|
||||
"updateApplication": "Update application"
|
||||
"creatingApplication": "Creating an application",
|
||||
"website": "Web site",
|
||||
"minecraftServer": "Minecraft server",
|
||||
"toDisplayRegistrationFormChooseType": "To display registration form for a new application choose necessary type.",
|
||||
"applicationName": "Application name:",
|
||||
"appDescriptionWillBeAlsoVisibleOnOauthPage": "Application's description will be displayed at the authorization page too. It isn't a required field. In authorization process the value may be overridden.",
|
||||
"description": "Description:",
|
||||
"websiteLinkWillBeUsedAsAdditionalId": "Site's link is optional, but it can be used as an additional identifier of the application.",
|
||||
"websiteLink": "Website link:",
|
||||
"redirectUriLimitsAllowableBaseAddress": "Redirection URI (redirectUri) determines a base address, that user will be allowed to be redirected to after authorization. In order to improve security it's better to use the whole path instead of just a domain name. For example: https://example.com/oauth/ely.",
|
||||
"redirectUri": "Redirect URI:",
|
||||
"createApplication": "Create application",
|
||||
"serverName": "Server name:",
|
||||
"ipAddressIsOptionButPreferable": "IP address is optional, but is very preferable. It might become handy in case of we suddenly decide to play on your server with the entire band (=",
|
||||
"serverIp": "Server IP:",
|
||||
"youCanAlsoSpecifyServerSite": "You also can specify either server's site URL or its community in a social network.",
|
||||
"updatingApplication": "Updating an application",
|
||||
"updateApplication": "Update application"
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user