diff --git a/src/components/ui/bsod/BSoD.js b/src/components/ui/bsod/BSoD.js index 3eac3d5..7113159 100644 --- a/src/components/ui/bsod/BSoD.js +++ b/src/components/ui/bsod/BSoD.js @@ -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 ( - -
- el && new BoxesField(el)} - /> +export default class BSoD extends React.Component<{ + store: Object +}, { + lastEventId?: string, +}> { + state = {}; -
-
- -
-
- -
-
- -
- - support@ely.by - -
- + 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 ( + +
+ el && new BoxesField(el)} + /> + +
+
+ +
+
+ +
+
+ +
+ + support@ely.by + +
+ +
-
- - ); + + ); + } } diff --git a/src/components/ui/form/Button.js b/src/components/ui/form/Button.js index 0a8bbe9..1dc4271 100644 --- a/src/components/ui/form/Button.js +++ b/src/components/ui/form/Button.js @@ -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, -} | HTMLButtonElement> { + component: string | ComponentType, +}> { static defaultProps = { color: COLOR_GREEN, component: 'button', diff --git a/src/components/ui/form/Captcha.js b/src/components/ui/form/Captcha.js index e7717a8..2cad790 100644 --- a/src/components/ui/form/Captcha.js +++ b/src/components/ui/form/Captcha.js @@ -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}); } diff --git a/src/components/ui/form/FormComponent.js b/src/components/ui/form/FormComponent.js index 2a77f3c..661d294 100644 --- a/src/components/ui/form/FormComponent.js +++ b/src/components/ui/form/FormComponent.js @@ -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 extends Component { 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); } diff --git a/src/components/ui/form/FormInputComponent.js b/src/components/ui/form/FormInputComponent.js index 515c91e..1e48bdd 100644 --- a/src/components/ui/form/FormInputComponent.js +++ b/src/components/ui/form/FormInputComponent.js @@ -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 extends FormComponent

{ + 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 ; } - setError(error) { + setError(error: Error) { this.setState({error}); } } diff --git a/src/components/ui/form/FormModel.js b/src/components/ui/form/FormModel.js index 8a92b42..955787e 100644 --- a/src/components/ui/form/FormModel.js +++ b/src/components/ui/form/FormModel.js @@ -32,7 +32,7 @@ export default class FormModel { const props: Object = { name, - ref: (el: ?FormInputComponent) => { + ref: (el: ?FormInputComponent) => { if (el) { if (!(el instanceof FormInputComponent)) { throw new Error('Expected FormInputComponent component'); diff --git a/src/services/captcha.js b/src/services/captcha.js index b823809..bed1492 100644 --- a/src/services/captcha.js +++ b/src/services/captcha.js @@ -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 { 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 { 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; } }; - diff --git a/src/services/logger/logger.js b/src/services/logger/logger.js index 737e19e..20944b5 100644 --- a/src/services/logger/logger.js +++ b/src/services/logger/logger.js @@ -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();