Skip to content

Commit

Permalink
Merge pull request #14443 from Expensify/cristi_passwordless-auto-login
Browse files Browse the repository at this point in the history
Passwordless - Web automatic login
  • Loading branch information
marcochavezf authored Feb 14, 2023
2 parents 1a5249d + da56773 commit c61c36f
Show file tree
Hide file tree
Showing 19 changed files with 2,055 additions and 49 deletions.
710 changes: 710 additions & 0 deletions assets/images/product-illustrations/abracadabra.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
931 changes: 931 additions & 0 deletions assets/images/product-illustrations/magic-code.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/components/DeeplinkWrapper/deeplinkRoutes.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ROUTES from '../../ROUTES';
import Permissions from '../../libs/Permissions';

/** @type {Array<object>} Routes regex used for desktop deeplinking */
export default [
Expand All @@ -21,6 +22,10 @@ export default [
{
// /v/*
pattern: '/v($|(//*))',

// Disable deep linking in desktop App when passwordless is enabled because
// we want to open the magic link in its own tab
isDisabled: betas => Permissions.canUsePasswordlessLogins(betas),
},
{
// /bank-account/*
Expand Down
13 changes: 12 additions & 1 deletion src/components/DeeplinkWrapper/index.website.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@ import _ from 'underscore';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import React, {PureComponent} from 'react';
import {withOnyx} from 'react-native-onyx';
import deeplinkRoutes from './deeplinkRoutes';
import FullScreenLoadingIndicator from '../FullscreenLoadingIndicator';
import TextLink from '../TextLink';
import * as Illustrations from '../Icon/Illustrations';
import withLocalize, {withLocalizePropTypes} from '../withLocalize';
import Text from '../Text';
import styles from '../../styles/styles';
import compose from '../../libs/compose';
import CONST from '../../CONST';
import CONFIG from '../../CONFIG';
import Icon from '../Icon';
import * as Expensicons from '../Icon/Expensicons';
import colors from '../../styles/colors';
import * as Browser from '../../libs/Browser';
import ONYXKEYS from '../../ONYXKEYS';

const propTypes = {
/** Children to render. */
Expand Down Expand Up @@ -54,6 +57,9 @@ class DeeplinkWrapper extends PureComponent {

// check if pathname matches with deeplink routes
const matchedRoute = _.find(deeplinkRoutes, (route) => {
if (route.isDisabled && route.isDisabled(this.props.betas)) {
return false;
}
const routeRegex = new RegExp(route.pattern);
return routeRegex.test(window.location.pathname);
});
Expand Down Expand Up @@ -156,4 +162,9 @@ class DeeplinkWrapper extends PureComponent {
}

DeeplinkWrapper.propTypes = propTypes;
export default withLocalize(DeeplinkWrapper);
export default compose(
withLocalize,
withOnyx({
betas: {key: ONYXKEYS.BETAS},
}),
)(DeeplinkWrapper);
4 changes: 4 additions & 0 deletions src/components/Icon/Illustrations.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Abracadabra from '../../../assets/images/product-illustrations/abracadabra.svg';
import BankArrowPink from '../../../assets/images/product-illustrations/bank-arrow--pink.svg';
import BankMouseGreen from '../../../assets/images/product-illustrations/bank-mouse--green.svg';
import BankUserGreen from '../../../assets/images/product-illustrations/bank-user--green.svg';
Expand All @@ -9,6 +10,7 @@ import JewelBoxBlue from '../../../assets/images/product-illustrations/jewel-box
import JewelBoxGreen from '../../../assets/images/product-illustrations/jewel-box--green.svg';
import JewelBoxPink from '../../../assets/images/product-illustrations/jewel-box--pink.svg';
import JewelBoxYellow from '../../../assets/images/product-illustrations/jewel-box--yellow.svg';
import MagicCode from '../../../assets/images/product-illustrations/magic-code.svg';
import MoneyEnvelopeBlue from '../../../assets/images/product-illustrations/money-envelope--blue.svg';
import MoneyMousePink from '../../../assets/images/product-illustrations/money-mouse--pink.svg';
import ReceiptsSearchYellow from '../../../assets/images/product-illustrations/receipts-search--yellow.svg';
Expand Down Expand Up @@ -37,6 +39,7 @@ import TreasureChest from '../../../assets/images/simple-illustrations/simple-il
import ThumbsUpStars from '../../../assets/images/simple-illustrations/simple-illustration__thumbsupstars.svg';

export {
Abracadabra,
BankArrowPink,
BankMouseGreen,
BankUserGreen,
Expand All @@ -48,6 +51,7 @@ export {
JewelBoxGreen,
JewelBoxPink,
JewelBoxYellow,
MagicCode,
MoneyEnvelopeBlue,
MoneyMousePink,
ReceiptsSearchYellow,
Expand Down
91 changes: 91 additions & 0 deletions src/components/ValidateCodeModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {View} from 'react-native';
import colors from '../styles/colors';
import styles from '../styles/styles';
import Icon from './Icon';
import withLocalize, {withLocalizePropTypes} from './withLocalize';
import Text from './Text';
import * as Expensicons from './Icon/Expensicons';
import * as Illustrations from './Icon/Illustrations';
import variables from '../styles/variables';
import TextLink from './TextLink';

const propTypes = {

/** Whether the user has been signed in with the link. */
isSuccessfullySignedIn: PropTypes.bool,

/** Code to display. */
code: PropTypes.string.isRequired,

/** Whether the user can get signed straight in the App from the current page */
shouldShowSignInHere: PropTypes.bool,

/** Callback to be called when user clicks the Sign in here link */
onSignInHereClick: PropTypes.func,

...withLocalizePropTypes,
};

const defaultProps = {
isSuccessfullySignedIn: false,
shouldShowSignInHere: false,
onSignInHereClick: () => {},
};

class ValidateCodeModal extends PureComponent {
render() {
return (
<View style={styles.deeplinkWrapperContainer}>
<View style={styles.deeplinkWrapperMessage}>
<View style={styles.mb2}>
<Icon
width={variables.modalTopIconWidth}
height={this.props.isSuccessfullySignedIn ? variables.modalTopBigIconHeight : variables.modalTopIconHeight}
src={this.props.isSuccessfullySignedIn ? Illustrations.Abracadabra : Illustrations.MagicCode}
/>
</View>
<Text style={[styles.textHeadline, styles.textXXLarge, styles.textAlignCenter]}>
{this.props.translate(this.props.isSuccessfullySignedIn ? 'validateCodeModal.successfulSignInTitle' : 'validateCodeModal.title')}
</Text>
<View style={[styles.mt2, styles.mb2]}>
<Text style={[styles.fontSizeNormal, styles.textAlignCenter]}>
{this.props.translate(this.props.isSuccessfullySignedIn ? 'validateCodeModal.successfulSignInDescription' : 'validateCodeModal.description')}
{this.props.shouldShowSignInHere
&& (
<>
{this.props.translate('validateCodeModal.or')}
{' '}
<TextLink onPress={this.props.onSignInHereClick}>
{this.props.translate('validateCodeModal.signInHere')}
</TextLink>
</>
)}
{this.props.shouldShowSignInHere ? '!' : '.'}
</Text>
</View>
{!this.props.isSuccessfullySignedIn && (
<View style={styles.mt6}>
<Text style={styles.magicCodeDigits}>
{this.props.code}
</Text>
</View>
)}
</View>
<View style={styles.deeplinkWrapperFooter}>
<Icon
width={variables.modalWordmarkWidth}
height={variables.modalWordmarkHeight}
fill={colors.green}
src={Expensicons.ExpensifyWordmark}
/>
</View>
</View>
);
}
}

ValidateCodeModal.propTypes = propTypes;
ValidateCodeModal.defaultProps = defaultProps;
export default withLocalize(ValidateCodeModal);
8 changes: 8 additions & 0 deletions src/languages/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,14 @@ export default {
youCanAlso: 'You can also',
openLinkInBrowser: 'open this link in your browser',
},
validateCodeModal: {
successfulSignInTitle: 'Abracadabra,\nyou are signed in!',
successfulSignInDescription: 'Head back to your original tab to continue.',
title: 'Here is your magic code',
description: 'Please enter the code using the device\nwhere it was originally requested',
or: ', or',
signInHere: 'just sign in here',
},
iOUConfirmationList: {
whoPaid: 'Who paid?',
whoWasThere: 'Who was there?',
Expand Down
8 changes: 8 additions & 0 deletions src/languages/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,14 @@ export default {
youCanAlso: 'También puedes',
openLinkInBrowser: 'abrir este enlace en tu navegador',
},
validateCodeModal: {
successfulSignInTitle: 'Abracadabra,\n¡sesión iniciada!',
successfulSignInDescription: 'Vuelve a la pestaña original para continuar.',
title: 'Aquí está tu código mágico',
or: ', ¡o',
description: 'Por favor, introduzca el código utilizando el dispositivo\nen el que se solicitó originalmente',
signInHere: 'simplemente inicia sesión aquí',
},
iOUConfirmationList: {
whoPaid: '¿Quién pago?',
whoWasThere: '¿Quién asistió?',
Expand Down
73 changes: 72 additions & 1 deletion src/libs/actions/Session/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import * as Welcome from '../Welcome';
import * as API from '../../API';
import * as NetworkStore from '../../Network/NetworkStore';
import DateUtils from '../../DateUtils';
import Navigation from '../../Navigation/Navigation';
import ROUTES from '../../../ROUTES';

let credentials = {};
Onyx.connect({
Expand Down Expand Up @@ -133,6 +135,13 @@ function beginSignIn(login) {
isLoading: false,
},
},
{
onyxMethod: CONST.ONYX.METHOD.MERGE,
key: ONYXKEYS.CREDENTIALS,
value: {
validateCode: null,
},
},
];

const failureData = [
Expand Down Expand Up @@ -237,8 +246,16 @@ function signIn(password, validateCode, twoFactorAuthCode) {
},
];

const params = {twoFactorAuthCode};
if (credentials.login) {
// The user initiated the sign in operation on the current device, sign in with the email
params.email = credentials.login;
} else {
// The user is signing in with the accountID and validateCode from the magic link
params.accountID = credentials.accountID;
}

// Conditionally pass a password or validateCode to command since we temporarily allow both flows
const params = {email: credentials.login, twoFactorAuthCode};
if (validateCode) {
params.validateCode = validateCode;
} else {
Expand All @@ -248,6 +265,58 @@ function signIn(password, validateCode, twoFactorAuthCode) {
API.write('SigninUser', params, {optimisticData, successData, failureData});
}

function signInWithValidateCode(accountID, validateCode) {
const optimisticData = [
{
onyxMethod: CONST.ONYX.METHOD.MERGE,
key: ONYXKEYS.ACCOUNT,
value: {
...CONST.DEFAULT_ACCOUNT_DATA,
isLoading: true,
},
},
];

const successData = [
{
onyxMethod: CONST.ONYX.METHOD.MERGE,
key: ONYXKEYS.ACCOUNT,
value: {
isLoading: false,
},
},
{
onyxMethod: CONST.ONYX.METHOD.MERGE,
key: ONYXKEYS.CREDENTIALS,
value: {
accountID,
validateCode,
},
},
];

const failureData = [
{
onyxMethod: CONST.ONYX.METHOD.MERGE,
key: ONYXKEYS.ACCOUNT,
value: {
isLoading: false,
},
},
];

// This is temporary for now. Server should login with the accountID and validateCode
API.write('SigninUser', {
validateCode,
accountID,
}, {optimisticData, successData, failureData});
}

function signInWithValidateCodeAndNavigate(accountID, validateCode) {
signInWithValidateCode(accountID, validateCode);
Navigation.navigate(ROUTES.HOME);
}

/**
* User forgot the password so let's send them the link to reset their password
*/
Expand Down Expand Up @@ -466,6 +535,8 @@ export {
beginSignIn,
updatePasswordAndSignin,
signIn,
signInWithValidateCode,
signInWithValidateCodeAndNavigate,
signInWithShortLivedAuthToken,
cleanupSession,
signOut,
Expand Down
2 changes: 1 addition & 1 deletion src/pages/SetPasswordPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import lodashGet from 'lodash/get';
import {
propTypes as validateLinkPropTypes,
defaultProps as validateLinkDefaultProps,
} from './validateLinkPropTypes';
} from './ValidateLoginPage/validateLinkPropTypes';
import styles from '../styles/styles';
import * as Session from '../libs/actions/Session';
import ONYXKEYS from '../ONYXKEYS';
Expand Down
34 changes: 0 additions & 34 deletions src/pages/ValidateLoginPage.js

This file was deleted.

Loading

0 comments on commit c61c36f

Please sign in to comment.