new signin/signup #3960

Merged
neb-b merged 15 commits from dev into master 2020-04-13 21:16:08 +02:00
73 changed files with 1566 additions and 652 deletions

View file

@ -30,6 +30,7 @@
"one-var": 0,
"prefer-promise-reject-errors": 0,
"react/jsx-indent": 0,
"react/jsx-no-comment-textnodes": 0,
"react-hooks/exhaustive-deps": "warn",
"react-hooks/rules-of-hooks": "error",
"react/no-unescaped-entities": 0,

View file

@ -68,7 +68,7 @@
"@babel/register": "^7.0.0",
"@exponent/electron-cookies": "^2.0.0",
"@hot-loader/react-dom": "^16.8",
"@lbry/components": "^4.0.1",
"@lbry/components": "^4.1.2",
"@reach/menu-button": "0.7.4",
"@reach/rect": "^0.2.1",
"@reach/tabs": "^0.1.5",
@ -131,7 +131,7 @@
"json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git",
"lbry-redux": "lbryio/lbry-redux#1097a63d44a20b87e443fbaa48f95fe3ea5e3f70",
"lbryinc": "lbryio/lbryinc#0addc624db54000b0447f4539f91f5758d26eef3",
"lbryinc": "lbryio/lbryinc#12aefaa14343d2f3eac01f2683701f58e53f1848",
"lint-staged": "^7.0.2",
"localforage": "^1.7.1",
"lodash-es": "^4.17.14",

View file

@ -1116,5 +1116,8 @@
"Repost %count%": "Repost %count%",
"File Description": "File Description",
"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 (
<div>
{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

View file

@ -125,7 +125,7 @@ class ChannelCreate extends React.PureComponent<Props, State> {
return (
<Form onSubmit={this.handleCreateChannel}>
{createChannelError && <div className="error-text">{createChannelError}</div>}
{createChannelError && <div className="error__text">{createChannelError}</div>}
<div>
<FormField
label={__('Name')}

View file

@ -17,6 +17,7 @@ type Props = {
isPageTitle?: boolean,
isBodyTable?: boolean,
defaultExpand?: boolean,
nag?: Node,
};
export default function Card(props: Props) {
@ -30,6 +31,7 @@ export default function Card(props: Props) {
isPageTitle = false,
isBodyTable = false,
defaultExpand,
nag,
} = props;
const [expanded, setExpanded] = useState(defaultExpand);
const expandable = defaultExpand !== undefined;
@ -73,6 +75,8 @@ export default function Card(props: Props) {
{actions && <div className="card__main-actions">{actions}</div>}
</>
)}
{nag}
</section>
);
}

View file

@ -2,11 +2,19 @@
import React from 'react';
type Props = {
children: any,
children: string,
};
export default function ErrorText(props: 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 = (
<fieldset-section>
{(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}>
{children}
@ -173,7 +173,7 @@ export class FormField extends React.PureComponent<Props> {
<fieldset-section>
{(label || errorMessage) && (
<label htmlFor={name}>
{errorMessage ? <span className="error-text">{errorMessage}</span> : label}
{errorMessage ? <span className="error__text">{errorMessage}</span> : label}
</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" />
</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(
<g>
<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 = {
message: string | Node,
actionText: string,
actionText?: string,
href?: string,
type?: string,
inline?: boolean,
relative?: boolean,
onClick?: () => void,
onClose?: () => void,
};
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 };
@ -26,18 +27,21 @@ export default function Nag(props: Props) {
'nag--helpful': type === 'helpful',
'nag--error': type === 'error',
'nag--inline': inline,
'nag--relative': relative,
})}
>
<div className="nag__message">{message}</div>
<Button
className={classnames('nag__button', {
'nag__button--helpful': type === 'helpful',
'nag__button--error': type === 'error',
})}
{...buttonProps}
>
{actionText}
</Button>
{(href || onClick) && (
<Button
className={classnames('nag__button', {
'nag__button--helpful': type === 'helpful',
'nag__button--error': type === 'error',
})}
{...buttonProps}
>
{actionText}
</Button>
)}
{onClose && (
<Button
className={classnames('nag__button nag__close', {

View file

@ -96,20 +96,20 @@ class ErrorBoundary extends React.Component<Props, State> {
}
/>
{!errorWasReported && (
<div className="error-wrapper">
<span className="error-text">
<div className="error__wrapper">
<span className="error__text">
{__('You are not currently sharing diagnostic data so this error was not reported.')}
</span>
</div>
)}
{errorWasReported && (
<div className="error-wrapper">
<div className="error__wrapper">
{/* @if TARGET='web' */}
<span className="error-text">{__('Error ID: %sentryEventId%', { sentryEventId })}</span>
<span className="error__text">{__('Error ID: %sentryEventId%', { sentryEventId })}</span>
{/* @endif */}
{/* @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 */}
</div>
)}

View file

@ -2,7 +2,14 @@ import * as SETTINGS from 'constants/settings';
import * as MODALS from 'constants/modal_types';
import { connect } from 'react-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 { doSignOut, doOpenModal } from 'redux/actions/app';
import { makeSelectClientSetting } from 'redux/selectors/settings';
@ -18,6 +25,7 @@ const select = state => ({
authenticated: selectUserVerifiedEmail(state),
email: selectUserEmail(state),
syncError: selectGetSyncErrorMessage(state),
emailToVerify: selectEmailToVerify(state),
});
const perform = dispatch => ({
@ -26,6 +34,8 @@ const perform = dispatch => ({
openMobileNavigation: () => dispatch(doOpenModal(MODALS.MOBILE_NAVIGATION)),
openChannelCreate: () => dispatch(doOpenModal(MODALS.CREATE_CHANNEL)),
openSignOutModal: () => dispatch(doOpenModal(MODALS.SIGN_OUT)),
clearEmailEntry: () => dispatch(doClearEmailEntry()),
clearPasswordEntry: () => dispatch(doClearPasswordEntry()),
});
export default connect(select, perform)(Header);

View file

@ -36,10 +36,13 @@ type Props = {
authenticated: boolean,
authHeader: boolean,
syncError: ?string,
emailToVerify?: string,
signOut: () => void,
openMobileNavigation: () => void,
openChannelCreate: () => void,
openSignOutModal: () => void,
clearEmailEntry: () => void,
clearPasswordEntry: () => void,
};
const Header = (props: Props) => {
@ -58,15 +61,37 @@ const Header = (props: Props) => {
openMobileNavigation,
openChannelCreate,
openSignOutModal,
clearEmailEntry,
clearPasswordEntry,
emailToVerify,
} = props;
// 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 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
const authHeaderAction = syncError ? { onClick: signOut } : { 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() {
if (automaticDarkModeEnabled) {
@ -170,9 +195,6 @@ const Header = (props: Props) => {
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.CREATOR_DASHBOARD}`)}>
<Icon aria-hidden icon={ICONS.ANALYTICS} />
{__('Creator Analytics')}
<span>
<span className="badge badge--alert">New!</span>
</span>
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.REWARDS}`)}>
<Icon aria-hidden icon={ICONS.FEATURED} />
@ -192,10 +214,16 @@ const Header = (props: Props) => {
<span className="menu__link-help">{email}</span>
</MenuItem>
) : (
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.AUTH}`)}>
<Icon aria-hidden icon={ICONS.SIGN_IN} />
{__('Sign In')}
</MenuItem>
<React.Fragment>
<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} />
{__('Sign In')}
</MenuItem>
</React.Fragment>
)}
</MenuList>
</Menu>
@ -223,7 +251,10 @@ const Header = (props: Props) => {
</MenuList>
</Menu>
{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>
) : (
@ -233,7 +264,12 @@ const Header = (props: Props) => {
{/* This pushes the close button to the right side */}
<span />
<Tooltip label={__('Go Back')}>
<Button button="link" icon={ICONS.REMOVE} {...closeButtonNavigationProps} />
<Button
button="alt"
// className="button--header-close"
icon={ICONS.REMOVE}
{...closeButtonNavigationProps}
/>
</Tooltip>
</div>
)

View file

@ -111,7 +111,7 @@ function Invited(props: Props) {
)}
actions={
<>
<p className="error-text">{__('Not a valid invite')}</p>
<p className="error__text">{__('Not a valid invite')}</p>
<div className="card__actions">
<Button
button="primary"

View file

@ -20,7 +20,7 @@ function PublishFormErrors(props: Props) {
// These are extra help
// If there is an error it will be presented as an inline error as well
return (
<div className="error-text">
<div className="error__text">
{!title && <div>{__('A title is required')}</div>}
{!name && <div>{__('A URL is required')}</div>}
{!isNameValid(name, false) && INVALID_NAME_ERROR}

View file

@ -28,6 +28,8 @@ import TagsFollowingManagePage from 'page/tagsFollowingManage';
import ListBlockedPage from 'page/listBlocked';
import FourOhFourPage from 'page/fourOhFour';
import SignInPage from 'page/signIn';
import SignUpPage from 'page/signUp';
import PasswordSetPage from 'page/passwordSet';
import SignInVerifyPage from 'page/signInVerify';
import ChannelsPage from 'page/channels';
import EmbedWrapperPage from 'page/embedWrapper';
@ -152,8 +154,10 @@ function AppRouter(props: Props) {
<Route path={`/`} exact component={HomePage} />
<Route path={`/$/${PAGES.DISCOVER}`} exact component={DiscoverPage} />
<Route path={`/$/${PAGES.AUTH}`} exact component={SignInPage} />
<Route path={`/$/${PAGES.AUTH}/*`} exact component={SignInPage} />
<Route path={`/$/${PAGES.AUTH_SIGNIN}`} 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.TAGS_FOLLOWING}`} exact component={TagsFollowingPage} />
<Route

View file

@ -74,7 +74,7 @@ function SelectAsset(props: Props) {
</FormField>
{assetSource === SOURCE_UPLOAD && (
<div>
{error && <div className="error-text">{error}</div>}
{error && <div className="error__text">{error}</div>}
{!pathSelected && (
<FileSelector
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">
{[
{
...(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),

View file

@ -1,4 +1,5 @@
// @flow
import type { Node } from 'react';
import * as MODALS from 'constants/modal_types';
import * as SETTINGS from 'constants/settings';
import React from 'react';
@ -33,7 +34,7 @@ type Props = {
};
type State = {
details: string,
details: string | Node,
message: string,
launchedModal: boolean,
error: boolean,

View file

@ -156,7 +156,7 @@ const SupportsLiquidate = (props: Props) => {
<React.Fragment>
{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')} />
</>
) : (

View file

@ -5,7 +5,7 @@ import * as CS from 'constants/claim_search';
import Nag from 'component/common/nag';
import { parseURI } from 'lbry-redux';
import Button from 'component/button';
import { Form } from 'component/common/form-components/form';
import Card from 'component/common/card';
type Props = {
subscribedChannels: Array<Subscription>,
@ -28,46 +28,46 @@ function UserChannelFollowIntro(props: Props) {
}, []);
return (
<React.Fragment>
<h1 className="section__title--large">{__('Find Channels to Follow')}</h1>
<p className="section__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.'
)}
</p>
<Form onSubmit={onContinue} className="section__body">
<div className="card__actions">
<Button button="secondary" onClick={onBack} label={__('Back')} />
<Button
button="primary"
type="Submit"
onClick={onContinue}
label={__('Continue')}
disabled={subscribedChannels.length < 2}
/>
</div>
</Form>
<div className="section__body">
<ClaimListDiscover
defaultOrderBy={CS.ORDER_BY_TOP}
defaultFreshness={CS.FRESH_ALL}
claimType="channel"
defaultTags={CS.TAGS_FOLLOWED}
/>
{followingCount > 0 && (
<Nag
type="helpful"
message={
followingCount === 1
? __('Nice! You are currently following %followingCount% creator', { followingCount })
: __('Nice! You are currently following %followingCount% creators', { followingCount })
}
actionText={__('Continue')}
onClick={onContinue}
/>
)}
</div>
</React.Fragment>
<Card
title={__('Find Channels to Follow')}
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.'
)}
actions={
<React.Fragment>
<div className="section__actions">
<Button button="secondary" onClick={onBack} label={__('Back')} />
<Button
button="primary"
type="Submit"
onClick={onContinue}
label={__('Continue')}
disabled={subscribedChannels.length < 2}
/>
</div>
<div className="section__body">
<ClaimListDiscover
defaultOrderBy={CS.ORDER_BY_TOP}
defaultFreshness={CS.FRESH_ALL}
claimType="channel"
defaultTags={CS.TAGS_FOLLOWED}
/>
{followingCount > 0 && (
<Nag
type="helpful"
message={
followingCount === 1
? __('Nice! You are currently following %followingCount% creator', { followingCount })
: __('Nice! You are currently following %followingCount% creators', { followingCount })
}
actionText={__('Continue')}
onClick={onContinue}
/>
)}
</div>
</React.Fragment>
}
/>
);
}

View file

@ -21,7 +21,4 @@ const perform = dispatch => ({
fetchAccessToken: () => dispatch(doFetchAccessToken()),
});
export default connect(
select,
perform
)(UserEmailVerify);
export default connect(select, perform)(UserEmailVerify);

View file

@ -1,6 +1,12 @@
import * as SETTINGS from 'constants/settings';
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 { doSetClientSetting, doSetDaemonSetting } from 'redux/actions/settings';
import { makeSelectClientSetting, selectDaemonSettings } from 'redux/selectors/settings';
@ -11,16 +17,15 @@ const select = state => ({
errorMessage: selectEmailNewErrorMessage(state),
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
daemonSettings: selectDaemonSettings(state),
emailExists: selectEmailAlreadyExists(state),
});
const perform = dispatch => ({
addUserEmail: email => dispatch(doUserEmailNew(email)),
setSync: value => dispatch(doSetClientSetting(SETTINGS.ENABLE_SYNC, value)),
setShareDiagnosticData: shouldShareData =>
dispatch(doSetDaemonSetting(DAEMON_SETTINGS.SHARE_USAGE_DATA, shouldShareData)),
doSignUp: (email, password) => dispatch(doUserSignUp(email, password)),
clearEmailEntry: () => dispatch(doClearEmailEntry()),
});
export default connect(
select,
perform
)(UserEmailNew);
export default connect(select, perform)(UserEmailNew);

View file

@ -1,29 +1,50 @@
// @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 analytics from 'analytics';
import { EMAIL_REGEX } from 'constants/email';
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 = {
errorMessage: ?string,
emailExists: boolean,
isPending: boolean,
addUserEmail: string => void,
syncEnabled: boolean,
setSync: boolean => void,
balance: number,
daemonSettings: { share_usage_data: boolean },
setShareDiagnosticData: boolean => void,
doSignUp: (string, ?string) => void,
clearEmailEntry: () => void,
};
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 [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 [formSyncEnabled, setFormSyncEnabled] = useState(true);
const valid = newEmail.match(EMAIL_REGEX);
const valid = email.match(EMAIL_REGEX);
function handleUsageDataChange() {
setLocalShareUsageData(!localShareUsageData);
@ -31,96 +52,123 @@ function UserEmailNew(props: Props) {
function handleSubmit() {
setSync(formSyncEnabled);
addUserEmail(newEmail);
doSignUp(email, password === '' ? undefined : password);
// @if TARGET='app'
setShareDiagnosticData(true);
// @endif
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 (
<React.Fragment>
<h1 className="section__title--large">{__('Sign In to lbry.tv')}</h1>
<p className="section__subtitle">
{/* @if TARGET='web' */}
{__('Create a new account or sign in.')}
{/* @endif */}
{/* @if TARGET='app' */}
{__('An account with lbry.tv allows you to earn rewards and backup your data.')}
{/* @endif */}
</p>
<Form onSubmit={handleSubmit} className="section__body">
<FormField
autoFocus
placeholder={__('hotstuff_96@hotmail.com')}
type="email"
name="sign_up_email"
label={__('Email')}
value={newEmail}
error={errorMessage}
onChange={e => setEmail(e.target.value)}
/>
<div className="main__sign-up">
<Card
title={__('Join lbry.tv')}
// @if TARGET='app'
subtitle={__('An account with lbry.tv allows you to earn rewards and backup your data.')}
// @endif
actions={
<div>
<Form onSubmit={handleSubmit} className="section">
<FormField
autoFocus
placeholder={__('hotstuff_96@hotmail.com')}
type="email"
name="sign_up_email"
label={__('Email')}
value={email}
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 && (
<FormField
type="checkbox"
name="sync_checkbox"
label={
<React.Fragment>
{__('Backup your account and wallet data.')}{' '}
<Button button="link" href="https://lbry.com/faq/account-sync" label={__('Learn More')} />
</React.Fragment>
}
checked={formSyncEnabled}
onChange={() => setFormSyncEnabled(!formSyncEnabled)}
/>
)}
{!shareUsageData && !IS_WEB && (
<FormField
type="checkbox"
name="share_data_checkbox"
checked={localShareUsageData}
onChange={handleUsageDataChange}
label={
<React.Fragment>
{__('Share usage data with LBRY inc.')}{' '}
<Button button="link" href="https://lbry.com/faq/privacy-and-data" label={__('Learn More')} />
{!localShareUsageData && <span className="error-text"> ({__('Required')})</span>}
</React.Fragment>
}
/>
)}
<div className="card__actions">
<Button
button="primary"
type="submit"
label={__('Continue')}
disabled={!newEmail || !valid || (!IS_WEB && (!localShareUsageData && !shareUsageData)) || isPending}
/>
</div>
</Form>
{/* @if TARGET='web' */}
<p className="help">
<React.Fragment>
<I18nMessage
tokens={{
terms: (
<Button
tabIndex="2"
button="link"
href="https://www.lbry.com/termsofservice"
label={__('Terms of Service')}
{!IS_WEB && (
<FormField
type="checkbox"
name="sync_checkbox"
label={
<React.Fragment>
{__('Backup your account and wallet data.')}{' '}
<Button button="link" href="https://lbry.com/faq/account-sync" label={__('Learn More')} />
</React.Fragment>
}
checked={formSyncEnabled}
onChange={() => setFormSyncEnabled(!formSyncEnabled)}
/>
),
}}
>
By continuing, I agree to the %terms% and confirm I am over the age of 13.
</I18nMessage>
</React.Fragment>
</p>
{/* @endif */}
</React.Fragment>
)}
{!shareUsageData && !IS_WEB && (
<FormField
type="checkbox"
name="share_data_checkbox"
checked={localShareUsageData}
onChange={handleUsageDataChange}
label={
<React.Fragment>
{__('Share usage data with LBRY inc.')}{' '}
<Button button="link" href="https://lbry.com/faq/privacy-and-data" label={__('Learn More')} />
{!localShareUsageData && <span className="error__text"> ({__('Required')})</span>}
</React.Fragment>
}
/>
)}
<div className="section__actions">
<Button
button="primary"
type="submit"
label={__('Join')}
disabled={
!email || !password || !valid || (!IS_WEB && !localShareUsageData && !shareUsageData) || isPending
}
/>
<Button button="link" onClick={handleChangeToSignIn} label={__('Sign In')} />
</div>
<p className="help">
<I18nMessage
tokens={{
terms: (
<Button button="link" href="https://www.lbry.com/termsofservice" label={__('Terms of Service')} />
),
}}
>
By continuing, I agree to the %terms% and confirm I am over the age of 13.
</I18nMessage>
</p>
</Form>
</div>
}
nag={errorMessage && <Nag type="error" relative message={<ErrorText>{errorMessage}</ErrorText>} />}
/>
</div>
);
}

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 })),
});
export default connect(
select,
perform
)(UserEmailVerify);
export default connect(select, perform)(UserEmailVerify);

View file

@ -3,6 +3,7 @@ import * as React from 'react';
import Button from 'component/button';
import UserSignOutButton from 'component/userSignOutButton';
import I18nMessage from 'component/i18nMessage';
import Card from 'component/common/card';
type Props = {
email: string,
@ -24,11 +25,14 @@ class UserEmailVerify extends React.PureComponent<Props> {
}
componentDidMount() {
this.emailVerifyCheckInterval = setInterval(() => this.checkIfVerified(), 5000);
this.emailVerifyCheckInterval = setInterval(() => {
this.checkIfVerified();
}, 5000);
}
componentDidUpdate() {
const { user } = this.props;
if (this.emailVerifyCheckInterval && user && user.has_verified_email) {
clearInterval(this.emailVerifyCheckInterval);
}
@ -57,40 +61,45 @@ class UserEmailVerify extends React.PureComponent<Props> {
const { email, isReturningUser, resendingEmail } = this.props;
return (
<React.Fragment>
<h1 className="section__title--large">{isReturningUser ? __('Check Your Email') : __('Confirm Your Email')}</h1>
<p className="section__subtitle">
{__(
'An email was sent to %email%. Follow the link to %verify_text%. After, this page will update automatically.',
{
email,
verify_text: isReturningUser ? __('sign in') : __('verify your email'),
}
)}
</p>
<div className="section__body section__actions">
<Button
button="primary"
label={__('Resend Email')}
onClick={this.handleResendVerificationEmail}
disabled={resendingEmail}
/>
<UserSignOutButton label={__('Start Over')} />
</div>
<p className="help">
<I18nMessage
tokens={{
help_link: <Button button="link" href="mailto:help@lbry.com" label="help@lbry.com" />,
chat_link: <Button button="link" href="https://chat.lbry.com" label="chat" />,
}}
>
Email %help_link% or join our %chat_link% if you encounter any trouble verifying.
</I18nMessage>
</p>
</React.Fragment>
<div className="main__sign-up">
<Card
title={isReturningUser ? __('Check Your Email') : __('Confirm Your Email')}
subtitle={
<p>
{__(
'An email was sent to %email%. Follow the link to %verify_text%. After, this page will update automatically.',
{
email,
verify_text: isReturningUser ? __('sign in') : __('verify your email'),
}
)}
</p>
}
actions={
<React.Fragment>
<div className="section__actions">
<Button
button="primary"
label={__('Resend Email')}
onClick={this.handleResendVerificationEmail}
disabled={resendingEmail}
/>
<UserSignOutButton label={__('Start Over')} />
</div>
<p className="help">
<I18nMessage
tokens={{
help_link: <Button button="link" href="mailto:help@lbry.com" label="help@lbry.com" />,
chat_link: <Button button="link" href="https://chat.lbry.com" label="chat" />,
}}
>
Email %help_link% or join our %chat_link% if you encounter any trouble verifying.
</I18nMessage>
</p>
</React.Fragment>
}
/>
</div>
);
}
}

View file

@ -4,6 +4,7 @@ import { isNameValid } from 'lbry-redux';
import Button from 'component/button';
import { Form, FormField } from 'component/common/form';
import { INVALID_NAME_ERROR } from 'constants/claim';
import Card from 'component/common/card';
export const DEFAULT_BID_FOR_FIRST_CHANNEL = 0.01;
type Props = {
@ -36,45 +37,51 @@ function UserFirstChannel(props: Props) {
}
return (
<Form onSubmit={handleCreateChannel}>
<h1 className="section__title--large">{__('Create A Channel')}</h1>
<div className="section__subtitle">
<p>{__('A channel is your identity on the LBRY network.')}</p>
<p>{__('You can have more than one or remove this later.')}</p>
</div>
<section className="section__body">
<fieldset-group class="fieldset-group--smushed fieldset-group--disabled-prefix">
<fieldset-section>
<label htmlFor="auth_first_channel">
{createChannelError || nameError ? (
<span className="error-text">{createChannelError || nameError}</span>
) : (
__('Your Channel')
)}
</label>
<div className="form-field__prefix">@</div>
</fieldset-section>
<div className="main__channel-creation">
<Card
title={__('Create A Channel')}
subtitle={
<React.Fragment>
<p>{__('A channel is your identity on the LBRY network.')}</p>
<p>{__('You can have more than one or remove this later.')}</p>
</React.Fragment>
}
actions={
<Form onSubmit={handleCreateChannel}>
<fieldset-group class="fieldset-group--smushed fieldset-group--disabled-prefix">
<fieldset-section>
<label htmlFor="auth_first_channel">
{createChannelError || nameError ? (
<span className="error__text">{createChannelError || nameError}</span>
) : (
__('Your Channel')
)}
</label>
<div className="form-field__prefix">@</div>
</fieldset-section>
<FormField
autoFocus
placeholder={__('channel')}
type="text"
name="auth_first_channel"
className="form-field--short"
value={channel}
onChange={handleChannelChange}
/>
</fieldset-group>
<div className="section__actions">
<Button
button="primary"
type="submit"
disabled={nameError || !channel || creatingChannel || claimingReward}
label={creatingChannel || claimingReward ? __('Creating') : __('Create')}
/>
</div>
</section>
</Form>
<FormField
autoFocus
placeholder={__('channel')}
type="text"
name="auth_first_channel"
className="form-field--short"
value={channel}
onChange={handleChannelChange}
/>
</fieldset-group>
<div className="section__actions">
<Button
button="primary"
type="submit"
disabled={nameError || !channel || creatingChannel || claimingReward}
label={creatingChannel || claimingReward ? __('Creating') : __('Create')}
/>
</div>
</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 {
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 { selectUser, selectUserIsPending, selectEmailToVerify, selectPasswordExists, doUserSignIn } from 'lbryinc';
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),
emailToVerify: selectEmailToVerify(state),
passwordExists: selectPasswordExists(state),
});
const perform = dispatch => ({
fetchUser: () => dispatch(doUserFetch()),
claimReward: () =>
dispatch(
doClaimRewardType(REWARD_TYPES.TYPE_CONFIRM_EMAIL, {
notifyError: false,
})
),
});
export default connect(
select,
perform
)(UserSignIn);
export default connect(select, {
doUserSignIn,
})(UserSignIn);

View file

@ -1,215 +1,58 @@
// @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 UserEmailReturning from 'component/userEmailReturning';
import UserSignInPassword from 'component/userSignInPassword';
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 },
history: { push: string => void },
location: { search: string },
youtubeChannels: Array<any>,
syncEnabled: boolean,
hasSynced: boolean,
syncingWallet: boolean,
getSyncError: ?string,
creatingChannel: boolean,
userFetchPending: boolean,
doUserSignIn: string => void,
emailToVerify: ?string,
passwordExists: 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 { user, location, history, doUserSignIn, userFetchPending, emailToVerify, passwordExists } = 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 [emailOnlyLogin, setEmailOnlyLogin] = React.useState(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 = !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);
const redirect = urlParams.get('redirect');
const showLoading = userFetchPending;
const showEmail = !passwordExists || emailOnlyLogin;
const showPassword = !showEmail && emailToVerify && passwordExists;
React.useEffect(() => {
fetchUser();
}, [fetchUser]);
if (hasVerifiedEmail || (!showEmail && !showPassword && !showLoading)) {
history.push(redirect || '/');
}
}, [showEmail, showPassword, showLoading, hasVerifiedEmail]);
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();
if (emailToVerify && emailOnlyLogin) {
doUserSignIn(emailToVerify);
}
}, [hasVerifiedEmail, claimReward, hasClaimedEmailAward, hasFetchedReward, syncEnabled, hasSynced, balance]);
}, [emailToVerify, emailOnlyLogin, doUserSignIn]);
// 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>;
return (
<section>
{(showEmail || showPassword) && (
<div>
{showEmail && <UserEmailReturning />}
{showPassword && <UserSignInPassword onHandleEmailOnly={() => setEmailOnlyLogin(true)} />}
</div>
)}
{!showEmail && !showPassword && showLoading && (
<div className="main--empty">
<Spinner delayed />
</div>
)}
</section>
);
}
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 { doSignOut } from 'redux/actions/app';
import { doClearEmailEntry, doClearPasswordEntry } from 'lbryinc';
import UserSignOutButton from './view';
const select = state => ({});
const perform = dispatch => ({
signOut: () => dispatch(doSignOut()),
});
export default connect(
select,
perform
)(UserSignOutButton);
export default connect(select, {
doSignOut,
doClearEmailEntry,
doClearPasswordEntry,
})(UserSignOutButton);

View file

@ -5,13 +5,25 @@ import Button from 'component/button';
type Props = {
button: string,
label?: string,
signOut: () => void,
doSignOut: () => void,
doClearEmailEntry: () => void,
doClearPasswordEntry: () => void,
};
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;

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 Button from 'component/button';
import { Form } from 'component/common/form';
import Card from 'component/common/card';
type Props = {
subscribedChannels: Array<Subscription>,
@ -16,36 +17,40 @@ function UserChannelFollowIntro(props: Props) {
const followingCount = (followedTags && followedTags.length) || 0;
return (
<React.Fragment>
<h1 className="section__title--large">{__('Tag Selection')}</h1>
<p className="section__subtitle">{__('Select some tags to help us show you interesting things.')}</p>
<Form onSubmit={onContinue} className="section__body">
<div className="card__actions">
<Button
button="primary"
type="Submit"
onClick={onContinue}
label={__('Continue')}
disabled={followedTags.length < 1}
/>
</div>
</Form>
<div className="section__body">
<TagsSelect hideHeader limitShow={300} help={false} showClose={false} title={__('Follow New Tags')} />
{followingCount > 0 && (
<Nag
type="helpful"
message={
followingCount === 1
? __('You are currently following %followingCount% tag', { followingCount })
: __('You are currently following %followingCount% tags', { followingCount })
}
actionText={__('Continue')}
onClick={onContinue}
/>
)}
</div>
</React.Fragment>
<Card
title={__('Tag Selection')}
subtitle={__('Select some tags to help us show you interesting things.')}
actions={
<React.Fragment>
<Form onSubmit={onContinue}>
<div className="section__actions">
<Button
button="primary"
type="Submit"
onClick={onContinue}
label={__('Continue')}
disabled={followedTags.length < 1}
/>
</div>
</Form>
<div className="section__body">
<TagsSelect hideHeader limitShow={300} help={false} showClose={false} title={__('Follow New Tags')} />
{followingCount > 0 && (
<Nag
type="helpful"
message={
followingCount === 1
? __('You are currently following %followingCount% tag', { followingCount })
: __('You are currently following %followingCount% tags', { followingCount })
}
actionText={__('Continue')}
onClick={onContinue}
/>
)}
</div>
</React.Fragment>
}
/>
);
}

View file

@ -92,7 +92,7 @@ class UserVerify extends React.PureComponent<Props> {
)}
actions={
<Fragment>
{errorMessage && <p className="error-text">{errorMessage}</p>}
{errorMessage && <p className="error__text">{errorMessage}</p>}
<CardVerify
label={__('Verify Card')}
disabled={isPending}

View file

@ -90,7 +90,7 @@ class WalletSend extends React.PureComponent<Props> {
}
/>
{!!Object.keys(errors).length || (
<span className="error-text">
<span className="error__text">
{(!!values.address && touched.address && errors.address) ||
(!!values.amount && touched.amount && errors.amount) ||
(parseFloat(values.amount) === balance &&

View file

@ -84,6 +84,7 @@ export const EYE = 'Eye';
export const EYE_OFF = 'EyeOff';
export const SIGN_OUT = 'SignOut';
export const SIGN_IN = 'SignIn';
export const SIGN_UP = 'Key';
export const TRENDING = 'Trending';
export const TOP = 'Top';
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_PASSWORD_SET = 'reset';
exports.BACKUP = 'backup';
exports.CHANNEL = 'channel';
exports.DISCOVER = 'discover';

View file

@ -55,7 +55,7 @@ function ModalRemoveFile(props: Props) {
onChange={() => setAbandonChecked(!abandonChecked)}
/>
{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' */}

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

View file

@ -168,7 +168,7 @@ class ModalWalletEncrypt extends React.PureComponent<Props, State> {
name="wallet-understand"
onChange={event => this.onChangeUnderstandConfirm(event)}
/>
{failMessage && <div className="error-text">{__(failMessage)}</div>}
{failMessage && <div className="error__text">{__(failMessage)}</div>}
</Form>
<div className="card__actions">
<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 (
<section className="card card--section">
<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" /> }}>
Rewards are currently disabled for your account. Turn on diagnostic data sharing, in %settings%, to
re-enable them.

View file

@ -18,6 +18,7 @@ import { SETTINGS } from 'lbry-redux';
import Card from 'component/common/card';
import { getPasswordFromCookie } from 'util/saved-passwords';
import Spinner from 'component/spinner';
import SettingAccountPassword from 'component/settingAccountPassword';
// @if TARGET='app'
export const IS_MAC = process.platform === 'darwin';
@ -261,11 +262,12 @@ class SettingsPage extends React.PureComponent<Props, State> {
<Page className="card-stack">
{!IS_WEB && noDaemonSettings ? (
<section className="card card--section">
<div className="card__title card__title--deprecated">{__('Failed to load settings.')}</div>
<div className="card__title card__t itle--deprecated">{__('Failed to load settings.')}</div>
</section>
) : (
<div>
<Card title={__('Language')} actions={<SettingLanguage />} />
{isAuthenticated && <SettingAccountPassword />}
{/* @if TARGET='app' */}
<Card
title={__('Sync')}

View file

@ -1,10 +1,3 @@
import { connect } from 'react-redux';
import SignUpPage from './view';
import SignInPage from './view';
const select = () => ({});
const perform = () => ({});
export default connect(
select,
perform
)(SignUpPage);
export default SignInPage;

View file

@ -1,4 +1,5 @@
// @flow
import * as PAGES from 'constants/pages';
import React, { useState } from 'react';
import { withRouter } from 'react-router';
import Page from 'component/page';
@ -6,7 +7,7 @@ import ReCAPTCHA from 'react-google-recaptcha';
import Button from 'component/button';
import { Lbryio } from 'lbryinc';
import I18nMessage from 'component/i18nMessage';
import * as PAGES from 'constants/pages';
import Card from 'component/common/card';
type Props = {
history: { push: string => void, location: { search: string } },
@ -88,40 +89,49 @@ function SignInVerifyPage(props: Props) {
return (
<Page authPage className="main--auth-page">
<section className="main--contained">
<h1 className="section__title--large">
{isAuthenticationSuccess ? __('Sign In Success!') : __('Sign In to lbry.tv')}
</h1>
<p className="section__subtitle">
{isAuthenticationSuccess
? __('You can now close this tab.')
: needsRecaptcha
? __('Click below to sign in to lbry.tv')
: __('Welcome back! You are automatically being signed in.')}
</p>
<p className="section__subtitle">
{showCaptchaMessage && !isAuthenticationSuccess && (
<I18nMessage
tokens={{
refresh: <Button button="link" label={__('refreshing')} onClick={() => window.location.reload()} />,
}}
>
Not seeing a captcha? Check your ad blocker or try %refresh%.
</I18nMessage>
)}
</p>
{!isAuthenticationSuccess && needsRecaptcha && (
<div className="section__actions">
<ReCAPTCHA
sitekey="6LePsJgUAAAAAFTuWOKRLnyoNKhm0HA4C3elrFMG"
onChange={onCaptchaChange}
asyncScriptOnLoad={onCaptchaReady}
onExpired={onAuthError}
onErrored={onAuthError}
/>
</div>
)}
</section>
<div className="main__sign-up">
<Card
title={isAuthenticationSuccess ? __('Sign In Success!') : __('Sign In to lbry.tv')}
subtitle={
<React.Fragment>
<p>
{isAuthenticationSuccess
? __('You can now close this tab.')
: needsRecaptcha
? __('Click below to sign in to lbry.tv')
: __('Welcome back! You are automatically being signed in.')}
</p>
{showCaptchaMessage && !isAuthenticationSuccess && (
<p>
<I18nMessage
tokens={{
refresh: (
<Button button="link" label={__('refreshing')} onClick={() => window.location.reload()} />
),
}}
>
Not seeing a captcha? Check your ad blocker or try %refresh%.
</I18nMessage>
</p>
)}
</React.Fragment>
}
actions={
!isAuthenticationSuccess &&
needsRecaptcha && (
<div className="section__actions">
<ReCAPTCHA
sitekey="6LePsJgUAAAAAFTuWOKRLnyoNKhm0HA4C3elrFMG"
onChange={onCaptchaChange}
asyncScriptOnLoad={onCaptchaReady}
onExpired={onAuthError}
onErrored={onAuthError}
/>
</div>
)
}
/>
</div>
</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__label {
white-space: normal;

View file

@ -138,6 +138,13 @@
& > *:not(:last-child) {
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 {
@ -172,19 +179,6 @@
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 {
color: var(--color-text-subtitle);
margin: var(--spacing-small) 0;
@ -245,3 +239,19 @@
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);
}
.channel-thumbnail {
width: 6rem;
height: 6rem;
}
&:hover {
.claim-preview__hover-actions {
display: block;

View file

@ -181,3 +181,13 @@
margin-top: 0;
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 {
max-width: 60rem;
max-width: 70rem;
margin-top: var(--spacing-main-padding);
margin-left: auto;
margin-right: auto;
@ -61,11 +61,10 @@
.main--contained {
margin: auto;
margin-top: 2rem;
display: flex;
flex-direction: column;
align-items: flex-start;
max-width: 40rem;
max-width: 50rem;
text-align: left;
& > * {
@ -76,3 +75,16 @@
.main--full-width {
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,7 +1,9 @@
.ReactModal__Body--open {
#app {
height: 100vh;
overflow-y: hidden;
@media (max-width: $breakpoint-small) {
height: 100vh;
overflow-y: hidden;
}
}
}

View file

@ -32,6 +32,10 @@ $nag-error-z-index: 100001;
z-index: 1 !important; /* booooooo */
}
.nag--relative {
position: relative;
}
.nag--helpful {
background-color: var(--color-secondary);
color: var(--color-white);

View file

@ -1,9 +1,8 @@
.section {
position: relative;
margin-top: var(--spacing-large);
&:first-of-type {
margin-top: 0;
~ .section {
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 {
@extend .section__subtitle;
padding: var(--spacing-small);
@ -82,6 +87,42 @@
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 {
@extend .section__actions;
justify-content: center;
@ -94,13 +135,3 @@
.section__actions--no-margin {
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;
font-size: var(--font-small);
color: var(--color-text-help);
margin-top: var(--spacing-miniscule);
margin-top: var(--spacing-small);
margin-bottom: var(--spacing-small);
}
@ -238,13 +238,13 @@ img {
}
}
.error-wrapper {
.error__wrapper {
background-color: var(--color-error);
padding: var(--spacing-small);
border-radius: var(--border-radius);
}
.error-text {
.error__text {
color: var(--color-text-error);
}

View file

@ -6,7 +6,7 @@
--color-header-button: #f7f7f7;
// Color
--color-background: #f7f7f7;
--color-background: #f9f9f9;
--color-background--splash: #212529;
--color-border: #ededed;
--color-background-overlay: #21252980;

View file

@ -2,7 +2,8 @@ const { DOMAIN } = require('../../config.js');
const AUTH_TOKEN = 'auth_token';
const 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 maxExpiration = 2147483647;
let sessionPassword;

View file

@ -819,10 +819,10 @@
prop-types "^15.6.2"
scheduler "^0.18.0"
"@lbry/components@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@lbry/components/-/components-4.0.1.tgz#8dcf7348920383d854c0db640faaf1ac5a72f7ef"
integrity sha512-vY84ziZ9EaXoezDBK2VsajvXcSPXDV0fr1VWn2w0iHkGa756RWvNySpnqaKMZH+myK12mvNNc/NkGIW5oO7+5w==
"@lbry/components@^4.1.2":
version "4.1.2"
resolved "https://registry.yarnpkg.com/@lbry/components/-/components-4.1.2.tgz#18eda2f1a73a6241e9a96594ccab8fffb9ef3ae9"
integrity sha512-GJx4BTdEtOlm5/JsKUVXBzYeepXTbZ4GjGFrKxzXh6jWw40aFOh8OrHwYM/LHRl1fuKRmB5xpZNWsjw6EKyEeQ==
"@mapbox/hast-util-table-cell-style@^0.1.3":
version "0.1.3"
@ -6147,9 +6147,9 @@ lbry-redux@lbryio/lbry-redux#1097a63d44a20b87e443fbaa48f95fe3ea5e3f70:
reselect "^3.0.0"
uuid "^3.3.2"
lbryinc@lbryio/lbryinc#0addc624db54000b0447f4539f91f5758d26eef3:
lbryinc@lbryio/lbryinc#12aefaa14343d2f3eac01f2683701f58e53f1848:
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:
reselect "^3.0.0"