React Native Password Manager: Creating the Components

October 16, 2017

Hello everyone. Today we are back with the second article in the series where we will create a password manager. In this article we will create the components that we will use in the different screens of the application.

Colors, dimensions, and platform-specific code

We will start by defining all the colors we will use in the application.

In the src folder, create a constants folder and add a colors.js file to it:

cd src
mkdir constants && cd constants
touch colors.js

Then add the following code to the colors.js file:

// COMMON
export const PRIMARY = '#01D88D'
export const PRIMARY_DARK = '#01BA6C'
export const WHITE = '#FFFFFF'
export const PRIMARY_TEXT = '#000000'
export const PLACE_HOLDER = '#999696'
export const DELETE_COLOR = '#F66464'
export const SELECTABLE_COLORS = [
  '#F66464',
  '#F664E9',
  '#9864F6',
  '#64A1F6',
  '#647CF6',
  '#01D88D',
  '#F6D464',
  '#F6A264',
  '#77909D',
]
export const levelLow = '#ff605e'
export const levelMedium = '#ffc45e'
export const levelHigh = '#01BA6C'
// ANDROID
export const SEARCH_PLACEHOLDER_COLOR = '#D5D5D5'
export const ANDROID_SEARCH_PLACEHOLDER_COLOR = '#727272'
export const ANDROID_SETTING_IC_COLOR = '#757575'
export const ANDROID_SEPARATOR = '#E5E5E5'
// IOS
export const IC_SEARCH_COLOR = '#BEBEBE'
export const IOS_SEPARATOR = '#C8C7CC'
export const IOS_SLIDER_LABEL = '#6D6D72'
export const IOS_BACKGROUND = '#EFEFF4'
export const IOS_TABICON = '#A4AAB3'

This lets us stop worrying about the value of each color once it has been defined. We only need to import the colors whenever we need them in a component.

We will do the same thing with dimensions. In the constants folder, create a dimensions.js file and add the following code:

// ANDROID
export const ANDROID_NAV_HEIGHT = 56
export const ANDROID_SEARCH_HEIGHT = 48
export const ANDROID_SEARCH_RADIUS = 2
export const ANDROID_MARGIN = 16
export const ACTION_BUTTON = 56
export const HALF_ACTION_BUTTON = ACTION_BUTTON / 2
export const ANDROID_BUTTON_HEIGHT = 36
export const ANDROID_ROW_HEIGHT = 56
export const ANDROID_ROW_FONTSIZE = 16
// iOS
export const IOS_NAV_BAR_HEIGHT = 44
export const IOS_STATUS_BAR_HEIGHT = 20
export const IOS_FULL_HEIGHT = IOS_NAV_BAR_HEIGHT + IOS_STATUS_BAR_HEIGHT
export const IOS_SEARCH_HEIGHT = 28
export const IOS_SEARCH_RADIUS = 2
export const IOS_MARGIN = 8
export const IOS_ROW_HEIGHT = 44
export const IOS_ROW_FONTSIZE = 17
export const IOS_ICON_CTNR = 29

As mentioned in the series presentation, the application we are going to create will have platform-specific components (Android/iOS).

To manage components by platform, we will do it in two different ways. The first is by using React Native's Platform API, which lets us detect the platform being used as follows:

import {
  View,
  TextInput,
  Platform
} from 'react-native';
...
render(){
  if (Platform.OS === 'android'){
    // return Android component
  }else{
    // return Android component
  }
}

Or by including the platform in the file name, like this: myComponent.ios.js and myComponent.android.js.

This method will be used when components are completely different between platforms. However, in some cases we may only need to change a few style properties and/or an icon depending on the platform. In that case we will use another method, which consists of creating a helper that will contain the functions needed to retrieve the right style properties or icons for the platform.

In the src folder, create a common folder and add a file named PlatformHelper.js:

mkdir common && cd common
touch PlatformHelper.js

First, we will create a function that wraps StyleSheet.create and lets us specify the platform directly inside the style, as follows:

{
  myButton:{
    width:150,
    height:50,
    ios:{
      backgroundColor:'red'
    },
    android:{
      backgroundColor:'green'
    }
  }
}

Here we created the style for a button that has the same dimensions on both platforms, but whose color changes depending on the platform.

In the PlatformHelper.js file, add the following code:

/*
  @flow
 */
import { StyleSheet, Platform } from 'react-native'
 
export const PlateformStyleSheet = (styles: Object): Object => {
  const platformStyles = {}
  Object.keys(styles).forEach(name => {
    let { ios, android, ...style } = { ...styles[name] }
    if (ios && Platform.OS === 'ios') {
      style = { ...style, ...ios }
    }
    if (android && Platform.OS === 'android') {
      style = { ...style, ...android }
    }
    platformStyles[name] = style
  })
  return StyleSheet.create(platformStyles)
}

Now, for icons, we will use the Ionicons icons that are available in the react-native-vector-icons library's icon list. Ionicons provides icons for iOS and Android. Here is how it works: take the example of an icon representing a plus sign. The icon name is "add"; for Android the name will be "md-add", and for iOS it will be "ios-add" or "ios-add-outline".

We will add a method to PlatformHelper to automatically select the right name for the platform:

export const IconPlateform = (iconName: string, isOutline: boolean = false): string => {
  let name = Platform.OS === 'android' ? `md-${iconName}` : `ios-${iconName}`
  if (Platform.OS === 'ios' && isOutline) {
    name = `${name}-outline`
  }
  return name
}

Components

We will now move on to creating the components.

searchbar react-native

We start with the search bar. Since this component is completely different between iOS and Android, we will create two separate components. First, create a components folder in the src folder. Then, inside this folder, create a new folder named SearchBar and add two files named index.ios.js and index.android.js:

mkdir components && cd components
mkdir SearchBar && cd SearchBar
touch index.android.js index.ios.js

Let's start with the iOS search bar. It will have two roles: the first is to allow searching for passwords in the list, and the second is to add a new password using the + button on the right. In addition, when we start typing in the search bar, we will have an animation where the + button rotates to become an x. In that case, the button will cancel the current entry and clear the search field.

Before moving on to the code, we will add the translations needed for this component:

locales/en.js

...
search_password: 'Search for a password',

locales/fr.js

...
search_password: 'Rechercher un mot de passe',

locales/string.js

...
search_password: I18n.t('search_password'),

Next, since we are using Flow in this project, we will define the type for the component's properties. In the src folder, create a folder named types, add a searchBar.js file, and add the following code:

export type Props = {
  onChangeText: (text: string) => void,
  addItem: () => void,
  onClear: () => void,
  openMenu: () => void,
}

Add the following code to the index.ios.js file in the SearchBar folder:

/*
 * @flow
 */
import React, { Component } from 'react'
import { StyleSheet, View, StatusBar, TouchableOpacity, TextInput } from 'react-native'
import Icon from 'react-native-vector-icons/Ionicons'
import * as Animatable from 'react-native-animatable'
import {
  IOS_STATUS_BAR_HEIGHT,
  IOS_FULL_HEIGHT,
  IOS_MARGIN,
  IOS_SEARCH_RADIUS,
  IOS_SEARCH_HEIGHT,
} from '../../constants/dimensions'
import { PRIMARY, PRIMARY_DARK, WHITE } from '../../constants/colors'
import strings from '../../locales/strings'
import type { Props } from '../../types/searchBar'
 
const AnimatedIcon = Animatable.createAnimatableComponent(Icon)
 
type State = {
  isEditing: boolean,
}
 
class SearchBar extends Component<Props, Props, State> {
  searchInput: Object
  state = { isEditing: false }
  static defaultProps = {
    onChangeText: (text: string) => console.log(text),
    addItem: () => console.log('add new item'),
    onClear: () => console.log('clear '),
    openMenu: () => console.log('open menu'),
  }
 
  onFocus() {
    this.setEditingMode(true)
  }
 
  setEditingMode(value: boolean) {
    this.setState({
      isEditing: value,
    })
  }
 
  onPress() {
    if (this.state.isEditing) {
      this.searchInput.clear()
      this.searchInput.blur()
      this.props.onClear()
      this.setEditingMode(false)
    } else {
      this.props.addItem()
    }
  }
 
  render() {
    const { isEditing } = this.state
    return (
      <View style={styles.container}>
        <StatusBar barStyle="light-content" />
        <View style={styles.content}>
          <TextInput
            ref={component => {
              this.searchInput = component
            }}
            style={styles.inputSearch}
            placeholderTextColor={WHITE}
            placeholder={strings.search_password}
            returnKeyType="done"
            selectionColor={WHITE}
            onFocus={() => this.onFocus()}
            onChangeText={text => this.props.onChangeText(text)}
          />
          <Icon name="ios-search" size={18} color={WHITE} style={styles.searchIcon} />
          <TouchableOpacity style={styles.touchable} onPress={() => this.onPress()}>
            <AnimatedIcon
              name={'md-add'}
              size={28}
              color={WHITE}
              style={isEditing && styles.editing}
              transition="rotate"
            />
          </TouchableOpacity>
        </View>
      </View>
    )
  }
}
export default SearchBar
 
const styles = StyleSheet.create({
  container: {
    justifyContent: 'flex-start',
    height: IOS_FULL_HEIGHT,
    backgroundColor: PRIMARY,
  },
  content: {
    marginTop: IOS_STATUS_BAR_HEIGHT,
    paddingVertical: IOS_MARGIN,
    paddingLeft: IOS_MARGIN,
    justifyContent: 'space-between',
    flexDirection: 'row',
  },
  inputSearch: {
    backgroundColor: PRIMARY_DARK,
    borderRadius: IOS_SEARCH_RADIUS,
    height: IOS_SEARCH_HEIGHT,
    paddingLeft: 32,
    color: WHITE,
    flex: 1,
  },
  searchIcon: {
    position: 'absolute',
    left: 16,
    top: 12,
    backgroundColor: 'transparent',
  },
  touchable: {
    paddingHorizontal: IOS_MARGIN,
  },
  editing: {
    transform: [
      {
        rotate: '45deg',
      },
    ],
  },
})

Let's go over what we did in this component in detail.

For Flow and type checking, the first thing we do is import the type for our properties:

import type { Props } from '../../types/searchBar'

Then, in the same way we declared the Props type, we declare a State type:

type State = {
  isEditing: boolean,
}

Then we pass the types we just created to our component:

class SearchBar extends Component<Props, Props, State>

The values between < > represent < defaultProps, props, state >. Here we define that our defaultProps will be of type Props, that props will be of type Props, and finally that state will be of type State.

Finally, all that remains is to define the initial state values and defaultProps:

state = { isEditing: false };
static defaultProps = {
  onChangeText: (text: string) => console.log(text),
  addItem: () => console.log('add new item'),
  onClear: () => console.log('clear '),
  openMenu: () => console.log('open menu'),
};

Next, for the TextInput, the first important thing to do is define a variable named searchInput of type Object. It will represent our TextInput and allow us to manipulate it. Then we attach the reference of our TextInput to searchInput:

ref={(component) => {
 this.searchInput = component;
}}

Thanks to that, when we click the button, depending on whether we are currently searching or not, we can either add a new password through the "addItem" property or clear the input area, remove focus, and hide the keyboard.

onPress() {
 if (this.state.isEditing) {
   this.searchInput.clear();
   this.searchInput.blur();
   this.props.onClear();
   this.setEditingMode(false);
 } else {
   this.props.addItem();
 }
}

Now let's move on to the Android search bar. In the index.android.js file, add the following code:

/*
 * @flow
 */
 
import React, { Component } from 'react'
import { StyleSheet, TextInput, View, TouchableNativeFeedback, StatusBar } from 'react-native'
import Icon from 'react-native-vector-icons/Ionicons'
import * as Animatable from 'react-native-animatable'
import { ANDROID_MARGIN, ANDROID_SEARCH_HEIGHT, ANDROID_SEARCH_RADIUS } from '../../constants/dimensions'
import { PRIMARY_DARK, WHITE, IC_SEARCH_COLOR, ANDROID_SEARCH_PLACEHOLDER_COLOR } from '../../constants/colors'
import strings from '../../locales/strings'
import type { Props } from '../../types/searchBar'
 
type State = {
  isEditing: boolean,
  icon: string,
}
 
class SearchBar extends Component<Props, Props, State> {
  searchInput: Object
  iconView: Object
  state = {
    isEditing: false,
    icon: 'md-menu',
  }
  static defaultProps = {
    onChangeText: (text: string) => console.log(text),
    addItem: () => console.log('add new item'),
    onClear: () => console.log('clear '),
    openMenu: () => console.log('open menu'),
  }
 
  focus() {
    this.searchInput.focus()
    this.setEditingMode(true)
  }
 
  onFocus() {
    this.setEditingMode(true)
    this.animate()
  }
 
  setEditingMode(value: boolean) {
    this.setState({
      isEditing: value,
    })
  }
 
  onPress() {
    if (this.state.isEditing) {
      this.searchInput.clear()
      this.searchInput.blur()
      this.props.onClear()
      this.setEditingMode(false)
      this.animate()
    } else {
      this.props.openMenu()
    }
  }
 
  animate() {
    const { isEditing } = this.state
    const rotationDeg = isEditing ? '0deg' : '360deg'
    const icon = isEditing ? 'md-menu' : 'md-arrow-round-back'
    this.iconView.transitionTo({ rotate: rotationDeg })
    setTimeout(() => this.setState({ icon }), 250)
  }
 
  render() {
    return (
      <View style={styles.container}>
        <StatusBar backgroundColor={PRIMARY_DARK} />
        <View style={styles.content}>
          <TouchableNativeFeedback
            background={TouchableNativeFeedback.SelectableBackgroundBorderless()}
            onPress={() => this.onPress()}
          >
            <Animatable.View
              style={[styles.iconCtnr]}
              ref={component => {
                this.iconView = component
              }}
            >
              <Icon name={this.state.icon} size={24} color={IC_SEARCH_COLOR} />
            </Animatable.View>
          </TouchableNativeFeedback>
          <TextInput
            ref={component => {
              this.searchInput = component
            }}
            style={{ flex: 1 }}
            placeholderTextColor={ANDROID_SEARCH_PLACEHOLDER_COLOR}
            placeholder={strings.search_password}
            returnKeyType="done"
            selectionColor={WHITE}
            onFocus={() => this.onFocus()}
            underlineColorAndroid={WHITE}
            onChangeText={text => this.props.onChangeText(text)}
          />
        </View>
      </View>
    )
  }
}
export default SearchBar
 
const styles = StyleSheet.create({
  container: {
    padding: 8,
  },
  content: {
    paddingLeft: ANDROID_MARGIN,
    paddingRight: ANDROID_MARGIN,
    backgroundColor: WHITE,
    height: ANDROID_SEARCH_HEIGHT,
    borderRadius: ANDROID_SEARCH_RADIUS,
    elevation: 6,
    flexDirection: 'row',
  },
  iconCtnr: {
    height: 22,
    width: 22,
    borderRadius: 22,
    marginTop: 12,
    marginRight: 16,
    alignItems: 'center',
    justifyContent: 'center',
  },
})

SliderRow

We will now create the SliderRow component. This component will let us select the length of the passwords that will be generated.

In the components folder, create a file named SliderRow.js:

/*
 * @flow
 */
 
import React from 'react'
import { View, Slider, Text, Platform } from 'react-native'
import { PlateformStyleSheet } from '../common/PlatformHelper'
import { ANDROID_MARGIN, ANDROID_ROW_FONTSIZE, IOS_ROW_HEIGHT, IOS_MARGIN } from '../constants/dimensions'
import { WHITE, PRIMARY_TEXT, ANDROID_SEPARATOR, IOS_SLIDER_LABEL } from '../constants/colors'
 
type Props = {
  selectedValue: number,
  onValueChange: (value: number) => void,
  onSlidingComplete: (value: number) => void,
  label: string,
}
 
const renderSlider = (
  selectedValue: number,
  onValueChange: (value: number) => void,
  onSlidingComplete: (value: number) => void
) => {
  if (Platform.OS === 'android') {
    return (
      <Slider
        style={styles.slider}
        minimumValue={8}
        maximumValue={60}
        step={1}
        value={selectedValue}
        onSlidingComplete={value => onSlidingComplete(value)}
        onValueChange={value => onValueChange(value)}
      />
    )
  }
  return (
    <View style={styles.sliderCtnr}>
      <Slider
        style={styles.slider}
        onSlidingComplete={value => onSlidingComplete(value)}
        minimumValue={8}
        maximumValue={60}
        step={1}
        value={selectedValue}
        onValueChange={value => onValueChange(value)}
      />
    </View>
  )
}
 
const SliderRow = (props: Props) => (
  <View style={styles.container}>
    <View style={styles.subContainer}>
      <Text style={styles.label}>{props.label}</Text>
      <Text style={styles.label}>{props.selectedValue}</Text>
    </View>
    <View style={styles.sliderCtnr}>
      {renderSlider(props.selectedValue, props.onValueChange, props.onSlidingComplete)}
    </View>
  </View>
)
 
SliderRow.defaultProps = {
  selectedValue: 12,
  onValueChange: (value: number) => console.log(`onValueChange : ${value}`),
  onSlidingComplete: (value: number) => console.log(`onSlidingComplete : ${value}`),
  label: 'Label',
}
 
export default SliderRow
 
const styles = PlateformStyleSheet({
  container: {
    android: {
      backgroundColor: WHITE,
      borderBottomWidth: 1,
      borderColor: ANDROID_SEPARATOR,
    },
  },
  subContainer: {
    android: {
      padding: ANDROID_MARGIN,
      flexDirection: 'row',
      justifyContent: 'space-between',
    },
    ios: {
      padding: IOS_MARGIN,
      flexDirection: 'row',
      justifyContent: 'space-between',
    },
  },
  label: {
    color: PRIMARY_TEXT,
    android: { fontSize: ANDROID_ROW_FONTSIZE },
    ios: {
      fontSize: 13,
      color: IOS_SLIDER_LABEL,
    },
  },
  slider: {
    android: {
      marginBottom: ANDROID_MARGIN,
    },
    ios: {
      marginHorizontal: IOS_MARGIN,
    },
  },
  sliderCtnr: {
    height: IOS_ROW_HEIGHT,
    backgroundColor: WHITE,
  },
})

Here we created a stateless component, which means that it does not manage any state. It only renders and uses the properties that are passed to it.

First, we created the Flow type for the component's properties.

Then we created a function named renderSlider. As you can see, depending on the platform we do not return exactly the same component, which is why we created this function. Since part of the component is identical on both platforms, we create a function that handles the platform-specific part of the component, and then in our overall component we call this function, as is the case in the SliderRow function.

Finally, we used our PlateformStyleSheet to define platform-specific styles.

To test the component, you can paste this code into the src/index.js file:

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */
 
import React, { Component } from 'react'
import { StyleSheet, View } from 'react-native'
import SearchBar from './components/SearchBar'
import SliderRow from './components/SliderRow'
 
export default class App extends Component {
  state = {
    passwordLength: 14,
  }
 
  setValue(lentgh: number) {
    this.setState({
      passwordLength: lentgh,
    })
  }
 
  render() {
    return (
      <View style={styles.container}>
        <SearchBar />
        <SliderRow
          label="Password length"
          selectedValue={this.state.passwordLength}
          onSlidingComplete={length => this.setValue(length)}
          onValueChange={length => this.setValue(length)}
        />
      </View>
    )
  }
}
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5FCFF',
  },
})

CheckBoxRow

In the same way as SliderRow, we will create the CheckBoxRow component:

/*
 * @flow
 */
 
import React from 'react'
import { View, TouchableNativeFeedback, Text, Switch, Platform } from 'react-native'
import {
  ANDROID_ROW_HEIGHT,
  ANDROID_MARGIN,
  ANDROID_ROW_FONTSIZE,
  IOS_ROW_HEIGHT,
  IOS_MARGIN,
  IOS_ROW_FONTSIZE,
} from '../constants/dimensions'
import { WHITE, PRIMARY_TEXT, ANDROID_SEPARATOR, IOS_SEPARATOR } from '../constants/colors'
import { PlateformStyleSheet } from '../common/PlatformHelper'
 
type Props = {
  isChecked: boolean,
  label: string,
  switchValueChange: (value: boolean) => void,
}
const renderContent = (label: string, isChecked: boolean, switchValueChange: (value: boolean) => void) => (
  <View style={styles.container}>
    <Text style={styles.label}>{label}</Text>
    <Switch onValueChange={value => switchValueChange(value)} value={isChecked} />
  </View>
)
 
const CheckBoxRow = (props: Props) => {
  if (Platform.OS === 'android') {
    return (
      <TouchableNativeFeedback onPress={() => props.switchValueChange(!props.isChecked)}>
        {renderContent(props.label, props.isChecked, props.switchValueChange)}
      </TouchableNativeFeedback>
    )
  }
  return renderContent(props.label, props.isChecked, props.switchValueChange)
}
 
CheckBoxRow.defaultProps = {
  isChecked: false,
  label: 'Label',
  switchValueChange: (value: boolean) => console.log(`switch value : ${value.toString()}`),
}
 
export default CheckBoxRow
 
const styles = PlateformStyleSheet({
  container: {
    backgroundColor: WHITE,
    justifyContent: 'space-between',
    alignItems: 'center',
    flexDirection: 'row',
    ios: {
      height: IOS_ROW_HEIGHT,
      paddingHorizontal: IOS_MARGIN,
      borderTopWidth: 1,
      borderColor: IOS_SEPARATOR,
    },
    android: {
      height: ANDROID_ROW_HEIGHT,
      paddingHorizontal: ANDROID_MARGIN,
      borderBottomWidth: 1,
      borderColor: ANDROID_SEPARATOR,
    },
  },
  label: {
    color: PRIMARY_TEXT,
    android: { fontSize: ANDROID_ROW_FONTSIZE },
    ios: { fontSize: IOS_ROW_FONTSIZE },
  },
})

To test it, modify the index.js file:

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */
 
import React, { Component } from 'react';
import { StyleSheet, View } from 'react-native';
import SearchBar from './components/SearchBar';
import SliderRow from './components/SliderRow';
import CheckBoxRow from './components/CheckBoxRow';
 
export default class App extends Component {
  state = {
    passwordLength: 14,
    isChecked: false,
  };
 
  setValue(lentgh: number) {
    this.setState({
      passwordLength: lentgh,
    });
  }
 
  setBoolValue(value: boolean) {
    this.setState({
      isChecked: value,
    });
  }
 
  render() {
    return (
      <View style={styles.container}>
        <SearchBar />
        <SliderRow
          label="Password length"
          selectedValue={this.state.passwordLength}
          onSlidingComplete={length => this.setValue(length)}
          onValueChange={length => this.setValue(length)}
        />
        <CheckBoxRow
          label="Automatic generation"
          isChecked={this.state.isChecked}
          switchValueChange={value => this.setBoolValue(value)}
        />
      </View>
    );
  }
}
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5FCFF',
  },
});

SettingRow

Finally, to finish with the components we will use in the settings screens, we will create the SettingRow component:

/*
 * @flow
 */
 
import React from 'react'
import { Text, View, Platform, TouchableNativeFeedback, TouchableOpacity } from 'react-native'
import Icon from 'react-native-vector-icons/Ionicons'
import { PlateformStyleSheet, IconPlateform } from '../common/PlatformHelper'
import {
  ANDROID_ROW_HEIGHT,
  ANDROID_MARGIN,
  ANDROID_ROW_FONTSIZE,
  IOS_ROW_HEIGHT,
  IOS_MARGIN,
  IOS_ROW_FONTSIZE,
  IOS_ICON_CTNR,
} from '../constants/dimensions'
import { WHITE, PRIMARY_TEXT, ANDROID_SEPARATOR, ANDROID_SETTING_IC_COLOR, IOS_SEPARATOR } from '../constants/colors'
 
type Props = {
  label: string,
  iconName: string,
  iconBackground: string,
  iosOultine: boolean,
  iosSeparator: boolean,
  onPress: () => void,
}
 
const renderSeparator = (separatorNeeded: boolean) => {
  if (separatorNeeded) {
    return <View style={styles.separator} />
  }
  return null
}
 
const SettingRowAndroid = (label: string, iconName: string, onPress: () => void) => (
  <TouchableNativeFeedback onPress={onPress}>
    <View style={styles.container}>
      <View style={styles.iconCtnr}>
        <Icon name={IconPlateform(iconName)} color={ANDROID_SETTING_IC_COLOR} size={24} />
      </View>
      <Text style={styles.label}>{label}</Text>
    </View>
  </TouchableNativeFeedback>
)
 
const SettingRowiOS = (
  label: string,
  iconName: string,
  iconBackground: string,
  isOutline: boolean,
  withSeparator: boolean,
  onPress: () => void
) => (
  <TouchableOpacity style={styles.container} activeOpacity={0.7} onPress={onPress}>
    <View style={[styles.iconCtnr, { backgroundColor: iconBackground }]}>
      <Icon name={IconPlateform(iconName, isOutline)} color={WHITE} size={24} />
    </View>
    <Text style={styles.label}>{label}</Text>
    {renderSeparator(withSeparator)}
  </TouchableOpacity>
)
 
const SettingRow = (props: Props) => {
  if (Platform.OS === 'android') {
    return SettingRowAndroid(props.label, props.iconName, props.onPress)
  }
  return SettingRowiOS(
    props.label,
    props.iconName,
    props.iconBackground,
    props.iosOultine,
    props.iosSeparator,
    props.onPress
  )
}
 
SettingRow.defaultProps = {
  label: 'Setting label',
  iconName: 'trash',
  iconBackground: 'red',
  iosOultine: false,
  iosSeparator: false,
  onPress: () => console.log('onPress'),
}
 
export default SettingRow
 
const styles = PlateformStyleSheet({
  container: {
    backgroundColor: WHITE,
    alignItems: 'center',
    flexDirection: 'row',
    ios: {
      height: IOS_ROW_HEIGHT,
      paddingHorizontal: IOS_MARGIN,
    },
    android: {
      height: ANDROID_ROW_HEIGHT,
      paddingHorizontal: ANDROID_MARGIN,
      borderBottomWidth: 1,
      borderColor: ANDROID_SEPARATOR,
    },
  },
  label: {
    color: PRIMARY_TEXT,
    android: { fontSize: ANDROID_ROW_FONTSIZE, marginLeft: ANDROID_MARGIN },
    ios: { fontSize: IOS_ROW_FONTSIZE },
  },
  iconCtnr: {
    alignItems: 'center',
    justifyContent: 'center',
    ios: {
      width: IOS_ICON_CTNR,
      height: IOS_ICON_CTNR,
      marginRight: ANDROID_MARGIN,
      borderRadius: 6,
    },
    android: {
      width: 23,
    },
  },
  separator: {
    height: 1,
    flex: 1,
    backgroundColor: IOS_SEPARATOR,
    position: 'absolute',
    bottom: 0,
    left: 53,
    right: 0,
  },
})

PasswordList

Now we will create the password list. Start by creating a PasswordList folder in the components folder:

mkdir PasswordList

For the list, we will need several components:

  • The list component, which will be a FlatList
  • The item component, which will represent a password in the list
  • The empty component, which will be used when the list is empty or when the search returns no results

Let's start with the empty component. In the PasswordList folder, create an EmptyComponent.js file and add the following code:

/*
 * @flow
 */
 
import React from 'react'
import { View, Text, StyleSheet, ScrollView, Image, Button } from 'react-native'
import strings from '../../locales/strings'
import { PRIMARY_TEXT, PRIMARY } from '../../constants/colors'
 
const img = require('../../img/archive.png')
 
type Props = {
  fromSearch: boolean,
  onPress: () => void,
}
 
const renderAction = (fromSearch: boolean, onPress: () => void) => {
  if (!fromSearch) {
    return <Button title={strings.addPassword} color={PRIMARY} onPress={onPress} />
  }
}
 
const EmptyComponent = (props: Props) => {
  const label = props.fromSearch ? strings.noResults : strings.noPasswords
  return (
    <ScrollView contentContainerStyle={styles.container} automaticallyAdjustContentInsets={false}>
      <View style={styles.subContainer}>
        <Image source={img} />
        <Text style={styles.title}>{label}</Text>
      </View>
      {renderAction(props.fromSearch, props.onPress)}
    </ScrollView>
  )
}
 
EmptyComponent.defaultProps = {
  onPress: () => console.log('add new password'),
  fromSearch: false,
}
 
export default EmptyComponent
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  subContainer: {
    padding: 12,
    alignItems: 'center',
    marginTop: 42,
  },
  title: {
    fontWeight: 'bold',
    marginTop: 12,
    marginBottom: 12,
    textAlign: 'center',
    color: PRIMARY_TEXT,
  },
})

Then add the localization resources used in the component.

fr.js

...
noPasswords: "Vous n'avez aucun mot de passe pour le moment",
noResults: 'Aucun mot de passe trouvés',
addPassword: 'Ajouter un mot de passe',

en.js

...
noPasswords: 'You have no passwords yet',
noResults: 'No password found',
addPassword: 'Add new password',

strings.js

... noPasswords: I18n.t('noPasswords'),
noResults: I18n.t('noResults'),
addPassword: I18n.t('addPassword'),

Next, still in the PasswordList folder, we will create the PasswordItem component.

/*
 * @flow
 */
 
import React from 'react'
import { View, Text, TouchableOpacity, TouchableNativeFeedback, Platform, StyleSheet } from 'react-native'
import Icon from 'react-native-vector-icons/FontAwesome'
import { WHITE } from '../../constants/colors'
 
type Props = {
  dimension: number,
  icon: string,
  name: string,
  color: string,
  onPress: () => void,
}
 
const renderContent = (icon: string, name: string, color: string, dimension: number) => (
  <View style={[styles.row, { width: dimension, height: dimension }]}>
    <View style={[styles.iconCtnr, { backgroundColor: color }]}>
      <Icon name={icon} size={44} color="white" />
    </View>
    <Text style={styles.label} numberOfLines={2}>
      {name}
    </Text>
  </View>
)
const PasswordItem = (props: Props) => {
  if (Platform.OS === 'android') {
    return (
      <TouchableNativeFeedback onPress={props.onPress}>
        {renderContent(props.icon, props.name, props.color, props.dimension)}
      </TouchableNativeFeedback>
    )
  }
  return (
    <TouchableOpacity activeOpacity={0.6} onPress={props.onPress}>
      {renderContent(props.icon, props.name, props.color, props.dimension)}
    </TouchableOpacity>
  )
}
 
PasswordItem.defaultProps = {
  color: '#F664E9',
  icon: 'dribbble',
  name: 'Dribble',
  dimension: 50,
}
 
export default PasswordItem
 
const styles = StyleSheet.create({
  row: {
    justifyContent: 'center',
    backgroundColor: WHITE,
    alignItems: 'center',
    borderWidth: 0.5,
    borderColor: '#E5E5E5',
  },
  iconCtnr: {
    height: 70,
    width: 70,
    borderRadius: 14,
    justifyContent: 'center',
    alignItems: 'center',
  },
  label: {
    marginTop: 18,
    fontSize: 18,
    color: '#565656',
    paddingHorizontal: 8,
    textAlign: 'center',
  },
})

To finish, we will create the password list. Since this list will receive the password list as a property, we will need to create a Flow type representing a password.

In the types folder, create a Password.js file:

export type Password = {
  key: string,
  name: string,
  color: string,
  password: string,
  icon: string,
  login: string,
  url: string,
}

Now we can create the list. In the components/PasswordList folder, add an index.js file with the following code:

/*
 * @flow
 */
 
import React from 'react'
import { FlatList, Dimensions, Platform } from 'react-native'
import PasswordItem from './PasswordItem'
import EmptyComponent from './EmptyComponent'
import type { Password } from '../../types/Password'
 
const { width } = Dimensions.get('window')
const dimension = width / 2
 
type Props = {
  data: Array<Password>,
  onItemPress: (item: Password) => void,
  fromSearch: boolean,
}
 
const defineContentInset = () => (Platform.OS === 'android' ? { bottom: 0 } : { bottom: 50 })
 
const renderItem = (item: Password, onPress: () => void) => (
  <PasswordItem dimension={dimension} name={item.name} icon={item.icon} color={item.color} onPress={onPress} />
)
 
const PasswordList = (props: Props) => (
  <FlatList
    numColumns={2}
    data={props.data}
    renderItem={({ item }) => renderItem(item, () => props.onItemPress(item))}
    keyExtractor={item => item.key}
    contentInset={defineContentInset()}
    automaticallyAdjustContentInsets={false}
    ListEmptyComponent={<EmptyComponent fromSearch={props.fromSearch} />}
  />
)
 
PasswordList.defaultProps = {
  data: [
    { name: 'Twitter', key: 'uuid', icon: 'twitter', color: 'red' },
    { name: 'Facebook', key: 'uuid-2', icon: 'facebook', color: 'blue' },
  ],
  onItemPress: item => console.log(`item : ${item.name}`),
  fromSearch: false,
}
 
export default PasswordList

A few clarifications:

First, the list we created displays items as a grid, and each item is a square whose dimensions are equal to half the screen width.

To do this, we calculate the dimensions with React Native's Dimensions API:

const { width } = Dimensions.get('window')
const dimension = width / 2

Then we specify that the list will be displayed in two columns with the FlatList's numColumns={2} property.

Finally, we define a defineContentInset function because, when we use TabNavigator for iOS, part of our list will be cut off by the tabs. That is why we add a bottom contentInset to the list on iOS, with the value being the height of the tabs.

We are almost finished creating the components. The only remaining components are those used on the password editing screen.

TextField

The TextField component will be composed of a TextInput and an icon on the left representing the concept associated with the TextField. This icon changes color when the field has input, and returns to the same color as the placeholder when the field is empty.

However, for the field containing the password value, the icon can have three different colors depending on the password's security level. To determine the security level, we will need to create a PasswordHelper that contains the function that determines the password level color.

In the common folder, create a PasswordHelper.js file.

To check our password's level, we will need to verify that it meets certain criteria (minimum length, uppercase, lowercase, digit, special character).

To do this, we will add functions that check whether each criterion is met.

const respectMinCharLenght = (value: string): boolean => value.length >= 8
 
const containsLowercase = (value: string): boolean => {
  const regex = /^(?=.*[a-z]).+$/
  return regex.test(value)
}
 
const containsUppercase = (value: string): boolean => {
  const regex = /^(?=.*[A-Z]).+$/
  return regex.test(value)
}
 
const containsSpecial = (value: string): boolean => {
  const regex = /^(?=.*[0-9_\W]).+$/
  return regex.test(value)
}

Then we will add the function that returns the password level based on the number of criteria that are met:

const SecurityLevel: Object = {
  LOW: 0,
  MEDIUM: 1,
  HIGH: 2,
}
 
const DefinePasswordLevel = (password: string): number => {
  let matchingCriteria = 0
  let result = SecurityLevel.LOW
  matchingCriteria = containsLowercase(password) ? matchingCriteria + 1 : matchingCriteria
  matchingCriteria = containsUppercase(password) ? matchingCriteria + 1 : matchingCriteria
  matchingCriteria = containsSpecial(password) ? matchingCriteria + 1 : matchingCriteria
 
  matchingCriteria = respectMinCharLenght(password) ? matchingCriteria + 1 : 1
 
  switch (matchingCriteria) {
    case 1:
      result = SecurityLevel.LOW
      break
    case 2:
      result = SecurityLevel.LOW
      break
    case 3:
      result = SecurityLevel.MEDIUM
      break
    case 4:
      result = SecurityLevel.HIGH
      break
    default:
      result = SecurityLevel.LOW
  }
  return result
}

Finally, we will create the function that returns the color corresponding to the password level. To do this, first import the colors defined in the colors.js file:

import { levelLow, levelMedium, levelHigh } from '../constants/colors'

Then add the GetLevelColor method:

export const GetLevelColor = (password: string): string => {
  const level = DefinePasswordLevel(password)
  if (level === SecurityLevel.MEDIUM) {
    return levelMedium
  }
  if (level === SecurityLevel.HIGH) {
    return levelHigh
  }
  return levelLow
}

We can now move on to creating the TextField component. In the components folder, add TextField.js:

/*
 * @flow
 */
 
import React, { Component } from 'react'
import { View, TextInput } from 'react-native'
import Icon from 'react-native-vector-icons/Ionicons'
import { PlateformStyleSheet, IconPlateform } from '../common/PlatformHelper'
import { GetLevelColor } from '../common/PasswordHelper'
import { IOS_MARGIN, ANDROID_MARGIN } from '../constants/dimensions'
import { PRIMARY_DARK, PLACE_HOLDER, WHITE, PRIMARY_TEXT } from '../constants/colors'
 
type Props = {
  placeholder: string,
  icon: string,
  fullWhite: boolean,
  value: string,
  secureTextEntry: boolean,
  returnKeyType: string,
  onChangeText: (text: string) => void,
  onSubmitEditing: () => void,
}
 
class TextField extends Component<Props, Props, void> {
  textInput: Object
  static defaultProps = {
    placeholder: 'TextFieldPlaceholder',
    icon: 'pricetag',
    fullWhite: false,
    value: '',
    secureTextEntry: false,
    returnKeyType: 'done',
    onChangeText: text => console.log(`Change : ${text}`),
    onSubmitEditing: () => console.log('end submit editing'),
  }
 
  getIconColor() {
    const length = this.props.value.length
    if (this.props.secureTextEntry && length >= 1) {
      return GetLevelColor(this.props.value)
    }
    return length > 0 ? PRIMARY_DARK : PLACE_HOLDER
  }
 
  focus() {
    const txtInput = this.textInput
    txtInput.focus()
  }
 
  render() {
    const textColor = this.props.fullWhite ? WHITE : PRIMARY_TEXT
    const color = this.props.fullWhite ? WHITE : PLACE_HOLDER
    const iconColor = this.props.fullWhite ? WHITE : this.getIconColor()
 
    return (
      <View style={styles.container}>
        <Icon name={IconPlateform(this.props.icon, true)} size={24} color={iconColor} />
        <TextInput
          style={[styles.textInput, { color: textColor }]}
          ref={c => {
            this.textInput = c
          }}
          placeholder={this.props.placeholder}
          autoCorrect={false}
          placeholderTextColor={color}
          value={this.props.value}
          onChangeText={text => this.props.onChangeText(text)}
          secureTextEntry={this.props.secureTextEntry}
          returnKeyType={this.props.returnKeyType}
          onSubmitEditing={this.props.onSubmitEditing}
          underlineColorAndroid={color}
        />
      </View>
    )
  }
}
export default TextField
const styles = PlateformStyleSheet({
  container: {
    flexDirection: 'row',
    android: {
      marginHorizontal: ANDROID_MARGIN,
      alignItems: 'center',
    },
    ios: {
      paddingVertical: IOS_MARGIN,
      marginBottom: IOS_MARGIN,
      marginHorizontal: IOS_MARGIN,
      borderBottomWidth: 1,
      borderColor: PLACE_HOLDER,
    },
  },
  textInput: {
    flex: 1,
    ios: {
      marginLeft: IOS_MARGIN,
    },
    android: {
      marginLeft: ANDROID_MARGIN,
    },
  },
})

ColorSelector

For each password we will be able to select a color that will be displayed in the password list. To do this, we will create the ColorSelector component:

/*
 * @flow
 */
 
import React from 'react'
import { ScrollView, View, TouchableOpacity, StyleSheet } from 'react-native'
import { SELECTABLE_COLORS, PRIMARY } from '../constants/colors'
 
type Props = {
  onPress: (color: string) => void,
}
 
const ColorSelector = (props: Props) => {
  const colors = SELECTABLE_COLORS.map((color, i) => (
    <TouchableOpacity key={i} onPress={() => props.onPress(color)}>
      <View style={[styles.colorItem, { backgroundColor: color }]} />
    </TouchableOpacity>
  ))
 
  return (
    <View style={styles.container}>
      <ScrollView
        horizontal
        automaticallyAdjustContentInsets={false}
        contentContainerStyle={styles.scrollView}
        showsHorizontalScrollIndicator={false}
      >
        {colors}
      </ScrollView>
    </View>
  )
}
 
ColorSelector.defaultProps = {
  onPress: color => console.log(`select color : ${color}`),
}
 
export default ColorSelector
 
const styles = StyleSheet.create({
  container: {
    borderWidth: 1,
    borderColor: PRIMARY,
    justifyContent: 'center',
    alignItems: 'center',
    borderRadius: 30,
    backgroundColor: 'white',
    paddingLeft: 6,
    paddingRight: 6,
    paddingBottom: 6,
    paddingTop: 6,
  },
  scrollView: {
    alignItems: 'center',
  },
  colorItem: {
    height: 26,
    width: 26,
    borderRadius: 26,
    marginRight: 2,
    marginLeft: 2,
  },
})

IconPicker & IconModal

Finally, to finish this article, we will create two components that let us select an icon for each password we add to the application.

  • IconPicker: a button that contains the selected icon and displays the icon selection component when clicked
  • IconModal: a component that lists all available icons and allows searching them by name

Let's start with the IconPicker component:

/*
 * @flow
 */
 
import React from 'react'
import { Platform, View, TouchableOpacity, TouchableNativeFeedback } from 'react-native'
import Icon from 'react-native-vector-icons/FontAwesome'
import { PlateformStyleSheet } from '../common/PlatformHelper'
import { PRIMARY, WHITE } from '../constants/colors'
 
type Props = {
  icon: string,
  onPress: () => void,
  color: string,
}
 
const IconPicker = (props: Props) => {
  if (Platform.OS === 'android') {
    return (
      <TouchableNativeFeedback
        background={TouchableNativeFeedback.SelectableBackgroundBorderless()}
        onPress={props.onPress}
      >
        <View style={styles.container}>
          <Icon name={props.icon} size={35} color={props.color} />
        </View>
      </TouchableNativeFeedback>
    )
  }
 
  return (
    <TouchableOpacity style={styles.container} onPress={props.onPress}>
      <Icon name={props.icon} size={35} color={props.color} />
    </TouchableOpacity>
  )
}
 
IconPicker.defaultProps = {
  color: PRIMARY,
  icon: 'cubes',
  onPress: console.log('onpress'),
}
 
export default IconPicker
 
const styles = PlateformStyleSheet({
  container: {
    android: {
      borderWidth: 1,
      borderColor: PRIMARY,
    },
    height: 75,
    width: 75,
    borderRadius: 75,
    backgroundColor: WHITE,
    justifyContent: 'center',
    alignItems: 'center',
  },
})

To display the list of available icons, we will need to manipulate the list provided by react-native-vector-icons. To make our lives easier, we will add lodash to our project:

yarn add lodash

Then we create our IconModal component:

/*
 * @flow
 */
 
import React, { Component } from 'react'
import { Text, FlatList, TouchableOpacity, Dimensions, Animated, Easing, View } from 'react-native'
import FontAwesome from 'react-native-vector-icons/FontAwesome'
import FontAwesomeGlyphs from 'react-native-vector-icons/glyphmaps/FontAwesome'
import _ from 'lodash'
 
import { WHITE } from '../constants/colors'
import { ANDROID_MARGIN, IOS_STATUS_BAR_HEIGHT } from '../constants/dimensions'
import strings from '../locales/strings'
import TextField from '../components/TextField'
import { PlateformStyleSheet } from '../common/PlatformHelper'
 
type State = {
  data: Array<string>,
  searchValue: string,
}
type Props = {
  onSelectIcon: (icon: string) => void,
  isOpen: boolean,
  toggleModal: () => void,
}
 
const GLYPH_MAPS = {
  FontAwesome: FontAwesomeGlyphs,
}
 
const ICON_SETS = _.map({ FontAwesome }, (component, name) => ({
  name,
  component,
})).map(iconSet => {
  // Some icons have multiple names, so group them by glyph
  const glyphMap = GLYPH_MAPS[iconSet.name]
  const newIconSet = iconSet
  newIconSet.glyphs = _.values(_.groupBy(Object.keys(glyphMap), name => glyphMap[name]))
  return newIconSet
})
 
const { width, height } = Dimensions.get('window')
 
class IconModal extends Component<Props, Props, State> {
  static defaultProps = {
    onSelectIcon: icon => console.log(icon),
    toggleModal: () => console.log('toogle modal'),
    isOpen: false,
  }
  state = {
    data: ICON_SETS[0].glyphs,
    searchValue: '',
  }
  animatedValue: Object
  constructor(props: Props) {
    super(props)
    this.animatedValue = new Animated.Value(0)
  }
  componentWillReceiveProps(nextProps: Props) {
    this.animateModal(nextProps.isOpen)
  }
 
  animateModal(isOpen: boolean) {
    const toValue = isOpen ? 0 : 1
    Animated.timing(this.animatedValue, {
      toValue,
      duration: 200,
      easing: Easing.linear,
    }).start()
  }
 
  selectIcon(iconName: string) {
    this.props.onSelectIcon(iconName)
    this.props.toggleModal()
  }
 
  search(value: string) {
    const result = _.values(_.filter(ICON_SETS[0].glyphs, data => data.join(', ').indexOf(value.toLowerCase()) > -1))
    this.setState({
      searchValue: value,
      data: value.length > 0 ? result : ICON_SETS[0].glyphs,
    })
  }
 
  render() {
    const rowDimension = width / 4
    const top = this.animatedValue.interpolate({
      inputRange: [0, 1],
      outputRange: [height, 0],
    })
    return (
      <Animated.View style={[styles.sView, { top }]}>
        <TextField
          fullWhite
          icon="search"
          placeholder={strings.search}
          value={this.state.searchValue}
          onChangeText={value => this.search(value)}
        />
        <FlatList
          data={this.state.data}
          numColumns={4}
          automaticallyAdjustContentInsets={false}
          initialListSize={40}
          keyExtractor={item => item[0]}
          renderItem={({ item }) => renderItem(() => this.selectIcon(item[0]), item[0], rowDimension)}
        />
        <TouchableOpacity activeOpacity={0.5} onPress={() => this.props.toggleModal()}>
          <Text style={styles.actionItm}>{strings.close}</Text>
        </TouchableOpacity>
      </Animated.View>
    )
  }
}
 
const renderItem = (onPress: (name: string) => void, icon: string, dimension: number) => (
  <TouchableOpacity activeOpacity={0.6} style={[styles.row, { width: dimension, height: dimension }]} onPress={onPress}>
    <View style={styles.iconCtnr}>
      <FontAwesome name={icon} size={40} color="white" />
    </View>
  </TouchableOpacity>
)
export default IconModal
 
const styles = PlateformStyleSheet({
  sView: {
    elevation: 7,
    position: 'absolute',
    right: 0,
    left: 0,
    bottom: 0,
    backgroundColor: 'rgba(0, 0, 0, 0.8)',
    justifyContent: 'flex-start',
    alignItems: 'center',
    ios: { paddingTop: IOS_STATUS_BAR_HEIGHT },
    android: { paddingTop: ANDROID_MARGIN },
  },
  actionItm: {
    fontSize: 22,
    fontWeight: 'bold',
    color: WHITE,
    marginBottom: ANDROID_MARGIN,
    marginTop: ANDROID_MARGIN,
  },
  row: {
    justifyContent: 'center',
    backgroundColor: 'transparent',
    alignItems: 'center',
    width: 70,
    height: 70,
  },
  iconCtnr: {
    height: 70,
    width: 70,
    justifyContent: 'center',
    alignItems: 'center',
  },
})

To finish, add the language resources:

en.js

...
search: 'Search',
close: 'Close',

fr.js

...
search: 'Rechercher',
close: 'Fermer',

strings.js

...
search: I18n.t('search'),
close: I18n.t('close'),

That's it, we have finished creating the application's components.

Series navigation