React Native Gestionnaire de mots de passe : Les écrans

23 octobre 2017

Thibault MOCELLIN
React Native Gestionnaire de mots de passe : Les écrans

Bonjour à tous dans cet article nous allons créer l'ensemble des écrans de l'application.

Afin de pouvoir visualiser le rendu final de chaque écran nous allons installer la librairie que nous utiliserons pour la navigation ce qui nous permettra d'avoir le header de chaque écran.

La librairie de navigation est react-navigation pour l'installer saisissez la commande suivante :

yarn add react-navigation

Ensuite nous allons créer le dossier qui contiendra l'ensemble des écrans :

cd src && mkdir screens

Pour pouvoir visualiser chaque écran au fur et à mesure il faut modifier le fichier index.js à la racine de src de la manière suivante :

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

import { StackNavigator } from 'react-navigation'

import SetupScreen from './screens/Setup'

const App = StackNavigator({
  Home: {
    screen: SetupScreen,
    navigationOptions: ({ navigation }: Object) => ({
      title: 'Hello title',
      headerStyle: { backgroundColor: '#01D88D' },
      headerTintColor: 'white',
      //header:null if we want to hide the default header
    }),
  },
})

export default App

Ici on utilise un simple StackNavigator avec une seul route, ensuite il nous suffira de remplacer l'écran par celui sur lequel on travail.

Création des écrans

Nous pouvons passer à la création des écrans

Setup

Le premier écran que nous allons créer est l'écran d'initialisation de l'application qui apparait lors de la première utilisation. Il permet à l'utilisateur de définir le mot de principale qui lui servira par la suite pour déverrouiller l'application et accéder à ses mots de passe.

Dans le dossier screens ajouter le fichier Setup.js :

/*
* @flow
*/

import React, { Component } from 'react'
import { ScrollView, Text, Button, View } from 'react-native'
import TextField from '../components/TextField'
import { PlateformStyleSheet } from '../common/PlatformHelper'
import { ANDROID_MARGIN, IOS_MARGIN } from '../constants/dimensions'
import { PRIMARY_TEXT, DELETE_COLOR, PRIMARY, WHITE } from '../constants/colors'
import strings from '../locales/strings'

type State = {
  password: string,
  confirmation: string,
}

export default class SetupScreen extends Component<void, void, State> {
  confirmation: Object
  state = {
    password: '',
    confirmation: '',
  }

  submit() {
    const { password, confirmation } = this.state
    console.log('====================================')
    console.log(`password : ${password} confirmation : ${confirmation}`)
    console.log('====================================')
  }

  render() {
    return (
      <ScrollView contentContainerStyle={styles.container}>
        <Text style={styles.instruction}>{strings.instructions}</Text>
        <TextField
          iconName="lock"
          iosOutline
          placeholder={strings.password}
          secureTextEntry
          value={this.state.password}
          onChangeText={text => this.setState({ password: text })}
          onSubmitEditing={() => {
            this.confirmation.focus()
          }}
          returnKeyType="next"
        />
        <TextField
          icon="lock"
          ref={c => {
            this.confirmation = c
          }}
          placeholder={strings.confirmation}
          secureTextEntry
          value={this.state.confirmation}
          onChangeText={text => this.setState({ confirmation: text })}
        />

        <Text style={[styles.instruction, { color: DELETE_COLOR }]}>
          Ce texte sera remplacer lorsqu'on ajoutera redux
        </Text>
        <View style={styles.submit}>
          <Button
            title={strings.save}
            color={PRIMARY}
            onPress={() => this.submit()}
          />
        </View>
      </ScrollView>
    )
  }
}

const styles = PlateformStyleSheet({
  container: {
    flex: 1,
    paddingTop: 18,
    backgroundColor: WHITE,
  },
  instruction: {
    android: { padding: ANDROID_MARGIN },
    ios: { padding: IOS_MARGIN },
    color: PRIMARY_TEXT,
    textAlign: 'justify',
    marginBottom: 18,
  },
  submit: {
    width: 150,

    alignSelf: 'center',
  },
})

Ensuite nous allons rajouter les ressources manquantes :

fr.js

...
// Setup screen ,
setup: 'Initialisation',
instructions:
  'Pour utiliser Olly vous devez définir seulement un mot de passe. Vous devez le garder en sécurité car si vous le perdez vous ne pourrez pas restaurer vos données',
password: 'Mot de passe',
confirmation: 'Confirmation du mot de passe',
save: 'Enregistrer',

en.js

...
// Setup screen ,
setup: 'Initialization',
instructions:
  "To use Olly you need to set only one password. You need to keep it safe because if you loose you can't recover your data ",
password: 'Password',
confirmation: 'Password confirmation',
save: 'Save',

strings.js

...
setup: I18n.t('setup'),
instructions: I18n.t('instructions'),
password: I18n.t('password'),
confirmation: I18n.t('confirmation'),
save: I18n.t('save'),

Unlock

Passons à l'écran de déverrouillage de l'application, ajoutez Unlock.js :

/*
* @flow
*/

import React, { Component } from 'react'
import { ScrollView, View, Image, Button, Text } from 'react-native'
import { PlateformStyleSheet } from '../common/PlatformHelper'
import TextField from '../components/TextField'
import strings from '../locales/strings'
import { ANDROID_MARGIN, IOS_MARGIN } from '../constants/dimensions'
import { PRIMARY, WHITE, DELETE_COLOR } from '../constants/colors'

type State = {
  password: string,
}
const image = require('../img/book.png')

export default class SetupScreen extends Component<void, void, State> {
  state = {
    password: '',
  }

  submit() {
    console.log('====================================')
    console.log(`mot de passe : ${this.state.password}`)
    console.log('====================================')
  }

  render() {
    return (
      <ScrollView contentContainerStyle={styles.container}>
        <Image source={image} style={styles.logo} />
        <TextField
          icon="lock"
          placeholder={strings.password}
          secureTextEntry
          value={this.state.password}
          onChangeText={text => this.setState({ password: text })}
        />
        <Text style={styles.error}>
          {' '}
          Ce texte sera remplacer lorsqu'on ajoutera redux
        </Text>
        <View style={styles.submit}>
          <Button
            title={strings.unlock}
            color={PRIMARY}
            onPress={() => this.submit()}
          />
        </View>
      </ScrollView>
    )
  }
}
const styles = PlateformStyleSheet({
  container: {
    flex: 1,
    backgroundColor: WHITE,
    justifyContent: 'center',
    android: { padding: ANDROID_MARGIN },
    ios: { padding: IOS_MARGIN },
  },
  logo: {
    marginBottom: 32,
    alignSelf: 'center',
  },
  error: {
    marginVertical: 16,
    color: DELETE_COLOR,
    textAlign: 'justify',
  },
  submit: {
    width: 150,
    alignSelf: 'center',
  },
})

Les ressources :

fr.js

...
// Unlock Screen
  unlock: 'Déverrouiller',

en.js

...
// Unlock Screen
  unlock: 'Unlock',

strings.js

...
save: I18n.t('unlock'),

Passwords

Maintenant nous allons ajouter l'écran qui liste l'ensemble des mots de passe et qui permet aussi d'ajouter et de rechercher des mots de passe.

Avant de passer à la création de l'écran dans l'article précédent ou l'on a créé les composants. J'ai omis un certain nombre de composants que nous allons devoir créer dans cet article. Le premier est le bouton permettant d'ajouter des mots de passe sur la version Android dans le dossier components ajoutez le fichier ActionButton.js et ajoutez le code suivant :

/*
* @flow
*/

import React from 'react'
import { StyleSheet, TouchableNativeFeedback, View } from 'react-native'
import Icon from 'react-native-vector-icons/Ionicons'
import { ACTION_BUTTON } from '../constants/dimensions'
import { WHITE, PRIMARY } from '../constants/colors'

type Props = {
  onPress: () => void,
}

const ActionButton = (props: Props) => (
  <View style={[styles.container, { elevation: 4 }]}>
    <TouchableNativeFeedback
      background={TouchableNativeFeedback.SelectableBackgroundBorderless()}
      onPress={() => props.onPress()}
    >
      <View style={styles.container}>
        <Icon name="md-add" size={24} color={WHITE} />
      </View>
    </TouchableNativeFeedback>
  </View>
)

ActionButton.defaultProps = {
  onPress: () => console.log('onPress'),
}

export default ActionButton
const styles = StyleSheet.create({
  container: {
    height: ACTION_BUTTON,
    width: ACTION_BUTTON,
    borderRadius: ACTION_BUTTON,
    backgroundColor: PRIMARY,
    alignItems: 'center',
    justifyContent: 'center',
  },
})

Ensuite nous passons à l'écran dans le dossier screens ajoutez Passwords.js :

/*
* @flow
*/
import React, { Component } from 'react'
import { View, Platform, StyleSheet } from 'react-native'
import _ from 'lodash'
import SearchBar from '../components/SearchBar'
import PasswordList from '../components/PasswordList'
import ActionButton from '../components/ActionButton'
import { WHITE } from '../constants/colors'
import { ANDROID_MARGIN } from '../constants/dimensions'
import type { Password } from '../types/Password'

type State = {
  passwords: Array<Password>,
  searchResults: Array<Password>,
  searchValue: string,
}

class PassworsScreen extends Component<void, void, State> {
  // les valeurs du state sont temporaires et changeront lorsqu'on ajoutera redux
  state = {
    passwords: [
      { name: 'Twitter', key: 'uuid', icon: 'twitter', color: 'red' },
      { name: 'Facebook', key: 'uuid-2', icon: 'facebook', color: 'blue' },
      { name: 'Twitter', key: 'uuid-3', icon: 'twitter', color: 'red' },
      { name: 'Facebook', key: 'uuid-4', icon: 'facebook', color: 'blue' },
      { name: 'Twitter', key: 'uuid-5', icon: 'twitter', color: 'red' },
      { name: 'Facebook', key: 'uuid-6', icon: 'facebook', color: 'blue' },
    ],
    searchValue: '',
    searchResults: [
      { name: 'Twitter', key: 'uuid', icon: 'twitter', color: 'red' },
      { name: 'Facebook', key: 'uuid-2', icon: 'facebook', color: 'blue' },
      { name: 'Twitter', key: 'uuid-3', icon: 'twitter', color: 'red' },
      { name: 'Facebook', key: 'uuid-4', icon: 'facebook', color: 'blue' },
      { name: 'Twitter', key: 'uuid-5', icon: 'twitter', color: 'red' },
      { name: 'Facebook', key: 'uuid-6', icon: 'facebook', color: 'blue' },
    ],
  }

  showPassword(password: Password) {
    console.log('====================================')
    console.log(JSON.stringify(password))
    console.log('====================================')
  }

  searchPassword(search: string) {
    const result = _.filter(this.state.passwords, data =>
      data.name.toLowerCase().includes(search.toLowerCase())
    )
    this.setState({
      searchResults: search.length > 0 ? result : this.state.passwords,
      searchValue: search,
    })
  }

  clearSearch() {
    this.setState({
      searchResults: this.state.passwords,
      searchValue: '',
    })
  }

  addNewItem() {
    console.log('====================================')
    console.log('add new item')
    console.log('====================================')
  }

  openMenu() {
    console.log('====================================')
    console.log('open drawer')
    console.log('====================================')
  }

  renderActionButton() {
    if (Platform.OS === 'android') {
      return (
        <View style={styles.action}>
          <ActionButton onPress={() => this.addNewItem()} />
        </View>
      )
    }
    return null
  }
  render() {
    const { searchResults, searchValue } = this.state

    return (
      <View style={styles.container}>
        <SearchBar
          onChangeText={text => this.searchPassword(text)}
          addItem={() => this.addNewItem()}
          onClear={() => this.clearSearch()}
          openMenu={() => this.openMenu()}
        />
        <PasswordList
          data={[]}
          onItemPress={item => this.showPassword(item)}
          fromSearch={searchValue.length > 0}
          emptyOnPress={() => this.addNewItem()}
        />
        {this.renderActionButton()}
      </View>
    )
  }
}

export default PassworsScreen

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: WHITE,
  },
  action: {
    position: 'absolute',
    bottom: 0,
    left: 0,
    right: 0,
    marginBottom: ANDROID_MARGIN,
    alignItems: 'center',
  },
})

Ici on voit bien que les écrans sont juste un assemblage des composants précédemment créés. L'ensemble des states et actions que l'on verra dans la suite de cet article seront modifiés lorsque nous ajouterons redux au projet.

Comme vous pouvez le voir le composant PasswordList possède un propriété emptyOnPress. Si vous vous rappelez lorsqu'on a créé le composant Empty qui est affiché lorsque la liste des mots de passe est vide, nous avions ajouter un propriété de type booléen nommée fromSearch. Cette propriété à pour but de définir si la liste est vide à cause d'une recherche ne renvoyant aucun résultats ou bien que la liste ne contient aucun mots de passe de base.

Lorsque nous sommes dans le second cas le composant affiche un bouton permettant d'ajouter des mots de passe, cependant lors de la création des composants, j'ai oublié d'ajouter la propriété onPress du bouton au composant PasswordList c'est ce que nous allons faire maintenant.

Dans le fichier index.js de components/PasswordList il faut premièrement rajouter la propriété emptyOnPress dans le type Flow Props :

type Props = {
  data: Array<Password>,
  onItemPress: (item: Password) => void,
  fromSearch: boolean,
  emptyOnPress: () => void,
}

Ensuite il faut la rajouter lorsqu'on utlise notre EmptyComponenent dans la propriété "ListEmptyComponent" de la FlatList :

<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} onPress={props.emptyOnPress} />}
  />
Et enfin définir un valeur par défaut pour cette propriété :

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,
  emptyOnPress: console.log('empty on press'),
};

Settings

L'écran Settings est l'écran qui permet à l'utilisateur de configurer l'application pour définir par exemple la longueur par défaut des mots de passe qui seront générés ou bien encore choisir si il veut que les mots de passe soient définis automatiquement lors de la création :

Toujours dans le dossier screens rajoutez Settings.js :

/*
* @flow
*/

import React, { Component } from 'react'
import { View, Alert } from 'react-native'
import { PlateformStyleSheet } from '../common/PlatformHelper'
import SettingRow from '../components/SettingRow'
import SliderRow from '../components/SliderRow'
import CheckBoxRow from '../components/CheckBoxRow'
import strings from '../locales/strings'
import { IOS_BACKGROUND, WHITE, DELETE_COLOR } from '../constants/colors'

type State = {
  passwordLength: number,
  autoGeneration: boolean,
}

class SettingsScreen extends Component<void, void, State> {
  state = {
    passwordLength: 18,
    autoGeneration: true,
  }

  setPasswordLength(length: number) {
    this.setState({
      passwordLength: length,
    })
  }

  setAutoGeneration(value: boolean) {
    this.setState({
      autoGeneration: value,
    })
  }

  deleteAllPasswords() {
    Alert.alert(strings.clear, strings.clearConfirmation, [
      { text: strings.cancel, style: 'cancel' },
      {
        text: strings.delete,
        onPress: () => console.log('all password delete'),
      },
    ])
  }

  render() {
    return (
      <View style={styles.container}>
        <SliderRow
          label={strings.passwordLength}
          onValueChange={value => this.setPasswordLength(value)}
          selectedValue={this.state.passwordLength}
        />
        <CheckBoxRow
          label={strings.passwordAuto}
          iconName="star"
          isChecked={this.state.autoGeneration}
          switchValueChange={value => this.setAutoGeneration(value)}
        />
        <View style={styles.itemContainer}>
          <SettingRow
            label={strings.deleteAllPasswords}
            iconName="trash"
            iconBackground={DELETE_COLOR}
            iosOultine
            onPress={() => this.deleteAllPasswords()}
          />
        </View>
      </View>
    )
  }
}

export default SettingsScreen

const styles = PlateformStyleSheet({
  container: {
    flex: 1,
    ios: {
      backgroundColor: IOS_BACKGROUND,
    },
    android: {
      backgroundColor: WHITE,
    },
  },
  itemContainer: {
    ios: {
      marginTop: 16,
    },
  },
})

Puis les ressources :

fr.js

...
passwordAuto: 'Génération automatique',
clear: 'Supprimer les données',
clearConfirmation: "Êtes-vous sûr de vouloir supprimer l'intégralité des mots de passe ?",
passwordLength: 'Longeur du mot de passe',
deleteAllPasswords: 'Supprimer tous les mots de passe',
delete: 'Supprimer',
cancel: 'Annuler',

en.js

...
// Settings screen
passwordAuto: 'Automatic generation',
clear: 'Clear data',
clearConfirmation: 'Are you sure you want to delete all passwords ?',
passwordLength: 'Password length',
deleteAllPasswords: 'Delete all passwords',
delete: 'Delete',
cancel: 'Cancel',

strings.js

...
passwordAuto: I18n.t('passwordAuto'),
clear: I18n.t('clear'),
clearConfirmation: I18n.t('clearConfirmation'),
passwordLength: I18n.t('passwordLength'),
deleteAllPasswords: I18n.t('deleteAllPasswords'),
delete: I18n.t('delete'),
cancel: I18n.t('cancel'),

Synchronization & InitSynchronization

Passons maintenant à la partie Synchronisation qui permet de sauvegarder ses données cryptées sur Dropbox, de les restaurer ou bien de les supprimer.

Cette partie ce devise en deux écrans l'écran d'initialisation qui sera affiché la première fois que l'utilisateur se rendra sur la partie synchronisation afin de liée son compte Dropbox à l'application. La seconde partie est l'écran qui permet de sauvegarder, récupérer et supprimer la sauvegarde.

Commençons par l'écran d'initialisation ajoutez le fichier InitSynchronization.js au dossier screens :

/*
* @flow
*/

import React, { Component } from 'react'
import { Text, ScrollView, View, Image, Button } from 'react-native'

import { WHITE, PRIMARY_TEXT } from '../constants/colors'
import { PlateformStyleSheet } from '../common/PlatformHelper'
import strings from '../locales/strings'

const image = require('../img/paper_plane.png')

export default class InitSynchronizationScreen extends Component {
  render() {
    return (
      <ScrollView contentContainerStyle={styles.container}>
        <Image style={styles.image} source={image} />
        <Text style={styles.title}>{strings.synchInstruction}</Text>
        <View style={styles.login}>
          <Button
            title={strings.login}
            onPress={() => console.log('log in dropbox')}
          />
        </View>
      </ScrollView>
    )
  }
}

const styles = PlateformStyleSheet({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    ios: {
      backgroundColor: WHITE,
    },
    android: {
      backgroundColor: WHITE,
    },
  },
  image: {
    marginBottom: 32,
  },
  title: {
    fontWeight: 'bold',
    marginTop: 12,
    marginBottom: 12,
    textAlign: 'center',
    color: PRIMARY_TEXT,
  },
  login: {
    width: 150,
    alignSelf: 'center',
  },
})

Ensuite on passe à l'écran Synchronization ajoutez le fichier Synchonization.js :

/*
* @flow
*/

import React, { Component } from 'react'
import { View, Alert, ActivityIndicator } from 'react-native'
import SettingRow from '../components/SettingRow'
import strings from '../locales/strings'
import { PlateformStyleSheet } from '../common/PlatformHelper'
import {
  IOS_BACKGROUND,
  WHITE,
  DELETE_COLOR,
  PRIMARY,
} from '../constants/colors'

export default class SynchronizationScreen extends Component {
  uploadBackup() {
    console.log('====================================')
    console.log('upload data')
    console.log('====================================')
  }
  downloadBackup() {
    console.log('====================================')
    console.log('download data')
    console.log('====================================')
  }
  deleteBackup() {
    Alert.alert(strings.clear, strings.clearConfirmation, [
      { text: strings.cancel, style: 'cancel' },
      { text: strings.delete, onPress: () => console.log('delete data') },
    ])
  }
  render() {
    return (
      <View style={styles.container}>
        <SettingRow
          label={strings.publish}
          iconName="cloud-upload"
          withSeparator
          iosOultine
          iosSeparator
          iconBackground={PRIMARY}
          onPress={() => this.uploadBackup()}
        />
        <SettingRow
          label={strings.pull}
          iconName="cloud-download"
          iosOultine
          iconBackground={PRIMARY}
          onPress={() => this.downloadBackup()}
        />
        <View style={styles.itmCtnr}>
          <SettingRow
            label={strings.deleteBackup}
            iconName="trash"
            iconBackground={DELETE_COLOR}
            iosOultine
            onPress={() => this.deleteBackup()}
          />
        </View>
      </View>
    )
  }
}

const styles = PlateformStyleSheet({
  container: {
    flex: 1,
    ios: {
      backgroundColor: IOS_BACKGROUND,
    },
    android: {
      backgroundColor: WHITE,
    },
  },
  itmCtnr: {
    ios: {
      marginTop: 16,
    },
  },
  loaderCtnr: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    backgroundColor: 'rgba( 0, 0, 0, 0.7 )',
    justifyContent: 'center',
    alignItems: 'center',
  },
})

Et enfin les ressources :

fr.js

...
// Synchronization screen
synchInstruction:
  'Avant de pouvoir synchroniser vos données. Vous devez vous connecter à DropBox.',
login: 'Se connecter',
publish: 'Publier les données',
pull: 'Récupérer les données',
deleteBackup: 'Supprimer la sauvegarde',

en.js

...
// Synchronization screen
synchInstruction: 'Before you can synchronize your data. You must log in in DropBox.',
login: 'Log in',
publish: 'Publish data',
pull: 'Download data',
deleteBackup: 'Delete backup',

strings.js

...
synchInstruction: I18n.t('synchInstruction'),
login: I18n.t('login'),
publish: I18n.t('publish'),
pull: I18n.t('pull'),
deleteBackup: I18n.t('deleteBackup'),

ReadOnly & Edition

Enfin pour terminer avec les écrans nous allons créer un écran qui permettra de consulter la fiche du mot de passe et un écran pour la création et la modification.

Nous allons commencer par l'écran ReadOnly.js mais avant cela nous allons rajouter un composant pour afficher les informations.

Dans le dossier components rajoutez ReadOnlyRow.js :

/*
* @flow
*/

import React from 'react'
import { View, Text } from 'react-native'
import { ANDROID_MARGIN, IOS_MARGIN } from '../constants/dimensions'
import { PlateformStyleSheet } from '../common/PlatformHelper'
import { PRIMARY_TEXT, IOS_SLIDER_LABEL } from '../constants/colors'

type Props = {
  label: string,
  value: string,
}

const ReadOnlyRow = (props: Props) => (
  <View>
    <Text style={styles.label}>{props.label} :</Text>
    <Text style={styles.value}>{props.value}</Text>
  </View>
)

ReadOnlyRow.defaultProps = {
  label: 'Label',
  value: 'value',
}

const styles = PlateformStyleSheet({
  container: {
    alignItems: 'flex-start',
  },
  value: {
    color: PRIMARY_TEXT,
    ios: { padding: IOS_MARGIN },
    android: { padding: ANDROID_MARGIN },
    fontWeight: 'bold',
    fontSize: 16,
  },
  label: {
    color: IOS_SLIDER_LABEL,
    ios: { paddingHorizontal: IOS_MARGIN },
    android: { paddingHorizontal: ANDROID_MARGIN },
    fontSize: 16,
  },
})

export default ReadOnlyRow

Puis maintenant dans le dossier screens ajoutez ReadOnly.js :

/*
* @flow
*/

import React, { Component } from 'react'
import { View, TouchableOpacity, Text, ScrollView } from 'react-native'
import Icon from 'react-native-vector-icons/FontAwesome'
import { PlateformStyleSheet } from '../common/PlatformHelper'
import ReadOnlyRow from '../components/ReadOnlyRow'
import strings from '../locales/strings'
import {
  IOS_BACKGROUND,
  WHITE,
  DELETE_COLOR,
  PRIMARY,
} from '../constants/colors'
import { ANDROID_MARGIN, IOS_MARGIN } from '../constants/dimensions'

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

class ReadOnlyScreen extends Component<void, void, State> {
  state = {
    key: '9939diz-cd',
    name: 'Facebook',
    color: '#64A1F6',
    password: 'poekfpOKOEOFE398045:=:kOZ',
    icon: 'facebook',
    login: 'someuse@gmail.com',
    url: 'https://www.facebook.com/',
  }

  editPassword() {
    console.log('====================================')
    console.log('copy password')
    console.log('====================================')
  }

  copyPassword() {
    console.log('====================================')
    console.log('copy password')
    console.log('====================================')
  }

  deletePassword() {
    console.log('====================================')
    console.log('delet password')
    console.log('====================================')
  }

  render() {
    const { icon, color, name, password, login, url } = this.state
    return (
      <ScrollView style={styles.scrollContent}>
        <View style={styles.container}>
          <View style={styles.iconCtnr}>
            <View style={styles.icon}>
              <Icon name={icon} size={35} color={color} />
            </View>
          </View>
          <ReadOnlyRow label={strings.siteName} value={name} />
          <ReadOnlyRow label={strings.siteUrl} value={url} />
          <ReadOnlyRow label={strings.userName} value={login} />
          <ReadOnlyRow label={strings.password} value={password} />

          <View style={styles.actionContainer}>
            <TouchableOpacity
              style={styles.action}
              onPress={() => this.copyPassword()}
            >
              <Text style={[styles.actionLabel, { color: '#647CF6' }]}>
                {strings.copy}
              </Text>
            </TouchableOpacity>
            <TouchableOpacity
              style={styles.action}
              onPress={() => this.editPassword()}
            >
              <Text style={[styles.actionLabel, { color: PRIMARY }]}>
                {strings.edit}
              </Text>
            </TouchableOpacity>
            <TouchableOpacity
              style={styles.action}
              onPress={() => this.deletePassword()}
            >
              <Text style={[styles.actionLabel, { color: DELETE_COLOR }]}>
                {strings.delete}
              </Text>
            </TouchableOpacity>
          </View>
        </View>
      </ScrollView>
    )
  }
}

export default ReadOnlyScreen

const styles = PlateformStyleSheet({
  scrollContent: {
    flex: 1,
    backgroundColor: IOS_BACKGROUND,
  },
  container: {
    backgroundColor: WHITE,
    marginHorizontal: ANDROID_MARGIN,
    marginBottom: ANDROID_MARGIN,
    android: {
      marginTop: ANDROID_MARGIN,
    },
    ios: {
      marginTop: 42,
    },
  },
  iconCtnr: {
    marginBottom: 24,
    ios: { marginTop: -32 },
    android: { marginTop: ANDROID_MARGIN },
    alignSelf: 'center',
  },
  icon: {
    android: {
      borderWidth: 1,
      borderColor: PRIMARY,
    },
    height: 75,
    width: 75,
    borderRadius: 75,
    backgroundColor: WHITE,
    justifyContent: 'center',
    alignItems: 'center',
  },
  actionContainer: {
    marginTop: 32,
  },
  action: {
    ios: {
      padding: IOS_MARGIN,
      marginBottom: 16,
    },
    android: {
      padding: ANDROID_MARGIN,
      marginBottom: 6,
    },
    alignItems: 'center',
    justifyContent: 'center',
  },
  actionLabel: {
    fontSize: 18,
    android: { fontWeight: 'bold' },
  },
})

Ensuite ajoutez le fichier Edition.js :

/*
* @flow
*/

import React, { Component } from 'react'
import { View, Button, TouchableOpacity } from 'react-native'
import Icon from 'react-native-vector-icons/FontAwesome'
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'

import { PlateformStyleSheet } from '../common/PlatformHelper'
import TextField from '../components/TextField'
import IconPicker from '../components/IconPicker'
import IconModal from '../components/IconModal'
import ColorSelector from '../components/ColorSelector'
import strings from '../locales/strings'
import { IOS_BACKGROUND, WHITE, PRIMARY } from '../constants/colors'
import { ANDROID_MARGIN, IOS_MARGIN } from '../constants/dimensions'

type State = {
  key: string,
  name: string,
  color: string,
  password: string,
  icon: string,
  login: string,
  url: string,
  modalIsOpen: boolean,
}

class ReadOnlyScreen extends Component<void, void, State> {
  loginField: Object
  urlField: Object
  passwordField: Object

  state = {
    key: '9939diz-cd',
    name: 'Facebook',
    color: '#64A1F6',
    password: 'poekfpOKOEOFE398045:=:kOZ',
    icon: 'facebook',
    login: 'someuse@gmail.com',
    url: 'https://www.facebook.com/',
    modalIsOpen: false,
  }

  save() {
    console.log('====================================')
    console.log('save password')
    console.log('====================================')
  }

  generatePassword() {
    console.log('====================================')
    console.log('generate password')
    console.log('====================================')
  }

  selectIcon(icon: string) {
    this.setState({
      icon,
    })
  }

  selectColor(color: string) {
    this.setState({ color })
  }

  toggleModal() {
    this.setState({
      modalIsOpen: !this.state.modalIsOpen,
    })
  }

  render() {
    const { icon, color, name, password, login, url, modalIsOpen } = this.state

    return (
      <View style={styles.main}>
        <KeyboardAwareScrollView style={styles.scrollContent}>
          <View style={styles.container}>
            <View style={styles.iconCtnr}>
              <View style={styles.icon}>
                <IconPicker
                  icon={icon}
                  color={color}
                  onPress={() => this.toggleModal()}
                />
              </View>
            </View>

            <TextField
              placeholder={strings.siteName}
              value={name}
              onSubmitEditing={() => this.urlField.focus()}
              onChangeText={text => this.setState({ name: text })}
              returnKeyType="next"
            />
            <TextField
              icon="globe"
              placeholder={strings.siteUrl}
              value={url}
              ref={c => {
                this.urlField = c
              }}
              onSubmitEditing={() => this.loginField.focus()}
              onChangeText={text => this.setState({ url: text })}
              returnKeyType="next"
            />
            <TextField
              icon="person"
              placeholder={strings.userName}
              value={login}
              ref={c => {
                this.loginField = c
              }}
              onSubmitEditing={() => this.passwordField.focus()}
              onChangeText={text => this.setState({ login: text })}
              returnKeyType="next"
            />
            <TextField
              icon="lock"
              placeholder={strings.password}
              value={password}
              ref={c => {
                this.passwordField = c
              }}
              onChangeText={text => this.setState({ password: text })}
              secureTextEntry
            />
            <View style={styles.colorSelector}>
              <ColorSelector
                onPress={colorValue => this.selectColor(colorValue)}
              />
            </View>

            <View style={styles.actionContainer}>
              <Button
                title={strings.save}
                color={PRIMARY}
                onPress={() => this.save()}
              />
            </View>
          </View>
        </KeyboardAwareScrollView>

        <TouchableOpacity
          style={styles.generate}
          onPress={() => this.generatePassword()}
        >
          <Icon name="magic" size={32} color={PRIMARY} />
        </TouchableOpacity>

        <IconModal
          onSelectIcon={iconName => this.selectIcon(iconName)}
          isOpen={modalIsOpen}
          toggleModal={() => this.toggleModal()}
        />
      </View>
    )
  }
}

export default ReadOnlyScreen

const styles = PlateformStyleSheet({
  main: {
    flex: 1,
  },
  scrollContent: {
    flex: 1,
    backgroundColor: IOS_BACKGROUND,
  },
  generate: {
    android: {
      top: ANDROID_MARGIN,
      padding: ANDROID_MARGIN,
    },
    ios: {
      top: 42,
      padding: IOS_MARGIN,
    },
    height: 60,
    width: 60,
    alignItems: 'center',
    justifyContent: 'center',
    position: 'absolute',
    right: ANDROID_MARGIN,
    backgroundColor: WHITE,
  },
  container: {
    backgroundColor: WHITE,
    marginHorizontal: ANDROID_MARGIN,
    marginBottom: ANDROID_MARGIN,
    android: {
      marginTop: ANDROID_MARGIN,
    },
    ios: {
      marginTop: 42,
    },
  },
  iconCtnr: {
    marginBottom: 24,
    ios: { marginTop: -32 },
    android: { marginTop: ANDROID_MARGIN },
    alignSelf: 'center',
  },
  actionContainer: {
    width: 150,
    alignSelf: 'center',
    marginTop: 32,
    android: {
      marginBottom: ANDROID_MARGIN,
    },
    ios: {
      marginBottom: IOS_MARGIN,
    },
  },
  colorSelector: {
    android: {
      padding: ANDROID_MARGIN,
    },
    ios: {
      padding: IOS_MARGIN,
    },
  },
})

Et enfin les ressources :

fr.js

...
// Edition
siteName: 'Nom du site',
siteUrl: 'Url du site',
userName: 'Login',
edit: 'Editer',
copy: 'Copier le mot de passe',

en.js

...
siteName: 'Site Name',
siteUrl: 'Site url',
userName: 'Login',
edit: 'Edit',
copy: 'Copy password',

strings.js

...
siteName: I18n.t('siteName'),
siteUrl: I18n.t('siteUrl'),
userName: I18n.t('userName'),
edit: I18n.t('edit'),
copy: I18n.t('copy'),

Nous en avons maintenant terminé avec les écrans de l'application vous pouvez retrouver le code source ici.