mirror of
https://github.com/elyby/accounts-frontend.git
synced 2024-11-08 17:12:25 +05:30
Migrate auth components to new Context api
This commit is contained in:
parent
08a2158042
commit
59debce051
@ -1,41 +1,35 @@
|
||||
/**
|
||||
* Helps with form fields binding, form serialization and errors rendering
|
||||
*/
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import AuthError from 'app/components/auth/authError/AuthError';
|
||||
import { userShape } from 'app/components/user/User';
|
||||
import { FormModel } from 'app/components/ui/form';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
export default class BaseAuthBody extends React.Component<
|
||||
import Context, { AuthContext } from './Context';
|
||||
|
||||
/**
|
||||
* Helps with form fields binding, form serialization and errors rendering
|
||||
*/
|
||||
|
||||
class BaseAuthBody extends React.Component<
|
||||
// TODO: this may be converted to generic type RouteComponentProps<T>
|
||||
RouteComponentProps<{ [key: string]: any }>
|
||||
> {
|
||||
static contextTypes = {
|
||||
clearErrors: PropTypes.func.isRequired,
|
||||
resolve: PropTypes.func.isRequired,
|
||||
requestRedraw: PropTypes.func.isRequired,
|
||||
auth: PropTypes.shape({
|
||||
error: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.shape({
|
||||
type: PropTypes.string,
|
||||
payload: PropTypes.object,
|
||||
}),
|
||||
]),
|
||||
scopes: PropTypes.array,
|
||||
}).isRequired,
|
||||
user: userShape,
|
||||
};
|
||||
static contextType = Context;
|
||||
/* TODO: use declare */ context: React.ContextType<typeof Context>;
|
||||
prevErrors: AuthContext['auth']['error'];
|
||||
|
||||
autoFocusField: string | null = '';
|
||||
|
||||
// eslint-disable-next-line react/no-deprecated
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
if (nextContext.auth.error !== this.context.auth.error) {
|
||||
this.form.setErrors(nextContext.auth.error || {});
|
||||
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();
|
||||
}
|
||||
|
||||
this.prevErrors = this.context.auth.error;
|
||||
}
|
||||
|
||||
renderErrors() {
|
||||
@ -48,7 +42,7 @@ export default class BaseAuthBody extends React.Component<
|
||||
this.context.resolve(this.serialize());
|
||||
}
|
||||
|
||||
onClearErrors = this.context.clearErrors;
|
||||
onClearErrors = () => this.context.clearErrors();
|
||||
|
||||
form = new FormModel({
|
||||
renderErrors: false,
|
||||
@ -65,6 +59,10 @@ export default class BaseAuthBody extends React.Component<
|
||||
autoFocus() {
|
||||
const fieldId = this.autoFocusField;
|
||||
|
||||
fieldId && this.form.focus(fieldId);
|
||||
if (fieldId && this.form.hasField(fieldId)) {
|
||||
this.form.focus(fieldId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseAuthBody;
|
||||
|
34
packages/app/components/auth/Context.tsx
Normal file
34
packages/app/components/auth/Context.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
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;
|
||||
}
|
||||
|
||||
const Context = React.createContext<AuthContext>({
|
||||
auth: {
|
||||
error: null,
|
||||
login: '',
|
||||
scopes: [],
|
||||
} as any,
|
||||
user: {
|
||||
id: null,
|
||||
isGuest: true,
|
||||
} as any,
|
||||
async requestRedraw() {},
|
||||
clearErrors() {},
|
||||
resolve() {},
|
||||
reject() {},
|
||||
});
|
||||
Context.displayName = 'AuthContext';
|
||||
|
||||
export const { Provider, Consumer } = Context;
|
||||
|
||||
export default Context;
|
@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { AccountsState } from 'app/components/accounts';
|
||||
import { User } from 'app/components/user';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { TransitionMotion, spring } from 'react-motion';
|
||||
import {
|
||||
@ -15,9 +14,9 @@ import MeasureHeight from 'app/components/MeasureHeight';
|
||||
import panelStyles from 'app/components/ui/panel.scss';
|
||||
import icons from 'app/components/ui/icons.scss';
|
||||
import authFlow from 'app/services/authFlow';
|
||||
import { userShape } from 'app/components/user/User';
|
||||
import { RootState } from 'app/reducers';
|
||||
|
||||
import { Provider as AuthContextProvider } from './Context';
|
||||
import { getLogin, State as AuthState } from './reducer';
|
||||
import * as actions from './actions';
|
||||
import helpLinks from './helpLinks.scss';
|
||||
@ -111,10 +110,11 @@ interface Props extends OwnProps {
|
||||
auth: AuthState;
|
||||
user: User;
|
||||
accounts: AccountsState;
|
||||
setErrors: (errors: { [key: string]: ValidationError }) => void;
|
||||
clearErrors: () => void;
|
||||
resolve: () => void;
|
||||
reject: () => void;
|
||||
|
||||
setErrors: (errors: { [key: string]: ValidationError }) => void;
|
||||
}
|
||||
|
||||
type State = {
|
||||
@ -126,28 +126,7 @@ type State = {
|
||||
direction: 'X' | 'Y';
|
||||
};
|
||||
|
||||
class PanelTransition extends React.Component<Props, State> {
|
||||
static childContextTypes = {
|
||||
auth: PropTypes.shape({
|
||||
error: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.shape({
|
||||
type: PropTypes.string,
|
||||
payload: PropTypes.object,
|
||||
}),
|
||||
]),
|
||||
login: PropTypes.string,
|
||||
}),
|
||||
user: userShape,
|
||||
accounts: PropTypes.shape({
|
||||
available: PropTypes.array,
|
||||
}),
|
||||
requestRedraw: PropTypes.func,
|
||||
clearErrors: PropTypes.func,
|
||||
resolve: PropTypes.func,
|
||||
reject: PropTypes.func,
|
||||
};
|
||||
|
||||
class PanelTransition extends React.PureComponent<Props, State> {
|
||||
state: State = {
|
||||
contextHeight: 0,
|
||||
panelId: this.props.Body && (this.props.Body.type as any).panelId,
|
||||
@ -166,25 +145,6 @@ class PanelTransition extends React.Component<Props, State> {
|
||||
|
||||
timerIds: NodeJS.Timeout[] = []; // this is a list of a probably running timeouts to clean on unmount
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
auth: this.props.auth,
|
||||
user: this.props.user,
|
||||
requestRedraw: (): Promise<void> =>
|
||||
new Promise(resolve =>
|
||||
this.setState({ isHeightDirty: true }, () => {
|
||||
this.setState({ isHeightDirty: false });
|
||||
|
||||
// wait till transition end
|
||||
this.timerIds.push(setTimeout(resolve, 200));
|
||||
}),
|
||||
),
|
||||
clearErrors: this.props.clearErrors,
|
||||
resolve: this.props.resolve,
|
||||
reject: this.props.reject,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const nextPanel: PanelId =
|
||||
this.props.Body && (this.props.Body.type as any).panelId;
|
||||
@ -222,7 +182,17 @@ class PanelTransition extends React.Component<Props, State> {
|
||||
render() {
|
||||
const { contextHeight, forceHeight } = this.state;
|
||||
|
||||
const { Title, Body, Footer, Links } = this.props;
|
||||
const {
|
||||
Title,
|
||||
Body,
|
||||
Footer,
|
||||
Links,
|
||||
auth,
|
||||
user,
|
||||
clearErrors,
|
||||
resolve,
|
||||
reject,
|
||||
} = this.props;
|
||||
|
||||
if (this.props.children) {
|
||||
return this.props.children;
|
||||
@ -245,84 +215,95 @@ class PanelTransition extends React.Component<Props, State> {
|
||||
this.isHeightMeasured = isHeightMeasured || formHeight > 0;
|
||||
|
||||
return (
|
||||
<TransitionMotion
|
||||
styles={[
|
||||
{
|
||||
key: panelId,
|
||||
data: { Title, Body, Footer, Links, hasBackButton: hasGoBack },
|
||||
style: {
|
||||
transformSpring: spring(0, transformSpringConfig),
|
||||
opacitySpring: spring(1, opacitySpringConfig),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'common',
|
||||
style: {
|
||||
heightSpring: isHeightMeasured
|
||||
? spring(forceHeight || formHeight, transformSpringConfig)
|
||||
: formHeight,
|
||||
switchContextHeightSpring: spring(
|
||||
forceHeight || contextHeight,
|
||||
changeContextSpringConfig,
|
||||
),
|
||||
},
|
||||
},
|
||||
]}
|
||||
willEnter={this.willEnter}
|
||||
willLeave={this.willLeave}
|
||||
>
|
||||
{items => {
|
||||
const panels = items.filter(({ key }) => key !== 'common');
|
||||
const [common] = items.filter(({ key }) => key === 'common');
|
||||
|
||||
const contentHeight = {
|
||||
overflow: 'hidden',
|
||||
height: forceHeight
|
||||
? common.style.switchContextHeightSpring
|
||||
: 'auto',
|
||||
};
|
||||
|
||||
this.tryToAutoFocus(panels.length);
|
||||
|
||||
const bodyHeight = {
|
||||
position: 'relative' as const,
|
||||
height: `${common.style.heightSpring}px`,
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
id={panelId}
|
||||
onSubmit={this.onFormSubmit}
|
||||
onInvalid={this.onFormInvalid}
|
||||
isLoading={this.props.auth.isLoading}
|
||||
>
|
||||
<Panel>
|
||||
<PanelHeader>
|
||||
{panels.map(config => this.getHeader(config))}
|
||||
</PanelHeader>
|
||||
<div style={contentHeight}>
|
||||
<MeasureHeight
|
||||
state={this.shouldMeasureHeight()}
|
||||
onMeasure={this.onUpdateContextHeight}
|
||||
>
|
||||
<PanelBody>
|
||||
<div style={bodyHeight}>
|
||||
{panels.map(config => this.getBody(config))}
|
||||
</div>
|
||||
</PanelBody>
|
||||
<PanelFooter>
|
||||
{panels.map(config => this.getFooter(config))}
|
||||
</PanelFooter>
|
||||
</MeasureHeight>
|
||||
</div>
|
||||
</Panel>
|
||||
<div className={helpLinksStyles}>
|
||||
{panels.map(config => this.getLinks(config))}
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
<AuthContextProvider
|
||||
value={{
|
||||
auth,
|
||||
user,
|
||||
requestRedraw: this.requestRedraw,
|
||||
clearErrors,
|
||||
resolve,
|
||||
reject,
|
||||
}}
|
||||
</TransitionMotion>
|
||||
>
|
||||
<TransitionMotion
|
||||
styles={[
|
||||
{
|
||||
key: panelId,
|
||||
data: { Title, Body, Footer, Links, hasBackButton: hasGoBack },
|
||||
style: {
|
||||
transformSpring: spring(0, transformSpringConfig),
|
||||
opacitySpring: spring(1, opacitySpringConfig),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'common',
|
||||
style: {
|
||||
heightSpring: isHeightMeasured
|
||||
? spring(forceHeight || formHeight, transformSpringConfig)
|
||||
: formHeight,
|
||||
switchContextHeightSpring: spring(
|
||||
forceHeight || contextHeight,
|
||||
changeContextSpringConfig,
|
||||
),
|
||||
},
|
||||
},
|
||||
]}
|
||||
willEnter={this.willEnter}
|
||||
willLeave={this.willLeave}
|
||||
>
|
||||
{items => {
|
||||
const panels = items.filter(({ key }) => key !== 'common');
|
||||
const [common] = items.filter(({ key }) => key === 'common');
|
||||
|
||||
const contentHeight = {
|
||||
overflow: 'hidden',
|
||||
height: forceHeight
|
||||
? common.style.switchContextHeightSpring
|
||||
: 'auto',
|
||||
};
|
||||
|
||||
this.tryToAutoFocus(panels.length);
|
||||
|
||||
const bodyHeight = {
|
||||
position: 'relative' as const,
|
||||
height: `${common.style.heightSpring}px`,
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
id={panelId}
|
||||
onSubmit={this.onFormSubmit}
|
||||
onInvalid={this.onFormInvalid}
|
||||
isLoading={this.props.auth.isLoading}
|
||||
>
|
||||
<Panel>
|
||||
<PanelHeader>
|
||||
{panels.map(config => this.getHeader(config))}
|
||||
</PanelHeader>
|
||||
<div style={contentHeight}>
|
||||
<MeasureHeight
|
||||
state={this.shouldMeasureHeight()}
|
||||
onMeasure={this.onUpdateContextHeight}
|
||||
>
|
||||
<PanelBody>
|
||||
<div style={bodyHeight}>
|
||||
{panels.map(config => this.getBody(config))}
|
||||
</div>
|
||||
</PanelBody>
|
||||
<PanelFooter>
|
||||
{panels.map(config => this.getFooter(config))}
|
||||
</PanelFooter>
|
||||
</MeasureHeight>
|
||||
</div>
|
||||
</Panel>
|
||||
<div className={helpLinksStyles}>
|
||||
{panels.map(config => this.getLinks(config))}
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}}
|
||||
</TransitionMotion>
|
||||
</AuthContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -442,8 +423,11 @@ class PanelTransition extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
shouldMeasureHeight() {
|
||||
const errorString = Object.values(this.props.auth.error || {}).reduce(
|
||||
(acc, item: ValidationError) => {
|
||||
const { user, accounts, auth } = this.props;
|
||||
const { isHeightDirty } = this.state;
|
||||
|
||||
const errorString = Object.values(auth.error || {}).reduce(
|
||||
(acc: string, item: ValidationError): string => {
|
||||
if (typeof item === 'string') {
|
||||
return acc + item;
|
||||
}
|
||||
@ -451,13 +435,13 @@ class PanelTransition extends React.Component<Props, State> {
|
||||
return acc + item.type;
|
||||
},
|
||||
'',
|
||||
);
|
||||
) as string;
|
||||
|
||||
return [
|
||||
errorString,
|
||||
this.state.isHeightDirty,
|
||||
this.props.user.lang,
|
||||
this.props.accounts.available.length,
|
||||
isHeightDirty,
|
||||
user.lang,
|
||||
accounts.available.length,
|
||||
].join('');
|
||||
}
|
||||
|
||||
@ -601,6 +585,16 @@ class PanelTransition extends React.Component<Props, State> {
|
||||
transform: `translate${direction}(${value}${unit})`,
|
||||
};
|
||||
}
|
||||
|
||||
requestRedraw = (): Promise<void> =>
|
||||
new Promise(resolve =>
|
||||
this.setState({ isHeightDirty: true }, () => {
|
||||
this.setState({ isHeightDirty: false });
|
||||
|
||||
// wait till transition end
|
||||
this.timerIds.push(setTimeout(resolve, 200));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
|
@ -1,23 +1,19 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { FormattedMessage as Message, MessageDescriptor } from 'react-intl';
|
||||
import { User } from 'app/components/user';
|
||||
import { userShape } from 'app/components/user/User';
|
||||
|
||||
import Context, { AuthContext } from './Context';
|
||||
|
||||
interface Props {
|
||||
isAvailable?: (context: Context) => boolean;
|
||||
isAvailable?: (context: AuthContext) => boolean;
|
||||
payload?: { [key: string]: any };
|
||||
label: MessageDescriptor;
|
||||
}
|
||||
|
||||
export type RejectionLinkProps = Props;
|
||||
|
||||
interface Context {
|
||||
reject: (payload: { [key: string]: any } | undefined) => void;
|
||||
user: User;
|
||||
}
|
||||
function RejectionLink(props: Props) {
|
||||
const context = useContext(Context);
|
||||
|
||||
function RejectionLink(props: Props, context: Context) {
|
||||
if (props.isAvailable && !props.isAvailable(context)) {
|
||||
// TODO: if want to properly support multiple links, we should control
|
||||
// the dividers ' | ' rendered from factory too
|
||||
@ -38,9 +34,4 @@ function RejectionLink(props: Props, context: Context) {
|
||||
);
|
||||
}
|
||||
|
||||
RejectionLink.contextTypes = {
|
||||
reject: PropTypes.func.isRequired,
|
||||
user: userShape,
|
||||
};
|
||||
|
||||
export default RejectionLink;
|
||||
|
@ -16,14 +16,16 @@ export default class ForgotPasswordBody extends BaseAuthBody {
|
||||
static hasGoBack = true;
|
||||
|
||||
state = {
|
||||
isLoginEdit: !this.getLogin(),
|
||||
isLoginEdit: false,
|
||||
};
|
||||
|
||||
autoFocusField = this.state.isLoginEdit ? 'login' : null;
|
||||
autoFocusField = 'login';
|
||||
|
||||
render() {
|
||||
const { isLoginEdit } = this.state;
|
||||
|
||||
const login = this.getLogin();
|
||||
const isLoginEditShown = this.state.isLoginEdit;
|
||||
const isLoginEditShown = isLoginEdit || !login;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -79,11 +81,13 @@ export default class ForgotPasswordBody extends BaseAuthBody {
|
||||
return login || user.username || user.email || '';
|
||||
}
|
||||
|
||||
onClickEdit = () => {
|
||||
onClickEdit = async () => {
|
||||
this.setState({
|
||||
isLoginEdit: true,
|
||||
});
|
||||
|
||||
this.context.requestRedraw().then(() => this.form.focus('login'));
|
||||
await this.context.requestRedraw();
|
||||
|
||||
this.form.focus('login');
|
||||
};
|
||||
}
|
||||
|
@ -45,13 +45,14 @@ interface OAuthState {
|
||||
|
||||
export interface State {
|
||||
credentials: Credentials;
|
||||
error:
|
||||
| null
|
||||
| string
|
||||
| {
|
||||
type: string;
|
||||
payload: { [key: string]: any };
|
||||
};
|
||||
error: null | {
|
||||
[key: string]:
|
||||
| string
|
||||
| {
|
||||
type: string;
|
||||
payload: { [key: string]: any };
|
||||
};
|
||||
};
|
||||
isLoading: boolean;
|
||||
isSwitcherEnabled: boolean;
|
||||
client: Client | null;
|
||||
@ -198,7 +199,9 @@ function scopes(state = [], { type, payload = [] }): State['scopes'] {
|
||||
}
|
||||
}
|
||||
|
||||
export function getLogin(state: RootState): string | null {
|
||||
export function getLogin(
|
||||
state: RootState | Pick<RootState, 'auth'>,
|
||||
): string | null {
|
||||
return state.auth.credentials.login || null;
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,10 @@ export default class FormModel {
|
||||
this.renderErrors = options.renderErrors !== false;
|
||||
}
|
||||
|
||||
hasField(fieldId: string) {
|
||||
return !!this.fields[fieldId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects form with React's component
|
||||
*
|
||||
|
@ -1,27 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* @typedef {object} User
|
||||
* @property {number} id
|
||||
* @property {string} uuid
|
||||
* @property {string} token
|
||||
* @property {string} username
|
||||
* @property {string} email
|
||||
* @property {string} avatar
|
||||
* @property {bool} isGuest
|
||||
* @property {bool} isActive
|
||||
* @property {number} passwordChangedAt - timestamp
|
||||
* @property {bool} hasMojangUsernameCollision
|
||||
*/
|
||||
export const userShape = PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
uuid: PropTypes.string,
|
||||
token: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
avatar: PropTypes.string,
|
||||
isGuest: PropTypes.bool.isRequired,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
passwordChangedAt: PropTypes.number,
|
||||
hasMojangUsernameCollision: PropTypes.bool,
|
||||
});
|
Loading…
Reference in New Issue
Block a user