Hello everyone, in this article we will add password management to our application.
Displaying passwords
To start this article, we will connect the Password.js screen to redux and retrieve the password list that is in the data reducer.
As usual, we will add the imports for connect, bindActionCreators, and ReduxState:
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import type { ReduxState } from '../reducers/types'Then we add the mapStateToProps function and connect the screen:
function mapStateToProps(state: ReduxState) {
const passwords = _.values(state.data.passwords.byId)
return {
passwords,
}
}
export default connect(mapStateToProps, dispatch => ({
actions: bindActionCreators({}, dispatch),
}))(PassworsScreen)Here we use Lodash's values function to transform our byId object, which contains passwords in the following form, into an array of objects:
byId: {
idPassword1: {
/* password attributes */
}
idPassword2: {
/* password 2 attributes */
}
}And we add an empty object in bindActionCreators because we do not have any actions for now.
Now we need to modify the screen's State type and add the Props type:
type State = {
searchResults: Array<Password>,
searchValue: string,
}
type Props = {
passwords: Array<Password>,
navigation: Object,
}Now that we have removed the passwords attribute from state, we need to replace every occurrence of this.state.passwords with this.props.passwords and also modify the component's initial state:
state = {
searchValue: '',
searchResults: this.props.passwords,
}We define that the search result list we use as the data source for our PasswordList component will have this.props.passwords as its value.
Currently one problem remains: because we use this.state.searchResults as the data source and define its values only when the component is initialized, when the this.props.passwords password list is altered (add/edit/delete), the state will not be updated and therefore the changes will not be reflected on screen.
To solve this problem, add the following code:
componentWillReceiveProps(nextProps: Props) {
this.setState({
searchResults: nextProps.passwords,
});
}The componentWillReceiveProps function is executed automatically whenever the component's props are modified.
In this way we solve the state update problem because, as soon as the list is modified, we will update the state automatically.
Actions, action types, and reducers
We will start by adding our action types for adding, editing, and deleting a password, as well as deleting all passwords.
Add the following types in the actions/types.js file:
export type AddPasswordAction = {
type: 'ADD_PASSWORD',
password: Password,
};
export type UpdatePasswordAction = {
type: 'UPDATE_PASSWORD',
password: Password,
};
export type DeletePasswordAction = {
type: 'DELETE_PASSWORD',
passwordKey: string,
};
export type DeleteAllPasswordsAction = {
type: 'DELETE_ALL_PASSWORDS',
};
export type Action =
...
| AddPasswordAction
| UpdatePasswordAction
| DeletePasswordAction
| DeleteAllPasswordsAction;Before creating the actions, we will add a reduxUtils.js file in the common folder. This file will contain two functions: one to remove a record from an array and the other to remove an attribute from an object. We will need these two functions in the actions and in the data reducer to update the password list.
Add reduxUtils.js in the common folder:
/*
* @flow
*/
import _ from 'lodash'
export const removeInObject = (object: Object, keyToRemove: string) => _.pickBy(object, obj => obj.key !== keyToRemove)
export const removeInArray = (array: Array<Object>, keyToRemove: string) => array.filter(key => key !== keyToRemove)Now let's move on to actions. Create a file named password.js, still in the actions folder:
/*
* @flow
*/
import CryptoJS from 'crypto-js'
import type {
ThunkAction,
Dispatch,
AddPasswordAction,
UpdatePasswordAction,
DeletePasswordAction,
DeleteAllPasswordsAction,
UpdateCryptedPasswordsAction,
} from './types'
import type { Password } from '../types/Password'
import type { NormalizedState } from '../types/NormalizedState'
import { Encrypt } from '../common/CryptoHelper'
import { removeInObject, removeInArray } from '../common/ReduxUtils'
/*
*** Actions ***
*/
export const EditPassword = (
password: Password,
edition: boolean,
key: CryptoJS.WordArray,
iv: string,
passwordsState: NormalizedState,
back: () => void
): ThunkAction => (dispatch: Dispatch) => {
let updatedPasswordsState = {}
if (edition) {
updatedPasswordsState = {
...passwordsState,
byId: { ...passwordsState.byId, [password.key]: password },
}
dispatch(updatePassword(password))
} else {
updatedPasswordsState = {
...passwordsState,
byId: { ...passwordsState.byId, [password.key]: password },
allIds: [...passwordsState.allIds, password.key],
}
dispatch(addPassword(password))
}
const cryptedPasswords = Encrypt(JSON.stringify(updatedPasswordsState), key, iv)
dispatch(updateCryptedPasswords(cryptedPasswords))
back()
}
export const DeletePassword = (
passwordKey: string,
key: CryptoJS.WordArray,
iv: string,
passwordsState: NormalizedState,
back: () => void
): ThunkAction => (dispatch: Dispatch) => {
let updatedPasswordsState = {}
updatedPasswordsState = {
...passwordsState,
byId: removeInObject(passwordsState.byId, passwordKey),
allIds: removeInArray(passwordsState.allIds, passwordKey),
}
dispatch(removePassword(passwordKey))
const cryptedPasswords = Encrypt(JSON.stringify(updatedPasswordsState), key, iv)
dispatch(updateCryptedPasswords(cryptedPasswords))
back()
}
export const DeleteAllPasswords = (key: CryptoJS.WordArray, iv: string): ThunkAction => (dispatch: Dispatch) => {
const emptyPassword = JSON.stringify({ allIds: [], byId: {} })
dispatch(removeAllPasswords())
const cryptedPasswords = Encrypt(emptyPassword, key, iv)
dispatch(updateCryptedPasswords(cryptedPasswords))
}
/*
*** Actions Creator ***
*/
const addPassword = (password: Password): AddPasswordAction => ({
type: 'ADD_PASSWORD',
password,
})
const updatePassword = (password: Password): UpdatePasswordAction => ({
type: 'UPDATE_PASSWORD',
password,
})
const removePassword = (passwordKey: string): DeletePasswordAction => ({
type: 'DELETE_PASSWORD',
passwordKey,
})
const removeAllPasswords = (): DeleteAllPasswordsAction => ({
type: 'DELETE_ALL_PASSWORDS',
})
const updateCryptedPasswords = (cryptedPassword: string): UpdateCryptedPasswordsAction => ({
type: 'UPDATE_CRYPTED_PASSWORDS',
cryptedPassword,
})For each action, we update the list of encrypted passwords.
Let's move on to the reducer. Each action we just created will modify the data state. Modify the data.js file in the reducers folder:
/*
* @flow
*/
import type { Action } from '../actions/types'
import type { DataState } from './types'
import { removeInObject, removeInArray } from '../common/ReduxUtils'
const initialState: DataState = {
passwords: { allIds: [], byId: {} },
key: '',
error: '',
}
const dataState = (state: DataState = initialState, action: Action): DataState => {
switch (action.type) {
case 'UNLOCK_APP':
return {
...state,
passwords: action.passwords,
key: action.key,
error: '',
}
case 'UNLOCK_APP_FAIL':
return {
...state,
error: action.error,
}
case 'ADD_PASSWORD':
return {
...state,
passwords: {
...state.passwords,
byId: {
...state.passwords.byId,
[action.password.key]: action.password,
},
allIds: [...state.passwords.allIds, action.password.key],
},
}
case 'UPDATE_PASSWORD':
return {
...state,
passwords: {
...state.passwords,
byId: {
...state.passwords.byId,
[action.password.key]: action.password,
},
},
}
case 'DELETE_PASSWORD':
return {
...state,
passwords: {
...state.passwords,
byId: removeInObject(state.passwords.byId, action.passwordKey),
allIds: removeInArray(state.passwords.allIds, action.passwordKey),
},
}
case 'DELETE_ALL_PASSWORDS':
return { ...state, passwords: { allIds: [], byId: {} } }
default:
return state
}
}
export default dataStateAdding a password
Now that we have created our actions and modified the data reducer, we can connect the editing screen to redux and start implementing password creation.
First, add the function that generates passwords in PasswordHelper:
export const GeneratePassword = (length: number): string => {
const plength = length
const keylistalpha = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
const keylistint = '123456789'
const keylistspec = '!@#_+/?$%'
let temp = ''
let len = plength / 2
len -= 1
for (let i = 0; i < len; i += 1) {
temp += keylistalpha.charAt(Math.floor(Math.random() * keylistalpha.length))
}
for (let i = 0; i < len; i += 1) {
temp += keylistspec.charAt(Math.floor(Math.random() * keylistspec.length))
}
for (let i = 0; i < len; i += 1) {
temp += keylistint.charAt(Math.floor(Math.random() * keylistint.length))
}
temp = temp
.split('')
.sort(() => 0.5 - Math.random())
.join('')
return temp
}We will also add the uuid library, which will let us generate a uuid whenever we create a password. This uuid will be used for the key attribute of our password.
yarn add uuidThe editing screen is used for creation but also for modifying a password. We therefore need to add a way to determine, when arriving on this screen, whether we are creating. In that case we initialize the state with default values. Or, if we are editing, we retrieve the values of the selected password.
For that, we will pass the key attribute in the navigation parameters when selecting a password, and when creating we will pass the value 0.
We will modify the Passwords screen to add the navigation parameter when adding a password. Modify the addNewItem function as follows:
addNewItem() {
this.props.navigation.navigate('Edit', { passwordKey: 0 });
}We can now connect the Edit.js screen to redux. Start by adding the necessary imports:
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import uuidV4 from 'uuid/v4'
import CryptoJS from 'crypto-js'
import type { ReduxState } from '../reducers/types'
import { GeneratePassword } from '../common/PasswordHelper'
import type { Password } from '../types/Password'
import type { NormalizedState } from '../types/NormalizedState'
import * as PasswordActions from '../actions/password'Then add mapStateToProps and bindActionCreator:
function mapStateToProps(state: ReduxState, ownProps: Object) {
const passwordKey = ownProps.navigation.state.params.passwordKey
const edition = passwordKey !== 0
const password = state.data.passwords.byId[passwordKey]
return {
edition,
password,
passwordLength: state.settings.passwordLength,
autoGeneration: state.settings.autoGeneration,
cryptoKey: state.data.key,
iv: state.user.iv,
passwords: state.data.passwords,
}
}
export default connect(mapStateToProps, dispatch => ({
actions: bindActionCreators(PasswordActions, dispatch),
}))(ReadOnlyScreen)Here you can see that we added a parameter named ownProps to the mapStateToProps function. This parameter lets us retrieve all the props passed to the screen that do not come from redux.
We retrieve the passwordKey value from ownProps, then determine whether we are editing, and finish by retrieving the password from its key in the password list.
We also retrieve the application settings as well as the key, iv, and passwords object from the application state.
Next, add the Props type and modify the class declaration:
type Props = {
edition: boolean,
password: Password,
passwordLength: number,
autoGeneration: boolean,
navigation: Object,
actions: Object,
cryptoKey: CryptoJS.WordArray,
iv: string,
passwords: NormalizedState,
};
class ReadOnlyScreen extends Component<void, Props, State>{
...Then modify the initial state declaration and add a constructor:
state = {
key: uuidV4(),
name: '',
color: PRIMARY,
password: this.props.autoGeneration ? GeneratePassword(this.props.passwordLength) : '',
icon: 'cubes',
login: '',
url: '',
modalIsOpen: false,
};
constructor(props: Props) {
super(props);
if (this.props.edition) {
const { key, name, color, password, icon, login, url } = this.props.password;
this.state = {
key,
name,
color,
password,
icon,
login,
url,
modalIsOpen: false,
};
}
}In the code above, we define state with default values, then in the class constructor we test whether we are editing. If so, we redefine state with the password values passed as props.
Finally, all that remains is to modify the generatePassword and save functions:
save() {
const { cryptoKey, iv, passwords, edition,navigation } = this.props;
const passwordToEdit: Password = {
key: this.state.key,
name: this.state.name,
color: this.state.color,
password: this.state.password,
icon: this.state.icon,
login: this.state.login,
url: this.state.url,
};
this.props.actions.EditPassword(passwordToEdit, edition, cryptoKey, iv, passwords, () =>
navigation.goBack(),
);
}
generatePassword() {
this.setState({
password: GeneratePassword(this.props.passwordLength),
});
}Editing and deleting a password
The first thing we will do is modify navigation when selecting a password in the list so it adds the passwordKey parameter, as we did for addNewItem.
Modify the showPassword function like this:
showPassword(password: Password) {
this.props.navigation.navigate('ReadOnly', {
siteName: password.name,
passwordKey: password.key,
});
}Next, connect the ReadOnly screen to redux. As before, start by adding the imports:
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import CryptoJS from 'crypto-js';
import type { ReduxState } from '../reducers/types';
import type { Password } from '../types/Password';
import type { NormalizedState } from '../types/NormalizedState';
import { deletePassword } from '../actions/passwords';Then add mapStateToProps and bindActionCreator:
function mapStateToProps(state: ReduxState, ownProps: Object) {
const passwordKey = ownProps.navigation.state.params.passwordKey;
const password = state.data.passwords.byId[passwordKey];
return {
password,
cryptoKey: state.data.key,
iv: state.user.iv,
passwords: state.data.passwords,
};
}
export default connect(mapStateToProps, dispatch => ({
actions: bindActionCreators({ DeletePassword }, dispatch),
}))(ReadOnlyScreen);Then remove the State type. We added it when creating the screen to display the data, but now all the information we need is contained in props.
However, we need to add the Props type:
type Props = {
password: Password,
cryptoKey: CryptoJS.WordArray,
iv: string,
passwords: NormalizedState,
}Finally, modify the screen like this:
class ReadOnlyScreen extends Component<void, Props, void> {
editPassword() {
this.props.navigation.navigate('Edit', {
passwordKey: this.props.password.key,
})
}
copyPassword() {
console.log('copy password')
}
deletePassword() {
const { cryptoKey, iv, passwords, navigation } = this.props
this.props.actions.DeletePassword(this.props.password.key, cryptoKey, iv, passwords, () => navigation.goBack())
}
render() {
if (!this.props.password) {
return <View />
}
const { icon, color, name, password, login, url } = this.props.password
return (
<ScrollView style={styles.scrollContent}>
<View style={styles.container}>
<View style={styles.iconCtnr}>
<View style={styles.icon}>
<Icon name={icon} size={35} color={color} />
</View>
</View>
<ReadOnlyRow label={strings.siteName} value={name} />
<ReadOnlyRow label={strings.siteUrl} value={url} />
<ReadOnlyRow label={strings.userName} value={login} />
<ReadOnlyRow label={strings.password} value={password} />
<View style={styles.actionContainer}>
<TouchableOpacity style={styles.action} onPress={() => this.copyPassword()}>
<Text style={[styles.actionLabel, { color: '#647CF6' }]}>{strings.copy}</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.action} onPress={() => this.editPassword()}>
<Text style={[styles.actionLabel, { color: PRIMARY }]}>{strings.edit}</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.action} onPress={() => this.deletePassword()}>
<Text style={[styles.actionLabel, { color: DELETE_COLOR }]}>{strings.delete}</Text>
</TouchableOpacity>
</View>
</View>
</ScrollView>
)
}
}In the render method, we test whether the password coming from props is null. If so, we return a simple view. We do this because, since the password is retrieved from redux state, when we delete the password the props will be modified and the render method will be executed again. Since the password will no longer exist, retrieving the data via
const { icon, color, name, password, login, url } = this.props.passwordwill return an error. Returning the view lets us work around this problem while navigation back to the password list screen takes effect.
Deleting all passwords
Deleting all passwords is available in the application settings. This screen is already connected to redux; we only need to add the deleteAllPassword action in bindActionCreator and then execute this action when clicking the row.
In Settings.js, add the import for the DeleteAllPasswords action and CryptoJS:
import CryptoJS from 'crypto-js'
import { DeleteAllPasswords } from '../actions/passwords'Then modify mapStateToProps to add the key and iv props, and modify bindActionCreator to add the DeleteAllPassword action:
function mapStateToProps(state: ReduxState) {
return {
passwordLength: state.settings.passwordLength,
autoGeneration: state.settings.autoGeneration,
cryptoKey: state.data.key,
iv: state.user.iv,
}
}
export default connect(mapStateToProps, dispatch => ({
actions: bindActionCreators({ ...{}, ...{ DeleteAllPasswords }, ...SettingsActions }, dispatch),
}))(SettingsScreen)Then add iv and cryptoKey in the Props type:
type Props = {
passwordLength: number,
autoGeneration: boolean,
navigation: Object,
actions: Object,
iv: string,
cryptoKey: CryptoJS.WordArray,
}And finally modify the deleteAllPassword function:
deleteAllPasswords() {
const { iv, cryptoKey } = this.props;
Alert.alert(strings.clear, strings.clearConfirmation, [
{ text: strings.cancel, style: 'cancel' },
{
text: strings.delete,
onPress: () => this.props.actions.DeleteAllPasswords(cryptoKey, iv),
},
]);
}Adding the password to the clipboard
To finish this article, we will add the last missing action on the ReadOnly screen: copying the password to the clipboard.
React Native makes it very simple to copy elements to the clipboard. Just import Clipboard from React Native and use the following function:
Clipboard.setString('hello world')In the ReadOnly screen, add Clipboard and Alert to the React Native imports, then modify the copyPassword function like this:
copyPassword() {
Clipboard.setString(this.props.password.password);
Alert.alert(strings.succes, strings.copyMessage);
}Finally, add the missing resources:
fr.js
...
succes: 'Succès',
copyMessage: 'Le mot de passe a bien été ajouté au presse-papieren.js
...
succes: 'Success',
copyMessage: 'The password has been added to the clipboard',strings.js
...
succes: I18n.t('succes'),
copyMessage: I18n.t('copyMessage'),We have now finished password management.