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:
Sean Yesmunt 2020-04-13 15:16:07 -04:00 committed by GitHub
parent f177f2dbb9
commit 2677cd17d8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
73 changed files with 1566 additions and 652 deletions

View file

@ -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,

View file

@ -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",

View file

@ -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%"
} }

View file

@ -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

View file

@ -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')}

View file

@ -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>
); );
} }

View file

@ -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>;
} }

View file

@ -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>}

View file

@ -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" />

View file

@ -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,9 +27,11 @@ 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>
{(href || onClick) && (
<Button <Button
className={classnames('nag__button', { className={classnames('nag__button', {
'nag__button--helpful': type === 'helpful', 'nag__button--helpful': type === 'helpful',
@ -38,6 +41,7 @@ export default function Nag(props: Props) {
> >
{actionText} {actionText}
</Button> </Button>
)}
{onClose && ( {onClose && (
<Button <Button
className={classnames('nag__button nag__close', { className={classnames('nag__button nag__close', {

View file

@ -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>
)} )}

View file

@ -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);

View file

@ -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>
) : ( ) : (
<React.Fragment>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.AUTH}`)}> <MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.AUTH}`)}>
<Icon aria-hidden icon={ICONS.SIGN_UP} />
{__('Join')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.AUTH_SIGNIN}`)}>
<Icon aria-hidden icon={ICONS.SIGN_IN} /> <Icon aria-hidden icon={ICONS.SIGN_IN} />
{__('Sign In')} {__('Sign In')}
</MenuItem> </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>
) )

View file

@ -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"

View file

@ -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}

View file

@ -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

View file

@ -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'}

View 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);

View 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)}
/>
)
}
/>
);
}

View file

@ -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),

View file

@ -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,

View file

@ -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')} />
</> </>
) : ( ) : (

View file

@ -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,15 +28,14 @@ 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.'
)} )}
</p> actions={
<Form onSubmit={onContinue} className="section__body"> <React.Fragment>
<div className="card__actions"> <div className="section__actions">
<Button button="secondary" onClick={onBack} label={__('Back')} /> <Button button="secondary" onClick={onBack} label={__('Back')} />
<Button <Button
button="primary" button="primary"
@ -46,7 +45,6 @@ function UserChannelFollowIntro(props: Props) {
disabled={subscribedChannels.length < 2} disabled={subscribedChannels.length < 2}
/> />
</div> </div>
</Form>
<div className="section__body"> <div className="section__body">
<ClaimListDiscover <ClaimListDiscover
defaultOrderBy={CS.ORDER_BY_TOP} defaultOrderBy={CS.ORDER_BY_TOP}
@ -68,6 +66,8 @@ function UserChannelFollowIntro(props: Props) {
)} )}
</div> </div>
</React.Fragment> </React.Fragment>
}
/>
); );
} }

View file

@ -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);

View file

@ -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);

View file

@ -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,35 +52,64 @@ 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>
<Form onSubmit={handleSubmit} className="section__body">
<FormField <FormField
autoFocus autoFocus
placeholder={__('hotstuff_96@hotmail.com')} placeholder={__('hotstuff_96@hotmail.com')}
type="email" type="email"
name="sign_up_email" name="sign_up_email"
label={__('Email')} label={__('Email')}
value={newEmail} value={email}
error={errorMessage}
onChange={e => setEmail(e.target.value)} onChange={e => setEmail(e.target.value)}
/> />
<FormField
type="password"
name="sign_in_password"
label={__('Password')}
value={password}
onChange={e => setPassword(e.target.value)}
/>
{!IS_WEB && ( {!IS_WEB && (
<FormField <FormField
@ -86,41 +136,39 @@ function UserEmailNew(props: Props) {
<React.Fragment> <React.Fragment>
{__('Share usage data with LBRY inc.')}{' '} {__('Share usage data with LBRY inc.')}{' '}
<Button button="link" href="https://lbry.com/faq/privacy-and-data" label={__('Learn More')} /> <Button button="link" href="https://lbry.com/faq/privacy-and-data" label={__('Learn More')} />
{!localShareUsageData && <span className="error-text"> ({__('Required')})</span>} {!localShareUsageData && <span className="error__text"> ({__('Required')})</span>}
</React.Fragment> </React.Fragment>
} }
/> />
)} )}
<div className="card__actions"> <div className="section__actions">
<Button <Button
button="primary" button="primary"
type="submit" type="submit"
label={__('Continue')} label={__('Join')}
disabled={!newEmail || !valid || (!IS_WEB && (!localShareUsageData && !shareUsageData)) || isPending} disabled={
!email || !password || !valid || (!IS_WEB && !localShareUsageData && !shareUsageData) || isPending
}
/> />
<Button button="link" onClick={handleChangeToSignIn} label={__('Sign In')} />
</div> </div>
</Form>
{/* @if TARGET='web' */}
<p className="help"> <p className="help">
<React.Fragment>
<I18nMessage <I18nMessage
tokens={{ tokens={{
terms: ( terms: (
<Button <Button button="link" href="https://www.lbry.com/termsofservice" label={__('Terms of Service')} />
tabIndex="2"
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. By continuing, I agree to the %terms% and confirm I am over the age of 13.
</I18nMessage> </I18nMessage>
</React.Fragment>
</p> </p>
{/* @endif */} </Form>
</React.Fragment> </div>
}
nag={errorMessage && <Nag type="error" relative message={<ErrorText>{errorMessage}</ErrorText>} />}
/>
</div>
); );
} }

View 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);

View 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;

View file

@ -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);

View file

@ -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,10 +61,11 @@ 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.',
{ {
@ -69,8 +74,10 @@ class UserEmailVerify extends React.PureComponent<Props> {
} }
)} )}
</p> </p>
}
<div className="section__body section__actions"> actions={
<React.Fragment>
<div className="section__actions">
<Button <Button
button="primary" button="primary"
label={__('Resend Email')} label={__('Resend Email')}
@ -79,7 +86,6 @@ class UserEmailVerify extends React.PureComponent<Props> {
/> />
<UserSignOutButton label={__('Start Over')} /> <UserSignOutButton label={__('Start Over')} />
</div> </div>
<p className="help"> <p className="help">
<I18nMessage <I18nMessage
tokens={{ tokens={{
@ -91,6 +97,9 @@ class UserEmailVerify extends React.PureComponent<Props> {
</I18nMessage> </I18nMessage>
</p> </p>
</React.Fragment> </React.Fragment>
}
/>
</div>
); );
} }
} }

View file

@ -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,18 +37,22 @@ 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')}
subtitle={
<React.Fragment>
<p>{__('A channel is your identity on the LBRY network.')}</p> <p>{__('A channel is your identity on the LBRY network.')}</p>
<p>{__('You can have more than one or remove this later.')}</p> <p>{__('You can have more than one or remove this later.')}</p>
</div> </React.Fragment>
<section className="section__body"> }
actions={
<Form onSubmit={handleCreateChannel}>
<fieldset-group class="fieldset-group--smushed fieldset-group--disabled-prefix"> <fieldset-group class="fieldset-group--smushed fieldset-group--disabled-prefix">
<fieldset-section> <fieldset-section>
<label htmlFor="auth_first_channel"> <label htmlFor="auth_first_channel">
{createChannelError || nameError ? ( {createChannelError || nameError ? (
<span className="error-text">{createChannelError || nameError}</span> <span className="error__text">{createChannelError || nameError}</span>
) : ( ) : (
__('Your Channel') __('Your Channel')
)} )}
@ -73,8 +78,10 @@ function UserFirstChannel(props: Props) {
label={creatingChannel || claimingReward ? __('Creating') : __('Create')} label={creatingChannel || claimingReward ? __('Creating') : __('Create')}
/> />
</div> </div>
</section>
</Form> </Form>
}
/>
</div>
); );
} }

View 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);

View 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;

View 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);

View 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;

View file

@ -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);

View file

@ -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; }
}, [emailToVerify, emailOnlyLogin, doUserSignIn]);
if (hasVerifiedEmail && !hasClaimedEmailAward && !hasFetchedReward && !delayForSync) { return (
claimReward(); <section>
} {(showEmail || showPassword) && (
}, [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> <div>
<YoutubeTransferStatus /> <Confetti recycle={false} style={{ position: 'fixed' }} /> {showEmail && <UserEmailReturning />}
{showPassword && <UserSignInPassword onHandleEmailOnly={() => setEmailOnlyLogin(true)} />}
</div> </div>
), )}
showSyncPassword && <SyncPassword />, {!showEmail && !showPassword && showLoading && (
showLoadingSpinner && (
<div className="main--empty"> <div className="main--empty">
<Spinner /> <Spinner delayed />
</div> </div>
), )}
]; </section>
);
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);

View 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);

View 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>
);
}

View file

@ -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);

View file

@ -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;

View 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);

View 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);

View file

@ -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,11 +17,13 @@ function UserChannelFollowIntro(props: Props) {
const followingCount = (followedTags && followedTags.length) || 0; const followingCount = (followedTags && followedTags.length) || 0;
return ( return (
<Card
title={__('Tag Selection')}
subtitle={__('Select some tags to help us show you interesting things.')}
actions={
<React.Fragment> <React.Fragment>
<h1 className="section__title--large">{__('Tag Selection')}</h1> <Form onSubmit={onContinue}>
<p className="section__subtitle">{__('Select some tags to help us show you interesting things.')}</p> <div className="section__actions">
<Form onSubmit={onContinue} className="section__body">
<div className="card__actions">
<Button <Button
button="primary" button="primary"
type="Submit" type="Submit"
@ -46,6 +49,8 @@ function UserChannelFollowIntro(props: Props) {
)} )}
</div> </div>
</React.Fragment> </React.Fragment>
}
/>
); );
} }

View file

@ -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}

View file

@ -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 &&

View file

@ -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';

View file

@ -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';

View file

@ -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' */}

View file

@ -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>
); );
} }

View file

@ -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} />

View file

@ -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;

View file

@ -1,10 +0,0 @@
import React from 'react';
import WalletSend from 'component/walletSend';
const WalletSendModal = () => (
<div>
<WalletSend />
</div>
);
export default WalletSendModal;

View file

@ -0,0 +1,3 @@
import PasswordResetPage from './view';
export default PasswordResetPage;

View 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>
);
}

View file

@ -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.

View file

@ -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';
@ -266,6 +267,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
) : ( ) : (
<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')}

View file

@ -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);

View file

@ -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,29 +89,36 @@ 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>
<p>
{isAuthenticationSuccess {isAuthenticationSuccess
? __('You can now close this tab.') ? __('You can now close this tab.')
: needsRecaptcha : needsRecaptcha
? __('Click below to sign in to lbry.tv') ? __('Click below to sign in to lbry.tv')
: __('Welcome back! You are automatically being signed in.')} : __('Welcome back! You are automatically being signed in.')}
</p> </p>
<p className="section__subtitle">
{showCaptchaMessage && !isAuthenticationSuccess && ( {showCaptchaMessage && !isAuthenticationSuccess && (
<p>
<I18nMessage <I18nMessage
tokens={{ tokens={{
refresh: <Button button="link" label={__('refreshing')} onClick={() => window.location.reload()} />, refresh: (
<Button button="link" label={__('refreshing')} onClick={() => window.location.reload()} />
),
}} }}
> >
Not seeing a captcha? Check your ad blocker or try %refresh%. Not seeing a captcha? Check your ad blocker or try %refresh%.
</I18nMessage> </I18nMessage>
)}
</p> </p>
{!isAuthenticationSuccess && needsRecaptcha && ( )}
</React.Fragment>
}
actions={
!isAuthenticationSuccess &&
needsRecaptcha && (
<div className="section__actions"> <div className="section__actions">
<ReCAPTCHA <ReCAPTCHA
sitekey="6LePsJgUAAAAAFTuWOKRLnyoNKhm0HA4C3elrFMG" sitekey="6LePsJgUAAAAAFTuWOKRLnyoNKhm0HA4C3elrFMG"
@ -120,8 +128,10 @@ function SignInVerifyPage(props: Props) {
onErrored={onAuthError} onErrored={onAuthError}
/> />
</div> </div>
)} )
</section> }
/>
</div>
</Page> </Page>
); );
} }

3
ui/page/signUp/index.js Normal file
View file

@ -0,0 +1,3 @@
import SignUpPage from './view';
export default SignUpPage;

12
ui/page/signUp/view.jsx Normal file
View 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>
);
}

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -1,9 +1,11 @@
.ReactModal__Body--open { .ReactModal__Body--open {
#app { #app {
@media (max-width: $breakpoint-small) {
height: 100vh; height: 100vh;
overflow-y: hidden; overflow-y: hidden;
} }
} }
}
.modal-overlay { .modal-overlay {
top: 0; top: 0;

View file

@ -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);

View file

@ -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);
}
}
}

View file

@ -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);
} }

View file

@ -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;

View file

@ -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;

View file

@ -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"