Merge remote-tracking branch 'origin/develop' into 22-oauth-app-managment

This commit is contained in:
SleepWalker 2018-05-02 21:55:34 +03:00
commit bd77abcdd8
8 changed files with 162 additions and 106 deletions

View File

@ -1,9 +1,8 @@
// @flow
import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { IntlProvider } from 'components/i18n';
import logger from 'services/logger';
import appInfo from 'components/auth/appInfo/AppInfo.intl.json';
import styles from './styles.scss';
@ -12,32 +11,64 @@ import messages from './BSoD.intl.json';
// TODO: probably it is better to render this view from the App view
// to remove dependencies from store and IntlProvider
export default function BSoD({store}: {store: *}) {
return (
<IntlProvider store={store}>
<div className={styles.body}>
<canvas className={styles.canvas}
ref={(el: ?HTMLCanvasElement) => el && new BoxesField(el)}
/>
export default class BSoD extends React.Component<{
store: Object
}, {
lastEventId?: string,
}> {
state = {};
<div className={styles.wrapper}>
<div className={styles.title}>
<Message {...appInfo.appName} />
</div>
<div className={styles.lineWithMargin}>
<Message {...messages.criticalErrorHappened} />
</div>
<div className={styles.line}>
<Message {...messages.reloadPageOrContactUs} />
</div>
<a href="mailto:support@ely.by" className={styles.support}>
support@ely.by
</a>
<div className={styles.easterEgg}>
<Message {...messages.alsoYouCanInteractWithBackground}/>
componentDidMount() {
// poll for event id
const timer = setInterval(() => {
if (!logger.getLastEventId()) {
return;
}
clearInterval(timer);
this.setState({
lastEventId: logger.getLastEventId()
});
}, 500);
}
render() {
const {store} = this.props;
const {lastEventId} = this.state;
let emailUrl = 'mailto:support@ely.by';
if (lastEventId) {
emailUrl += `?subject=Bug report for #${lastEventId}`;
}
return (
<IntlProvider store={store}>
<div className={styles.body}>
<canvas className={styles.canvas}
ref={(el: ?HTMLCanvasElement) => el && new BoxesField(el)}
/>
<div className={styles.wrapper}>
<div className={styles.title}>
<Message {...appInfo.appName} />
</div>
<div className={styles.lineWithMargin}>
<Message {...messages.criticalErrorHappened} />
</div>
<div className={styles.line}>
<Message {...messages.reloadPageOrContactUs} />
</div>
<a href={emailUrl} className={styles.support}>
support@ely.by
</a>
<div className={styles.easterEgg}>
<Message {...messages.alsoYouCanInteractWithBackground}/>
</div>
</div>
</div>
</div>
</IntlProvider>
);
</IntlProvider>
);
}
}

View File

@ -1,27 +1,24 @@
// @flow
import React from 'react';
import type { MessageDescriptor } from 'react-intl';
import type { ComponentType } from 'react';
import type { Color } from 'components/ui';
import React from 'react';
import classNames from 'classnames';
import buttons from 'components/ui/buttons.scss';
import { COLOR_GREEN } from 'components/ui';
import FormComponent from './FormComponent';
import type { Color } from 'components/ui';
import type { MessageDescriptor } from 'react-intl';
export default class Button extends FormComponent<{
label: string | MessageDescriptor,
block?: bool,
small?: bool,
loading?: bool,
className?: string,
color?: Color,
color: Color,
disabled?: bool,
component?: string | ComponentType<any>,
} | HTMLButtonElement> {
component: string | ComponentType<any>,
}> {
static defaultProps = {
color: COLOR_GREEN,
component: 'button',

View File

@ -1,35 +1,41 @@
import PropTypes from 'prop-types';
// @flow
import React from 'react';
import classNames from 'classnames';
import type { CaptchaID } from 'services/captcha';
import type { Skin } from 'components/ui';
import captcha from 'services/captcha';
import logger from 'services/logger';
import { skins, SKIN_DARK } from 'components/ui';
import { ComponentLoader } from 'components/ui/loader';
import styles from './form.scss';
import FormInputComponent from './FormInputComponent';
export default class Captcha extends FormInputComponent {
static displayName = 'Captcha';
static propTypes = {
skin: PropTypes.oneOf(skins),
delay: PropTypes.number
};
export default class Captcha extends FormInputComponent<{
delay: number,
skin: Skin,
}, {
code: string,
}> {
el: ?HTMLDivElement;
captchaId: CaptchaID;
static defaultProps = {
skin: SKIN_DARK,
skin: 'dark',
delay: 0
};
componentDidMount() {
setTimeout(() => {
captcha.render(this.el, {
this.el && captcha.render(this.el, {
skin: this.props.skin,
onSetCode: this.setCode
}).then((captchaId) => this.captchaId = captchaId, (error) => logger.error('Error rendering captcha', { error }));
})
.then((captchaId) => {this.captchaId = captchaId;})
.catch((error) => {
logger.error('Failed rendering captcha', {
error
});
});
}, this.props.delay);
}
@ -64,5 +70,5 @@ export default class Captcha extends FormInputComponent {
this.reset();
}
setCode = (code) => this.setState({code});
setCode = (code: string) => this.setState({code});
}

View File

@ -1,10 +1,9 @@
// @flow
import type { MessageDescriptor } from 'react-intl';
import { Component } from 'react';
import { intlShape } from 'react-intl';
export default class FormComponent extends Component {
static displayName = 'FormComponent';
export default class FormComponent<P, S = void> extends Component<P, S> {
static contextTypes = {
intl: intlShape.isRequired
};
@ -16,7 +15,7 @@ export default class FormComponent extends Component {
*
* @return {string}
*/
formatMessage(message) {
formatMessage(message: string | MessageDescriptor) {
if (message && message.id && this.context && this.context.intl) {
message = this.context.intl.formatMessage(message);
}

View File

@ -1,15 +1,18 @@
import PropTypes from 'prop-types';
// @flow
import type { MessageDescriptor } from 'react-intl';
import React from 'react';
import FormComponent from './FormComponent';
import FormError from './FormError';
export default class FormInputComponent extends FormComponent {
static displayName = 'FormInputComponent';
type Error = string | MessageDescriptor;
static propTypes = {
error: PropTypes.string
};
export default class FormInputComponent<P, S = void> extends FormComponent<P & {
error?: Error,
}, S & {
error?: Error,
}> {
el: ?HTMLDivElement;
componentWillReceiveProps() {
if (this.state && this.state.error) {
@ -19,7 +22,7 @@ export default class FormInputComponent extends FormComponent {
}
}
setEl = (el) => {
setEl = (el: ?HTMLDivElement) => {
this.el = el;
};
@ -29,7 +32,7 @@ export default class FormInputComponent extends FormComponent {
return <FormError error={error} />;
}
setError(error) {
setError(error: Error) {
this.setState({error});
}
}

View File

@ -32,7 +32,7 @@ export default class FormModel {
const props: Object = {
name,
ref: (el: ?FormInputComponent) => {
ref: (el: ?FormInputComponent<any>) => {
if (el) {
if (!(el instanceof FormInputComponent)) {
throw new Error('Expected FormInputComponent component');

View File

@ -1,3 +1,4 @@
// @flow
import { loadScript } from 'functions';
import options from 'services/api/options';
@ -5,6 +6,8 @@ let readyPromise;
let lang = 'en';
let sitekey;
export opaque type CaptchaID = string;
export default {
/**
* @param {DOMNode|string} el - dom node or id of element where to render captcha
@ -14,7 +17,10 @@ export default {
*
* @return {Promise} - resolves to captchaId
*/
render(el, {skin: theme, onSetCode: callback}) {
render(el: HTMLElement, {skin: theme, onSetCode: callback}: {
skin: 'dark' | 'light',
onSetCode: (string) => void,
}): Promise<CaptchaID> {
return this.loadApi().then(() =>
window.grecaptcha.render(el, {
sitekey,
@ -27,7 +33,7 @@ export default {
/**
* @param {string} captchaId - captcha id, returned from render promise
*/
reset(captchaId) {
reset(captchaId: CaptchaID) {
this.loadApi().then(() => window.grecaptcha.reset(captchaId));
},
@ -36,7 +42,7 @@ export default {
*
* @see https://developers.google.com/recaptcha/docs/language
*/
setLang(newLang) {
setLang(newLang: string) {
lang = newLang;
},
@ -45,7 +51,7 @@ export default {
*
* @see http://www.google.com/recaptcha/admin
*/
setApiKey(apiKey) {
setApiKey(apiKey: string) {
sitekey = apiKey;
},
@ -54,14 +60,14 @@ export default {
*
* @return {Promise}
*/
loadApi() {
loadApi(): Promise<void> {
if (!readyPromise) {
readyPromise = Promise.all([
new Promise((resolve) => {
window.onReCaptchaReady = resolve;
}),
options.get().then((resp) => this.setApiKey(resp.reCaptchaPublicKey))
]);
]).then(() => {});
loadScript(`https://recaptcha.net/recaptcha/api.js?onload=onReCaptchaReady&render=explicit&hl=${lang}`);
}
@ -69,4 +75,3 @@ export default {
return readyPromise;
}
};

View File

@ -1,3 +1,5 @@
// @flow
import type {User} from 'components/user';
import Raven from 'raven-js';
import abbreviate from './abbreviate';
@ -5,8 +7,8 @@ import abbreviate from './abbreviate';
const isTest = process.env.__TEST__; // eslint-disable-line
const isProduction = process.env.__PROD__; // eslint-disable-line
const logger = {
init({sentryCdn}) {
class Logger {
init({ sentryCdn }: { sentryCdn: string }) {
if (sentryCdn) {
Raven.config(sentryCdn, {
logger: 'accounts-js-app',
@ -37,54 +39,67 @@ const logger = {
message = '';
}
logger.info(`Unhandled rejection${message}`, {
this.info(`Unhandled rejection${message}`, {
error,
event
});
});
}
},
}
setUser(user) {
setUser(user: User) {
Raven.setUserContext({
username: user.username,
email: user.email,
id: user.id
});
}
};
[
// 'fatal',
'error',
'warning',
'info',
'debug'
].forEach((level) => {
const method = level === 'warning' ? 'warn' : level;
error(message: string | Error, context: Object) {
log('error', message, context);
}
logger[method] = (message, context) => {
if (isTest) {
return;
}
info(message: string | Error, context: Object) {
log('info', message, context);
}
if (typeof context !== 'object') {
// it would better to always have an object here
context = {
message: context
};
}
warn(message: string | Error, context: Object) {
log('warning', message, context);
}
prepareContext(context).then((context) => {
console[method](message, context); // eslint-disable-line
getLastEventId(): string | void {
return Raven.lastEventId();
}
}
Raven.captureException(message, {
level,
extra: context
});
function log(
level: 'error' | 'warning' | 'info' | 'debug',
message: string | Error,
context: Object
) {
const method: 'error' | 'warn' | 'info' | 'debug' = level === 'warning' ? 'warn' : level;
if (isTest) {
return;
}
if (typeof context !== 'object') {
// it would better to always have an object here
context = {
message: context
};
}
prepareContext(context).then((context) => {
console[method](message, context); // eslint-disable-line
Raven.captureException(message, {
level,
extra: context,
...(typeof message === 'string' ? { fingerprint: [message] } : {}),
});
};
});
});
}
/**
* prepare data for JSON.stringify
@ -93,7 +108,7 @@ const logger = {
*
* @return {Promise}
*/
function prepareContext(context) {
function prepareContext(context: any) {
if (context instanceof Response) {
// TODO: rewrite abbreviate to use promises and recursively find Response
return context.json()
@ -120,4 +135,4 @@ function prepareContext(context) {
return Promise.resolve(abbreviate(context));
}
export default logger;
export default new Logger();