new signin/signup #3960
73 changed files with 1566 additions and 652 deletions
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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%"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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>}
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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,9 +27,11 @@ 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>
|
||||
{(href || onClick) && (
|
||||
<Button
|
||||
className={classnames('nag__button', {
|
||||
'nag__button--helpful': type === 'helpful',
|
||||
|
@ -38,6 +41,7 @@ export default function Nag(props: Props) {
|
|||
>
|
||||
{actionText}
|
||||
</Button>
|
||||
)}
|
||||
{onClose && (
|
||||
<Button
|
||||
className={classnames('nag__button nag__close', {
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
) : (
|
||||
<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>
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'}
|
||||
|
|
22
ui/component/settingAccountPassword/index.js
Normal file
22
ui/component/settingAccountPassword/index.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
selectUser,
|
||||
selectPasswordSetSuccess,
|
||||
selectPasswordSetError,
|
||||
doUserPasswordSet,
|
||||
doClearPasswordEntry,
|
||||
} from 'lbryinc';
|
||||
import { doToast } from 'lbry-redux';
|
||||
import UserSignIn from './view';
|
||||
|
||||
const select = state => ({
|
||||
user: selectUser(state),
|
||||
passwordSetSuccess: selectPasswordSetSuccess(state),
|
||||
passwordSetError: selectPasswordSetError(state),
|
||||
});
|
||||
|
||||
export default connect(select, {
|
||||
doUserPasswordSet,
|
||||
doToast,
|
||||
doClearPasswordEntry,
|
||||
})(UserSignIn);
|
88
ui/component/settingAccountPassword/view.jsx
Normal file
88
ui/component/settingAccountPassword/view.jsx
Normal file
|
@ -0,0 +1,88 @@
|
|||
// @flow
|
||||
import React, { useState } from 'react';
|
||||
import { FormField, Form } from 'component/common/form';
|
||||
import Button from 'component/button';
|
||||
import ErrorText from 'component/common/error-text';
|
||||
import Card from 'component/common/card';
|
||||
|
||||
type Props = {
|
||||
user: ?User,
|
||||
doToast: ({ message: string }) => void,
|
||||
doUserPasswordSet: (string, ?string) => void,
|
||||
doClearPasswordEntry: () => void,
|
||||
passwordSetSuccess: boolean,
|
||||
passwordSetError: ?string,
|
||||
};
|
||||
|
||||
export default function SettingAccountPassword(props: Props) {
|
||||
const { user, doToast, doUserPasswordSet, passwordSetSuccess, passwordSetError, doClearPasswordEntry } = props;
|
||||
const [oldPassword, setOldPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [isAddingPassword, setIsAddingPassword] = useState(false);
|
||||
const hasPassword = user && user.password_set;
|
||||
|
||||
function handleSubmit() {
|
||||
doUserPasswordSet(newPassword, oldPassword);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (passwordSetSuccess) {
|
||||
setIsAddingPassword(false);
|
||||
doToast({
|
||||
message: __('Password updated successfully.'),
|
||||
});
|
||||
doClearPasswordEntry();
|
||||
setOldPassword('');
|
||||
setNewPassword('');
|
||||
}
|
||||
}, [passwordSetSuccess, setOldPassword, setNewPassword, doClearPasswordEntry, doToast]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={__('Account Password')}
|
||||
subtitle={hasPassword ? __('') : __('You do not currently have a password set.')}
|
||||
actions={
|
||||
isAddingPassword ? (
|
||||
<div>
|
||||
<Form onSubmit={handleSubmit} className="section">
|
||||
{hasPassword && (
|
||||
<FormField
|
||||
type="password"
|
||||
name="setting_set_old_password"
|
||||
label={__('Old Password')}
|
||||
value={oldPassword}
|
||||
onChange={e => setOldPassword(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
<FormField
|
||||
type="password"
|
||||
name="setting_set_new_password"
|
||||
label={__('New Password')}
|
||||
value={newPassword}
|
||||
onChange={e => setNewPassword(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="section__actions">
|
||||
<Button button="primary" type="submit" label={__('Set Password')} disabled={!newPassword} />
|
||||
{!hasPassword && (
|
||||
<Button button="link" label={__('Cancel')} onClick={() => setIsAddingPassword(false)} />
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
{passwordSetError && (
|
||||
<div className="section">
|
||||
<ErrorText>{passwordSetError}</ErrorText>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
button="primary"
|
||||
label={hasPassword ? __('Update Your Password') : __('Add A Password')}
|
||||
onClick={() => setIsAddingPassword(true)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -90,7 +90,12 @@ function SideNavigation(props: Props) {
|
|||
<ul className="navigation-links">
|
||||
{[
|
||||
{
|
||||
...(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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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')} />
|
||||
</>
|
||||
) : (
|
||||
|
|
|
@ -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,15 +28,14 @@ function UserChannelFollowIntro(props: Props) {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h1 className="section__title--large">{__('Find Channels to Follow')}</h1>
|
||||
<p className="section__subtitle">
|
||||
{__(
|
||||
<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.'
|
||||
)}
|
||||
</p>
|
||||
<Form onSubmit={onContinue} className="section__body">
|
||||
<div className="card__actions">
|
||||
actions={
|
||||
<React.Fragment>
|
||||
<div className="section__actions">
|
||||
<Button button="secondary" onClick={onBack} label={__('Back')} />
|
||||
<Button
|
||||
button="primary"
|
||||
|
@ -46,7 +45,6 @@ function UserChannelFollowIntro(props: Props) {
|
|||
disabled={subscribedChannels.length < 2}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
<div className="section__body">
|
||||
<ClaimListDiscover
|
||||
defaultOrderBy={CS.ORDER_BY_TOP}
|
||||
|
@ -68,6 +66,8 @@ function UserChannelFollowIntro(props: Props) {
|
|||
)}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,4 @@ const perform = dispatch => ({
|
|||
fetchAccessToken: () => dispatch(doFetchAccessToken()),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(UserEmailVerify);
|
||||
export default connect(select, perform)(UserEmailVerify);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,35 +52,64 @@ 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">
|
||||
<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={newEmail}
|
||||
error={errorMessage}
|
||||
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
|
||||
|
@ -86,41 +136,39 @@ function UserEmailNew(props: Props) {
|
|||
<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>}
|
||||
{!localShareUsageData && <span className="error__text"> ({__('Required')})</span>}
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<div className="card__actions">
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
button="primary"
|
||||
type="submit"
|
||||
label={__('Continue')}
|
||||
disabled={!newEmail || !valid || (!IS_WEB && (!localShareUsageData && !shareUsageData)) || isPending}
|
||||
label={__('Join')}
|
||||
disabled={
|
||||
!email || !password || !valid || (!IS_WEB && !localShareUsageData && !shareUsageData) || isPending
|
||||
}
|
||||
/>
|
||||
<Button button="link" onClick={handleChangeToSignIn} label={__('Sign In')} />
|
||||
</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')}
|
||||
/>
|
||||
<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>
|
||||
</React.Fragment>
|
||||
</p>
|
||||
{/* @endif */}
|
||||
</React.Fragment>
|
||||
</Form>
|
||||
</div>
|
||||
}
|
||||
nag={errorMessage && <Nag type="error" relative message={<ErrorText>{errorMessage}</ErrorText>} />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
22
ui/component/userEmailReturning/index.js
Normal file
22
ui/component/userEmailReturning/index.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
selectEmailNewErrorMessage,
|
||||
selectEmailToVerify,
|
||||
doUserCheckIfEmailExists,
|
||||
doClearEmailEntry,
|
||||
selectEmailDoesNotExist,
|
||||
selectEmailAlreadyExists,
|
||||
} from 'lbryinc';
|
||||
import UserEmailReturning from './view';
|
||||
|
||||
const select = state => ({
|
||||
errorMessage: selectEmailNewErrorMessage(state),
|
||||
emailToVerify: selectEmailToVerify(state),
|
||||
emailDoesNotExist: selectEmailDoesNotExist(state),
|
||||
emailExists: selectEmailAlreadyExists(state),
|
||||
});
|
||||
|
||||
export default connect(select, {
|
||||
doUserCheckIfEmailExists,
|
||||
doClearEmailEntry,
|
||||
})(UserEmailReturning);
|
111
ui/component/userEmailReturning/view.jsx
Normal file
111
ui/component/userEmailReturning/view.jsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
// @flow
|
||||
import * as PAGES from 'constants/pages';
|
||||
import React, { useState } from 'react';
|
||||
import { FormField, Form } from 'component/common/form';
|
||||
import Button from 'component/button';
|
||||
import { EMAIL_REGEX } from 'constants/email';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import UserEmailVerify from 'component/userEmailVerify';
|
||||
import Card from 'component/common/card';
|
||||
import Nag from 'component/common/nag';
|
||||
import analytics from 'analytics';
|
||||
|
||||
type Props = {
|
||||
errorMessage: ?string,
|
||||
emailToVerify: ?string,
|
||||
emailDoesNotExist: boolean,
|
||||
doClearEmailEntry: () => void,
|
||||
doUserSignIn: (string, ?string) => void,
|
||||
doUserCheckIfEmailExists: string => void,
|
||||
};
|
||||
|
||||
function UserEmailReturning(props: Props) {
|
||||
const { errorMessage, doUserCheckIfEmailExists, emailToVerify, doClearEmailEntry, emailDoesNotExist } = props;
|
||||
const { push, location } = useHistory();
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
const emailFromUrl = urlParams.get('email');
|
||||
const emailExistsFromUrl = urlParams.get('email_exists');
|
||||
const defaultEmail = emailFromUrl ? decodeURIComponent(emailFromUrl) : '';
|
||||
const [email, setEmail] = useState(defaultEmail);
|
||||
const valid = email.match(EMAIL_REGEX);
|
||||
const showEmailVerification = emailToVerify;
|
||||
|
||||
function handleSubmit() {
|
||||
doUserCheckIfEmailExists(email);
|
||||
analytics.emailProvidedEvent();
|
||||
}
|
||||
|
||||
function handleChangeToSignIn() {
|
||||
doClearEmailEntry();
|
||||
let url = `/$/${PAGES.AUTH}`;
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
|
||||
urlParams.delete('email_exists');
|
||||
urlParams.delete('email');
|
||||
if (email) {
|
||||
urlParams.set('email', encodeURIComponent(email));
|
||||
}
|
||||
|
||||
push(`${url}?${urlParams.toString()}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="main__sign-in">
|
||||
{showEmailVerification ? (
|
||||
<UserEmailVerify />
|
||||
) : (
|
||||
<Card
|
||||
title={__('Sign In to lbry.tv')}
|
||||
actions={
|
||||
<div>
|
||||
<Form onSubmit={handleSubmit} className="section">
|
||||
<FormField
|
||||
autoFocus={!emailExistsFromUrl}
|
||||
placeholder={__('hotstuff_96@hotmail.com')}
|
||||
type="email"
|
||||
id="username"
|
||||
autoComplete="on"
|
||||
name="sign_in_email"
|
||||
label={__('Email')}
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
autoFocus={emailExistsFromUrl}
|
||||
button="primary"
|
||||
type="submit"
|
||||
label={__('Sign In')}
|
||||
disabled={!email || !valid}
|
||||
/>
|
||||
<Button button="link" onClick={handleChangeToSignIn} label={__('Join')} />
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
}
|
||||
nag={
|
||||
<React.Fragment>
|
||||
{!emailDoesNotExist && emailExistsFromUrl && (
|
||||
<Nag type="helpful" relative message={__('That email is already in use. Did you mean to sign in?')} />
|
||||
)}
|
||||
{emailDoesNotExist && (
|
||||
<Nag
|
||||
type="helpful"
|
||||
relative
|
||||
message={__("We can't find that email. Did you mean to join?")}
|
||||
actionText={__('Join')}
|
||||
/>
|
||||
)}
|
||||
{!emailExistsFromUrl && !emailDoesNotExist && errorMessage && (
|
||||
<Nag type="error" relative message={errorMessage} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserEmailReturning;
|
|
@ -23,7 +23,4 @@ const perform = dispatch => ({
|
|||
toast: message => dispatch(doToast({ message })),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(UserEmailVerify);
|
||||
export default connect(select, perform)(UserEmailVerify);
|
||||
|
|
|
@ -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,10 +61,11 @@ 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">
|
||||
<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.',
|
||||
{
|
||||
|
@ -69,8 +74,10 @@ class UserEmailVerify extends React.PureComponent<Props> {
|
|||
}
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className="section__body section__actions">
|
||||
}
|
||||
actions={
|
||||
<React.Fragment>
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
button="primary"
|
||||
label={__('Resend Email')}
|
||||
|
@ -79,7 +86,6 @@ class UserEmailVerify extends React.PureComponent<Props> {
|
|||
/>
|
||||
<UserSignOutButton label={__('Start Over')} />
|
||||
</div>
|
||||
|
||||
<p className="help">
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
|
@ -91,6 +97,9 @@ class UserEmailVerify extends React.PureComponent<Props> {
|
|||
</I18nMessage>
|
||||
</p>
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,18 +37,22 @@ function UserFirstChannel(props: Props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleCreateChannel}>
|
||||
<h1 className="section__title--large">{__('Create A Channel')}</h1>
|
||||
<div className="section__subtitle">
|
||||
<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>
|
||||
</div>
|
||||
<section className="section__body">
|
||||
</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>
|
||||
<span className="error__text">{createChannelError || nameError}</span>
|
||||
) : (
|
||||
__('Your Channel')
|
||||
)}
|
||||
|
@ -73,8 +78,10 @@ function UserFirstChannel(props: Props) {
|
|||
label={creatingChannel || claimingReward ? __('Creating') : __('Create')}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</Form>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
26
ui/component/userPasswordReset/index.js
Normal file
26
ui/component/userPasswordReset/index.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doUserPasswordReset,
|
||||
selectPasswordResetSuccess,
|
||||
selectPasswordResetIsPending,
|
||||
selectPasswordResetError,
|
||||
doClearPasswordEntry,
|
||||
doClearEmailEntry,
|
||||
selectEmailToVerify,
|
||||
} from 'lbryinc';
|
||||
import { doToast } from 'lbry-redux';
|
||||
import UserSignIn from './view';
|
||||
|
||||
const select = state => ({
|
||||
passwordResetSuccess: selectPasswordResetSuccess(state),
|
||||
passwordResetIsPending: selectPasswordResetIsPending(state),
|
||||
passwordResetError: selectPasswordResetError(state),
|
||||
emailToVerify: selectEmailToVerify(state),
|
||||
});
|
||||
|
||||
export default connect(select, {
|
||||
doUserPasswordReset,
|
||||
doToast,
|
||||
doClearPasswordEntry,
|
||||
doClearEmailEntry,
|
||||
})(UserSignIn);
|
107
ui/component/userPasswordReset/view.jsx
Normal file
107
ui/component/userPasswordReset/view.jsx
Normal file
|
@ -0,0 +1,107 @@
|
|||
// @flow
|
||||
import * as PAGES from 'constants/pages';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import Card from 'component/common/card';
|
||||
import Spinner from 'component/spinner';
|
||||
import { Form, FormField } from 'component/common/form';
|
||||
import { EMAIL_REGEX } from 'constants/email';
|
||||
import ErrorText from 'component/common/error-text';
|
||||
import Button from 'component/button';
|
||||
import Nag from 'component/common/nag';
|
||||
|
||||
type Props = {
|
||||
user: ?User,
|
||||
doToast: ({ message: string }) => void,
|
||||
doUserPasswordReset: string => void,
|
||||
doClearPasswordEntry: () => void,
|
||||
doClearEmailEntry: () => void,
|
||||
passwordResetPending: boolean,
|
||||
passwordResetSuccess: boolean,
|
||||
passwordResetError: ?string,
|
||||
emailToVerify: ?string,
|
||||
};
|
||||
|
||||
function UserPasswordReset(props: Props) {
|
||||
const {
|
||||
doUserPasswordReset,
|
||||
passwordResetPending,
|
||||
passwordResetError,
|
||||
passwordResetSuccess,
|
||||
doToast,
|
||||
doClearPasswordEntry,
|
||||
doClearEmailEntry,
|
||||
emailToVerify,
|
||||
} = props;
|
||||
const { push } = useHistory();
|
||||
const [email, setEmail] = React.useState(emailToVerify || '');
|
||||
const valid = email.match(EMAIL_REGEX);
|
||||
|
||||
function handleSubmit() {
|
||||
if (email) {
|
||||
doUserPasswordReset(email);
|
||||
}
|
||||
}
|
||||
|
||||
function handleRestart() {
|
||||
setEmail('');
|
||||
doClearPasswordEntry();
|
||||
doClearEmailEntry();
|
||||
push(`/$/${PAGES.AUTH_SIGNIN}`);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (passwordResetSuccess) {
|
||||
doToast({
|
||||
message: __('Email sent!'),
|
||||
});
|
||||
}
|
||||
}, [passwordResetSuccess, doToast]);
|
||||
|
||||
return (
|
||||
<section className="main__sign-in">
|
||||
<Card
|
||||
title={__('Reset Your Password')}
|
||||
actions={
|
||||
<div>
|
||||
<Form onSubmit={handleSubmit} className="section">
|
||||
<FormField
|
||||
autoFocus
|
||||
disabled={passwordResetSuccess}
|
||||
placeholder={__('hotstuff_96@hotmail.com')}
|
||||
type="email"
|
||||
name="sign_in_email"
|
||||
id="username"
|
||||
autoComplete="on"
|
||||
label={__('Email')}
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
button="primary"
|
||||
type="submit"
|
||||
label={passwordResetPending ? __('Resetting') : __('Reset Password')}
|
||||
disabled={!email || !valid || passwordResetPending || passwordResetSuccess}
|
||||
/>
|
||||
<Button button="link" label={__('Cancel')} onClick={handleRestart} />
|
||||
{passwordResetPending && <Spinner type="small" />}
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
}
|
||||
nag={
|
||||
<React.Fragment>
|
||||
{passwordResetError && <Nag type="error" relative message={<ErrorText>{passwordResetError}</ErrorText>} />}
|
||||
{passwordResetSuccess && (
|
||||
<Nag type="helpful" relative message={__('Check your email for a link to reset your password.')} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserPasswordReset;
|
16
ui/component/userPasswordSet/index.js
Normal file
16
ui/component/userPasswordSet/index.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doClearEmailEntry, doUserFetch } from 'lbryinc';
|
||||
import { doToast } from 'lbry-redux';
|
||||
import UserSignIn from './view';
|
||||
|
||||
const select = state => ({
|
||||
// passwordSetSuccess: selectPasswordSetSuccess(state),
|
||||
// passwordSetIsPending: selectPasswordSetIsPending(state),
|
||||
// passwordSetError: selectPasswordSetError(state),
|
||||
});
|
||||
|
||||
export default connect(select, {
|
||||
doToast,
|
||||
doClearEmailEntry,
|
||||
doUserFetch,
|
||||
})(UserSignIn);
|
108
ui/component/userPasswordSet/view.jsx
Normal file
108
ui/component/userPasswordSet/view.jsx
Normal file
|
@ -0,0 +1,108 @@
|
|||
// @flow
|
||||
import * as PAGES from 'constants/pages';
|
||||
import React from 'react';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import { useHistory } from 'react-router';
|
||||
import Card from 'component/common/card';
|
||||
import { Form, FormField } from 'component/common/form';
|
||||
import ErrorText from 'component/common/error-text';
|
||||
import Button from 'component/button';
|
||||
import Nag from 'component/common/nag';
|
||||
import Spinner from 'component/spinner';
|
||||
|
||||
type Props = {
|
||||
user: ?User,
|
||||
doClearEmailEntry: () => void,
|
||||
doUserFetch: () => void,
|
||||
doToast: ({ message: string }) => void,
|
||||
history: { push: string => void },
|
||||
location: { search: string },
|
||||
passwordSetPending: boolean,
|
||||
passwordSetError: ?string,
|
||||
};
|
||||
|
||||
function UserPasswordReset(props: Props) {
|
||||
const { doClearEmailEntry, doToast, doUserFetch } = props;
|
||||
const { location, push } = useHistory();
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
const email = urlParams.get('email');
|
||||
const authToken = urlParams.get('auth_token');
|
||||
const verificationToken = urlParams.get('verification_token');
|
||||
const [password, setPassword] = React.useState('');
|
||||
const [error, setError] = React.useState();
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
function handleSubmit() {
|
||||
setLoading(true);
|
||||
|
||||
Lbryio.call('user_email', 'confirm', {
|
||||
email: email,
|
||||
verification_token: verificationToken,
|
||||
})
|
||||
.then(() =>
|
||||
Lbryio.call(
|
||||
'user_password',
|
||||
'set',
|
||||
{
|
||||
auth_token: authToken,
|
||||
new_password: password,
|
||||
},
|
||||
'post'
|
||||
)
|
||||
)
|
||||
.then(doUserFetch)
|
||||
.then(() => {
|
||||
setLoading(false);
|
||||
doToast({
|
||||
message: __('Password successfully changed!'),
|
||||
});
|
||||
push(`/`);
|
||||
})
|
||||
.catch(error => {
|
||||
setLoading(false);
|
||||
setError(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
function handleRestart() {
|
||||
doClearEmailEntry();
|
||||
push(`/$/${PAGES.AUTH_SIGNIN}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="main__sign-in">
|
||||
<Card
|
||||
title={__('Choose A New Password')}
|
||||
subtitle={__('Setting a new password for %email%', { email })}
|
||||
actions={
|
||||
<div>
|
||||
<Form onSubmit={handleSubmit} className="section">
|
||||
<FormField
|
||||
autoFocus
|
||||
type="password"
|
||||
name="password_set"
|
||||
label={__('New Password')}
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
button="primary"
|
||||
type="submit"
|
||||
label={loading ? __('Update Password') : __('Updating Password')}
|
||||
disabled={!password || loading}
|
||||
/>
|
||||
<Button button="link" label={__('Cancel')} onClick={handleRestart} />
|
||||
{loading && <Spinner type="small" />}
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
}
|
||||
nag={error && <Nag type="error" relative message={<ErrorText>{error}</ErrorText>} />}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserPasswordReset;
|
|
@ -1,55 +1,14 @@
|
|||
import * as SETTINGS from 'constants/settings';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
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);
|
||||
|
|
|
@ -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 (emailToVerify && emailOnlyLogin) {
|
||||
doUserSignIn(emailToVerify);
|
||||
}
|
||||
}, [emailToVerify, emailOnlyLogin, doUserSignIn]);
|
||||
|
||||
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 && (
|
||||
return (
|
||||
<section>
|
||||
{(showEmail || showPassword) && (
|
||||
<div>
|
||||
<YoutubeTransferStatus /> <Confetti recycle={false} style={{ position: 'fixed' }} />
|
||||
{showEmail && <UserEmailReturning />}
|
||||
{showPassword && <UserSignInPassword onHandleEmailOnly={() => setEmailOnlyLogin(true)} />}
|
||||
</div>
|
||||
),
|
||||
showSyncPassword && <SyncPassword />,
|
||||
showLoadingSpinner && (
|
||||
)}
|
||||
{!showEmail && !showPassword && showLoading && (
|
||||
<div className="main--empty">
|
||||
<Spinner />
|
||||
<Spinner delayed />
|
||||
</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>;
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(UserSignIn);
|
||||
|
|
22
ui/component/userSignInPassword/index.js
Normal file
22
ui/component/userSignInPassword/index.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
selectUser,
|
||||
selectUserIsPending,
|
||||
selectEmailToVerify,
|
||||
selectEmailNewErrorMessage,
|
||||
doUserSignIn,
|
||||
doClearEmailEntry,
|
||||
} from 'lbryinc';
|
||||
import UserSignIn from './view';
|
||||
|
||||
const select = state => ({
|
||||
user: selectUser(state),
|
||||
userFetchPending: selectUserIsPending(state),
|
||||
emailToVerify: selectEmailToVerify(state),
|
||||
errorMessage: selectEmailNewErrorMessage(state),
|
||||
});
|
||||
|
||||
export default connect(select, {
|
||||
doUserSignIn,
|
||||
doClearEmailEntry,
|
||||
})(UserSignIn);
|
67
ui/component/userSignInPassword/view.jsx
Normal file
67
ui/component/userSignInPassword/view.jsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
// @flow
|
||||
import React, { useState } from 'react';
|
||||
import { FormField, Form } from 'component/common/form';
|
||||
import Button from 'component/button';
|
||||
import Card from 'component/common/card';
|
||||
import analytics from 'analytics';
|
||||
import Nag from 'component/common/nag';
|
||||
import UserPasswordReset from 'component/userPasswordReset';
|
||||
|
||||
type Props = {
|
||||
errorMessage: ?string,
|
||||
emailToVerify: ?string,
|
||||
doClearEmailEntry: () => void,
|
||||
doUserSignIn: (string, ?string) => void,
|
||||
onHandleEmailOnly: () => void,
|
||||
};
|
||||
|
||||
export default function UserSignInPassword(props: Props) {
|
||||
const { errorMessage, doUserSignIn, emailToVerify, onHandleEmailOnly } = props;
|
||||
const [password, setPassword] = useState('');
|
||||
const [forgotPassword, setForgotPassword] = React.useState(false);
|
||||
|
||||
function handleSubmit() {
|
||||
if (emailToVerify) {
|
||||
doUserSignIn(emailToVerify, password);
|
||||
analytics.emailProvidedEvent();
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangeToSignIn() {
|
||||
onHandleEmailOnly();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="main__sign-in">
|
||||
{forgotPassword ? (
|
||||
<UserPasswordReset />
|
||||
) : (
|
||||
<Card
|
||||
title={__('Enter Your lbry.tv Password')}
|
||||
subtitle={__('Signing in as %email%', { email: emailToVerify })}
|
||||
actions={
|
||||
<Form onSubmit={handleSubmit} className="section">
|
||||
<FormField
|
||||
autoFocus
|
||||
type="password"
|
||||
name="sign_in_password"
|
||||
id="password"
|
||||
autoComplete="on"
|
||||
label={__('Password')}
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
helper={<Button button="link" label={__('Forgot Password?')} onClick={() => setForgotPassword(true)} />}
|
||||
/>
|
||||
|
||||
<div className="section__actions">
|
||||
<Button button="primary" type="submit" label={__('Continue')} disabled={!password} />
|
||||
<Button button="link" onClick={handleChangeToSignIn} label={__('Use Magic Link')} />
|
||||
</div>
|
||||
</Form>
|
||||
}
|
||||
nag={errorMessage && <Nag type="error" relative message={errorMessage} />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,14 +1,12 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { 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);
|
||||
|
|
|
@ -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;
|
||||
|
|
52
ui/component/userSignUp/index.js
Normal file
52
ui/component/userSignUp/index.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
import * as SETTINGS from 'constants/settings';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
selectEmailToVerify,
|
||||
selectUser,
|
||||
selectAccessToken,
|
||||
makeSelectIsRewardClaimPending,
|
||||
selectClaimedRewards,
|
||||
rewards as REWARD_TYPES,
|
||||
doClaimRewardType,
|
||||
doUserFetch,
|
||||
selectUserIsPending,
|
||||
selectYoutubeChannels,
|
||||
selectGetSyncIsPending,
|
||||
selectGetSyncErrorMessage,
|
||||
selectSyncHash,
|
||||
} from 'lbryinc';
|
||||
import { selectMyChannelClaims, selectBalance, selectFetchingMyChannels, selectCreatingChannel } from 'lbry-redux';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import UserSignIn from './view';
|
||||
|
||||
const select = state => ({
|
||||
emailToVerify: selectEmailToVerify(state),
|
||||
user: selectUser(state),
|
||||
accessToken: selectAccessToken(state),
|
||||
channels: selectMyChannelClaims(state),
|
||||
claimedRewards: selectClaimedRewards(state),
|
||||
claimingReward: makeSelectIsRewardClaimPending()(state, {
|
||||
reward_type: REWARD_TYPES.TYPE_CONFIRM_EMAIL,
|
||||
}),
|
||||
balance: selectBalance(state),
|
||||
fetchingChannels: selectFetchingMyChannels(state),
|
||||
youtubeChannels: selectYoutubeChannels(state),
|
||||
userFetchPending: selectUserIsPending(state),
|
||||
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
|
||||
syncingWallet: selectGetSyncIsPending(state),
|
||||
getSyncError: selectGetSyncErrorMessage(state),
|
||||
hasSynced: Boolean(selectSyncHash(state)),
|
||||
creatingChannel: selectCreatingChannel(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchUser: () => dispatch(doUserFetch()),
|
||||
claimReward: () =>
|
||||
dispatch(
|
||||
doClaimRewardType(REWARD_TYPES.TYPE_CONFIRM_EMAIL, {
|
||||
notifyError: false,
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(UserSignIn);
|
215
ui/component/userSignUp/view.jsx
Normal file
215
ui/component/userSignUp/view.jsx
Normal file
|
@ -0,0 +1,215 @@
|
|||
// @flow
|
||||
import * as PAGES from 'constants/pages';
|
||||
import React from 'react';
|
||||
import { withRouter } from 'react-router';
|
||||
import UserEmailNew from 'component/userEmailNew';
|
||||
import UserEmailVerify from 'component/userEmailVerify';
|
||||
import UserFirstChannel from 'component/userFirstChannel';
|
||||
import UserChannelFollowIntro from 'component/userChannelFollowIntro';
|
||||
import UserTagFollowIntro from 'component/userTagFollowIntro';
|
||||
import { DEFAULT_BID_FOR_FIRST_CHANNEL } from 'component/userFirstChannel/view';
|
||||
import { rewards as REWARDS, YOUTUBE_STATUSES } from 'lbryinc';
|
||||
import UserVerify from 'component/userVerify';
|
||||
import Spinner from 'component/spinner';
|
||||
import YoutubeTransferStatus from 'component/youtubeTransferStatus';
|
||||
import SyncPassword from 'component/syncPassword';
|
||||
import useFetched from 'effects/use-fetched';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import Confetti from 'react-confetti';
|
||||
|
||||
type Props = {
|
||||
user: ?User,
|
||||
emailToVerify: ?string,
|
||||
channels: ?Array<string>,
|
||||
balance: ?number,
|
||||
fetchingChannels: boolean,
|
||||
claimingReward: boolean,
|
||||
claimReward: () => void,
|
||||
fetchUser: () => void,
|
||||
claimedRewards: Array<Reward>,
|
||||
history: { replace: string => void },
|
||||
location: { search: string },
|
||||
youtubeChannels: Array<any>,
|
||||
syncEnabled: boolean,
|
||||
hasSynced: boolean,
|
||||
syncingWallet: boolean,
|
||||
getSyncError: ?string,
|
||||
creatingChannel: boolean,
|
||||
};
|
||||
|
||||
function UserSignIn(props: Props) {
|
||||
const {
|
||||
emailToVerify,
|
||||
user,
|
||||
claimingReward,
|
||||
claimedRewards,
|
||||
channels,
|
||||
claimReward,
|
||||
balance,
|
||||
history,
|
||||
location,
|
||||
fetchUser,
|
||||
youtubeChannels,
|
||||
syncEnabled,
|
||||
syncingWallet,
|
||||
getSyncError,
|
||||
hasSynced,
|
||||
fetchingChannels,
|
||||
creatingChannel,
|
||||
} = props;
|
||||
const { search } = location;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const redirect = urlParams.get('redirect');
|
||||
const step = urlParams.get('step');
|
||||
const shouldRedirectImmediately = urlParams.get('immediate');
|
||||
const [initialSignInStep, setInitialSignInStep] = React.useState();
|
||||
const [hasSeenFollowList, setHasSeenFollowList] = usePersistedState('channel-follow-intro', false);
|
||||
const [hasSkippedRewards, setHasSkippedRewards] = usePersistedState('skip-rewards-intro', false);
|
||||
const [hasSeenTagsList, setHasSeenTagsList] = usePersistedState('channel-follow-intro', false);
|
||||
const hasVerifiedEmail = user && user.has_verified_email;
|
||||
const rewardsApproved = user && user.is_reward_approved;
|
||||
const isIdentityVerified = user && user.is_identity_verified;
|
||||
const hasFetchedReward = useFetched(claimingReward);
|
||||
const channelCount = channels ? channels.length : 0;
|
||||
const hasClaimedEmailAward = claimedRewards.some(reward => reward.reward_type === REWARDS.TYPE_CONFIRM_EMAIL);
|
||||
const hasYoutubeChannels = youtubeChannels && Boolean(youtubeChannels.length);
|
||||
const isYoutubeTransferComplete =
|
||||
hasYoutubeChannels &&
|
||||
youtubeChannels.every(channel => channel.transfer_state === YOUTUBE_STATUSES.COMPLETED_TRANSFER);
|
||||
|
||||
// Complexity warning
|
||||
// We can't just check if we are currently fetching something
|
||||
// We may want to keep a component rendered while something is being fetched, instead of replacing it with the large spinner
|
||||
// The verbose variable names are an attempt to alleviate _some_ of the confusion from handling all edge cases that come from
|
||||
// reward claiming, channel creation, account syncing, and youtube transfer
|
||||
// The possible screens for the sign in flow
|
||||
const showEmail = !hasVerifiedEmail;
|
||||
const showEmailVerification = emailToVerify && !hasVerifiedEmail;
|
||||
const showUserVerification = hasVerifiedEmail && !rewardsApproved && !isIdentityVerified && !hasSkippedRewards;
|
||||
const showSyncPassword = syncEnabled && getSyncError;
|
||||
const showChannelCreation =
|
||||
hasVerifiedEmail &&
|
||||
balance !== undefined &&
|
||||
balance !== null &&
|
||||
balance > DEFAULT_BID_FOR_FIRST_CHANNEL &&
|
||||
channelCount === 0 &&
|
||||
!hasYoutubeChannels;
|
||||
const showYoutubeTransfer = hasVerifiedEmail && hasYoutubeChannels && !isYoutubeTransferComplete;
|
||||
const showFollowIntro = step === 'channels' || (hasVerifiedEmail && !hasSeenFollowList);
|
||||
const showTagsIntro = step === 'tags' || (hasVerifiedEmail && !hasSeenTagsList);
|
||||
const canHijackSignInFlowWithSpinner = hasVerifiedEmail && !getSyncError && !showFollowIntro;
|
||||
const isCurrentlyFetchingSomething = fetchingChannels || claimingReward || syncingWallet || creatingChannel;
|
||||
const isWaitingForSomethingToFinish =
|
||||
// If the user has claimed the email award, we need to wait until the balance updates sometime in the future
|
||||
(!hasFetchedReward && !hasClaimedEmailAward) || (syncEnabled && !hasSynced);
|
||||
const showLoadingSpinner =
|
||||
canHijackSignInFlowWithSpinner && (isCurrentlyFetchingSomething || isWaitingForSomethingToFinish);
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchUser();
|
||||
}, [fetchUser]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Don't claim the reward if sync is enabled until after a sync has been completed successfully
|
||||
// If we do it before, we could end up trying to sync a wallet with a non-zero balance which will fail to sync
|
||||
const delayForSync = syncEnabled && !hasSynced;
|
||||
|
||||
if (hasVerifiedEmail && !hasClaimedEmailAward && !hasFetchedReward && !delayForSync) {
|
||||
claimReward();
|
||||
}
|
||||
}, [hasVerifiedEmail, claimReward, hasClaimedEmailAward, hasFetchedReward, syncEnabled, hasSynced, balance]);
|
||||
|
||||
// Loop through this list from the end, until it finds a matching component
|
||||
// If it never finds one, assume the user has completed every step and redirect them
|
||||
const SIGN_IN_FLOW = [
|
||||
showEmail && <UserEmailNew />,
|
||||
showEmailVerification && <UserEmailVerify />,
|
||||
showUserVerification && <UserVerify onSkip={() => setHasSkippedRewards(true)} />,
|
||||
showChannelCreation && <UserFirstChannel />,
|
||||
showFollowIntro && (
|
||||
<UserChannelFollowIntro
|
||||
onContinue={() => {
|
||||
let url = `/$/${PAGES.AUTH}?reset_scroll=1`;
|
||||
if (redirect) {
|
||||
url += `&redirect=${redirect}`;
|
||||
}
|
||||
if (shouldRedirectImmediately) {
|
||||
url += `&immediate=true`;
|
||||
}
|
||||
|
||||
history.replace(url);
|
||||
setHasSeenFollowList(true);
|
||||
}}
|
||||
onBack={() => {
|
||||
let url = `/$/${PAGES.AUTH}?reset_scroll=1&step=tags`;
|
||||
if (redirect) {
|
||||
url += `&redirect=${redirect}`;
|
||||
}
|
||||
if (shouldRedirectImmediately) {
|
||||
url += `&immediate=true`;
|
||||
}
|
||||
|
||||
history.replace(url);
|
||||
setHasSeenFollowList(false);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
showTagsIntro && (
|
||||
<UserTagFollowIntro
|
||||
onContinue={() => {
|
||||
let url = `/$/${PAGES.AUTH}?reset_scroll=1&step=channels`;
|
||||
if (redirect) {
|
||||
url += `&redirect=${redirect}`;
|
||||
}
|
||||
if (shouldRedirectImmediately) {
|
||||
url += `&immediate=true`;
|
||||
}
|
||||
|
||||
history.replace(url);
|
||||
setHasSeenTagsList(true);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
showYoutubeTransfer && (
|
||||
<div>
|
||||
<YoutubeTransferStatus /> <Confetti recycle={false} style={{ position: 'fixed' }} />
|
||||
</div>
|
||||
),
|
||||
showSyncPassword && <SyncPassword />,
|
||||
showLoadingSpinner && (
|
||||
<div className="main--empty">
|
||||
<Spinner />
|
||||
</div>
|
||||
),
|
||||
];
|
||||
|
||||
function getSignInStep() {
|
||||
for (var i = SIGN_IN_FLOW.length - 1; i > -1; i--) {
|
||||
const Component = SIGN_IN_FLOW[i];
|
||||
if (Component) {
|
||||
// If we want to redirect immediately,
|
||||
// remember the first step so we can redirect once a new step has been reached
|
||||
// Ignore the loading step
|
||||
if (redirect && shouldRedirectImmediately) {
|
||||
if (!initialSignInStep) {
|
||||
setInitialSignInStep(i);
|
||||
} else if (i !== initialSignInStep && i !== SIGN_IN_FLOW.length - 1) {
|
||||
history.replace(redirect);
|
||||
}
|
||||
}
|
||||
|
||||
return Component;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const componentToRender = getSignInStep();
|
||||
|
||||
if (!componentToRender) {
|
||||
history.replace(redirect || '/');
|
||||
}
|
||||
|
||||
return <section className="main--contained">{componentToRender}</section>;
|
||||
}
|
||||
|
||||
export default withRouter(UserSignIn);
|
|
@ -4,6 +4,7 @@ import Nag from 'component/common/nag';
|
|||
import TagsSelect from 'component/tagsSelect';
|
||||
import Button from 'component/button';
|
||||
import { Form } from 'component/common/form';
|
||||
import Card from 'component/common/card';
|
||||
|
||||
type Props = {
|
||||
subscribedChannels: Array<Subscription>,
|
||||
|
@ -16,11 +17,13 @@ function UserChannelFollowIntro(props: Props) {
|
|||
const followingCount = (followedTags && followedTags.length) || 0;
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={__('Tag Selection')}
|
||||
subtitle={__('Select some tags to help us show you interesting things.')}
|
||||
actions={
|
||||
<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">
|
||||
<Form onSubmit={onContinue}>
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
button="primary"
|
||||
type="Submit"
|
||||
|
@ -46,6 +49,8 @@ function UserChannelFollowIntro(props: Props) {
|
|||
)}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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' */}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
import React from 'react';
|
||||
import WalletAddress from 'component/walletAddress';
|
||||
import Page from 'component/page';
|
||||
|
||||
const WalletAddressPage = () => (
|
||||
<Page className="main--contained">
|
||||
<WalletAddress />
|
||||
</Page>
|
||||
);
|
||||
|
||||
export default WalletAddressPage;
|
|
@ -1,10 +0,0 @@
|
|||
import React from 'react';
|
||||
import WalletSend from 'component/walletSend';
|
||||
|
||||
const WalletSendModal = () => (
|
||||
<div>
|
||||
<WalletSend />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default WalletSendModal;
|
3
ui/page/passwordSet/index.js
Normal file
3
ui/page/passwordSet/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import PasswordResetPage from './view';
|
||||
|
||||
export default PasswordResetPage;
|
12
ui/page/passwordSet/view.jsx
Normal file
12
ui/page/passwordSet/view.jsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import UserPasswordSet from 'component/userPasswordSet';
|
||||
import Page from 'component/page';
|
||||
|
||||
export default function PasswordResetPage() {
|
||||
return (
|
||||
<Page authPage className="main--auth-page">
|
||||
<UserPasswordSet />
|
||||
</Page>
|
||||
);
|
||||
}
|
|
@ -94,7 +94,7 @@ class RewardsPage extends PureComponent<Props> {
|
|||
return (
|
||||
<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.
|
||||
|
|
|
@ -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')}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,29 +89,36 @@ 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">
|
||||
<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>
|
||||
<p className="section__subtitle">
|
||||
{showCaptchaMessage && !isAuthenticationSuccess && (
|
||||
<p>
|
||||
<I18nMessage
|
||||
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%.
|
||||
</I18nMessage>
|
||||
)}
|
||||
</p>
|
||||
{!isAuthenticationSuccess && needsRecaptcha && (
|
||||
)}
|
||||
</React.Fragment>
|
||||
}
|
||||
actions={
|
||||
!isAuthenticationSuccess &&
|
||||
needsRecaptcha && (
|
||||
<div className="section__actions">
|
||||
<ReCAPTCHA
|
||||
sitekey="6LePsJgUAAAAAFTuWOKRLnyoNKhm0HA4C3elrFMG"
|
||||
|
@ -120,8 +128,10 @@ function SignInVerifyPage(props: Props) {
|
|||
onErrored={onAuthError}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
|
3
ui/page/signUp/index.js
Normal file
3
ui/page/signUp/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import SignUpPage from './view';
|
||||
|
||||
export default SignUpPage;
|
12
ui/page/signUp/view.jsx
Normal file
12
ui/page/signUp/view.jsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import UserSignUp from 'component/userSignUp';
|
||||
import Page from 'component/page';
|
||||
|
||||
export default function SignUpPage() {
|
||||
return (
|
||||
<Page authPage className="main--auth-page">
|
||||
<UserSignUp />
|
||||
</Page>
|
||||
);
|
||||
}
|
|
@ -46,6 +46,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.button--header-close {
|
||||
background-color: var(--color-primary-alt);
|
||||
padding: var(--spacing-small);
|
||||
}
|
||||
|
||||
.button--download-link {
|
||||
.button__label {
|
||||
white-space: normal;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -95,6 +95,11 @@
|
|||
margin-right: var(--spacing-medium);
|
||||
}
|
||||
|
||||
.channel-thumbnail {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.claim-preview__hover-actions {
|
||||
display: block;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
.ReactModal__Body--open {
|
||||
#app {
|
||||
@media (max-width: $breakpoint-small) {
|
||||
height: 100vh;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -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"
|
||||
|
||||
|
|
Loading…
Reference in a new issue