diff --git a/.eslintrc.json b/.eslintrc.json
index 3e9ce49a7..cfe7863e5 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,34 +1,35 @@
{
- "extends": [
- "airbnb",
- "prettier",
- "prettier/react",
- "jest-enzyme",
- "plugin:jest/recommended"
- ],
- "parser": "babel-eslint",
- "plugins": ["prettier", "jest"],
- "parserOptions": {
- "ecmaVersion": 2016,
- "sourceType": "module",
- "ecmaFeatures": {
- "jsx": true
- }
- },
- "env": {
- "es6": true,
- "browser": true,
- "node": true
- },
- "rules": {
- "array-bracket-spacing": [2, "never"],
- "semi": [2, "always"],
- "react/prefer-stateless-function": [0],
- "react/jsx-filename-extension": [
- 1,
- {
- "extensions": [".js", ".jsx"]
- }
- ]
- }
+ "extends": [
+ "airbnb",
+ "prettier",
+ "prettier/react",
+ "jest-enzyme",
+ "plugin:jest/recommended"
+ ],
+ "parser": "babel-eslint",
+ "plugins": ["prettier", "jest"],
+ "parserOptions": {
+ "ecmaVersion": 2016,
+ "sourceType": "module",
+ "ecmaFeatures": {
+ "jsx": true
+ }
+ },
+ "env": {
+ "es6": true,
+ "browser": true,
+ "node": true
+ },
+ "rules": {
+ "array-bracket-spacing": [2, "never"],
+ "semi": [2, "always"],
+ "jsx-a11y/label-has-for": 0,
+ "react/prefer-stateless-function": [0],
+ "react/jsx-filename-extension": [
+ 1,
+ {
+ "extensions": [".js", ".jsx"]
+ }
+ ]
+ }
}
diff --git a/client/assets/default/index.css b/client/assets/default/index.css
new file mode 100644
index 000000000..16c2c3224
--- /dev/null
+++ b/client/assets/default/index.css
@@ -0,0 +1 @@
+/* */
diff --git a/client/components/footer/__snapshots__/footer.test.js.snap b/client/components/footer/__snapshots__/footer.test.js.snap
index 6dc42d5b5..4e57b9dba 100644
--- a/client/components/footer/__snapshots__/footer.test.js.snap
+++ b/client/components/footer/__snapshots__/footer.test.js.snap
@@ -13,20 +13,20 @@ exports[` rendering should render correctly 1`] = `
- link one
+ link-1
- link two
+ link-2
@@ -35,9 +35,7 @@ exports[` rendering should render correctly 1`] = `
>
- this is secondary text
-
+ />
`;
diff --git a/client/components/footer/footer.test.js b/client/components/footer/footer.test.js
index 06f67ed0b..41a56483a 100644
--- a/client/components/footer/footer.test.js
+++ b/client/components/footer/footer.test.js
@@ -3,26 +3,14 @@ import {shallow} from "enzyme";
import React from "react";
import renderer from "react-test-renderer";
+import getConfig from "../../utils/get-config";
import Footer from "./footer";
+const defaultConfig = getConfig("default");
const createTestProps = props => {
return {
language: "en",
- footer: {
- links: [
- {
- text: {en: "link one"},
- url: "www.testurl.com",
- },
- {
- text: {en: "link two"},
- url: "www.testurl2.com",
- },
- ],
- secondary_text: {
- en: "this is secondary text",
- },
- },
+ footer: defaultConfig.components.footer,
...props,
};
};
@@ -48,9 +36,14 @@ describe(" rendering", () => {
expect(wrapper.find(".owisp-footer-link")).toHaveLength(0);
});
it("should render secondary text", () => {
- const {secondary_text} = props.footer;
- expect(wrapper.find(".owisp-footer-row-2-inner").text()).toBe(
- secondary_text.en,
- );
+ wrapper.setProps({
+ footer: {...props.footer, secondary_text: {en: "secondary text"}},
+ });
+ expect(
+ wrapper
+ .update()
+ .find(".owisp-footer-row-2-inner")
+ .text(),
+ ).toBe("secondary text");
});
});
diff --git a/client/components/header/__snapshots__/header.test.js.snap b/client/components/header/__snapshots__/header.test.js.snap
index cf5972f11..f2dbb38ac 100644
--- a/client/components/header/__snapshots__/header.test.js.snap
+++ b/client/components/header/__snapshots__/header.test.js.snap
@@ -46,20 +46,20 @@ exports[` rendering should render with links 1`] = `
- test link 1
+ link-1
- test link 2
+ link-2
diff --git a/client/components/header/header.js b/client/components/header/header.js
index 2a7350a84..9e4dcb977 100644
--- a/client/components/header/header.js
+++ b/client/components/header/header.js
@@ -2,6 +2,7 @@ import "./index.css";
import PropTypes from "prop-types";
import React from "react";
+import {Link} from "react-router-dom";
import getAssetPath from "../../utils/get-asset-path";
import getText from "../../utils/get-text";
@@ -18,11 +19,13 @@ export default class Header extends React.Component {
{logo.url ? (
-
+
+
+
) : null}
diff --git a/client/components/header/header.test.js b/client/components/header/header.test.js
index 4b92d203f..dd94acb99 100644
--- a/client/components/header/header.test.js
+++ b/client/components/header/header.test.js
@@ -2,34 +2,17 @@ import {shallow} from "enzyme";
import React from "react";
import renderer from "react-test-renderer";
+import getConfig from "../../utils/get-config";
import Header from "./header";
+const defaultConfig = getConfig("default");
const createTestProps = props => {
return {
setLanguage: jest.fn(),
orgSlug: "default",
language: "en",
languages: [{slug: "en", text: "english"}, {slug: "it", text: "italian"}],
- header: {
- logo: {
- alternate_text: null,
- url: null,
- },
- links: [
- {
- text: {
- en: "test link 1",
- },
- url: "https://testlink1.com",
- },
- {
- text: {
- en: "test link 2",
- },
- url: "https://testlink2.com",
- },
- ],
- },
+ header: defaultConfig.components.header,
...props,
};
};
diff --git a/client/components/login/__snapshots__/login.test.js.snap b/client/components/login/__snapshots__/login.test.js.snap
new file mode 100644
index 000000000..86b31926e
--- /dev/null
+++ b/client/components/login/__snapshots__/login.test.js.snap
@@ -0,0 +1,130 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` rendering should render correctly 1`] = `
+
+`;
diff --git a/client/components/login/index.css b/client/components/login/index.css
new file mode 100644
index 000000000..350d3a3d1
--- /dev/null
+++ b/client/components/login/index.css
@@ -0,0 +1,213 @@
+.owisp-login-container {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: flex-start;
+ background: #f2f2f2;
+ width: 100%;
+}
+.owisp-login-form {
+ font-family: sans-serif;
+ background: #ffffff;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ padding: 24px 50px;
+}
+.owisp-login-form.success .owisp-login-input:invalid {
+ box-shadow: none;
+}
+.owisp-login-header-content {
+ text-transform: capitalize;
+ color: #444;
+ font-size: 28px;
+ margin-bottom: 4px;
+}
+.owisp-login-fieldset {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: flex-start;
+ align-self: flex-start;
+}
+.owisp-login-label {
+ margin-top: 15px;
+ margin-bottom: 5px;
+}
+.owisp-login-label-text {
+ color: #6b6b6b;
+ text-transform: capitalize;
+}
+.owisp-login-input {
+ padding: 10px;
+ border-radius: 3px;
+ font-size: 18px;
+ border: 2px solid #e7e7e7;
+ margin-top: 5px;
+ width: 380px;
+ box-sizing: border-box;
+}
+.owisp-login-input:focus {
+ border-color: #a7a7a7;
+}
+.owisp-login-input.error {
+ border-color: #d93025;
+}
+.owisp-login-form-btn {
+ width: 100%;
+ font-size: 18px;
+ height: 50px;
+ background: #293b52;
+ color: #ffffff;
+ text-transform: capitalize;
+ border: 0;
+ border-radius: 3px;
+ cursor: pointer;
+}
+.owisp-login-form-btn:focus {
+ outline: dotted 1px #444;
+ outline-offset: 0.5px;
+}
+.owisp-login-login-btn {
+ margin-top: 4px;
+}
+.owisp-login-form-btn:hover {
+ background: #223144;
+}
+.owisp-login-error {
+ font-size: 12px;
+ color: #d93025;
+ width: 324px;
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ flex-wrap: nowrap;
+}
+.owisp-login-error-icon {
+ display: inline-block;
+ background: #d93025;
+ color: #ffffff;
+ min-width: 16px;
+ align-self: center;
+ height: 16px;
+ border-radius: 50%;
+ text-align: center;
+ margin-right: 8px;
+ font-weight: 600;
+}
+.owisp-login-register-link {
+ text-decoration: none;
+ width: 100%;
+}
+.owisp-login-add-info {
+ margin-top: 15px;
+ color: #444;
+ max-width: 380px;
+}
+.owisp-login-additional-link {
+ color: #444;
+}
+.owisp-login-additional-link:hover,
+.owisp-login-additional-link:focus {
+ outline: dotted 1px #444;
+ outline-offset: 0.5px;
+}
+.owisp-login-social-hr {
+ outline: 0;
+ border: 0;
+ text-align: center;
+ height: 1.5em;
+ line-height: 1em;
+ position: relative;
+ width: 100%;
+}
+.owisp-login-social-hr:before {
+ content: "";
+ background: #e6e6e6;
+ position: absolute;
+ left: 0;
+ top: 48%;
+ width: 100%;
+ height: 2px;
+}
+.owisp-login-social-hr:after {
+ content: attr(data-content);
+ position: relative;
+ display: inline-block;
+ padding: 0 0.5em;
+ line-height: 1.5em;
+ color: #798993;
+ background-color: #fcfcfa;
+}
+.owisp-login-divider-description {
+ color: #798993;
+ text-transform: uppercase;
+ font-size: 0.9rem;
+ width: 100%;
+ text-align: center;
+}
+.owisp-login-social-links-div {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ flex-wrap: wrap;
+ margin-top: 8px;
+}
+.owisp-login-social-link {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ padding: 4px 8px;
+ align-items: center;
+ text-decoration: none;
+ color: #798993;
+ border-radius: 3px;
+ margin-right: 12px;
+ box-shadow: 1px 1px 1px 1px hsla(0, 0%, 53%, 0.33);
+}
+.owisp-login-social-link:hover {
+ background: #efefef;
+}
+.owisp-login-social-link:focus {
+ outline: dotted 1px #444;
+ outline-offset: 0.5px;
+}
+.owisp-login-social-link-icon {
+ width: 28px;
+ height: auto;
+}
+.owisp-login-social-link-text {
+ margin-left: 3px;
+ text-transform: capitalize;
+}
+.owisp-login-links-div {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ justify-content: flex-start;
+ align-items: center;
+ margin-top: 20px;
+}
+.owisp-login-link {
+ line-height: 1.4;
+ text-decoration: none;
+ text-transform: capitalize;
+ color: #444;
+}
+.owisp-login-link:focus {
+ outline: dotted 1px #444;
+ outline-offset: 0.5px;
+}
+.owisp-login-link:hover {
+ text-decoration: underline;
+}
+.owisp-login-error-non-field {
+ box-sizing: border-box;
+ font-size: 16px;
+ width: 100%;
+ max-width: 380px;
+ margin-top: 15px;
+}
diff --git a/client/components/login/index.js b/client/components/login/index.js
new file mode 100644
index 000000000..0094357b5
--- /dev/null
+++ b/client/components/login/index.js
@@ -0,0 +1,18 @@
+import {connect} from "react-redux";
+
+import Component from "./login";
+
+const mapStateToProps = state => {
+ return {
+ loginForm: state.organization.configuration.components.login_form,
+ privacyPolicy: state.organization.configuration.privacy_policy,
+ termsAndConditions: state.organization.configuration.terms_and_conditions,
+ language: state.language,
+ orgSlug: state.organization.configuration.slug,
+ };
+};
+
+export default connect(
+ mapStateToProps,
+ null,
+)(Component);
diff --git a/client/components/login/login.js b/client/components/login/login.js
new file mode 100644
index 000000000..1e87f2a25
--- /dev/null
+++ b/client/components/login/login.js
@@ -0,0 +1,340 @@
+/* eslint-disable camelcase */
+import "./index.css";
+
+import axios from "axios";
+import PropTypes from "prop-types";
+import qs from "qs";
+import React from "react";
+import {Link} from "react-router-dom";
+
+import {loginApiUrl} from "../../constants";
+import getAssetPath from "../../utils/get-asset-path";
+import getText from "../../utils/get-text";
+import renderAdditionalInfo from "../../utils/render-additional-info";
+
+export default class Login extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ username: "",
+ password: "",
+ errors: {},
+ };
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleChange = this.handleChange.bind(this);
+ }
+
+ handleChange(event) {
+ this.setState({[event.target.name]: event.target.value});
+ }
+
+ handleSubmit(event) {
+ event.preventDefault();
+ const {orgSlug} = this.props;
+ const {username, password, errors} = this.state;
+ const url = loginApiUrl.replace("{orgSlug}", orgSlug);
+ this.setState({
+ errors: {},
+ });
+ return axios({
+ method: "post",
+ headers: {
+ "content-type": "application/x-www-form-urlencoded",
+ },
+ url,
+ data: qs.stringify({
+ username,
+ password,
+ }),
+ })
+ .then(() => {
+ this.setState({
+ errors: {},
+ username: "",
+ password: "",
+ });
+ })
+ .catch(error => {
+ const {data} = error.response;
+ this.setState({
+ errors: {
+ ...errors,
+ ...(data.non_field_errors
+ ? {nonField: data.non_field_errors[0]}
+ : {nonField: ""}),
+ ...(data.detail ? {nonField: data.detail} : {}),
+ ...(data.username ? {username: data.username} : {username: ""}),
+ ...(data.password ? {password: data.password} : {password: ""}),
+ },
+ });
+ });
+ }
+
+ render() {
+ const {errors, username, password} = this.state;
+ const {
+ language,
+ loginForm,
+ orgSlug,
+ termsAndConditions,
+ privacyPolicy,
+ } = this.props;
+ const {
+ links,
+ buttons,
+ input_fields,
+ social_login,
+ header,
+ additional_info_text,
+ } = loginForm;
+ return (
+
+
+
+ );
+ }
+}
+
+Login.propTypes = {
+ loginForm: PropTypes.shape({
+ header: PropTypes.object,
+ social_login: PropTypes.shape({
+ divider_text: PropTypes.object,
+ description: PropTypes.object,
+ links: PropTypes.arrayOf(PropTypes.object),
+ }),
+ input_fields: PropTypes.shape({
+ username: PropTypes.object,
+ password: PropTypes.object,
+ }),
+ additional_info_text: PropTypes.object,
+ buttons: PropTypes.object,
+ links: PropTypes.object,
+ }).isRequired,
+ language: PropTypes.string.isRequired,
+ orgSlug: PropTypes.string.isRequired,
+ privacyPolicy: PropTypes.shape({
+ title: PropTypes.object,
+ content: PropTypes.object,
+ }).isRequired,
+ termsAndConditions: PropTypes.shape({
+ title: PropTypes.object,
+ content: PropTypes.object,
+ }).isRequired,
+};
diff --git a/client/components/login/login.test.js b/client/components/login/login.test.js
new file mode 100644
index 000000000..693e8c95c
--- /dev/null
+++ b/client/components/login/login.test.js
@@ -0,0 +1,94 @@
+/* eslint-disable prefer-promise-reject-errors */
+import axios from "axios";
+import {shallow} from "enzyme";
+import React from "react";
+import {BrowserRouter as Router} from "react-router-dom";
+import renderer from "react-test-renderer";
+
+import getConfig from "../../utils/get-config";
+import Login from "./login";
+
+jest.mock("axios");
+
+const defaultConfig = getConfig("default");
+const createTestProps = props => {
+ return {
+ language: "en",
+ orgSlug: "default",
+ loginForm: defaultConfig.components.login_form,
+ privacyPolicy: defaultConfig.privacy_policy,
+ termsAndConditions: defaultConfig.terms_and_conditions,
+ ...props,
+ };
+};
+describe(" rendering", () => {
+ let props;
+ it("should render correctly", () => {
+ props = createTestProps();
+ const component = renderer
+ .create(
+
+
+ ,
+ )
+ .toJSON();
+ expect(component).toMatchSnapshot();
+ });
+});
+
+describe(" interactions", () => {
+ let props;
+ let wrapper;
+ beforeEach(() => {
+ props = createTestProps();
+ wrapper = shallow();
+ });
+ it("should change state values when handleChange function is invoked", () => {
+ wrapper
+ .find("#owisp-login-username")
+ .simulate("change", {target: {value: "test username", name: "username"}});
+ expect(wrapper.state("username")).toEqual("test username");
+ wrapper
+ .find("#owisp-login-password")
+ .simulate("change", {target: {value: "test password", name: "password"}});
+ expect(wrapper.state("password")).toEqual("test password");
+ });
+ it("should execute handleSubmit correctly when form is submitted", () => {
+ axios
+ .mockImplementationOnce(() => {
+ return Promise.reject({
+ response: {
+ data: {
+ username: "username error",
+ password: "password error",
+ detail: "error details",
+ non_field_errors: "non field errors",
+ },
+ },
+ });
+ })
+ .mockImplementationOnce(() => {
+ return Promise.resolve();
+ });
+ const event = {preventDefault: () => {}};
+ return wrapper
+ .instance()
+ .handleSubmit(event)
+ .then(() => {
+ expect(wrapper.instance().state.errors).toEqual({
+ username: "username error",
+ nonField: "error details",
+ password: "password error",
+ });
+ expect(wrapper.find(".owisp-login-error")).toHaveLength(3);
+ })
+ .then(() => {
+ return wrapper
+ .instance()
+ .handleSubmit(event)
+ .then(() => {
+ expect(wrapper.instance().state.errors).toEqual({});
+ });
+ });
+ });
+});
diff --git a/client/components/organization-wrapper/index.js b/client/components/organization-wrapper/index.js
index 8c9997b5c..93f78e3fc 100644
--- a/client/components/organization-wrapper/index.js
+++ b/client/components/organization-wrapper/index.js
@@ -1,11 +1,13 @@
+import {withCookies} from "react-cookie";
import {connect} from "react-redux";
import setOrganization from "../../actions/set-organization";
import Component from "./organization-wrapper";
-const mapStateToProps = state => {
+const mapStateToProps = (state, ownProps) => {
return {
organization: state.organization,
+ cookies: ownProps.cookies,
};
};
@@ -16,7 +18,9 @@ const mapDispatchToProps = dispatch => {
},
};
};
-export default connect(
- mapStateToProps,
- mapDispatchToProps,
-)(Component);
+export default withCookies(
+ connect(
+ mapStateToProps,
+ mapDispatchToProps,
+ )(Component),
+);
diff --git a/client/components/organization-wrapper/organization-wrapper.js b/client/components/organization-wrapper/organization-wrapper.js
index dba162821..ed87ad2c5 100644
--- a/client/components/organization-wrapper/organization-wrapper.js
+++ b/client/components/organization-wrapper/organization-wrapper.js
@@ -9,6 +9,7 @@ import getAssetPath from "../../utils/get-asset-path";
import DoesNotExist from "../404";
import Footer from "../footer";
import Header from "../header";
+import Login from "../login";
import PasswordConfirm from "../password-confirm";
import PasswordReset from "../password-reset";
import Registration from "../registration";
@@ -16,14 +17,20 @@ import Registration from "../registration";
export default class OrganizationWrapper extends React.Component {
constructor(props) {
super(props);
- const {match, setOrganization} = this.props;
+ const {match, setOrganization} = props;
const organizationSlug = match.params.organization;
if (organizationSlug) setOrganization(organizationSlug);
}
+ componentDidUpdate(prevProps) {
+ const {setOrganization, match} = this.props;
+ if (prevProps.match.params.organization !== match.params.organization) {
+ if (match.params.organization) setOrganization(match.params.organization);
+ }
+ }
+
render() {
- const {match} = this.props;
- const {organization} = this.props;
+ const {organization, match} = this.props;
const {title, favicon} = organization.configuration;
const orgSlug = organization.configuration.slug;
const cssPath = organization.configuration.css_path;
@@ -34,7 +41,7 @@ export default class OrganizationWrapper extends React.Component {
} />
}
/>
}
/>
+ } />
} />
diff --git a/client/components/password-confirm/password-confirm.test.js b/client/components/password-confirm/password-confirm.test.js
index 90f3b0bd8..cb12deb03 100644
--- a/client/components/password-confirm/password-confirm.test.js
+++ b/client/components/password-confirm/password-confirm.test.js
@@ -6,55 +6,16 @@ import React from "react";
import {BrowserRouter as Router} from "react-router-dom";
import renderer from "react-test-renderer";
+import getConfig from "../../utils/get-config";
import PasswordConfirm from "./password-confirm";
jest.mock("axios");
-
+const defaultConfig = getConfig("default");
const createTestProps = props => {
return {
language: "en",
orgSlug: "default",
- passwordConfirm: {
- heading: {
- en: "reset your password",
- },
- additional_text: {
- en: "please enter your new password",
- },
- input_fields: {
- password: {
- type: "password",
- pattern: ".{6,}",
- pattern_description: {
- en: "password must be a minimum of 6 characters",
- },
- placeholder: {
- en: "password",
- },
- label: {
- en: "password",
- },
- },
- password_confirm: {
- type: "password",
- pattern: null,
- pattern_description: {
- en: null,
- },
- placeholder: {
- en: "confirm password",
- },
- label: {
- en: "confirm",
- },
- },
- },
- buttons: {
- submit: {
- en: "change password",
- },
- },
- },
+ passwordConfirm: defaultConfig.components.confirm_form,
match: {
params: {
uid: "testUid",
diff --git a/client/components/password-reset/password-reset.test.js b/client/components/password-reset/password-reset.test.js
index 5f576f5ef..ab7895d64 100644
--- a/client/components/password-reset/password-reset.test.js
+++ b/client/components/password-reset/password-reset.test.js
@@ -5,48 +5,17 @@ import React from "react";
import {BrowserRouter as Router} from "react-router-dom";
import renderer from "react-test-renderer";
+import getConfig from "../../utils/get-config";
import PasswordReset from "./password-reset";
jest.mock("axios");
+const defaultConfig = getConfig("default");
const createTestProps = props => {
return {
language: "en",
orgSlug: "default",
- passwordReset: {
- heading: {
- en: "reset your password",
- },
- additional_text: {
- en:
- "enter your email and we'll send you the instructions to reset your password",
- },
- input_fields: {
- email: {
- type: "email",
- pattern: null,
- pattern_description: {
- en: null,
- },
- placeholder: {
- en: "email address",
- },
- label: {
- en: "email",
- },
- },
- },
- buttons: {
- send: {
- en: "send",
- },
- },
- login_page_link: {
- text: {
- en: "Take me Back to Sign In",
- },
- },
- },
+ passwordReset: defaultConfig.components.reset_form,
...props,
};
};
diff --git a/client/components/registration/__snapshots__/registration.test.js.snap b/client/components/registration/__snapshots__/registration.test.js.snap
index b2edbb5a7..948cf3f83 100644
--- a/client/components/registration/__snapshots__/registration.test.js.snap
+++ b/client/components/registration/__snapshots__/registration.test.js.snap
@@ -36,7 +36,7 @@ exports[` rendering should render correctly 1`] = `
id="owisp-registration-username"
name="username"
onChange={[Function]}
- pattern="[a-zA-Z@.+\\\\-_]{1,150}"
+ pattern="[a-zA-Z@.+\\\\-_\\\\d]{1,150}"
placeholder="enter phone number or username"
required={true}
title="only letters, numbers, and @/./+/-/_ characters"
@@ -60,6 +60,7 @@ exports[` rendering should render correctly 1`] = `
onChange={[Function]}
placeholder="email address"
required={true}
+ title={null}
type="email"
value=""
/>
@@ -103,16 +104,50 @@ exports[` rendering should render correctly 1`] = `
onChange={[Function]}
placeholder="confirm password"
required={true}
+ title={null}
type="password"
value=""
/>
+
+
`;
diff --git a/client/components/registration/index.css b/client/components/registration/index.css
index c42e7faba..6b5b05649 100644
--- a/client/components/registration/index.css
+++ b/client/components/registration/index.css
@@ -45,15 +45,32 @@
font-size: 18px;
border: 2px solid #e7e7e7;
margin-top: 5px;
- text-transform: capitalize;
width: 300px;
}
+.owisp-registration-input:focus {
+ border-color: #a7a7a7;
+}
.owisp-registration-input.error {
border-color: #d93025;
}
+.owisp-registration-add-info {
+ margin-top: 15px;
+ color: #444;
+ max-width: 330px;
+}
+.owisp-registration-additional-link {
+ color: #444;
+}
+.owisp-registration-additional-link:hover,
+.owisp-registration-additional-link:focus {
+ outline: dotted 1px #444;
+ outline-offset: 0.5px;
+}
+.owisp-registration-label-registration-btn {
+ align-self: flex-start;
+}
.owisp-registration-submit-btn {
width: 100%;
- margin-top: 20px;
font-size: 18px;
height: 50px;
background: #293b52;
@@ -66,6 +83,10 @@
.owisp-registration-submit-btn:hover {
background: #223144;
}
+.owisp-registration-submit-btn:focus {
+ outline: dotted 1px #444;
+ outline-offset: 0.5px;
+}
.owisp-registration-error {
font-size: 12px;
color: #d93025;
@@ -86,3 +107,16 @@
margin-right: 8px;
font-weight: 600;
}
+.owisp-registration-link {
+ line-height: 1.4;
+ text-decoration: none;
+ text-transform: capitalize;
+ color: #444;
+}
+.owisp-registration-link:focus {
+ outline: dotted 1px #444;
+ outline-offset: 0.5px;
+}
+.owisp-registration-link:hover {
+ text-decoration: underline;
+}
diff --git a/client/components/registration/index.js b/client/components/registration/index.js
index 1097bcc6b..3029cefad 100644
--- a/client/components/registration/index.js
+++ b/client/components/registration/index.js
@@ -5,6 +5,8 @@ import Component from "./registration";
const mapStateToProps = state => {
return {
registration: state.organization.configuration.components.registration_form,
+ privacyPolicy: state.organization.configuration.privacy_policy,
+ termsAndConditions: state.organization.configuration.terms_and_conditions,
language: state.language,
orgSlug: state.organization.configuration.slug,
};
diff --git a/client/components/registration/registration.js b/client/components/registration/registration.js
index 76517f483..877234405 100644
--- a/client/components/registration/registration.js
+++ b/client/components/registration/registration.js
@@ -1,12 +1,15 @@
+/* eslint-disable camelcase */
import "./index.css";
import axios from "axios";
import PropTypes from "prop-types";
import qs from "qs";
import React from "react";
+import {Link} from "react-router-dom";
import {passwordConfirmError, registerApiUrl} from "../../constants";
import getText from "../../utils/get-text";
+import renderAdditionalInfo from "../../utils/render-additional-info";
export default class Registration extends React.Component {
constructor(props) {
@@ -30,9 +33,9 @@ export default class Registration extends React.Component {
handleSubmit(event) {
event.preventDefault();
const {registration, orgSlug} = this.props;
- const inputFields = registration.input_fields;
+ const {input_fields} = registration;
const {username, email, password1, password2, errors} = this.state;
- if (inputFields.password_confirm) {
+ if (input_fields.password_confirm) {
if (password1 !== password2) {
this.setState({
errors: {
@@ -40,12 +43,12 @@ export default class Registration extends React.Component {
password2: passwordConfirmError,
},
});
- return;
+ return false;
}
}
this.setState({errors: {...errors, password2: null}});
const url = registerApiUrl.replace("{orgSlug}", orgSlug);
- axios({
+ return axios({
method: "post",
headers: {
"content-type": "application/x-www-form-urlencoded",
@@ -89,9 +92,14 @@ export default class Registration extends React.Component {
}
render() {
- const {registration, language} = this.props;
- const inputFields = registration.input_fields;
- const {buttons} = registration;
+ const {
+ registration,
+ language,
+ termsAndConditions,
+ privacyPolicy,
+ orgSlug,
+ } = this.props;
+ const {buttons, additional_info_text, input_fields, links} = registration;
const {username, email, password1, password2, errors, success} = this.state;
return (
@@ -106,38 +114,38 @@ export default class Registration extends React.Component {
-
+ {additional_info_text ? (
+
+ {renderAdditionalInfo(
+ additional_info_text,
+ language,
+ termsAndConditions,
+ privacyPolicy,
+ orgSlug,
+ "registration",
+ )}
+
+ ) : null}
+ {buttons.register ? (
+ <>
+ {buttons.register.label ? (
+
+ ) : null}
+
+ >
+ ) : null}
+ {links ? (
+
+ {links.forget_password ? (
+
+ {getText(links.forget_password, language)}
+
+ ) : null}
+ {links.login ? (
+
+ {getText(links.login, language)}
+
+ ) : null}
+
+ ) : null}
@@ -325,7 +380,17 @@ Registration.propTypes = {
password_confirm: PropTypes.object,
username: PropTypes.object,
}),
+ additional_info_text: PropTypes.object,
+ links: PropTypes.object,
}).isRequired,
language: PropTypes.string.isRequired,
orgSlug: PropTypes.string.isRequired,
+ privacyPolicy: PropTypes.shape({
+ title: PropTypes.object,
+ content: PropTypes.object,
+ }).isRequired,
+ termsAndConditions: PropTypes.shape({
+ title: PropTypes.object,
+ content: PropTypes.object,
+ }).isRequired,
};
diff --git a/client/components/registration/registration.test.js b/client/components/registration/registration.test.js
index 4bf02aa4b..9cc00d3aa 100644
--- a/client/components/registration/registration.test.js
+++ b/client/components/registration/registration.test.js
@@ -1,172 +1,44 @@
+/* eslint-disable prefer-promise-reject-errors */
+import axios from "axios";
/* eslint-disable camelcase */
import {shallow} from "enzyme";
import React from "react";
+import {BrowserRouter as Router} from "react-router-dom";
import renderer from "react-test-renderer";
+import getConfig from "../../utils/get-config";
import Registration from "./registration";
+jest.mock("axios");
+
+const defaultConfig = getConfig("default");
const createTestProps = props => {
return {
language: "en",
orgSlug: "default",
- registration: {
- header: {
- en: " sign up",
- },
- input_fields: {
- username: {
- type: "text",
- pattern: "[a-zA-Z@.+\\-_]{1,150}",
- pattern_description: {
- en: "only letters, numbers, and @/./+/-/_ characters",
- },
- placeholder: {
- en: "enter phone number or username",
- },
- label: {
- en: "phone number or username",
- },
- },
- email: {
- type: "email",
- pattern: undefined,
- pattern_description: {
- en: undefined,
- },
- placeholder: {
- en: "email address",
- },
- label: {
- en: "email",
- },
- },
- password: {
- type: "password",
- pattern: ".{6,}",
- pattern_description: {
- en: "password must be a minimum of 6 characters",
- },
- placeholder: {
- en: "password",
- },
- label: {
- en: "password",
- },
- },
- password_confirm: {
- type: "password",
- pattern: undefined,
- pattern_description: {
- en: undefined,
- },
- placeholder: {
- en: "confirm password",
- },
- label: {
- en: "confirm",
- },
- },
- },
- buttons: {
- register: {
- en: "sign up",
- },
- },
- },
+ registration: defaultConfig.components.registration_form,
+ privacyPolicy: defaultConfig.privacy_policy,
+ termsAndConditions: defaultConfig.terms_and_conditions,
...props,
};
};
describe(" rendering", () => {
let props;
- let wrapper;
beforeEach(() => {
props = createTestProps();
- wrapper = shallow();
});
it("should render correctly", () => {
props = createTestProps();
- const component = renderer.create().toJSON();
+ const component = renderer
+ .create(
+
+
+ ,
+ )
+ .toJSON();
expect(component).toMatchSnapshot();
});
- it("should render 4 input fields", () => {
- expect(wrapper.find(".owisp-registration-input")).toHaveLength(4);
- });
- it("should render username field correctly", () => {
- const {username} = props.registration.input_fields;
- expect(wrapper.find(".owisp-registration-label-username").text()).toBe(
- username.label.en,
- );
- expect(
- wrapper.find(".owisp-registration-input-username").prop("placeholder"),
- ).toBe(username.placeholder.en);
- expect(
- wrapper.find(".owisp-registration-input-username").prop("pattern"),
- ).toBe(username.pattern);
- expect(
- wrapper.find(".owisp-registration-input-username").prop("title"),
- ).toBe(username.pattern_description.en);
- expect(
- wrapper.find(".owisp-registration-input-username").prop("type"),
- ).toBe(username.type);
- });
-
- it("should render email field correctly", () => {
- const {email} = props.registration.input_fields;
- expect(wrapper.find(".owisp-registration-label-email").text()).toBe(
- email.label.en,
- );
- expect(
- wrapper.find(".owisp-registration-input-email").prop("placeholder"),
- ).toBe(email.placeholder.en);
- expect(
- wrapper.find(".owisp-registration-input-email").prop("pattern"),
- ).toBe(email.pattern);
- expect(wrapper.find(".owisp-registration-input-email").prop("title")).toBe(
- email.pattern_description.en,
- );
- expect(wrapper.find(".owisp-registration-input-email").prop("type")).toBe(
- email.type,
- );
- });
-
- it("should render password field correctly", () => {
- const {password} = props.registration.input_fields;
- expect(wrapper.find(".owisp-registration-label-password").text()).toBe(
- password.label.en,
- );
- expect(
- wrapper.find(".owisp-registration-input-password").prop("placeholder"),
- ).toBe(password.placeholder.en);
- expect(
- wrapper.find(".owisp-registration-input-password").prop("pattern"),
- ).toBe(password.pattern);
- expect(
- wrapper.find(".owisp-registration-input-password").prop("title"),
- ).toBe(password.pattern_description.en);
- expect(
- wrapper.find(".owisp-registration-input-password").prop("type"),
- ).toBe(password.type);
- });
-
- it("should render confirm password field correctly", () => {
- const {password_confirm} = props.registration.input_fields;
- expect(wrapper.find(".owisp-registration-label-confirm").text()).toBe(
- password_confirm.label.en,
- );
- expect(
- wrapper.find(".owisp-registration-input-confirm").prop("placeholder"),
- ).toBe(password_confirm.placeholder.en);
- expect(
- wrapper.find(".owisp-registration-input-confirm").prop("pattern"),
- ).toBe(password_confirm.pattern);
- expect(
- wrapper.find(".owisp-registration-input-confirm").prop("title"),
- ).toBe(password_confirm.pattern_description.en);
- expect(wrapper.find(".owisp-registration-input-confirm").prop("type")).toBe(
- password_confirm.type,
- );
- });
});
describe(" interactions", () => {
@@ -195,35 +67,59 @@ describe(" interactions", () => {
expect(wrapper.state("password2")).toEqual("testpassword");
});
- it("should call handleSubmit when form is submitted", () => {
- const mockedHandleSubmit = jest
- .fn()
+ it("should execute handleSubmit correctly when form is submitted", () => {
+ axios
.mockImplementationOnce(() => {
- wrapper.setState({
- errors: {},
- username: "",
- email: "",
- password1: "",
- password2: "",
- success: true,
+ return Promise.reject({
+ response: {
+ data: {
+ username: "username error",
+ email: "email error",
+ password1: "password1 error",
+ password2: "password2 error",
+ },
+ },
});
})
.mockImplementationOnce(() => {
- wrapper.setState({
- errors: {
- username: "invalid username",
- email: "invalid email",
- },
+ return Promise.resolve();
+ });
+ wrapper.setState({
+ password1: "wrong password",
+ password2: "wrong password1",
+ });
+ const event = {preventDefault: () => {}};
+ wrapper.instance().handleSubmit(event);
+ expect(
+ wrapper.update().find(".owisp-registration-error-confirm"),
+ ).toHaveLength(1);
+ wrapper.setState({
+ password1: "password",
+ password2: "password",
+ });
+ return wrapper
+ .instance()
+ .handleSubmit(event)
+ .then(() => {
+ expect(wrapper.instance().state.errors).toEqual({
+ username: "username error",
+ email: "email error",
+ password1: "password1 error",
+ password2: "password2 error",
});
+ expect(wrapper.find(".owisp-registration-error")).toHaveLength(4);
+ })
+ .then(() => {
+ return wrapper
+ .instance()
+ .handleSubmit(event)
+ .then(() => {
+ expect(wrapper.instance().state.errors).toEqual({});
+ expect(wrapper.instance().state.success).toEqual(true);
+ expect(
+ wrapper.find(".owisp-registration-form.success"),
+ ).toHaveLength(1);
+ });
});
- wrapper.instance().handleSubmit = mockedHandleSubmit;
- wrapper.instance().forceUpdate();
- wrapper.find(".owisp-registration-form").simulate("submit");
- expect(mockedHandleSubmit).toHaveBeenCalledTimes(1);
- expect(wrapper.find(".owisp-registration-form.success")).toHaveLength(1);
- wrapper.find(".owisp-registration-form").simulate("submit");
- expect(mockedHandleSubmit).toHaveBeenCalledTimes(2);
- expect(wrapper.find(".owisp-registration-input.error")).toHaveLength(2);
- expect(wrapper.find(".owisp-registration-error")).toHaveLength(2);
});
});
diff --git a/client/constants/index.js b/client/constants/index.js
index 7849d989c..f65bcfa78 100644
--- a/client/constants/index.js
+++ b/client/constants/index.js
@@ -2,3 +2,4 @@ export const passwordConfirmError = "The two password fields didn't match.";
export const registerApiUrl = "/api/v1/{orgSlug}/account/";
export const resetApiUrl = "/api/v1/{orgSlug}/account/password/reset/";
export const confirmApiUrl = "/api/v1/{orgSlug}/account/password/reset/confirm";
+export const loginApiUrl = "/api/v1/{orgSlug}/account/token";
diff --git a/client/index.js b/client/index.js
index 6febd9b68..3ebe011b6 100644
--- a/client/index.js
+++ b/client/index.js
@@ -2,6 +2,7 @@ import "./index.css";
import PropTypes from "prop-types";
import React from "react";
+import {CookiesProvider} from "react-cookie";
import {render} from "react-dom";
import {Provider, connect} from "react-redux";
import {BrowserRouter} from "react-router-dom";
@@ -45,8 +46,10 @@ const App = connect(
)(BaseApp);
render(
-
-
- ,
+
+
+
+
+ ,
document.getElementById("root"),
);
diff --git a/client/utils/render-additional-info.js b/client/utils/render-additional-info.js
new file mode 100644
index 000000000..b83a772ce
--- /dev/null
+++ b/client/utils/render-additional-info.js
@@ -0,0 +1,97 @@
+import React from "react";
+import {Link} from "react-router-dom";
+
+import getText from "./get-text";
+
+const renderAdditionalInfo = (
+ textObj,
+ language,
+ termsAndConditions,
+ privacyPolicy,
+ orgSlug,
+ component,
+) => {
+ const textNodes = [];
+ const text = getText(textObj, language);
+ const privacyPolicyTitle = getText(privacyPolicy.title, language);
+ const termsAndConditionsTitle = getText(termsAndConditions.title, language);
+ if (text.includes("{terms_and_conditions}")) {
+ const Array1 = text.split("{terms_and_conditions}");
+ if (Array1[0].includes("{privacy_policy}")) {
+ const Array2 = Array1[0].split("{privacy_policy}");
+ textNodes.push(Array2[0]);
+ textNodes.push(
+
+ {privacyPolicyTitle}
+ ,
+ );
+ textNodes.push(Array2[1]);
+ textNodes.push(
+
+ {termsAndConditionsTitle}
+ ,
+ );
+ textNodes.push(Array1[1]);
+ } else if (Array1[1].includes("{privacy_policy}")) {
+ const Array2 = Array1[1].split("{privacy_policy}");
+ textNodes.push(Array1[0]);
+ textNodes.push(
+
+ {termsAndConditionsTitle}
+ ,
+ );
+ textNodes.push(Array2[0]);
+ textNodes.push(
+
+ {privacyPolicyTitle}
+ ,
+ );
+ textNodes.push(Array2[1]);
+ } else {
+ textNodes.push(Array1[0]);
+ textNodes.push(
+
+ {termsAndConditionsTitle}
+ ,
+ );
+ textNodes.push(Array1[1]);
+ }
+ } else if (text.includes("{privacy_policy}")) {
+ const Array1 = text.split("{privacy_policy}");
+ textNodes.push(Array1[0]);
+ textNodes.push(
+
+ {privacyPolicyTitle}
+ ,
+ );
+ textNodes.push(Array1[1]);
+ } else {
+ textNodes.push(text);
+ }
+ return textNodes;
+};
+export default renderAdditionalInfo;
diff --git a/org-configurations/default-configuration.yml b/org-configurations/default-configuration.yml
index 13833d00e..d77ddc07e 100644
--- a/org-configurations/default-configuration.yml
+++ b/org-configurations/default-configuration.yml
@@ -21,12 +21,14 @@ client:
# value
title: "Wifi Login"
+ auto_login: True
+
# path of favicon
favicon: null
# path of the custom css file relative to organization's folder in
# assets directory.
- css_path: null
+ css_path: "index.css"
languages:
- text: "english"
slug: "en"
@@ -63,7 +65,7 @@ client:
input_fields:
username:
type: "text"
- pattern: '[a-zA-Z@.+\-_]{1,150}'
+ pattern: '[a-zA-Z@.+\-_\d]{1,150}'
pattern_description:
en: "only letters, numbers, and @/./+/-/_ characters"
placeholder:
@@ -99,7 +101,14 @@ client:
en: "confirm"
buttons:
register:
- en: 'sign up' # text for register button
+ label: null
+ text:
+ en: "sign up"
+ additional_info_text:
+ en: "By signing up, you accept the {terms_and_conditions} and the {privacy_policy} of this WiFi service. "
+ links:
+ login:
+ en: "Already have an account? Log in"
reset_form:
heading:
@@ -152,34 +161,54 @@ client:
en: 'change password'
login_form:
- input_field:
+ header:
+ en: " sign in"
+ social_login:
+ divider_text:
+ en: "OR"
+ description:
+ en: "login with"
+ links:
+ - facebook:
+ text: null
+ url: null
+ icon: null #relative to organization's folder in assets directory.
+ input_fields:
username:
type: "text"
+ pattern: '[a-zA-Z@.+\-_\d]{1,150}'
+ pattern_description:
+ en: "only letters, numbers, and @/./+/-/_ characters"
placeholder:
- en: "Enter phone number or username"
+ en: "enter username"
label:
- en: "Phone number or username" # default username field label
- pattern: null # pattern for validation
+ en: "username"
password:
type: "password"
+ pattern: ".{6,}"
+ pattern_description:
+ en: "password must be a minimum of 6 characters"
placeholder:
- en: "Enter password"
+ en: "password"
label:
- en: "Password" # default password field
- pattern: null
+ en: "password"
+ additional_info_text:
+ en: "By logging in, you accept the {terms_and_conditions} and the {privacy_policy} of this WiFi service. "
buttons:
login:
- label:
- en: null # label field text for login button
- value:
- en: 'Login' # text for login button
- register:
- label:
- en: null # label field text for register button
- value:
- en: 'Sign up for free' # text for register button
+ label: null
+ text:
+ en: "sign in"
links:
- - forget_password :
- en: "Forgot your password?"
- - account:
- en: "Manage your account"
+ forget_password :
+ en: "Forgot your password?"
+ register:
+ en: "Not registered yet? Create an account"
+ privacy_policy:
+ title:
+ en: "privacy policy"
+ content: null
+ terms_and_conditions:
+ title:
+ en: "terms and conditions"
+ content: null
diff --git a/package-lock.json b/package-lock.json
index a85d2120a..f26b16f2e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1496,6 +1496,11 @@
"@babel/types": "^7.3.0"
}
},
+ "@types/cookie": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz",
+ "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow=="
+ },
"@types/events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
@@ -1513,6 +1518,15 @@
"@types/node": "*"
}
},
+ "@types/hoist-non-react-statics": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
+ "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
+ "requires": {
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0"
+ }
+ },
"@types/istanbul-lib-coverage": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz",
@@ -1556,6 +1570,25 @@
"integrity": "sha512-b8bbUOTwzIY3V5vDTY1fIJ+ePKDUBqt2hC2woVGotdQQhG/2Sh62HOKHrT7ab+VerXAcPyAiTEipPu/FsreUtg==",
"dev": true
},
+ "@types/object-assign": {
+ "version": "4.0.30",
+ "resolved": "https://registry.npmjs.org/@types/object-assign/-/object-assign-4.0.30.tgz",
+ "integrity": "sha1-iUk3HVqZ9Dge4PHfCpt6GH4H5lI="
+ },
+ "@types/prop-types": {
+ "version": "15.7.1",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.1.tgz",
+ "integrity": "sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg=="
+ },
+ "@types/react": {
+ "version": "16.8.23",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-16.8.23.tgz",
+ "integrity": "sha512-abkEOIeljniUN9qB5onp++g0EY38h7atnDHxwKUFz1r3VH1+yG1OKi2sNPTyObL40goBmfKFpdii2lEzwLX1cA==",
+ "requires": {
+ "@types/prop-types": "*",
+ "csstype": "^2.2.0"
+ }
+ },
"@types/stack-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
@@ -3511,6 +3544,11 @@
"cssom": "0.3.x"
}
},
+ "csstype": {
+ "version": "2.6.6",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.6.tgz",
+ "integrity": "sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg=="
+ },
"cyclist": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz",
@@ -5079,13 +5117,11 @@
},
"balanced-match": {
"version": "1.0.0",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
- "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -5102,8 +5138,7 @@
},
"concat-map": {
"version": "0.0.1",
- "bundled": true,
- "optional": true
+ "bundled": true
},
"console-control-strings": {
"version": "1.1.0",
@@ -5232,7 +5267,6 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
- "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -9098,6 +9132,16 @@
"scheduler": "^0.13.6"
}
},
+ "react-cookie": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.0.1.tgz",
+ "integrity": "sha512-h61qAtSXvfjNa81h3XCFdFoyFaF+nb7gjK0cxQuTiCPMPAe50D950FjLCFhaIfSpAesQFAmkxf5XFpWoEVBDAA==",
+ "requires": {
+ "@types/hoist-non-react-statics": "^3.0.1",
+ "hoist-non-react-statics": "^3.0.0",
+ "universal-cookie": "^4.0.0"
+ }
+ },
"react-dom": {
"version": "16.8.6",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.6.tgz",
@@ -11051,6 +11095,17 @@
"crypto-random-string": "^1.0.0"
}
},
+ "universal-cookie": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.2.tgz",
+ "integrity": "sha512-n14lhA//lQeYRweP9j9uXsshN9Cs4LunVSnvAGmnA69SofwsjpUU03geaCaPC9LlsH2rkBy99o3zxQyVOldGvA==",
+ "requires": {
+ "@types/cookie": "^0.3.3",
+ "@types/object-assign": "^4.0.30",
+ "cookie": "^0.4.0",
+ "object-assign": "^4.1.1"
+ }
+ },
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
diff --git a/package.json b/package.json
index 3e6566957..93d5537ec 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"prop-types": "^15.7.2",
"qs": "^6.7.0",
"react": "^16.8.6",
+ "react-cookie": "^4.0.1",
"react-dom": "^16.8.6",
"react-helmet": "^5.2.1",
"react-redux": "^7.1.0",
@@ -96,5 +97,10 @@
"\\.(css|less|sass|scss)$": "/__mocks__/styleMock.js",
"\\.(gif|ttf|eot|svg)$": "/__mocks__/fileMock.js"
}
+ },
+ "nodemonConfig": {
+ "ignore": [
+ "client/*"
+ ]
}
}