new signin/signup (#3960)
* new signin/signup * cleanup and password reset * new flow working * cleanup * add 'autoComplete' props * fix prop * try to call email/confirm before resetting password * Dont use password reset token for email confirmation. * add password reset * password manager improvements * update lbryinc * cleanup * slightly improve close button on sign up page * moar fixes * fix password autofil Co-authored-by: Mark Beamer Jr <markbeamerjr@gmail.com>
This commit is contained in:
parent
f177f2dbb9
commit
2677cd17d8
73 changed files with 1566 additions and 652 deletions
|
@ -30,6 +30,7 @@
|
||||||
"one-var": 0,
|
"one-var": 0,
|
||||||
"prefer-promise-reject-errors": 0,
|
"prefer-promise-reject-errors": 0,
|
||||||
"react/jsx-indent": 0,
|
"react/jsx-indent": 0,
|
||||||
|
"react/jsx-no-comment-textnodes": 0,
|
||||||
"react-hooks/exhaustive-deps": "warn",
|
"react-hooks/exhaustive-deps": "warn",
|
||||||
"react-hooks/rules-of-hooks": "error",
|
"react-hooks/rules-of-hooks": "error",
|
||||||
"react/no-unescaped-entities": 0,
|
"react/no-unescaped-entities": 0,
|
||||||
|
|
|
@ -68,7 +68,7 @@
|
||||||
"@babel/register": "^7.0.0",
|
"@babel/register": "^7.0.0",
|
||||||
"@exponent/electron-cookies": "^2.0.0",
|
"@exponent/electron-cookies": "^2.0.0",
|
||||||
"@hot-loader/react-dom": "^16.8",
|
"@hot-loader/react-dom": "^16.8",
|
||||||
"@lbry/components": "^4.0.1",
|
"@lbry/components": "^4.1.2",
|
||||||
"@reach/menu-button": "0.7.4",
|
"@reach/menu-button": "0.7.4",
|
||||||
"@reach/rect": "^0.2.1",
|
"@reach/rect": "^0.2.1",
|
||||||
"@reach/tabs": "^0.1.5",
|
"@reach/tabs": "^0.1.5",
|
||||||
|
@ -131,7 +131,7 @@
|
||||||
"json-loader": "^0.5.4",
|
"json-loader": "^0.5.4",
|
||||||
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
||||||
"lbry-redux": "lbryio/lbry-redux#1097a63d44a20b87e443fbaa48f95fe3ea5e3f70",
|
"lbry-redux": "lbryio/lbry-redux#1097a63d44a20b87e443fbaa48f95fe3ea5e3f70",
|
||||||
"lbryinc": "lbryio/lbryinc#0addc624db54000b0447f4539f91f5758d26eef3",
|
"lbryinc": "lbryio/lbryinc#12aefaa14343d2f3eac01f2683701f58e53f1848",
|
||||||
"lint-staged": "^7.0.2",
|
"lint-staged": "^7.0.2",
|
||||||
"localforage": "^1.7.1",
|
"localforage": "^1.7.1",
|
||||||
"lodash-es": "^4.17.14",
|
"lodash-es": "^4.17.14",
|
||||||
|
|
|
@ -1116,5 +1116,8 @@
|
||||||
"Repost %count%": "Repost %count%",
|
"Repost %count%": "Repost %count%",
|
||||||
"File Description": "File Description",
|
"File Description": "File Description",
|
||||||
"View %count% reposts": "View %count% reposts",
|
"View %count% reposts": "View %count% reposts",
|
||||||
"Preparing your content": "Preparing your content"
|
"Preparing your content": "Preparing your content",
|
||||||
|
"Already have an account? %sign_in%": "Already have an account? %sign_in%",
|
||||||
|
"Sign in with a password (optional)": "Sign in with a password (optional)",
|
||||||
|
"Don't have an account? %sign_up%": "Don't have an account? %sign_up%"
|
||||||
}
|
}
|
||||||
|
|
|
@ -165,7 +165,7 @@ class CardVerify extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{scriptFailedToLoad && (
|
{scriptFailedToLoad && (
|
||||||
<div className="error-text">There was an error connecting to Stripe. Please try again later.</div>
|
<div className="error__text">There was an error connecting to Stripe. Please try again later.</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -125,7 +125,7 @@ class ChannelCreate extends React.PureComponent<Props, State> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={this.handleCreateChannel}>
|
<Form onSubmit={this.handleCreateChannel}>
|
||||||
{createChannelError && <div className="error-text">{createChannelError}</div>}
|
{createChannelError && <div className="error__text">{createChannelError}</div>}
|
||||||
<div>
|
<div>
|
||||||
<FormField
|
<FormField
|
||||||
label={__('Name')}
|
label={__('Name')}
|
||||||
|
|
|
@ -17,6 +17,7 @@ type Props = {
|
||||||
isPageTitle?: boolean,
|
isPageTitle?: boolean,
|
||||||
isBodyTable?: boolean,
|
isBodyTable?: boolean,
|
||||||
defaultExpand?: boolean,
|
defaultExpand?: boolean,
|
||||||
|
nag?: Node,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Card(props: Props) {
|
export default function Card(props: Props) {
|
||||||
|
@ -30,6 +31,7 @@ export default function Card(props: Props) {
|
||||||
isPageTitle = false,
|
isPageTitle = false,
|
||||||
isBodyTable = false,
|
isBodyTable = false,
|
||||||
defaultExpand,
|
defaultExpand,
|
||||||
|
nag,
|
||||||
} = props;
|
} = props;
|
||||||
const [expanded, setExpanded] = useState(defaultExpand);
|
const [expanded, setExpanded] = useState(defaultExpand);
|
||||||
const expandable = defaultExpand !== undefined;
|
const expandable = defaultExpand !== undefined;
|
||||||
|
@ -73,6 +75,8 @@ export default function Card(props: Props) {
|
||||||
{actions && <div className="card__main-actions">{actions}</div>}
|
{actions && <div className="card__main-actions">{actions}</div>}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{nag}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,19 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: any,
|
children: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ErrorText(props: Props) {
|
export default function ErrorText(props: Props) {
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
|
|
||||||
return <span className="error-text">{children}</span>;
|
if (!children) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a period to the end of error messages
|
||||||
|
let errorMessage = children[0].toUpperCase() + children.slice(1);
|
||||||
|
errorMessage = errorMessage.endsWith('.') ? errorMessage : `${errorMessage}.`;
|
||||||
|
|
||||||
|
return <span className="error__text">{errorMessage}</span>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,7 +112,7 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
input = (
|
input = (
|
||||||
<fieldset-section>
|
<fieldset-section>
|
||||||
{(label || errorMessage) && (
|
{(label || errorMessage) && (
|
||||||
<label htmlFor={name}>{errorMessage ? <span className="error-text">{errorMessage}</span> : label}</label>
|
<label htmlFor={name}>{errorMessage ? <span className="error__text">{errorMessage}</span> : label}</label>
|
||||||
)}
|
)}
|
||||||
<select id={name} {...inputProps}>
|
<select id={name} {...inputProps}>
|
||||||
{children}
|
{children}
|
||||||
|
@ -173,7 +173,7 @@ export class FormField extends React.PureComponent<Props> {
|
||||||
<fieldset-section>
|
<fieldset-section>
|
||||||
{(label || errorMessage) && (
|
{(label || errorMessage) && (
|
||||||
<label htmlFor={name}>
|
<label htmlFor={name}>
|
||||||
{errorMessage ? <span className="error-text">{errorMessage}</span> : label}
|
{errorMessage ? <span className="error__text">{errorMessage}</span> : label}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
{prefix && <label htmlFor={name}>{prefix}</label>}
|
{prefix && <label htmlFor={name}>{prefix}</label>}
|
||||||
|
|
|
@ -306,6 +306,11 @@ export const icons = {
|
||||||
<line x1="15" y1="12" x2="3" y2="12" />
|
<line x1="15" y1="12" x2="3" y2="12" />
|
||||||
</g>
|
</g>
|
||||||
),
|
),
|
||||||
|
[ICONS.SIGN_UP]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
[ICONS.SIGN_OUT]: buildIcon(
|
[ICONS.SIGN_OUT]: buildIcon(
|
||||||
<g>
|
<g>
|
||||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||||
|
|
|
@ -7,16 +7,17 @@ import Button from 'component/button';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
message: string | Node,
|
message: string | Node,
|
||||||
actionText: string,
|
actionText?: string,
|
||||||
href?: string,
|
href?: string,
|
||||||
type?: string,
|
type?: string,
|
||||||
inline?: boolean,
|
inline?: boolean,
|
||||||
|
relative?: boolean,
|
||||||
onClick?: () => void,
|
onClick?: () => void,
|
||||||
onClose?: () => void,
|
onClose?: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Nag(props: Props) {
|
export default function Nag(props: Props) {
|
||||||
const { message, actionText, href, onClick, onClose, type, inline } = props;
|
const { message, actionText, href, onClick, onClose, type, inline, relative } = props;
|
||||||
|
|
||||||
const buttonProps = onClick ? { onClick } : { href };
|
const buttonProps = onClick ? { onClick } : { href };
|
||||||
|
|
||||||
|
@ -26,18 +27,21 @@ export default function Nag(props: Props) {
|
||||||
'nag--helpful': type === 'helpful',
|
'nag--helpful': type === 'helpful',
|
||||||
'nag--error': type === 'error',
|
'nag--error': type === 'error',
|
||||||
'nag--inline': inline,
|
'nag--inline': inline,
|
||||||
|
'nag--relative': relative,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="nag__message">{message}</div>
|
<div className="nag__message">{message}</div>
|
||||||
<Button
|
{(href || onClick) && (
|
||||||
className={classnames('nag__button', {
|
<Button
|
||||||
'nag__button--helpful': type === 'helpful',
|
className={classnames('nag__button', {
|
||||||
'nag__button--error': type === 'error',
|
'nag__button--helpful': type === 'helpful',
|
||||||
})}
|
'nag__button--error': type === 'error',
|
||||||
{...buttonProps}
|
})}
|
||||||
>
|
{...buttonProps}
|
||||||
{actionText}
|
>
|
||||||
</Button>
|
{actionText}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{onClose && (
|
{onClose && (
|
||||||
<Button
|
<Button
|
||||||
className={classnames('nag__button nag__close', {
|
className={classnames('nag__button nag__close', {
|
||||||
|
|
|
@ -96,20 +96,20 @@ class ErrorBoundary extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{!errorWasReported && (
|
{!errorWasReported && (
|
||||||
<div className="error-wrapper">
|
<div className="error__wrapper">
|
||||||
<span className="error-text">
|
<span className="error__text">
|
||||||
{__('You are not currently sharing diagnostic data so this error was not reported.')}
|
{__('You are not currently sharing diagnostic data so this error was not reported.')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{errorWasReported && (
|
{errorWasReported && (
|
||||||
<div className="error-wrapper">
|
<div className="error__wrapper">
|
||||||
{/* @if TARGET='web' */}
|
{/* @if TARGET='web' */}
|
||||||
<span className="error-text">{__('Error ID: %sentryEventId%', { sentryEventId })}</span>
|
<span className="error__text">{__('Error ID: %sentryEventId%', { sentryEventId })}</span>
|
||||||
{/* @endif */}
|
{/* @endif */}
|
||||||
{/* @if TARGET='app' */}
|
{/* @if TARGET='app' */}
|
||||||
<span className="error-text">{__('This error was reported and will be fixed.')}</span>
|
<span className="error__text">{__('This error was reported and will be fixed.')}</span>
|
||||||
{/* @endif */}
|
{/* @endif */}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -2,7 +2,14 @@ import * as SETTINGS from 'constants/settings';
|
||||||
import * as MODALS from 'constants/modal_types';
|
import * as MODALS from 'constants/modal_types';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectBalance, formatCredits } from 'lbry-redux';
|
import { selectBalance, formatCredits } from 'lbry-redux';
|
||||||
import { selectUserVerifiedEmail, selectGetSyncErrorMessage, selectUserEmail } from 'lbryinc';
|
import {
|
||||||
|
selectUserVerifiedEmail,
|
||||||
|
selectGetSyncErrorMessage,
|
||||||
|
selectUserEmail,
|
||||||
|
doClearEmailEntry,
|
||||||
|
doClearPasswordEntry,
|
||||||
|
selectEmailToVerify,
|
||||||
|
} from 'lbryinc';
|
||||||
import { doSetClientSetting } from 'redux/actions/settings';
|
import { doSetClientSetting } from 'redux/actions/settings';
|
||||||
import { doSignOut, doOpenModal } from 'redux/actions/app';
|
import { doSignOut, doOpenModal } from 'redux/actions/app';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
|
@ -18,6 +25,7 @@ const select = state => ({
|
||||||
authenticated: selectUserVerifiedEmail(state),
|
authenticated: selectUserVerifiedEmail(state),
|
||||||
email: selectUserEmail(state),
|
email: selectUserEmail(state),
|
||||||
syncError: selectGetSyncErrorMessage(state),
|
syncError: selectGetSyncErrorMessage(state),
|
||||||
|
emailToVerify: selectEmailToVerify(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
|
@ -26,6 +34,8 @@ const perform = dispatch => ({
|
||||||
openMobileNavigation: () => dispatch(doOpenModal(MODALS.MOBILE_NAVIGATION)),
|
openMobileNavigation: () => dispatch(doOpenModal(MODALS.MOBILE_NAVIGATION)),
|
||||||
openChannelCreate: () => dispatch(doOpenModal(MODALS.CREATE_CHANNEL)),
|
openChannelCreate: () => dispatch(doOpenModal(MODALS.CREATE_CHANNEL)),
|
||||||
openSignOutModal: () => dispatch(doOpenModal(MODALS.SIGN_OUT)),
|
openSignOutModal: () => dispatch(doOpenModal(MODALS.SIGN_OUT)),
|
||||||
|
clearEmailEntry: () => dispatch(doClearEmailEntry()),
|
||||||
|
clearPasswordEntry: () => dispatch(doClearPasswordEntry()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(Header);
|
export default connect(select, perform)(Header);
|
||||||
|
|
|
@ -36,10 +36,13 @@ type Props = {
|
||||||
authenticated: boolean,
|
authenticated: boolean,
|
||||||
authHeader: boolean,
|
authHeader: boolean,
|
||||||
syncError: ?string,
|
syncError: ?string,
|
||||||
|
emailToVerify?: string,
|
||||||
signOut: () => void,
|
signOut: () => void,
|
||||||
openMobileNavigation: () => void,
|
openMobileNavigation: () => void,
|
||||||
openChannelCreate: () => void,
|
openChannelCreate: () => void,
|
||||||
openSignOutModal: () => void,
|
openSignOutModal: () => void,
|
||||||
|
clearEmailEntry: () => void,
|
||||||
|
clearPasswordEntry: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Header = (props: Props) => {
|
const Header = (props: Props) => {
|
||||||
|
@ -58,15 +61,37 @@ const Header = (props: Props) => {
|
||||||
openMobileNavigation,
|
openMobileNavigation,
|
||||||
openChannelCreate,
|
openChannelCreate,
|
||||||
openSignOutModal,
|
openSignOutModal,
|
||||||
|
clearEmailEntry,
|
||||||
|
clearPasswordEntry,
|
||||||
|
emailToVerify,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
// on the verify page don't let anyone escape other than by closing the tab to keep session data consistent
|
// on the verify page don't let anyone escape other than by closing the tab to keep session data consistent
|
||||||
const isVerifyPage = history.location.pathname.includes(PAGES.AUTH_VERIFY);
|
const isVerifyPage = history.location.pathname.includes(PAGES.AUTH_VERIFY);
|
||||||
|
const isSignUpPage = history.location.pathname.includes(PAGES.AUTH);
|
||||||
|
const isSignInPage = history.location.pathname.includes(PAGES.AUTH_SIGNIN);
|
||||||
|
|
||||||
// Sign out if they click the "x" when they are on the password prompt
|
// Sign out if they click the "x" when they are on the password prompt
|
||||||
const authHeaderAction = syncError ? { onClick: signOut } : { navigate: '/' };
|
const authHeaderAction = syncError ? { onClick: signOut } : { navigate: '/' };
|
||||||
const homeButtonNavigationProps = isVerifyPage ? {} : authHeader ? authHeaderAction : { navigate: '/' };
|
const homeButtonNavigationProps = isVerifyPage ? {} : authHeader ? authHeaderAction : { navigate: '/' };
|
||||||
const closeButtonNavigationProps = authHeader ? authHeaderAction : { onClick: () => history.goBack() };
|
const closeButtonNavigationProps = {
|
||||||
|
onClick: () => {
|
||||||
|
clearEmailEntry();
|
||||||
|
clearPasswordEntry();
|
||||||
|
|
||||||
|
if (isSignInPage && !emailToVerify) {
|
||||||
|
history.goBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSignUpPage) {
|
||||||
|
history.goBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (syncError) {
|
||||||
|
signOut();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
function handleThemeToggle() {
|
function handleThemeToggle() {
|
||||||
if (automaticDarkModeEnabled) {
|
if (automaticDarkModeEnabled) {
|
||||||
|
@ -170,9 +195,6 @@ const Header = (props: Props) => {
|
||||||
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.CREATOR_DASHBOARD}`)}>
|
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.CREATOR_DASHBOARD}`)}>
|
||||||
<Icon aria-hidden icon={ICONS.ANALYTICS} />
|
<Icon aria-hidden icon={ICONS.ANALYTICS} />
|
||||||
{__('Creator Analytics')}
|
{__('Creator Analytics')}
|
||||||
<span>
|
|
||||||
<span className="badge badge--alert">New!</span>
|
|
||||||
</span>
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.REWARDS}`)}>
|
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.REWARDS}`)}>
|
||||||
<Icon aria-hidden icon={ICONS.FEATURED} />
|
<Icon aria-hidden icon={ICONS.FEATURED} />
|
||||||
|
@ -192,10 +214,16 @@ const Header = (props: Props) => {
|
||||||
<span className="menu__link-help">{email}</span>
|
<span className="menu__link-help">{email}</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
) : (
|
) : (
|
||||||
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.AUTH}`)}>
|
<React.Fragment>
|
||||||
<Icon aria-hidden icon={ICONS.SIGN_IN} />
|
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.AUTH}`)}>
|
||||||
{__('Sign In')}
|
<Icon aria-hidden icon={ICONS.SIGN_UP} />
|
||||||
</MenuItem>
|
{__('Join')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.AUTH_SIGNIN}`)}>
|
||||||
|
<Icon aria-hidden icon={ICONS.SIGN_IN} />
|
||||||
|
{__('Sign In')}
|
||||||
|
</MenuItem>
|
||||||
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
@ -223,7 +251,10 @@ const Header = (props: Props) => {
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</Menu>
|
</Menu>
|
||||||
{IS_WEB && !authenticated && (
|
{IS_WEB && !authenticated && (
|
||||||
<Button navigate={`/$/${PAGES.AUTH}`} button="primary" label={__('Sign In')} />
|
<div className="header__auth-buttons">
|
||||||
|
<Button navigate={`/$/${PAGES.AUTH_SIGNIN}`} button="link" label={__('Sign In')} />
|
||||||
|
<Button navigate={`/$/${PAGES.AUTH}`} button="primary" label={__('Join')} />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
@ -233,7 +264,12 @@ const Header = (props: Props) => {
|
||||||
{/* This pushes the close button to the right side */}
|
{/* This pushes the close button to the right side */}
|
||||||
<span />
|
<span />
|
||||||
<Tooltip label={__('Go Back')}>
|
<Tooltip label={__('Go Back')}>
|
||||||
<Button button="link" icon={ICONS.REMOVE} {...closeButtonNavigationProps} />
|
<Button
|
||||||
|
button="alt"
|
||||||
|
// className="button--header-close"
|
||||||
|
icon={ICONS.REMOVE}
|
||||||
|
{...closeButtonNavigationProps}
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -111,7 +111,7 @@ function Invited(props: Props) {
|
||||||
)}
|
)}
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<p className="error-text">{__('Not a valid invite')}</p>
|
<p className="error__text">{__('Not a valid invite')}</p>
|
||||||
<div className="card__actions">
|
<div className="card__actions">
|
||||||
<Button
|
<Button
|
||||||
button="primary"
|
button="primary"
|
||||||
|
|
|
@ -20,7 +20,7 @@ function PublishFormErrors(props: Props) {
|
||||||
// These are extra help
|
// These are extra help
|
||||||
// If there is an error it will be presented as an inline error as well
|
// If there is an error it will be presented as an inline error as well
|
||||||
return (
|
return (
|
||||||
<div className="error-text">
|
<div className="error__text">
|
||||||
{!title && <div>{__('A title is required')}</div>}
|
{!title && <div>{__('A title is required')}</div>}
|
||||||
{!name && <div>{__('A URL is required')}</div>}
|
{!name && <div>{__('A URL is required')}</div>}
|
||||||
{!isNameValid(name, false) && INVALID_NAME_ERROR}
|
{!isNameValid(name, false) && INVALID_NAME_ERROR}
|
||||||
|
|
|
@ -28,6 +28,8 @@ import TagsFollowingManagePage from 'page/tagsFollowingManage';
|
||||||
import ListBlockedPage from 'page/listBlocked';
|
import ListBlockedPage from 'page/listBlocked';
|
||||||
import FourOhFourPage from 'page/fourOhFour';
|
import FourOhFourPage from 'page/fourOhFour';
|
||||||
import SignInPage from 'page/signIn';
|
import SignInPage from 'page/signIn';
|
||||||
|
import SignUpPage from 'page/signUp';
|
||||||
|
import PasswordSetPage from 'page/passwordSet';
|
||||||
import SignInVerifyPage from 'page/signInVerify';
|
import SignInVerifyPage from 'page/signInVerify';
|
||||||
import ChannelsPage from 'page/channels';
|
import ChannelsPage from 'page/channels';
|
||||||
import EmbedWrapperPage from 'page/embedWrapper';
|
import EmbedWrapperPage from 'page/embedWrapper';
|
||||||
|
@ -152,8 +154,10 @@ function AppRouter(props: Props) {
|
||||||
|
|
||||||
<Route path={`/`} exact component={HomePage} />
|
<Route path={`/`} exact component={HomePage} />
|
||||||
<Route path={`/$/${PAGES.DISCOVER}`} exact component={DiscoverPage} />
|
<Route path={`/$/${PAGES.DISCOVER}`} exact component={DiscoverPage} />
|
||||||
<Route path={`/$/${PAGES.AUTH}`} exact component={SignInPage} />
|
<Route path={`/$/${PAGES.AUTH_SIGNIN}`} exact component={SignInPage} />
|
||||||
<Route path={`/$/${PAGES.AUTH}/*`} exact component={SignInPage} />
|
<Route path={`/$/${PAGES.AUTH_PASSWORD_SET}`} exact component={PasswordSetPage} />
|
||||||
|
<Route path={`/$/${PAGES.AUTH}`} exact component={SignUpPage} />
|
||||||
|
<Route path={`/$/${PAGES.AUTH}/*`} exact component={SignUpPage} />
|
||||||
<Route path={`/$/${PAGES.WELCOME}`} exact component={Welcome} />
|
<Route path={`/$/${PAGES.WELCOME}`} exact component={Welcome} />
|
||||||
<Route path={`/$/${PAGES.TAGS_FOLLOWING}`} exact component={TagsFollowingPage} />
|
<Route path={`/$/${PAGES.TAGS_FOLLOWING}`} exact component={TagsFollowingPage} />
|
||||||
<Route
|
<Route
|
||||||
|
|
|
@ -74,7 +74,7 @@ function SelectAsset(props: Props) {
|
||||||
</FormField>
|
</FormField>
|
||||||
{assetSource === SOURCE_UPLOAD && (
|
{assetSource === SOURCE_UPLOAD && (
|
||||||
<div>
|
<div>
|
||||||
{error && <div className="error-text">{error}</div>}
|
{error && <div className="error__text">{error}</div>}
|
||||||
{!pathSelected && (
|
{!pathSelected && (
|
||||||
<FileSelector
|
<FileSelector
|
||||||
label={'File to upload'}
|
label={'File to upload'}
|
||||||
|
|
22
ui/component/settingAccountPassword/index.js
Normal file
22
ui/component/settingAccountPassword/index.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
selectUser,
|
||||||
|
selectPasswordSetSuccess,
|
||||||
|
selectPasswordSetError,
|
||||||
|
doUserPasswordSet,
|
||||||
|
doClearPasswordEntry,
|
||||||
|
} from 'lbryinc';
|
||||||
|
import { doToast } from 'lbry-redux';
|
||||||
|
import UserSignIn from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
user: selectUser(state),
|
||||||
|
passwordSetSuccess: selectPasswordSetSuccess(state),
|
||||||
|
passwordSetError: selectPasswordSetError(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, {
|
||||||
|
doUserPasswordSet,
|
||||||
|
doToast,
|
||||||
|
doClearPasswordEntry,
|
||||||
|
})(UserSignIn);
|
88
ui/component/settingAccountPassword/view.jsx
Normal file
88
ui/component/settingAccountPassword/view.jsx
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
// @flow
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { FormField, Form } from 'component/common/form';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import ErrorText from 'component/common/error-text';
|
||||||
|
import Card from 'component/common/card';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
user: ?User,
|
||||||
|
doToast: ({ message: string }) => void,
|
||||||
|
doUserPasswordSet: (string, ?string) => void,
|
||||||
|
doClearPasswordEntry: () => void,
|
||||||
|
passwordSetSuccess: boolean,
|
||||||
|
passwordSetError: ?string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SettingAccountPassword(props: Props) {
|
||||||
|
const { user, doToast, doUserPasswordSet, passwordSetSuccess, passwordSetError, doClearPasswordEntry } = props;
|
||||||
|
const [oldPassword, setOldPassword] = useState('');
|
||||||
|
const [newPassword, setNewPassword] = useState('');
|
||||||
|
const [isAddingPassword, setIsAddingPassword] = useState(false);
|
||||||
|
const hasPassword = user && user.password_set;
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
doUserPasswordSet(newPassword, oldPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (passwordSetSuccess) {
|
||||||
|
setIsAddingPassword(false);
|
||||||
|
doToast({
|
||||||
|
message: __('Password updated successfully.'),
|
||||||
|
});
|
||||||
|
doClearPasswordEntry();
|
||||||
|
setOldPassword('');
|
||||||
|
setNewPassword('');
|
||||||
|
}
|
||||||
|
}, [passwordSetSuccess, setOldPassword, setNewPassword, doClearPasswordEntry, doToast]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title={__('Account Password')}
|
||||||
|
subtitle={hasPassword ? __('') : __('You do not currently have a password set.')}
|
||||||
|
actions={
|
||||||
|
isAddingPassword ? (
|
||||||
|
<div>
|
||||||
|
<Form onSubmit={handleSubmit} className="section">
|
||||||
|
{hasPassword && (
|
||||||
|
<FormField
|
||||||
|
type="password"
|
||||||
|
name="setting_set_old_password"
|
||||||
|
label={__('Old Password')}
|
||||||
|
value={oldPassword}
|
||||||
|
onChange={e => setOldPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<FormField
|
||||||
|
type="password"
|
||||||
|
name="setting_set_new_password"
|
||||||
|
label={__('New Password')}
|
||||||
|
value={newPassword}
|
||||||
|
onChange={e => setNewPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="section__actions">
|
||||||
|
<Button button="primary" type="submit" label={__('Set Password')} disabled={!newPassword} />
|
||||||
|
{!hasPassword && (
|
||||||
|
<Button button="link" label={__('Cancel')} onClick={() => setIsAddingPassword(false)} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
{passwordSetError && (
|
||||||
|
<div className="section">
|
||||||
|
<ErrorText>{passwordSetError}</ErrorText>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
button="primary"
|
||||||
|
label={hasPassword ? __('Update Your Password') : __('Add A Password')}
|
||||||
|
onClick={() => setIsAddingPassword(true)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -90,7 +90,12 @@ function SideNavigation(props: Props) {
|
||||||
<ul className="navigation-links">
|
<ul className="navigation-links">
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
...(expanded && !isAuthenticated ? { ...buildLink(PAGES.AUTH, __('Sign In'), ICONS.SIGN_IN) } : {}),
|
...(expanded && !isAuthenticated ? { ...buildLink(PAGES.AUTH, __('Sign Up'), ICONS.SIGN_UP) } : {}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...(expanded && !isAuthenticated
|
||||||
|
? { ...buildLink(PAGES.AUTH_SIGNIN, __('Sign In'), ICONS.SIGN_IN) }
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...buildLink(null, __('Home'), ICONS.HOME),
|
...buildLink(null, __('Home'), ICONS.HOME),
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import type { Node } from 'react';
|
||||||
import * as MODALS from 'constants/modal_types';
|
import * as MODALS from 'constants/modal_types';
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
@ -33,7 +34,7 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
details: string,
|
details: string | Node,
|
||||||
message: string,
|
message: string,
|
||||||
launchedModal: boolean,
|
launchedModal: boolean,
|
||||||
error: boolean,
|
error: boolean,
|
||||||
|
|
|
@ -156,7 +156,7 @@ const SupportsLiquidate = (props: Props) => {
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{abandonClaimError ? (
|
{abandonClaimError ? (
|
||||||
<>
|
<>
|
||||||
<div className="error-text">{__('%message%', { message: abandonClaimError })}</div>
|
<div className="error__text">{__('%message%', { message: abandonClaimError })}</div>
|
||||||
<Button disabled={error} button="primary" onClick={handleClose} label={__('Done')} />
|
<Button disabled={error} button="primary" onClick={handleClose} label={__('Done')} />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -5,7 +5,7 @@ import * as CS from 'constants/claim_search';
|
||||||
import Nag from 'component/common/nag';
|
import Nag from 'component/common/nag';
|
||||||
import { parseURI } from 'lbry-redux';
|
import { parseURI } from 'lbry-redux';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import { Form } from 'component/common/form-components/form';
|
import Card from 'component/common/card';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
subscribedChannels: Array<Subscription>,
|
subscribedChannels: Array<Subscription>,
|
||||||
|
@ -28,46 +28,46 @@ function UserChannelFollowIntro(props: Props) {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<Card
|
||||||
<h1 className="section__title--large">{__('Find Channels to Follow')}</h1>
|
title={__('Find Channels to Follow')}
|
||||||
<p className="section__subtitle">
|
subtitle={__(
|
||||||
{__(
|
'LBRY works better if you find and follow at least 5 creators you like. You can also block channels you never want to see.'
|
||||||
'LBRY works better if you find and follow at least 5 creators you like. You can also block channels you never want to see.'
|
)}
|
||||||
)}
|
actions={
|
||||||
</p>
|
<React.Fragment>
|
||||||
<Form onSubmit={onContinue} className="section__body">
|
<div className="section__actions">
|
||||||
<div className="card__actions">
|
<Button button="secondary" onClick={onBack} label={__('Back')} />
|
||||||
<Button button="secondary" onClick={onBack} label={__('Back')} />
|
<Button
|
||||||
<Button
|
button="primary"
|
||||||
button="primary"
|
type="Submit"
|
||||||
type="Submit"
|
onClick={onContinue}
|
||||||
onClick={onContinue}
|
label={__('Continue')}
|
||||||
label={__('Continue')}
|
disabled={subscribedChannels.length < 2}
|
||||||
disabled={subscribedChannels.length < 2}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
<div className="section__body">
|
||||||
</Form>
|
<ClaimListDiscover
|
||||||
<div className="section__body">
|
defaultOrderBy={CS.ORDER_BY_TOP}
|
||||||
<ClaimListDiscover
|
defaultFreshness={CS.FRESH_ALL}
|
||||||
defaultOrderBy={CS.ORDER_BY_TOP}
|
claimType="channel"
|
||||||
defaultFreshness={CS.FRESH_ALL}
|
defaultTags={CS.TAGS_FOLLOWED}
|
||||||
claimType="channel"
|
/>
|
||||||
defaultTags={CS.TAGS_FOLLOWED}
|
{followingCount > 0 && (
|
||||||
/>
|
<Nag
|
||||||
{followingCount > 0 && (
|
type="helpful"
|
||||||
<Nag
|
message={
|
||||||
type="helpful"
|
followingCount === 1
|
||||||
message={
|
? __('Nice! You are currently following %followingCount% creator', { followingCount })
|
||||||
followingCount === 1
|
: __('Nice! You are currently following %followingCount% creators', { followingCount })
|
||||||
? __('Nice! You are currently following %followingCount% creator', { followingCount })
|
}
|
||||||
: __('Nice! You are currently following %followingCount% creators', { followingCount })
|
actionText={__('Continue')}
|
||||||
}
|
onClick={onContinue}
|
||||||
actionText={__('Continue')}
|
/>
|
||||||
onClick={onContinue}
|
)}
|
||||||
/>
|
</div>
|
||||||
)}
|
</React.Fragment>
|
||||||
</div>
|
}
|
||||||
</React.Fragment>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,4 @@ const perform = dispatch => ({
|
||||||
fetchAccessToken: () => dispatch(doFetchAccessToken()),
|
fetchAccessToken: () => dispatch(doFetchAccessToken()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(select, perform)(UserEmailVerify);
|
||||||
select,
|
|
||||||
perform
|
|
||||||
)(UserEmailVerify);
|
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectEmailNewIsPending, selectEmailNewErrorMessage, doUserEmailNew } from 'lbryinc';
|
import {
|
||||||
|
selectEmailNewIsPending,
|
||||||
|
selectEmailNewErrorMessage,
|
||||||
|
selectEmailAlreadyExists,
|
||||||
|
doUserSignUp,
|
||||||
|
doClearEmailEntry,
|
||||||
|
} from 'lbryinc';
|
||||||
import { DAEMON_SETTINGS } from 'lbry-redux';
|
import { DAEMON_SETTINGS } from 'lbry-redux';
|
||||||
import { doSetClientSetting, doSetDaemonSetting } from 'redux/actions/settings';
|
import { doSetClientSetting, doSetDaemonSetting } from 'redux/actions/settings';
|
||||||
import { makeSelectClientSetting, selectDaemonSettings } from 'redux/selectors/settings';
|
import { makeSelectClientSetting, selectDaemonSettings } from 'redux/selectors/settings';
|
||||||
|
@ -11,16 +17,15 @@ const select = state => ({
|
||||||
errorMessage: selectEmailNewErrorMessage(state),
|
errorMessage: selectEmailNewErrorMessage(state),
|
||||||
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
|
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
|
||||||
daemonSettings: selectDaemonSettings(state),
|
daemonSettings: selectDaemonSettings(state),
|
||||||
|
emailExists: selectEmailAlreadyExists(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
addUserEmail: email => dispatch(doUserEmailNew(email)),
|
|
||||||
setSync: value => dispatch(doSetClientSetting(SETTINGS.ENABLE_SYNC, value)),
|
setSync: value => dispatch(doSetClientSetting(SETTINGS.ENABLE_SYNC, value)),
|
||||||
setShareDiagnosticData: shouldShareData =>
|
setShareDiagnosticData: shouldShareData =>
|
||||||
dispatch(doSetDaemonSetting(DAEMON_SETTINGS.SHARE_USAGE_DATA, shouldShareData)),
|
dispatch(doSetDaemonSetting(DAEMON_SETTINGS.SHARE_USAGE_DATA, shouldShareData)),
|
||||||
|
doSignUp: (email, password) => dispatch(doUserSignUp(email, password)),
|
||||||
|
clearEmailEntry: () => dispatch(doClearEmailEntry()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(select, perform)(UserEmailNew);
|
||||||
select,
|
|
||||||
perform
|
|
||||||
)(UserEmailNew);
|
|
||||||
|
|
|
@ -1,29 +1,50 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import * as PAGES from 'constants/pages';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { FormField, Form } from 'component/common/form';
|
import { FormField, Form } from 'component/common/form';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import analytics from 'analytics';
|
import analytics from 'analytics';
|
||||||
import { EMAIL_REGEX } from 'constants/email';
|
import { EMAIL_REGEX } from 'constants/email';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import Card from 'component/common/card';
|
||||||
|
import ErrorText from 'component/common/error-text';
|
||||||
|
import Nag from 'component/common/nag';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
errorMessage: ?string,
|
errorMessage: ?string,
|
||||||
|
emailExists: boolean,
|
||||||
isPending: boolean,
|
isPending: boolean,
|
||||||
addUserEmail: string => void,
|
|
||||||
syncEnabled: boolean,
|
syncEnabled: boolean,
|
||||||
setSync: boolean => void,
|
setSync: boolean => void,
|
||||||
balance: number,
|
balance: number,
|
||||||
daemonSettings: { share_usage_data: boolean },
|
daemonSettings: { share_usage_data: boolean },
|
||||||
setShareDiagnosticData: boolean => void,
|
setShareDiagnosticData: boolean => void,
|
||||||
|
doSignUp: (string, ?string) => void,
|
||||||
|
clearEmailEntry: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function UserEmailNew(props: Props) {
|
function UserEmailNew(props: Props) {
|
||||||
const { errorMessage, isPending, addUserEmail, setSync, daemonSettings, setShareDiagnosticData } = props;
|
const {
|
||||||
|
errorMessage,
|
||||||
|
isPending,
|
||||||
|
doSignUp,
|
||||||
|
setSync,
|
||||||
|
daemonSettings,
|
||||||
|
setShareDiagnosticData,
|
||||||
|
clearEmailEntry,
|
||||||
|
emailExists,
|
||||||
|
} = props;
|
||||||
const { share_usage_data: shareUsageData } = daemonSettings;
|
const { share_usage_data: shareUsageData } = daemonSettings;
|
||||||
const [newEmail, setEmail] = useState('');
|
const { push, location } = useHistory();
|
||||||
|
const urlParams = new URLSearchParams(location.search);
|
||||||
|
const emailFromUrl = urlParams.get('email');
|
||||||
|
const defaultEmail = emailFromUrl ? decodeURIComponent(emailFromUrl) : '';
|
||||||
|
const [email, setEmail] = useState(defaultEmail);
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
const [localShareUsageData, setLocalShareUsageData] = React.useState(false);
|
const [localShareUsageData, setLocalShareUsageData] = React.useState(false);
|
||||||
const [formSyncEnabled, setFormSyncEnabled] = useState(true);
|
const [formSyncEnabled, setFormSyncEnabled] = useState(true);
|
||||||
const valid = newEmail.match(EMAIL_REGEX);
|
const valid = email.match(EMAIL_REGEX);
|
||||||
|
|
||||||
function handleUsageDataChange() {
|
function handleUsageDataChange() {
|
||||||
setLocalShareUsageData(!localShareUsageData);
|
setLocalShareUsageData(!localShareUsageData);
|
||||||
|
@ -31,96 +52,123 @@ function UserEmailNew(props: Props) {
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
setSync(formSyncEnabled);
|
setSync(formSyncEnabled);
|
||||||
addUserEmail(newEmail);
|
doSignUp(email, password === '' ? undefined : password);
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
setShareDiagnosticData(true);
|
setShareDiagnosticData(true);
|
||||||
// @endif
|
// @endif
|
||||||
analytics.emailProvidedEvent();
|
analytics.emailProvidedEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleChangeToSignIn(additionalParams) {
|
||||||
|
clearEmailEntry();
|
||||||
|
|
||||||
|
let url = `/$/${PAGES.AUTH_SIGNIN}`;
|
||||||
|
const urlParams = new URLSearchParams(location.search);
|
||||||
|
|
||||||
|
urlParams.delete('email');
|
||||||
|
if (email) {
|
||||||
|
urlParams.set('email', encodeURIComponent(email));
|
||||||
|
}
|
||||||
|
|
||||||
|
urlParams.delete('email_exists');
|
||||||
|
if (emailExists) {
|
||||||
|
urlParams.set('email_exists', '1');
|
||||||
|
}
|
||||||
|
|
||||||
|
push(`${url}?${urlParams.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (emailExists) {
|
||||||
|
handleChangeToSignIn();
|
||||||
|
}
|
||||||
|
}, [emailExists]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<div className="main__sign-up">
|
||||||
<h1 className="section__title--large">{__('Sign In to lbry.tv')}</h1>
|
<Card
|
||||||
<p className="section__subtitle">
|
title={__('Join lbry.tv')}
|
||||||
{/* @if TARGET='web' */}
|
// @if TARGET='app'
|
||||||
{__('Create a new account or sign in.')}
|
subtitle={__('An account with lbry.tv allows you to earn rewards and backup your data.')}
|
||||||
{/* @endif */}
|
// @endif
|
||||||
{/* @if TARGET='app' */}
|
actions={
|
||||||
{__('An account with lbry.tv allows you to earn rewards and backup your data.')}
|
<div>
|
||||||
{/* @endif */}
|
<Form onSubmit={handleSubmit} className="section">
|
||||||
</p>
|
<FormField
|
||||||
<Form onSubmit={handleSubmit} className="section__body">
|
autoFocus
|
||||||
<FormField
|
placeholder={__('hotstuff_96@hotmail.com')}
|
||||||
autoFocus
|
type="email"
|
||||||
placeholder={__('hotstuff_96@hotmail.com')}
|
name="sign_up_email"
|
||||||
type="email"
|
label={__('Email')}
|
||||||
name="sign_up_email"
|
value={email}
|
||||||
label={__('Email')}
|
onChange={e => setEmail(e.target.value)}
|
||||||
value={newEmail}
|
/>
|
||||||
error={errorMessage}
|
<FormField
|
||||||
onChange={e => setEmail(e.target.value)}
|
type="password"
|
||||||
/>
|
name="sign_in_password"
|
||||||
|
label={__('Password')}
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
{!IS_WEB && (
|
{!IS_WEB && (
|
||||||
<FormField
|
<FormField
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="sync_checkbox"
|
name="sync_checkbox"
|
||||||
label={
|
label={
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{__('Backup your account and wallet data.')}{' '}
|
{__('Backup your account and wallet data.')}{' '}
|
||||||
<Button button="link" href="https://lbry.com/faq/account-sync" label={__('Learn More')} />
|
<Button button="link" href="https://lbry.com/faq/account-sync" label={__('Learn More')} />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
checked={formSyncEnabled}
|
checked={formSyncEnabled}
|
||||||
onChange={() => setFormSyncEnabled(!formSyncEnabled)}
|
onChange={() => setFormSyncEnabled(!formSyncEnabled)}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!shareUsageData && !IS_WEB && (
|
|
||||||
<FormField
|
|
||||||
type="checkbox"
|
|
||||||
name="share_data_checkbox"
|
|
||||||
checked={localShareUsageData}
|
|
||||||
onChange={handleUsageDataChange}
|
|
||||||
label={
|
|
||||||
<React.Fragment>
|
|
||||||
{__('Share usage data with LBRY inc.')}{' '}
|
|
||||||
<Button button="link" href="https://lbry.com/faq/privacy-and-data" label={__('Learn More')} />
|
|
||||||
{!localShareUsageData && <span className="error-text"> ({__('Required')})</span>}
|
|
||||||
</React.Fragment>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="card__actions">
|
|
||||||
<Button
|
|
||||||
button="primary"
|
|
||||||
type="submit"
|
|
||||||
label={__('Continue')}
|
|
||||||
disabled={!newEmail || !valid || (!IS_WEB && (!localShareUsageData && !shareUsageData)) || isPending}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
{/* @if TARGET='web' */}
|
|
||||||
<p className="help">
|
|
||||||
<React.Fragment>
|
|
||||||
<I18nMessage
|
|
||||||
tokens={{
|
|
||||||
terms: (
|
|
||||||
<Button
|
|
||||||
tabIndex="2"
|
|
||||||
button="link"
|
|
||||||
href="https://www.lbry.com/termsofservice"
|
|
||||||
label={__('Terms of Service')}
|
|
||||||
/>
|
/>
|
||||||
),
|
)}
|
||||||
}}
|
|
||||||
>
|
{!shareUsageData && !IS_WEB && (
|
||||||
By continuing, I agree to the %terms% and confirm I am over the age of 13.
|
<FormField
|
||||||
</I18nMessage>
|
type="checkbox"
|
||||||
</React.Fragment>
|
name="share_data_checkbox"
|
||||||
</p>
|
checked={localShareUsageData}
|
||||||
{/* @endif */}
|
onChange={handleUsageDataChange}
|
||||||
</React.Fragment>
|
label={
|
||||||
|
<React.Fragment>
|
||||||
|
{__('Share usage data with LBRY inc.')}{' '}
|
||||||
|
<Button button="link" href="https://lbry.com/faq/privacy-and-data" label={__('Learn More')} />
|
||||||
|
{!localShareUsageData && <span className="error__text"> ({__('Required')})</span>}
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="section__actions">
|
||||||
|
<Button
|
||||||
|
button="primary"
|
||||||
|
type="submit"
|
||||||
|
label={__('Join')}
|
||||||
|
disabled={
|
||||||
|
!email || !password || !valid || (!IS_WEB && !localShareUsageData && !shareUsageData) || isPending
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button button="link" onClick={handleChangeToSignIn} label={__('Sign In')} />
|
||||||
|
</div>
|
||||||
|
<p className="help">
|
||||||
|
<I18nMessage
|
||||||
|
tokens={{
|
||||||
|
terms: (
|
||||||
|
<Button button="link" href="https://www.lbry.com/termsofservice" label={__('Terms of Service')} />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
By continuing, I agree to the %terms% and confirm I am over the age of 13.
|
||||||
|
</I18nMessage>
|
||||||
|
</p>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
nag={errorMessage && <Nag type="error" relative message={<ErrorText>{errorMessage}</ErrorText>} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
22
ui/component/userEmailReturning/index.js
Normal file
22
ui/component/userEmailReturning/index.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
selectEmailNewErrorMessage,
|
||||||
|
selectEmailToVerify,
|
||||||
|
doUserCheckIfEmailExists,
|
||||||
|
doClearEmailEntry,
|
||||||
|
selectEmailDoesNotExist,
|
||||||
|
selectEmailAlreadyExists,
|
||||||
|
} from 'lbryinc';
|
||||||
|
import UserEmailReturning from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
errorMessage: selectEmailNewErrorMessage(state),
|
||||||
|
emailToVerify: selectEmailToVerify(state),
|
||||||
|
emailDoesNotExist: selectEmailDoesNotExist(state),
|
||||||
|
emailExists: selectEmailAlreadyExists(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, {
|
||||||
|
doUserCheckIfEmailExists,
|
||||||
|
doClearEmailEntry,
|
||||||
|
})(UserEmailReturning);
|
111
ui/component/userEmailReturning/view.jsx
Normal file
111
ui/component/userEmailReturning/view.jsx
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
// @flow
|
||||||
|
import * as PAGES from 'constants/pages';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { FormField, Form } from 'component/common/form';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import { EMAIL_REGEX } from 'constants/email';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import UserEmailVerify from 'component/userEmailVerify';
|
||||||
|
import Card from 'component/common/card';
|
||||||
|
import Nag from 'component/common/nag';
|
||||||
|
import analytics from 'analytics';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
errorMessage: ?string,
|
||||||
|
emailToVerify: ?string,
|
||||||
|
emailDoesNotExist: boolean,
|
||||||
|
doClearEmailEntry: () => void,
|
||||||
|
doUserSignIn: (string, ?string) => void,
|
||||||
|
doUserCheckIfEmailExists: string => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
function UserEmailReturning(props: Props) {
|
||||||
|
const { errorMessage, doUserCheckIfEmailExists, emailToVerify, doClearEmailEntry, emailDoesNotExist } = props;
|
||||||
|
const { push, location } = useHistory();
|
||||||
|
const urlParams = new URLSearchParams(location.search);
|
||||||
|
const emailFromUrl = urlParams.get('email');
|
||||||
|
const emailExistsFromUrl = urlParams.get('email_exists');
|
||||||
|
const defaultEmail = emailFromUrl ? decodeURIComponent(emailFromUrl) : '';
|
||||||
|
const [email, setEmail] = useState(defaultEmail);
|
||||||
|
const valid = email.match(EMAIL_REGEX);
|
||||||
|
const showEmailVerification = emailToVerify;
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
doUserCheckIfEmailExists(email);
|
||||||
|
analytics.emailProvidedEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChangeToSignIn() {
|
||||||
|
doClearEmailEntry();
|
||||||
|
let url = `/$/${PAGES.AUTH}`;
|
||||||
|
const urlParams = new URLSearchParams(location.search);
|
||||||
|
|
||||||
|
urlParams.delete('email_exists');
|
||||||
|
urlParams.delete('email');
|
||||||
|
if (email) {
|
||||||
|
urlParams.set('email', encodeURIComponent(email));
|
||||||
|
}
|
||||||
|
|
||||||
|
push(`${url}?${urlParams.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="main__sign-in">
|
||||||
|
{showEmailVerification ? (
|
||||||
|
<UserEmailVerify />
|
||||||
|
) : (
|
||||||
|
<Card
|
||||||
|
title={__('Sign In to lbry.tv')}
|
||||||
|
actions={
|
||||||
|
<div>
|
||||||
|
<Form onSubmit={handleSubmit} className="section">
|
||||||
|
<FormField
|
||||||
|
autoFocus={!emailExistsFromUrl}
|
||||||
|
placeholder={__('hotstuff_96@hotmail.com')}
|
||||||
|
type="email"
|
||||||
|
id="username"
|
||||||
|
autoComplete="on"
|
||||||
|
name="sign_in_email"
|
||||||
|
label={__('Email')}
|
||||||
|
value={email}
|
||||||
|
onChange={e => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="section__actions">
|
||||||
|
<Button
|
||||||
|
autoFocus={emailExistsFromUrl}
|
||||||
|
button="primary"
|
||||||
|
type="submit"
|
||||||
|
label={__('Sign In')}
|
||||||
|
disabled={!email || !valid}
|
||||||
|
/>
|
||||||
|
<Button button="link" onClick={handleChangeToSignIn} label={__('Join')} />
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
nag={
|
||||||
|
<React.Fragment>
|
||||||
|
{!emailDoesNotExist && emailExistsFromUrl && (
|
||||||
|
<Nag type="helpful" relative message={__('That email is already in use. Did you mean to sign in?')} />
|
||||||
|
)}
|
||||||
|
{emailDoesNotExist && (
|
||||||
|
<Nag
|
||||||
|
type="helpful"
|
||||||
|
relative
|
||||||
|
message={__("We can't find that email. Did you mean to join?")}
|
||||||
|
actionText={__('Join')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!emailExistsFromUrl && !emailDoesNotExist && errorMessage && (
|
||||||
|
<Nag type="error" relative message={errorMessage} />
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserEmailReturning;
|
|
@ -23,7 +23,4 @@ const perform = dispatch => ({
|
||||||
toast: message => dispatch(doToast({ message })),
|
toast: message => dispatch(doToast({ message })),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(select, perform)(UserEmailVerify);
|
||||||
select,
|
|
||||||
perform
|
|
||||||
)(UserEmailVerify);
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import * as React from 'react';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import UserSignOutButton from 'component/userSignOutButton';
|
import UserSignOutButton from 'component/userSignOutButton';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
|
import Card from 'component/common/card';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
email: string,
|
email: string,
|
||||||
|
@ -24,11 +25,14 @@ class UserEmailVerify extends React.PureComponent<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.emailVerifyCheckInterval = setInterval(() => this.checkIfVerified(), 5000);
|
this.emailVerifyCheckInterval = setInterval(() => {
|
||||||
|
this.checkIfVerified();
|
||||||
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
const { user } = this.props;
|
const { user } = this.props;
|
||||||
|
|
||||||
if (this.emailVerifyCheckInterval && user && user.has_verified_email) {
|
if (this.emailVerifyCheckInterval && user && user.has_verified_email) {
|
||||||
clearInterval(this.emailVerifyCheckInterval);
|
clearInterval(this.emailVerifyCheckInterval);
|
||||||
}
|
}
|
||||||
|
@ -57,40 +61,45 @@ class UserEmailVerify extends React.PureComponent<Props> {
|
||||||
const { email, isReturningUser, resendingEmail } = this.props;
|
const { email, isReturningUser, resendingEmail } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<div className="main__sign-up">
|
||||||
<h1 className="section__title--large">{isReturningUser ? __('Check Your Email') : __('Confirm Your Email')}</h1>
|
<Card
|
||||||
|
title={isReturningUser ? __('Check Your Email') : __('Confirm Your Email')}
|
||||||
<p className="section__subtitle">
|
subtitle={
|
||||||
{__(
|
<p>
|
||||||
'An email was sent to %email%. Follow the link to %verify_text%. After, this page will update automatically.',
|
{__(
|
||||||
{
|
'An email was sent to %email%. Follow the link to %verify_text%. After, this page will update automatically.',
|
||||||
email,
|
{
|
||||||
verify_text: isReturningUser ? __('sign in') : __('verify your email'),
|
email,
|
||||||
}
|
verify_text: isReturningUser ? __('sign in') : __('verify your email'),
|
||||||
)}
|
}
|
||||||
</p>
|
)}
|
||||||
|
</p>
|
||||||
<div className="section__body section__actions">
|
}
|
||||||
<Button
|
actions={
|
||||||
button="primary"
|
<React.Fragment>
|
||||||
label={__('Resend Email')}
|
<div className="section__actions">
|
||||||
onClick={this.handleResendVerificationEmail}
|
<Button
|
||||||
disabled={resendingEmail}
|
button="primary"
|
||||||
/>
|
label={__('Resend Email')}
|
||||||
<UserSignOutButton label={__('Start Over')} />
|
onClick={this.handleResendVerificationEmail}
|
||||||
</div>
|
disabled={resendingEmail}
|
||||||
|
/>
|
||||||
<p className="help">
|
<UserSignOutButton label={__('Start Over')} />
|
||||||
<I18nMessage
|
</div>
|
||||||
tokens={{
|
<p className="help">
|
||||||
help_link: <Button button="link" href="mailto:help@lbry.com" label="help@lbry.com" />,
|
<I18nMessage
|
||||||
chat_link: <Button button="link" href="https://chat.lbry.com" label="chat" />,
|
tokens={{
|
||||||
}}
|
help_link: <Button button="link" href="mailto:help@lbry.com" label="help@lbry.com" />,
|
||||||
>
|
chat_link: <Button button="link" href="https://chat.lbry.com" label="chat" />,
|
||||||
Email %help_link% or join our %chat_link% if you encounter any trouble verifying.
|
}}
|
||||||
</I18nMessage>
|
>
|
||||||
</p>
|
Email %help_link% or join our %chat_link% if you encounter any trouble verifying.
|
||||||
</React.Fragment>
|
</I18nMessage>
|
||||||
|
</p>
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { isNameValid } from 'lbry-redux';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import { Form, FormField } from 'component/common/form';
|
import { Form, FormField } from 'component/common/form';
|
||||||
import { INVALID_NAME_ERROR } from 'constants/claim';
|
import { INVALID_NAME_ERROR } from 'constants/claim';
|
||||||
|
import Card from 'component/common/card';
|
||||||
export const DEFAULT_BID_FOR_FIRST_CHANNEL = 0.01;
|
export const DEFAULT_BID_FOR_FIRST_CHANNEL = 0.01;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -36,45 +37,51 @@ function UserFirstChannel(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleCreateChannel}>
|
<div className="main__channel-creation">
|
||||||
<h1 className="section__title--large">{__('Create A Channel')}</h1>
|
<Card
|
||||||
<div className="section__subtitle">
|
title={__('Create A Channel')}
|
||||||
<p>{__('A channel is your identity on the LBRY network.')}</p>
|
subtitle={
|
||||||
<p>{__('You can have more than one or remove this later.')}</p>
|
<React.Fragment>
|
||||||
</div>
|
<p>{__('A channel is your identity on the LBRY network.')}</p>
|
||||||
<section className="section__body">
|
<p>{__('You can have more than one or remove this later.')}</p>
|
||||||
<fieldset-group class="fieldset-group--smushed fieldset-group--disabled-prefix">
|
</React.Fragment>
|
||||||
<fieldset-section>
|
}
|
||||||
<label htmlFor="auth_first_channel">
|
actions={
|
||||||
{createChannelError || nameError ? (
|
<Form onSubmit={handleCreateChannel}>
|
||||||
<span className="error-text">{createChannelError || nameError}</span>
|
<fieldset-group class="fieldset-group--smushed fieldset-group--disabled-prefix">
|
||||||
) : (
|
<fieldset-section>
|
||||||
__('Your Channel')
|
<label htmlFor="auth_first_channel">
|
||||||
)}
|
{createChannelError || nameError ? (
|
||||||
</label>
|
<span className="error__text">{createChannelError || nameError}</span>
|
||||||
<div className="form-field__prefix">@</div>
|
) : (
|
||||||
</fieldset-section>
|
__('Your Channel')
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
<div className="form-field__prefix">@</div>
|
||||||
|
</fieldset-section>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
autoFocus
|
autoFocus
|
||||||
placeholder={__('channel')}
|
placeholder={__('channel')}
|
||||||
type="text"
|
type="text"
|
||||||
name="auth_first_channel"
|
name="auth_first_channel"
|
||||||
className="form-field--short"
|
className="form-field--short"
|
||||||
value={channel}
|
value={channel}
|
||||||
onChange={handleChannelChange}
|
onChange={handleChannelChange}
|
||||||
/>
|
/>
|
||||||
</fieldset-group>
|
</fieldset-group>
|
||||||
<div className="section__actions">
|
<div className="section__actions">
|
||||||
<Button
|
<Button
|
||||||
button="primary"
|
button="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={nameError || !channel || creatingChannel || claimingReward}
|
disabled={nameError || !channel || creatingChannel || claimingReward}
|
||||||
label={creatingChannel || claimingReward ? __('Creating') : __('Create')}
|
label={creatingChannel || claimingReward ? __('Creating') : __('Create')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</Form>
|
||||||
</Form>
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
26
ui/component/userPasswordReset/index.js
Normal file
26
ui/component/userPasswordReset/index.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
doUserPasswordReset,
|
||||||
|
selectPasswordResetSuccess,
|
||||||
|
selectPasswordResetIsPending,
|
||||||
|
selectPasswordResetError,
|
||||||
|
doClearPasswordEntry,
|
||||||
|
doClearEmailEntry,
|
||||||
|
selectEmailToVerify,
|
||||||
|
} from 'lbryinc';
|
||||||
|
import { doToast } from 'lbry-redux';
|
||||||
|
import UserSignIn from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
passwordResetSuccess: selectPasswordResetSuccess(state),
|
||||||
|
passwordResetIsPending: selectPasswordResetIsPending(state),
|
||||||
|
passwordResetError: selectPasswordResetError(state),
|
||||||
|
emailToVerify: selectEmailToVerify(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, {
|
||||||
|
doUserPasswordReset,
|
||||||
|
doToast,
|
||||||
|
doClearPasswordEntry,
|
||||||
|
doClearEmailEntry,
|
||||||
|
})(UserSignIn);
|
107
ui/component/userPasswordReset/view.jsx
Normal file
107
ui/component/userPasswordReset/view.jsx
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
// @flow
|
||||||
|
import * as PAGES from 'constants/pages';
|
||||||
|
import React from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import Card from 'component/common/card';
|
||||||
|
import Spinner from 'component/spinner';
|
||||||
|
import { Form, FormField } from 'component/common/form';
|
||||||
|
import { EMAIL_REGEX } from 'constants/email';
|
||||||
|
import ErrorText from 'component/common/error-text';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import Nag from 'component/common/nag';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
user: ?User,
|
||||||
|
doToast: ({ message: string }) => void,
|
||||||
|
doUserPasswordReset: string => void,
|
||||||
|
doClearPasswordEntry: () => void,
|
||||||
|
doClearEmailEntry: () => void,
|
||||||
|
passwordResetPending: boolean,
|
||||||
|
passwordResetSuccess: boolean,
|
||||||
|
passwordResetError: ?string,
|
||||||
|
emailToVerify: ?string,
|
||||||
|
};
|
||||||
|
|
||||||
|
function UserPasswordReset(props: Props) {
|
||||||
|
const {
|
||||||
|
doUserPasswordReset,
|
||||||
|
passwordResetPending,
|
||||||
|
passwordResetError,
|
||||||
|
passwordResetSuccess,
|
||||||
|
doToast,
|
||||||
|
doClearPasswordEntry,
|
||||||
|
doClearEmailEntry,
|
||||||
|
emailToVerify,
|
||||||
|
} = props;
|
||||||
|
const { push } = useHistory();
|
||||||
|
const [email, setEmail] = React.useState(emailToVerify || '');
|
||||||
|
const valid = email.match(EMAIL_REGEX);
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
if (email) {
|
||||||
|
doUserPasswordReset(email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRestart() {
|
||||||
|
setEmail('');
|
||||||
|
doClearPasswordEntry();
|
||||||
|
doClearEmailEntry();
|
||||||
|
push(`/$/${PAGES.AUTH_SIGNIN}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (passwordResetSuccess) {
|
||||||
|
doToast({
|
||||||
|
message: __('Email sent!'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [passwordResetSuccess, doToast]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="main__sign-in">
|
||||||
|
<Card
|
||||||
|
title={__('Reset Your Password')}
|
||||||
|
actions={
|
||||||
|
<div>
|
||||||
|
<Form onSubmit={handleSubmit} className="section">
|
||||||
|
<FormField
|
||||||
|
autoFocus
|
||||||
|
disabled={passwordResetSuccess}
|
||||||
|
placeholder={__('hotstuff_96@hotmail.com')}
|
||||||
|
type="email"
|
||||||
|
name="sign_in_email"
|
||||||
|
id="username"
|
||||||
|
autoComplete="on"
|
||||||
|
label={__('Email')}
|
||||||
|
value={email}
|
||||||
|
onChange={e => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="section__actions">
|
||||||
|
<Button
|
||||||
|
button="primary"
|
||||||
|
type="submit"
|
||||||
|
label={passwordResetPending ? __('Resetting') : __('Reset Password')}
|
||||||
|
disabled={!email || !valid || passwordResetPending || passwordResetSuccess}
|
||||||
|
/>
|
||||||
|
<Button button="link" label={__('Cancel')} onClick={handleRestart} />
|
||||||
|
{passwordResetPending && <Spinner type="small" />}
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
nag={
|
||||||
|
<React.Fragment>
|
||||||
|
{passwordResetError && <Nag type="error" relative message={<ErrorText>{passwordResetError}</ErrorText>} />}
|
||||||
|
{passwordResetSuccess && (
|
||||||
|
<Nag type="helpful" relative message={__('Check your email for a link to reset your password.')} />
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserPasswordReset;
|
16
ui/component/userPasswordSet/index.js
Normal file
16
ui/component/userPasswordSet/index.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { doClearEmailEntry, doUserFetch } from 'lbryinc';
|
||||||
|
import { doToast } from 'lbry-redux';
|
||||||
|
import UserSignIn from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
// passwordSetSuccess: selectPasswordSetSuccess(state),
|
||||||
|
// passwordSetIsPending: selectPasswordSetIsPending(state),
|
||||||
|
// passwordSetError: selectPasswordSetError(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, {
|
||||||
|
doToast,
|
||||||
|
doClearEmailEntry,
|
||||||
|
doUserFetch,
|
||||||
|
})(UserSignIn);
|
108
ui/component/userPasswordSet/view.jsx
Normal file
108
ui/component/userPasswordSet/view.jsx
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
// @flow
|
||||||
|
import * as PAGES from 'constants/pages';
|
||||||
|
import React from 'react';
|
||||||
|
import { Lbryio } from 'lbryinc';
|
||||||
|
import { useHistory } from 'react-router';
|
||||||
|
import Card from 'component/common/card';
|
||||||
|
import { Form, FormField } from 'component/common/form';
|
||||||
|
import ErrorText from 'component/common/error-text';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import Nag from 'component/common/nag';
|
||||||
|
import Spinner from 'component/spinner';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
user: ?User,
|
||||||
|
doClearEmailEntry: () => void,
|
||||||
|
doUserFetch: () => void,
|
||||||
|
doToast: ({ message: string }) => void,
|
||||||
|
history: { push: string => void },
|
||||||
|
location: { search: string },
|
||||||
|
passwordSetPending: boolean,
|
||||||
|
passwordSetError: ?string,
|
||||||
|
};
|
||||||
|
|
||||||
|
function UserPasswordReset(props: Props) {
|
||||||
|
const { doClearEmailEntry, doToast, doUserFetch } = props;
|
||||||
|
const { location, push } = useHistory();
|
||||||
|
const urlParams = new URLSearchParams(location.search);
|
||||||
|
const email = urlParams.get('email');
|
||||||
|
const authToken = urlParams.get('auth_token');
|
||||||
|
const verificationToken = urlParams.get('verification_token');
|
||||||
|
const [password, setPassword] = React.useState('');
|
||||||
|
const [error, setError] = React.useState();
|
||||||
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
Lbryio.call('user_email', 'confirm', {
|
||||||
|
email: email,
|
||||||
|
verification_token: verificationToken,
|
||||||
|
})
|
||||||
|
.then(() =>
|
||||||
|
Lbryio.call(
|
||||||
|
'user_password',
|
||||||
|
'set',
|
||||||
|
{
|
||||||
|
auth_token: authToken,
|
||||||
|
new_password: password,
|
||||||
|
},
|
||||||
|
'post'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then(doUserFetch)
|
||||||
|
.then(() => {
|
||||||
|
setLoading(false);
|
||||||
|
doToast({
|
||||||
|
message: __('Password successfully changed!'),
|
||||||
|
});
|
||||||
|
push(`/`);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
setLoading(false);
|
||||||
|
setError(error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRestart() {
|
||||||
|
doClearEmailEntry();
|
||||||
|
push(`/$/${PAGES.AUTH_SIGNIN}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="main__sign-in">
|
||||||
|
<Card
|
||||||
|
title={__('Choose A New Password')}
|
||||||
|
subtitle={__('Setting a new password for %email%', { email })}
|
||||||
|
actions={
|
||||||
|
<div>
|
||||||
|
<Form onSubmit={handleSubmit} className="section">
|
||||||
|
<FormField
|
||||||
|
autoFocus
|
||||||
|
type="password"
|
||||||
|
name="password_set"
|
||||||
|
label={__('New Password')}
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="section__actions">
|
||||||
|
<Button
|
||||||
|
button="primary"
|
||||||
|
type="submit"
|
||||||
|
label={loading ? __('Update Password') : __('Updating Password')}
|
||||||
|
disabled={!password || loading}
|
||||||
|
/>
|
||||||
|
<Button button="link" label={__('Cancel')} onClick={handleRestart} />
|
||||||
|
{loading && <Spinner type="small" />}
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
nag={error && <Nag type="error" relative message={<ErrorText>{error}</ErrorText>} />}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserPasswordReset;
|
|
@ -1,55 +1,14 @@
|
||||||
import * as SETTINGS from 'constants/settings';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import {
|
import { selectUser, selectUserIsPending, selectEmailToVerify, selectPasswordExists, doUserSignIn } from 'lbryinc';
|
||||||
selectEmailToVerify,
|
|
||||||
selectUser,
|
|
||||||
selectAccessToken,
|
|
||||||
makeSelectIsRewardClaimPending,
|
|
||||||
selectClaimedRewards,
|
|
||||||
rewards as REWARD_TYPES,
|
|
||||||
doClaimRewardType,
|
|
||||||
doUserFetch,
|
|
||||||
selectUserIsPending,
|
|
||||||
selectYoutubeChannels,
|
|
||||||
selectGetSyncIsPending,
|
|
||||||
selectGetSyncErrorMessage,
|
|
||||||
selectSyncHash,
|
|
||||||
} from 'lbryinc';
|
|
||||||
import { selectMyChannelClaims, selectBalance, selectFetchingMyChannels, selectCreatingChannel } from 'lbry-redux';
|
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
|
||||||
import UserSignIn from './view';
|
import UserSignIn from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = state => ({
|
||||||
emailToVerify: selectEmailToVerify(state),
|
|
||||||
user: selectUser(state),
|
user: selectUser(state),
|
||||||
accessToken: selectAccessToken(state),
|
|
||||||
channels: selectMyChannelClaims(state),
|
|
||||||
claimedRewards: selectClaimedRewards(state),
|
|
||||||
claimingReward: makeSelectIsRewardClaimPending()(state, {
|
|
||||||
reward_type: REWARD_TYPES.TYPE_CONFIRM_EMAIL,
|
|
||||||
}),
|
|
||||||
balance: selectBalance(state),
|
|
||||||
fetchingChannels: selectFetchingMyChannels(state),
|
|
||||||
youtubeChannels: selectYoutubeChannels(state),
|
|
||||||
userFetchPending: selectUserIsPending(state),
|
userFetchPending: selectUserIsPending(state),
|
||||||
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
|
emailToVerify: selectEmailToVerify(state),
|
||||||
syncingWallet: selectGetSyncIsPending(state),
|
passwordExists: selectPasswordExists(state),
|
||||||
getSyncError: selectGetSyncErrorMessage(state),
|
|
||||||
hasSynced: Boolean(selectSyncHash(state)),
|
|
||||||
creatingChannel: selectCreatingChannel(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
export default connect(select, {
|
||||||
fetchUser: () => dispatch(doUserFetch()),
|
doUserSignIn,
|
||||||
claimReward: () =>
|
})(UserSignIn);
|
||||||
dispatch(
|
|
||||||
doClaimRewardType(REWARD_TYPES.TYPE_CONFIRM_EMAIL, {
|
|
||||||
notifyError: false,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
select,
|
|
||||||
perform
|
|
||||||
)(UserSignIn);
|
|
||||||
|
|
|
@ -1,215 +1,58 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as PAGES from 'constants/pages';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import UserEmailNew from 'component/userEmailNew';
|
import UserEmailReturning from 'component/userEmailReturning';
|
||||||
import UserEmailVerify from 'component/userEmailVerify';
|
import UserSignInPassword from 'component/userSignInPassword';
|
||||||
import UserFirstChannel from 'component/userFirstChannel';
|
|
||||||
import UserChannelFollowIntro from 'component/userChannelFollowIntro';
|
|
||||||
import UserTagFollowIntro from 'component/userTagFollowIntro';
|
|
||||||
import { DEFAULT_BID_FOR_FIRST_CHANNEL } from 'component/userFirstChannel/view';
|
|
||||||
import { rewards as REWARDS, YOUTUBE_STATUSES } from 'lbryinc';
|
|
||||||
import UserVerify from 'component/userVerify';
|
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
import YoutubeTransferStatus from 'component/youtubeTransferStatus';
|
|
||||||
import SyncPassword from 'component/syncPassword';
|
|
||||||
import useFetched from 'effects/use-fetched';
|
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
|
||||||
import Confetti from 'react-confetti';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
user: ?User,
|
user: ?User,
|
||||||
emailToVerify: ?string,
|
history: { push: string => void },
|
||||||
channels: ?Array<string>,
|
|
||||||
balance: ?number,
|
|
||||||
fetchingChannels: boolean,
|
|
||||||
claimingReward: boolean,
|
|
||||||
claimReward: () => void,
|
|
||||||
fetchUser: () => void,
|
|
||||||
claimedRewards: Array<Reward>,
|
|
||||||
history: { replace: string => void },
|
|
||||||
location: { search: string },
|
location: { search: string },
|
||||||
youtubeChannels: Array<any>,
|
userFetchPending: boolean,
|
||||||
syncEnabled: boolean,
|
doUserSignIn: string => void,
|
||||||
hasSynced: boolean,
|
emailToVerify: ?string,
|
||||||
syncingWallet: boolean,
|
passwordExists: boolean,
|
||||||
getSyncError: ?string,
|
|
||||||
creatingChannel: boolean,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function UserSignIn(props: Props) {
|
function UserSignIn(props: Props) {
|
||||||
const {
|
const { user, location, history, doUserSignIn, userFetchPending, emailToVerify, passwordExists } = props;
|
||||||
emailToVerify,
|
|
||||||
user,
|
|
||||||
claimingReward,
|
|
||||||
claimedRewards,
|
|
||||||
channels,
|
|
||||||
claimReward,
|
|
||||||
balance,
|
|
||||||
history,
|
|
||||||
location,
|
|
||||||
fetchUser,
|
|
||||||
youtubeChannels,
|
|
||||||
syncEnabled,
|
|
||||||
syncingWallet,
|
|
||||||
getSyncError,
|
|
||||||
hasSynced,
|
|
||||||
fetchingChannels,
|
|
||||||
creatingChannel,
|
|
||||||
} = props;
|
|
||||||
const { search } = location;
|
const { search } = location;
|
||||||
const urlParams = new URLSearchParams(search);
|
const urlParams = new URLSearchParams(search);
|
||||||
const redirect = urlParams.get('redirect');
|
const [emailOnlyLogin, setEmailOnlyLogin] = React.useState(false);
|
||||||
const step = urlParams.get('step');
|
|
||||||
const shouldRedirectImmediately = urlParams.get('immediate');
|
|
||||||
const [initialSignInStep, setInitialSignInStep] = React.useState();
|
|
||||||
const [hasSeenFollowList, setHasSeenFollowList] = usePersistedState('channel-follow-intro', false);
|
|
||||||
const [hasSkippedRewards, setHasSkippedRewards] = usePersistedState('skip-rewards-intro', false);
|
|
||||||
const [hasSeenTagsList, setHasSeenTagsList] = usePersistedState('channel-follow-intro', false);
|
|
||||||
const hasVerifiedEmail = user && user.has_verified_email;
|
const hasVerifiedEmail = user && user.has_verified_email;
|
||||||
const rewardsApproved = user && user.is_reward_approved;
|
const redirect = urlParams.get('redirect');
|
||||||
const isIdentityVerified = user && user.is_identity_verified;
|
const showLoading = userFetchPending;
|
||||||
const hasFetchedReward = useFetched(claimingReward);
|
const showEmail = !passwordExists || emailOnlyLogin;
|
||||||
const channelCount = channels ? channels.length : 0;
|
const showPassword = !showEmail && emailToVerify && passwordExists;
|
||||||
const hasClaimedEmailAward = claimedRewards.some(reward => reward.reward_type === REWARDS.TYPE_CONFIRM_EMAIL);
|
|
||||||
const hasYoutubeChannels = youtubeChannels && Boolean(youtubeChannels.length);
|
|
||||||
const isYoutubeTransferComplete =
|
|
||||||
hasYoutubeChannels &&
|
|
||||||
youtubeChannels.every(channel => channel.transfer_state === YOUTUBE_STATUSES.COMPLETED_TRANSFER);
|
|
||||||
|
|
||||||
// Complexity warning
|
|
||||||
// We can't just check if we are currently fetching something
|
|
||||||
// We may want to keep a component rendered while something is being fetched, instead of replacing it with the large spinner
|
|
||||||
// The verbose variable names are an attempt to alleviate _some_ of the confusion from handling all edge cases that come from
|
|
||||||
// reward claiming, channel creation, account syncing, and youtube transfer
|
|
||||||
// The possible screens for the sign in flow
|
|
||||||
const showEmail = !emailToVerify && !hasVerifiedEmail;
|
|
||||||
const showEmailVerification = emailToVerify && !hasVerifiedEmail;
|
|
||||||
const showUserVerification = hasVerifiedEmail && !rewardsApproved && !isIdentityVerified && !hasSkippedRewards;
|
|
||||||
const showSyncPassword = syncEnabled && getSyncError;
|
|
||||||
const showChannelCreation =
|
|
||||||
hasVerifiedEmail &&
|
|
||||||
balance !== undefined &&
|
|
||||||
balance !== null &&
|
|
||||||
balance > DEFAULT_BID_FOR_FIRST_CHANNEL &&
|
|
||||||
channelCount === 0 &&
|
|
||||||
!hasYoutubeChannels;
|
|
||||||
const showYoutubeTransfer = hasVerifiedEmail && hasYoutubeChannels && !isYoutubeTransferComplete;
|
|
||||||
const showFollowIntro = step === 'channels' || (hasVerifiedEmail && !hasSeenFollowList);
|
|
||||||
const showTagsIntro = step === 'tags' || (hasVerifiedEmail && !hasSeenTagsList);
|
|
||||||
const canHijackSignInFlowWithSpinner = hasVerifiedEmail && !getSyncError && !showFollowIntro;
|
|
||||||
const isCurrentlyFetchingSomething = fetchingChannels || claimingReward || syncingWallet || creatingChannel;
|
|
||||||
const isWaitingForSomethingToFinish =
|
|
||||||
// If the user has claimed the email award, we need to wait until the balance updates sometime in the future
|
|
||||||
(!hasFetchedReward && !hasClaimedEmailAward) || (syncEnabled && !hasSynced);
|
|
||||||
const showLoadingSpinner =
|
|
||||||
canHijackSignInFlowWithSpinner && (isCurrentlyFetchingSomething || isWaitingForSomethingToFinish);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
fetchUser();
|
if (hasVerifiedEmail || (!showEmail && !showPassword && !showLoading)) {
|
||||||
}, [fetchUser]);
|
history.push(redirect || '/');
|
||||||
|
}
|
||||||
|
}, [showEmail, showPassword, showLoading, hasVerifiedEmail]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// Don't claim the reward if sync is enabled until after a sync has been completed successfully
|
if (emailToVerify && emailOnlyLogin) {
|
||||||
// If we do it before, we could end up trying to sync a wallet with a non-zero balance which will fail to sync
|
doUserSignIn(emailToVerify);
|
||||||
const delayForSync = syncEnabled && !hasSynced;
|
|
||||||
|
|
||||||
if (hasVerifiedEmail && !hasClaimedEmailAward && !hasFetchedReward && !delayForSync) {
|
|
||||||
claimReward();
|
|
||||||
}
|
}
|
||||||
}, [hasVerifiedEmail, claimReward, hasClaimedEmailAward, hasFetchedReward, syncEnabled, hasSynced, balance]);
|
}, [emailToVerify, emailOnlyLogin, doUserSignIn]);
|
||||||
|
|
||||||
// Loop through this list from the end, until it finds a matching component
|
return (
|
||||||
// If it never finds one, assume the user has completed every step and redirect them
|
<section>
|
||||||
const SIGN_IN_FLOW = [
|
{(showEmail || showPassword) && (
|
||||||
showEmail && <UserEmailNew />,
|
<div>
|
||||||
showEmailVerification && <UserEmailVerify />,
|
{showEmail && <UserEmailReturning />}
|
||||||
showUserVerification && <UserVerify onSkip={() => setHasSkippedRewards(true)} />,
|
{showPassword && <UserSignInPassword onHandleEmailOnly={() => setEmailOnlyLogin(true)} />}
|
||||||
showChannelCreation && <UserFirstChannel />,
|
</div>
|
||||||
showFollowIntro && (
|
)}
|
||||||
<UserChannelFollowIntro
|
{!showEmail && !showPassword && showLoading && (
|
||||||
onContinue={() => {
|
<div className="main--empty">
|
||||||
let url = `/$/${PAGES.AUTH}?reset_scroll=1`;
|
<Spinner delayed />
|
||||||
if (redirect) {
|
</div>
|
||||||
url += `&redirect=${redirect}`;
|
)}
|
||||||
}
|
</section>
|
||||||
if (shouldRedirectImmediately) {
|
);
|
||||||
url += `&immediate=true`;
|
|
||||||
}
|
|
||||||
|
|
||||||
history.replace(url);
|
|
||||||
setHasSeenFollowList(true);
|
|
||||||
}}
|
|
||||||
onBack={() => {
|
|
||||||
let url = `/$/${PAGES.AUTH}?reset_scroll=1&step=tags`;
|
|
||||||
if (redirect) {
|
|
||||||
url += `&redirect=${redirect}`;
|
|
||||||
}
|
|
||||||
if (shouldRedirectImmediately) {
|
|
||||||
url += `&immediate=true`;
|
|
||||||
}
|
|
||||||
|
|
||||||
history.replace(url);
|
|
||||||
setHasSeenFollowList(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
showTagsIntro && (
|
|
||||||
<UserTagFollowIntro
|
|
||||||
onContinue={() => {
|
|
||||||
let url = `/$/${PAGES.AUTH}?reset_scroll=1&step=channels`;
|
|
||||||
if (redirect) {
|
|
||||||
url += `&redirect=${redirect}`;
|
|
||||||
}
|
|
||||||
if (shouldRedirectImmediately) {
|
|
||||||
url += `&immediate=true`;
|
|
||||||
}
|
|
||||||
|
|
||||||
history.replace(url);
|
|
||||||
setHasSeenTagsList(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
showYoutubeTransfer && (
|
|
||||||
<div>
|
|
||||||
<YoutubeTransferStatus /> <Confetti recycle={false} style={{ position: 'fixed' }} />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
showSyncPassword && <SyncPassword />,
|
|
||||||
showLoadingSpinner && (
|
|
||||||
<div className="main--empty">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
function getSignInStep() {
|
|
||||||
for (var i = SIGN_IN_FLOW.length - 1; i > -1; i--) {
|
|
||||||
const Component = SIGN_IN_FLOW[i];
|
|
||||||
if (Component) {
|
|
||||||
// If we want to redirect immediately,
|
|
||||||
// remember the first step so we can redirect once a new step has been reached
|
|
||||||
// Ignore the loading step
|
|
||||||
if (redirect && shouldRedirectImmediately) {
|
|
||||||
if (!initialSignInStep) {
|
|
||||||
setInitialSignInStep(i);
|
|
||||||
} else if (i !== initialSignInStep && i !== SIGN_IN_FLOW.length - 1) {
|
|
||||||
history.replace(redirect);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Component;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const componentToRender = getSignInStep();
|
|
||||||
|
|
||||||
if (!componentToRender) {
|
|
||||||
history.replace(redirect || '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
return <section className="main--contained">{componentToRender}</section>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(UserSignIn);
|
export default withRouter(UserSignIn);
|
||||||
|
|
22
ui/component/userSignInPassword/index.js
Normal file
22
ui/component/userSignInPassword/index.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
selectUser,
|
||||||
|
selectUserIsPending,
|
||||||
|
selectEmailToVerify,
|
||||||
|
selectEmailNewErrorMessage,
|
||||||
|
doUserSignIn,
|
||||||
|
doClearEmailEntry,
|
||||||
|
} from 'lbryinc';
|
||||||
|
import UserSignIn from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
user: selectUser(state),
|
||||||
|
userFetchPending: selectUserIsPending(state),
|
||||||
|
emailToVerify: selectEmailToVerify(state),
|
||||||
|
errorMessage: selectEmailNewErrorMessage(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, {
|
||||||
|
doUserSignIn,
|
||||||
|
doClearEmailEntry,
|
||||||
|
})(UserSignIn);
|
67
ui/component/userSignInPassword/view.jsx
Normal file
67
ui/component/userSignInPassword/view.jsx
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
// @flow
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { FormField, Form } from 'component/common/form';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import Card from 'component/common/card';
|
||||||
|
import analytics from 'analytics';
|
||||||
|
import Nag from 'component/common/nag';
|
||||||
|
import UserPasswordReset from 'component/userPasswordReset';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
errorMessage: ?string,
|
||||||
|
emailToVerify: ?string,
|
||||||
|
doClearEmailEntry: () => void,
|
||||||
|
doUserSignIn: (string, ?string) => void,
|
||||||
|
onHandleEmailOnly: () => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function UserSignInPassword(props: Props) {
|
||||||
|
const { errorMessage, doUserSignIn, emailToVerify, onHandleEmailOnly } = props;
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [forgotPassword, setForgotPassword] = React.useState(false);
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
if (emailToVerify) {
|
||||||
|
doUserSignIn(emailToVerify, password);
|
||||||
|
analytics.emailProvidedEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChangeToSignIn() {
|
||||||
|
onHandleEmailOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="main__sign-in">
|
||||||
|
{forgotPassword ? (
|
||||||
|
<UserPasswordReset />
|
||||||
|
) : (
|
||||||
|
<Card
|
||||||
|
title={__('Enter Your lbry.tv Password')}
|
||||||
|
subtitle={__('Signing in as %email%', { email: emailToVerify })}
|
||||||
|
actions={
|
||||||
|
<Form onSubmit={handleSubmit} className="section">
|
||||||
|
<FormField
|
||||||
|
autoFocus
|
||||||
|
type="password"
|
||||||
|
name="sign_in_password"
|
||||||
|
id="password"
|
||||||
|
autoComplete="on"
|
||||||
|
label={__('Password')}
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
helper={<Button button="link" label={__('Forgot Password?')} onClick={() => setForgotPassword(true)} />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="section__actions">
|
||||||
|
<Button button="primary" type="submit" label={__('Continue')} disabled={!password} />
|
||||||
|
<Button button="link" onClick={handleChangeToSignIn} label={__('Use Magic Link')} />
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
}
|
||||||
|
nag={errorMessage && <Nag type="error" relative message={errorMessage} />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,14 +1,12 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doSignOut } from 'redux/actions/app';
|
import { doSignOut } from 'redux/actions/app';
|
||||||
|
import { doClearEmailEntry, doClearPasswordEntry } from 'lbryinc';
|
||||||
import UserSignOutButton from './view';
|
import UserSignOutButton from './view';
|
||||||
|
|
||||||
const select = state => ({});
|
const select = state => ({});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
export default connect(select, {
|
||||||
signOut: () => dispatch(doSignOut()),
|
doSignOut,
|
||||||
});
|
doClearEmailEntry,
|
||||||
|
doClearPasswordEntry,
|
||||||
export default connect(
|
})(UserSignOutButton);
|
||||||
select,
|
|
||||||
perform
|
|
||||||
)(UserSignOutButton);
|
|
||||||
|
|
|
@ -5,13 +5,25 @@ import Button from 'component/button';
|
||||||
type Props = {
|
type Props = {
|
||||||
button: string,
|
button: string,
|
||||||
label?: string,
|
label?: string,
|
||||||
signOut: () => void,
|
doSignOut: () => void,
|
||||||
|
doClearEmailEntry: () => void,
|
||||||
|
doClearPasswordEntry: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function UserSignOutButton(props: Props) {
|
function UserSignOutButton(props: Props) {
|
||||||
const { button = 'link', signOut, label } = props;
|
const { button = 'link', doSignOut, doClearEmailEntry, doClearPasswordEntry, label } = props;
|
||||||
|
|
||||||
return <Button button={button} label={label || __('Sign Out')} onClick={signOut} />;
|
return (
|
||||||
|
<Button
|
||||||
|
button={button}
|
||||||
|
label={label || __('Sign Out')}
|
||||||
|
onClick={() => {
|
||||||
|
doClearPasswordEntry();
|
||||||
|
doClearEmailEntry();
|
||||||
|
doSignOut();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UserSignOutButton;
|
export default UserSignOutButton;
|
||||||
|
|
52
ui/component/userSignUp/index.js
Normal file
52
ui/component/userSignUp/index.js
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import * as SETTINGS from 'constants/settings';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
selectEmailToVerify,
|
||||||
|
selectUser,
|
||||||
|
selectAccessToken,
|
||||||
|
makeSelectIsRewardClaimPending,
|
||||||
|
selectClaimedRewards,
|
||||||
|
rewards as REWARD_TYPES,
|
||||||
|
doClaimRewardType,
|
||||||
|
doUserFetch,
|
||||||
|
selectUserIsPending,
|
||||||
|
selectYoutubeChannels,
|
||||||
|
selectGetSyncIsPending,
|
||||||
|
selectGetSyncErrorMessage,
|
||||||
|
selectSyncHash,
|
||||||
|
} from 'lbryinc';
|
||||||
|
import { selectMyChannelClaims, selectBalance, selectFetchingMyChannels, selectCreatingChannel } from 'lbry-redux';
|
||||||
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
|
import UserSignIn from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
emailToVerify: selectEmailToVerify(state),
|
||||||
|
user: selectUser(state),
|
||||||
|
accessToken: selectAccessToken(state),
|
||||||
|
channels: selectMyChannelClaims(state),
|
||||||
|
claimedRewards: selectClaimedRewards(state),
|
||||||
|
claimingReward: makeSelectIsRewardClaimPending()(state, {
|
||||||
|
reward_type: REWARD_TYPES.TYPE_CONFIRM_EMAIL,
|
||||||
|
}),
|
||||||
|
balance: selectBalance(state),
|
||||||
|
fetchingChannels: selectFetchingMyChannels(state),
|
||||||
|
youtubeChannels: selectYoutubeChannels(state),
|
||||||
|
userFetchPending: selectUserIsPending(state),
|
||||||
|
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
|
||||||
|
syncingWallet: selectGetSyncIsPending(state),
|
||||||
|
getSyncError: selectGetSyncErrorMessage(state),
|
||||||
|
hasSynced: Boolean(selectSyncHash(state)),
|
||||||
|
creatingChannel: selectCreatingChannel(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
fetchUser: () => dispatch(doUserFetch()),
|
||||||
|
claimReward: () =>
|
||||||
|
dispatch(
|
||||||
|
doClaimRewardType(REWARD_TYPES.TYPE_CONFIRM_EMAIL, {
|
||||||
|
notifyError: false,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, perform)(UserSignIn);
|
215
ui/component/userSignUp/view.jsx
Normal file
215
ui/component/userSignUp/view.jsx
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
// @flow
|
||||||
|
import * as PAGES from 'constants/pages';
|
||||||
|
import React from 'react';
|
||||||
|
import { withRouter } from 'react-router';
|
||||||
|
import UserEmailNew from 'component/userEmailNew';
|
||||||
|
import UserEmailVerify from 'component/userEmailVerify';
|
||||||
|
import UserFirstChannel from 'component/userFirstChannel';
|
||||||
|
import UserChannelFollowIntro from 'component/userChannelFollowIntro';
|
||||||
|
import UserTagFollowIntro from 'component/userTagFollowIntro';
|
||||||
|
import { DEFAULT_BID_FOR_FIRST_CHANNEL } from 'component/userFirstChannel/view';
|
||||||
|
import { rewards as REWARDS, YOUTUBE_STATUSES } from 'lbryinc';
|
||||||
|
import UserVerify from 'component/userVerify';
|
||||||
|
import Spinner from 'component/spinner';
|
||||||
|
import YoutubeTransferStatus from 'component/youtubeTransferStatus';
|
||||||
|
import SyncPassword from 'component/syncPassword';
|
||||||
|
import useFetched from 'effects/use-fetched';
|
||||||
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
|
import Confetti from 'react-confetti';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
user: ?User,
|
||||||
|
emailToVerify: ?string,
|
||||||
|
channels: ?Array<string>,
|
||||||
|
balance: ?number,
|
||||||
|
fetchingChannels: boolean,
|
||||||
|
claimingReward: boolean,
|
||||||
|
claimReward: () => void,
|
||||||
|
fetchUser: () => void,
|
||||||
|
claimedRewards: Array<Reward>,
|
||||||
|
history: { replace: string => void },
|
||||||
|
location: { search: string },
|
||||||
|
youtubeChannels: Array<any>,
|
||||||
|
syncEnabled: boolean,
|
||||||
|
hasSynced: boolean,
|
||||||
|
syncingWallet: boolean,
|
||||||
|
getSyncError: ?string,
|
||||||
|
creatingChannel: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
function UserSignIn(props: Props) {
|
||||||
|
const {
|
||||||
|
emailToVerify,
|
||||||
|
user,
|
||||||
|
claimingReward,
|
||||||
|
claimedRewards,
|
||||||
|
channels,
|
||||||
|
claimReward,
|
||||||
|
balance,
|
||||||
|
history,
|
||||||
|
location,
|
||||||
|
fetchUser,
|
||||||
|
youtubeChannels,
|
||||||
|
syncEnabled,
|
||||||
|
syncingWallet,
|
||||||
|
getSyncError,
|
||||||
|
hasSynced,
|
||||||
|
fetchingChannels,
|
||||||
|
creatingChannel,
|
||||||
|
} = props;
|
||||||
|
const { search } = location;
|
||||||
|
const urlParams = new URLSearchParams(search);
|
||||||
|
const redirect = urlParams.get('redirect');
|
||||||
|
const step = urlParams.get('step');
|
||||||
|
const shouldRedirectImmediately = urlParams.get('immediate');
|
||||||
|
const [initialSignInStep, setInitialSignInStep] = React.useState();
|
||||||
|
const [hasSeenFollowList, setHasSeenFollowList] = usePersistedState('channel-follow-intro', false);
|
||||||
|
const [hasSkippedRewards, setHasSkippedRewards] = usePersistedState('skip-rewards-intro', false);
|
||||||
|
const [hasSeenTagsList, setHasSeenTagsList] = usePersistedState('channel-follow-intro', false);
|
||||||
|
const hasVerifiedEmail = user && user.has_verified_email;
|
||||||
|
const rewardsApproved = user && user.is_reward_approved;
|
||||||
|
const isIdentityVerified = user && user.is_identity_verified;
|
||||||
|
const hasFetchedReward = useFetched(claimingReward);
|
||||||
|
const channelCount = channels ? channels.length : 0;
|
||||||
|
const hasClaimedEmailAward = claimedRewards.some(reward => reward.reward_type === REWARDS.TYPE_CONFIRM_EMAIL);
|
||||||
|
const hasYoutubeChannels = youtubeChannels && Boolean(youtubeChannels.length);
|
||||||
|
const isYoutubeTransferComplete =
|
||||||
|
hasYoutubeChannels &&
|
||||||
|
youtubeChannels.every(channel => channel.transfer_state === YOUTUBE_STATUSES.COMPLETED_TRANSFER);
|
||||||
|
|
||||||
|
// Complexity warning
|
||||||
|
// We can't just check if we are currently fetching something
|
||||||
|
// We may want to keep a component rendered while something is being fetched, instead of replacing it with the large spinner
|
||||||
|
// The verbose variable names are an attempt to alleviate _some_ of the confusion from handling all edge cases that come from
|
||||||
|
// reward claiming, channel creation, account syncing, and youtube transfer
|
||||||
|
// The possible screens for the sign in flow
|
||||||
|
const showEmail = !hasVerifiedEmail;
|
||||||
|
const showEmailVerification = emailToVerify && !hasVerifiedEmail;
|
||||||
|
const showUserVerification = hasVerifiedEmail && !rewardsApproved && !isIdentityVerified && !hasSkippedRewards;
|
||||||
|
const showSyncPassword = syncEnabled && getSyncError;
|
||||||
|
const showChannelCreation =
|
||||||
|
hasVerifiedEmail &&
|
||||||
|
balance !== undefined &&
|
||||||
|
balance !== null &&
|
||||||
|
balance > DEFAULT_BID_FOR_FIRST_CHANNEL &&
|
||||||
|
channelCount === 0 &&
|
||||||
|
!hasYoutubeChannels;
|
||||||
|
const showYoutubeTransfer = hasVerifiedEmail && hasYoutubeChannels && !isYoutubeTransferComplete;
|
||||||
|
const showFollowIntro = step === 'channels' || (hasVerifiedEmail && !hasSeenFollowList);
|
||||||
|
const showTagsIntro = step === 'tags' || (hasVerifiedEmail && !hasSeenTagsList);
|
||||||
|
const canHijackSignInFlowWithSpinner = hasVerifiedEmail && !getSyncError && !showFollowIntro;
|
||||||
|
const isCurrentlyFetchingSomething = fetchingChannels || claimingReward || syncingWallet || creatingChannel;
|
||||||
|
const isWaitingForSomethingToFinish =
|
||||||
|
// If the user has claimed the email award, we need to wait until the balance updates sometime in the future
|
||||||
|
(!hasFetchedReward && !hasClaimedEmailAward) || (syncEnabled && !hasSynced);
|
||||||
|
const showLoadingSpinner =
|
||||||
|
canHijackSignInFlowWithSpinner && (isCurrentlyFetchingSomething || isWaitingForSomethingToFinish);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
fetchUser();
|
||||||
|
}, [fetchUser]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// Don't claim the reward if sync is enabled until after a sync has been completed successfully
|
||||||
|
// If we do it before, we could end up trying to sync a wallet with a non-zero balance which will fail to sync
|
||||||
|
const delayForSync = syncEnabled && !hasSynced;
|
||||||
|
|
||||||
|
if (hasVerifiedEmail && !hasClaimedEmailAward && !hasFetchedReward && !delayForSync) {
|
||||||
|
claimReward();
|
||||||
|
}
|
||||||
|
}, [hasVerifiedEmail, claimReward, hasClaimedEmailAward, hasFetchedReward, syncEnabled, hasSynced, balance]);
|
||||||
|
|
||||||
|
// Loop through this list from the end, until it finds a matching component
|
||||||
|
// If it never finds one, assume the user has completed every step and redirect them
|
||||||
|
const SIGN_IN_FLOW = [
|
||||||
|
showEmail && <UserEmailNew />,
|
||||||
|
showEmailVerification && <UserEmailVerify />,
|
||||||
|
showUserVerification && <UserVerify onSkip={() => setHasSkippedRewards(true)} />,
|
||||||
|
showChannelCreation && <UserFirstChannel />,
|
||||||
|
showFollowIntro && (
|
||||||
|
<UserChannelFollowIntro
|
||||||
|
onContinue={() => {
|
||||||
|
let url = `/$/${PAGES.AUTH}?reset_scroll=1`;
|
||||||
|
if (redirect) {
|
||||||
|
url += `&redirect=${redirect}`;
|
||||||
|
}
|
||||||
|
if (shouldRedirectImmediately) {
|
||||||
|
url += `&immediate=true`;
|
||||||
|
}
|
||||||
|
|
||||||
|
history.replace(url);
|
||||||
|
setHasSeenFollowList(true);
|
||||||
|
}}
|
||||||
|
onBack={() => {
|
||||||
|
let url = `/$/${PAGES.AUTH}?reset_scroll=1&step=tags`;
|
||||||
|
if (redirect) {
|
||||||
|
url += `&redirect=${redirect}`;
|
||||||
|
}
|
||||||
|
if (shouldRedirectImmediately) {
|
||||||
|
url += `&immediate=true`;
|
||||||
|
}
|
||||||
|
|
||||||
|
history.replace(url);
|
||||||
|
setHasSeenFollowList(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
showTagsIntro && (
|
||||||
|
<UserTagFollowIntro
|
||||||
|
onContinue={() => {
|
||||||
|
let url = `/$/${PAGES.AUTH}?reset_scroll=1&step=channels`;
|
||||||
|
if (redirect) {
|
||||||
|
url += `&redirect=${redirect}`;
|
||||||
|
}
|
||||||
|
if (shouldRedirectImmediately) {
|
||||||
|
url += `&immediate=true`;
|
||||||
|
}
|
||||||
|
|
||||||
|
history.replace(url);
|
||||||
|
setHasSeenTagsList(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
showYoutubeTransfer && (
|
||||||
|
<div>
|
||||||
|
<YoutubeTransferStatus /> <Confetti recycle={false} style={{ position: 'fixed' }} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
showSyncPassword && <SyncPassword />,
|
||||||
|
showLoadingSpinner && (
|
||||||
|
<div className="main--empty">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
function getSignInStep() {
|
||||||
|
for (var i = SIGN_IN_FLOW.length - 1; i > -1; i--) {
|
||||||
|
const Component = SIGN_IN_FLOW[i];
|
||||||
|
if (Component) {
|
||||||
|
// If we want to redirect immediately,
|
||||||
|
// remember the first step so we can redirect once a new step has been reached
|
||||||
|
// Ignore the loading step
|
||||||
|
if (redirect && shouldRedirectImmediately) {
|
||||||
|
if (!initialSignInStep) {
|
||||||
|
setInitialSignInStep(i);
|
||||||
|
} else if (i !== initialSignInStep && i !== SIGN_IN_FLOW.length - 1) {
|
||||||
|
history.replace(redirect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Component;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentToRender = getSignInStep();
|
||||||
|
|
||||||
|
if (!componentToRender) {
|
||||||
|
history.replace(redirect || '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return <section className="main--contained">{componentToRender}</section>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withRouter(UserSignIn);
|
|
@ -4,6 +4,7 @@ import Nag from 'component/common/nag';
|
||||||
import TagsSelect from 'component/tagsSelect';
|
import TagsSelect from 'component/tagsSelect';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import { Form } from 'component/common/form';
|
import { Form } from 'component/common/form';
|
||||||
|
import Card from 'component/common/card';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
subscribedChannels: Array<Subscription>,
|
subscribedChannels: Array<Subscription>,
|
||||||
|
@ -16,36 +17,40 @@ function UserChannelFollowIntro(props: Props) {
|
||||||
const followingCount = (followedTags && followedTags.length) || 0;
|
const followingCount = (followedTags && followedTags.length) || 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<Card
|
||||||
<h1 className="section__title--large">{__('Tag Selection')}</h1>
|
title={__('Tag Selection')}
|
||||||
<p className="section__subtitle">{__('Select some tags to help us show you interesting things.')}</p>
|
subtitle={__('Select some tags to help us show you interesting things.')}
|
||||||
<Form onSubmit={onContinue} className="section__body">
|
actions={
|
||||||
<div className="card__actions">
|
<React.Fragment>
|
||||||
<Button
|
<Form onSubmit={onContinue}>
|
||||||
button="primary"
|
<div className="section__actions">
|
||||||
type="Submit"
|
<Button
|
||||||
onClick={onContinue}
|
button="primary"
|
||||||
label={__('Continue')}
|
type="Submit"
|
||||||
disabled={followedTags.length < 1}
|
onClick={onContinue}
|
||||||
/>
|
label={__('Continue')}
|
||||||
</div>
|
disabled={followedTags.length < 1}
|
||||||
</Form>
|
/>
|
||||||
<div className="section__body">
|
</div>
|
||||||
<TagsSelect hideHeader limitShow={300} help={false} showClose={false} title={__('Follow New Tags')} />
|
</Form>
|
||||||
{followingCount > 0 && (
|
<div className="section__body">
|
||||||
<Nag
|
<TagsSelect hideHeader limitShow={300} help={false} showClose={false} title={__('Follow New Tags')} />
|
||||||
type="helpful"
|
{followingCount > 0 && (
|
||||||
message={
|
<Nag
|
||||||
followingCount === 1
|
type="helpful"
|
||||||
? __('You are currently following %followingCount% tag', { followingCount })
|
message={
|
||||||
: __('You are currently following %followingCount% tags', { followingCount })
|
followingCount === 1
|
||||||
}
|
? __('You are currently following %followingCount% tag', { followingCount })
|
||||||
actionText={__('Continue')}
|
: __('You are currently following %followingCount% tags', { followingCount })
|
||||||
onClick={onContinue}
|
}
|
||||||
/>
|
actionText={__('Continue')}
|
||||||
)}
|
onClick={onContinue}
|
||||||
</div>
|
/>
|
||||||
</React.Fragment>
|
)}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -92,7 +92,7 @@ class UserVerify extends React.PureComponent<Props> {
|
||||||
)}
|
)}
|
||||||
actions={
|
actions={
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{errorMessage && <p className="error-text">{errorMessage}</p>}
|
{errorMessage && <p className="error__text">{errorMessage}</p>}
|
||||||
<CardVerify
|
<CardVerify
|
||||||
label={__('Verify Card')}
|
label={__('Verify Card')}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
|
|
|
@ -90,7 +90,7 @@ class WalletSend extends React.PureComponent<Props> {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{!!Object.keys(errors).length || (
|
{!!Object.keys(errors).length || (
|
||||||
<span className="error-text">
|
<span className="error__text">
|
||||||
{(!!values.address && touched.address && errors.address) ||
|
{(!!values.address && touched.address && errors.address) ||
|
||||||
(!!values.amount && touched.amount && errors.amount) ||
|
(!!values.amount && touched.amount && errors.amount) ||
|
||||||
(parseFloat(values.amount) === balance &&
|
(parseFloat(values.amount) === balance &&
|
||||||
|
|
|
@ -84,6 +84,7 @@ export const EYE = 'Eye';
|
||||||
export const EYE_OFF = 'EyeOff';
|
export const EYE_OFF = 'EyeOff';
|
||||||
export const SIGN_OUT = 'SignOut';
|
export const SIGN_OUT = 'SignOut';
|
||||||
export const SIGN_IN = 'SignIn';
|
export const SIGN_IN = 'SignIn';
|
||||||
|
export const SIGN_UP = 'Key';
|
||||||
export const TRENDING = 'Trending';
|
export const TRENDING = 'Trending';
|
||||||
export const TOP = 'Top';
|
export const TOP = 'Top';
|
||||||
export const NEW = 'New';
|
export const NEW = 'New';
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
exports.AUTH = 'signin';
|
exports.AUTH = 'signup';
|
||||||
|
exports.AUTH_SIGNIN = 'signin';
|
||||||
exports.AUTH_VERIFY = 'verify';
|
exports.AUTH_VERIFY = 'verify';
|
||||||
|
exports.AUTH_PASSWORD_SET = 'reset';
|
||||||
exports.BACKUP = 'backup';
|
exports.BACKUP = 'backup';
|
||||||
exports.CHANNEL = 'channel';
|
exports.CHANNEL = 'channel';
|
||||||
exports.DISCOVER = 'discover';
|
exports.DISCOVER = 'discover';
|
||||||
|
|
|
@ -55,7 +55,7 @@ function ModalRemoveFile(props: Props) {
|
||||||
onChange={() => setAbandonChecked(!abandonChecked)}
|
onChange={() => setAbandonChecked(!abandonChecked)}
|
||||||
/>
|
/>
|
||||||
{abandonChecked === true && (
|
{abandonChecked === true && (
|
||||||
<p className="help error-text">This action is permanent and cannot be undone.</p>
|
<p className="help error__text">This action is permanent and cannot be undone.</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* @if TARGET='app' */}
|
{/* @if TARGET='app' */}
|
||||||
|
|
|
@ -71,7 +71,7 @@ export default function ModalRevokeClaim(props: Props) {
|
||||||
'This will prevent others from resolving and accessing the content you published. It will return the LBC to your spendable balance, less a small transaction fee.'
|
'This will prevent others from resolving and accessing the content you published. It will return the LBC to your spendable balance, less a small transaction fee.'
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p className="help error-text"> {__('FINAL WARNING: This action is permanent and cannot be undone.')}</p>
|
<p className="help error__text"> {__('FINAL WARNING: This action is permanent and cannot be undone.')}</p>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,7 +168,7 @@ class ModalWalletEncrypt extends React.PureComponent<Props, State> {
|
||||||
name="wallet-understand"
|
name="wallet-understand"
|
||||||
onChange={event => this.onChangeUnderstandConfirm(event)}
|
onChange={event => this.onChangeUnderstandConfirm(event)}
|
||||||
/>
|
/>
|
||||||
{failMessage && <div className="error-text">{__(failMessage)}</div>}
|
{failMessage && <div className="error__text">{__(failMessage)}</div>}
|
||||||
</Form>
|
</Form>
|
||||||
<div className="card__actions">
|
<div className="card__actions">
|
||||||
<Button button="link" label={__('Cancel')} onClick={closeModal} />
|
<Button button="link" label={__('Cancel')} onClick={closeModal} />
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import WalletAddress from 'component/walletAddress';
|
|
||||||
import Page from 'component/page';
|
|
||||||
|
|
||||||
const WalletAddressPage = () => (
|
|
||||||
<Page className="main--contained">
|
|
||||||
<WalletAddress />
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default WalletAddressPage;
|
|
|
@ -1,10 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import WalletSend from 'component/walletSend';
|
|
||||||
|
|
||||||
const WalletSendModal = () => (
|
|
||||||
<div>
|
|
||||||
<WalletSend />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default WalletSendModal;
|
|
3
ui/page/passwordSet/index.js
Normal file
3
ui/page/passwordSet/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import PasswordResetPage from './view';
|
||||||
|
|
||||||
|
export default PasswordResetPage;
|
12
ui/page/passwordSet/view.jsx
Normal file
12
ui/page/passwordSet/view.jsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import UserPasswordSet from 'component/userPasswordSet';
|
||||||
|
import Page from 'component/page';
|
||||||
|
|
||||||
|
export default function PasswordResetPage() {
|
||||||
|
return (
|
||||||
|
<Page authPage className="main--auth-page">
|
||||||
|
<UserPasswordSet />
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
|
@ -94,7 +94,7 @@ class RewardsPage extends PureComponent<Props> {
|
||||||
return (
|
return (
|
||||||
<section className="card card--section">
|
<section className="card card--section">
|
||||||
<h2 className="card__title card__title--deprecated">{__('Rewards Disabled')}</h2>
|
<h2 className="card__title card__title--deprecated">{__('Rewards Disabled')}</h2>
|
||||||
<p className="error-text">
|
<p className="error__text">
|
||||||
<I18nMessage tokens={{ settings: <Button button="link" navigate="/$/settings" label="Settings" /> }}>
|
<I18nMessage tokens={{ settings: <Button button="link" navigate="/$/settings" label="Settings" /> }}>
|
||||||
Rewards are currently disabled for your account. Turn on diagnostic data sharing, in %settings%, to
|
Rewards are currently disabled for your account. Turn on diagnostic data sharing, in %settings%, to
|
||||||
re-enable them.
|
re-enable them.
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { SETTINGS } from 'lbry-redux';
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
import { getPasswordFromCookie } from 'util/saved-passwords';
|
import { getPasswordFromCookie } from 'util/saved-passwords';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
|
import SettingAccountPassword from 'component/settingAccountPassword';
|
||||||
|
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
export const IS_MAC = process.platform === 'darwin';
|
export const IS_MAC = process.platform === 'darwin';
|
||||||
|
@ -261,11 +262,12 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||||
<Page className="card-stack">
|
<Page className="card-stack">
|
||||||
{!IS_WEB && noDaemonSettings ? (
|
{!IS_WEB && noDaemonSettings ? (
|
||||||
<section className="card card--section">
|
<section className="card card--section">
|
||||||
<div className="card__title card__title--deprecated">{__('Failed to load settings.')}</div>
|
<div className="card__title card__t itle--deprecated">{__('Failed to load settings.')}</div>
|
||||||
</section>
|
</section>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<Card title={__('Language')} actions={<SettingLanguage />} />
|
<Card title={__('Language')} actions={<SettingLanguage />} />
|
||||||
|
{isAuthenticated && <SettingAccountPassword />}
|
||||||
{/* @if TARGET='app' */}
|
{/* @if TARGET='app' */}
|
||||||
<Card
|
<Card
|
||||||
title={__('Sync')}
|
title={__('Sync')}
|
||||||
|
|
|
@ -1,10 +1,3 @@
|
||||||
import { connect } from 'react-redux';
|
import SignInPage from './view';
|
||||||
import SignUpPage from './view';
|
|
||||||
|
|
||||||
const select = () => ({});
|
export default SignInPage;
|
||||||
const perform = () => ({});
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
select,
|
|
||||||
perform
|
|
||||||
)(SignUpPage);
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import * as PAGES from 'constants/pages';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
|
@ -6,7 +7,7 @@ import ReCAPTCHA from 'react-google-recaptcha';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import { Lbryio } from 'lbryinc';
|
import { Lbryio } from 'lbryinc';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
import * as PAGES from 'constants/pages';
|
import Card from 'component/common/card';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
history: { push: string => void, location: { search: string } },
|
history: { push: string => void, location: { search: string } },
|
||||||
|
@ -88,40 +89,49 @@ function SignInVerifyPage(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page authPage className="main--auth-page">
|
<Page authPage className="main--auth-page">
|
||||||
<section className="main--contained">
|
<div className="main__sign-up">
|
||||||
<h1 className="section__title--large">
|
<Card
|
||||||
{isAuthenticationSuccess ? __('Sign In Success!') : __('Sign In to lbry.tv')}
|
title={isAuthenticationSuccess ? __('Sign In Success!') : __('Sign In to lbry.tv')}
|
||||||
</h1>
|
subtitle={
|
||||||
<p className="section__subtitle">
|
<React.Fragment>
|
||||||
{isAuthenticationSuccess
|
<p>
|
||||||
? __('You can now close this tab.')
|
{isAuthenticationSuccess
|
||||||
: needsRecaptcha
|
? __('You can now close this tab.')
|
||||||
? __('Click below to sign in to lbry.tv')
|
: needsRecaptcha
|
||||||
: __('Welcome back! You are automatically being signed in.')}
|
? __('Click below to sign in to lbry.tv')
|
||||||
</p>
|
: __('Welcome back! You are automatically being signed in.')}
|
||||||
<p className="section__subtitle">
|
</p>
|
||||||
{showCaptchaMessage && !isAuthenticationSuccess && (
|
{showCaptchaMessage && !isAuthenticationSuccess && (
|
||||||
<I18nMessage
|
<p>
|
||||||
tokens={{
|
<I18nMessage
|
||||||
refresh: <Button button="link" label={__('refreshing')} onClick={() => window.location.reload()} />,
|
tokens={{
|
||||||
}}
|
refresh: (
|
||||||
>
|
<Button button="link" label={__('refreshing')} onClick={() => window.location.reload()} />
|
||||||
Not seeing a captcha? Check your ad blocker or try %refresh%.
|
),
|
||||||
</I18nMessage>
|
}}
|
||||||
)}
|
>
|
||||||
</p>
|
Not seeing a captcha? Check your ad blocker or try %refresh%.
|
||||||
{!isAuthenticationSuccess && needsRecaptcha && (
|
</I18nMessage>
|
||||||
<div className="section__actions">
|
</p>
|
||||||
<ReCAPTCHA
|
)}
|
||||||
sitekey="6LePsJgUAAAAAFTuWOKRLnyoNKhm0HA4C3elrFMG"
|
</React.Fragment>
|
||||||
onChange={onCaptchaChange}
|
}
|
||||||
asyncScriptOnLoad={onCaptchaReady}
|
actions={
|
||||||
onExpired={onAuthError}
|
!isAuthenticationSuccess &&
|
||||||
onErrored={onAuthError}
|
needsRecaptcha && (
|
||||||
/>
|
<div className="section__actions">
|
||||||
</div>
|
<ReCAPTCHA
|
||||||
)}
|
sitekey="6LePsJgUAAAAAFTuWOKRLnyoNKhm0HA4C3elrFMG"
|
||||||
</section>
|
onChange={onCaptchaChange}
|
||||||
|
asyncScriptOnLoad={onCaptchaReady}
|
||||||
|
onExpired={onAuthError}
|
||||||
|
onErrored={onAuthError}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
3
ui/page/signUp/index.js
Normal file
3
ui/page/signUp/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import SignUpPage from './view';
|
||||||
|
|
||||||
|
export default SignUpPage;
|
12
ui/page/signUp/view.jsx
Normal file
12
ui/page/signUp/view.jsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import UserSignUp from 'component/userSignUp';
|
||||||
|
import Page from 'component/page';
|
||||||
|
|
||||||
|
export default function SignUpPage() {
|
||||||
|
return (
|
||||||
|
<Page authPage className="main--auth-page">
|
||||||
|
<UserSignUp />
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
|
@ -46,6 +46,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button--header-close {
|
||||||
|
background-color: var(--color-primary-alt);
|
||||||
|
padding: var(--spacing-small);
|
||||||
|
}
|
||||||
|
|
||||||
.button--download-link {
|
.button--download-link {
|
||||||
.button__label {
|
.button__label {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
|
|
@ -138,6 +138,13 @@
|
||||||
& > *:not(:last-child) {
|
& > *:not(:last-child) {
|
||||||
margin-right: var(--spacing-medium);
|
margin-right: var(--spacing-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* .badge rule inherited from file page prices, should be refactored */
|
||||||
|
.badge {
|
||||||
|
float: right;
|
||||||
|
margin-left: var(--spacing-small);
|
||||||
|
margin-top: 8px; // should be flex'd, but don't blame me! I just moved it down 3px
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__title.card__title--deprecated {
|
.card__title.card__title--deprecated {
|
||||||
|
@ -172,19 +179,6 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__title {
|
|
||||||
font-size: var(--font-title);
|
|
||||||
font-weight: var(--font-weight-light);
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
/* .badge rule inherited from file page prices, should be refactored */
|
|
||||||
.badge {
|
|
||||||
float: right;
|
|
||||||
margin-left: var(--spacing-small);
|
|
||||||
margin-top: 8px; // should be flex'd, but don't blame me! I just moved it down 3px
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card__subtitle {
|
.card__subtitle {
|
||||||
color: var(--color-text-subtitle);
|
color: var(--color-text-subtitle);
|
||||||
margin: var(--spacing-small) 0;
|
margin: var(--spacing-small) 0;
|
||||||
|
@ -245,3 +239,19 @@
|
||||||
margin-bottom: var(--spacing-small);
|
margin-bottom: var(--spacing-small);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card__bottom-gutter {
|
||||||
|
@extend .help;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: var(--spacing-medium);
|
||||||
|
|
||||||
|
&:only-child,
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> *:not(:last-child) {
|
||||||
|
margin-right: var(--spacing-medium);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -95,6 +95,11 @@
|
||||||
margin-right: var(--spacing-medium);
|
margin-right: var(--spacing-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.channel-thumbnail {
|
||||||
|
width: 6rem;
|
||||||
|
height: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
.claim-preview__hover-actions {
|
.claim-preview__hover-actions {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
@ -181,3 +181,13 @@
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-left: var(--spacing-small);
|
margin-left: var(--spacing-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header__auth-buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
|
||||||
|
& > *:not(:last-child) {
|
||||||
|
margin: 0 var(--spacing-medium);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.main--auth-page {
|
.main--auth-page {
|
||||||
max-width: 60rem;
|
max-width: 70rem;
|
||||||
margin-top: var(--spacing-main-padding);
|
margin-top: var(--spacing-main-padding);
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
@ -61,11 +61,10 @@
|
||||||
|
|
||||||
.main--contained {
|
.main--contained {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
margin-top: 2rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
max-width: 40rem;
|
max-width: 50rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
|
@ -76,3 +75,16 @@
|
||||||
.main--full-width {
|
.main--full-width {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main__sign-in,
|
||||||
|
.main__sign-up {
|
||||||
|
max-width: 27rem;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main__channel-creation {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
max-width: 32rem;
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
.ReactModal__Body--open {
|
.ReactModal__Body--open {
|
||||||
#app {
|
#app {
|
||||||
height: 100vh;
|
@media (max-width: $breakpoint-small) {
|
||||||
overflow-y: hidden;
|
height: 100vh;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,10 @@ $nag-error-z-index: 100001;
|
||||||
z-index: 1 !important; /* booooooo */
|
z-index: 1 !important; /* booooooo */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nag--relative {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.nag--helpful {
|
.nag--helpful {
|
||||||
background-color: var(--color-secondary);
|
background-color: var(--color-secondary);
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
.section {
|
.section {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-top: var(--spacing-large);
|
|
||||||
|
|
||||||
&:first-of-type {
|
~ .section {
|
||||||
margin-top: 0;
|
margin-top: var(--spacing-large);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +50,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section__subtitle {
|
||||||
|
color: var(--color-text-subtitle);
|
||||||
|
margin: var(--spacing-small) 0;
|
||||||
|
font-size: var(--font-body);
|
||||||
|
}
|
||||||
|
|
||||||
.section__subtitle--status {
|
.section__subtitle--status {
|
||||||
@extend .section__subtitle;
|
@extend .section__subtitle;
|
||||||
padding: var(--spacing-small);
|
padding: var(--spacing-small);
|
||||||
|
@ -82,6 +87,42 @@
|
||||||
margin-top: var(--spacing-medium);
|
margin-top: var(--spacing-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: var(--spacing-large);
|
||||||
|
|
||||||
|
&:only-child,
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> *:not(:last-child) {
|
||||||
|
margin-right: var(--spacing-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
margin-bottom: var(--spacing-small);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button--primary,
|
||||||
|
.button ~ .button--link {
|
||||||
|
&:focus {
|
||||||
|
@include focus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button--primary ~ .button--link {
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
margin-left: var(--spacing-small);
|
||||||
|
padding: var(--spacing-xsmall);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.section__actions--centered {
|
.section__actions--centered {
|
||||||
@extend .section__actions;
|
@extend .section__actions;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -94,13 +135,3 @@
|
||||||
.section__actions--no-margin {
|
.section__actions--no-margin {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
|
||||||
.section__actions {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
> * {
|
|
||||||
margin-bottom: var(--spacing-small);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -201,7 +201,7 @@ img {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: var(--font-small);
|
font-size: var(--font-small);
|
||||||
color: var(--color-text-help);
|
color: var(--color-text-help);
|
||||||
margin-top: var(--spacing-miniscule);
|
margin-top: var(--spacing-small);
|
||||||
margin-bottom: var(--spacing-small);
|
margin-bottom: var(--spacing-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,13 +238,13 @@ img {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-wrapper {
|
.error__wrapper {
|
||||||
background-color: var(--color-error);
|
background-color: var(--color-error);
|
||||||
padding: var(--spacing-small);
|
padding: var(--spacing-small);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-text {
|
.error__text {
|
||||||
color: var(--color-text-error);
|
color: var(--color-text-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
--color-header-button: #f7f7f7;
|
--color-header-button: #f7f7f7;
|
||||||
|
|
||||||
// Color
|
// Color
|
||||||
--color-background: #f7f7f7;
|
--color-background: #f9f9f9;
|
||||||
--color-background--splash: #212529;
|
--color-background--splash: #212529;
|
||||||
--color-border: #ededed;
|
--color-border: #ededed;
|
||||||
--color-background-overlay: #21252980;
|
--color-background-overlay: #21252980;
|
||||||
|
|
|
@ -2,7 +2,8 @@ const { DOMAIN } = require('../../config.js');
|
||||||
const AUTH_TOKEN = 'auth_token';
|
const AUTH_TOKEN = 'auth_token';
|
||||||
const SAVED_PASSWORD = 'saved_password';
|
const SAVED_PASSWORD = 'saved_password';
|
||||||
const DEPRECATED_SAVED_PASSWORD = 'saved-password';
|
const DEPRECATED_SAVED_PASSWORD = 'saved-password';
|
||||||
const domain = typeof window === 'object' ? window.location.hostname : DOMAIN;
|
const domain =
|
||||||
|
typeof window === 'object' && window.location.hostname.includes('localhost') ? window.location.hostname : DOMAIN;
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
const maxExpiration = 2147483647;
|
const maxExpiration = 2147483647;
|
||||||
let sessionPassword;
|
let sessionPassword;
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -819,10 +819,10 @@
|
||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
scheduler "^0.18.0"
|
scheduler "^0.18.0"
|
||||||
|
|
||||||
"@lbry/components@^4.0.1":
|
"@lbry/components@^4.1.2":
|
||||||
version "4.0.1"
|
version "4.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@lbry/components/-/components-4.0.1.tgz#8dcf7348920383d854c0db640faaf1ac5a72f7ef"
|
resolved "https://registry.yarnpkg.com/@lbry/components/-/components-4.1.2.tgz#18eda2f1a73a6241e9a96594ccab8fffb9ef3ae9"
|
||||||
integrity sha512-vY84ziZ9EaXoezDBK2VsajvXcSPXDV0fr1VWn2w0iHkGa756RWvNySpnqaKMZH+myK12mvNNc/NkGIW5oO7+5w==
|
integrity sha512-GJx4BTdEtOlm5/JsKUVXBzYeepXTbZ4GjGFrKxzXh6jWw40aFOh8OrHwYM/LHRl1fuKRmB5xpZNWsjw6EKyEeQ==
|
||||||
|
|
||||||
"@mapbox/hast-util-table-cell-style@^0.1.3":
|
"@mapbox/hast-util-table-cell-style@^0.1.3":
|
||||||
version "0.1.3"
|
version "0.1.3"
|
||||||
|
@ -6147,9 +6147,9 @@ lbry-redux@lbryio/lbry-redux#1097a63d44a20b87e443fbaa48f95fe3ea5e3f70:
|
||||||
reselect "^3.0.0"
|
reselect "^3.0.0"
|
||||||
uuid "^3.3.2"
|
uuid "^3.3.2"
|
||||||
|
|
||||||
lbryinc@lbryio/lbryinc#0addc624db54000b0447f4539f91f5758d26eef3:
|
lbryinc@lbryio/lbryinc#12aefaa14343d2f3eac01f2683701f58e53f1848:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/0addc624db54000b0447f4539f91f5758d26eef3"
|
resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/12aefaa14343d2f3eac01f2683701f58e53f1848"
|
||||||
dependencies:
|
dependencies:
|
||||||
reselect "^3.0.0"
|
reselect "^3.0.0"
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue