@shopgate/pwa-common
Version:
Common library for the Shopgate Connect PWA.
274 lines (259 loc) • 8.19 kB
JavaScript
import _inheritsLoose from "@babel/runtime/helpers/inheritsLoose";
import "core-js/modules/es.string.replace.js";
import React from 'react';
import PropTypes from 'prop-types';
import { router, stack as routeStack, onDidPush, onDidPop, onDidReplace, onDidReset, onUpdate, ACTION_POP } from '@virtuous/conductor';
import Route from '@virtuous/conductor/Route';
import { RouterContext, Router as OrigRouter } from '@virtuous/react-conductor';
import { UIEvents } from '@shopgate/pwa-core';
import { hasSGJavaScriptBridge } from '@shopgate/pwa-core/helpers';
import { hasWebBridge } from '@shopgate/engage/core';
import { sanitizeLink } from "../../subscriptions/helpers/handleLinks";
import authRoutes from "../../collections/AuthRoutes";
import { EVENT_USER_INITIALIZED } from "../../constants/user";
import connect from "./connector";
/**
* Adds additional history listeners to compensate bugs and improve the behaviour within
* browser environments.
*/
import { jsx as _jsx } from "react/jsx-runtime";
const createBrowserListeners = () => {
const {
history
} = router;
// Remove the original listener from the router.
router.historyListener();
// Add new one which injects an intermediate function which intercepts history events
router.historyListener = history.listen((location, action) => {
const {
pathname: locationPathname,
search,
hash,
state
} = location;
// Create a pathname which fulfills the router expectations
const pathname = `${locationPathname}${search}${hash}`;
if (action === ACTION_POP && router.routeIndex === 0) {
/**
* Within browser environments we need to handle situations where users come back to old
* Engage history entries from previous sessions, Without special handling the page wouldn't
* render correct content. So we replace the current visible page with content from the route.
*/
router.replace({
pathname,
state
});
return;
}
/**
* Conductor 2.5.0 contains a bug within the handler for native history events. It ignores that
* the search and hash parameters are stored within separate properties of the history location
* object. The routing methods expect those parameters as part of the pathname.
*/
router.handleNativeEvent({
...location,
pathname
}, action);
}).bind(router);
};
/**
* The Router component.
*/
let Router = /*#__PURE__*/function (_React$Component) {
/**
* @param {Object} props The component props.
*/
function Router(props) {
var _this;
_this = _React$Component.call(this, props) || this;
/**
* Updates the user initialized component state
*/
_this.setUserInitialized = () => {
_this.setState({
userInitialized: true
});
};
/**
* Determines the current location from the browser history
* @returns {string}
*/
_this.getHistoryLocation = () => {
const {
hash,
pathname,
search
} = router.history.location;
return sanitizeLink(`${pathname}${search}${hash}`);
};
/**
* Determines if the current route is a protected route
* @returns {null|string}
*/
_this.getRouteProtector = () => authRoutes.getProtector(_this.getHistoryLocation());
/**
* @param {Object} data Data for the update method
*/
_this.update = data => {
const {
prev,
next
} = data;
if (data?.id) {
/**
* The only change right now compared to the original component. When invoked for "onUpdate"
* only the updated route is passed instead of an object with prev and next.
*/
_this.setState({
updated: Date.now()
});
return;
}
_this.setState({
prev: prev ? prev.id : null,
next: next.id,
updated: Date.now()
});
};
if (typeof props.history === 'function') {
router.constructor(props.history);
}
_this.contextValue = null;
_this.state = {
prev: null,
next: null,
updated: null,
userInitialized: false,
initialRouteProtected: !!_this.getRouteProtector()
};
UIEvents.addListener(EVENT_USER_INITIALIZED, _this.setUserInitialized);
onDidPush(_this.update);
onDidPop(_this.update);
onDidReplace(_this.update);
onDidReset(_this.update);
onUpdate(_this.update);
if (hasWebBridge() || hasSGJavaScriptBridge()) {
createBrowserListeners();
}
return _this;
}
/**
* @param {Object} nextProps The next component props.
* @param {Object} nextState The next component state.
* @returns {boolean}
*/
_inheritsLoose(Router, _React$Component);
var _proto = Router.prototype;
_proto.shouldComponentUpdate = function shouldComponentUpdate(nextProps, nextState) {
const {
isUserLoggedIn
} = this.props;
const {
updated,
userInitialized
} = this.state;
return updated !== nextState.updated || userInitialized !== nextState.userInitialized || isUserLoggedIn !== nextProps.isUserLoggedIn;
}
/**
* Replaces the initial route with a protector if necessary
* @param {Object} nextProps The next component props
* @param {Object} nextState The next components state
*/;
_proto.UNSAFE_componentWillUpdate = function UNSAFE_componentWillUpdate(nextProps, nextState) {
const {
isUserLoggedIn
} = nextProps;
const {
userInitialized,
initialRouteProtected
} = nextState;
if (initialRouteProtected && userInitialized && !isUserLoggedIn) {
const protector = this.getRouteProtector();
const location = this.getHistoryLocation();
// Get the initial route from the route stack which was created by the router on init
const initialRoute = routeStack.getByIndex(0);
const {
id
} = initialRoute;
// Prepare the redirect state for the protector route
const state = {
redirect: {
location
}
};
const routeReplacement = new Route({
id,
state,
pathname: protector
});
// Replace the auto-generated route with the replacement
routeStack.update(id, routeReplacement);
// Update the browser url with the protector route
router.history.replace({
pathname: protector,
state: {
...state,
route: {
id
}
}
});
// eslint-disable-next-line react/no-will-update-set-state
this.setState({
initialRouteProtected: false
});
}
}
/**
* Removes the listener for the EVENT_USER_INITIALIZED event
*/;
_proto.componentWillUnmount = function componentWillUnmount() {
UIEvents.removeListener(EVENT_USER_INITIALIZED, this.setUserInitialized);
};
/**
* @returns {JSX}
*/
_proto.render = function render() {
const {
children
} = this.props;
const {
prev,
next,
initialRouteProtected,
userInitialized
} = this.state;
if (initialRouteProtected && !userInitialized) {
/**
* When the initial route is a protected route, the first rendering of the Router needs to
* be postponed till we know the final login state of the user.
*/
return null;
}
const stack = Array.from(routeStack.getAll());
const nextContextValue = {
prev,
next,
stack
};
// Only update the reference if something actually changed.
if (!this.contextValue || this.contextValue.prev !== prev || this.contextValue.next !== next || this.contextValue.stack !== stack) {
this.contextValue = nextContextValue;
}
return /*#__PURE__*/_jsx(RouterContext.Provider, {
value: this.contextValue,
children: children
});
};
return Router;
}(React.Component);
Router.defaultProps = {
history: null,
isUserLoggedIn: false
};
Router.Push = OrigRouter.Push;
Router.Pop = OrigRouter.Pop;
Router.Replace = OrigRouter.Replace;
Router.Reset = OrigRouter.Reset;
Router.ResetTo = OrigRouter.ResetTo;
export default connect(Router);