onboarding + youtube transfer + channel page

This commit is contained in:
Sean Yesmunt 2019-09-26 12:28:08 -04:00
parent c0a8e02004
commit 4c014e3147
55 changed files with 775 additions and 337 deletions

View file

@ -128,8 +128,8 @@
"husky": "^0.14.3", "husky": "^0.14.3",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git", "lbry-format": "https://github.com/lbryio/lbry-format.git",
"lbry-redux": "lbryio/lbry-redux#04ae0913a444abac200731c7ed53796d763a0bbb", "lbry-redux": "lbryio/lbry-redux#d44cd9ca56dee784dba42c0cc13061ae75cbd46c",
"lbryinc": "lbryio/lbryinc#d99232ebc754a49649a2ff4132478415faef08e2", "lbryinc": "lbryio/lbryinc#368040d64658cf2a4b8a7a6725ec1787329ce65d",
"lint-staged": "^7.0.2", "lint-staged": "^7.0.2",
"localforage": "^1.7.1", "localforage": "^1.7.1",
"lodash-es": "^4.17.14", "lodash-es": "^4.17.14",
@ -151,6 +151,7 @@
"raw-loader": "^2.0.0", "raw-loader": "^2.0.0",
"rc-progress": "^2.0.6", "rc-progress": "^2.0.6",
"react": "^16.8.2", "react": "^16.8.2",
"react-confetti": "^4.0.1",
"react-dom": "^16.8.2", "react-dom": "^16.8.2",
"react-draggable": "^3.3.0", "react-draggable": "^3.3.0",
"react-ga": "^2.5.7", "react-ga": "^2.5.7",

View file

@ -1,7 +1,7 @@
import * as SETTINGS from 'constants/settings'; import * as SETTINGS from 'constants/settings';
import { hot } from 'react-hot-loader/root'; import { hot } from 'react-hot-loader/root';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectUser, doRewardList, doFetchRewardedContent, doFetchAccessToken, selectAccessToken } from 'lbryinc'; import { selectUser, doRewardList, doFetchRewardedContent, doFetchAccessToken } from 'lbryinc';
import { doFetchTransactions, doFetchChannelListMine } from 'lbry-redux'; import { doFetchTransactions, doFetchChannelListMine } from 'lbry-redux';
import { makeSelectClientSetting, selectThemePath } from 'redux/selectors/settings'; import { makeSelectClientSetting, selectThemePath } from 'redux/selectors/settings';
import { selectIsUpgradeAvailable, selectAutoUpdateDownloaded } from 'redux/selectors/app'; import { selectIsUpgradeAvailable, selectAutoUpdateDownloaded } from 'redux/selectors/app';
@ -12,7 +12,6 @@ const select = state => ({
user: selectUser(state), user: selectUser(state),
theme: selectThemePath(state), theme: selectThemePath(state),
language: makeSelectClientSetting(SETTINGS.LANGUAGE)(state), language: makeSelectClientSetting(SETTINGS.LANGUAGE)(state),
accessToken: selectAccessToken(state),
autoUpdateDownloaded: selectAutoUpdateDownloaded(state), autoUpdateDownloaded: selectAutoUpdateDownloaded(state),
isUpgradeAvailable: selectIsUpgradeAvailable(state), isUpgradeAvailable: selectIsUpgradeAvailable(state),
}); });
@ -22,6 +21,8 @@ const perform = dispatch => ({
fetchRewardedContent: () => dispatch(doFetchRewardedContent()), fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
fetchTransactions: () => dispatch(doFetchTransactions()), fetchTransactions: () => dispatch(doFetchTransactions()),
fetchAccessToken: () => dispatch(doFetchAccessToken()), fetchAccessToken: () => dispatch(doFetchAccessToken()),
fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
onSignedIn: () => dispatch(doSignIn()),
requestDownloadUpgrade: () => dispatch(doDownloadUpgradeRequested()), requestDownloadUpgrade: () => dispatch(doDownloadUpgradeRequested()),
fetchChannelListMine: () => dispatch(doFetchChannelListMine()), fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
onSignedIn: () => dispatch(doSignIn()), onSignedIn: () => dispatch(doSignIn()),

View file

@ -12,6 +12,7 @@ import Yrbl from 'component/yrbl';
import FileViewer from 'component/fileViewer'; import FileViewer from 'component/fileViewer';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import usePrevious from 'util/use-previous'; import usePrevious from 'util/use-previous';
import SyncBackgroundManager from 'component/syncBackgroundManager';
import Button from 'component/button'; import Button from 'component/button';
export const MAIN_WRAPPER_CLASS = 'main-wrapper'; export const MAIN_WRAPPER_CLASS = 'main-wrapper';
@ -27,11 +28,13 @@ type Props = {
fetchRewardedContent: () => void, fetchRewardedContent: () => void,
fetchTransactions: () => void, fetchTransactions: () => void,
fetchAccessToken: () => void, fetchAccessToken: () => void,
autoUpdateDownloaded: boolean, fetchChannelListMine: () => void,
isUpgradeAvailable: boolean, onSignedIn: () => void,
requestDownloadUpgrade: () => void, requestDownloadUpgrade: () => void,
fetchChannelListMine: () => void, fetchChannelListMine: () => void,
onSignedIn: () => void, onSignedIn: () => void,
isUpgradeAvailable: boolean,
autoUpdateDownloaded: boolean,
}; };
function App(props: Props) { function App(props: Props) {
@ -42,11 +45,11 @@ function App(props: Props) {
fetchTransactions, fetchTransactions,
user, user,
fetchAccessToken, fetchAccessToken,
requestDownloadUpgrade,
autoUpdateDownloaded,
isUpgradeAvailable,
fetchChannelListMine, fetchChannelListMine,
onSignedIn, onSignedIn,
autoUpdateDownloaded,
isUpgradeAvailable,
requestDownloadUpgrade,
} = props; } = props;
const appRef = useRef(); const appRef = useRef();
const isEnhancedLayout = useKonamiListener(); const isEnhancedLayout = useKonamiListener();
@ -118,6 +121,7 @@ function App(props: Props) {
<Router /> <Router />
<ModalRouter /> <ModalRouter />
<FileViewer pageUri={uri} /> <FileViewer pageUri={uri} />
<SyncBackgroundManager />
{/* @if TARGET='app' */} {/* @if TARGET='app' */}
{showUpgradeButton && ( {showUpgradeButton && (

View file

@ -1,4 +1,5 @@
// @flow // @flow
import type { Node } from 'react';
import React, { Fragment, useEffect, forwardRef } from 'react'; import React, { Fragment, useEffect, forwardRef } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { parseURI, convertToShareLink } from 'lbry-redux'; import { parseURI, convertToShareLink } from 'lbry-redux';
@ -45,6 +46,9 @@ type Props = {
channelIsBlocked: boolean, channelIsBlocked: boolean,
isSubscribed: boolean, isSubscribed: boolean,
beginPublish: string => void, beginPublish: string => void,
actions: boolean | Node | string | number,
properties: boolean | Node | string | number,
onClick?: any => any,
}; };
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => { const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
@ -70,12 +74,14 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
channelIsBlocked, channelIsBlocked,
isSubscribed, isSubscribed,
beginPublish, beginPublish,
actions,
properties,
onClick,
} = props; } = props;
const shouldFetch = claim === undefined || (claim !== null && claim.value_type === 'channel' && isEmpty(claim.meta)); const shouldFetch = claim === undefined || (claim !== null && claim.value_type === 'channel' && isEmpty(claim.meta));
const abandoned = !isResolvingUri && !claim; const abandoned = !isResolvingUri && !claim;
const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0; const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
const showPublishLink = abandoned && placeholder === 'publish'; const showPublishLink = abandoned && placeholder === 'publish';
const includeChannelTooltip = type !== 'inline' && type !== 'tooltip';
const hideActions = type === 'small' || type === 'tooltip'; const hideActions = type === 'small' || type === 'tooltip';
let name; let name;
@ -90,6 +96,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
} }
const isChannel = isValid ? parseURI(uri).isChannel : false; const isChannel = isValid ? parseURI(uri).isChannel : false;
const includeChannelTooltip = type !== 'inline' && type !== 'tooltip' && !isChannel;
const signingChannel = claim && claim.signing_channel; const signingChannel = claim && claim.signing_channel;
let shouldHide = let shouldHide =
placeholder !== 'loading' && ((abandoned && !showPublishLink) || (!claimIsMine && obscureNsfw && nsfw)); placeholder !== 'loading' && ((abandoned && !showPublishLink) || (!claimIsMine && obscureNsfw && nsfw));
@ -129,8 +136,10 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
} }
} }
function onClick(e) { function handleOnClick(e) {
if ((isChannel || title) && !pending) { if (onClick) {
onClick(e);
} else if ((isChannel || title) && !pending) {
history.push(formatLbryUriForWeb(uri)); history.push(formatLbryUriForWeb(uri));
} }
} }
@ -161,7 +170,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
<li <li
ref={ref} ref={ref}
role="link" role="link"
onClick={pending || type === 'inline' ? undefined : onClick} onClick={pending || type === 'inline' ? undefined : handleOnClick}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
className={classnames('claim-preview', { className={classnames('claim-preview', {
'claim-preview--small': type === 'small' || type === 'tooltip', 'claim-preview--small': type === 'small' || type === 'tooltip',
@ -178,15 +187,19 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
<div className="claim-preview-title"> <div className="claim-preview-title">
{claim ? <TruncatedText text={title || claim.name} lines={1} /> : <span>{__('Nothing here')}</span>} {claim ? <TruncatedText text={title || claim.name} lines={1} /> : <span>{__('Nothing here')}</span>}
</div> </div>
{!hideActions && ( {actions !== undefined
<div className="card__actions--inline"> ? actions
{isChannel && !channelIsBlocked && ( : !hideActions && (
<SubscribeButton uri={uri.startsWith('lbry://') ? uri : `lbry://${uri}`} /> <div className="card__actions--inline">
{isChannel && !channelIsBlocked && !claimIsMine && (
<SubscribeButton uri={uri.startsWith('lbry://') ? uri : `lbry://${uri}`} />
)}
{isChannel && !isSubscribed && !claimIsMine && (
<BlockButton uri={uri.startsWith('lbry://') ? uri : `lbry://${uri}`} />
)}
{!isChannel && claim && <FileProperties uri={uri} />}
</div>
)} )}
{isChannel && !isSubscribed && <BlockButton uri={uri.startsWith('lbry://') ? uri : `lbry://${uri}`} />}
{!isChannel && claim && <FileProperties uri={uri} />}
</div>
)}
</div> </div>
<div className="claim-preview-properties"> <div className="claim-preview-properties">
@ -219,7 +232,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
</div> </div>
)} )}
</div> </div>
<ClaimTags uri={uri} type={type} /> {properties !== undefined ? properties : <ClaimTags uri={uri} type={type} />}
</div> </div>
</div> </div>
</li> </li>

View file

@ -104,9 +104,11 @@ export class FormField extends React.PureComponent<Props> {
input = ( input = (
<Wrapper> <Wrapper>
<checkbox-element {...elementProps}> <checkbox-element {...elementProps}>
<input id={name} type="checkbox" {...inputProps} /> <input id={name} type="checkbox" {...inputProps} tabIndex={0} />
<label htmlFor={name}>{label}</label> <label htmlFor={name} tabIndex={-1}>
<checkbox-toggle onClick={inputProps.onChange} /> {label}
</label>
<checkbox-toggle onClick={inputProps.onChange} tabIndex={-1} />
</checkbox-element> </checkbox-element>
</Wrapper> </Wrapper>
); );

View file

@ -303,6 +303,13 @@ export const icons = {
<line x1="15" y1="12" x2="3" y2="12" /> <line x1="15" y1="12" x2="3" y2="12" />
</g> </g>
), ),
[ICONS.SIGN_IN]: buildIcon(
<g>
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
<polyline points="10 17 15 12 10 7" />
<line x1="15" y1="12" x2="3" y2="12" />
</g>
),
[ICONS.SIGN_OUT]: buildIcon( [ICONS.SIGN_OUT]: buildIcon(
<g> <g>
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /> <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />

View file

@ -3,8 +3,8 @@ import { connect } from 'react-redux';
import { selectBalance, formatCredits } from 'lbry-redux'; import { selectBalance, formatCredits } from 'lbry-redux';
import { selectUserVerifiedEmail } from 'lbryinc'; import { selectUserVerifiedEmail } from 'lbryinc';
import { doSetClientSetting } from 'redux/actions/settings'; import { doSetClientSetting } from 'redux/actions/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doSignOut } from 'redux/actions/app'; import { doSignOut } from 'redux/actions/app';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import Header from './view'; import Header from './view';
const select = state => ({ const select = state => ({

View file

@ -13,11 +13,8 @@ import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
import Tooltip from 'component/common/tooltip'; import Tooltip from 'component/common/tooltip';
type Props = { type Props = {
autoUpdateDownloaded: boolean,
balance: string, balance: string,
isUpgradeAvailable: boolean,
roundedBalance: number, roundedBalance: number,
downloadUpgradeRequested: any => void,
history: { push: string => void, goBack: () => void, goForward: () => void }, history: { push: string => void, goBack: () => void, goForward: () => void },
currentTheme: string, currentTheme: string,
automaticDarkModeEnabled: boolean, automaticDarkModeEnabled: boolean,
@ -137,7 +134,10 @@ const Header = (props: Props) => {
{__('Sign Out')} {__('Sign Out')}
</MenuItem> </MenuItem>
) : ( ) : (
<MenuItem onSelect={() => {}} /> <MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.AUTH}`)}>
<Icon aria-hidden icon={ICONS.SIGN_IN} />
{__('Sign In')}
</MenuItem>
)} )}
</MenuList> </MenuList>
</Menu> </Menu>

View file

@ -77,7 +77,7 @@ class InviteNew extends React.PureComponent<Props> {
<Card <Card
title={__('Invite a Friend')} title={__('Invite a Friend')}
subtitle={__('When your friends start using LBRY, the network gets stronger!')} subtitle={__('When your friends start using LBRY, the network gets stronger!')}
body={ actions={
<React.Fragment> <React.Fragment>
<FormInviteNew <FormInviteNew
errorMessage={errorMessage} errorMessage={errorMessage}

View file

@ -16,7 +16,7 @@ class RewardSummary extends React.Component<Props> {
const hasRewards = unclaimedRewardAmount > 0; const hasRewards = unclaimedRewardAmount > 0;
return ( return (
<Card <Card
title={__('Rewards')} title={__('Available Rewards')}
subtitle={ subtitle={
<React.Fragment> <React.Fragment>
{fetching && __('You have...')} {fetching && __('You have...')}

View file

@ -24,6 +24,7 @@ import FollowingPage from 'page/following';
import ListBlockedPage from 'page/listBlocked'; import ListBlockedPage from 'page/listBlocked';
import FourOhFourPage from 'page/fourOhFour'; import FourOhFourPage from 'page/fourOhFour';
import SignInPage from 'page/signIn'; import SignInPage from 'page/signIn';
import ChannelsPage from 'page/channels';
// Tell the browser we are handling scroll restoration // Tell the browser we are handling scroll restoration
if ('scrollRestoration' in history) { if ('scrollRestoration' in history) {
@ -89,6 +90,7 @@ function AppRouter(props: Props) {
<PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} /> <PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
<PrivateRoute {...props} path={`/$/${PAGES.WALLET_SEND}`} exact component={WalletSendPage} /> <PrivateRoute {...props} path={`/$/${PAGES.WALLET_SEND}`} exact component={WalletSendPage} />
<PrivateRoute {...props} path={`/$/${PAGES.WALLET_RECEIVE}`} exact component={WalletReceivePage} /> <PrivateRoute {...props} path={`/$/${PAGES.WALLET_RECEIVE}`} exact component={WalletReceivePage} />
<PrivateRoute {...props} path={`/$/${PAGES.CHANNELS}`} component={ChannelsPage} />
{/* Below need to go at the end to make sure we don't match any of our pages first */} {/* Below need to go at the end to make sure we don't match any of our pages first */}
<Route path="/:claimName" exact component={ShowPage} /> <Route path="/:claimName" exact component={ShowPage} />

View file

@ -49,6 +49,9 @@ function SideBar(props: Props) {
...buildLink(PAGES.LIBRARY, __('Library'), ICONS.LIBRARY), ...buildLink(PAGES.LIBRARY, __('Library'), ICONS.LIBRARY),
}, },
// @endif // @endif
{
...buildLink(PAGES.CHANNELS, __('Channels'), ICONS.CHANNEL),
},
{ {
...buildLink(PAGES.PUBLISHED, __('Publishes'), ICONS.PUBLISH), ...buildLink(PAGES.PUBLISHED, __('Publishes'), ICONS.PUBLISH),
}, },

View file

@ -124,6 +124,11 @@ export default class SplashScreen extends React.PureComponent<Props, State> {
clearTimeout(this.timeout); clearTimeout(this.timeout);
} }
//
//
// Try to unlock by default here
//
//
// Make sure there isn't another active modal (like INCOMPATIBLE_DAEMON) // Make sure there isn't another active modal (like INCOMPATIBLE_DAEMON)
if (launchedModal === false && !modal) { if (launchedModal === false && !modal) {
this.setState({ launchedModal: true }, () => notifyUnlockWallet()); this.setState({ launchedModal: true }, () => notifyUnlockWallet());

View file

@ -0,0 +1,24 @@
import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux';
import { selectBalance } from 'lbry-redux';
import { doGetSync, selectSyncHash, selectUserVerifiedEmail } from 'lbryinc';
import { doSetClientSetting } from 'redux/actions/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import WalletSecurityAndSync from './view';
const select = state => ({
balance: selectBalance(state),
verifiedEmail: selectUserVerifiedEmail(state),
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
hasSyncHash: selectSyncHash(state),
});
const perform = dispatch => ({
getSync: password => dispatch(doGetSync(password)),
setSync: value => dispatch(doSetClientSetting(SETTINGS.ENABLE_SYNC, value)),
});
export default connect(
select,
perform
)(WalletSecurityAndSync);

View file

@ -0,0 +1,22 @@
// @flow
import React from 'react';
type Props = {
syncEnabled: boolean,
verifiedEmail?: string,
getSync: () => void,
};
function SyncBackgroundManager(props: Props) {
const { syncEnabled, getSync, verifiedEmail } = props;
React.useEffect(() => {
if (syncEnabled && verifiedEmail) {
getSync();
}
}, [syncEnabled, verifiedEmail, getSync]);
return null;
}
export default SyncBackgroundManager;

View file

@ -0,0 +1,18 @@
import { connect } from 'react-redux';
import { doGetSync, selectGetSyncIsPending } from 'lbryinc';
import { doSetClientSetting } from 'redux/actions/settings';
import SyncPassword from './view';
const select = state => ({
getSyncIsPending: selectGetSyncIsPending(state),
});
const perform = dispatch => ({
getSync: password => dispatch(doGetSync(password)),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
});
export default connect(
select,
perform
)(SyncPassword);

View file

@ -0,0 +1,54 @@
// @flow
import React from 'react';
import { Form, FormField } from 'component/common/form';
import Button from 'component/button';
import Card from 'component/common/card';
import { setSavedPassword } from 'util/saved-passwords';
type Props = {
getSync: (?string) => void,
getSyncIsPending: boolean,
};
function SyncPassword(props: Props) {
const { getSync, getSyncIsPending } = props;
const [password, setPassword] = React.useState('');
const [rememberPassword, setRememberPassword] = React.useState(true);
function handleSubmit() {
if (rememberPassword) {
setSavedPassword(password);
}
getSync(password);
}
return (
<Form onSubmit={handleSubmit}>
<Card
title={__('Enter Your LBRY Password')}
actions={
<div>
<FormField
type="password"
label={__('Password')}
value={password}
onChange={e => setPassword(e.target.value)}
/>
<FormField
type="checkbox"
label={__('Remember My Password')}
checked={rememberPassword}
onChange={() => setRememberPassword(!rememberPassword)}
/>
<div className="card__actions">
<Button type="submit" button="primary" label={__('Continue')} disabled={!password || getSyncIsPending} />
</div>
</div>
}
/>
</Form>
);
}
export default SyncPassword;

View file

@ -75,20 +75,18 @@ export default function TagSelect(props: Props) {
} }
body={ body={
<React.Fragment> <React.Fragment>
<section className="section"> <TagsSearch
<TagsSearch onRemove={handleTagClick}
onRemove={handleTagClick} onSelect={onSelect}
onSelect={onSelect} suggestMature={suggestMature && !hasMatureTag}
suggestMature={suggestMature && !hasMatureTag} tagsPasssedIn={tagsToDisplay}
tagsPasssedIn={tagsToDisplay} />
/> {help !== false && (
{help !== false && ( <p className="help">
<p className="help"> {help || __("The tags you follow will change what's trending for you.")}{' '}
{help || __("The tags you follow will change what's trending for you.")}{' '} <Button button="link" label={__('Learn more')} href="https://lbry.com/faq/trending" />.
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/trending" />. </p>
</p> )}
)}
</section>
</React.Fragment> </React.Fragment>
} }
/> />

View file

@ -39,8 +39,10 @@ function UserEmail(props: Props) {
subtitle={__( subtitle={__(
'This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to save account information and earn rewards.' 'This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to save account information and earn rewards.'
)} )}
body={ actions={
isVerified ? ( !isVerified ? (
<Button button="primary" label={__('Add Email')} navigate={`/$/${PAGES.AUTH}`} />
) : (
<FormField <FormField
type="text" type="text"
className="form-field--copyable" className="form-field--copyable"
@ -58,9 +60,8 @@ function UserEmail(props: Props) {
inputButton={<UserSignOutButton button="inverse" />} inputButton={<UserSignOutButton button="inverse" />}
value={email || ''} value={email || ''}
/> />
) : null )
} }
actions={!isVerified ? <Button button="primary" label={__('Add Email')} navigate={`/$/${PAGES.AUTH}`} /> : null}
/> />
); );
} }

View file

@ -1,14 +1,21 @@
import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectBalance } from 'lbry-redux';
import { selectEmailNewIsPending, selectEmailNewErrorMessage, doUserEmailNew } from 'lbryinc'; import { selectEmailNewIsPending, selectEmailNewErrorMessage, doUserEmailNew } from 'lbryinc';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doSetClientSetting } from 'redux/actions/settings';
import UserEmailNew from './view'; import UserEmailNew from './view';
const select = state => ({ const select = state => ({
isPending: selectEmailNewIsPending(state), isPending: selectEmailNewIsPending(state),
errorMessage: selectEmailNewErrorMessage(state), errorMessage: selectEmailNewErrorMessage(state),
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
balance: selectBalance(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
addUserEmail: email => dispatch(doUserEmailNew(email)), addUserEmail: email => dispatch(doUserEmailNew(email)),
setSync: value => dispatch(doSetClientSetting(SETTINGS.ENABLE_SYNC, value)),
}); });
export default connect( export default connect(

View file

@ -4,19 +4,19 @@ import { FormField, Form } from 'component/common/form';
import Button from 'component/button'; import Button from 'component/button';
import { Lbryio } from 'lbryinc'; import { Lbryio } from 'lbryinc';
import analytics from 'analytics'; import analytics from 'analytics';
import { EMAIL_REGEX } from 'constants/email';
type Props = { type Props = {
errorMessage: ?string, errorMessage: ?string,
isPending: boolean, isPending: boolean,
addUserEmail: string => void, addUserEmail: string => void,
syncEnabled: boolean,
setSync: boolean => void,
balance: number,
}; };
// "Email regex that 99.99% works"
// https://emailregex.com/
const EMAIL_REGEX = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/;
function UserEmailNew(props: Props) { function UserEmailNew(props: Props) {
const { errorMessage, isPending, addUserEmail } = props; const { errorMessage, isPending, addUserEmail, syncEnabled, setSync, balance } = props;
const [newEmail, setEmail] = useState(''); const [newEmail, setEmail] = useState('');
const valid = newEmail.match(EMAIL_REGEX); const valid = newEmail.match(EMAIL_REGEX);
@ -29,6 +29,12 @@ function UserEmailNew(props: Props) {
// @endif // @endif
} }
React.useEffect(() => {
if (syncEnabled && balance) {
setSync(false);
}
}, [balance, syncEnabled, setSync]);
return ( return (
<div> <div>
<h1 className="section__title--large">{__('Welcome To LBRY')}</h1> <h1 className="section__title--large">{__('Welcome To LBRY')}</h1>
@ -39,12 +45,23 @@ function UserEmailNew(props: Props) {
className="form-field--short" className="form-field--short"
placeholder={__('hotstuff_96@hotmail.com')} placeholder={__('hotstuff_96@hotmail.com')}
type="email" type="email"
id="sign_up_email" name="sign_up_email"
label={__('Email')} label={__('Email')}
value={newEmail} value={newEmail}
error={errorMessage} error={errorMessage}
onChange={e => setEmail(e.target.value)} onChange={e => setEmail(e.target.value)}
/> />
<FormField
type="checkbox"
name="sync_checkbox"
label={__('Sync your balance between devices')}
helper={
balance > 0 ? __('This is only available for empty wallets') : __('Maybe some more text about something')
}
checked={syncEnabled}
onChange={() => setSync(!syncEnabled)}
disabled={balance > 0}
/>
<div className="card__actions"> <div className="card__actions">
<Button button="primary" type="submit" label={__('Continue')} disabled={!newEmail || !valid || isPending} /> <Button button="primary" type="submit" label={__('Continue')} disabled={!newEmail || !valid || isPending} />
</div> </div>

View file

@ -1,3 +1,4 @@
import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
selectEmailToVerify, selectEmailToVerify,
@ -7,12 +8,19 @@ import {
selectClaimedRewards, selectClaimedRewards,
rewards as REWARD_TYPES, rewards as REWARD_TYPES,
doClaimRewardType, doClaimRewardType,
doUserFetch,
selectUserIsPending,
selectYoutubeChannels,
selectGetSyncIsPending,
selectGetSyncErrorMessage,
selectSyncHash,
} from 'lbryinc'; } from 'lbryinc';
import { selectMyChannelClaims, selectBalance, selectFetchingMyChannels } from 'lbry-redux'; import { selectMyChannelClaims, selectBalance, selectFetchingMyChannels } from 'lbry-redux';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import UserSignIn from './view'; import UserSignIn from './view';
const select = state => ({ const select = state => ({
email: selectEmailToVerify(state), emailToVerify: selectEmailToVerify(state),
user: selectUser(state), user: selectUser(state),
accessToken: selectAccessToken(state), accessToken: selectAccessToken(state),
channels: selectMyChannelClaims(state), channels: selectMyChannelClaims(state),
@ -22,9 +30,16 @@ const select = state => ({
}), }),
balance: selectBalance(state), balance: selectBalance(state),
fetchingChannels: selectFetchingMyChannels(state), fetchingChannels: selectFetchingMyChannels(state),
youtubeChannels: selectYoutubeChannels(state),
userFetchPending: selectUserIsPending(state),
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
syncIsPending: selectGetSyncIsPending(state),
getSyncError: selectGetSyncErrorMessage(state),
syncHash: selectSyncHash(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
fetchUser: () => dispatch(doUserFetch()),
claimReward: () => claimReward: () =>
dispatch( dispatch(
doClaimRewardType(REWARD_TYPES.TYPE_CONFIRM_EMAIL, { doClaimRewardType(REWARD_TYPES.TYPE_CONFIRM_EMAIL, {

View file

@ -4,24 +4,38 @@ import { withRouter } from 'react-router';
import UserEmailNew from 'component/userEmailNew'; import UserEmailNew from 'component/userEmailNew';
import UserEmailVerify from 'component/userEmailVerify'; import UserEmailVerify from 'component/userEmailVerify';
import UserFirstChannel from 'component/userFirstChannel'; import UserFirstChannel from 'component/userFirstChannel';
import UserVerify from 'component/userVerify';
import Spinner from 'component/spinner';
import { DEFAULT_BID_FOR_FIRST_CHANNEL } from 'component/userFirstChannel/view'; import { DEFAULT_BID_FOR_FIRST_CHANNEL } from 'component/userFirstChannel/view';
import { rewards as REWARDS } from 'lbryinc'; import { rewards as REWARDS } from 'lbryinc';
import usePrevious from 'util/use-previous'; import usePrevious from 'util/use-previous';
import UserVerify from 'component/userVerify';
import Spinner from 'component/spinner';
import YoutubeTransferWelcome from 'component/youtubeTransferWelcome';
import SyncPassword from 'component/syncPassword';
/*
- Brand new user
- Brand new user, not auto approved
- Second device (first time user), first device has a password + rewards not approved
- Second device (first time user), first device has a password + rewards approved
*/
type Props = { type Props = {
user: ?User, user: ?User,
email: ?string, emailToVerify: ?string,
fetchingChannels: boolean,
channels: ?Array<string>, channels: ?Array<string>,
balance: ?number, balance: ?number,
fetchingChannels: boolean, fetchingChannels: boolean,
claimingReward: boolean, claimingReward: boolean,
claimReward: () => void, claimReward: () => void,
fetchUser: () => void,
claimedRewards: Array<Reward>, claimedRewards: Array<Reward>,
history: { replace: string => void }, history: { replace: string => void },
location: { search: string }, location: { search: string },
youtubeChannels: Array<any>,
syncIsPending: boolean,
getSyncError: ?string,
hasSyncedSuccessfully: boolean,
}; };
function useFetched(fetching) { function useFetched(fetching) {
@ -38,64 +52,96 @@ function useFetched(fetching) {
} }
function UserSignIn(props: Props) { function UserSignIn(props: Props) {
const { email, user, channels, claimingReward, claimReward, claimedRewards, balance, history, location } = props; const {
emailToVerify,
user,
claimingReward,
claimedRewards,
channels,
claimReward,
balance,
history,
location,
fetchUser,
youtubeChannels,
syncEnabled,
syncIsPending,
getSyncError,
syncHash,
fetchingChannels,
} = props;
const { search } = location; const { search } = location;
const urlParams = new URLSearchParams(search); const urlParams = new URLSearchParams(search);
const redirect = urlParams.get('redirect'); const redirect = urlParams.get('redirect');
const hasFetchedReward = useFetched(claimingReward);
const hasVerifiedEmail = user && user.has_verified_email; const hasVerifiedEmail = user && user.has_verified_email;
const rewardsApproved = user && user.is_reward_approved; const rewardsApproved = user && user.is_reward_approved;
const hasFetchedReward = useFetched(claimingReward);
// const hasFetchedSync = useFetched(syncIsPending);
// const hasTriedSyncForReal = syncEnabled && hasFetchedSync;
const channelCount = channels ? channels.length : 0; const channelCount = channels ? channels.length : 0;
const hasFetchedChannels = channels !== undefined;
const hasClaimedEmailAward = claimedRewards.some(reward => reward.reward_type === REWARDS.TYPE_CONFIRM_EMAIL); const hasClaimedEmailAward = claimedRewards.some(reward => reward.reward_type === REWARDS.TYPE_CONFIRM_EMAIL);
const memoizedClaimReward = React.useCallback(() => { const hasYoutubeChannels = youtubeChannels && youtubeChannels.length;
claimReward(); const hasTransferrableYoutubeChannels = hasYoutubeChannels && youtubeChannels.some(channel => channel.transferable);
}, [claimReward]); const hasPendingYoutubeTransfer =
hasYoutubeChannels && youtubeChannels.some(channel => channel.transfer_state === 'pending_transfer');
React.useEffect(() => { React.useEffect(() => {
if (hasVerifiedEmail && balance !== undefined && !hasClaimedEmailAward && !hasFetchedReward) { if (
memoizedClaimReward(); hasVerifiedEmail &&
balance !== undefined &&
!hasClaimedEmailAward &&
!hasFetchedReward &&
(!syncEnabled || (syncEnabled && syncHash))
) {
claimReward();
} }
}, [hasVerifiedEmail, memoizedClaimReward, balance, hasClaimedEmailAward, hasFetchedReward]); }, [hasVerifiedEmail, claimReward, balance, hasClaimedEmailAward, hasFetchedReward, syncEnabled, syncHash]);
if ( React.useEffect(() => {
!user || fetchUser();
(balance === 0 && !hasFetchedReward) || }, [fetchUser]);
(hasVerifiedEmail && balance === undefined) ||
(hasVerifiedEmail && !hasFetchedChannels)
) {
return (
<div className="main--empty">
<Spinner delayed />
</div>
);
}
if (balance === 0 && hasClaimedEmailAward) { const SIGN_IN_FLOW = [
history.replace(redirect || '/'); !emailToVerify && !hasVerifiedEmail && <UserEmailNew />,
} emailToVerify && !hasVerifiedEmail && <UserEmailVerify />,
hasVerifiedEmail && !rewardsApproved && <UserVerify />,
if (rewardsApproved && channelCount > 0) { getSyncError && !syncHash && <SyncPassword />,
history.replace(redirect || '/'); hasVerifiedEmail && balance > DEFAULT_BID_FOR_FIRST_CHANNEL && channelCount === 0 && !hasYoutubeChannels && (
} <UserFirstChannel />
),
if (rewardsApproved && hasFetchedReward && balance && (balance === 0 || balance < DEFAULT_BID_FOR_FIRST_CHANNEL)) { hasVerifiedEmail && hasYoutubeChannels && (hasTransferrableYoutubeChannels || hasPendingYoutubeTransfer) && (
history.replace(redirect || '/'); <YoutubeTransferWelcome />
} ),
hasVerifiedEmail &&
return ( balance === 0 &&
<section> !getSyncError &&
{hasVerifiedEmail && !rewardsApproved ? ( (fetchingChannels ||
<UserVerify /> !hasFetchedReward ||
) : ( claimingReward ||
<div className="main--contained"> syncIsPending ||
{!email && !hasVerifiedEmail && <UserEmailNew />} (syncEnabled && !syncHash) ||
{email && !hasVerifiedEmail && <UserEmailVerify />} // Just claimed the email award, wait until the balance updates to move forward
{hasVerifiedEmail && balance && balance > 0 && channelCount === 0 && <UserFirstChannel />} (balance === 0 && hasFetchedReward && hasClaimedEmailAward)) && (
<div className="main--empty">
<Spinner />
</div> </div>
)} ),
</section> ];
);
let componentToRender;
for (var i = SIGN_IN_FLOW.length - 1; i > -1; i--) {
const Component = SIGN_IN_FLOW[i];
if (Component) {
componentToRender = Component;
break;
}
}
if (!componentToRender) {
history.replace(redirect || '/');
}
return <section className="main--contained">{componentToRender}</section>;
} }
export default withRouter(UserSignIn); export default withRouter(UserSignIn);

View file

@ -97,7 +97,6 @@ class WalletSendTip extends React.PureComponent<Props, State> {
inputButton={ inputButton={
<Button <Button
button="primary" button="primary"
type="submit"
label={__('Send')} label={__('Send')}
disabled={isPending || tipError || !tipAmount} disabled={isPending || tipError || !tipAmount}
onClick={this.handleSendButtonClicked} onClick={this.handleSendButtonClicked}

View file

@ -1,59 +0,0 @@
// @flow
import React from 'react';
import Button from 'component/button';
type Channel = {
yt_channel_name: string,
lbry_channel_name: string,
channel_claim_id: string,
sync_status: string,
status_token: string,
transferable: boolean,
transfer_state: string,
publish_to_address: Array<string>,
};
type Props = {
channel: Channel,
};
export default function YoutubeChannelItem(props: Props) {
const {
yt_channel_name: ytName,
lbry_channel_name: lbryName,
sync_status: syncStatus,
status_token: statusToken,
transferable,
transfer_state: transferState,
} = props.channel;
const LBRY_YT_URL = 'https://lbry.com/youtube/status/';
const NOT_TRANSFERED = 'not_transferred';
const PENDING_TRANSFER = 'pending_transfer';
const COMPLETED_TRANSFER = 'completed_transfer';
function renderSwitch(param) {
switch (param) {
case NOT_TRANSFERED:
return __('Not Transferred');
case PENDING_TRANSFER:
return __('Pending Transfer');
case COMPLETED_TRANSFER:
return __('Completed Transfer');
}
}
// | Youtube Name | LBRY Name | SyncStatus | TransferStatus |
return (
<tr>
<td>{ytName}</td>
<td>
<Button button={'link'} navigate={`lbry://${lbryName}`} label={lbryName} />
</td>
<td>
<Button button={'link'} href={`${LBRY_YT_URL}${statusToken}`} label={syncStatus} />
</td>
<td>{transferable ? renderSwitch(transferState) : __('Not Transferable')}</td>
</tr>
);
}

View file

@ -1,70 +0,0 @@
// @flow
import * as React from 'react';
import YoutubeChannelListItem from './internal/youtubeChannel';
import Button from 'component/button';
import Spinner from 'component/spinner';
type Props = {
ytChannels: Array<any>,
ytImportPending: boolean,
userFetchPending: boolean,
claimChannels: () => void,
updateUser: () => void,
};
export default function YoutubeChannelList(props: Props) {
const { ytChannels, ytImportPending, userFetchPending, claimChannels, updateUser } = props;
const hasChannels = ytChannels && ytChannels.length;
const transferEnabled = ytChannels && ytChannels.some(el => el.transferable === true);
return (
hasChannels && (
<section className="card card--section">
<h2 className="card__title--between">
<span>Synced Youtube Channels{userFetchPending && <Spinner type="small" />}</span>
<div className="card__actions--inline">
<Button button="inverse" onClick={updateUser} label={__('Refresh')} />
</div>
</h2>
{transferEnabled && !IS_WEB && (
<p className="card__subtitle">LBRY is currently holding channels you can take control of.</p>
)}
{!transferEnabled && !IS_WEB && (
<p className="card__subtitle">LBRY is currently holding channels but none are ready for transfer yet.</p>
)}
{IS_WEB && (
<p className="card__subtitle">
{__(`LBRY.tv can't import accounts yet. `)}
<Button button="link" label={__('Download the app')} href="https://lbry.com/get" />
</p>
)}
<table className="table">
<thead>
<tr>
<th>{__('Youtube Name')}</th>
<th>{__('LBRY Name')} </th>
<th>{__('Sync Status')} </th>
<th>{__('Transfer Status')}</th>
</tr>
</thead>
<tbody>
{ytChannels.map(channel => (
<YoutubeChannelListItem
key={`yt${channel.yt_channel_name}${channel.lbry_channel_name}`}
channel={channel}
/>
))}
</tbody>
</table>
<div className="card__actions">
<Button
disabled={IS_WEB || !transferEnabled || ytImportPending}
button="primary"
onClick={claimChannels}
label={__('Claim Channels')}
/>
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/youtube#transfer" />
</div>
</section>
)
);
}

View file

@ -0,0 +1,29 @@
import { connect } from 'react-redux';
import {
selectYoutubeChannels,
selectYTImportPending,
selectUserIsPending,
doClaimYoutubeChannels,
doUserFetch,
selectYTImportVideosComplete,
doCheckYoutubeTransfer,
} from 'lbryinc';
import YoutubeChannelList from './view';
const select = state => ({
youtubeChannels: selectYoutubeChannels(state),
ytImportPending: selectYTImportPending(state),
userFetchPending: selectUserIsPending(state),
videosImported: selectYTImportVideosComplete(state),
});
const perform = dispatch => ({
claimChannels: () => dispatch(doClaimYoutubeChannels()),
updateUser: () => dispatch(doUserFetch()),
checkYoutubeTransfer: () => dispatch(doCheckYoutubeTransfer()),
});
export default connect(
select,
perform
)(YoutubeChannelList);

View file

@ -0,0 +1,121 @@
// @flow
import * as React from 'react';
import Button from 'component/button';
import ClaimPreview from 'component/claimPreview';
import Card from 'component/common/card';
type Props = {
youtubeChannels: Array<any>,
ytImportPending: boolean,
claimChannels: () => void,
updateUser: () => void,
checkYoutubeTransfer: () => void,
videosImported: ?Array<number>, // [currentAmountImported, totalAmountToImport]
};
const LBRY_YT_URL = 'https://lbry.com/youtube/status/';
const NOT_TRANSFERED = 'not_transferred';
const PENDING_TRANSFER = 'pending_transfer';
const COMPLETED_TRANSFER = 'completed_transfer';
export default function YoutubeTransferStatus(props: Props) {
const { youtubeChannels, ytImportPending, claimChannels, videosImported, checkYoutubeTransfer, updateUser } = props;
const hasChannels = youtubeChannels && youtubeChannels.length;
const transferEnabled = youtubeChannels && youtubeChannels.some(el => el.transferable === true);
const transferComplete =
youtubeChannels &&
youtubeChannels.some(({ transfer_state: transferState }) => transferState === COMPLETED_TRANSFER);
let youtubeUrls =
youtubeChannels &&
youtubeChannels.map(
({ lbry_channel_name: channelName, channel_claim_id: claimId }) => `lbry://${channelName}#${claimId}`
);
let total;
let complete;
if (!transferComplete && videosImported) {
complete = videosImported[0];
total = videosImported[1];
}
function getMessage(channel) {
const { transferable, transfer_state: transferState, sync_status: syncStatus } = channel;
if (!transferable) {
switch (transferState) {
case NOT_TRANSFERED:
return syncStatus[0].toUpperCase() + syncStatus.slice(1);
case PENDING_TRANSFER:
return __('Transfer in progress');
case COMPLETED_TRANSFER:
return __('Completed transfer');
}
} else {
return __('Ready to transfer');
}
}
React.useEffect(() => {
// If a channel is transferrable, theres nothing to check
if (!transferComplete) {
checkYoutubeTransfer();
let interval = setInterval(() => {
checkYoutubeTransfer();
updateUser();
}, 60 * 1000);
return () => {
clearInterval(interval);
};
}
}, [transferComplete, checkYoutubeTransfer, updateUser]);
return (
hasChannels &&
!transferComplete && (
<div>
<Card
title={youtubeUrls.length > 1 ? __('Your YouTube Channels') : __('Your YouTube Channel')}
subtitle={
<span>
{__('Your videos are currently being transferred. There is nothing else for you to do.')}{' '}
<Button button="link" href={LBRY_YT_URL} label={__('Learn more')} />.
</span>
}
body={
<section>
{youtubeUrls.map((url, index) => {
const channel = youtubeChannels[index];
const transferState = getMessage(channel);
return (
<div
key={url}
style={{ border: '1px solid #ccc', borderRadius: 'var(--card-radius)', marginBottom: '1rem' }}
>
<ClaimPreview uri={url} actions={<span className="help">{transferState}</span>} properties={''} />
</div>
);
})}
{videosImported && (
<div className="section help">
{complete} / {total} {__('videos transferred')}
</div>
)}
</section>
}
actions={
transferEnabled &&
!ytImportPending && (
<div className="card__actions">
<Button button="primary" onClick={claimChannels} label={__('Claim Channels')} />
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/youtube#transfer" />
</div>
)
}
/>
</div>
)
);
}

View file

@ -6,12 +6,11 @@ import {
doClaimYoutubeChannels, doClaimYoutubeChannels,
doUserFetch, doUserFetch,
} from 'lbryinc'; } from 'lbryinc';
import YoutubeChannelList from './view'; import YoutubeChannelList from './view';
const select = state => ({ const select = state => ({
ytChannels: selectYoutubeChannels(state), youtubeChannels: selectYoutubeChannels(state),
ytImportPending: selectYTImportPending(state), youtubeImportPending: selectYTImportPending(state),
userFetchPending: selectUserIsPending(state), userFetchPending: selectUserIsPending(state),
}); });

View file

@ -0,0 +1,76 @@
// @flow
import * as PAGES from 'constants/pages';
import React from 'react';
import classnames from 'classnames';
import ClaimPreview from 'component/claimPreview';
import Button from 'component/button';
import Confetti from 'react-confetti';
type Props = {
youtubeChannels: Array<{ lbry_channel_name: string, channel_claim_id: string, transfer_state: string }>,
claimChannels: () => void,
};
export default function UserYoutubeTransfer(props: Props) {
const { youtubeChannels, claimChannels } = props;
const hasYoutubeChannels = youtubeChannels && youtubeChannels.length;
const hasPendingYoutubeTransfer =
hasYoutubeChannels && youtubeChannels.some(channel => channel.transfer_state === 'pending_transfer');
return (
<div>
<div className="section__header">
{!hasPendingYoutubeTransfer ? (
<React.Fragment>
<h1 className="section__title--large">{__('Welcome back!')}</h1>
<p className="section__subtitle">{__('Your channel is ready to be sent over.')}</p>
</React.Fragment>
) : (
<React.Fragment>
<h1 className="section__title--large">{__('Good to Go!')}</h1>
<p className="section__subtitle">
{__('You now control your channel and your videos are being transferred to your account.')}
</p>
</React.Fragment>
)}
</div>
<section className="section">
{youtubeChannels.map(({ lbry_channel_name: channelName, channel_claim_id: claimId }) => (
<div key={channelName} className={classnames('card--claim-preview-wrap')}>
<ClaimPreview disabled onClick={() => {}} actions={false} uri={`lbry://${channelName}#${claimId}`} />
</div>
))}
</section>
{hasPendingYoutubeTransfer ? (
<section className="section">
<div className="section__header">
<h1 className="section__title">{__('Transfer In Progress...')}</h1>
<p className="section__subtitle">{__('You can now publish and comment using your official channel.')}</p>
</div>
<div className="card__actions">
<Button
button="primary"
label={youtubeChannels.length > 1 ? __('View Your Channels') : __('View Your Channel')}
navigate={`/$/${PAGES.CHANNELS}`}
/>
</div>
</section>
) : (
<section className="section">
<div className="section__header">
<h1 className="section__title">{__('Begin Transfer')}</h1>
<p className="section__subtitle">{__('Do it to it.')}</p>
</div>
<div className="section__actions">
<Button button="primary" label={__('Transfer')} onClick={claimChannels} />
</div>
</section>
)}
{hasPendingYoutubeTransfer && <Confetti recycle={false} style={{ position: 'fixed' }} />}
</div>
);
}

View file

@ -0,0 +1,3 @@
// "Email regex that 99.99% works"
// https://emailregex.com/
export const EMAIL_REGEX = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/;

View file

@ -77,3 +77,4 @@ export const VIEW = 'View';
export const EYE = 'Eye'; export const EYE = 'Eye';
export const EYE_OFF = 'EyeOff'; export const EYE_OFF = 'EyeOff';
export const SIGN_OUT = 'SignOut'; export const SIGN_OUT = 'SignOut';
export const SIGN_IN = 'SignIn';

View file

@ -23,3 +23,4 @@ export const WALLET = 'wallet';
export const WALLET_SEND = 'wallet/send'; export const WALLET_SEND = 'wallet/send';
export const WALLET_RECEIVE = 'wallet/receive'; export const WALLET_RECEIVE = 'wallet/receive';
export const BLOCKED = 'blocked'; export const BLOCKED = 'blocked';
export const CHANNELS = 'channels';

View file

@ -20,3 +20,4 @@ export const HIDE_BALANCE = 'hide_balance';
export const HIDE_SPLASH_ANIMATION = 'hide_splash_animation'; export const HIDE_SPLASH_ANIMATION = 'hide_splash_animation';
export const FLOATING_PLAYER = 'floating_player'; export const FLOATING_PLAYER = 'floating_player';
export const DARK_MODE_TIMES = 'dark_mode_times'; export const DARK_MODE_TIMES = 'dark_mode_times';
export const ENABLE_SYNC = 'enable_sync';

View file

@ -1,10 +1,11 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import { Modal } from 'modal/modal'; import { Modal } from 'modal/modal';
import { deleteAuthToken } from 'util/saved-passwords'; import { deleteSavedPassword } from 'util/saved-passwords';
type Props = { type Props = {
closeModal: () => void, closeModal: () => void,
callback?: () => void,
}; };
class ModalPasswordUnsave extends React.PureComponent<Props> { class ModalPasswordUnsave extends React.PureComponent<Props> {
@ -18,8 +19,11 @@ class ModalPasswordUnsave extends React.PureComponent<Props> {
confirmButtonLabel={__('Forget')} confirmButtonLabel={__('Forget')}
abortButtonLabel={__('Nevermind')} abortButtonLabel={__('Nevermind')}
onConfirmed={() => onConfirmed={() =>
deleteAuthToken().then(() => { deleteSavedPassword().then(() => {
this.props.closeModal(); this.props.closeModal();
if (this.props.callback) {
this.props.callback();
}
}) })
} }
onAborted={this.props.closeModal} onAborted={this.props.closeModal}

View file

@ -67,11 +67,11 @@ class ModalWalletUnlock extends React.PureComponent<Props, State> {
onConfirmed={() => unlockWallet(password)} onConfirmed={() => unlockWallet(password)}
onAborted={quit} onAborted={quit}
> >
<Form onSubmit={() => unlockWallet(password)}> <p>
<p> {__('Your wallet has been encrypted with a local password. Please enter your wallet password to proceed.')}{' '}
{__('Your wallet has been encrypted with a local password. Please enter your wallet password to proceed.')}{' '} <Button button="link" label={__('Learn more')} href="https://lbry.com/faq/wallet-encryption" />.
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/wallet-encryption" />. </p>
</p> <Form className="section" onSubmit={() => unlockWallet(password)}>
<FormField <FormField
autoFocus autoFocus
error={walletUnlockSucceded === false ? 'Incorrect Password' : false} error={walletUnlockSucceded === false ? 'Incorrect Password' : false}
@ -88,7 +88,6 @@ class ModalWalletUnlock extends React.PureComponent<Props, State> {
name="wallet-remember-password" name="wallet-remember-password"
onChange={event => this.onChangeRememberPassword(event)} onChange={event => this.onChangeRememberPassword(event)}
checked={rememberPassword} checked={rememberPassword}
helper={__('You will no longer see this at startup')}
/> />
</fieldset-section> </fieldset-section>
</Form> </Form>

View file

@ -1,10 +1,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import AccountPage from './view'; import AccountPage from './view';
import { selectYoutubeChannels } from 'lbryinc';
const select = state => ({ const select = state => ({});
ytChannels: selectYoutubeChannels(state),
});
export default connect( export default connect(
select, select,

View file

@ -7,16 +7,16 @@ import UserEmail from 'component/userEmail';
import InviteNew from 'component/inviteNew'; import InviteNew from 'component/inviteNew';
import InviteList from 'component/inviteList'; import InviteList from 'component/inviteList';
const AccountPage = () => { const AccountPage = (props: Props) => {
return ( return (
<Page> <Page>
<div className="columns"> <div className="columns section">
<div> <div>
<RewardSummary />
<RewardTotal /> <RewardTotal />
<UserEmail />
</div> </div>
<div> <div>
<UserEmail /> <RewardSummary />
<InviteNew /> <InviteNew />
</div> </div>
</div> </div>

View file

@ -0,0 +1,19 @@
import { connect } from 'react-redux';
import { selectMyChannelClaims, doFetchChannelListMine, selectFetchingMyChannels } from 'lbry-redux';
import { selectYoutubeChannels } from 'lbryinc';
import ChannelsPage from './view';
const select = state => ({
channels: selectMyChannelClaims(state),
fetchingChannels: selectFetchingMyChannels(state),
youtubeChannels: selectYoutubeChannels(state),
});
const perform = dispatch => ({
fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
});
export default connect(
select,
perform
)(ChannelsPage);

View file

@ -0,0 +1,48 @@
// @flow
import React, { useEffect } from 'react';
import ClaimList from 'component/claimList';
import Page from 'component/page';
import Button from 'component/button';
import YoutubeTransferStatus from 'component/youtubeTransferStatus';
type Props = {
channels: Array<ChannelClaim>,
fetchChannelListMine: () => void,
fetchingChannels: boolean,
youtubeChannels: ?Array<any>,
};
export default function ChannelsPage(props: Props) {
const { channels, fetchChannelListMine, fetchingChannels, youtubeChannels } = props;
const hasYoutubeChannels = youtubeChannels && Boolean(youtubeChannels.length);
useEffect(() => {
fetchChannelListMine();
}, [fetchChannelListMine]);
return (
<Page>
{hasYoutubeChannels && <YoutubeTransferStatus />}
{channels && channels.length ? (
<div className="card">
<ClaimList
header={__('Your Channels On LBRY')}
loading={fetchingChannels}
uris={channels.map(channel => channel.permanent_url)}
/>
</div>
) : (
<section className="main--empty">
<div className=" section--small">
<h2 className="section__title--large">{__('No Channels Created Yet')}</h2>
<div className="section__actions">
<Button button="primary" navigate="/$/publish" label={__('Create A Channel')} />
</div>
</div>
</section>
)}
</Page>
);
}

View file

@ -1,8 +1,13 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import * as SETTINGS from 'constants/settings'; import * as SETTINGS from 'constants/settings';
import { doClearCache, doNotifyEncryptWallet, doNotifyDecryptWallet } from 'redux/actions/app'; import { doClearCache, doNotifyEncryptWallet, doNotifyDecryptWallet, doNotifyForgetPassword } from 'redux/actions/app';
import { doSetDaemonSetting, doSetClientSetting, doGetThemes, doSetDarkTime } from 'redux/actions/settings'; import {
import { selectIsPasswordSaved } from 'redux/selectors/app'; doSetDaemonSetting,
doSetClientSetting,
doGetThemes,
doChangeLanguage,
doSetDarkTime,
} from 'redux/actions/settings';
import { doSetPlayingUri } from 'redux/actions/content'; import { doSetPlayingUri } from 'redux/actions/content';
import { makeSelectClientSetting, selectDaemonSettings, selectosNotificationsEnabled } from 'redux/selectors/settings'; import { makeSelectClientSetting, selectDaemonSettings, selectosNotificationsEnabled } from 'redux/selectors/settings';
import { doWalletStatus, selectWalletIsEncrypted, selectBlockedChannelsCount } from 'lbry-redux'; import { doWalletStatus, selectWalletIsEncrypted, selectBlockedChannelsCount } from 'lbry-redux';

View file

@ -11,7 +11,6 @@ import I18nMessage from 'component/i18nMessage';
import Page from 'component/page'; import Page from 'component/page';
import SettingLanguage from 'component/settingLanguage'; import SettingLanguage from 'component/settingLanguage';
import FileSelector from 'component/common/file-selector'; import FileSelector from 'component/common/file-selector';
import WalletSecurityAndSync from '../../component/walletSecurityAndSync';
import { getSavedPassword } from 'util/saved-passwords'; import { getSavedPassword } from 'util/saved-passwords';
type Price = { type Price = {
@ -62,7 +61,7 @@ type Props = {
supportOption: boolean, supportOption: boolean,
userBlockedChannelsCount?: number, userBlockedChannelsCount?: number,
hideBalance: boolean, hideBalance: boolean,
confirmForgetPassword: () => void, confirmForgetPassword: ({}) => void,
floatingPlayer: boolean, floatingPlayer: boolean,
clearPlayingUri: () => void, clearPlayingUri: () => void,
darkModeTimes: DarkModeTimes, darkModeTimes: DarkModeTimes,
@ -150,7 +149,11 @@ class SettingsPage extends React.PureComponent<Props, State> {
onConfirmForgetPassword() { onConfirmForgetPassword() {
const { confirmForgetPassword } = this.props; const { confirmForgetPassword } = this.props;
confirmForgetPassword(); confirmForgetPassword({
callback: () => {
this.setState({ storedPassword: false });
},
});
} }
onChangeTime(event: SyntheticInputEvent<*>, options: OptionTimes) { onChangeTime(event: SyntheticInputEvent<*>, options: OptionTimes) {
@ -245,7 +248,6 @@ class SettingsPage extends React.PureComponent<Props, State> {
<p className="help">{__('LBRY downloads will be saved here.')}</p> <p className="help">{__('LBRY downloads will be saved here.')}</p>
</div> </div>
</section> </section>
<WalletSecurityAndSync />
<section className="card card--section"> <section className="card card--section">
<h2 className="card__title">{__('Network and Data Settings')}</h2> <h2 className="card__title">{__('Network and Data Settings')}</h2>
<Form> <Form>
@ -277,9 +279,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
</Form> </Form>
</section> </section>
<section className="card card--section"> <section className="card card--section">
<header className="card__header"> <h2 className="card__title">{__('Max Purchase Price')}</h2>
<h2 className="card__title">{__('Max Purchase Price')}</h2>
</header>
<Form> <Form>
<FormField <FormField
@ -514,16 +514,18 @@ class SettingsPage extends React.PureComponent<Props, State> {
</React.Fragment> </React.Fragment>
} }
/> />
{this.state.storedPassword && (
<p className="card__subtitle card__help"> {walletEncrypted && this.state.storedPassword && (
{__('Your password is saved in your OS keychain.')}{' '} <FormField
<Button type="checkbox"
button="link" name="save_password"
label={__('I want to type it manually')} onChange={this.onConfirmForgetPassword}
onClick={this.onConfirmForgetPassword} checked={this.state.storedPassword}
/> label={__('Save Password')}
</p> helper={<React.Fragment>{__('Automatically unlock your wallet on startup')}</React.Fragment>}
/>
)} )}
<FormField <FormField
type="checkbox" type="checkbox"
name="hide_balance" name="hide_balance"

View file

@ -312,9 +312,9 @@ export function doNotifyUnlockWallet() {
}; };
} }
export function doNotifyForgetPassword() { export function doNotifyForgetPassword(props) {
return dispatch => { return dispatch => {
dispatch(doOpenModal(MODALS.WALLET_PASSWORD_UNSAVE)); dispatch(doOpenModal(MODALS.WALLET_PASSWORD_UNSAVE, props));
}; };
} }

View file

@ -12,6 +12,7 @@ const defaultState = {
// UX // UX
[SETTINGS.NEW_USER_ACKNOWLEDGED]: false, [SETTINGS.NEW_USER_ACKNOWLEDGED]: false,
[SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED]: false, [SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED]: false,
[SETTINGS.ENABLE_SYNC]: true,
// UI // UI
[SETTINGS.LANGUAGE]: window.localStorage.getItem(SETTINGS.LANGUAGE) || 'en', [SETTINGS.LANGUAGE]: window.localStorage.getItem(SETTINGS.LANGUAGE) || 'en',

View file

@ -23,7 +23,6 @@
&:disabled { &:disabled {
opacity: 0.5; opacity: 0.5;
color: white !important;
} }
} }

View file

@ -61,13 +61,27 @@
margin-bottom: 0; margin-bottom: 0;
} }
.card--claim-preview-wrap {
@extend .card;
margin: var(--spacing-xlarge) 0;
}
.card--claim-preview-selected {
background-color: rgba($lbry-teal-1, 0.1);
&:hover {
transition: transform 0.2s ease-in-out;
transform: scale(1.1);
}
}
// C A R D // C A R D
// A C T I O N S // A C T I O N S
.card__actions { .card__actions {
display: flex; display: flex;
align-items: center; align-items: center;
margin-top: var(--spacing-medium); margin-top: var(--spacing-large);
font-size: var(--font-body); font-size: var(--font-body);
&:only-child { &:only-child {

View file

@ -91,7 +91,6 @@ $border-color--dark: var(--dm-color-04);
padding: var(--spacing-medium); padding: var(--spacing-medium);
&:not(.claim-preview--inline) { &:not(.claim-preview--inline) {
@include mediaThumbHoverZoom;
cursor: pointer; cursor: pointer;
} }
@ -102,12 +101,6 @@ $border-color--dark: var(--dm-color-04);
} }
} }
.claim-preview--injected {
padding: var(--spacing-medium);
position: relative;
}
.claim-preview--injected,
.claim-preview { .claim-preview {
border-top: 1px solid $border-color; border-top: 1px solid $border-color;

View file

@ -13,7 +13,7 @@ form {
.button--inverse { .button--inverse {
&:not(:hover), &:not(:hover),
&:hover { &:hover {
// @extend .button--inverse; @extend .button--inverse;
} }
} }
} }

View file

@ -66,7 +66,7 @@
} }
.main--contained { .main--contained {
max-width: 35rem; max-width: 40rem;
min-width: 25rem; min-width: 25rem;
margin: auto; margin: auto;
margin-top: 5rem; margin-top: 5rem;

View file

@ -3,7 +3,7 @@
height: 100vh; height: 100vh;
align-items: center; align-items: center;
background-color: var(--color-background); background-color: var(--color-background--splash);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;

View file

@ -10,8 +10,16 @@
margin-bottom: var(--spacing-main-padding); margin-bottom: var(--spacing-main-padding);
} }
.section--padded {
padding: var(--spacing-large);
}
.section--small { .section--small {
max-width: 30rem; max-width: 35rem;
}
.section__header {
margin-bottom: var(--spacing-large);
} }
.section__flex { .section__flex {

View file

@ -293,3 +293,7 @@ radio-toggle,
border-color: var(--dm-color-04); border-color: var(--dm-color-04);
} }
} }
.rc-progress-line-path {
stroke: $lbry-teal-3;
}

View file

@ -1,49 +1,47 @@
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
export const setSavedPassword = value => { export const setSavedPassword = value => {
return new Promise( return new Promise(resolve => {
resolve => { ipcRenderer.once('set-password-response', (event, success) => {
ipcRenderer.once('set-password-response', (event, success) => { resolve(success);
resolve(success); });
}); ipcRenderer.send('set-password', value);
ipcRenderer.send('set-password', value); });
},
reject => {
reject(false);
}
);
}; };
export const getSavedPassword = () => { export const getSavedPassword = () => {
return new Promise( return new Promise(resolve => {
resolve => { ipcRenderer.once('get-password-response', (event, password) => {
ipcRenderer.once('get-password-response', (event, password) => { resolve(password);
resolve(password); });
}); ipcRenderer.send('get-password');
ipcRenderer.send('get-password'); });
}, };
reject => reject(false)
); export const deleteSavedPassword = () => {
return new Promise(resolve => {
// @if TARGET='app'
ipcRenderer.once('delete-password-response', (event, success) => {
resolve();
});
ipcRenderer.send('delete-password');
// @endif;
});
}; };
export const deleteAuthToken = () => { export const deleteAuthToken = () => {
return new Promise( return new Promise(resolve => {
resolve => { // @if TARGET='app'
// @if TARGET='app' ipcRenderer.once('delete-auth-token-response', (event, success) => {
ipcRenderer.once('delete-auth-token-response', (event, success) => {
resolve();
});
ipcRenderer.send('delete-auth-token');
// @endif;
// @if TARGET='web'
document.cookie = 'auth_token= ; expires = Thu, 01 Jan 1970 00:00:00 GMT';
resolve(); resolve();
// @endif });
}, ipcRenderer.send('delete-auth-token');
reject => { // @endif;
reject(false); // @if TARGET='web'
} document.cookie = 'auth_token= ; expires = Thu, 01 Jan 1970 00:00:00 GMT';
); resolve();
// @endif
});
}; };
export const testKeychain = () => { export const testKeychain = () => {

View file

@ -701,14 +701,8 @@
"Get ??? LBC": "Get ??? LBC", "Get ??? LBC": "Get ??? LBC",
"LBRY names cannot contain spaces or reserved symbols ($#@;/\"<>%{}|^~[]`)": "LBRY names cannot contain spaces or reserved symbols ($#@;/\"<>%{}|^~[]`)", "LBRY names cannot contain spaces or reserved symbols ($#@;/\"<>%{}|^~[]`)": "LBRY names cannot contain spaces or reserved symbols ($#@;/\"<>%{}|^~[]`)",
"Creating channel...": "Creating channel...", "Creating channel...": "Creating channel...",
"From": "From", "Remember Password": "Remember Password",
"To": "To", "You will no longer see this at startup": "You will no longer see this at startup",
"Multi-language support is brand new and incomplete. Switching your language may have unintended consequences, like glossolalia.": "Multi-language support is brand new and incomplete. Switching your language may have unintended consequences, like glossolalia.",
"discovery": "discovery",
"This will add a Support button along side tipping. Similar to tips, supports help %discovery_link% but the LBC is returned to your wallet if revoked. Both also help secure your %vanity_names_link%.": "This will add a Support button along side tipping. Similar to tips, supports help %discovery_link% but the LBC is returned to your wallet if revoked. Both also help secure your %vanity_names_link%.",
"Tip %amount% LBC": "Tip %amount% LBC",
"Not enough credits": "Not enough credits",
"You have %credit_amount% in unclaimed rewards.": "You have %credit_amount% in unclaimed rewards.",
"URI does not include name.": "URI does not include name.", "URI does not include name.": "URI does not include name.",
"to fix it. If that doesn't work, press CMD/CTRL-R to reset to the homepage.": "to fix it. If that doesn't work, press CMD/CTRL-R to reset to the homepage.", "to fix it. If that doesn't work, press CMD/CTRL-R to reset to the homepage.": "to fix it. If that doesn't work, press CMD/CTRL-R to reset to the homepage.",
"Add Email": "Add Email", "Add Email": "Add Email",

View file

@ -6850,17 +6850,17 @@ lazy-val@^1.0.3, lazy-val@^1.0.4:
yargs "^13.2.2" yargs "^13.2.2"
zstd-codec "^0.1.1" zstd-codec "^0.1.1"
lbry-redux@lbryio/lbry-redux#04ae0913a444abac200731c7ed53796d763a0bbb: lbry-redux@lbryio/lbry-redux#d44cd9ca56dee784dba42c0cc13061ae75cbd46c:
version "0.0.1" version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/04ae0913a444abac200731c7ed53796d763a0bbb" resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/d44cd9ca56dee784dba42c0cc13061ae75cbd46c"
dependencies: dependencies:
proxy-polyfill "0.1.6" proxy-polyfill "0.1.6"
reselect "^3.0.0" reselect "^3.0.0"
uuid "^3.3.2" uuid "^3.3.2"
lbryinc@lbryio/lbryinc#d99232ebc754a49649a2ff4132478415faef08e2: lbryinc@lbryio/lbryinc#368040d64658cf2a4b8a7a6725ec1787329ce65d:
version "0.0.1" version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/d99232ebc754a49649a2ff4132478415faef08e2" resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/368040d64658cf2a4b8a7a6725ec1787329ce65d"
dependencies: dependencies:
reselect "^3.0.0" reselect "^3.0.0"
@ -9660,6 +9660,13 @@ react-compound-slider@^1.2.2:
prop-types "^15.7.2" prop-types "^15.7.2"
warning "^3.0.0" warning "^3.0.0"
react-confetti@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/react-confetti/-/react-confetti-4.0.1.tgz#f9e76b198ce02f1c13809a1d1ec1bc92f5450dde"
integrity sha512-uQrb1Q4p8Wg3xyxSGtsIxdd+hOd3jRNpVq5qET6m9B+fihsjF7mHbMngoiziya3DZtstaqCBPpTcyByXLu8CnQ==
dependencies:
tween-functions "^1.2.0"
react-dom@^16.8.2, react-dom@^16.8.6: react-dom@^16.8.2, react-dom@^16.8.6:
version "16.8.6" version "16.8.6"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f"
@ -11696,6 +11703,11 @@ tunnel-agent@^0.6.0:
dependencies: dependencies:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
tween-functions@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/tween-functions/-/tween-functions-1.2.0.tgz#1ae3a50e7c60bb3def774eac707acbca73bbc3ff"
integrity sha1-GuOlDnxguz3vd06scHrLynO7w/8=
tweetnacl@^0.14.3, tweetnacl@~0.14.0: tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5" version "0.14.5"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"