Add phone number field and coaching consent toggle

MB-196

Adds phone number and coaching consent toggle to the profile. Currently,
toggled off in production, until we are ready for MB learners to start
recieving coaching.
This commit is contained in:
Thomas Tracy
2020-03-04 17:02:20 -05:00
parent a9d3463619
commit 7d8e2f2f85
6 changed files with 124 additions and 11 deletions

View File

@@ -16,3 +16,5 @@ SEGMENT_KEY=null
SITE_NAME='edX'
SUPPORT_URL='http://localhost:18000/support'
USER_INFO_COOKIE_NAME='edx-user-info'
# Temporary, Remove this once we are ready to release the feature.
TEMP_COACHING_FEATURE_FLAG=true

View File

@@ -13,7 +13,7 @@ import {
getCountryList,
getLanguageList,
} from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@edx/paragon';
import { Hyperlink, Input, ValidationFormGroup } from '@edx/paragon';
import messages from './AccountSettingsPage.messages';
import { fetchSettings, saveSettings, updateDraft } from './data/actions';
@@ -327,6 +327,45 @@ class AccountSettingsPage extends React.Component {
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies.empty'])}
{...editableFieldProps}
/>
{process.env.TEMP_COACHING_FEATURE_FLAG &&
<>
<EditableField
name="phone_number"
type="text"
value={this.props.formValues.phone_number}
label={this.props.intl.formatMessage(messages['account.settings.field.phone_number'])}
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.phone_number.empty'])}
{...editableFieldProps}
/>
<ValidationFormGroup
for="coachingConsent"
helpText={this.props.intl.formatMessage(messages['account.settings.field.coaching_consent.tooltip'])}
invalid={
!this.props.formValues.phone_number && this.props.formValues.coaching_consent
}
invalidMessage={this.props.intl.formatMessage(messages['account.settings.field.coaching_consent.error'])}
className="custom-control custom-switch"
>
<Input
name="coaching_consent"
className="custom-control-input"
type="checkbox"
id="coachingConsent"
checked={this.props.formValues.coaching_consent}
value={this.props.formValues.coaching_consent}
onChange={(e) => {
this.handleEditableFieldChange(e.target.name, e.target.checked);
if (this.props.formValues.phone_number) {
this.handleSubmit(e.target.name, e.target.checked);
} else {
this.handleSubmit(e.target.name, false);
}
}}
/>
<label className="custom-control-label" htmlFor="coachingConsent">{this.props.intl.formatMessage(messages['account.settings.field.coaching_consent'])}</label>
</ValidationFormGroup>
</>
}
</div>
<div className="account-section" id="social-media">
@@ -474,6 +513,8 @@ AccountSettingsPage.propTypes = {
level_of_education: PropTypes.string,
gender: PropTypes.string,
language_proficiencies: PropTypes.string,
phone_number: PropTypes.string,
coaching_consent: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
social_link_linkedin: PropTypes.string,
social_link_facebook: PropTypes.string,
social_link_twitter: PropTypes.string,

View File

@@ -257,6 +257,31 @@ const messages = defineMessages({
defaultMessage: 'Other',
description: 'The label for catch-all gender option.',
},
'account.settings.field.phone_number': {
id: 'account.settings.field.phone_number',
defaultMessage: 'Phone Number',
description: 'The label for a phone numbers setting in the user profile',
},
'account.settings.field.phone_number.empty': {
id: 'account.settings.field.phone_number.empty',
defaultMessage: 'Add a phone number',
description: 'placeholder for a profiles empty phone number field',
},
'account.settings.field.coaching_consent': {
id: 'account.settings.field.coaching_consent',
defaultMessage: 'Coaching consent',
description: 'The label for the coaching consent setting in the user profile',
},
'account.settings.field.coaching_consent.tooltip': {
id: 'account.settings.field.coaching_consent.tooltip',
defaultMessage: 'MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available in English and Spanish languages. Standard messaging rates apply. Text STOP at anytime to opt-out of messages.',
description: 'A tooltip explaining what coaching is and who it is for',
},
'account.settings.field.coaching_consent.error': {
id: 'account.settings.field.coaching_consent.error',
defaultMessage: 'A phone number is required to sign up for coaching',
description: 'An error message that displays when a user attempts to consent to coaching without first providing a phone number in their profile',
},
'account.settings.field.language.proficiencies': {
id: 'account.settings.field.language.proficiencies',
defaultMessage: 'Spoken languages',

View File

@@ -35,4 +35,13 @@
margin-bottom: map-get($spacers, 5);
padding-top: 1rem;
}
.custom-switch {
padding: 0;
max-width: 500px;
.custom-control-label {
left: 2.25rem;
line-height: 1.6rem;
}
}
}

View File

@@ -1,4 +1,4 @@
import { call, put, delay, takeEvery, all } from 'redux-saga/effects';
import { call, put, delay, takeEvery, all, debounce } from 'redux-saga/effects';
import { publish } from '@edx/frontend-platform';
import { getLocale, handleRtl, LOCALE_CHANGED } from '@edx/frontend-platform/i18n';
@@ -37,7 +37,7 @@ import { getSettings, patchSettings, getTimeZones } from './service';
export function* handleFetchSettings() {
try {
yield put(fetchSettingsBegin());
const { username, roles: userRoles } = getAuthenticatedUser();
const { username, userId, roles: userRoles } = getAuthenticatedUser();
const {
thirdPartyAuthProviders, profileDataManager, timeZones, ...values
@@ -45,6 +45,7 @@ export function* handleFetchSettings() {
getSettings,
username,
userRoles,
userId,
);
if (values.country) yield put(fetchTimeZones(values.country));
@@ -65,7 +66,7 @@ export function* handleSaveSettings(action) {
try {
yield put(saveSettingsBegin());
const { username } = getAuthenticatedUser();
const { username, userId } = getAuthenticatedUser();
const { commitValues, formId } = action.payload;
const commitData = { [formId]: commitValues };
let savedValues = null;
@@ -83,7 +84,7 @@ export function* handleSaveSettings(action) {
handleRtl();
savedValues = commitData;
} else {
savedValues = yield call(patchSettings, username, commitData);
savedValues = yield call(patchSettings, username, commitData, userId);
}
yield put(saveSettingsSuccess(savedValues, commitData));
if (savedValues.country) yield put(fetchTimeZones(savedValues.country));
@@ -107,7 +108,7 @@ export function* handleFetchTimeZones(action) {
export default function* saga() {
yield takeEvery(FETCH_SETTINGS.BASE, handleFetchSettings);
yield takeEvery(SAVE_SETTINGS.BASE, handleSaveSettings);
yield debounce(500, SAVE_SETTINGS.BASE, handleSaveSettings);
yield takeEvery(FETCH_TIME_ZONES.BASE, handleFetchTimeZones);
yield all([
deleteAccountSaga(),

View File

@@ -150,16 +150,45 @@ export async function getProfileDataManager(username, userRoles) {
}
/**
* A single function to GET everything considered a setting.
* Currently encapsulates Account, Preferences, and ThirdPartyAuth
* get all settings related to the coaching plugin. Settings used
* by Microbachelors students.
* @param {Number} userId users are identified in the api by LMS id
*/
export async function getSettings(username, userRoles) {
export async function getCoachingPreferences(userId) {
const { data } = await getAuthenticatedHttpClient()
.get(`${getConfig().LMS_BASE_URL}/api/coaching/v1/users/${userId}/`);
return data.coaching_consent;
}
/**
* patch all of the settings related to coaching.
* @param {Number} userId users are identified in the api by LMS id
* @param {Object} commitValues { coaching_consent }
*/
export async function patchCoachingPreferences(userId, commitValues) {
const requestUrl = `${getConfig().LMS_BASE_URL}/api/coaching/v1/users/${userId}/`;
const body = {
...commitValues,
user: userId,
};
const options = { headers: { 'Content-Type': 'application/json' } };
getAuthenticatedHttpClient()
.patch(requestUrl, body, options);
return commitValues;
}
/**
* A single function to GET everything considered a setting.
* Currently encapsulates Account, Preferences, Coaching, and ThirdPartyAuth
*/
export async function getSettings(username, userRoles, userId) {
const results = await Promise.all([
getAccount(username),
getPreferences(username),
getThirdPartyAuthProviders(),
getProfileDataManager(username, userRoles),
getTimeZones(),
getCoachingPreferences(userId),
]);
return {
@@ -168,20 +197,23 @@ export async function getSettings(username, userRoles) {
thirdPartyAuthProviders: results[2],
profileDataManager: results[3],
timeZones: results[4],
coaching_consent: results[5],
};
}
/**
* A single function to PATCH everything considered a setting.
* Currently encapsulates Account, Preferences, and ThirdPartyAuth
* Currently encapsulates Account, Preferences, coaching and ThirdPartyAuth
*/
export async function patchSettings(username, commitValues) {
export async function patchSettings(username, commitValues, userId) {
// Note: time_zone exists in the return value from user/v1/accounts
// but it is always null and won't update. It also exists in
// user/v1/preferences where it does update. This is the one we use.
const preferenceKeys = ['time_zone'];
const coachingKeys = ['coaching_consent'];
const accountCommitValues = omit(commitValues, preferenceKeys);
const preferenceCommitValues = pick(commitValues, preferenceKeys);
const coachingCommitValues = pick(commitValues, coachingKeys);
const patchRequests = [];
if (!isEmpty(accountCommitValues)) {
@@ -190,6 +222,9 @@ export async function patchSettings(username, commitValues) {
if (!isEmpty(preferenceCommitValues)) {
patchRequests.push(patchPreferences(username, preferenceCommitValues));
}
if (!isEmpty(coachingCommitValues)) {
patchRequests.push(patchCoachingPreferences(userId, coachingCommitValues));
}
const results = await Promise.all(patchRequests);
// Assigns in order of requests. Preference keys