diff --git a/docs/decisions/0002-coaching-addition.rst b/docs/decisions/0002-coaching-addition.rst
new file mode 100644
index 0000000..eb21830
--- /dev/null
+++ b/docs/decisions/0002-coaching-addition.rst
@@ -0,0 +1,30 @@
+1. Add Coaching Consent
+--------------------------------
+
+Status
+------
+
+Accepted
+
+Context
+-------
+
+We need to provide users who are eligible for coaching with both an always available
+coaching toggle and a one-time form they can view to signup for coaching.
+
+Decision
+--------
+
+While the coaching functionality is currently both limited, closed source, and the form
+exists outside of the standard design of this MFE, it was decided to add it here as a
+temporary measure due to it being at it's core, an account setting.
+
+The longer term solutions include either:
+ - using the frontend plugins feature when they become available to inject our coaching
+ work into the account MFE
+ - roll it into it's own MFE if enough additional coaching frontend work is required
+
+Consequences
+------------
+
+Code will exist inside this Open edX MFE that integrates with a closed source app.
diff --git a/src/account-settings/coaching/CoachingConsent.jsx b/src/account-settings/coaching/CoachingConsent.jsx
new file mode 100644
index 0000000..c4889ad
--- /dev/null
+++ b/src/account-settings/coaching/CoachingConsent.jsx
@@ -0,0 +1,293 @@
+import React from 'react';
+
+import { getConfig, getQueryParameters } from '@edx/frontend-platform';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { Hyperlink } from '@edx/paragon';
+import { faCheck } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+
+import PageLoading from '../PageLoading';
+import CoachingConsentForm from './CoachingConsentForm';
+import messages from './CoachingConsent.messages';
+import LogoSVG from '../../logo.svg';
+import { fetchSettings, saveSettings } from '../data/actions';
+import { coachingConsentPageSelector } from '../data/selectors';
+
+const Logo = ({ src, alt, ...attributes }) => (
+ <>
+
+ >
+);
+
+const SuccessMessage = props => (
+
+
+
{props.header}
+
{props.message}
+
+ {props.continue}
+
+
+);
+
+const AutoRedirect = (props) => {
+ window.location.href = props.redirectUrl;
+ return <>>;
+};
+
+const VIEWS = {
+ NOT_LOADED: 'NOT_LOADED',
+ LOADED: 'LOADED',
+ SUCCESS: 'SUCCESS',
+ SUCCESS_PENDING: 'SUCCESS_PENDING',
+ DECLINED: 'DECLINED',
+ DECLINE_PENDING: 'DECLINE_PENDING',
+};
+
+class CoachingConsent extends React.Component {
+ constructor(props, context) {
+ super(props, context);
+
+ // Used to redirect back to the courseware.
+ const nextUrl = this.sanitizeForwardingUrl(getQueryParameters().next);
+ this.state = {
+ redirectUrl: nextUrl || `${getConfig().LMS_BASE_URL}/dashboard/`,
+ formErrors: {},
+ formSubmitted: false,
+ declineSubmitted: false,
+ allSubmissionsComplete: false,
+ };
+
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.declineCoaching = this.declineCoaching.bind(this);
+ }
+
+ componentDidMount() {
+ this.props.fetchSettings();
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ /*
+ When we are submitting the form, we're calling saveSettings 3 times, which causes
+ multiple parallel redux flows. Because of this we can't rely on just the redux states
+ being sent in through props. For instance if the coaching submission and name
+ submission happen in near parallel, the coaching flow could return errors in
+ formErrors and the name flow could overwrite the formErrors with an empty object.
+
+ To minimize disruption to the rest of the app, we're going to manage flow state from
+ within this component.
+ */
+
+ // If a new error comes in, store it before the next redux call overwrites it.
+ let allFormErrors = {};
+ let allSubmissionsComplete = false;
+
+ // Collect new errors and add to state (will be cleared on new submission)
+ const newErrorsFound = (
+ this.props.formErrors !== prevProps.formErrors
+ && Object.keys(this.props.formErrors).length > 0
+ );
+ if (newErrorsFound) {
+ allFormErrors = Object.assign({}, this.state.formErrors, this.props.formErrors);
+ }
+
+ // Check if all values from the form have confirmation values
+ if (
+ this.state.formSubmitted &&
+ this.props.confirmationValues.coaching &&
+ this.props.confirmationValues.name &&
+ this.props.confirmationValues.phone_number
+ ) {
+ allSubmissionsComplete = true;
+ }
+
+ // Check if all values from the decline link have confirmation values
+ if (this.props.confirmationValues.coaching && this.state.declineSubmitted) {
+ allSubmissionsComplete = true;
+ }
+ if (newErrorsFound || (allSubmissionsComplete !== prevState.allSubmissionsComplete)) {
+ this.setState({
+ formErrors: allFormErrors,
+ allSubmissionsComplete,
+ });
+ }
+ }
+
+ sanitizeForwardingUrl(url) {
+ // Redirect to root of MFE if invalid next param is sent
+ return url && url.startsWith(getConfig().LMS_BASE_URL) ? url : `${getConfig().LMS_BASE_URL}/dashboard/`;
+ }
+
+ async handleSubmit(e) {
+ e.preventDefault();
+ this.setState({
+ formErrors: {},
+ formSubmitted: true,
+ });
+ // Must store target values or they disappear before the async function can use them.
+ const fullName = e.target.fullName.value;
+ const phoneNumber = e.target.phoneNumber.value;
+ const coachingValues = this.props.formValues.coaching;
+
+ // These will overwrite each other's redux states (see componentDidUpdate note)
+ this.props.saveSettings('name', fullName);
+ this.props.saveSettings('phone_number', phoneNumber);
+ this.props.saveSettings('coaching', {
+ ...coachingValues,
+ phone_number: phoneNumber,
+ coaching_consent: true,
+ consent_form_seen: true,
+ });
+ }
+
+ async declineCoaching(e) {
+ e.preventDefault();
+ this.setState({
+ formErrors: {},
+ declineSubmitted: true,
+ });
+ // Must store target values or they disappear before the async function can use them.
+ const coachingValues = this.props.formValues.coaching;
+ this.props.saveSettings('coaching', {
+ ...coachingValues,
+ coaching_consent: false,
+ consent_form_seen: true,
+ });
+ }
+
+ renderView(currentView) {
+ switch (currentView) {
+ case VIEWS.NOT_LOADED:
+ return ;
+ case VIEWS.LOADED:
+ return ();
+ case VIEWS.SUCCESS_PENDING:
+ return ;
+ case VIEWS.SUCCESS:
+ return ();
+ case VIEWS.DECLINE_PENDING:
+ return ;
+ case VIEWS.DECLINED:
+ return ;
+ default:
+ return <>>;
+ }
+ }
+
+ render() {
+ const { loaded } = this.props;
+ const formHasErrors = Object.keys(this.state.formErrors).length > 0;
+ let currentView = null;
+
+ // This amount of logic was making the template very hard to read, so I broke it out into views.
+ if (!loaded) {
+ currentView = VIEWS.NOT_LOADED;
+ } else if (this.state.formSubmitted && !formHasErrors) {
+ if (this.state.allSubmissionsComplete) {
+ currentView = VIEWS.SUCCESS;
+ } else {
+ currentView = VIEWS.SUCCESS_PENDING;
+ }
+ } else if (this.state.declineSubmitted && !formHasErrors) {
+ if (this.state.allSubmissionsComplete) {
+ currentView = VIEWS.DECLINED;
+ } else {
+ currentView = VIEWS.DECLINE_PENDING;
+ }
+ } else {
+ currentView = VIEWS.LOADED;
+ }
+
+ return (
+
+
+
+
+ {this.renderView(currentView)}
+
+ );
+ }
+}
+
+Logo.defaultProps = {
+ src: '',
+ alt: '',
+};
+
+Logo.propTypes = {
+ src: PropTypes.string,
+ alt: PropTypes.string,
+};
+
+SuccessMessage.defaultProps = {
+ header: '',
+ message: '',
+ continueUrl: '',
+ continue: '',
+};
+
+SuccessMessage.propTypes = {
+ header: PropTypes.string,
+ message: PropTypes.string,
+ continueUrl: PropTypes.string,
+ continue: PropTypes.string,
+};
+
+AutoRedirect.defaultProps = {
+ redirectUrl: '',
+};
+
+AutoRedirect.propTypes = {
+ redirectUrl: PropTypes.string,
+};
+
+CoachingConsent.defaultProps = {
+ loaded: false,
+};
+
+CoachingConsent.propTypes = {
+ intl: intlShape.isRequired,
+ loaded: PropTypes.bool,
+ formValues: PropTypes.shape({
+ name: PropTypes.string,
+ phone_number: PropTypes.string,
+ coaching: PropTypes.shape({
+ coaching_consent: PropTypes.bool.isRequired,
+ user: PropTypes.number.isRequired,
+ eligible_for_coaching: PropTypes.bool.isRequired,
+ consent_form_seen: PropTypes.bool.isRequired,
+ }),
+ }).isRequired,
+ formErrors: PropTypes.shape({
+ coaching: PropTypes.object,
+ }).isRequired,
+ confirmationValues: PropTypes.shape({
+ coaching: PropTypes.object,
+ name: PropTypes.object,
+ phone_number: PropTypes.object,
+ }).isRequired,
+ fetchSettings: PropTypes.func.isRequired,
+ saveSettings: PropTypes.func.isRequired,
+};
+
+export default connect(coachingConsentPageSelector, {
+ fetchSettings,
+ saveSettings,
+})(injectIntl(CoachingConsent));
diff --git a/src/account-settings/coaching/CoachingConsent.messages.js b/src/account-settings/coaching/CoachingConsent.messages.js
new file mode 100644
index 0000000..0eae5ae
--- /dev/null
+++ b/src/account-settings/coaching/CoachingConsent.messages.js
@@ -0,0 +1,61 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ 'account.settings.coaching.consent.welcome.header': {
+ id: 'account.settings.coaching.consent.welcome.header',
+ defaultMessage: 'Let’s get started.',
+ description: 'The welcome header for consent form.',
+ },
+ 'account.settings.coaching.consent.welcome.subheader': {
+ id: 'account.settings.coaching.consent.welcome.subheader',
+ defaultMessage: "We're here for you from start to finish",
+ description: 'The welcome subheader for consent form.',
+ },
+ 'account.settings.coaching.consent.description': {
+ id: 'account.settings.coaching.consent.description',
+ defaultMessage: "MicroBachelors programs include coaching that focuses on your career, education, and how you'll achieve results through one-on-one communication with an experienced professional. If you’re interested, provide the information below and click “Submit,” and our coaching partner will connect with you via email and/or text message to help you move forward. Terms and conditions apply.*",
+ description: 'Text describing what Coaching is.',
+ },
+ 'account.settings.coaching.consent.text-messaging.disclaimer': {
+ id: 'account.settings.coaching.consent.text-messaging.disclaimer',
+ defaultMessage: '* Coaching services are included at no additional cost to learners with US phone numbers. Coaching includes recurring text messages. Message and data rates may apply. Text STOP to opt-out.',
+ description: 'Text describing what Coaching is.',
+ },
+ 'account.settings.coaching.consent.accept-coaching': {
+ id: 'account.settings.coaching.consent.accept-coaching',
+ defaultMessage: 'Sign up for coaching',
+ description: 'Text to confirm coaching enablement',
+ },
+ 'account.settings.coaching.consent.decline-coaching': {
+ id: 'account.settings.coaching.consent.decline-coaching',
+ defaultMessage: 'I prefer not to be contacted with free coaching services',
+ description: 'Text to decline coaching enablement',
+ },
+ 'account.settings.coaching.consent.label.name': {
+ id: 'account.settings.coaching.consent.label.name',
+ defaultMessage: 'Please confirm your name',
+ description: 'Label for name input',
+ },
+ 'account.settings.coaching.consent.label.phone-number': {
+ id: 'account.settings.coaching.consent.label.phone-number',
+ defaultMessage: 'Enter your mobile number',
+ description: 'Label for mobile phone number input',
+ },
+ 'account.settings.coaching.consent.success.header': {
+ id: 'account.settings.coaching.consent.success.header',
+ defaultMessage: 'Success!',
+ description: 'Heading announcing that submission succeeded',
+ },
+ 'account.settings.coaching.consent.success.message': {
+ id: 'account.settings.coaching.consent.success.message',
+ defaultMessage: "You're signed up for coaching. You will receive a text message confirmation.",
+ description: 'Text announcing that you have signed up and will receive texts',
+ },
+ 'account.settings.coaching.consent.success.continue': {
+ id: 'account.settings.coaching.consent.success.continue',
+ defaultMessage: 'Start my course',
+ description: 'Text that the user will be sent back to the courseware',
+ },
+});
+
+export default messages;
diff --git a/src/account-settings/coaching/CoachingConsentForm.jsx b/src/account-settings/coaching/CoachingConsentForm.jsx
new file mode 100644
index 0000000..1403fc5
--- /dev/null
+++ b/src/account-settings/coaching/CoachingConsentForm.jsx
@@ -0,0 +1,104 @@
+import React from 'react';
+
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { Input, Button, Hyperlink } from '@edx/paragon';
+import PropTypes from 'prop-types';
+
+import messages from './CoachingConsent.messages';
+
+const ErrorMessage = props => (
+