diff --git a/client/modules/accounts/components/auth/index.js b/client/modules/accounts/components/auth/index.js
new file mode 100644
index 00000000000..526fc99f7cf
--- /dev/null
+++ b/client/modules/accounts/components/auth/index.js
@@ -0,0 +1,3 @@
+export LoginButtons from "./loginButtons";
+export SignIn from "./signIn";
+export SignUp from "./signUp";
diff --git a/client/modules/accounts/components/auth/loginButtons.js b/client/modules/accounts/components/auth/loginButtons.js
new file mode 100644
index 00000000000..1b7b958d682
--- /dev/null
+++ b/client/modules/accounts/components/auth/loginButtons.js
@@ -0,0 +1,76 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import { Button, Divider, Translation } from "/imports/plugins/core/ui/client/components";
+
+class LoginButtons extends Component {
+ static propTypes = {
+ capitalizeName: PropTypes.func,
+ currentView: PropTypes.string,
+ loginServices: PropTypes.func,
+ onSeparator: PropTypes.func,
+ onSocialClick: PropTypes.func
+ }
+
+ renderLoginButtons() {
+ const enabledServices = this.props.loginServices().filter((service) =>{
+ return service.enabled;
+ });
+
+ return (
+
+ {this.props.loginServices &&
+ enabledServices.map((service) => (
+
+ ))
+ }
+
+ );
+ }
+
+ renderSeparator() {
+ if (this.props.onSeparator()) {
+ return (
+
+ );
+ }
+ }
+
+ render() {
+ return (
+
+ {this.renderLoginButtons()}
+ {this.renderSeparator()}
+
+ );
+ }
+}
+
+export default LoginButtons;
diff --git a/client/modules/accounts/components/auth/signIn.js b/client/modules/accounts/components/auth/signIn.js
new file mode 100644
index 00000000000..212fae21aaf
--- /dev/null
+++ b/client/modules/accounts/components/auth/signIn.js
@@ -0,0 +1,182 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import classnames from "classnames";
+import { Button, TextField, Translation } from "/imports/plugins/core/ui/client/components";
+
+class SignIn extends Component {
+ static propTypes = {
+ credentials: PropTypes.object,
+ isLoading: PropTypes.bool,
+ loginFormMessages: PropTypes.func,
+ messages: PropTypes.object,
+ onError: PropTypes.func,
+ onForgotPasswordClick: PropTypes.func,
+ onFormSubmit: PropTypes.func,
+ onSignUpClick: PropTypes.func,
+ uniqueId: PropTypes.string
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ email: props.credentials.email,
+ password: props.credentials.password
+ };
+
+ this.handleFieldChange = this.handleFieldChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ }
+
+ handleFieldChange = (event, value, field) => {
+ this.setState({
+ [field]: value
+ });
+ }
+
+ handleSubmit = (event) => {
+ if (this.props.onFormSubmit) {
+ this.props.onFormSubmit(event, this.state.email, this.state.password);
+ }
+ }
+
+ renderEmailErrors() {
+ if (this.props.onError(this.props.messages.errors && this.props.messages.errors.email)) {
+ return (
+
+
+
+ );
+ }
+ }
+
+ renderPasswordErrors() {
+ return (
+
+ {this.props.onError(this.props.messages.errors && this.props.messages.errors.password) &&
+ this.props.messages.errors.password.map((error, i) => (
+
+ ))
+ }
+
+ );
+ }
+
+ renderFormMessages() {
+ if (this.props.loginFormMessages) {
+ return (
+
+ {this.props.loginFormMessages()}
+
+ );
+ }
+ }
+
+ renderSpinnerOnWait() {
+ if (this.props.isLoading === true) {
+ return (
+
+
+
+ );
+ }
+ return (
+
+ );
+ }
+
+ render() {
+ const emailClasses = classnames({
+ "form-group": true,
+ "form-group-email": true,
+ "has-error has-feedback": this.props.onError(this.props.messages.errors && this.props.messages.errors.email)
+ });
+
+ const passwordClasses = classnames({
+ "form-group": true,
+ "has-error has-feedback": this.props.onError(this.props.messages.errors && this.props.messages.errors.password)
+ });
+ return (
+
+ );
+ }
+}
+
+export default SignIn;
diff --git a/client/modules/accounts/components/auth/signUp.js b/client/modules/accounts/components/auth/signUp.js
new file mode 100644
index 00000000000..bc73024e606
--- /dev/null
+++ b/client/modules/accounts/components/auth/signUp.js
@@ -0,0 +1,185 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import classnames from "classnames";
+import { Button, TextField, Translation } from "/imports/plugins/core/ui/client/components";
+
+class SignUp extends Component {
+ static propTypes = {
+ credentials: PropTypes.object,
+ hasPasswordService: PropTypes.func,
+ isLoading: PropTypes.bool,
+ loginFormMessages: PropTypes.func,
+ messages: PropTypes.object,
+ onError: PropTypes.func,
+ onFormSubmit: PropTypes.func,
+ onSignInClick: PropTypes.func,
+ uniqueId: PropTypes.string
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ email: props.credentials.email,
+ password: props.credentials.password
+ };
+
+ this.handleFieldChange = this.handleFieldChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ }
+
+ handleFieldChange = (event, value, field) => {
+ this.setState({
+ [field]: value
+ });
+ }
+
+ handleSubmit = (event) => {
+ if (this.props.onFormSubmit) {
+ this.props.onFormSubmit(event, this.state.email, this.state.password);
+ }
+ }
+
+ renderEmailErrors() {
+ if (this.props.onError(this.props.messages.errors && this.props.messages.errors.email)) {
+ return (
+
+
+
+ );
+ }
+ }
+
+ renderPasswordErrors() {
+ return (
+
+ {this.props.onError(this.props.messages.errors && this.props.messages.errors.password) &&
+ this.props.messages.errors.password.map((error, i) => (
+
+ ))
+ }
+
+ );
+ }
+
+ renderFormMessages() {
+ if (this.props.loginFormMessages) {
+ return (
+
+ {this.props.loginFormMessages()}
+
+ );
+ }
+ }
+
+ renderSpinnerOnWait() {
+ if (this.props.isLoading === true) {
+ return (
+
+
+
+ );
+ }
+ return (
+
+ );
+ }
+
+ renderForm(emailClasses, passwordClasses) {
+ if (this.props.hasPasswordService()) {
+ return (
+
+ );
+ }
+ }
+
+ render() {
+ const emailClasses = classnames({
+ "form-group": true,
+ "form-group-email": true,
+ "has-error has-feedback": this.props.onError(this.props.messages.errors && this.props.messages.errors.email)
+ });
+
+ const passwordClasses = classnames({
+ "form-group": true,
+ "form-group-password": true,
+ "has-error has-feedback": this.props.onError(this.props.messages.errors && this.props.messages.errors.password)
+ });
+ return (
+
+
+
+
+
+
+
+ {this.renderForm(emailClasses, passwordClasses)}
+
+
+ );
+ }
+}
+
+export default SignUp;
diff --git a/client/modules/accounts/components/dropdown/mainDropdown.js b/client/modules/accounts/components/dropdown/mainDropdown.js
new file mode 100644
index 00000000000..6dbb66dca19
--- /dev/null
+++ b/client/modules/accounts/components/dropdown/mainDropdown.js
@@ -0,0 +1,132 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import { Reaction } from "/client/api";
+import { Button, DropDownMenu, MenuItem, Translation } from "/imports/plugins/core/ui/client/components";
+import { LoginContainer } from "../../containers/auth";
+
+const iconStyle = {
+ margin: "10px 10px 10px 6px",
+ width: "20px",
+ fontSize: "inherit",
+ textAlign: "center"
+};
+
+const menuStyle = {
+ padding: "0px 10px 10px 10px",
+ minWidth: 220,
+ minHeight: 50
+};
+
+class MainDropdown extends Component {
+ static propTypes = {
+ adminShortcuts: PropTypes.object,
+ currentUser: PropTypes.oneOfType(
+ [PropTypes.bool, PropTypes.object]
+ ),
+ handleChange: PropTypes.func,
+ userImage: PropTypes.oneOfType(
+ [PropTypes.bool, PropTypes.string]
+ ),
+ userName: PropTypes.string,
+ userShortcuts: PropTypes.object
+ }
+
+ buttonElement() {
+ return (
+
+ );
+ }
+
+ renderAdminIcons() {
+ return (
+ Reaction.Apps(this.props.adminShortcuts).map((shortcut) => (
+
+ ))
+ );
+ }
+
+ renderUserIcons() {
+ return (
+ Reaction.Apps(this.props.userShortcuts).map((option) => (
+
+ ))
+ );
+ }
+
+ renderSignOutButton() {
+ return (
+
+ );
+ }
+
+ renderSignInComponent() {
+ return (
+
+ );
+ }
+
+ render() {
+ return (
+
+ {this.props.currentUser ?
+
+
+
+ {this.renderUserIcons()}
+ {this.renderAdminIcons()}
+ {this.renderSignOutButton()}
+
+
+
+ :
+
+ {this.renderSignInComponent()}
+
+ }
+
+ );
+ }
+}
+
+export default MainDropdown;
diff --git a/client/modules/accounts/components/helpers/index.js b/client/modules/accounts/components/helpers/index.js
new file mode 100644
index 00000000000..be7e8d25998
--- /dev/null
+++ b/client/modules/accounts/components/helpers/index.js
@@ -0,0 +1 @@
+export LoginFormMessages from "./loginFormMessages";
diff --git a/client/modules/accounts/components/helpers/loginFormMessages.js b/client/modules/accounts/components/helpers/loginFormMessages.js
new file mode 100644
index 00000000000..a07b8ccc0d3
--- /dev/null
+++ b/client/modules/accounts/components/helpers/loginFormMessages.js
@@ -0,0 +1,41 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+
+class LoginFormMessages extends Component {
+ static propTypes = {
+ formMessages: PropTypes.object,
+ loginFormMessages: PropTypes.func
+ }
+
+ renderFormMessages() {
+ if (this.props.loginFormMessages) {
+ if (this.props.formMessages.info) {
+ return (
+
+
+ {this.props.loginFormMessages()}
+
+
+ );
+ } else if (this.props.formMessages.alerts) {
+ return (
+
+
+ {this.props.loginFormMessages()}
+
+
+ );
+ }
+ }
+ }
+
+ render() {
+ return (
+
+ {this.renderFormMessages()}
+
+ );
+ }
+}
+
+export default LoginFormMessages;
diff --git a/client/modules/accounts/components/index.js b/client/modules/accounts/components/index.js
new file mode 100644
index 00000000000..2d0c5401ca8
--- /dev/null
+++ b/client/modules/accounts/components/index.js
@@ -0,0 +1,3 @@
+export { SignIn, SignUp, LoginButtons } from "./auth";
+export { Forgot, UpdatePasswordOverlay } from "./passwordReset";
+export { LoginFormMessages } from "./helpers";
diff --git a/client/modules/accounts/components/passwordReset/forgot.js b/client/modules/accounts/components/passwordReset/forgot.js
new file mode 100644
index 00000000000..187aa7321a2
--- /dev/null
+++ b/client/modules/accounts/components/passwordReset/forgot.js
@@ -0,0 +1,142 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import classnames from "classnames";
+import { Button, TextField, Translation } from "/imports/plugins/core/ui/client/components";
+
+class Forgot extends Component {
+ static propTypes = {
+ credentials: PropTypes.object,
+ isDisabled: PropTypes.bool,
+ isLoading: PropTypes.bool,
+ loginFormMessages: PropTypes.func,
+ messages: PropTypes.object,
+ onError: PropTypes.func,
+ onFormSubmit: PropTypes.func,
+ onSignInClick: PropTypes.func,
+ uniqueId: PropTypes.string
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ email: props.credentials.email
+ };
+
+ this.handleFieldChange = this.handleFieldChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ }
+
+ handleFieldChange = (event, value, field) => {
+ this.setState({
+ [field]: value
+ });
+ }
+
+ handleSubmit = (event) => {
+ if (this.props.onFormSubmit) {
+ this.props.onFormSubmit(event, this.state.email);
+ }
+ }
+
+ renderFormMessages() {
+ if (this.props.loginFormMessages) {
+ return (
+
+ {this.props.loginFormMessages()}
+
+ );
+ }
+ }
+
+ renderEmailErrors() {
+ if (this.props.onError(this.props.messages.errors && this.props.messages.errors.email)) {
+ return (
+
+
+
+ );
+ }
+ }
+
+ renderSpinnerOnWait() {
+ if (this.props.isLoading === true) {
+ return (
+
+
+
+ );
+ }
+ return (
+
+ );
+ }
+
+ render() {
+ const emailClasses = classnames({
+ "form-group": true,
+ "form-group-email": true,
+ "has-error has-feedback": this.props.onError(this.props.messages.errors && this.props.messages.errors.email)
+ });
+
+ return (
+
+ );
+ }
+}
+
+export default Forgot;
diff --git a/client/modules/accounts/components/passwordReset/index.js b/client/modules/accounts/components/passwordReset/index.js
new file mode 100644
index 00000000000..dffa716c048
--- /dev/null
+++ b/client/modules/accounts/components/passwordReset/index.js
@@ -0,0 +1,2 @@
+export Forgot from "./forgot";
+export UpdatePasswordOverlay from "./updatePasswordOverlay";
diff --git a/client/modules/accounts/components/passwordReset/updatePasswordOverlay.js b/client/modules/accounts/components/passwordReset/updatePasswordOverlay.js
new file mode 100644
index 00000000000..be9a6ed754d
--- /dev/null
+++ b/client/modules/accounts/components/passwordReset/updatePasswordOverlay.js
@@ -0,0 +1,164 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import classnames from "classnames";
+import { Button, TextField, Translation } from "/imports/plugins/core/ui/client/components";
+
+class UpdatePasswordOverlay extends Component {
+ static propTypes = {
+ isDisabled: PropTypes.bool,
+ isOpen: PropTypes.bool,
+ loginFormMessages: PropTypes.func,
+ messages: PropTypes.object,
+ onCancel: PropTypes.func,
+ onError: PropTypes.func,
+ onFormSubmit: PropTypes.func,
+ uniqueId: PropTypes.string
+ }
+
+ constructor() {
+ super();
+
+ this.state = {
+ password: ""
+ };
+
+ this.handleFieldChange = this.handleFieldChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleCancel = this.handleCancel.bind(this);
+ }
+
+ handleFieldChange = (event, value, field) => {
+ this.setState({
+ [field]: value
+ });
+ }
+
+ handleSubmit = (event) => {
+ if (this.props.onFormSubmit) {
+ this.props.onFormSubmit(event, this.state.password);
+ }
+ }
+
+ handleCancel = (event) => {
+ if (this.props.onCancel) {
+ this.props.onCancel(event);
+ }
+ }
+
+ renderFormMessages() {
+ if (this.props.loginFormMessages) {
+ return (
+
+ {this.props.loginFormMessages()}
+
+ );
+ }
+ }
+
+ renderPasswordErrors() {
+ return (
+
+ {this.props.onError(this.props.messages.errors && this.props.messages.errors.password) &&
+ this.props.messages.errors.password.map((error, i) => (
+
+ ))
+ }
+
+ );
+ }
+
+ renderSpinnerOnWait() {
+ if (this.props.isDisabled === true) {
+ return (
+
+
+
+ );
+ }
+ return (
+
+
+
+ );
+ }
+
+ render() {
+ const passwordClasses = classnames({
+ "form-group": true,
+ "has-error has-feedback": this.props.onError(this.props.messages.errors && this.props.messages.errors.password)
+ });
+
+ return (
+
+ {this.props.isOpen === true &&
+
}
+
+ );
+ }
+}
+
+export default UpdatePasswordOverlay;
diff --git a/client/modules/accounts/containers/auth/authContainer.js b/client/modules/accounts/containers/auth/authContainer.js
new file mode 100644
index 00000000000..d81aecd8850
--- /dev/null
+++ b/client/modules/accounts/containers/auth/authContainer.js
@@ -0,0 +1,220 @@
+import _ from "lodash";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import { Meteor } from "meteor/meteor";
+import { Router } from "/client/api";
+import { composeWithTracker } from "/lib/api/compose";
+import { SignIn, SignUp, LoginButtons } from "../../components";
+import { MessagesContainer } from "../helpers";
+import { ServiceConfigHelper } from "../../helpers";
+import { LoginFormSharedHelpers } from "/client/modules/accounts/helpers";
+import { LoginFormValidation } from "/lib/api";
+
+class AuthContainer extends Component {
+ static propTypes = {
+ currentRoute: PropTypes.object,
+ currentView: PropTypes.string,
+ formMessages: PropTypes.object
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ formMessages: props.formMessages,
+ isLoading: false
+ };
+
+ this.handleFormSubmit = this.handleFormSubmit.bind(this);
+ this.hasError = this.hasError.bind(this);
+ this.formMessages = this.formMessages.bind(this);
+ this.services = this.services.bind(this);
+ this.shouldShowSeperator = this.shouldShowSeperator.bind(this);
+ this.handleSocialLogin = this.handleSocialLogin.bind(this);
+ this.capitalizeName = this.capitalizeName.bind(this);
+ this.hasPasswordService = this.hasPasswordService.bind(this);
+ }
+
+ handleFormSubmit = (event, email, password) => {
+ event.preventDefault();
+
+ this.setState({
+ isLoading: true
+ });
+ const errors = {};
+ const username = email.trim();
+ const pword = password.trim();
+
+ const validatedEmail = LoginFormValidation.email(username);
+ const validatedPassword = LoginFormValidation.password(pword, { validationLevel: "exists" });
+
+ if (validatedEmail !== true) {
+ errors.email = validatedEmail;
+ }
+
+ if (validatedPassword !== true) {
+ errors.password = validatedPassword;
+ }
+
+ if (_.isEmpty(errors) === false) {
+ this.setState({
+ isLoading: false,
+ formMessages: {
+ errors: errors
+ }
+ });
+ return;
+ }
+
+ if (this.props.currentView === "loginFormSignInView") {
+ Meteor.loginWithPassword(username, pword, (error) => {
+ if (error) {
+ this.setState({
+ isLoading: false,
+ formMessages: {
+ alerts: [error]
+ }
+ });
+ } else {
+ Router.go(this.props.currentRoute.route.path);
+ }
+ });
+ } else if (this.props.currentView === "loginFormSignUpView") {
+ const newUserData = {
+ email: username,
+ password: pword
+ };
+
+ Accounts.createUser(newUserData, (error) => {
+ if (error) {
+ this.setState({
+ isLoading: false,
+ formMessages: {
+ alerts: [error]
+ }
+ });
+ } else {
+ Router.go(this.props.currentRoute.route.path);
+ }
+ });
+ }
+ }
+
+ hasError = (error) => {
+ // True here means the field is valid
+ // We're checking if theres some other message to display
+ if (error !== true && typeof error !== "undefined") {
+ return true;
+ }
+
+ return false;
+ }
+
+ formMessages = () => {
+ return (
+
+ );
+ }
+
+ services = () => {
+ const serviceHelper = new ServiceConfigHelper();
+ return serviceHelper.services();
+ }
+
+ shouldShowSeperator = () => {
+ const serviceHelper = new ServiceConfigHelper();
+ const services = serviceHelper.services();
+ const enabledServices = _.filter(services, {
+ enabled: true
+ });
+
+ return !!Package["accounts-password"] && enabledServices.length > 0;
+ }
+
+ capitalizeName = (str) => {
+ return LoginFormSharedHelpers.capitalize(str);
+ }
+
+ handleSocialLogin = (value) => {
+ let serviceName = value;
+
+ // Get proper service name
+ if (serviceName === "meteor-developer") {
+ serviceName = "MeteorDeveloperAccount";
+ } else {
+ serviceName = this.capitalizeName(serviceName);
+ }
+
+ const loginWithService = Meteor["loginWith" + serviceName];
+ const options = {}; // use default scope unless specified
+
+ loginWithService(options, (error) => {
+ if (error) {
+ this.setState({
+ formMessages: {
+ alerts: [error]
+ }
+ });
+ }
+ });
+ }
+
+ hasPasswordService = () => {
+ return !!Package["accounts-password"];
+ }
+
+ renderAuthView() {
+ if (this.props.currentView === "loginFormSignInView") {
+ return (
+
+ );
+ } else if (this.props.currentView === "loginFormSignUpView") {
+ return (
+
+ );
+ }
+ }
+
+ render() {
+ return (
+
+
+ {this.renderAuthView()}
+
+ );
+ }
+}
+
+function composer(props, onData) {
+ const formMessages = {};
+
+ onData(null, {
+ formMessages,
+ currentRoute: Router.current()
+ });
+}
+
+export default composeWithTracker(composer)(AuthContainer);
diff --git a/client/modules/accounts/containers/auth/index.js b/client/modules/accounts/containers/auth/index.js
new file mode 100644
index 00000000000..7e3508baa54
--- /dev/null
+++ b/client/modules/accounts/containers/auth/index.js
@@ -0,0 +1,2 @@
+export AuthContainer from "./authContainer";
+export LoginContainer from "./loginContainer";
diff --git a/client/modules/accounts/containers/auth/loginContainer.js b/client/modules/accounts/containers/auth/loginContainer.js
new file mode 100644
index 00000000000..19de8c65745
--- /dev/null
+++ b/client/modules/accounts/containers/auth/loginContainer.js
@@ -0,0 +1,94 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import { composeWithTracker } from "/lib/api/compose";
+import AuthContainer from "./authContainer";
+import { ForgotContainer } from "../passwordReset";
+
+class LoginContainer extends Component {
+ static propTypes = {
+ credentials: PropTypes.object,
+ loginFormCurrentView: PropTypes.string,
+ uniqueId: PropTypes.string
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ currentView: props.loginFormCurrentView
+ };
+
+ this.showForgotPasswordView = this.showForgotPasswordView.bind(this);
+ this.showSignUpView = this.showSignUpView.bind(this);
+ this.showSignInView = this.showSignInView.bind(this);
+ }
+
+ showForgotPasswordView(event) {
+ event.preventDefault();
+
+ this.setState({
+ currentView: "loginFormResetPasswordView"
+ });
+ }
+
+ showSignUpView(event) {
+ event.preventDefault();
+
+ this.setState({
+ currentView: "loginFormSignUpView"
+ });
+ }
+
+ showSignInView(event) {
+ event.preventDefault();
+
+ this.setState({
+ currentView: "loginFormSignInView"
+ });
+ }
+
+ render() {
+ if (this.state.currentView === "loginFormSignInView" || this.state.currentView === "loginFormSignUpView") {
+ return (
+
+ );
+ } else if (this.state.currentView === "loginFormResetPasswordView") {
+ return (
+
+ );
+ }
+ }
+
+}
+
+function composer(props, onData) {
+ let startView = "loginFormSignInView";
+
+ if (props) {
+ if (props.startView) {
+ startView = props.startView;
+ }
+ }
+ const uniqueId = Random.id();
+ const credentials = {};
+
+ onData(null, {
+ loginFormCurrentView: startView,
+ uniqueId,
+ credentials
+ });
+}
+
+export default composeWithTracker(composer)(LoginContainer);
diff --git a/client/modules/accounts/containers/dropdown/mainDropdownContainer.js b/client/modules/accounts/containers/dropdown/mainDropdownContainer.js
new file mode 100644
index 00000000000..53162617f58
--- /dev/null
+++ b/client/modules/accounts/containers/dropdown/mainDropdownContainer.js
@@ -0,0 +1,153 @@
+import React, { Component } from "react";
+import { Meteor } from "meteor/meteor";
+import { Accounts } from "meteor/accounts-base";
+import { Roles } from "meteor/alanning:roles";
+import { Reaction } from "/client/api";
+import { i18nextDep, i18next } from "/client/api";
+import { composeWithTracker } from "/lib/api/compose";
+import * as Collections from "/lib/collections";
+import { Tags } from "/lib/collections";
+import MainDropdown from "../../components/dropdown/mainDropdown";
+
+class MainDropdownContainer extends Component {
+ constructor(props) {
+ super(props);
+ this.handleChange = this.handleChange.bind(this);
+ }
+
+ handleChange = (event, value) => {
+ event.preventDefault();
+
+ if (value === "logout") {
+ return Meteor.logout((error) => {
+ if (error) {
+ Logger.warn("Failed to logout.", error);
+ }
+ });
+ }
+
+ if (value.name === "createProduct") {
+ Reaction.setUserPreferences("reaction-dashboard", "viewAs", "administrator");
+ Meteor.call("products/createProduct", (error, productId) => {
+ let currentTag;
+ let currentTagId;
+
+ if (error) {
+ throw new Meteor.Error("createProduct error", error);
+ } else if (productId) {
+ currentTagId = Session.get("currentTag");
+ currentTag = Tags.findOne(currentTagId);
+ if (currentTag) {
+ Meteor.call("products/updateProductTags", productId, currentTag.name, currentTagId);
+ }
+ // go to new product
+ Reaction.Router.go("product", {
+ handle: productId
+ });
+ }
+ });
+ } else if (value.name !== "account/profile") {
+ return Reaction.showActionView(value);
+ } else if (value.route || value.name) {
+ const route = value.name || value.route;
+ return Reaction.Router.go(route);
+ }
+ }
+
+ render() {
+ return (
+
+
+
+ );
+ }
+}
+
+function getCurrentUser() {
+ if (typeof Reaction === "object") {
+ const shopId = Reaction.getShopId();
+ const user = Accounts.user();
+ if (!shopId || typeof user !== "object") return null;
+ // shoppers should always be guests
+ const isGuest = Roles.userIsInRole(user, "guest", shopId);
+ // but if a user has never logged in then they are anonymous
+ const isAnonymous = Roles.userIsInRole(user, "anonymous", shopId);
+
+ return isGuest && !isAnonymous ? user : null;
+ }
+ return null;
+}
+
+function getUserGravatar(currentUser, size) {
+ const options = {
+ secure: true,
+ size: size,
+ default: "identicon"
+ };
+ const user = currentUser || Accounts.user();
+ if (!user) return false;
+ const account = Collections.Accounts.findOne(user._id);
+ // first we check picture exists. Picture has higher priority to display
+ if (account && account.profile && account.profile.picture) {
+ return account.profile.picture;
+ }
+ if (user.emails && user.emails.length === 1) {
+ const email = user.emails[0].address;
+ return Gravatar.imageUrl(email, options);
+ }
+}
+
+function displayName(displayUser) {
+ i18nextDep.depend();
+
+ const user = displayUser || Accounts.user();
+ if (user) {
+ if (user.profile && user.profile.name) {
+ return user.profile.name;
+ } else if (user.username) {
+ return user.username;
+ }
+
+ // todo: previous check was user.services !== "anonymous", "resume". Is this
+ // new check covers previous check?
+ if (Roles.userIsInRole(user._id || user.userId, "account/profile",
+ Reaction.getShopId())) {
+ return i18next.t("accountsUI.guest", { defaultValue: "Guest" });
+ }
+ }
+}
+
+function getAdminShortcutIcons() {
+ // get shortcuts with audience permissions based on user roles
+ const roles = Roles.getRolesForUser(Meteor.userId(), Reaction.getShopId());
+
+ return {
+ provides: "shortcut",
+ enabled: true,
+ audience: roles
+ };
+}
+
+const composer = (props, onData) => {
+ const currentUser = getCurrentUser();
+ const userImage = getUserGravatar(currentUser, 40);
+ const userName = displayName(currentUser);
+ const adminShortcuts = getAdminShortcutIcons();
+ const userShortcuts = {
+ provides: "userAccountDropdown",
+ enabled: true
+ };
+
+ onData(null, {
+ adminShortcuts,
+ currentUser,
+ userImage,
+ userName,
+ userShortcuts
+ });
+};
+
+export default composeWithTracker(composer)(MainDropdownContainer);
diff --git a/client/modules/accounts/containers/helpers/index.js b/client/modules/accounts/containers/helpers/index.js
new file mode 100644
index 00000000000..ec71f9a8f79
--- /dev/null
+++ b/client/modules/accounts/containers/helpers/index.js
@@ -0,0 +1 @@
+export MessagesContainer from "./messagesContainer";
diff --git a/client/modules/accounts/containers/helpers/messagesContainer.js b/client/modules/accounts/containers/helpers/messagesContainer.js
new file mode 100644
index 00000000000..a7b5a6b85f2
--- /dev/null
+++ b/client/modules/accounts/containers/helpers/messagesContainer.js
@@ -0,0 +1,40 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import { LoginFormMessages } from "../../components";
+
+class MessagesContainer extends Component {
+ static propTypes = {
+ messages: PropTypes.object
+ }
+
+ constructor() {
+ super();
+
+ this.loginFormMessages = this.loginFormMessages.bind(this);
+ }
+
+ loginFormMessages = () => {
+ let reasons = "";
+ if (this.props.messages.info) {
+ this.props.messages.info.forEach(function (info) {
+ reasons = info.reason;
+ });
+ } else if (this.props.messages.alerts) {
+ this.props.messages.alerts.forEach(function (alert) {
+ reasons = alert.reason;
+ });
+ }
+ return reasons;
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+export default MessagesContainer;
diff --git a/client/modules/accounts/containers/index.js b/client/modules/accounts/containers/index.js
new file mode 100644
index 00000000000..7c3d195f51b
--- /dev/null
+++ b/client/modules/accounts/containers/index.js
@@ -0,0 +1,3 @@
+export { LoginContainer, AuthContainer } from "./auth";
+export { ForgotContainer, UpdatePasswordOverlayContainer } from "./passwordReset";
+export { MessagesContainer } from "./helpers";
diff --git a/client/modules/accounts/containers/passwordReset/forgotContainer.js b/client/modules/accounts/containers/passwordReset/forgotContainer.js
new file mode 100644
index 00000000000..9fbe0d67e80
--- /dev/null
+++ b/client/modules/accounts/containers/passwordReset/forgotContainer.js
@@ -0,0 +1,121 @@
+import _ from "lodash";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import { Meteor } from "meteor/meteor";
+import { i18next } from "/client/api";
+import { composeWithTracker } from "/lib/api/compose";
+import { MessagesContainer } from "../helpers";
+import { Forgot } from "../../components";
+
+class ForgotContainer extends Component {
+ static propTypes = {
+ formMessages: PropTypes.object
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ formMessages: props.formMessages,
+ isLoading: false,
+ isDisabled: false
+ };
+
+ this.handleFormSubmit = this.handleFormSubmit.bind(this);
+ this.formMessages = this.formMessages.bind(this);
+ this.hasError = this.hasError.bind(this);
+ }
+
+ handleFormSubmit = (event, email) => {
+ event.preventDefault();
+
+ this.setState({
+ isLoading: true
+ });
+
+ let newEmail;
+ email === undefined ? newEmail = "" : newEmail = email;
+
+ const emailAddress = newEmail.trim();
+ const validatedEmail = LoginFormValidation.email(emailAddress);
+ const errors = {};
+
+ if (validatedEmail !== true) {
+ errors.email = validatedEmail;
+ }
+
+ if (_.isEmpty(errors) === false) {
+ this.setState({
+ isLoading: false,
+ formMessages: {
+ errors: errors
+ }
+ });
+ return;
+ }
+
+ Meteor.call("accounts/sendResetPasswordEmail", { email: emailAddress }, (error) => {
+ // Show some message confirming result
+ if (error) {
+ this.setState({
+ isLoading: false,
+ formMessages: {
+ alerts: [error]
+ }
+ });
+ } else {
+ this.setState({
+ isLoading: false,
+ isDisabled: true,
+ formMessages: {
+ info: [{
+ reason: i18next.t("accountsUI.info.passwordResetSend") || "Password reset mail sent."
+ }]
+ }
+ });
+ }
+ });
+ }
+
+ formMessages = () => {
+ return (
+
+ );
+ }
+
+ hasError = (error) => {
+ // True here means the field is valid
+ // We're checking if theres some other message to display
+ if (error !== true && typeof error !== "undefined") {
+ return true;
+ }
+
+ return false;
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+function composer(props, onData) {
+ const formMessages = {};
+
+ onData(null, {
+ formMessages
+ });
+}
+
+export default composeWithTracker(composer)(ForgotContainer);
diff --git a/client/modules/accounts/containers/passwordReset/index.js b/client/modules/accounts/containers/passwordReset/index.js
new file mode 100644
index 00000000000..ff6c77aa157
--- /dev/null
+++ b/client/modules/accounts/containers/passwordReset/index.js
@@ -0,0 +1,2 @@
+export ForgotContainer from "./forgotContainer";
+export UpdatePasswordOverlayContainer from "./passwordOverlayContainer";
diff --git a/client/modules/accounts/containers/passwordReset/passwordOverlayContainer.js b/client/modules/accounts/containers/passwordReset/passwordOverlayContainer.js
new file mode 100644
index 00000000000..b95fa281250
--- /dev/null
+++ b/client/modules/accounts/containers/passwordReset/passwordOverlayContainer.js
@@ -0,0 +1,127 @@
+import _ from "lodash";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import { Accounts } from "meteor/accounts-base";
+import { composeWithTracker } from "/lib/api/compose";
+import { UpdatePasswordOverlay } from "/client/modules/accounts/components";
+import { MessagesContainer } from "/client/modules/accounts/containers/helpers";
+
+class UpdatePasswordOverlayContainer extends Component {
+ static propTypes = {
+ callback: PropTypes.func,
+ formMessages: PropTypes.object,
+ isOpen: PropTypes.bool,
+ token: PropTypes.string,
+ uniqueId: PropTypes.string
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ formMessages: props.formMessages,
+ isOpen: props.isOpen,
+ isDisabled: false
+ };
+
+ this.handleFormSubmit = this.handleFormSubmit.bind(this);
+ this.handleFormCancel = this.handleFormCancel.bind(this);
+ this.formMessages = this.formMessages.bind(this);
+ this.hasError = this.hasError.bind(this);
+ }
+
+ handleFormSubmit = (event, passwordValue) => {
+ event.preventDefault();
+
+ this.setState({
+ isDisabled: true
+ });
+
+ const password = passwordValue.trim();
+ const validatedPassword = LoginFormValidation.password(password);
+ const errors = {};
+
+ if (validatedPassword !== true) {
+ errors.password = validatedPassword;
+ }
+
+ if (_.isEmpty(errors) === false) {
+ this.setState({
+ isDisabled: false,
+ formMessages: {
+ errors: errors
+ }
+ });
+ return;
+ }
+
+ Accounts.resetPassword(this.props.token, password, (error) => {
+ if (error) {
+ this.setState({
+ isDisabled: false,
+ formMessages: {
+ alerts: [error]
+ }
+ });
+ } else {
+ this.props.callback();
+
+ this.setState({
+ isOpen: !this.state.isOpen
+ });
+ }
+ });
+ }
+
+ handleFormCancel = (event) => {
+ event.preventDefault();
+ this.setState({
+ isOpen: !this.state.isOpen
+ });
+ }
+
+ formMessages = () => {
+ return (
+
+ );
+ }
+
+ hasError = (error) => {
+ // True here means the field is valid
+ // We're checking if theres some other message to display
+ if (error !== true && typeof error !== "undefined") {
+ return true;
+ }
+
+ return false;
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+function composer(props, onData) {
+ const uniqueId = Random.id();
+ const formMessages = {};
+
+ onData(null, {
+ uniqueId,
+ formMessages
+ });
+}
+
+export default composeWithTracker(composer)(UpdatePasswordOverlayContainer);
diff --git a/client/modules/accounts/helpers/helpers.js b/client/modules/accounts/helpers/helpers.js
index 74ae429fe8c..28c30b14523 100644
--- a/client/modules/accounts/helpers/helpers.js
+++ b/client/modules/accounts/helpers/helpers.js
@@ -1,9 +1,6 @@
-import _ from "lodash";
-import { ServiceConfigHelper } from "./util";
import { Template } from "meteor/templating";
export const LoginFormSharedHelpers = {
-
messages: function () {
return Template.instance().formMessages.get();
},
@@ -14,34 +11,9 @@ export const LoginFormSharedHelpers = {
if (error !== true && typeof error !== "undefined") {
return "has-error has-feedback";
}
-
- return false;
- },
-
- formErrors() {
- return Template.instance().formErrors.get();
- },
-
- uniqueId: function () {
- return Template.instance().uniqueId;
},
-
- services() {
- const serviceHelper = new ServiceConfigHelper();
- return serviceHelper.services();
- },
-
- shouldShowSeperator() {
- const serviceHelper = new ServiceConfigHelper();
- const services = serviceHelper.services();
- const enabledServices = _.filter(services, {
- enabled: true
- });
-
- return !!Package["accounts-password"] && enabledServices.length > 0;
- },
-
- hasPasswordService() {
- return !!Package["accounts-password"];
+ capitalize: function (str) {
+ const finalString = str === null ? "" : String(str);
+ return finalString.charAt(0).toUpperCase() + finalString.slice(1);
}
};
diff --git a/client/modules/accounts/templates/dropdown/dropdown.html b/client/modules/accounts/templates/dropdown/dropdown.html
index 9bad2ff4f53..e8bf1d42cd4 100644
--- a/client/modules/accounts/templates/dropdown/dropdown.html
+++ b/client/modules/accounts/templates/dropdown/dropdown.html
@@ -3,7 +3,7 @@
{{#if currentUser}}
{{!--> avatar user=currentUser class="circular-icon" size="small" shape="circle"--}}
-
+
{{displayName}}