diff --git a/client/coral-framework/graphql/mutations.js b/client/coral-framework/graphql/mutations.js
index 29ec3fc96..dd54d0438 100644
--- a/client/coral-framework/graphql/mutations.js
+++ b/client/coral-framework/graphql/mutations.js
@@ -321,6 +321,50 @@ const SetUsernameFragment = gql`
}
`;
+export const withAttachLocalAuth = withMutation(
+ gql`
+ mutation AttachLocalAuth($input: AttachLocalAuthInput!) {
+ attachLocalAuth(input: $input) {
+ ...AttachLocalAuthResponse
+ }
+ }
+ `,
+ {
+ props: ({ mutate }) => ({
+ attachLocalAuth: input => {
+ return mutate({
+ variables: {
+ input,
+ },
+ update: proxy => {
+ const AttachLocalAuthQuery = gql`
+ query Talk_AttachLocalAuth {
+ me {
+ id
+ email
+ }
+ }
+ `;
+
+ const prev = proxy.readQuery({ query: AttachLocalAuthQuery });
+
+ const data = update(prev, {
+ me: {
+ email: { $set: input.email },
+ },
+ });
+
+ proxy.writeQuery({
+ query: AttachLocalAuthQuery,
+ data,
+ });
+ },
+ });
+ },
+ }),
+ }
+);
+
export const withUpdateEmailAddress = withMutation(
gql`
mutation UpdateEmailAddress($input: UpdateEmailAddressInput!) {
diff --git a/plugin-api/beta/client/hocs/index.js b/plugin-api/beta/client/hocs/index.js
index d33e7acc9..910969227 100644
--- a/plugin-api/beta/client/hocs/index.js
+++ b/plugin-api/beta/client/hocs/index.js
@@ -28,5 +28,6 @@ export {
withChangePassword,
withChangeUsername,
withUpdateEmailAddress,
+ withAttachLocalAuth,
} from 'coral-framework/graphql/mutations';
export { compose } from 'recompose';
diff --git a/plugins/talk-plugin-facebook-auth/client/components/ErrorMessage.css b/plugins/talk-plugin-facebook-auth/client/components/ErrorMessage.css
new file mode 100644
index 000000000..039ffae30
--- /dev/null
+++ b/plugins/talk-plugin-facebook-auth/client/components/ErrorMessage.css
@@ -0,0 +1,8 @@
+.errorMsg {
+ color: #FA4643;
+ font-size: 0.9em;
+}
+
+.warningIcon {
+ color: #FA4643;
+}
diff --git a/plugins/talk-plugin-facebook-auth/client/components/ErrorMessage.js b/plugins/talk-plugin-facebook-auth/client/components/ErrorMessage.js
new file mode 100644
index 000000000..f39a8fc08
--- /dev/null
+++ b/plugins/talk-plugin-facebook-auth/client/components/ErrorMessage.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styles from './ErrorMessage.css';
+import { Icon } from 'plugin-api/beta/client/components/ui';
+
+const ErrorMessage = ({ children }) => (
+
+
+ {children}
+
+);
+
+ErrorMessage.propTypes = {
+ children: PropTypes.node,
+};
+
+export default ErrorMessage;
diff --git a/plugins/talk-plugin-facebook-auth/client/components/InputField.css b/plugins/talk-plugin-facebook-auth/client/components/InputField.css
new file mode 100644
index 000000000..3442befde
--- /dev/null
+++ b/plugins/talk-plugin-facebook-auth/client/components/InputField.css
@@ -0,0 +1,80 @@
+
+.detailItem {
+ margin-bottom: 12px;
+}
+
+.detailItemContainer {
+ display: flex;
+}
+
+.columnDisplay {
+ flex-direction: column;
+
+ .detailItemMessage {
+ padding: 4px 0 0;
+ }
+}
+
+.detailItemContent {
+ border: solid 1px #787D80;
+ border-radius: 2px;
+ background-color: white;
+ height: 30px;
+ display: inline-block;
+ width: 230px;
+ display: flex;
+ box-sizing: border-box;
+
+ > .detailIcon {
+ font-size: 1.2em;
+ padding: 0 5px;
+ color: #787D80;
+ line-height: 30px;
+ }
+
+ &.error {
+ border: solid 2px #FA4643;
+ }
+
+ &.disabled {
+ background-color: #E0E0E0;
+ }
+}
+
+.detailLabel {
+ color: #4C4C4D;
+ font-size: 1em;
+ display: block;
+ margin-bottom: 4px;
+}
+
+.detailValue {
+ background: transparent;
+ border: none;
+ font-size: 1em;
+ color: #000;
+ outline: none;
+ flex: 1;
+ height: 100%;
+ box-sizing: border-box;
+}
+
+.detailItemMessage {
+ flex-grow: 1;
+ display: flex;
+ align-items: center;
+ padding-left: 6px;
+ padding-top: 16px;
+
+ .warningIcon, .checkIcon {
+ font-size: 17px;
+ }
+}
+
+.checkIcon {
+ color: #00CD73;
+}
+
+.warningIcon {
+ color: #FA4643;
+}
diff --git a/plugins/talk-plugin-facebook-auth/client/components/InputField.js b/plugins/talk-plugin-facebook-auth/client/components/InputField.js
new file mode 100644
index 000000000..26937d5b9
--- /dev/null
+++ b/plugins/talk-plugin-facebook-auth/client/components/InputField.js
@@ -0,0 +1,94 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cn from 'classnames';
+import styles from './InputField.css';
+import ErrorMessage from './ErrorMessage';
+import { Icon } from 'plugin-api/beta/client/components/ui';
+
+const InputField = ({
+ id = '',
+ label = '',
+ type = 'text',
+ name = '',
+ onChange = () => {},
+ showError = true,
+ hasError = false,
+ errorMsg = '',
+ children,
+ columnDisplay = false,
+ showSuccess = false,
+ validationType = '',
+ icon = '',
+ value = '',
+ defaultValue = '',
+ disabled = false,
+}) => {
+ const inputValue = {
+ ...(value ? { value } : {}),
+ ...(defaultValue ? { defaultValue } : {}),
+ };
+
+ return (
+
+
+ {label && (
+
+ )}
+
+ {icon && }
+
+
+
+ {!hasError &&
+ showSuccess &&
+ value && }
+ {hasError && showError && {errorMsg}}
+
+
+ {children}
+
+ );
+};
+
+InputField.propTypes = {
+ id: PropTypes.string,
+ disabled: PropTypes.bool,
+ label: PropTypes.string,
+ type: PropTypes.string,
+ name: PropTypes.string.isRequired,
+ onChange: PropTypes.func,
+ value: PropTypes.string,
+ defaultValue: PropTypes.string,
+ icon: PropTypes.string,
+ showError: PropTypes.bool,
+ hasError: PropTypes.bool,
+ errorMsg: PropTypes.string,
+ children: PropTypes.node,
+ columnDisplay: PropTypes.bool,
+ showSuccess: PropTypes.bool,
+ validationType: PropTypes.string,
+};
+
+export default InputField;
diff --git a/plugins/talk-plugin-local-auth/client/components/AddEmailAddressDialog.css b/plugins/talk-plugin-local-auth/client/components/AddEmailAddressDialog.css
new file mode 100644
index 000000000..7fa3ce78c
--- /dev/null
+++ b/plugins/talk-plugin-local-auth/client/components/AddEmailAddressDialog.css
@@ -0,0 +1,74 @@
+.dialog {
+ border: none;
+ box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
+ width: 400px;
+ top: 50%;
+ transform: translateY(-50%);
+ padding: 20px;
+ border-radius: 4px;
+ font-family: Helvetica,Helvetica Neue,Verdana,sans-serif;
+}
+
+.title {
+ font-size: 1.2em;
+ margin-bottom: 8px;
+}
+
+.description {
+ font-size: 1em;
+ line-height: 20px;
+ margin: 0;
+ margin-bottom: 15px;
+}
+
+.list {
+ padding: 0;
+ margin: 20px 0;
+ list-style: none;
+}
+
+.item {
+ display: flex;
+ margin-bottom: 20px;
+
+ .itemIcon {
+ flex-grow: 0;
+ }
+
+ .text {
+ flex-grow: 1;
+ padding-left: 10px;
+ }
+
+ > i.itemIcon {
+ font-size: 1.3em;
+ }
+}
+
+.button {
+ color: #787D80;
+ border-radius: 2px;
+ background-color: transparent;
+ height: 30px;
+ font-size: 0.9em;
+ line-height: normal;
+
+ &:hover {
+ background-color: #eaeaea;
+ }
+
+ &.cancel {
+ background-color: transparent;
+ color: #787D80;
+ }
+
+ &.proceed {
+ background-color: #3498DB;
+ color: white;
+ }
+
+ &.danger {
+ background-color: #FA4643;
+ color: white;
+ }
+}
\ No newline at end of file
diff --git a/plugins/talk-plugin-local-auth/client/components/AddEmailAddressDialog.js b/plugins/talk-plugin-local-auth/client/components/AddEmailAddressDialog.js
new file mode 100644
index 000000000..4468d7865
--- /dev/null
+++ b/plugins/talk-plugin-local-auth/client/components/AddEmailAddressDialog.js
@@ -0,0 +1,177 @@
+import React from 'react';
+import isEqual from 'lodash/isEqual';
+import PropTypes from 'prop-types';
+import cn from 'classnames';
+import styles from './AddEmailAddressDialog.css';
+import { Button, Dialog, Icon } from 'plugin-api/beta/client/components/ui';
+import validate from 'coral-framework/helpers/validate';
+import errorMsj from 'coral-framework/helpers/error';
+import InputField from './InputField';
+import { getErrorMessages } from 'coral-framework/utils';
+
+const initialState = {
+ showErrors: false,
+ errors: {},
+ formData: {},
+};
+
+class AddEmailAddressDialog extends React.Component {
+ state = initialState;
+ validKeys = ['emailAddress', 'confirmEmailAddress'];
+
+ onChange = e => {
+ const { name, value, type } = e.target;
+ this.setState(
+ state => ({
+ formData: {
+ ...state.formData,
+ [name]: value,
+ },
+ }),
+ () => {
+ this.fieldValidation(value, type, name);
+ }
+ );
+ };
+
+ fieldValidation = (value, type, name) => {
+ if (!value.length) {
+ this.addError({
+ [name]: 'Field is required',
+ });
+ } else if (!validate[type](value)) {
+ this.addError({ [name]: errorMsj[type] });
+ } else {
+ this.removeError(name);
+ }
+ };
+
+ addError = err => {
+ this.setState(({ errors }) => ({
+ errors: { ...errors, ...err },
+ }));
+ };
+
+ removeError = errKey => {
+ this.setState(state => {
+ const { [errKey]: _, ...errors } = state.errors;
+ return {
+ errors,
+ };
+ });
+ };
+
+ hasError = err => {
+ return Object.keys(this.state.errors).indexOf(err) !== -1;
+ };
+
+ formHasError = () => {
+ const formHasErrors = !!Object.keys(this.state.errors).length;
+ const formIncomplete = !isEqual(
+ Object.keys(this.state.formData),
+ this.validKeys
+ );
+ return formHasErrors || formIncomplete;
+ };
+
+ showError = () => {
+ this.setState({
+ showError: true,
+ });
+ };
+
+ confirmChanges = async () => {
+ if (this.formHasError()) {
+ this.showError();
+ return;
+ }
+
+ const { emailAddress } = this.state.formData;
+ const { attachLocalAuth } = this.props;
+
+ try {
+ await attachLocalAuth({
+ email: emailAddress,
+ });
+ this.props.notify('success', 'Email Added!');
+ } catch (err) {
+ this.props.notify('error', getErrorMessages(err));
+ }
+ };
+
+ render() {
+ const { errors, formData, showErrors } = this.state;
+ return (
+
+ );
+ }
+}
+
+AddEmailAddressDialog.propTypes = {
+ attachLocalAuth: PropTypes.func.isRequired,
+ notify: PropTypes.func.isRequired,
+};
+
+export default AddEmailAddressDialog;
diff --git a/plugins/talk-plugin-local-auth/client/containers/AddEmailAddressDialog.js b/plugins/talk-plugin-local-auth/client/containers/AddEmailAddressDialog.js
new file mode 100644
index 000000000..639236ea5
--- /dev/null
+++ b/plugins/talk-plugin-local-auth/client/containers/AddEmailAddressDialog.js
@@ -0,0 +1,26 @@
+import { compose, gql } from 'react-apollo';
+import { bindActionCreators } from 'redux';
+import { connect, withFragments } from 'plugin-api/beta/client/hocs';
+import AddEmailAddressDialog from '../components/AddEmailAddressDialog';
+import { notify } from 'coral-framework/actions/notification';
+
+import { withAttachLocalAuth } from 'plugin-api/beta/client/hocs';
+
+const mapDispatchToProps = dispatch => bindActionCreators({ notify }, dispatch);
+
+const withData = withFragments({
+ root: gql`
+ fragment TalkPluginLocalAuth_AddEmailAddressDialog_root on RootQuery {
+ me {
+ id
+ email
+ }
+ }
+ `,
+});
+
+export default compose(
+ connect(null, mapDispatchToProps),
+ withAttachLocalAuth,
+ withData
+)(AddEmailAddressDialog);
diff --git a/plugins/talk-plugin-local-auth/client/index.js b/plugins/talk-plugin-local-auth/client/index.js
index c669effaa..ce5816583 100644
--- a/plugins/talk-plugin-local-auth/client/index.js
+++ b/plugins/talk-plugin-local-auth/client/index.js
@@ -1,4 +1,5 @@
import ChangePassword from './containers/ChangePassword';
+import AddEmailAddressDialog from './containers/AddEmailAddressDialog';
import Profile from './containers/Profile';
import translations from './translations.yml';
@@ -7,5 +8,6 @@ export default {
slots: {
profileHeader: [Profile],
profileSettings: [ChangePassword],
+ stream: [AddEmailAddressDialog],
},
};