зеркало из https://github.com/mozilla/hubs.git
Refactor authentication UI
This commit is contained in:
Родитель
6ab1747080
Коммит
cb10f5428a
|
@ -1,10 +1,11 @@
|
|||
import React from "react";
|
||||
import { useAccessibleOutlineStyle } from "../src/react-components/input/useAccessibleOutlineStyle";
|
||||
import "../src/react-components/styles/global.scss";
|
||||
import { WrappedIntlProvider } from "../src/react-components/wrapped-intl-provider";
|
||||
|
||||
const Layout = ({ children }) => {
|
||||
useAccessibleOutlineStyle();
|
||||
return <>{children}</>;
|
||||
return <WrappedIntlProvider>{children}</WrappedIntlProvider>;
|
||||
};
|
||||
|
||||
export const decorators = [
|
||||
|
|
|
@ -4,7 +4,7 @@ import "./utils/configs";
|
|||
import styles from "./assets/stylesheets/cloud.scss";
|
||||
import classNames from "classnames";
|
||||
import { WrappedIntlProvider } from "./react-components/wrapped-intl-provider";
|
||||
import { Page } from "./react-components/layout/Page";
|
||||
import { PageContainer } from "./react-components/layout/PageContainer";
|
||||
import { AuthContextProvider } from "./react-components/auth/AuthContext";
|
||||
import Store from "./storage/store";
|
||||
|
||||
|
@ -14,7 +14,7 @@ registerTelemetry("/cloud", "Hubs Cloud Landing Page");
|
|||
|
||||
function HubsCloudPage() {
|
||||
return (
|
||||
<Page>
|
||||
<PageContainer>
|
||||
<div className={styles.hero}>
|
||||
<section className={styles.colLg}>
|
||||
<div className={classNames(styles.hideLgUp, styles.centerLg)}>
|
||||
|
@ -76,7 +76,7 @@ function HubsCloudPage() {
|
|||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</Page>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { injectIntl, FormattedMessage } from "react-intl";
|
||||
import DialogContainer from "./dialog-container.js";
|
||||
import IfFeature from "./if-feature";
|
||||
|
||||
class AuthDialog extends Component {
|
||||
static propTypes = {
|
||||
intl: PropTypes.object,
|
||||
verifying: PropTypes.bool,
|
||||
verified: PropTypes.bool,
|
||||
authOrigin: PropTypes.string
|
||||
};
|
||||
|
||||
render() {
|
||||
const { authOrigin, verifying, verified } = this.props;
|
||||
const { formatMessage } = this.props.intl;
|
||||
const title = verifying || !verified ? "" : formatMessage({ id: "auth.verified-title" });
|
||||
|
||||
if (!verifying && !verified) {
|
||||
return (
|
||||
<DialogContainer title={title} closable={true} {...this.props}>
|
||||
<FormattedMessage className="preformatted" id="auth.verify-failed" />
|
||||
</DialogContainer>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<DialogContainer title={title} closable={!verifying} {...this.props}>
|
||||
{verifying ? (
|
||||
<div className="loader-wrap loader-mid">
|
||||
<div className="loader">
|
||||
<div className="loader-center" />
|
||||
</div>
|
||||
</div>
|
||||
) : authOrigin === "spoke" ? (
|
||||
<FormattedMessage className="preformatted" id="auth.spoke-verified" />
|
||||
) : (
|
||||
<div>
|
||||
<FormattedMessage className="preformatted" id="auth.verified" />
|
||||
<IfFeature name="show_newsletter_signup">
|
||||
<p>
|
||||
Want Hubs news sent to your inbox?{"\n"}
|
||||
<a href="https://eepurl.com/gX_fH9" target="_blank" rel="noopener noreferrer">
|
||||
Subscribe for updates
|
||||
</a>.
|
||||
</p>
|
||||
</IfFeature>
|
||||
</div>
|
||||
)}
|
||||
</DialogContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(AuthDialog);
|
|
@ -0,0 +1,47 @@
|
|||
import React, { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import configs from "../../utils/configs";
|
||||
import { SignInModal, SignInStep, SubmitEmail, WaitForVerification, SignInComplete } from "./SignInModal";
|
||||
|
||||
// TODO: Migrate to use AuthContext
|
||||
export function RoomSignInModalContainer({ onClose, step, onSubmitEmail, message, continueText, onContinue }) {
|
||||
const [cachedEmail, setCachedEmail] = useState();
|
||||
|
||||
return (
|
||||
<SignInModal onClose={onClose} closeable>
|
||||
{step === SignInStep.submit && (
|
||||
<SubmitEmail
|
||||
onSubmitEmail={email => {
|
||||
setCachedEmail(email);
|
||||
onSubmitEmail(email);
|
||||
}}
|
||||
initialEmail={cachedEmail}
|
||||
termsUrl={configs.link("terms_of_use", "https://github.com/mozilla/hubs/blob/master/TERMS.md")}
|
||||
showTerms={configs.feature("show_terms")}
|
||||
privacyUrl={configs.link("privacy_notice", "https://github.com/mozilla/hubs/blob/master/PRIVACY.md")}
|
||||
showPrivacy={configs.feature("show_privacy")}
|
||||
message={message}
|
||||
/>
|
||||
)}
|
||||
{step === SignInStep.waitForVerification && (
|
||||
<WaitForVerification
|
||||
onCancel={onClose}
|
||||
email={cachedEmail}
|
||||
showNewsletterSignup={configs.feature("show_newsletter_signup")}
|
||||
/>
|
||||
)}
|
||||
{step === SignInStep.complete && (
|
||||
<SignInComplete message={message} continueText={continueText} onContinue={onContinue} />
|
||||
)}
|
||||
</SignInModal>
|
||||
);
|
||||
}
|
||||
|
||||
RoomSignInModalContainer.propTypes = {
|
||||
onClose: PropTypes.func,
|
||||
onSubmitEmail: PropTypes.func,
|
||||
step: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
continueText: PropTypes.string,
|
||||
onContinue: PropTypes.func
|
||||
};
|
|
@ -0,0 +1,134 @@
|
|||
import React, { useCallback, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { CloseButton } from "../input/CloseButton";
|
||||
import { Modal } from "../modal/Modal";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import styles from "./SignInModal.scss";
|
||||
import { Button } from "../input/Button";
|
||||
import { TextInputField } from "../input/TextInputField";
|
||||
|
||||
export const SignInStep = {
|
||||
submit: "submit",
|
||||
waitForVerification: "waitForVerification",
|
||||
complete: "complete"
|
||||
};
|
||||
|
||||
export function SubmitEmail({ onSubmitEmail, initialEmail, showPrivacy, privacyUrl, showTerms, termsUrl, message }) {
|
||||
const [email, setEmail] = useState(initialEmail);
|
||||
|
||||
const onSubmitForm = useCallback(
|
||||
e => {
|
||||
e.preventDefault();
|
||||
onSubmitEmail(email);
|
||||
},
|
||||
[onSubmitEmail, email]
|
||||
);
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmitForm} className={styles.modalContent}>
|
||||
<p>{message || <FormattedMessage id="sign-in.prompt" />}</p>
|
||||
<TextInputField
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
placeholder="example@example.com"
|
||||
/>
|
||||
{(showTerms || showPrivacy) && (
|
||||
<b className={styles.terms}>
|
||||
By proceeding, you agree to the{" "}
|
||||
{showTerms && (
|
||||
<>
|
||||
<a rel="noopener noreferrer" target="_blank" href={termsUrl}>
|
||||
terms of use
|
||||
</a>{" "}
|
||||
</>
|
||||
)}
|
||||
{showTerms && showPrivacy && "and "}
|
||||
{showPrivacy && (
|
||||
<a rel="noopener noreferrer" target="_blank" href={privacyUrl}>
|
||||
privacy notice
|
||||
</a>
|
||||
)}.
|
||||
</b>
|
||||
)}
|
||||
<Button preset="accept" type="submit">
|
||||
Next
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
SubmitEmail.defaultProps = {
|
||||
initialEmail: ""
|
||||
};
|
||||
|
||||
SubmitEmail.propTypes = {
|
||||
message: PropTypes.string,
|
||||
showTerms: PropTypes.bool,
|
||||
termsUrl: PropTypes.string,
|
||||
showPrivacy: PropTypes.bool,
|
||||
privacyUrl: PropTypes.string,
|
||||
initialEmail: PropTypes.string,
|
||||
onSubmitEmail: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export function WaitForVerification({ email, onCancel, showNewsletterSignup }) {
|
||||
return (
|
||||
<div className={styles.modalContent}>
|
||||
<p>
|
||||
<FormattedMessage id="sign-in.auth-started" values={{ email }} />
|
||||
</p>
|
||||
{showNewsletterSignup && (
|
||||
<p className={styles.newsletter}>
|
||||
Want Hubs news sent to your inbox?<br />
|
||||
<a href="https://eepurl.com/gX_fH9" target="_blank" rel="noopener noreferrer">
|
||||
Subscribe for updates
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
<Button preset="cancel" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
WaitForVerification.propTypes = {
|
||||
showNewsletterSignup: PropTypes.bool,
|
||||
email: PropTypes.string.isRequired,
|
||||
onCancel: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export function SignInComplete({ message, continueText, onContinue }) {
|
||||
return (
|
||||
<div className={styles.modalContent}>
|
||||
<b>{message}</b>
|
||||
<p>{continueText}</p>
|
||||
<Button preset="green" onClick={onContinue}>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SignInComplete.propTypes = {
|
||||
message: PropTypes.string.isRequired,
|
||||
continueText: PropTypes.string.isRequired,
|
||||
onContinue: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export function SignInModal({ closeable, onClose, children, ...rest }) {
|
||||
return (
|
||||
<Modal title="Sign In" beforeTitle={closeable && <CloseButton onClick={onClose} />} {...rest}>
|
||||
{children}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
SignInModal.propTypes = {
|
||||
closeable: PropTypes.bool,
|
||||
onClose: PropTypes.func,
|
||||
children: PropTypes.node
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
@use "../styles/theme.scss";
|
||||
|
||||
:local(.modal-content) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
line-height: 1.25;
|
||||
|
||||
& > * {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:local(.terms) {
|
||||
color: theme.$darkgrey;
|
||||
font-size: theme.$font-size-xs;
|
||||
}
|
||||
|
||||
:local(.newsletter) {
|
||||
color: theme.$darkgrey;
|
||||
font-size: theme.$font-size-sm;
|
||||
line-height: 1.5;
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
import React from "react";
|
||||
import { Center } from "../layout/Center";
|
||||
import { Page } from "../layout/Page";
|
||||
import { RoomLayout } from "../layout/RoomLayout";
|
||||
import { SignInModal, SubmitEmail, WaitForVerification } from "./SignInModal";
|
||||
import backgroundUrl from "../../assets/images/home-hero-background-unbranded.png";
|
||||
|
||||
export default {
|
||||
title: "SignInModal"
|
||||
};
|
||||
|
||||
export const PageSubmit = () => (
|
||||
<Page style={{ backgroundImage: `url(${backgroundUrl})`, backgroundSize: "cover" }}>
|
||||
<Center>
|
||||
<SignInModal disableFullscreen>
|
||||
<SubmitEmail
|
||||
termsUrl="https://github.com/mozilla/hubs/blob/master/TERMS.md"
|
||||
showTerms
|
||||
privacyUrl="https://github.com/mozilla/hubs/blob/master/PRIVACY.md"
|
||||
showPrivacy
|
||||
/>
|
||||
</SignInModal>
|
||||
</Center>
|
||||
</Page>
|
||||
);
|
||||
|
||||
PageSubmit.parameters = {
|
||||
layout: "fullscreen"
|
||||
};
|
||||
|
||||
export const PageWaitForVerification = () => (
|
||||
<Page style={{ backgroundImage: `url(${backgroundUrl})`, backgroundSize: "cover" }}>
|
||||
<Center>
|
||||
<SignInModal disableFullscreen>
|
||||
<WaitForVerification email="example@example.com" showNewsletterSignup />
|
||||
</SignInModal>
|
||||
</Center>
|
||||
</Page>
|
||||
);
|
||||
|
||||
PageWaitForVerification.parameters = {
|
||||
layout: "fullscreen"
|
||||
};
|
||||
|
||||
export const RoomSubmit = () => (
|
||||
<RoomLayout
|
||||
modal={
|
||||
<SignInModal closeable>
|
||||
<SubmitEmail
|
||||
termsUrl="https://github.com/mozilla/hubs/blob/master/TERMS.md"
|
||||
showTerms
|
||||
privacyUrl="https://github.com/mozilla/hubs/blob/master/PRIVACY.md"
|
||||
showPrivacy
|
||||
/>
|
||||
</SignInModal>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
RoomSubmit.parameters = {
|
||||
layout: "fullscreen"
|
||||
};
|
||||
|
||||
export const RoomWaitForVerification = () => (
|
||||
<RoomLayout
|
||||
modal={
|
||||
<SignInModal closeable>
|
||||
<WaitForVerification email="example@example.com" showNewsletterSignup />
|
||||
</SignInModal>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
RoomWaitForVerification.parameters = {
|
||||
layout: "fullscreen"
|
||||
};
|
|
@ -0,0 +1,89 @@
|
|||
import React, { useCallback, useReducer, useContext, useEffect } from "react";
|
||||
import configs from "../../utils/configs";
|
||||
import { AuthContext } from "./AuthContext";
|
||||
import { SignInModal, SignInStep, WaitForVerification, SubmitEmail } from "./SignInModal";
|
||||
|
||||
const SignInAction = {
|
||||
submitEmail: "submitEmail",
|
||||
verificationReceived: "verificationReceived",
|
||||
cancel: "cancel"
|
||||
};
|
||||
|
||||
const initialSignInState = {
|
||||
step: SignInStep.submit,
|
||||
email: ""
|
||||
};
|
||||
|
||||
function loginReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case SignInAction.submitEmail:
|
||||
return { step: SignInStep.waitForVerification, email: action.email };
|
||||
case SignInAction.verificationReceived:
|
||||
return { ...state, step: SignInStep.complete };
|
||||
case SignInAction.cancel:
|
||||
return { ...state, step: SignInStep.submit };
|
||||
}
|
||||
}
|
||||
|
||||
function useSignIn() {
|
||||
const auth = useContext(AuthContext);
|
||||
const [state, dispatch] = useReducer(loginReducer, initialSignInState);
|
||||
|
||||
const submitEmail = useCallback(
|
||||
email => {
|
||||
auth.signIn(email).then(() => {
|
||||
dispatch({ type: SignInAction.verificationReceived });
|
||||
});
|
||||
dispatch({ type: SignInAction.submitEmail, email });
|
||||
},
|
||||
[auth]
|
||||
);
|
||||
|
||||
const cancel = useCallback(() => {
|
||||
dispatch({ type: SignInAction.cancel });
|
||||
}, []);
|
||||
|
||||
return {
|
||||
step: state.step,
|
||||
email: state.email,
|
||||
submitEmail,
|
||||
cancel
|
||||
};
|
||||
}
|
||||
|
||||
export function SignInModalContainer() {
|
||||
const qs = new URLSearchParams(location.search);
|
||||
const { step, submitEmail, cancel, email } = useSignIn();
|
||||
const redirectUrl = qs.get("sign_in_destination_url") || "/";
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (step === SignInStep.complete) {
|
||||
window.location = redirectUrl;
|
||||
}
|
||||
},
|
||||
[step, redirectUrl]
|
||||
);
|
||||
|
||||
return (
|
||||
<SignInModal disableFullscreen>
|
||||
{step === SignInStep.submit ? (
|
||||
<SubmitEmail
|
||||
onSubmitEmail={submitEmail}
|
||||
initialEmail={email}
|
||||
signInReason={qs.get("sign_in_reason")}
|
||||
termsUrl={configs.link("terms_of_use", "https://github.com/mozilla/hubs/blob/master/TERMS.md")}
|
||||
showTerms={configs.feature("show_terms")}
|
||||
privacyUrl={configs.link("privacy_notice", "https://github.com/mozilla/hubs/blob/master/PRIVACY.md")}
|
||||
showPrivacy={configs.feature("show_privacy")}
|
||||
/>
|
||||
) : (
|
||||
<WaitForVerification
|
||||
onCancel={cancel}
|
||||
email={email}
|
||||
showNewsletterSignup={configs.feature("show_newsletter_signup")}
|
||||
/>
|
||||
)}
|
||||
</SignInModal>
|
||||
);
|
||||
}
|
|
@ -1,176 +0,0 @@
|
|||
import React, { useCallback, useState, useReducer, useContext, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Page } from "../layout/Page";
|
||||
import styles from "./SignInPage.scss";
|
||||
import configs from "../../utils/configs";
|
||||
import IfFeature from "../if-feature";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { AuthContext } from "../auth/AuthContext";
|
||||
|
||||
const SignInStep = {
|
||||
submit: "submit",
|
||||
waitForVerification: "waitForVerification",
|
||||
complete: "complete"
|
||||
};
|
||||
|
||||
const SignInAction = {
|
||||
submitEmail: "submitEmail",
|
||||
verificationReceived: "verificationReceived",
|
||||
cancel: "cancel"
|
||||
};
|
||||
|
||||
const initialSignInState = {
|
||||
step: SignInStep.submit,
|
||||
email: ""
|
||||
};
|
||||
|
||||
function loginReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case SignInAction.submitEmail:
|
||||
return { step: SignInStep.waitForVerification, email: action.email };
|
||||
case SignInAction.verificationReceived:
|
||||
return { ...state, step: SignInStep.complete };
|
||||
case SignInAction.cancel:
|
||||
return { ...state, step: SignInStep.submit };
|
||||
}
|
||||
}
|
||||
|
||||
function useSignIn() {
|
||||
const auth = useContext(AuthContext);
|
||||
const [state, dispatch] = useReducer(loginReducer, initialSignInState);
|
||||
|
||||
const submitEmail = useCallback(
|
||||
email => {
|
||||
auth.signIn(email).then(() => {
|
||||
dispatch({ type: SignInAction.verificationReceived });
|
||||
});
|
||||
dispatch({ type: SignInAction.submitEmail, email });
|
||||
},
|
||||
[auth]
|
||||
);
|
||||
|
||||
const cancel = useCallback(() => {
|
||||
dispatch({ type: SignInAction.cancel });
|
||||
}, []);
|
||||
|
||||
return {
|
||||
step: state.step,
|
||||
email: state.email,
|
||||
submitEmail,
|
||||
cancel
|
||||
};
|
||||
}
|
||||
|
||||
function SubmitEmail({ onSubmitEmail, initialEmail }) {
|
||||
const [email, setEmail] = useState(initialEmail);
|
||||
|
||||
const onSubmitForm = useCallback(
|
||||
e => {
|
||||
e.preventDefault();
|
||||
onSubmitEmail(email);
|
||||
},
|
||||
[onSubmitEmail, email]
|
||||
);
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmitForm} className={styles.signInContainer}>
|
||||
<h1>
|
||||
<FormattedMessage id="sign-in.in" />
|
||||
</h1>
|
||||
<b>
|
||||
<FormattedMessage id="sign-in.prompt" />
|
||||
</b>
|
||||
<input
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
placeholder="example@example.com"
|
||||
/>
|
||||
{(configs.feature("show_terms") || configs.feature("show_privacy")) && (
|
||||
<b className={styles.terms}>
|
||||
By proceeding, you agree to the{" "}
|
||||
<IfFeature name="show_terms">
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href={configs.link("terms_of_use", "https://github.com/mozilla/hubs/blob/master/TERMS.md")}
|
||||
>
|
||||
terms of use
|
||||
</a>{" "}
|
||||
</IfFeature>
|
||||
{configs.feature("show_terms") && configs.feature("show_privacy") && "and "}
|
||||
<IfFeature name="show_privacy">
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href={configs.link("privacy_notice", "https://github.com/mozilla/hubs/blob/master/PRIVACY.md")}
|
||||
>
|
||||
privacy notice
|
||||
</a>
|
||||
</IfFeature>.
|
||||
</b>
|
||||
)}
|
||||
<button type="submit">next</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
SubmitEmail.defaultProps = {
|
||||
initialEmail: ""
|
||||
};
|
||||
|
||||
SubmitEmail.propTypes = {
|
||||
initialEmail: PropTypes.string,
|
||||
onSubmitEmail: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
function WaitForVerification({ email, onCancel }) {
|
||||
return (
|
||||
<div className={styles.signInContainer}>
|
||||
<p>
|
||||
<FormattedMessage id="sign-in.auth-started" values={{ email }} />
|
||||
</p>
|
||||
<IfFeature name="show_newsletter_signup">
|
||||
<p>
|
||||
Want Hubs news sent to your inbox?{"\n"}
|
||||
<a href="https://eepurl.com/gX_fH9" target="_blank" rel="noopener noreferrer">
|
||||
Subscribe for updates
|
||||
</a>.
|
||||
</p>
|
||||
</IfFeature>
|
||||
<button onClick={onCancel}>cancel</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
WaitForVerification.propTypes = {
|
||||
email: PropTypes.string.isRequired,
|
||||
onCancel: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export function SignInPage() {
|
||||
const qs = new URLSearchParams(location.search);
|
||||
const { step, submitEmail, cancel, email } = useSignIn();
|
||||
const redirectUrl = qs.get("sign_in_destination_url") || "/";
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (step === SignInStep.complete) {
|
||||
window.location = redirectUrl;
|
||||
}
|
||||
},
|
||||
[step, redirectUrl]
|
||||
);
|
||||
|
||||
return (
|
||||
<Page style={{ backgroundImage: configs.image("home_background", true), backgroundSize: "cover" }}>
|
||||
{step === SignInStep.submit ? (
|
||||
<SubmitEmail onSubmitEmail={submitEmail} initialEmail={email} signInReason={qs.get("sign_in_reason")} />
|
||||
) : (
|
||||
<WaitForVerification onCancel={cancel} email={email} />
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
@import '../../assets/stylesheets/shared';
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
background-color: #F3F3F3;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:local(.sign-in-container) {
|
||||
@extend %centered-flex-column;
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
width: 480px;
|
||||
padding: 2em;
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
@extend %input-field;
|
||||
margin: 1.5em 0 0.5em 0;
|
||||
}
|
||||
|
||||
button {
|
||||
@extend %action-button;
|
||||
}
|
||||
|
||||
:local(.terms) {
|
||||
font-size: 7pt;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import styles from "./VerifyModal.scss";
|
||||
import { Spinner } from "../misc/Spinner";
|
||||
import { Modal } from "../modal/Modal";
|
||||
|
||||
export const VerificationStep = {
|
||||
verifying: "verifying",
|
||||
complete: "complete",
|
||||
error: "error"
|
||||
};
|
||||
|
||||
export function EmailVerifying() {
|
||||
return (
|
||||
<div className={styles.modalContent}>
|
||||
<b>Email Verifying</b>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function EmailVerified({ origin }) {
|
||||
return (
|
||||
<div className={styles.modalContent}>
|
||||
<b>Verification Complete</b>
|
||||
<p>Please close this browser window and return to {origin}.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
EmailVerified.propTypes = {
|
||||
origin: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export function VerificationError({ error }) {
|
||||
return (
|
||||
<div className={styles.modalContent}>
|
||||
<b>Error Verifying Email</b>
|
||||
<p>{(error && error.message) || "Unknown Error"}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
VerificationError.propTypes = {
|
||||
error: PropTypes.object
|
||||
};
|
||||
|
||||
export function VerifyModal({ children }) {
|
||||
return (
|
||||
<Modal title="Verify" disableFullscreen>
|
||||
{children}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
VerifyModal.propTypes = {
|
||||
children: PropTypes.node
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
:local(.modal-content) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
line-height: 1.25;
|
||||
|
||||
svg, p {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
& > * {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import React from "react";
|
||||
import { Center } from "../layout/Center";
|
||||
import { Page } from "../layout/Page";
|
||||
import { VerifyModal, EmailVerifying, EmailVerified, VerificationError } from "./VerifyModal";
|
||||
import backgroundUrl from "../../assets/images/home-hero-background-unbranded.png";
|
||||
|
||||
export default {
|
||||
title: "VerifyModal"
|
||||
};
|
||||
|
||||
export const Verifying = () => (
|
||||
<Page style={{ backgroundImage: `url(${backgroundUrl})`, backgroundSize: "cover" }}>
|
||||
<Center>
|
||||
<VerifyModal>
|
||||
<EmailVerifying />
|
||||
</VerifyModal>
|
||||
</Center>
|
||||
</Page>
|
||||
);
|
||||
|
||||
Verifying.parameters = {
|
||||
layout: "fullscreen"
|
||||
};
|
||||
|
||||
export const Verified = () => (
|
||||
<Page style={{ backgroundImage: `url(${backgroundUrl})`, backgroundSize: "cover" }}>
|
||||
<Center>
|
||||
<VerifyModal>
|
||||
<EmailVerified origin="hubs.mozilla.com" />
|
||||
</VerifyModal>
|
||||
</Center>
|
||||
</Page>
|
||||
);
|
||||
|
||||
Verified.parameters = {
|
||||
layout: "fullscreen"
|
||||
};
|
||||
|
||||
export const Error = () => (
|
||||
<Page style={{ backgroundImage: `url(${backgroundUrl})`, backgroundSize: "cover" }}>
|
||||
<Center>
|
||||
<VerifyModal>
|
||||
<VerificationError />
|
||||
</VerifyModal>
|
||||
</Center>
|
||||
</Page>
|
||||
);
|
||||
|
||||
Error.parameters = {
|
||||
layout: "fullscreen"
|
||||
};
|
|
@ -0,0 +1,61 @@
|
|||
import React, { useState, useContext, useEffect } from "react";
|
||||
import { AuthContext } from "./AuthContext";
|
||||
import { VerifyModal, VerificationError, EmailVerified, EmailVerifying } from "./VerifyModal";
|
||||
|
||||
const VerificationStep = {
|
||||
verifying: "verifying",
|
||||
complete: "complete",
|
||||
error: "error"
|
||||
};
|
||||
|
||||
function useVerify() {
|
||||
const [step, setStep] = useState(VerificationStep.verifying);
|
||||
const [error, setError] = useState();
|
||||
const { verify } = useContext(AuthContext);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
const verifyAsync = async () => {
|
||||
try {
|
||||
const qs = new URLSearchParams(location.search);
|
||||
|
||||
const authParams = {
|
||||
topic: qs.get("auth_topic"),
|
||||
token: qs.get("auth_token"),
|
||||
origin: qs.get("auth_origin"),
|
||||
payload: qs.get("auth_payload")
|
||||
};
|
||||
|
||||
await verify(authParams);
|
||||
setStep(VerificationStep.complete);
|
||||
} catch (error) {
|
||||
setStep(VerificationStep.error);
|
||||
setError(error);
|
||||
}
|
||||
};
|
||||
|
||||
verifyAsync();
|
||||
},
|
||||
[verify]
|
||||
);
|
||||
|
||||
return { step, error };
|
||||
}
|
||||
|
||||
export function VerifyModalContainer() {
|
||||
const { step, error } = useVerify();
|
||||
|
||||
let content;
|
||||
|
||||
if (step === VerificationStep.error) {
|
||||
content = <VerificationError error={error} />;
|
||||
} else if (step === VerificationStep.complete) {
|
||||
const qs = new URLSearchParams(location.search);
|
||||
const origin = qs.get("auth_origin");
|
||||
content = <EmailVerified origin={origin} />;
|
||||
} else {
|
||||
content = <EmailVerifying />;
|
||||
}
|
||||
|
||||
return <VerifyModal>{content}</VerifyModal>;
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
import React, { useState, useContext, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Page } from "../layout/Page";
|
||||
import styles from "./SignInPage.scss";
|
||||
import { Loader } from "../misc/Loader";
|
||||
import { AuthContext } from "../auth/AuthContext";
|
||||
import configs from "../../utils/configs";
|
||||
|
||||
const VerificationStep = {
|
||||
verifying: "verifying",
|
||||
complete: "complete",
|
||||
error: "error"
|
||||
};
|
||||
|
||||
function useVerify() {
|
||||
const [step, setStep] = useState(VerificationStep.verifying);
|
||||
const [error, setError] = useState();
|
||||
const auth = useContext(AuthContext);
|
||||
|
||||
useEffect(() => {
|
||||
const verifyAsync = async () => {
|
||||
try {
|
||||
const qs = new URLSearchParams(location.search);
|
||||
|
||||
const authParams = {
|
||||
topic: qs.get("auth_topic"),
|
||||
token: qs.get("auth_token"),
|
||||
origin: qs.get("auth_origin"),
|
||||
payload: qs.get("auth_payload")
|
||||
};
|
||||
|
||||
await auth.verify(authParams);
|
||||
setStep(VerificationStep.complete);
|
||||
} catch (error) {
|
||||
setStep(VerificationStep.error);
|
||||
setError(error);
|
||||
}
|
||||
};
|
||||
|
||||
verifyAsync();
|
||||
}, []);
|
||||
|
||||
return { step, error };
|
||||
}
|
||||
|
||||
function EmailVerifying() {
|
||||
return (
|
||||
<div className={styles.signInContainer}>
|
||||
<h1>Email Verifying</h1>
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EmailVerified() {
|
||||
const qs = new URLSearchParams(location.search);
|
||||
const origin = qs.get("auth_origin");
|
||||
|
||||
return (
|
||||
<div className={styles.signInContainer}>
|
||||
<h1>Verification Complete</h1>
|
||||
<b>Please close this browser window and return to {origin}.</b>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function VerificationError({ error }) {
|
||||
return (
|
||||
<div className={styles.signInContainer}>
|
||||
<h1>Error Verifying Email</h1>
|
||||
<b>{(error && error.message) || "Unknown Error"}</b>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
VerificationError.propTypes = {
|
||||
error: PropTypes.object
|
||||
};
|
||||
|
||||
export function VerifyPage() {
|
||||
const { step, error } = useVerify();
|
||||
|
||||
let content;
|
||||
|
||||
if (step === VerificationStep.error) {
|
||||
content = <VerificationError error={error} />;
|
||||
} else if (step === VerificationStep.complete) {
|
||||
content = <EmailVerified />;
|
||||
} else {
|
||||
content = <EmailVerifying />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page style={{ backgroundImage: configs.image("home_background", true), backgroundSize: "cover" }}>{content}</Page>
|
||||
);
|
||||
}
|
|
@ -45,10 +45,8 @@ const fetchAvatar = async avatarId => {
|
|||
export default class AvatarEditor extends Component {
|
||||
static propTypes = {
|
||||
avatarId: PropTypes.string,
|
||||
onSignIn: PropTypes.func,
|
||||
onSave: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
signedIn: PropTypes.bool,
|
||||
hideDelete: PropTypes.bool,
|
||||
debug: PropTypes.bool,
|
||||
className: PropTypes.string
|
||||
|
@ -453,7 +451,7 @@ export default class AvatarEditor extends Component {
|
|||
<div className="loader">
|
||||
<div className="loader-center" />
|
||||
</div>
|
||||
) : this.props.signedIn ? (
|
||||
) : (
|
||||
<form onSubmit={this.uploadAvatar} className="center">
|
||||
{this.textField("name", "Name", false, true)}
|
||||
<div className="split">
|
||||
|
@ -580,10 +578,6 @@ export default class AvatarEditor extends Component {
|
|||
</div>
|
||||
)}
|
||||
</form>
|
||||
) : (
|
||||
<a onClick={this.props.onSignIn}>
|
||||
<FormattedMessage id="sign-in.in" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -3,7 +3,6 @@ import { FormattedMessage } from "react-intl";
|
|||
import classNames from "classnames";
|
||||
import configs from "../../utils/configs";
|
||||
import IfFeature from "../if-feature";
|
||||
import { Page } from "../layout/Page";
|
||||
import { CreateRoomButton } from "./CreateRoomButton";
|
||||
import { PWAButton } from "./PWAButton";
|
||||
import { useFavoriteRooms } from "./useFavoriteRooms";
|
||||
|
@ -14,6 +13,7 @@ import { AuthContext } from "../auth/AuthContext";
|
|||
import { createAndRedirectToNewHub } from "../../utils/phoenix-utils";
|
||||
import { MediaGrid } from "./MediaGrid";
|
||||
import { RoomTile } from "./RoomTile";
|
||||
import { PageContainer } from "../layout/PageContainer";
|
||||
|
||||
export function HomePage() {
|
||||
const auth = useContext(AuthContext);
|
||||
|
@ -57,7 +57,7 @@ export function HomePage() {
|
|||
});
|
||||
|
||||
return (
|
||||
<Page className={styles.homePage} style={pageStyle}>
|
||||
<PageContainer className={styles.homePage} style={pageStyle}>
|
||||
<section>
|
||||
<div className={styles.appInfo}>
|
||||
<div className={logoStyles}>
|
||||
|
@ -129,6 +129,6 @@ export function HomePage() {
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Page>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import classNames from "classnames";
|
||||
import styles from "./Center.scss";
|
||||
|
||||
export function Center({ children, className, ...rest }) {
|
||||
return (
|
||||
<div className={classNames(styles.center, className)} {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Center.propTypes = {
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
:local(.center) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
|
@ -1,65 +1,73 @@
|
|||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { WrappedIntlProvider } from "../wrapped-intl-provider";
|
||||
import IfFeature from "../if-feature";
|
||||
import UnlessFeature from "../unless-feature";
|
||||
import configs from "../../utils/configs";
|
||||
import styles from "./Footer.scss";
|
||||
|
||||
export function Footer() {
|
||||
export function Footer({
|
||||
hidePoweredBy,
|
||||
showWhatsNewLink,
|
||||
showTerms,
|
||||
termsUrl,
|
||||
showPrivacy,
|
||||
privacyUrl,
|
||||
showCompanyLogo,
|
||||
companyLogoUrl
|
||||
}) {
|
||||
return (
|
||||
<WrappedIntlProvider>
|
||||
<footer>
|
||||
<div className={styles.poweredBy}>
|
||||
<UnlessFeature name="hide_powered_by">
|
||||
<footer>
|
||||
<div className={styles.poweredBy}>
|
||||
{!hidePoweredBy && (
|
||||
<>
|
||||
<span className={styles.prefix}>
|
||||
<FormattedMessage id="home.powered_by_prefix" />
|
||||
</span>
|
||||
<a className={styles.link} href="https://hubs.mozilla.com/cloud">
|
||||
<FormattedMessage id="home.powered_by_link" />
|
||||
</a>
|
||||
</UnlessFeature>
|
||||
</div>
|
||||
<nav>
|
||||
<ul>
|
||||
<IfFeature name="show_whats_new_link">
|
||||
<li>
|
||||
<a href="/whats-new">
|
||||
<FormattedMessage id="home.whats_new_link" />
|
||||
</a>
|
||||
</li>
|
||||
</IfFeature>
|
||||
<IfFeature name="show_terms">
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={configs.link("terms_of_use", "https://github.com/mozilla/hubs/blob/master/TERMS.md")}
|
||||
>
|
||||
<FormattedMessage id="home.terms_of_use" />
|
||||
</a>
|
||||
</li>
|
||||
</IfFeature>
|
||||
<IfFeature name="show_privacy">
|
||||
<li>
|
||||
<a
|
||||
className={styles.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={configs.link("privacy_notice", "https://github.com/mozilla/hubs/blob/master/PRIVACY.md")}
|
||||
>
|
||||
<FormattedMessage id="home.privacy_notice" />
|
||||
</a>
|
||||
</li>
|
||||
</IfFeature>
|
||||
<IfFeature name="show_company_logo">
|
||||
<li>
|
||||
<img className={styles.companyLogo} src={configs.image("company_logo")} />
|
||||
</li>
|
||||
</IfFeature>
|
||||
</ul>
|
||||
</nav>
|
||||
</footer>
|
||||
</WrappedIntlProvider>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<nav>
|
||||
<ul>
|
||||
{showWhatsNewLink && (
|
||||
<li>
|
||||
<a href="/whats-new">
|
||||
<FormattedMessage id="home.whats_new_link" />
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{showTerms && (
|
||||
<li>
|
||||
<a target="_blank" rel="noopener noreferrer" href={termsUrl}>
|
||||
<FormattedMessage id="home.terms_of_use" />
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{showPrivacy && (
|
||||
<li>
|
||||
<a className={styles.link} target="_blank" rel="noopener noreferrer" href={privacyUrl}>
|
||||
<FormattedMessage id="home.privacy_notice" />
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{showCompanyLogo && (
|
||||
<li>
|
||||
<img className={styles.companyLogo} src={companyLogoUrl} />
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</nav>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
Footer.propTypes = {
|
||||
hidePoweredBy: PropTypes.bool,
|
||||
showWhatsNewLink: PropTypes.bool,
|
||||
showTerms: PropTypes.bool,
|
||||
termsUrl: PropTypes.string,
|
||||
showPrivacy: PropTypes.bool,
|
||||
privacyUrl: PropTypes.string,
|
||||
showCompanyLogo: PropTypes.bool,
|
||||
companyLogoUrl: PropTypes.string
|
||||
};
|
||||
|
|
|
@ -1,90 +1,109 @@
|
|||
import React, { useContext } from "react";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCog } from "@fortawesome/free-solid-svg-icons/faCog";
|
||||
import IfFeature from "../if-feature";
|
||||
import configs from "../../utils/configs";
|
||||
import maskEmail from "../../utils/mask-email";
|
||||
import styles from "./Header.scss";
|
||||
import { AuthContext } from "../auth/AuthContext";
|
||||
import { WrappedIntlProvider } from "../wrapped-intl-provider";
|
||||
|
||||
export function Header() {
|
||||
const auth = useContext(AuthContext);
|
||||
|
||||
export function Header({
|
||||
showCloud,
|
||||
enableSpoke,
|
||||
showDocsLink,
|
||||
docsUrl,
|
||||
showSourceLink,
|
||||
showCommunityLink,
|
||||
communityUrl,
|
||||
isAdmin,
|
||||
isSignedIn,
|
||||
email,
|
||||
onSignOut
|
||||
}) {
|
||||
return (
|
||||
<WrappedIntlProvider>
|
||||
<header>
|
||||
<nav>
|
||||
<ul>
|
||||
<header>
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/">Home</a>
|
||||
</li>
|
||||
{showCloud && (
|
||||
<li>
|
||||
<a href="/">Home</a>
|
||||
</li>
|
||||
<IfFeature name="show_cloud">
|
||||
<li>
|
||||
<a href="/cloud">
|
||||
<FormattedMessage id="home.cloud_link" />
|
||||
</a>
|
||||
</li>
|
||||
</IfFeature>
|
||||
<IfFeature name="enable_spoke">
|
||||
<li>
|
||||
<a href="/spoke">
|
||||
<FormattedMessage id="editor-name" />
|
||||
</a>
|
||||
</li>
|
||||
</IfFeature>
|
||||
<IfFeature name="show_docs_link">
|
||||
<li>
|
||||
<a href={configs.link("docs", "https://hubs.mozilla.com/docs")}>
|
||||
<FormattedMessage id="home.docs_link" />
|
||||
</a>
|
||||
</li>
|
||||
</IfFeature>
|
||||
<IfFeature name="show_source_link">
|
||||
<li>
|
||||
<a href="https://github.com/mozilla/hubs">
|
||||
<FormattedMessage id="home.source_link" />
|
||||
</a>
|
||||
</li>
|
||||
</IfFeature>
|
||||
<IfFeature name="show_community_link">
|
||||
<li>
|
||||
<a href={configs.link("community", "https://discord.gg/wHmY4nd")}>
|
||||
<FormattedMessage id="home.community_link" />
|
||||
</a>
|
||||
</li>
|
||||
</IfFeature>
|
||||
{auth.isAdmin && (
|
||||
<li>
|
||||
<a href="/admin" rel="noreferrer noopener">
|
||||
<i>
|
||||
<FontAwesomeIcon icon={faCog} />
|
||||
</i>
|
||||
|
||||
<FormattedMessage id="home.admin" />
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</nav>
|
||||
<div className={styles.signIn}>
|
||||
{auth.isSignedIn ? (
|
||||
<div>
|
||||
<span>
|
||||
<FormattedMessage id="sign-in.as" /> {maskEmail(auth.email)}
|
||||
</span>{" "}
|
||||
<a href="#" onClick={auth.signOut}>
|
||||
<FormattedMessage id="sign-in.out" />
|
||||
<a href="/cloud">
|
||||
<FormattedMessage id="home.cloud_link" />
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<a href="/signin" rel="noreferrer noopener">
|
||||
<FormattedMessage id="sign-in.in" />
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
</WrappedIntlProvider>
|
||||
{enableSpoke && (
|
||||
<li>
|
||||
<a href="/spoke">
|
||||
<FormattedMessage id="editor-name" />
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{showDocsLink && (
|
||||
<li>
|
||||
<a href={docsUrl}>
|
||||
<FormattedMessage id="home.docs_link" />
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{showSourceLink && (
|
||||
<li>
|
||||
<a href="https://github.com/mozilla/hubs">
|
||||
<FormattedMessage id="home.source_link" />
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{showCommunityLink && (
|
||||
<li>
|
||||
<a href={communityUrl}>
|
||||
<FormattedMessage id="home.community_link" />
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<li>
|
||||
<a href="/admin" rel="noreferrer noopener">
|
||||
<i>
|
||||
<FontAwesomeIcon icon={faCog} />
|
||||
</i>
|
||||
|
||||
<FormattedMessage id="home.admin" />
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</nav>
|
||||
<div className={styles.signIn}>
|
||||
{isSignedIn ? (
|
||||
<div>
|
||||
<span>
|
||||
<FormattedMessage id="sign-in.as" /> {maskEmail(email)}
|
||||
</span>{" "}
|
||||
<a href="#" onClick={onSignOut}>
|
||||
<FormattedMessage id="sign-in.out" />
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<a href="/signin" rel="noreferrer noopener">
|
||||
<FormattedMessage id="sign-in.in" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
Header.propTypes = {
|
||||
showCloud: PropTypes.bool,
|
||||
enableSpoke: PropTypes.bool,
|
||||
showDocsLink: PropTypes.bool,
|
||||
docsUrl: PropTypes.string,
|
||||
showSourceLink: PropTypes.bool,
|
||||
showCommunityLink: PropTypes.bool,
|
||||
communityUrl: PropTypes.string,
|
||||
isAdmin: PropTypes.bool,
|
||||
isSignedIn: PropTypes.bool,
|
||||
email: PropTypes.string,
|
||||
onSignOut: PropTypes.func
|
||||
};
|
||||
|
|
|
@ -4,16 +4,78 @@ import "./Page.scss";
|
|||
import { Header } from "./Header";
|
||||
import { Footer } from "./Footer";
|
||||
|
||||
export function Page({ children, ...rest }) {
|
||||
export function Page({
|
||||
showCloud,
|
||||
enableSpoke,
|
||||
showDocsLink,
|
||||
docsUrl,
|
||||
showSourceLink,
|
||||
showCommunityLink,
|
||||
communityUrl,
|
||||
isAdmin,
|
||||
isSignedIn,
|
||||
email,
|
||||
onSignOut,
|
||||
hidePoweredBy,
|
||||
showWhatsNewLink,
|
||||
showTerms,
|
||||
termsUrl,
|
||||
showPrivacy,
|
||||
privacyUrl,
|
||||
showCompanyLogo,
|
||||
companyLogoUrl,
|
||||
children,
|
||||
...rest
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Header
|
||||
showCloud={showCloud}
|
||||
enableSpoke={enableSpoke}
|
||||
showDocsLink={showDocsLink}
|
||||
docsUrl={docsUrl}
|
||||
showSourceLink={showSourceLink}
|
||||
showCommunityLink={showCommunityLink}
|
||||
communityUrl={communityUrl}
|
||||
isAdmin={isAdmin}
|
||||
isSignedIn={isSignedIn}
|
||||
email={email}
|
||||
onSignOut={onSignOut}
|
||||
/>
|
||||
<main {...rest}>{children}</main>
|
||||
<Footer />
|
||||
<Footer
|
||||
hidePoweredBy={hidePoweredBy}
|
||||
showWhatsNewLink={showWhatsNewLink}
|
||||
showTerms={showTerms}
|
||||
termsUrl={termsUrl}
|
||||
showPrivacy={showPrivacy}
|
||||
privacyUrl={privacyUrl}
|
||||
showCompanyLogo={showCompanyLogo}
|
||||
companyLogoUrl={companyLogoUrl}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Page.propTypes = {
|
||||
showCloud: PropTypes.bool,
|
||||
enableSpoke: PropTypes.bool,
|
||||
showDocsLink: PropTypes.bool,
|
||||
docsUrl: PropTypes.string,
|
||||
showSourceLink: PropTypes.bool,
|
||||
showCommunityLink: PropTypes.bool,
|
||||
communityUrl: PropTypes.string,
|
||||
isAdmin: PropTypes.bool,
|
||||
isSignedIn: PropTypes.bool,
|
||||
email: PropTypes.string,
|
||||
onSignOut: PropTypes.func,
|
||||
hidePoweredBy: PropTypes.bool,
|
||||
showWhatsNewLink: PropTypes.bool,
|
||||
showTerms: PropTypes.bool,
|
||||
termsUrl: PropTypes.string,
|
||||
showPrivacy: PropTypes.bool,
|
||||
privacyUrl: PropTypes.string,
|
||||
showCompanyLogo: PropTypes.bool,
|
||||
companyLogoUrl: PropTypes.string,
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #ui-root, .home-root {
|
||||
html, body, :global(#root), :global(#ui-root), :global(.home-root) {
|
||||
margin: 0;
|
||||
height:100%;
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ body {
|
|||
color: var(--home-text-color);
|
||||
}
|
||||
|
||||
#ui-root, .home-root {
|
||||
:global(#root), :global(#ui-root), :global(.home-root) {
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ h2 {
|
|||
main {
|
||||
display: block;
|
||||
order: -1;
|
||||
flex: 1;
|
||||
|
||||
@media(min-width: $breakpoint-lg) {
|
||||
order: 0;
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import React, { useContext } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Page } from "./Page";
|
||||
import { AuthContext } from "../auth/AuthContext";
|
||||
import configs from "../../utils/configs";
|
||||
|
||||
export function PageContainer({ children, ...rest }) {
|
||||
const auth = useContext(AuthContext);
|
||||
|
||||
return (
|
||||
<Page
|
||||
showCloud={configs.feature("show_cloud")}
|
||||
enableSpoke={configs.feature("enable_spoke")}
|
||||
showDocsLink={configs.feature("show_docs_link")}
|
||||
docsUrl={configs.link("docs", "https://hubs.mozilla.com/docs")}
|
||||
showSourceLink={configs.feature("show_source_link")}
|
||||
showCommunityLink={configs.feature("show_community_link")}
|
||||
communityUrl={configs.link("community", "https://discord.gg/wHmY4nd")}
|
||||
isAdmin={auth.isAdmin}
|
||||
isSignedIn={auth.isSignedIn}
|
||||
email={auth.email}
|
||||
onSignOut={auth.signOut}
|
||||
hidePoweredBy={configs.feature("hide_powered_by")}
|
||||
showWhatsNewLink={configs.feature("show_whats_new_link")}
|
||||
showTerms={configs.feature("show_terms")}
|
||||
termsUrl={configs.link("terms_of_use", "https://github.com/mozilla/hubs/blob/master/TERMS.md")}
|
||||
showPrivacy={configs.feature("show_privacy")}
|
||||
privacyUrl={configs.link("privacy_notice", "https://github.com/mozilla/hubs/blob/master/PRIVACY.md")}
|
||||
showCompanyLogo={configs.feature("show_company_logo")}
|
||||
companyLogoUrl={configs.image("company_logo")}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
PageContainer.propTypes = {
|
||||
children: PropTypes.node
|
||||
};
|
|
@ -1,20 +0,0 @@
|
|||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import styles from "./Loader.scss";
|
||||
|
||||
export function Loader({ message }) {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.loader}>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
{message && <div className={styles.loaderText}>{message}</div>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Loader.propTypes = {
|
||||
message: PropTypes.string
|
||||
};
|
|
@ -1,52 +0,0 @@
|
|||
:local(.loader) {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 80px;
|
||||
|
||||
div {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
width: 0.6em;
|
||||
background: var(--loading-spinner-color);
|
||||
animation: loading 1s ease-in-out infinite;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
div:nth-child(1) {
|
||||
left: 0.2em;
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
div:nth-child(2) {
|
||||
left: 1em;
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
div:nth-child(3) {
|
||||
left: 1.8em;
|
||||
animation-delay: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
top: 24px;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
40% {
|
||||
top: 8px;
|
||||
height: 3.5em;
|
||||
}
|
||||
}
|
||||
|
||||
:local(.loading-text) {
|
||||
font-weight: normal;
|
||||
font-size: 0.8em;
|
||||
color: var(--loading-text-color);
|
||||
margin-top: 0;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import React from "react";
|
||||
import { ReactComponent as SpinnerSvg } from "./Spinner.svg";
|
||||
import styles from "./Spinner.scss";
|
||||
|
||||
export function Spinner() {
|
||||
return <SpinnerSvg className={styles.spinner} />;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
@use "../styles/theme.scss";
|
||||
|
||||
:local(.spinner) {
|
||||
animation: rotate 3s infinite linear;
|
||||
|
||||
*[stroke=\#000] {
|
||||
stroke: theme.$blue;
|
||||
}
|
||||
|
||||
*[fill=\#000] {
|
||||
fill: theme.$blue;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,16 +2,6 @@ import React from "react";
|
|||
import PropTypes from "prop-types";
|
||||
import classNames from "classnames";
|
||||
import styles from "./Modal.scss";
|
||||
import { IconButton } from "../input/IconButton";
|
||||
import { ReactComponent as CloseIcon } from "../icons/Close.svg";
|
||||
|
||||
export function CloseButton(props) {
|
||||
return (
|
||||
<IconButton {...props} className={styles.sidebarButton}>
|
||||
<CloseIcon width={16} height={16} />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
export function Modal({ title, beforeTitle, afterTitle, children, contentClassName, className, disableFullscreen }) {
|
||||
return (
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import styles from "./LoadingScreen.scss";
|
||||
import { ReactComponent as Spinner } from "../misc/Spinner.svg";
|
||||
import { Spinner } from "../misc/Spinner";
|
||||
import { useRandomMessageTransition } from "./useRandomMessageTransition";
|
||||
|
||||
export function LoadingScreen({ logoSrc, message, infoMessages }) {
|
||||
|
@ -11,7 +11,7 @@ export function LoadingScreen({ logoSrc, message, infoMessages }) {
|
|||
<div className={styles.loadingScreen}>
|
||||
<div className={styles.center}>
|
||||
<img className={styles.logo} src={logoSrc} />
|
||||
<Spinner className={styles.spinner} />
|
||||
<Spinner />
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
<div className={styles.bottom}>
|
||||
|
|
|
@ -41,27 +41,6 @@
|
|||
max-height: 140px;
|
||||
}
|
||||
|
||||
:local(.spinner) {
|
||||
animation: rotate 3s infinite linear;
|
||||
|
||||
*[stroke=\#000] {
|
||||
stroke: theme.$blue;
|
||||
}
|
||||
|
||||
*[fill=\#000] {
|
||||
fill: theme.$blue;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:local(.bottom) {
|
||||
position: absolute;
|
||||
bottom: 10%;
|
||||
|
|
|
@ -32,7 +32,6 @@ import ChangeSceneDialog from "./change-scene-dialog.js";
|
|||
import AvatarUrlDialog from "./avatar-url-dialog.js";
|
||||
import InviteDialog from "./invite-dialog.js";
|
||||
import InviteTeamDialog from "./invite-team-dialog.js";
|
||||
import SignInDialog from "./sign-in-dialog.js";
|
||||
import RoomSettingsDialog from "./room-settings-dialog.js";
|
||||
import CloseRoomDialog from "./close-room-dialog.js";
|
||||
import Tip from "./tip.js";
|
||||
|
@ -100,6 +99,8 @@ import { PlacePopoverContainer } from "./room/PlacePopoverContainer";
|
|||
import { SharePopoverContainer } from "./room/SharePopoverContainer";
|
||||
import { VoiceButtonContainer } from "./room/VoiceButtonContainer";
|
||||
import { ReactionButtonContainer } from "./room/ReactionButtonContainer";
|
||||
import { RoomSignInModalContainer } from "./auth/RoomSignInModalContainer";
|
||||
import { SignInStep } from "./auth/SignInModal";
|
||||
|
||||
const avatarEditorDebug = qsTruthy("avatarEditorDebug");
|
||||
|
||||
|
@ -417,18 +418,22 @@ class UIRoot extends Component {
|
|||
onContinueAfterSignIn
|
||||
} = this.props;
|
||||
|
||||
this.showNonHistoriedDialog(SignInDialog, {
|
||||
this.showNonHistoriedDialog(RoomSignInModalContainer, {
|
||||
step: SignInStep.submit,
|
||||
message: getMessages()[signInMessageId],
|
||||
onSignIn: async email => {
|
||||
onSubmitEmail: async email => {
|
||||
const { authComplete } = await authChannel.startAuthentication(email, this.props.hubChannel);
|
||||
|
||||
this.showNonHistoriedDialog(SignInDialog, { authStarted: true, onClose: onContinueAfterSignIn });
|
||||
this.showNonHistoriedDialog(RoomSignInModalContainer, {
|
||||
step: SignInStep.waitForVerification,
|
||||
onClose: onContinueAfterSignIn
|
||||
});
|
||||
|
||||
await authComplete;
|
||||
|
||||
this.setState({ signedIn: true });
|
||||
this.showNonHistoriedDialog(SignInDialog, {
|
||||
authComplete: true,
|
||||
this.showNonHistoriedDialog(RoomSignInModalContainer, {
|
||||
step: SignInStep.complete,
|
||||
message: getMessages()[signInCompleteMessageId],
|
||||
continueText: getMessages()[signInContinueTextId],
|
||||
onClose: onContinueAfterSignIn,
|
||||
|
@ -861,22 +866,6 @@ class UIRoot extends Component {
|
|||
|
||||
renderDialog = (DialogClass, props = {}) => <DialogClass {...{ onClose: this.closeDialog, ...props }} />;
|
||||
|
||||
showSignInDialog = () => {
|
||||
this.showNonHistoriedDialog(SignInDialog, {
|
||||
message: getMessages()["sign-in.prompt"],
|
||||
onSignIn: async email => {
|
||||
const { authComplete } = await this.props.authChannel.startAuthentication(email, this.props.hubChannel);
|
||||
|
||||
this.showNonHistoriedDialog(SignInDialog, { authStarted: true });
|
||||
|
||||
await authComplete;
|
||||
|
||||
this.setState({ signedIn: true });
|
||||
this.closeDialog();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
signOut = async () => {
|
||||
await this.props.authChannel.signOut(this.props.hubChannel);
|
||||
this.setState({ signedIn: false });
|
||||
|
@ -1499,7 +1488,7 @@ class UIRoot extends Component {
|
|||
<AvatarEditor
|
||||
className={styles.avatarEditor}
|
||||
signedIn={this.state.signedIn}
|
||||
onSignIn={this.showSignInDialog}
|
||||
onSignIn={this.showContextualSignInDialog}
|
||||
onSave={() => {
|
||||
if (props.location.state.detail && props.location.state.detail.returnToProfile) {
|
||||
this.props.history.goBack();
|
||||
|
|
|
@ -6,8 +6,11 @@ import Store from "./storage/store";
|
|||
import "./utils/theme";
|
||||
import { getLocale, getMessages } from "./utils/i18n";
|
||||
import { AuthContextProvider } from "./react-components/auth/AuthContext";
|
||||
import { SignInPage } from "./react-components/auth/SignInPage";
|
||||
import { SignInModalContainer } from "./react-components/auth/SignInModalContainer";
|
||||
import { PageContainer } from "./react-components/layout/PageContainer";
|
||||
import configs from "./utils/configs";
|
||||
import "./assets/stylesheets/globals.scss";
|
||||
import { Center } from "./react-components/layout/Center";
|
||||
|
||||
registerTelemetry("/signin", "Hubs Sign In Page");
|
||||
|
||||
|
@ -18,7 +21,11 @@ function Root() {
|
|||
return (
|
||||
<WrappedIntlProvider locale={getLocale()} messages={getMessages()}>
|
||||
<AuthContextProvider store={store}>
|
||||
<SignInPage />
|
||||
<PageContainer style={{ backgroundImage: configs.image("home_background", true), backgroundSize: "cover" }}>
|
||||
<Center>
|
||||
<SignInModalContainer />
|
||||
</Center>
|
||||
</PageContainer>
|
||||
</AuthContextProvider>
|
||||
</WrappedIntlProvider>
|
||||
);
|
||||
|
|
|
@ -5,8 +5,11 @@ import registerTelemetry from "./telemetry";
|
|||
import Store from "./storage/store";
|
||||
import "./utils/theme";
|
||||
import { AuthContextProvider } from "./react-components/auth/AuthContext";
|
||||
import { VerifyPage } from "./react-components/auth/VerifyPage";
|
||||
import { VerifyModalContainer } from "./react-components/auth/VerifyModalContainer";
|
||||
import configs from "./utils/configs";
|
||||
import "./assets/stylesheets/globals.scss";
|
||||
import { PageContainer } from "./react-components/layout/PageContainer";
|
||||
import { Center } from "./react-components/layout/Center";
|
||||
|
||||
registerTelemetry("/verify", "Hubs Verify Email Page");
|
||||
|
||||
|
@ -17,7 +20,11 @@ function Root() {
|
|||
return (
|
||||
<WrappedIntlProvider>
|
||||
<AuthContextProvider store={store}>
|
||||
<VerifyPage />
|
||||
<PageContainer style={{ backgroundImage: configs.image("home_background", true), backgroundSize: "cover" }}>
|
||||
<Center>
|
||||
<VerifyModalContainer />
|
||||
</Center>
|
||||
</PageContainer>
|
||||
</AuthContextProvider>
|
||||
</WrappedIntlProvider>
|
||||
);
|
||||
|
|
Загрузка…
Ссылка в новой задаче