refactor: remove legacy code and move redesign code to src

VAN-670
This commit is contained in:
Waheed Ahmed
2021-07-28 14:27:04 +05:00
parent 0cc2191658
commit 32300db8dd
254 changed files with 28 additions and 16186 deletions

View File

@@ -0,0 +1,104 @@
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons';
import {
Form, Button,
} from '@edx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
import messages from './messages';
const EnterpriseSSO = (props) => {
const { intl } = props;
const tpaProvider = props.provider;
const handleSubmit = (e, url) => {
e.preventDefault();
window.location.href = getConfig().LMS_BASE_URL + url;
};
const handleClick = (e) => {
e.preventDefault();
window.location.href = LOGIN_PAGE;
};
if (tpaProvider) {
return (
<div className="d-flex justify-content-center m-4">
<div className="d-flex flex-column">
<div className="mw-450">
<Form className="m-0">
<p>{intl.formatMessage(messages['enterprisetpa.title.heading'], { providerName: tpaProvider.name })}</p>
<Button
id={tpaProvider.id}
key={tpaProvider.id}
type="submit"
variant="link"
className={`btn-tpa btn-${tpaProvider.id}`}
onClick={(e) => handleSubmit(e, tpaProvider.loginUrl)}
>
{tpaProvider.iconImage ? (
<div aria-hidden="true">
<img className="icon-image" src={tpaProvider.iconImage} alt={`icon ${tpaProvider.name}`} />
<span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span>
</div>
)
: (
<>
<div className="font-container" aria-hidden="true">
<FontAwesomeIcon
icon={SUPPORTED_ICON_CLASSES.includes(tpaProvider.iconClass) ? ['fab', tpaProvider.iconClass] : faSignInAlt}
/>
</div>
<span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span>
</>
)}
</Button>
<div className="mb-4" />
<Button
type="submit"
variant="outline-primary"
state="Complete"
className="w-100"
onClick={(e) => handleClick(e)}
>
{intl.formatMessage(messages['enterprisetpa.login.button.text'])}
</Button>
</Form>
</div>
</div>
</div>
);
}
return <div />;
};
EnterpriseSSO.defaultProps = {
provider: {
id: '',
name: '',
iconClass: '',
iconImage: '',
loginUrl: '',
registerUrl: '',
},
};
EnterpriseSSO.propTypes = {
provider: PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
iconClass: PropTypes.string,
iconImage: PropTypes.string,
loginUrl: PropTypes.string,
registerUrl: PropTypes.string,
}),
intl: intlShape.isRequired,
};
export default injectIntl(EnterpriseSSO);

View File

@@ -0,0 +1,100 @@
import React, { useState } from 'react';
import {
Form, TransitionReplace,
} from '@edx/paragon';
import PropTypes from 'prop-types';
const FormGroup = (props) => {
const [hasFocus, setHasFocus] = useState(false);
const handleFocus = (e) => {
setHasFocus(true);
if (props.handleFocus) { props.handleFocus(e); }
};
const handleClick = (e) => {
if (props.handleClick) { props.handleClick(e); }
};
const handleOnBlur = (e) => {
setHasFocus(false);
if (props.handleBlur) { props.handleBlur(e); }
};
return (
<Form.Group controlId={props.name} className={props.className} isInvalid={props.errorMessage !== ''}>
<Form.Control
as={props.as}
type={props.type}
className="form-field"
autoComplete={props.autoComplete}
name={props.name}
value={props.value}
onFocus={handleFocus}
onBlur={handleOnBlur}
onClick={handleClick}
onChange={props.handleChange}
controlClassName={props.borderClass}
trailingElement={props.trailingElement}
floatingLabel={props.floatingLabel}
>
{props.options ? props.options() : null}
</Form.Control>
<TransitionReplace>
{hasFocus && props.helpText ? (
<Form.Control.Feedback type="default" key="help-text" className="d-block form-text-size">
{props.helpText.map((message, index) => (
<span key={`help-text-${index.toString()}`}>
{message}
<br />
</span>
))}
</Form.Control.Feedback>
) : <div key="empty" />}
</TransitionReplace>
{props.errorMessage !== '' && (
<Form.Control.Feedback key="error" className="form-text-size" hasIcon={false} feedback-for={props.name} type="invalid">{props.errorMessage}</Form.Control.Feedback>
)}
{props.children}
</Form.Group>
);
};
FormGroup.defaultProps = {
as: 'input',
errorMessage: '',
borderClass: '',
autoComplete: null,
handleBlur: null,
handleChange: () => {},
handleFocus: null,
handleClick: null,
helpText: [],
options: null,
trailingElement: null,
type: 'text',
children: null,
className: '',
};
FormGroup.propTypes = {
as: PropTypes.string,
errorMessage: PropTypes.string,
borderClass: PropTypes.string,
autoComplete: PropTypes.string,
floatingLabel: PropTypes.string.isRequired,
handleBlur: PropTypes.func,
handleChange: PropTypes.func,
handleFocus: PropTypes.func,
handleClick: PropTypes.func,
helpText: PropTypes.arrayOf(PropTypes.string),
name: PropTypes.string.isRequired,
options: PropTypes.func,
trailingElement: PropTypes.element,
type: PropTypes.string,
value: PropTypes.string.isRequired,
children: PropTypes.element,
className: PropTypes.string,
};
export default FormGroup;

View File

@@ -0,0 +1,98 @@
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import PropTypes from 'prop-types';
import { Button, Hyperlink, Icon } from '@edx/paragon';
import { Institution } from '@edx/paragon/icons';
import messages from './messages';
export const RenderInstitutionButton = props => {
const { onSubmitHandler, buttonTitle } = props;
return (
<Button
className="btn-sm text-body p-0 mb-4 border-0"
variant="link"
data-event-name="institution_login"
onClick={onSubmitHandler}
>
<Icon src={Institution} className="institute-icon" />
{buttonTitle}
</Button>
);
};
const InstitutionLogistration = props => {
const lmsBaseUrl = getConfig().LMS_BASE_URL;
const {
intl,
secondaryProviders,
headingTitle,
} = props;
return (
<>
<div className="d-flex justify-content-left mb-4 mt-2">
<div className="flex-column">
<h4 className="mb-2 font-weight-bold institute-heading">
{headingTitle}
</h4>
<p className="mb-2">
{intl.formatMessage(messages['institution.login.page.sub.heading'])}
</p>
</div>
</div>
<div className="mb-5">
<table className="pgn__data-table table-striped table-borderless">
<tbody>
{secondaryProviders.map(provider => (
<tr key={provider} className="pgn__data-table-row">
<td>
<Hyperlink
className="btn nav-item p-0 mb-1 secondary-provider-link"
destination={lmsBaseUrl + provider.loginUrl}
>
{provider.name}
</Hyperlink>
</td>
</tr>
))}
</tbody>
</table>
</div>
</>
);
};
const LogistrationDefaultProps = {
secondaryProviders: [],
buttonTitle: '',
};
const LogistrationProps = {
secondaryProviders: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequried,
loginUrl: PropTypes.string.isRequired,
})),
};
RenderInstitutionButton.propTypes = {
...LogistrationProps,
buttonTitle: PropTypes.string,
onSubmitHandler: PropTypes.func.isRequired,
};
RenderInstitutionButton.defaultProps = {
...LogistrationDefaultProps,
};
InstitutionLogistration.propTypes = {
...LogistrationProps,
intl: intlShape.isRequired,
headingTitle: PropTypes.string,
};
InstitutionLogistration.defaultProps = {
...LogistrationDefaultProps,
headingTitle: '',
};
export default injectIntl(InstitutionLogistration);

View File

@@ -0,0 +1,92 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Redirect } from 'react-router-dom';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Tabs,
Tab,
Icon,
} from '@edx/paragon';
import { ChevronLeft } from '@edx/paragon/icons';
import messages from './messages';
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
import { updatePathWithQueryParams, getTpaHint } from '../data/utils';
import { LoginPage } from '../login';
import { RegistrationPage } from '../register';
const Logistration = (props) => {
const { intl, selectedPage } = props;
const tpa = getTpaHint();
const [institutionLogin, setInstitutionLogin] = useState(false);
const [key, setKey] = useState('');
const handleInstitutionLogin = (e) => {
sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
if (typeof e === 'string') {
sendPageEvent('login_and_registration', e === '/login' ? 'login' : 'register');
} else {
sendPageEvent('login_and_registration', e.target.dataset.eventName);
}
setInstitutionLogin(!institutionLogin);
};
const handleOnSelect = (tabKey) => {
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' });
setKey(tabKey);
};
const tabTitle = (
<div className="d-flex">
<Icon src={ChevronLeft} className="left-icon" />
<span className="ml-2">
{selectedPage === LOGIN_PAGE
? intl.formatMessage(messages['logistration.sign.in'])
: intl.formatMessage(messages['logistration.register'])}
</span>
</div>
);
return (
<div>
{institutionLogin
? (
<Tabs defaultActiveKey="" id="controlled-tab" onSelect={handleInstitutionLogin}>
<Tab title={tabTitle} eventKey={selectedPage === LOGIN_PAGE ? LOGIN_PAGE : REGISTER_PAGE} />
</Tabs>
)
: (
<>
{!tpa && (
<Tabs defaultActiveKey={selectedPage} id="controlled-tab" onSelect={handleOnSelect}>
<Tab title={intl.formatMessage(messages['logistration.register'])} eventKey={REGISTER_PAGE} />
<Tab title={intl.formatMessage(messages['logistration.sign.in'])} eventKey={LOGIN_PAGE} />
</Tabs>
)}
</>
)}
{ key && (
<Redirect to={updatePathWithQueryParams(key)} />
)}
<div id="main-content" className="main-content">
{selectedPage === LOGIN_PAGE
? <LoginPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />
: <RegistrationPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />}
</div>
</div>
);
};
Logistration.propTypes = {
intl: intlShape.isRequired,
selectedPage: PropTypes.string,
};
Logistration.defaultProps = {
selectedPage: REGISTER_PAGE,
};
export default injectIntl(Logistration);

View File

@@ -0,0 +1,16 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
export default function NotFoundPage() {
return (
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
<p className="my-0 py-5 text-muted mw-32em">
<FormattedMessage
id="error.notfound.message"
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
description="error message when a page does not exist"
/>
</p>
</div>
);
}

View File

@@ -0,0 +1,106 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Form, IconButton, useToggle, Tooltip, OverlayTrigger, Icon,
} from '@edx/paragon';
import {
Check, Remove, Visibility, VisibilityOff,
} from '@edx/paragon/icons';
import messages from './messages';
import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
const PasswordField = (props) => {
const { formatMessage } = props.intl;
const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true);
const [showTooltip, setShowTooltip] = useState(false);
const handleBlur = (e) => {
if (props.handleBlur) { props.handleBlur(e); }
setShowTooltip(props.showRequirements && false);
};
const handleFocus = (e) => {
if (props.handleFocus) {
props.handleFocus(e);
}
setTimeout(() => setShowTooltip(props.showRequirements && true), 150);
};
const HideButton = (
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="passwordValidation" src={VisibilityOff} iconAs={Icon} onClick={setHiddenTrue} size="sm" variant="secondary" alt={formatMessage(messages['hide.password'])} />
);
const ShowButton = (
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="passwordValidation" src={Visibility} iconAs={Icon} onClick={setHiddenFalse} size="sm" variant="secondary" alt={formatMessage(messages['show.password'])} />
);
const placement = window.innerWidth < 768 ? 'top' : 'left';
const tooltip = (
<Tooltip id={`password-requirement-${placement}`}>
<span id="letter-check" className="d-flex position-relative align-content-start">
{LETTER_REGEX.test(props.value) ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1" src={Remove} />}
{formatMessage(messages['one.letter'])}
</span>
<span id="number-check" className="d-flex position-relative align-content-start">
{NUMBER_REGEX.test(props.value) ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1" src={Remove} />}
{formatMessage(messages['one.number'])}
</span>
<span id="characters-check" className="d-flex position-relative align-content-start">
{props.value.length >= 8 ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1" src={Remove} />}
{formatMessage(messages['eight.characters'])}
</span>
</Tooltip>
);
return (
<Form.Group controlId={props.name} isInvalid={props.errorMessage !== ''}>
<OverlayTrigger key="tooltip" placement={placement} overlay={tooltip} show={showTooltip}>
<Form.Control
as="input"
className="form-field"
type={isPasswordHidden ? 'password' : 'text'}
name={props.name}
value={props.value}
onFocus={handleFocus}
onBlur={handleBlur}
onChange={props.handleChange}
controlClassName={props.borderClass}
trailingElement={isPasswordHidden ? ShowButton : HideButton}
floatingLabel={props.floatingLabel}
/>
</OverlayTrigger>
{props.errorMessage !== '' && (
<Form.Control.Feedback key="error" className="form-text-size" hasIcon={false} feedback-for={props.name} type="invalid">
{props.errorMessage}
<span className="sr-only">{formatMessage(messages['password.sr.only.helping.text'])}</span>
</Form.Control.Feedback>
)}
</Form.Group>
);
};
PasswordField.defaultProps = {
borderClass: '',
errorMessage: '',
handleBlur: null,
handleFocus: null,
handleChange: () => {},
showRequirements: true,
};
PasswordField.propTypes = {
borderClass: PropTypes.string,
errorMessage: PropTypes.string,
floatingLabel: PropTypes.string.isRequired,
handleBlur: PropTypes.func,
handleFocus: PropTypes.func,
handleChange: PropTypes.func,
intl: intlShape.isRequired,
name: PropTypes.string.isRequired,
showRequirements: PropTypes.bool,
value: PropTypes.string.isRequired,
};
export default injectIntl(PasswordField);

View File

@@ -0,0 +1,55 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Redirect } from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import { WELCOME_PAGE } from '../data/constants';
import { setCookie } from '../data/utils';
function RedirectLogistration(props) {
const {
finishAuthUrl, redirectUrl, redirectToWelcomePage, success,
} = props;
let finalRedirectUrl = '';
if (success) {
// If we're in a third party auth pipeline, we must complete the pipeline
// once user has successfully logged in. Otherwise, redirect to the specified redirect url.
// Note: For multiple enterprise use case, we need to make sure that user first visits the
// enterprise selection page and then complete the auth workflow
if (finishAuthUrl && !redirectUrl.includes(finishAuthUrl)) {
finalRedirectUrl = getConfig().LMS_BASE_URL + finishAuthUrl;
} else {
finalRedirectUrl = redirectUrl;
}
if (redirectToWelcomePage) {
setCookie('van-504-returning-user', true);
// use this component to redirect WelcomePage after successful registration
// return <Redirect to={WELCOME_PAGE} />;
const registrationResult = { redirectUrl: finalRedirectUrl, success };
return <Redirect to={{ pathname: WELCOME_PAGE, state: { registrationResult } }} />;
}
window.location.href = finalRedirectUrl;
}
return <></>;
}
RedirectLogistration.defaultProps = {
finishAuthUrl: null,
success: false,
redirectUrl: '',
redirectToWelcomePage: false,
};
RedirectLogistration.propTypes = {
finishAuthUrl: PropTypes.string,
success: PropTypes.bool,
redirectUrl: PropTypes.string,
redirectToWelcomePage: PropTypes.bool,
};
export default RedirectLogistration;

View File

@@ -0,0 +1,8 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import {
faApple, faFacebook, faGoogle, faMicrosoft,
} from '@fortawesome/free-brands-svg-icons';
export default function registerIcons() {
library.add(faApple, faFacebook, faGoogle, faMicrosoft);
}

View File

@@ -0,0 +1,75 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons';
import messages from './messages';
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
function SocialAuthProviders(props) {
const { intl, referrer, socialAuthProviders } = props;
function handleSubmit(e) {
e.preventDefault();
const url = e.currentTarget.dataset.providerUrl;
window.location.href = getConfig().LMS_BASE_URL + url;
}
const socialAuth = socialAuthProviders.map((provider, index) => (
<button
id={provider.id}
key={provider.id}
type="button"
className={`btn-social btn-${provider.id} ${index % 2 === 0 ? 'mr-3' : ''}`}
data-provider-url={referrer === LOGIN_PAGE ? provider.loginUrl : provider.registerUrl}
onClick={handleSubmit}
>
{provider.iconImage ? (
<div aria-hidden="true">
<img className="icon-image" src={provider.iconImage} alt={`icon ${provider.name}`} />
</div>
)
: (
<>
<div className="font-container" aria-hidden="true">
<FontAwesomeIcon
icon={SUPPORTED_ICON_CLASSES.includes(provider.iconClass) ? ['fab', provider.iconClass] : faSignInAlt}
/>
</div>
</>
)}
<span id="provider-name" className="mr-auto pl-2" aria-hidden="true">{provider.name}</span>
<span className="sr-only">
{referrer === LOGIN_PAGE
? intl.formatMessage(messages['sso.sign.in.with'], { providerName: provider.name })
: intl.formatMessage(messages['sso.create.account.using'], { providerName: provider.name })}
</span>
</button>
));
return <>{socialAuth}</>;
}
SocialAuthProviders.defaultProps = {
referrer: LOGIN_PAGE,
socialAuthProviders: [],
};
SocialAuthProviders.propTypes = {
intl: intlShape.isRequired,
referrer: PropTypes.string,
socialAuthProviders: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
iconClass: PropTypes.string,
iconImage: PropTypes.string,
loginUrl: PropTypes.string,
registerUrl: PropTypes.string,
})),
};
export default injectIntl(SocialAuthProviders);

View File

@@ -0,0 +1,61 @@
import React from 'react';
import PropTypes from 'prop-types';
import { TransitionReplace } from '@edx/paragon';
const onChildExit = (htmlNode) => {
// If the leaving child has focus, take control and redirect it
if (htmlNode.contains(document.activeElement)) {
// Get the newly entering sibling.
// It's the previousSibling, but not for any explicit reason. So checking for both.
const enteringChild = htmlNode.previousSibling || htmlNode.nextSibling;
// There's no replacement, do nothing.
if (!enteringChild) return; // eslint-disable-line curly
// Get all the focusable elements in the entering child and focus the first one
const focusableElements = enteringChild.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (focusableElements.length) {
focusableElements[0].focus();
}
}
};
function SwitchContent({ expression, cases, className }) {
const getContent = (caseKey) => {
if (cases[caseKey]) {
if (typeof cases[caseKey] === 'string') {
return getContent(cases[caseKey]);
}
return React.cloneElement(cases[caseKey], { key: caseKey });
} else if (cases.default) { // eslint-disable-line no-else-return
if (typeof cases.default === 'string') {
return getContent(cases.default);
}
React.cloneElement(cases.default, { key: 'default' });
}
return null;
};
return (
<TransitionReplace
className={className}
onChildExit={onChildExit}
>
{getContent(expression)}
</TransitionReplace>
);
}
SwitchContent.propTypes = {
expression: PropTypes.string,
cases: PropTypes.objectOf(PropTypes.node).isRequired,
className: PropTypes.string,
};
SwitchContent.defaultProps = {
expression: null,
className: null,
};
export default SwitchContent;

View File

@@ -0,0 +1,42 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Alert } from '@edx/paragon';
import messages from './messages';
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
const ThirdPartyAuthAlert = (props) => {
const { currentProvider, intl, referrer } = props;
const platformName = getConfig().SITE_NAME;
let message;
if (referrer === LOGIN_PAGE) {
message = intl.formatMessage(messages['login.third.party.auth.account.not.linked'], { currentProvider, platformName });
} else {
message = intl.formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
}
return (
<Alert id="tpa-alert" className={referrer === REGISTER_PAGE ? 'alert-success mt-n2' : 'alert-warning mt-n2'}>
{referrer === REGISTER_PAGE ? (
<Alert.Heading>{intl.formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
) : null}
<p>{ message }</p>
</Alert>
);
};
ThirdPartyAuthAlert.defaultProps = {
referrer: LOGIN_PAGE,
};
ThirdPartyAuthAlert.propTypes = {
currentProvider: PropTypes.string.isRequired,
intl: intlShape.isRequired,
referrer: PropTypes.string,
};
export default injectIntl(ThirdPartyAuthAlert);

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { Route } from 'react-router-dom';
import { AppContext } from '@edx/frontend-platform/react';
import { DEFAULT_REDIRECT_URL } from '../data/constants';
/**
* This wrapper redirects the requester to our default redirect url if they are
* already authenticated.
*/
const UnAuthOnlyRoute = (props) => {
const { authenticatedUser, config } = React.useContext(AppContext);
if (authenticatedUser) {
global.location.href = config.LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
return null;
}
return <Route {...props} />;
};
export default UnAuthOnlyRoute;

View File

@@ -0,0 +1,22 @@
import { AsyncActionType } from '../../data/utils';
export const THIRD_PARTY_AUTH_CONTEXT = new AsyncActionType('THIRD_PARTY_AUTH', 'GET_THIRD_PARTY_AUTH_CONTEXT');
// Third party auth context
export const getThirdPartyAuthContext = (urlParams) => ({
type: THIRD_PARTY_AUTH_CONTEXT.BASE,
payload: { urlParams },
});
export const getThirdPartyAuthContextBegin = () => ({
type: THIRD_PARTY_AUTH_CONTEXT.BEGIN,
});
export const getThirdPartyAuthContextSuccess = (thirdPartyAuthContext) => ({
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
payload: { thirdPartyAuthContext },
});
export const getThirdPartyAuthContextFailure = () => ({
type: THIRD_PARTY_AUTH_CONTEXT.FAILURE,
});

View File

@@ -0,0 +1,32 @@
import { THIRD_PARTY_AUTH_CONTEXT } from './actions';
import { PENDING_STATE, COMPLETE_STATE } from '../../data/constants';
export const defaultState = {
thirdPartyAuthApiStatus: null,
};
const reducer = (state = defaultState, action) => {
switch (action.type) {
case THIRD_PARTY_AUTH_CONTEXT.BEGIN:
return {
...state,
thirdPartyAuthApiStatus: PENDING_STATE,
};
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS:
return {
...state,
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
thirdPartyAuthApiStatus: COMPLETE_STATE,
};
case THIRD_PARTY_AUTH_CONTEXT.FAILURE:
return {
...state,
thirdPartyAuthApiStatus: COMPLETE_STATE,
};
default:
return state;
}
};
export default reducer;

View File

@@ -0,0 +1,34 @@
import { call, put, takeEvery } from 'redux-saga/effects';
import { logError } from '@edx/frontend-platform/logging';
// Actions
import {
THIRD_PARTY_AUTH_CONTEXT,
getThirdPartyAuthContextBegin,
getThirdPartyAuthContextSuccess,
getThirdPartyAuthContextFailure,
} from './actions';
// Services
import {
getThirdPartyAuthContext,
} from './service';
export function* fetchThirdPartyAuthContext(action) {
try {
yield put(getThirdPartyAuthContextBegin());
const { thirdPartyAuthContext } = yield call(getThirdPartyAuthContext, action.payload.urlParams);
yield put(getThirdPartyAuthContextSuccess(
thirdPartyAuthContext,
));
} catch (e) {
yield put(getThirdPartyAuthContextFailure());
logError(e);
}
}
export default function* saga() {
yield takeEvery(THIRD_PARTY_AUTH_CONTEXT.BASE, fetchThirdPartyAuthContext);
}

View File

@@ -0,0 +1,10 @@
import { createSelector } from 'reselect';
export const storeName = 'commonComponents';
export const commonComponentsSelector = state => ({ ...state[storeName] });
export const thirdPartyAuthContextSelector = createSelector(
commonComponentsSelector,
commonComponents => commonComponents.thirdPartyAuthContext,
);

View File

@@ -0,0 +1,23 @@
import { camelCaseObject, convertKeyNames, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
// eslint-disable-next-line import/prefer-default-export
export async function getThirdPartyAuthContext(urlParams) {
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
params: urlParams,
isPublic: true,
};
const { data } = await getAuthenticatedHttpClient()
.get(
`${getConfig().LMS_BASE_URL}/api/mfe_context`,
requestConfig,
)
.catch((e) => {
throw (e);
});
return {
thirdPartyAuthContext: camelCaseObject(convertKeyNames(data, { fullname: 'name' })),
};
}

View File

@@ -0,0 +1,65 @@
import { runSaga } from 'redux-saga';
import * as actions from '../actions';
import { fetchThirdPartyAuthContext } from '../sagas';
import * as api from '../service';
import initializeMockLogging from '../../../setupTest';
const { loggingService } = initializeMockLogging();
describe('fetchThirdPartyAuthContext', () => {
const params = {
payload: { urlParams: {} },
};
const data = {
currentProvider: null,
providers: [],
secondaryProviders: [],
finishAuthUrl: null,
pipelineUserDetails: {},
};
beforeEach(() => {
loggingService.logError.mockReset();
});
it('should call service and dispatch success action', async () => {
const getThirdPartyAuthContext = jest.spyOn(api, 'getThirdPartyAuthContext')
.mockImplementation(() => Promise.resolve({ thirdPartyAuthContext: data }));
const dispatched = [];
await runSaga(
{ dispatch: (action) => dispatched.push(action) },
fetchThirdPartyAuthContext,
params,
);
expect(getThirdPartyAuthContext).toHaveBeenCalledTimes(1);
expect(dispatched).toEqual([
actions.getThirdPartyAuthContextBegin(),
actions.getThirdPartyAuthContextSuccess(data),
]);
getThirdPartyAuthContext.mockClear();
});
it('should call service and dispatch error action', async () => {
const getThirdPartyAuthContext = jest.spyOn(api, 'getThirdPartyAuthContext')
.mockImplementation(() => Promise.reject(new Error('something went wrong')));
const dispatched = [];
await runSaga(
{ dispatch: (action) => dispatched.push(action) },
fetchThirdPartyAuthContext,
params,
);
expect(getThirdPartyAuthContext).toHaveBeenCalledTimes(1);
expect(loggingService.logError).toHaveBeenCalled();
expect(dispatched).toEqual([
actions.getThirdPartyAuthContextBegin(),
actions.getThirdPartyAuthContextFailure(),
]);
getThirdPartyAuthContext.mockClear();
});
});

View File

@@ -0,0 +1,14 @@
export { default as RedirectLogistration } from './RedirectLogistration';
export { default as registerIcons } from './RegisterFaIcons';
export { default as UnAuthOnlyRoute } from './UnAuthOnlyRoute';
export { default as NotFoundPage } from './NotFoundPage';
export { default as SocialAuthProviders } from './SocialAuthProviders';
export { default as ThirdPartyAuthAlert } from './ThirdPartyAuthAlert';
export { default as InstitutionLogistration } from './InstitutionLogistration';
export { RenderInstitutionButton } from './InstitutionLogistration';
export { default as reducer } from './data/reducers';
export { default as saga } from './data/sagas';
export { storeName } from './data/selectors';
export { default as FormGroup } from './FormGroup';
export { default as PasswordField } from './PasswordField';
export { default as Logistration } from './Logistration';

View File

@@ -0,0 +1,128 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'institution.login.page.sub.heading': {
id: 'institution.login.page.sub.heading',
defaultMessage: 'Choose your institution from the list below',
description: 'Heading of the institutions list',
},
// Confirmation Alert Message
'forgot.password.confirmation.title': {
id: 'forgot.password.confirmation.title',
defaultMessage: 'Check your email',
description: 'Forgot password confirmation message title',
},
'forgot.password.confirmation.support.link': {
id: 'forgot.password.confirmation.support.link',
defaultMessage: 'contact technical support',
description: 'Technical support link text',
},
'forgot.password.confirmation.info': {
id: 'forgot.password.confirmation.info',
defaultMessage: 'If you do not receive a password reset message after 1 minute, verify that you entered the correct '
+ 'email address, or check your spam folder.',
description: 'Part of message that appears after user requests password change',
},
// Logistration strinsg
'logistration.sign.in': {
id: 'logistration.sign.in',
defaultMessage: 'Sign in',
description: 'Text that appears on the tab to switch between login and register',
},
'logistration.register': {
id: 'logistration.register',
defaultMessage: 'Register',
description: 'Text that appears on the tab to switch between login and register',
},
'internal.server.error.message': {
id: 'internal.server.error.message',
defaultMessage: 'An error has occurred. Try refreshing the page, or check your internet connection.',
description: 'Error message that appears when server responds with 500 error code',
},
'server.ratelimit.error.message': {
id: 'server.ratelimit.error.message',
defaultMessage: 'An error has occurred because of too many requests. Please try again after some time.',
description: 'Error message that appears when server responds with 429 error code',
},
// enterprise sso strings
'enterprisetpa.title.heading': {
id: 'enterprisetpa.title.heading',
defaultMessage: 'Would you like to sign in using your {providerName} credentials?',
description: 'Header text used in enterprise third party authentication',
},
'enterprisetpa.sso.button.title': {
id: 'enterprisetpa.sso.button.title',
defaultMessage: 'Sign in using {providerName}',
description: 'Text for third party auth provider buttons',
},
'enterprisetpa.login.button.text': {
id: 'enterprisetpa.login.button.text',
defaultMessage: 'Show me other ways to sign in or register',
description: 'Button text for login',
},
// social auth providers
'sso.sign.in.with': {
id: 'sso.sign.in.with',
defaultMessage: 'Sign in with {providerName}',
description: 'Screen reader text that appears before social auth provider name',
},
'sso.create.account.using': {
id: 'sso.create.account.using',
defaultMessage: 'Create account using {providerName}',
description: 'Screen reader text that appears before social auth provider name',
},
// password field strings
'show.password': {
id: 'show.password',
defaultMessage: 'Show password',
description: 'aria label for show password icon on password field',
},
'hide.password': {
id: 'hide.password',
defaultMessage: 'Hide password',
description: 'aria label for hide password icon on password field',
},
'one.letter': {
id: 'one.letter',
defaultMessage: '1 Letter',
description: 'password requirement to have 1 letter',
},
'one.number': {
id: 'one.number',
defaultMessage: '1 Number',
description: 'password requirement to have 1 number',
},
'eight.characters': {
id: 'eight.characters',
defaultMessage: '8 Characters',
description: 'password requirement to have a minimum of 8 characters',
},
'password.sr.only.helping.text': {
id: 'password.sr.only.helping.text',
defaultMessage: 'Password must contain at least 8 characters, at least one letter, and at least one number',
description: 'Password helping text for the sr-only class',
},
// third party auth
'tpa.alert.heading': {
id: 'tpa.alert.heading',
defaultMessage: 'Almost done!',
description: 'Success alert heading after user has successfully signed in with social auth',
},
'login.third.party.auth.account.not.linked': {
id: 'login.third.party.auth.account.not.linked',
defaultMessage: 'You have successfully signed into {currentProvider}, but your {currentProvider} '
+ 'account does not have a linked {platformName} account. To link your accounts, '
+ 'sign in now using your {platformName} password.',
description: 'Message that appears on login page if user has successfully authenticated with social '
+ 'auth but no associated platform account exists',
},
'register.third.party.auth.account.not.linked': {
id: 'register.third.party.auth.account.not.linked',
defaultMessage: 'You\'ve successfully signed into {currentProvider}! We just need a little more information '
+ 'before you start learning with {platformName}.',
description: 'Message that appears on register page if user has successfully authenticated with TPA '
+ 'but no associated platform account exists',
},
});
export default messages;

View File

@@ -0,0 +1,95 @@
import React from 'react';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import FormGroup from '../FormGroup';
import PasswordField from '../PasswordField';
describe('FormGroup', () => {
const props = {
floatingLabel: 'Email',
helpText: ['Email field help text'],
name: 'email',
value: '',
handleFocus: jest.fn(),
};
it('should show help text on field focus', () => {
const formGroup = mount(<FormGroup {...props} />);
expect(formGroup.find('.pgn-transition-replace-group').find('div#email-1').exists()).toBeFalsy();
formGroup.find('input#email').simulate('focus');
expect(formGroup.find('.pgn-transition-replace-group').find('div#email-1').text()).toEqual('Email field help text');
});
});
describe('PasswordField', () => {
const IntlPasswordField = injectIntl(PasswordField);
let props = {};
beforeEach(() => {
props = {
floatingLabel: 'Password',
name: 'password',
value: 'password123',
handleFocus: jest.fn(),
};
});
it('should show/hide password on icon click', () => {
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
passwordField.find('button[aria-label="Show password"]').simulate('click');
expect(passwordField.find('input').prop('type')).toEqual('text');
passwordField.find('button[aria-label="Hide password"]').simulate('click');
expect(passwordField.find('input').prop('type')).toEqual('password');
});
it('should show password requirement tooltip on focus', async () => {
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
jest.useFakeTimers();
await act(async () => {
passwordField.find('input').simulate('focus');
jest.runAllTimers();
});
passwordField.update();
expect(passwordField.find('#password-requirement-left').exists()).toBeTruthy();
});
it('should show all password requirement checks as failed', async () => {
props = {
...props,
value: '',
};
jest.useFakeTimers();
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
await act(async () => {
passwordField.find('input').simulate('focus');
jest.runAllTimers();
});
passwordField.update();
expect(passwordField.find('#letter-check span').prop('className')).toEqual('pgn__icon mr-1');
expect(passwordField.find('#number-check span').prop('className')).toEqual('pgn__icon mr-1');
expect(passwordField.find('#characters-check span').prop('className')).toEqual('pgn__icon mr-1');
});
it('should update password requirement checks', async () => {
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
jest.useFakeTimers();
await act(async () => {
passwordField.find('input').simulate('focus');
jest.runAllTimers();
});
passwordField.update();
expect(passwordField.find('#letter-check span').prop('className')).toEqual('pgn__icon text-success mr-1');
expect(passwordField.find('#number-check span').prop('className')).toEqual('pgn__icon text-success mr-1');
expect(passwordField.find('#characters-check span').prop('className')).toEqual('pgn__icon text-success mr-1');
});
});

View File

@@ -0,0 +1,176 @@
import React from 'react';
import { mount } from 'enzyme';
import configureStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import * as analytics from '@edx/frontend-platform/analytics';
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import Logistration from '../Logistration';
import { RenderInstitutionButton } from '../InstitutionLogistration';
import { COMPLETE_STATE, LOGIN_PAGE } from '../../data/constants';
jest.mock('@edx/frontend-platform/analytics');
analytics.sendPageEvent = jest.fn();
const mockStore = configureStore();
const IntlLogistration = injectIntl(Logistration);
describe('Logistration', () => {
let store = {};
const secondaryProviders = {
id: 'saml-test',
name: 'Test University',
loginUrl: '/dummy-auth',
registerUrl: '/dummy_auth',
};
const reduxWrapper = children => (
<IntlProvider locale="en">
<MemoryRouter>
<Provider store={store}>{children}</Provider>
</MemoryRouter>
</IntlProvider>
);
it('should render registration page', () => {
configure({
loggingService: { logError: jest.fn() },
config: {
ENVIRONMENT: 'production',
LANGUAGE_PREFERENCE_COOKIE_NAME: 'yum',
},
messages: { 'es-419': {}, de: {}, 'en-us': {} },
});
store = mockStore({
register: {
registrationResult: { success: false, redirectUrl: '' },
registrationError: {},
},
commonComponents: {
thirdPartyAuthApiStatus: null,
},
});
const logistration = mount(reduxWrapper(<IntlLogistration />));
expect(logistration.find('#main-content').find('RegistrationPage').exists()).toBeTruthy();
});
it('should render login page', () => {
store = mockStore({
login: {
loginResult: { success: false, redirectUrl: '' },
},
commonComponents: {
thirdPartyAuthApiStatus: null,
},
});
const props = { selectedPage: LOGIN_PAGE };
const logistration = mount(reduxWrapper(<IntlLogistration {...props} />));
expect(logistration.find('#main-content').find('LoginPage').exists()).toBeTruthy();
});
it('should display institution login option when secondary providers are present', () => {
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: 'true',
});
store = mockStore({
login: {
loginResult: { success: false, redirectUrl: '' },
},
commonComponents: {
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
providers: [],
secondaryProviders: [secondaryProviders],
},
thirdPartyAuthApiStatus: COMPLETE_STATE,
},
});
const props = { selectedPage: LOGIN_PAGE };
const logistration = mount(reduxWrapper(<IntlLogistration {...props} />));
expect(logistration.text().includes('Institution/campus credentials')).toBe(true);
// on clicking "Institution/campus credentials" button, it should display institution login page
logistration.find(RenderInstitutionButton).simulate('click', { institutionLogin: true });
expect(logistration.text().includes('Test University')).toBe(true);
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: '',
});
});
it('send tracking and page events when institutional login button is clicked', () => {
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: 'true',
});
store = mockStore({
login: {
loginResult: { success: false, redirectUrl: '' },
},
commonComponents: {
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
providers: [],
secondaryProviders: [secondaryProviders],
},
thirdPartyAuthApiStatus: COMPLETE_STATE,
},
});
const props = { selectedPage: LOGIN_PAGE };
const logistration = mount(reduxWrapper(<IntlLogistration {...props} />));
logistration.find(RenderInstitutionButton).simulate('click', { institutionLogin: true });
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
expect(analytics.sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login');
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: '',
});
});
it('should not display institution register button', () => {
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: 'true',
});
store = mockStore({
register: {
registrationResult: { success: false, redirectUrl: '' },
registrationError: {},
},
commonComponents: {
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
providers: [],
secondaryProviders: [secondaryProviders],
},
thirdPartyAuthApiStatus: COMPLETE_STATE,
},
});
delete window.location;
window.location = { href: getConfig().BASE_URL };
const root = mount(reduxWrapper(<IntlLogistration />));
root.find(RenderInstitutionButton).simulate('click', { institutionLogin: true });
expect(root.text().includes('Test University')).toBe(true);
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: '',
});
});
});

View File

@@ -0,0 +1,76 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import SocialAuthProviders from '../SocialAuthProviders';
import registerIcons from '../RegisterFaIcons';
registerIcons();
describe('SocialAuthProviders', () => {
let props = {};
const appleProvider = {
id: 'oa2-apple-id',
name: 'Apple',
iconClass: null,
iconImage: 'https://edx.devstack.lms/logo.png',
loginUrl: '/auth/login/apple-id/?auth_entry=login&next=/dashboard',
};
const facebookProvider = {
id: 'oa2-facebook',
name: 'Facebook',
iconClass: null,
iconImage: 'https://edx.devstack.lms/facebook-logo.png',
loginUrl: '/auth/login/facebook/?auth_entry=login&next=/dashboard',
};
it('should match social auth provider with iconImage snapshot', () => {
props = { socialAuthProviders: [appleProvider, facebookProvider] };
const tree = renderer.create(
<IntlProvider locale="en">
<SocialAuthProviders {...props} />
</IntlProvider>,
).toJSON();
expect(tree).toMatchSnapshot();
});
it('should match social auth provider with iconClass snapshot', () => {
props = {
socialAuthProviders: [{
...appleProvider,
iconClass: 'google',
iconImage: null,
}],
};
const tree = renderer.create(
<IntlProvider locale="en">
<SocialAuthProviders {...props} />
</IntlProvider>,
).toJSON();
expect(tree).toMatchSnapshot();
});
it('should match social auth provider with default icon snapshot', () => {
props = {
socialAuthProviders: [{
...appleProvider,
iconClass: 'default',
iconImage: null,
}],
};
const tree = renderer.create(
<IntlProvider locale="en">
<SocialAuthProviders {...props} />
</IntlProvider>,
).toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,40 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import ThirdPartyAuthAlert from '../ThirdPartyAuthAlert';
import { REGISTER_PAGE } from '../../data/constants';
describe('ThirdPartyAuthAlert', () => {
let props = {};
beforeEach(() => {
props = {
currentProvider: 'Google',
platformName: 'edX',
};
});
it('should match login page third party auth alert message snapshot', () => {
const tree = renderer.create(
<IntlProvider locale="en">
<ThirdPartyAuthAlert {...props} />
</IntlProvider>,
).toJSON();
expect(tree).toMatchSnapshot();
});
it('should match register page third party auth alert message snapshot', () => {
props = {
...props,
referrer: REGISTER_PAGE,
};
const tree = renderer.create(
<IntlProvider locale="en">
<ThirdPartyAuthAlert {...props} />
</IntlProvider>,
).toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,63 @@
import React from 'react';
import { mount } from 'enzyme';
import { BrowserRouter as Router, MemoryRouter, Switch } from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import { UnAuthOnlyRoute } from '..';
import { DEFAULT_REDIRECT_URL, LOGIN_PAGE } from '../../data/constants';
const RRD = require('react-router-dom');
// Just render plain div with its children
// eslint-disable-next-line react/prop-types
RRD.BrowserRouter = ({ children }) => <div>{ children }</div>;
module.exports = RRD;
const TestApp = () => (
<Router>
<div>
<Switch>
<UnAuthOnlyRoute path={LOGIN_PAGE} render={() => (<span>Login Page</span>)} />
</Switch>
</div>
</Router>
);
describe('UnAuthOnlyRoute', () => {
const routerWrapper = () => (
<MemoryRouter initialEntries={[LOGIN_PAGE]}>
<TestApp />
</MemoryRouter>
);
it('should redirect to dashboard if already logged in', () => {
const dashboardURL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
delete window.location;
window.location = { href: '' };
const user = {
username: 'gonzo',
other: 'data',
};
const mockUseContext = jest.fn().mockImplementation(() => ({
authenticatedUser: user,
config: getConfig(),
}));
React.useContext = mockUseContext;
mount(routerWrapper());
expect(window.location.href).toBe(dashboardURL);
});
it('should render test login components', () => {
const mockUseContext = jest.fn().mockImplementation(() => ({
authenticatedUser: null,
config: {},
}));
React.useContext = mockUseContext;
const wrapper = mount(routerWrapper());
expect(wrapper.find('span').text()).toBe('Login Page');
});
});

View File

@@ -0,0 +1,154 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SocialAuthProviders should match social auth provider with default icon snapshot 1`] = `
<button
className="btn-social btn-oa2-apple-id mr-3"
data-provider-url="/auth/login/apple-id/?auth_entry=login&next=/dashboard"
id="oa2-apple-id"
onClick={[Function]}
type="button"
>
<div
aria-hidden="true"
className="font-container"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-sign-in-alt fa-w-16 "
data-icon="sign-in-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M416 448h-84c-6.6 0-12-5.4-12-12v-40c0-6.6 5.4-12 12-12h84c17.7 0 32-14.3 32-32V160c0-17.7-14.3-32-32-32h-84c-6.6 0-12-5.4-12-12V76c0-6.6 5.4-12 12-12h84c53 0 96 43 96 96v192c0 53-43 96-96 96zm-47-201L201 79c-15-15-41-4.5-41 17v96H24c-13.3 0-24 10.7-24 24v96c0 13.3 10.7 24 24 24h136v96c0 21.5 26 32 41 17l168-168c9.3-9.4 9.3-24.6 0-34z"
fill="currentColor"
style={Object {}}
/>
</svg>
</div>
<span
aria-hidden="true"
className="mr-auto pl-2"
id="provider-name"
>
Apple
</span>
<span
className="sr-only"
>
Sign in with Apple
</span>
</button>
`;
exports[`SocialAuthProviders should match social auth provider with iconClass snapshot 1`] = `
<button
className="btn-social btn-oa2-apple-id mr-3"
data-provider-url="/auth/login/apple-id/?auth_entry=login&next=/dashboard"
id="oa2-apple-id"
onClick={[Function]}
type="button"
>
<div
aria-hidden="true"
className="font-container"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-google fa-w-16 "
data-icon="google"
data-prefix="fab"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 488 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"
fill="currentColor"
style={Object {}}
/>
</svg>
</div>
<span
aria-hidden="true"
className="mr-auto pl-2"
id="provider-name"
>
Apple
</span>
<span
className="sr-only"
>
Sign in with Apple
</span>
</button>
`;
exports[`SocialAuthProviders should match social auth provider with iconImage snapshot 1`] = `
Array [
<button
className="btn-social btn-oa2-apple-id mr-3"
data-provider-url="/auth/login/apple-id/?auth_entry=login&next=/dashboard"
id="oa2-apple-id"
onClick={[Function]}
type="button"
>
<div
aria-hidden="true"
>
<img
alt="icon Apple"
className="icon-image"
src="https://edx.devstack.lms/logo.png"
/>
</div>
<span
aria-hidden="true"
className="mr-auto pl-2"
id="provider-name"
>
Apple
</span>
<span
className="sr-only"
>
Sign in with Apple
</span>
</button>,
<button
className="btn-social btn-oa2-facebook "
data-provider-url="/auth/login/facebook/?auth_entry=login&next=/dashboard"
id="oa2-facebook"
onClick={[Function]}
type="button"
>
<div
aria-hidden="true"
>
<img
alt="icon Facebook"
className="icon-image"
src="https://edx.devstack.lms/facebook-logo.png"
/>
</div>
<span
aria-hidden="true"
className="mr-auto pl-2"
id="provider-name"
>
Facebook
</span>
<span
className="sr-only"
>
Sign in with Facebook
</span>
</button>,
]
`;

View File

@@ -0,0 +1,46 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ThirdPartyAuthAlert should match login page third party auth alert message snapshot 1`] = `
<div
className="fade alert-content alert-warning mt-n2 alert show"
id="tpa-alert"
role="alert"
>
<div
className="pgn__alert-message-wrapper"
>
<div
className="alert-message-content"
>
<p>
You have successfully signed into Google, but your Google account does not have a linked edX account. To link your accounts, sign in now using your edX password.
</p>
</div>
</div>
</div>
`;
exports[`ThirdPartyAuthAlert should match register page third party auth alert message snapshot 1`] = `
<div
className="fade alert-content alert-success mt-n2 alert show"
id="tpa-alert"
role="alert"
>
<div
className="pgn__alert-message-wrapper"
>
<div
className="alert-message-content"
>
<div
className="alert-heading h4"
>
Almost done!
</div>
<p>
You've successfully signed into Google! We just need a little more information before you start learning with edX.
</p>
</div>
</div>
</div>
`;