summaryrefslogtreecommitdiffhomepage
path: root/gui
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2019-03-11 11:20:35 +0100
committerAndrej Mihajlov <and@mullvad.net>2019-03-13 11:24:55 +0100
commit183b79329b03dbc35b6a15df8bf26bcd7848e553 (patch)
tree1bb6433db7b624151ddb843a2e122898457262d5 /gui
parent0048a0e03ab8379e00eef55a871234f0aabc31ca (diff)
downloadmullvadvpn-183b79329b03dbc35b6a15df8bf26bcd7848e553.tar.xz
mullvadvpn-183b79329b03dbc35b6a15df8bf26bcd7848e553.zip
Revamp transition container to support transition queues
Diffstat (limited to 'gui')
-rw-r--r--gui/src/renderer/app.tsx12
-rw-r--r--gui/src/renderer/components/PlatformWindow.tsx21
-rw-r--r--gui/src/renderer/components/TransitionContainer.tsx469
-rw-r--r--gui/src/renderer/routes.tsx97
4 files changed, 400 insertions, 199 deletions
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index 4176575d97..a6c92aa672 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -11,7 +11,7 @@ import { Provider } from 'react-redux';
import { bindActionCreators } from 'redux';
import { InvalidAccountError } from '../main/errors';
-import makeRoutes from './routes';
+import AppRoutes from './routes';
import accountActions from './redux/account/actions';
import connectionActions from './redux/connection/actions';
@@ -181,10 +181,12 @@ export default class AppRenderer {
return (
<Provider store={this.reduxStore}>
<ConnectedRouter history={this.memoryHistory}>
- {makeRoutes({
- app: this,
- locale: this.locale,
- })}
+ <AppRoutes
+ sharedProps={{
+ app: this,
+ locale: this.locale,
+ }}
+ />
</ConnectedRouter>
</Provider>
);
diff --git a/gui/src/renderer/components/PlatformWindow.tsx b/gui/src/renderer/components/PlatformWindow.tsx
index baf446fd5b..cc10374fec 100644
--- a/gui/src/renderer/components/PlatformWindow.tsx
+++ b/gui/src/renderer/components/PlatformWindow.tsx
@@ -1,14 +1,18 @@
import * as React from 'react';
-import { Component, Styles, View } from 'reactxp';
+import { Component, Styles, Types, View } from 'reactxp';
interface IProps {
arrowPosition?: number;
}
+const containerStyle = Styles.createViewStyle({ flex: 1 });
+
export default class PlatformWindow extends Component<IProps> {
public render() {
- let style;
+ return <View style={[containerStyle, this.platformStyle()]}>{this.props.children}</View>;
+ }
+ private platformStyle(): Types.ViewStyleRuleSet {
if (process.platform === 'darwin') {
const arrowPosition = this.props.arrowPosition;
let arrowPositionCss = '50%';
@@ -24,10 +28,15 @@ export default class PlatformWindow extends Component<IProps> {
`url(../../assets/images/app-header-backdrop.svg) no-repeat`,
];
- // @ts-ignore
- style = Styles.createViewStyle({ WebkitMask: webkitMask.join(',') }, false);
+ return Styles.createViewStyle(
+ {
+ // @ts-ignore
+ WebkitMask: webkitMask.join(','),
+ },
+ false,
+ );
+ } else {
+ return undefined;
}
-
- return <View style={style}>{this.props.children}</View>;
}
}
diff --git a/gui/src/renderer/components/TransitionContainer.tsx b/gui/src/renderer/components/TransitionContainer.tsx
index a2632653d3..8ef9611ac0 100644
--- a/gui/src/renderer/components/TransitionContainer.tsx
+++ b/gui/src/renderer/components/TransitionContainer.tsx
@@ -1,229 +1,376 @@
import * as React from 'react';
-import { Animated, Component, Styles, Types, UserInterface, View } from 'reactxp';
+import { Animated, Component, Styles, Types, View } from 'reactxp';
import { ITransitionGroupProps } from '../transitions';
+interface ITransitioningViewProps {
+ viewId: string;
+}
+
+type TransitioningView = React.ReactElement<ITransitioningViewProps>;
+
+interface ITransitionQueueItem {
+ view: TransitioningView;
+ transition: ITransitionGroupProps;
+}
+
interface IProps extends ITransitionGroupProps {
- children: React.ReactNode;
+ children: TransitioningView;
}
interface IState {
- previousChildren?: React.ReactNode;
- childrenAnimation?: Types.AnimatedViewStyleRuleSet;
- previousChildrenAnimation?: Types.AnimatedViewStyleRuleSet;
- dimensions: Types.Dimensions;
- isAnimating: boolean;
+ currentItem?: ITransitionQueueItem;
+ nextItem?: ITransitionQueueItem;
+ itemQueue: ITransitionQueueItem[];
+ currentItemStyle?: Array<Types.StyleRuleSet<Types.AnimatedViewStyle>>;
+ nextItemStyle?: Array<Types.StyleRuleSet<Types.AnimatedViewStyle>>;
}
-const dimensions = UserInterface.measureWindow();
const styles = {
- animationDefaultStyle: Styles.createViewStyle({
+ animatedContainer: Styles.createViewStyle({
position: 'absolute',
- width: dimensions.width,
- height: dimensions.height,
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0,
+ }),
+ transitionView: Styles.createViewStyle({
+ flex: 1,
}),
- allowPointerEventsStyle: Styles.createViewStyle({
+ userInteractionBlocker: Styles.createViewStyle({
// @ts-ignore
- pointerEvents: 'auto',
+ zIndex: 2,
}),
- transitionContainerStyle: Styles.createViewStyle({
- width: dimensions.width,
- height: dimensions.height,
+ transitionContainer: Styles.createViewStyle({
+ flex: 1,
+ }),
+ orderFront: Styles.createViewStyle({
+ // @ts-ignore
+ zIndex: 1,
+ }),
+ orderBack: Styles.createViewStyle({
+ // @ts-ignore
+ zIndex: 0,
}),
};
+export class TransitionView extends Component<ITransitioningViewProps> {
+ public render() {
+ return <View style={styles.transitionView}>{this.props.children}</View>;
+ }
+}
+
export default class TransitionContainer extends Component<IProps, IState> {
public state: IState = {
- dimensions: UserInterface.measureWindow(),
- isAnimating: false,
+ itemQueue: [],
};
+ private containerSize = { width: 0, height: 0 };
+
+ private animation?: Types.Animated.CompositeAnimation;
+ private isCycling = false;
+
+ private slideValueA = Animated.createValue(0);
+ private slideAnimationStyleA = Styles.createAnimatedViewStyle({
+ transform: [{ translateY: this.slideValueA }],
+ });
+
+ private slideValueB = Animated.createValue(0);
+ private slideAnimationStyleB = Styles.createAnimatedViewStyle({
+ transform: [{ translateY: this.slideValueB }],
+ });
+
+ private pushValueA = Animated.createValue(0);
+ private pushStyleA = Styles.createAnimatedViewStyle({
+ transform: [{ translateX: this.pushValueA }],
+ });
+
+ private pushValueB = Animated.createValue(0);
+ private pushStyleB = Styles.createAnimatedViewStyle({
+ transform: [{ translateX: this.pushValueB }],
+ });
+
+ constructor(props: IProps) {
+ super(props);
+
+ this.state.currentItem = this.makeItem(props);
+ }
+
public UNSAFE_componentWillReceiveProps(nextProps: IProps) {
- switch (nextProps.name) {
- case 'slide-up':
- this.slideUpTransition(nextProps);
- break;
- case 'slide-down':
- this.slideDownTransition(nextProps);
- break;
- case 'push':
- this.pushTransition(nextProps);
- break;
- case 'pop':
- this.popTransition(nextProps);
- break;
- default:
- break;
+ const candidate = nextProps.children;
+
+ if (candidate && this.state.currentItem) {
+ // synchronize updates to the last added child.
+ const itemQueueCount = this.state.itemQueue.length;
+ const lastItemInQueue =
+ itemQueueCount > 0 ? this.state.itemQueue[itemQueueCount - 1] : undefined;
+
+ if (lastItemInQueue && lastItemInQueue.view.props.viewId === candidate.props.viewId) {
+ this.setState({
+ itemQueue: [...this.state.itemQueue.slice(0, -1), this.makeItem(nextProps)],
+ });
+ } else if (
+ itemQueueCount === 0 &&
+ this.state.nextItem &&
+ this.state.nextItem.view.props.viewId === candidate.props.viewId
+ ) {
+ this.setState({
+ nextItem: this.makeItem(nextProps),
+ });
+ } else if (
+ itemQueueCount === 0 &&
+ !this.state.nextItem &&
+ this.state.currentItem.view.props.viewId === candidate.props.viewId
+ ) {
+ this.setState({
+ currentItem: this.makeItem(nextProps),
+ });
+ } else {
+ // add new item
+ this.setState({
+ itemQueue: [...this.state.itemQueue, this.makeItem(nextProps)],
+ });
+ }
+ } else if (candidate && !this.state.currentItem) {
+ this.setState({ currentItem: this.makeItem(nextProps) });
+ }
+ }
+
+ public componentDidUpdate() {
+ this.cycle();
+ }
+
+ public componentWillUnmount() {
+ if (this.animation) {
+ this.animation.stop();
}
}
- public onFinishedAnimation = (_result: Types.Animated.EndResult) => {
- this.setState({
- childrenAnimation: styles.allowPointerEventsStyle,
- previousChildren: null,
- isAnimating: false,
- });
+ public render() {
+ const disableUserInteraction =
+ this.state.itemQueue.length > 0 || this.state.nextItem ? true : false;
+
+ return (
+ <View style={styles.transitionContainer} onLayout={this.onLayout}>
+ {this.state.currentItem && (
+ <Animated.View
+ key={this.state.currentItem.view.props.viewId}
+ style={[styles.animatedContainer, this.state.currentItemStyle]}>
+ {this.state.currentItem.view}
+ </Animated.View>
+ )}
+
+ {this.state.nextItem && (
+ <Animated.View
+ key={this.state.nextItem.view.props.viewId}
+ style={[styles.animatedContainer, this.state.nextItemStyle]}>
+ {this.state.nextItem.view}
+ </Animated.View>
+ )}
+
+ {disableUserInteraction && (
+ <View style={[styles.animatedContainer, styles.userInteractionBlocker]} />
+ )}
+ </View>
+ );
+ }
+
+ private onLayout = (event: Types.ViewOnLayoutEvent) => {
+ this.containerSize = { width: event.width, height: event.height };
};
- public slideUpTransition(nextProps: IProps) {
- const currentTranslationValue = Animated.createValue(this.state.dimensions.height);
- this.setState(
- {
- previousChildren: this.props.children,
- childrenAnimation: Styles.createAnimatedViewStyle({
- // @ts-ignore
- zIndex: 1,
- transform: [{ translateY: currentTranslationValue }],
- }),
- previousChildrenAnimation: Styles.createAnimatedViewStyle({
- // @ts-ignore
- zIndex: 0,
- transform: [{ translateY: Animated.createValue(0) }],
- }),
- isAnimating: true,
+ private cycle() {
+ if (!this.isCycling) {
+ this.isCycling = true;
+ this.cycleUnguarded(() => {
+ this.isCycling = false;
+ });
+ }
+ }
+
+ private cycleUnguarded(onFinish: () => void) {
+ const itemQueue = this.state.itemQueue;
+
+ const continueCycling = () => {
+ this.makeNextItemCurrent(() => {
+ this.cycleUnguarded(onFinish);
+ });
+ };
+
+ if (itemQueue.length > 0) {
+ const nextItem = itemQueue[0];
+ const transition = nextItem.transition;
+
+ switch (transition.name) {
+ case 'slide-up':
+ this.slideUp(transition.duration, continueCycling);
+ break;
+
+ case 'slide-down':
+ this.slideDown(transition.duration, continueCycling);
+ break;
+
+ case 'push':
+ this.push(transition.duration, continueCycling);
+ break;
+
+ case 'pop':
+ this.pop(transition.duration, continueCycling);
+ break;
+
+ default:
+ this.replace(() => {
+ this.cycleUnguarded(onFinish);
+ });
+ break;
+ }
+ } else {
+ this.animation = undefined;
+ onFinish();
+ }
+ }
+
+ private makeItem(props: IProps): ITransitionQueueItem {
+ return {
+ transition: {
+ name: props.name,
+ duration: props.duration,
},
+ view: React.cloneElement(props.children),
+ };
+ }
+
+ private makeNextItemCurrent(completion: () => void) {
+ this.setState(
+ (state) => ({
+ currentItem: state.nextItem,
+ nextItem: undefined,
+ currentItemStyle: [],
+ nextItemStyle: [],
+ }),
+ completion,
+ );
+ }
+
+ private slideUp(duration: number, completion: Types.Animated.EndCallback) {
+ this.slideValueA.setValue(0);
+ this.slideValueB.setValue(this.containerSize.height);
+
+ this.setState(
+ (state) => ({
+ nextItem: state.itemQueue[0],
+ itemQueue: state.itemQueue.slice(1),
+ currentItemStyle: [this.slideAnimationStyleA, styles.orderBack],
+ nextItemStyle: [this.slideAnimationStyleB, styles.orderFront],
+ }),
() => {
- Animated.timing(currentTranslationValue, {
+ const animation = Animated.timing(this.slideValueB, {
toValue: 0,
easing: Animated.Easing.InOut(),
- duration: nextProps.duration,
- }).start(this.onFinishedAnimation);
+ duration,
+ });
+
+ animation.start(completion);
+ this.animation = animation;
},
);
}
- public slideDownTransition(nextProps: IProps) {
- const previousTranslationValue = Animated.createValue(0);
+ private slideDown(duration: number, completion: Types.Animated.EndCallback) {
+ this.slideValueA.setValue(0);
+ this.slideValueB.setValue(0);
+
this.setState(
- {
- previousChildren: this.props.children,
- childrenAnimation: Styles.createAnimatedViewStyle({
- // @ts-ignore
- zIndex: 0,
- transform: [{ translateY: Animated.createValue(0) }],
- }),
- previousChildrenAnimation: Styles.createAnimatedViewStyle({
- // @ts-ignore
- zIndex: 1,
- transform: [{ translateY: previousTranslationValue }],
- }),
- isAnimating: true,
- },
+ (state) => ({
+ nextItem: state.itemQueue[0],
+ itemQueue: state.itemQueue.slice(1),
+ currentItemStyle: [this.slideAnimationStyleA, styles.orderFront],
+ nextItemStyle: [this.slideAnimationStyleB, styles.orderBack],
+ }),
() => {
- Animated.timing(previousTranslationValue, {
- toValue: this.state.dimensions.height,
+ const animation = Animated.timing(this.slideValueA, {
+ toValue: this.containerSize.height,
easing: Animated.Easing.InOut(),
- duration: nextProps.duration,
- }).start(this.onFinishedAnimation);
+ duration,
+ });
+
+ animation.start(completion);
+ this.animation = animation;
},
);
}
- public pushTransition(nextProps: IProps) {
- const currentTranslationValue = Animated.createValue(this.state.dimensions.width);
- const previousTranslationValue = Animated.createValue(0);
+ private push(duration: number, completion: Types.Animated.EndCallback) {
+ this.pushValueA.setValue(0);
+ this.pushValueB.setValue(this.containerSize.width);
+
this.setState(
- {
- previousChildren: this.props.children,
- childrenAnimation: Styles.createAnimatedViewStyle({
- // @ts-ignore
- zIndex: 1,
- transform: [{ translateX: currentTranslationValue }],
- }),
- previousChildrenAnimation: Styles.createAnimatedViewStyle({
- // @ts-ignore
- zIndex: 0,
- transform: [{ translateX: previousTranslationValue }],
- }),
- isAnimating: true,
- },
+ (state) => ({
+ nextItem: state.itemQueue[0],
+ itemQueue: state.itemQueue.slice(1),
+ currentItemStyle: [this.pushStyleA, styles.orderBack],
+ nextItemStyle: [this.pushStyleB, styles.orderFront],
+ }),
() => {
- const compositeAnimation = Animated.parallel([
- Animated.timing(currentTranslationValue, {
- toValue: 0,
+ const animation = Animated.parallel([
+ Animated.timing(this.pushValueA, {
+ toValue: -this.containerSize.width * 0.5,
easing: Animated.Easing.InOut(),
- duration: nextProps.duration,
+ duration,
}),
- Animated.timing(previousTranslationValue, {
- toValue: -this.state.dimensions.width / 2,
+ Animated.timing(this.pushValueB, {
+ toValue: 0,
easing: Animated.Easing.InOut(),
- duration: nextProps.duration,
+ duration,
}),
]);
- compositeAnimation.start(this.onFinishedAnimation);
+
+ animation.start(completion);
+ this.animation = animation;
},
);
}
- public popTransition(nextProps: IProps) {
- const currentTranslationValue = Animated.createValue(-this.state.dimensions.width / 2);
- const previousTranslationValue = Animated.createValue(0);
+ private pop(duration: number, completion: Types.Animated.EndCallback) {
+ this.pushValueA.setValue(-this.containerSize.width * 0.5);
+ this.pushValueB.setValue(0);
+
this.setState(
- {
- previousChildren: this.props.children,
- childrenAnimation: Styles.createAnimatedViewStyle({
- // @ts-ignore
- zIndex: 0,
- transform: [{ translateX: currentTranslationValue }],
- }),
- previousChildrenAnimation: Styles.createAnimatedViewStyle({
- // @ts-ignore
- zIndex: 1,
- transform: [{ translateX: previousTranslationValue }],
- }),
- isAnimating: true,
- },
+ (state) => ({
+ nextItem: state.itemQueue[0],
+ itemQueue: state.itemQueue.slice(1),
+ currentItemStyle: [this.pushStyleB, styles.orderFront],
+ nextItemStyle: [this.pushStyleA, styles.orderBack],
+ }),
() => {
- const compositeAnimation = Animated.parallel([
- Animated.timing(currentTranslationValue, {
+ const animation = Animated.parallel([
+ Animated.timing(this.pushValueA, {
toValue: 0,
easing: Animated.Easing.InOut(),
- duration: nextProps.duration,
+ duration,
}),
- Animated.timing(previousTranslationValue, {
- toValue: this.state.dimensions.width,
+ Animated.timing(this.pushValueB, {
+ toValue: this.containerSize.width,
easing: Animated.Easing.InOut(),
- duration: nextProps.duration,
+ duration,
}),
]);
- compositeAnimation.start(this.onFinishedAnimation);
+
+ animation.start(completion);
+ this.animation = animation;
},
);
}
- public render() {
- const { children } = this.props;
- const {
- isAnimating,
- previousChildren,
- childrenAnimation,
- previousChildrenAnimation,
- } = this.state;
-
- return (
- <View style={styles.transitionContainerStyle} ignorePointerEvents={isAnimating}>
- {previousChildren && (
- <Animated.View
- key={getChildKey(previousChildren)}
- style={[styles.animationDefaultStyle, previousChildrenAnimation]}>
- {previousChildren}
- </Animated.View>
- )}
-
- <Animated.View
- key={getChildKey(children)}
- style={[styles.animationDefaultStyle, childrenAnimation]}>
- {children}
- </Animated.View>
- </View>
+ private replace(completion: () => void) {
+ this.setState(
+ (state) => ({
+ currentItem: state.itemQueue[0],
+ nextItem: undefined,
+ itemQueue: state.itemQueue.slice(1),
+ currentItemStyle: [],
+ nextItemStyle: [],
+ }),
+ completion,
);
}
}
-
-function getChildKey(child?: React.ReactNode): string | number | undefined {
- return child &&
- typeof child === 'object' &&
- 'key' in child &&
- (typeof child.key === 'string' || typeof child.key === 'number')
- ? child.key
- : undefined;
-}
diff --git a/gui/src/renderer/routes.tsx b/gui/src/renderer/routes.tsx
index a31dd0de4a..ae694cefbc 100644
--- a/gui/src/renderer/routes.tsx
+++ b/gui/src/renderer/routes.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
-import { Route, RouteComponentProps, Switch } from 'react-router';
+import { Route, RouteComponentProps, RouteProps, Switch, withRouter } from 'react-router';
import App from './app';
-import TransitionContainer from './components/TransitionContainer';
+import TransitionContainer, { TransitionView } from './components/TransitionContainer';
import AccountPage from './containers/AccountPage';
import AdvancedSettingsPage from './containers/AdvancedSettingsPage';
import ConnectPage from './containers/ConnectPage';
@@ -21,42 +21,85 @@ export interface ISharedRouteProps {
type CustomRouteProps = {
component: React.ComponentClass<ISharedRouteProps>;
-} & Route['props'];
+} & RouteProps;
-export default function makeRoutes(componentProps: ISharedRouteProps) {
- // Renders a route extended with shared props
- function CustomRoute({ component: ComponentClass, ...routeProps }: CustomRouteProps) {
- const renderOverride = () => <ComponentClass {...componentProps} />;
+interface IAppRoutesProps extends RouteComponentProps {
+ sharedProps: ISharedRouteProps;
+}
+
+interface IAppRoutesState {
+ previousLocation?: IAppRoutesProps['location'];
+ currentLocation: IAppRoutesProps['location'];
+}
+
+class AppRoutes extends React.Component<IAppRoutesProps, IAppRoutesState> {
+ private unobserveHistory?: () => void;
+
+ constructor(props: IAppRoutesProps) {
+ super(props);
+
+ this.state = {
+ currentLocation: props.location,
+ };
+ }
- return <Route {...routeProps} render={renderOverride} />;
+ public componentDidMount() {
+ // React throttles updates, so it's impossible to capture the intermediate navigation without
+ // listening to the history directly.
+ this.unobserveHistory = this.props.history.listen((location) => {
+ this.setState((state) => ({
+ previousLocation: state.currentLocation,
+ currentLocation: location,
+ }));
+ });
}
- // store previous route
- let sourceRoute: string | null = null;
+ public componentWillUnmount() {
+ if (this.unobserveHistory) {
+ this.unobserveHistory();
+ }
+ }
+
+ public render() {
+ const location = this.state.currentLocation;
+ const transitionProps = getTransitionProps(
+ this.state.previousLocation ? this.state.previousLocation.pathname : null,
+ location.pathname,
+ );
+
+ // Renders a route extended with shared props
+ const CustomRoute = ({ component: ComponentClass, ...routeProps }: CustomRouteProps) => {
+ const renderOverride = () => <ComponentClass {...this.props.sharedProps} />;
- function renderRoute({ location }: RouteComponentProps) {
- const destinationRoute = location.pathname;
- const transitionProps = getTransitionProps(sourceRoute, destinationRoute);
- sourceRoute = destinationRoute;
+ return <Route {...routeProps} render={renderOverride} />;
+ };
return (
<PlatformWindowContainer>
<TransitionContainer {...transitionProps}>
- <Switch key={location.key} location={location}>
- <CustomRoute exact={true} path="/" component={LaunchPage} />
- <CustomRoute exact={true} path="/login" component={LoginPage} />
- <CustomRoute exact={true} path="/connect" component={ConnectPage} />
- <CustomRoute exact={true} path="/settings" component={SettingsPage} />
- <CustomRoute exact={true} path="/settings/account" component={AccountPage} />
- <CustomRoute exact={true} path="/settings/preferences" component={PreferencesPage} />
- <CustomRoute exact={true} path="/settings/advanced" component={AdvancedSettingsPage} />
- <CustomRoute exact={true} path="/settings/support" component={SupportPage} />
- <CustomRoute exact={true} path="/select-location" component={SelectLocationPage} />
- </Switch>
+ <TransitionView viewId={location.key || ''}>
+ <Switch key={location.key} location={location}>
+ <CustomRoute exact={true} path="/" component={LaunchPage} />
+ <CustomRoute exact={true} path="/login" component={LoginPage} />
+ <CustomRoute exact={true} path="/connect" component={ConnectPage} />
+ <CustomRoute exact={true} path="/settings" component={SettingsPage} />
+ <CustomRoute exact={true} path="/settings/account" component={AccountPage} />
+ <CustomRoute exact={true} path="/settings/preferences" component={PreferencesPage} />
+ <CustomRoute
+ exact={true}
+ path="/settings/advanced"
+ component={AdvancedSettingsPage}
+ />
+ <CustomRoute exact={true} path="/settings/support" component={SupportPage} />
+ <CustomRoute exact={true} path="/select-location" component={SelectLocationPage} />
+ </Switch>
+ </TransitionView>
</TransitionContainer>
</PlatformWindowContainer>
);
}
-
- return <Route render={renderRoute} />;
}
+
+const AppRoutesWithRouter = withRouter(AppRoutes);
+
+export default AppRoutesWithRouter;