React Native Gestionnaire de mots de passe : Création des composants

16 octobre 2017

Thibault MOCELLIN
React Native Gestionnaire de mots de passe : Création des composants

Bonjour à tous on se retrouve aujourd'hui dans le second article de la série dans laquelle nous allons créer un gestionnaire de mots de passe. Dans cet article nous allons créer les composants que nous utiliserons dans les différents écrans de l'application.

Couleurs, Dimensions et Plateformes spécifiques

Nous allons commencer par définir l'ensemble des couleurs que nous utiliserons dans l'application.

Dans le dossier src créer un dossier constants et ajoutez-y un fichier colors.js :

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

Ensuite ajoutez le code suivant dans le fichier colors.js :

// 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'

Cela nous permet de ne plus tenir compte des valeurs de chaque couleur une fois définit, nous avons juste à les importer dès que l'on en a besoin dans un composant.

Nous allons procéder de la même manière avec les dimensions, dans le dossier constants créez un fichier dimensions.js et ajoutez le code suivant :

// 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

Comme il a été évoqué dans la présentation de la série, l'application que nous allons créer possédera des composants spécifiques par plateforme (Android / iOS).

Pour gérez les composants par plateforme nous allons le faire de deux manières différentes, la première étant en utilisant l'api Platform de React Native qui permet de détecter la plateforme utilisée de la manière suivante :

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

Ou bien en incluant la plateforme dans le nom du fichier comme ceci myComponent.ios.js et myComponent.android.js.

Cette méthode sera utilisée lorsque les composants seront complètements différents entre chaque plateforme. Cependant il possible dans certains cas que nous ayons juste besoin de changer quelques propriétés de style et/ou bien un icône en fonction de la plateforme dans ce cas la nous allons utiliser une autre méthode qui consiste à créer un Helper qui contiendra les fonctions nécessaires pour récupérer les bonnes propriétés de style ou bien les icônes en fonction de la plateforme.

Dans le dossier src créez un dossier common et ajoutez-y un fichier nommé PlatformHelper.js :

mkdir common && cd common
touch PlatformHelper.js

Premièrement nous allons créer une fonction qui sera une surcouche de StyleSheet.Create qui nous permettra de spécifier la plateforme directement dans le style de la manière suivante :

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

Ici on a créé le style pour un boutton qui sur les deux plateformes aura les mêmes dimensions cependant sa couleur changera en fonction de la plateforme.

Dans le fichier PlatformHelper.js ajoutez le code suivant :

/*
  @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)
}

Maintenant concernant les icônes nous allons utiliser les icônes Ionicons qui sont disponibles parmi la liste d'icônes de la librairie react-native-vector-icons. Ionicons propose des icônes pour iOS et Android le fonctionnement est le suivant prenons l'exemple de l'icône représente un plus, le nom de l'icône est "add" ensuite pour Android le nom sera "md-add" et pour iOS "ios-add" ou "ios-add-outline".

Nous allons rajouter une méthode au PlatformHelper pour sélectionner le bon nom automatiquement en fonction de la plateforme :

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
}

Les composants

Nous allons maintenant passer à la création des composants.

SearchBar

searchbar react-native

On commence avec la barre de recherche, ce composant étant complètement différent entre iOS et Android nous allons créer deux composants distincts. Tous d'abord créer un dossier components dans le dossier src, ensuite dans ce dossier créer un nouveau dossier nommé SearchBar et ajoutez-y deux fichiers nommés index.ios.js et index.android.js :

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

Commençons par la barre de recherche iOS celle-ci aura deux rôles le premier sera donc de permettre la recherche de mots de passe dans la liste, le second sera d'ajouter un nouveau mot de passe grâce au bouton + situé à droite. De plus nous aurons une animation lorsque l'on rentre en saisie dans la barre de recherche le bouton + effectuera une rotation afin de devenir une x et dans ce cas le bouton permettra d'annuler la saisie en cours et vider la zone de recherche.

Avant de passer au code nous allons ajouter les traductions nécessaires pour ce composant :

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'),

Ensuite puisque nous utilisons Flow dans ce projet nous allons définir le type pour les propriétés du composant dans le dossier src créez un dossier nommé types et ajoutez un fichier searchBar.js et ajoutez le code suivant :

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

Rajoutez le code suivant dans le fichier index.ios.js du dossier SearchBar :

/*
 * @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',
      },
    ],
  },
})

Revenons en détail sur ce que nous avons fait dans ce composant :

Concernant Flow et le typechecking la première chose que nous faisons c'est d'importer le type de nos propriétés :

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

Ensuite de la même manière que nous avons déclarer le type Props nous déclarons un type State :

type State = {
  isEditing: boolean,
}

Puis on passe à notre composant les types de que nous venons de créer :

class SearchBar extends Component<Props, Props, State> Les valeurs entre < > représentent < defaultProps , props , state > ici nous définissons donc que nos defaultProps seront du type Props que les props seront du type Props et enfin que le state sera du type State.

Enfin il ne nous reste plus qu'à définir les valeurs initiales du state ainsi que les 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'),
};

Ensuite concernant le textInput la première chose importante à faire est de définir une variable nommée searchInput de type object qui représentera notre textInput et qui nous permettra de le manipuler. Ensuite on attache à searchInput la référence de notre textInput :

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

Grace à ça lorsqu'on clique sur le bouton selon si on est entrain d'effectuer une recherche ou non, on va pouvoir soit ajouter un nouveau mot de passe via la propriété "addItem" soit vider la zone de saisie, retirer le focus et faire disparaitre le clavier.

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

Passons maintenant à la barre de recherche Android dans le fichier index.android.js ajoutez le code suivant :

/*
 * @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

Nous allons maintenant créer le composant SliderRow ce composant va nous permettre de sélectionner la longueur des mots de passe qui seront générés.

Dans le dossier components créez un fichier nommée 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,
  },
})

Ici nous avons créé un composant stateless ce qui signifie qu'il ne gère aucun état (state), il restitue et utilise juste les propriétés qui lui sont passées.

Premièrement nous avons créé le type Flow pour les propriétés du composant.

Ensuite nous avons créer un fonction nommée renderSlider, comme vous le constatez en fonction de la plateforme nous ne retournons pas tout à fait le même composant c'est pour cela que nous avons créer cette fonction. Etant données qu'une partie du composant est identique au deux plateformes nous créons une fonction qui elle va s'occuper de gérer la partie du composant qui est spécifique aux plateformes et ensuite dans notre composant globale nous appelons cette fonction comme c'est le cas dans la fonction SliderRow.

Enfin nous avons utilisé notre PlateformStyleSheet pour définir des styles spécifiques en fonction de la plateforme.

Pour tester le composant vous pouvez coller ce code dans le fichier index.js de src :

/**
 * 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="Longueur du mot de passe"
          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

De la même manière que SliderRow nous allons créer le composant CheckBoxRow :

/*
* @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 },
  },
});
Pour tester modifiez le fichier index.js :

/**
 * 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="Longueur du mot de passe"
          selectedValue={this.state.passwordLength}
          onSlidingComplete={length => this.setValue(length)}
          onValueChange={length => this.setValue(length)}
        />
        <CheckBoxRow
          label="Génération automatique"
          isChecked={this.state.isChecked}
          switchValueChange={value => this.setBoolValue(value)}
        />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5FCFF',
  },
});

SettingRow

Enfin pour finir avec les composants que nous utiliserons dans les écrans de parametrage nous allons créer le composant SettingRow :

/*
* @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 Maintenant nous allons créer la liste des mots de passe commençons par créer un dossier PasswordList dans le dossier components :

mkdir PasswordList Ensuite pour la liste nous allons avoir de besoin de plusieurs composants :

Le composant de la liste qui sera une FlatList Le composant item qui représentera un mot de passe dans la liste Le composant emtpy qui sera utilisé lorsque la liste sera vide ou bien que la recherche ne retournera aucun résultats Commençons par le composant empty dans le dossier PasswordList créez un fichier EmptyComponent.js et ajoutez le code suivant : (Clonez le repo github pour récupérer les images utilisées)

/*
* @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,
  },
})

Ensuite il faut ajouter les ressources de localisations utilisées dans le composant

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'),

Ensuite nous allons créer le composant PasswordItem toujours dans le dossier PasswordList

/*
* @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',
  },
})

Pour terminer nous allons créer la liste des mots de passe. Sachant que cette liste recevra en propriété la liste des mots de passe, nous allons devoir créer un type Flow représentant un mot de passe.

Dans le dossier types créer un fichier Password.js :

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

Maintenant nous pouvons créer la liste dans le dossier components/PasswordList ajoutez un fichier index.js avec le code suivant :

/*
* @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

Quelques précisions :

Premièrement la liste que nous avons créer affiche les items sous forme de grille et chaque item est un carrée dont les dimensions sont égales à la moitié de la largeur de l'écran.

Pour cela on calcule les dimensions avec l'api Dimension de React Native :

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

Ensuite on définit que l'affichage de la liste se fera sur deux colonnes grace la propriété numColumns={2} de la FlatList.

Enfin on définit une fonction defineContentInset car lorsqu'on la va utiliser le TabNavigator pour iOS une partie de notre liste sera coupe par les Tabs c'est pour cela qu'on ajoute un contentInset en bas de la liste sur iOS qui prend pour valeur la hauteur des Tabs.

Nous en avons presque terminé avec la création des composants il ne nous reste plus qu'à créer les composants utilisés sur l'écran d'édition des mots de passe.

TextField

Le composant TextField sera composé d'un TextInput ainsi que d'un icône sur la gauche représentant le concept lié au TextField. Cet icône change de couleur lorsque la zone est saisie et redevient de la même couleur que le placeholder lorsque la zone est vide.

Cependant pour le champ contenant la valeur du mot de passe l'icône peut avoir trois couleurs différentes en fonction du niveau de sécurité du mot de passe. Pour déterminer le niveaux de sécurité nous allons devoir créer un PasswordHelper qui contiendra la fonction qui détermine la couleur du niveau du mot de passe.

Dans le dossier common créez un fichier PasswordHelper.js.

Pour vérifier le niveau de notre mot de passe nous allons devoir vérifier qu'il respecte certains critères (longeur minimum, majuscule, minuscule, chiffre, caractère spéciaux).

Pour faire cela nous allons ajouter des fonctions qui vérifie si chacun des critères est respecté.

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)
}

Ensuite nous allons ajouter la fonction qui retourne le niveaux du mot de passe en fonction du nombre de critère qui sont respectés :

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
}

Enfin nous allons créer la fonction qui retourne la couleur correspondant au niveau du mot de passe. Pour cela il faut d'abord importer les couleurs qui sont définies dans le fichier colors.js :

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

Puis on ajoute la méthode GetLevelColor :

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

Nous pouvons maintenant passer à la création du composant TextField dans le dossier components ajoutez 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

Pour chaque mot de passe nous allons pouvoir sélectionner une couleur qui sera afficher dans la liste des mots de passe pour ce faire nous allons créer le composant ColorSelector :

/*
* @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

Enfin pour terminer cet article nous allons créer deux composants qui vont nous permettre de sélectionner un icône pour chaque mot de passe que l'on ajoutera à l'application.

  • IconPicker : bouton qui contiendra l'icône sélectionné et qui lorsqu'on clique dessus affiche le composant de sélection des icônes
  • IconModal : Composant qui liste l'ensemble des icônes disponibles et permet de les rechercher par nom Commençons par le composant IconPicker :
/*
* @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',
  },
})

Afin d'afficher la liste des icônes disponibles nous allons avoir besoin de manipuler la liste fournie par react-native-vector-icons pour nous simplifier la vie nous allons ajouter lodash à notre projet :

yarn add lodash

Ensuite on créé notre composant IconModal :

/*
* @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',
  },
})

Pour terminer on ajoute les ressources de langues :

en.js

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

fr.js

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

strings.js

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

Voila nous en avons terminé avec la création des composants de l'application vous pouvez retrouver le code source ici.