Added phone verification functionality

This commit is contained in:
Liam Cardenas 2018-01-15 05:32:01 -08:00
parent b9b7af2bbd
commit 4409315353
15 changed files with 225 additions and 59 deletions

View file

@ -1,6 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import { doUserFieldNew, doUserInviteNew } from 'redux/actions/user';
import { doUserEmailNew, doUserPhoneNew, doUserInviteNew } from 'redux/actions/user';
import { selectEmailNewIsPending, selectEmailNewErrorMessage } from 'redux/selectors/user';
import UserFieldNew from './view';
@ -10,7 +10,8 @@ const select = state => ({
});
const perform = dispatch => ({
addUserEmail: email => dispatch(doUserFieldNew(email)),
addUserEmail: email => dispatch(doUserEmailNew(email)),
addUserPhone: phone => dispatch(doUserPhoneNew(phone)),
});
export default connect(select, perform)(UserFieldNew);

View file

@ -6,25 +6,58 @@ class UserFieldNew extends React.PureComponent {
super(props);
this.state = {
phone: '',
email: '',
};
}
handleEmailChanged(event) {
handleChanged(event, fieldType) {
console.log({
[fieldType]: event.target.value,
});
this.setState({
email: event.target.value,
[fieldType]: event.target.value,
});
}
handleSubmit() {
const { email } = this.state;
this.props.addUserEmail(email);
const { email, phone } = this.state;
if (phone) {
this.props.addUserPhone(phone);
} else {
this.props.addUserEmail(email);
}
}
render() {
const { cancelButton, errorMessage, isPending } = this.props;
const { cancelButton, errorMessage, isPending, fieldType } = this.props;
return (
return fieldType === 'phone' ? (
<div>
<p>
{__(
'Enter your phone number and we will send you a verification code. We will not share your phone number with third parties.'
)}
</p>
<Form onSubmit={this.handleSubmit.bind(this)}>
<FormRow
type="text"
label="Phone"
placeholder="(555) 555-5555"
name="phone"
value={this.state.phone}
errorMessage={errorMessage}
onChange={event => {
this.handleChanged(event, 'phone');
}}
/>
<div className="form-row-submit">
<Submit label="Submit" disabled={isPending} />
{cancelButton}
</div>
</Form>
</div>
) : (
<div>
<p>
{__("We'll let you know about LBRY updates, security issues, and great new content.")}
@ -39,7 +72,7 @@ class UserFieldNew extends React.PureComponent {
value={this.state.email}
errorMessage={errorMessage}
onChange={event => {
this.handleEmailChanged(event);
this.handleChanged(event, 'email');
}}
/>
<div className="form-row-submit">

View file

@ -1,9 +1,10 @@
import React from 'react';
import { connect } from 'react-redux';
import { doUserFieldVerify, doUserFieldVerifyFailure } from 'redux/actions/user';
import { doUserEmailVerify, doUserPhoneVerify, doUserEmailVerifyFailure } from 'redux/actions/user';
import {
selectEmailVerifyIsPending,
selectEmailToVerify,
selectPhoneToVerify,
selectEmailVerifyErrorMessage,
} from 'redux/selectors/user';
import UserFieldVerify from './view';
@ -11,12 +12,14 @@ import UserFieldVerify from './view';
const select = state => ({
isPending: selectEmailVerifyIsPending(state),
email: selectEmailToVerify(state),
phone: selectPhoneToVerify(state),
errorMessage: selectEmailVerifyErrorMessage(state),
});
const perform = dispatch => ({
verifyUserEmail: (code, recaptcha) => dispatch(doUserFieldVerify(code, recaptcha)),
verifyUserEmailFailure: error => dispatch(doUserFieldVerifyFailure(error)),
verifyUserEmail: (code, recaptcha) => dispatch(doUserEmailVerify(code, recaptcha)),
verifyUserPhone: code => dispatch(doUserPhoneVerify(code)),
verifyUserEmailFailure: error => dispatch(doUserEmailVerifyFailure(error)),
});
export default connect(select, perform)(UserFieldVerify);

View file

@ -19,19 +19,24 @@ class UserFieldVerify extends React.PureComponent {
handleSubmit() {
const { code } = this.state;
try {
const verification = JSON.parse(atob(code));
this.props.verifyUserEmail(verification.token, verification.recaptcha);
} catch (error) {
this.props.verifyUserEmailFailure('Invalid Verification Token');
const { fieldType } = this.props;
if (fieldType === 'phone') {
this.props.verifyUserPhone(code);
} else {
try {
const verification = JSON.parse(atob(code));
this.props.verifyUserEmail(verification.token, verification.recaptcha);
} catch (error) {
this.props.verifyUserEmailFailure('Invalid Verification Token');
}
}
}
render() {
const { cancelButton, errorMessage, email, isPending } = this.props;
const { cancelButton, errorMessage, email, isPending, phone } = this.props;
return (
<Form onSubmit={this.handleSubmit.bind(this)}>
<p>Please enter the verification code emailed to {email}.</p>
<p>Please enter the verification code sent to {phone || email}.</p>
<FormRow
type="text"
label={__('Verification Code')}

View file

@ -9,6 +9,9 @@ import {
selectIdentityVerifyErrorMessage,
} from 'redux/selectors/user';
import UserVerify from './view';
import { selectCurrentModal } from 'redux/selectors/app';
import { doOpenModal } from 'redux/actions/app';
import { PHONE_COLLECTION } from 'constants/modal_types';
const select = (state, props) => {
const selectReward = makeSelectRewardByType();
@ -17,12 +20,14 @@ const select = (state, props) => {
isPending: selectIdentityVerifyIsPending(state),
errorMessage: selectIdentityVerifyErrorMessage(state),
reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }),
modal: selectCurrentModal(state),
};
};
const perform = dispatch => ({
navigate: uri => dispatch(doNavigate(uri)),
verifyUserIdentity: token => dispatch(doUserIdentityVerify(token)),
verifyPhone: () => dispatch(doOpenModal(PHONE_COLLECTION)),
});
export default connect(select, perform)(UserVerify);

View file

@ -23,7 +23,7 @@ class UserVerify extends React.PureComponent {
}
render() {
const { errorMessage, isPending, navigate } = this.props;
const { errorMessage, isPending, navigate, verifyPhone, modal } = this.props;
return (
<div>
<section className="card card--form">
@ -74,12 +74,13 @@ class UserVerify extends React.PureComponent {
)}`}
</div>
<div className="card__actions">
{errorMessage && <p className="form-field__error">{errorMessage}</p>}
<CardVerify
<Link
onClick={() => {
verifyPhone();
}}
button="alt"
icon="icon-phone"
label={__('Submit Phone Number')}
disabled={isPending}
token={this.onToken.bind(this)}
stripeKey={lbryio.getStripeToken()}
/>
</div>
<div className="card__content">

View file

@ -108,6 +108,14 @@ export const USER_EMAIL_NEW_FAILURE = 'USER_EMAIL_NEW_FAILURE';
export const USER_EMAIL_VERIFY_STARTED = 'USER_EMAIL_VERIFY_STARTED';
export const USER_EMAIL_VERIFY_SUCCESS = 'USER_EMAIL_VERIFY_SUCCESS';
export const USER_EMAIL_VERIFY_FAILURE = 'USER_EMAIL_VERIFY_FAILURE';
export const USER_PHONE_DECLINE = 'USER_PHONE_DECLINE';
export const USER_PHONE_NEW_STARTED = 'USER_PHONE_NEW_STARTED';
export const USER_PHONE_NEW_SUCCESS = 'USER_PHONE_NEW_SUCCESS';
export const USER_PHONE_NEW_EXISTS = 'USER_PHONE_NEW_EXISTS';
export const USER_PHONE_NEW_FAILURE = 'USER_PHONE_NEW_FAILURE';
export const USER_PHONE_VERIFY_STARTED = 'USER_PHONE_VERIFY_STARTED';
export const USER_PHONE_VERIFY_SUCCESS = 'USER_PHONE_VERIFY_SUCCESS';
export const USER_PHONE_VERIFY_FAILURE = 'USER_PHONE_VERIFY_FAILURE';
export const USER_IDENTITY_VERIFY_STARTED = 'USER_IDENTITY_VERIFY_STARTED';
export const USER_IDENTITY_VERIFY_SUCCESS = 'USER_IDENTITY_VERIFY_SUCCESS';
export const USER_IDENTITY_VERIFY_FAILURE = 'USER_IDENTITY_VERIFY_FAILURE';

View file

@ -6,7 +6,7 @@ export const ERROR = 'error';
export const INSUFFICIENT_CREDITS = 'insufficient_credits';
export const UPGRADE = 'upgrade';
export const WELCOME = 'welcome';
export const EMAIL_COLLECTION = 'email_collection';
export const PHONE_COLLECTION = 'phone_collection';
export const FIRST_REWARD = 'first_reward';
export const AUTHENTICATION_FAILURE = 'auth_failure';
export const TRANSACTION_FAILED = 'transaction_failed';

View file

@ -12,7 +12,7 @@ import { Provider } from 'react-redux';
import { doConditionalAuthNavigate, doDaemonReady, doShowSnackBar } from 'redux/actions/app';
import { doNavigate } from 'redux/actions/navigation';
import { doDownloadLanguages } from 'redux/actions/settings';
import { doUserFieldVerify } from 'redux/actions/user';
import { doUserEmailVerify } from 'redux/actions/user';
import 'scss/all.scss';
import store from 'store';
import app from './app';
@ -35,7 +35,7 @@ ipcRenderer.on('open-uri-requested', (event, uri, newSession) => {
}
if (verification.token && verification.recaptcha) {
app.store.dispatch(doConditionalAuthNavigate(newSession));
app.store.dispatch(doUserFieldVerify(verification.token, verification.recaptcha));
app.store.dispatch(doUserEmailVerify(verification.token, verification.recaptcha));
} else {
app.store.dispatch(doShowSnackBar({ message: 'Invalid Verification URI' }));
}

View file

@ -3,18 +3,19 @@ import * as settings from 'constants/settings';
import { connect } from 'react-redux';
import { doCloseModal } from 'redux/actions/app';
import { doSetClientSetting } from 'redux/actions/settings';
import { selectEmailToVerify, selectUser } from 'redux/selectors/user';
import { selectPhoneToVerify, selectUser } from 'redux/selectors/user';
import ModalPhoneCollection from './view';
import { doNavigate } from 'redux/actions/navigation';
const select = state => ({
email: selectEmailToVerify(state),
phone: selectPhoneToVerify(state),
user: selectUser(state),
});
const perform = dispatch => () => ({
closeModal: () => {
dispatch(doSetClientSetting(settings.EMAIL_COLLECTION_ACKNOWLEDGED, true));
dispatch(doCloseModal());
dispatch(doNavigate('/rewards'));
},
});

View file

@ -6,14 +6,14 @@ import UserFieldVerify from 'component/userFieldVerify';
class ModalPhoneCollection extends React.PureComponent {
renderInner() {
const { closeModal, email, user } = this.props;
const { closeModal, phone, user } = this.props;
const cancelButton = <Link button="text" onClick={closeModal} label={__('Not Now')} />;
if (!user.has_verified_email && !email) {
return <UserFieldNew cancelButton={cancelButton} />;
} else if (!user.has_verified_email) {
return <UserFieldVerify cancelButton={cancelButton} />;
if (!user.phone_number && !phone) {
return <UserFieldNew cancelButton={cancelButton} fieldType="phone" />;
} else if (!user.phone_number) {
return <UserFieldVerify cancelButton={cancelButton} fieldType="phone" />;
}
closeModal();
}

View file

@ -40,11 +40,10 @@ class ModalRouter extends React.PureComponent {
return;
}
const transitionModal = [
this.checkShowWelcome,
this.checkShowEmail,
this.checkShowCreditIntro,
].reduce((acc, func) => (!acc ? func.bind(this)(props) : acc), false);
const transitionModal = [this.checkShowWelcome, this.checkShowCreditIntro].reduce(
(acc, func) => (!acc ? func.bind(this)(props) : acc),
false
);
if (
transitionModal &&
@ -65,18 +64,6 @@ class ModalRouter extends React.PureComponent {
}
}
checkShowEmail(props) {
const { isEmailCollectionAcknowledged, isVerificationCandidate, user } = props;
if (
!isEmailCollectionAcknowledged &&
isVerificationCandidate &&
user &&
!user.has_verified_email
) {
return modals.EMAIL_COLLECTION;
}
}
checkShowCreditIntro(props) {
const { balance, page, isCreditIntroAcknowledged } = props;
@ -124,7 +111,7 @@ class ModalRouter extends React.PureComponent {
return <ModalAffirmPurchase {...modalProps} />;
case modals.CONFIRM_CLAIM_REVOKE:
return <ModalRevokeClaim {...modalProps} />;
case modals.EMAIL_COLLECTION:
case modals.PHONE_COLLECTION:
return <ModalPhoneCollection {...modalProps} />;
default:
return null;

View file

@ -3,7 +3,7 @@ import * as MODALS from 'constants/modal_types';
import Lbryio from 'lbryio';
import { doOpenModal, doShowSnackBar } from 'redux/actions/app';
import { doClaimRewardType, doRewardList } from 'redux/actions/rewards';
import { selectEmailToVerify } from 'redux/selectors/user';
import { selectEmailToVerify, selectPhoneToVerify } from 'redux/selectors/user';
import rewards from 'rewards';
export function doFetchInviteStatus() {
@ -78,7 +78,72 @@ export function doUserFetch() {
};
}
export function doUserFieldNew(email) {
export function doUserPhoneNew(phone) {
return dispatch => {
dispatch({
type: ACTIONS.USER_PHONE_NEW_STARTED,
phone,
});
const success = () => {
dispatch({
type: ACTIONS.USER_PHONE_NEW_SUCCESS,
data: { phone },
});
};
const failure = error => {
dispatch({
type: ACTIONS.USER_PHONE_NEW_FAILURE,
data: { error },
});
};
Lbryio.call('user_phone', 'new', { phone_number: phone, country_code: 1 }, 'post').then(
success,
failure
);
};
}
export function doUserPhoneVerifyFailure(error) {
return {
type: ACTIONS.USER_PHONE_VERIFY_FAILURE,
data: { error },
};
}
export function doUserPhoneVerify(verificationCode) {
return (dispatch, getState) => {
const phone_number = selectPhoneToVerify(getState());
dispatch({
type: ACTIONS.USER_PHONE_VERIFY_STARTED,
code: verificationCode,
});
Lbryio.call(
'user_phone',
'confirm',
{
verification_code: verificationCode,
phone_number,
country_code: '1',
},
'post'
)
.then(userEmail => {
dispatch({
type: ACTIONS.USER_PHONE_VERIFY_SUCCESS,
data: { phone_number },
});
dispatch(doUserFetch());
})
.catch(error => dispatch(doUserPhoneVerifyFailure(error)));
};
}
export function doUserEmailNew(email) {
return dispatch => {
dispatch({
type: ACTIONS.USER_EMAIL_NEW_STARTED,
@ -116,14 +181,14 @@ export function doUserFieldNew(email) {
};
}
export function doUserFieldVerifyFailure(error) {
export function doUserEmailVerifyFailure(error) {
return {
type: ACTIONS.USER_EMAIL_VERIFY_FAILURE,
data: { error },
};
}
export function doUserFieldVerify(verificationToken, recaptcha) {
export function doUserEmailVerify(verificationToken, recaptcha) {
return (dispatch, getState) => {
const email = selectEmailToVerify(getState());
@ -154,7 +219,7 @@ export function doUserFieldVerify(verificationToken, recaptcha) {
throw new Error('Your email is still not verified.'); // shouldn't happen
}
})
.catch(error => dispatch(doUserFieldVerifyFailure(error)));
.catch(error => dispatch(doUserEmailVerifyFailure(error)));
};
}

View file

@ -55,6 +55,52 @@ reducers[ACTIONS.USER_FETCH_FAILURE] = state =>
user: null,
});
reducers[ACTIONS.USER_PHONE_NEW_STARTED] = state =>
Object.assign({}, state, {
phoneNewIsPending: true,
phoneNewErrorMessage: '',
});
reducers[ACTIONS.USER_PHONE_NEW_SUCCESS] = (state, action) =>
Object.assign({}, state, {
phoneToVerify: action.data.phone,
phoneNewIsPending: false,
});
reducers[ACTIONS.USER_PHONE_NEW_EXISTS] = (state, action) =>
Object.assign({}, state, {
phoneToVerify: action.data.phone,
phoneNewIsPending: false,
});
reducers[ACTIONS.USER_PHONE_NEW_FAILURE] = (state, action) =>
Object.assign({}, state, {
phoneNewIsPending: false,
phoneNewErrorMessage: action.data.error,
});
reducers[ACTIONS.USER_PHONE_VERIFY_STARTED] = state =>
Object.assign({}, state, {
phoneVerifyIsPending: true,
phoneVerifyErrorMessage: '',
});
reducers[ACTIONS.USER_PHONE_VERIFY_SUCCESS] = (state, action) => {
const user = Object.assign({}, state.user);
user.phone_number = action.data.phone_number;
return Object.assign({}, state, {
phoneToVerify: '',
phoneVerifyIsPending: false,
user,
});
};
reducers[ACTIONS.USER_PHONE_VERIFY_FAILURE] = (state, action) =>
Object.assign({}, state, {
phoneVerifyIsPending: false,
phoneVerifyErrorMessage: action.data.error,
});
reducers[ACTIONS.USER_EMAIL_NEW_STARTED] = state =>
Object.assign({}, state, {
emailNewIsPending: true,

View file

@ -16,12 +16,23 @@ export const selectUserEmail = createSelector(
user => (user ? user.primary_email : null)
);
export const selectUserPhone = createSelector(
selectUser,
user => (user ? user.phone_number : null)
);
export const selectEmailToVerify = createSelector(
selectState,
selectUserEmail,
(state, userEmail) => state.emailToVerify || userEmail
);
export const selectPhoneToVerify = createSelector(
selectState,
selectUserPhone,
(state, userPhone) => state.phoneToVerify || userPhone
);
export const selectUserIsRewardApproved = createSelector(
selectUser,
user => user && user.is_reward_approved