React Native Gestionnaire de mots de passe : Gestion de la synchronisation

30 novembre 2017

Thibault MOCELLIN
React Native Gestionnaire de mots de passe : Gestion de la synchronisation

Bonjour à tous aujourd'hui dans cet article qui est le dernier de la série nous allons ajouter la sauvegarde et la récupération des données sur dropbox.

Pour mettre en place ceci nous allons devoir créer une application sur Dropbox pour pouvoir utiliser leur api, mettre en place l'authentification OAuth pour que l'utilisateur autorise l'application à se connecter à Dropbox.

Ecran de connexion

Nous allons commencer par modifier la gestion de l'écran de synchronisation afin d'afficher un écran demandant à l'utilisateur de se connecter à Dropbox avant de pouvoir sauvegarder ses données.

Commencez par créer un dossier Synchronization dans le dossier Screens puis déplacez le fichier Synchronization.js dedans.

Puis ajoutez dans le dossier Synchronization un fichier index.js avec le code suivant :

/*
* @flow
*/
import React, { Component } from 'react'
import { View } from 'react-native'
import Synchronization from './Synchronization'

export default class Index extends Component {
  render() {
    return <Synchronization />
  }
}

Ensuite il faut remplacer les imports suivants du fichier Synchronization.js :

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';
import NavBar from '../components/NavBar';
par ceux-ci :

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';
import NavBar from '../../components/NavBar';

Maintenant nous allons créer l'écran permettant de se connecter à Dropbox, ajoutez le fichier Login.js toujours dans le dossier Synchronisation :

/*
* @flow
*/
import React, { Component } from 'react';
import { View, Image, Button, Text, StyleSheet } from 'react-native';
import strings from '../../locales/strings';
import { PRIMARY_TEXT, PRIMARY, WHITE } from '../../constants/colors';
import NavBar from '../../components/NavBar';

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

type Props = {
  navigation: Object,
};

class Login extends Component<void, Props, void> {
  render() {
    return (
      <View style={styles.container}>
        <NavBar
          title={strings.synchronization}
          actionLeft={() => this.props.navigation.navigate('DrawerOpen')}
        />
        <View style={styles.subContainer}>
          <Image source={img} />
          <Text style={styles.title}> {strings.synchInstruction}</Text>
          <Button title={strings.login} onPress={()=> console.log("login"))} color={PRIMARY} />
        </View>
      </View>
    );
  }
}

export default Login;

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: WHITE,
  },
  subContainer: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    padding: 16,
  },
  title: {
    fontWeight: 'bold',
    marginTop: 32,
    marginBottom: 32,
    textAlign: 'center',
    color: PRIMARY_TEXT,
  },
});

Reducers

Afin de pouvoir définir quel écran afficher lorsque l'utilisateur se rend sur la partie synchronisation il faut que l'on stock une information nous permettant de savoir si l'utilisateur c'est connecté ou non.

Pour ce faire nous allons rajouter le reducer synchronization.

Dans le dossier reducers rajoutez le fichier synchronization.js avec le code suivant :

/*
* @flow
*/

import type { Action } from '../actions/types'
import type { SynchronizationState } from './types'

const initialState: SynchronizationState = {
  userLoggedToDropbox: false,
}

const synchronizationState = (
  state: SynchronizationState = initialState,
  action: Action
): SynchronizationState => {
  switch (action.type) {
    default:
      return state
  }
}

export default synchronizationState

Ensuite nous allons rajouter le type SynchronizationState dans le fichier types.js de reducers :

...
export type SynchronizationState = {
  +userLoggedToDropbox: boolean,
};
export type ReduxState = {
  ...
  +synchronization: SynchronizationState,
};

Pour terminer nous allons ajouter le reducer synchronization au root reducer :

...
import synchronization from './synchronization';

const rootReducer = combineReducers({
  ...
  synchronization,
});

Connexion à Dropbox

Concernant la connexion avec Dropbox j'ai déjà écris un article dessus, le fonctionnement est le même pour cette application je vous invite donc à suivre les explications de cet article avant de passer à la suite.

Il faut effectuer les modifications suivantes par rapport à l'article ci-dessus :

Remplacer dropboxoauthsample par olly partout dans le code et dans les redirect url de Dropbox Pour iOS remplacer #import "RCTLinkingManager.h" par #import <React/RCTLinkingManager.h> En cas de problème allez directement voir le code final sur github.

Passons à la gestion de la connexion avec Dropbox dans le code, cette partie est aussi couverte dans l'article ci-dessus cependant nous allons la complexifier un peu en ajoutant Redux.

Modifiez le fichier synchronization/Login.js de la manière suivante :

/*
* @flow
*/
import React, { Component } from 'react';
import { View, Image, Button, Text, StyleSheet, Linking, Platform } from 'react-native'; // #1
import uuidV4 from 'uuid/v4'; // #2
import strings from '../../locales/strings';
import { PRIMARY_TEXT, PRIMARY, WHITE } from '../../constants/colors';
import NavBar from '../../components/NavBar';

const appKey = 'puzyl8gt4mor5kp'; // #3

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

type Props = {
  navigation: Object,
};

type State = { // #4
  verification: string,
};

class Login extends Component<void, Props, State> {
  state = {
    verification: uuidV4(), // #5
  };

  logIn() { // #6
    const redirectUri = Platform.OS === 'ios' ? 'olly://open' : 'https://www.olly.com/open';#7

    const url = `https://www.dropbox.com/oauth2/authorize?response_type=token&client_id=${appKey}&redirect_uri=${redirectUri}&state=${this
      .state.verification}`;
    Linking.openURL(url).catch(err => console.error('An error occurred', err));
  }

  render() {
    return (
      <View style={styles.container}>
        <NavBar
          title={strings.synchronization}
          actionLeft={() => this.props.navigation.navigate('DrawerOpen')}
        />
        <View style={styles.subContainer}>
          <Image source={img} />
          <Text style={styles.title}> {strings.synchInstruction}</Text>
          <Button title={strings.login} onPress={() => this.logIn()} color={PRIMARY} />
        </View>
      </View>
    );
  }
}
  1. On importe Linking et Platform
  2. On importe uuidV4
  3. On définit l'app key que Dropbox nous fournit pour notre application
  4. On définit le type State avec la variable verification (voir article ci-dessus)
  5. On définit la valeur de verification
  6. Dans la méthode logIn on teste la plateforme pour définir quel valeur de redirectUri sera passée dans l'url, puis on construit l'url et enfin on ouvre le navigateur avec Linking

Maintenant nous allons ajouter le code nous permettant de s'abonner à l'événement du Linking correspondant au lancement de l'application depuis un url externe.

Afin de parser simplement les paramètres renvoyés par Dropbox nous allons ajouter la librairie shitty-qs :

yarn add shitty-qs

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

componentDidMount() {
  Linking.addEventListener('url', event => this.handleLinkingUrl(event));
}

componentWillUnmount() {
  Linking.removeEventListener('url', event => this.handleLinkingUrl(event));
}

handleLinkingUrl(event: Object) {
  const [, queryString] = event.url.match(/\#(.*)/);
  const query = shittyQs(queryString);
  if (this.state.verification === query.state) {
    this.props.setAccessToken(query.access_token);
  } else {
    alert('Verification Failed');
  }
}

Ici on s'abonne lorsque le composant est montée et on définit le callBack handleLinkingUrl, lorsque le composant est démonté on se désabonne.

Dans la méthode handleLinkingUrl on vient récupérer les paramètres de l'url, ensuite on teste que la valeur de verification passée depuis Dropbox est identique à celle du state si c'est le cas alors on execute l'action SetAccessToken. L'action SetAccessToken est passé en propriété nous devons donc la rajouter au type Props :

type Props = {
  navigation: Object,
  setAccessToken: (token: string) => void,
}

Pour terminer avec la partie connexion nous allons ajouter l'action SetAccessToken et modifier le reducer Synchronization.

Dans le dossier actions ajoutez le fichier synchronization avec le code suivant :

/*
* @flow
*/

import type { ThunkAction, Dispatch, SetAccessTokenAction } from './types';

/*
*** Actions ***
*/

export const SetAccessToken = (token: string): ThunkAction => (dispatch: Dispatch) => {
  dispatch(setAccessToken(token));
};

/*
*** Actions Creator ***
*/

const setAccessToken = (token: string): SetAccessTokenAction => ({
  type: 'SET_ACCESS_TOKEN',
  token,
});
Puis ajoutez le type SetAccessTokenAction dans le fichier types.js :

...
/*
**************
* Synchronization
**************
*/
export type SetAccessTokenAction = {
  type: 'SET_ACCESS_TOKEN',
  token: string,
};

export type Action =
  ...
  /** *** Synchronization **** */
  | SetAccessTokenAction;

Ensuite modifiez le reducer synchronization de la manière suivante :

/*
* @flow
*/

import type { Action } from '../actions/types'
import type { SynchronizationState } from './types'

const initialState: SynchronizationState = {
  userLoggedToDropbox: false,
  accessToken: '',
}

const synchronizationState = (
  state: SynchronizationState = initialState,
  action: Action
): SynchronizationState => {
  switch (action.type) {
    case 'SET_ACCESS_TOKEN':
      return { ...state, userLoggedToDropbox: true, accessToken: action.token }

    default:
      return state
  }
}

export default synchronizationState

Rajoutez accesToken dans le type SynchronizationState :

export type SynchronizationState = {
  +userLoggedToDropbox: boolean,
  +accessToken: string,
}

Enfin nous allons connecter le fichier synchronization/index.js à redux et gérer l'affichage de l'écran en fonction du state :

/*
* @flow
*/
import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import type { ReduxState } from '../../reducers/types'
import * as synchronizationActions from '../../actions/synchronization'
import Synchronization from './Synchronization'
import Login from './Login'

type Props = {
  navigation: Object,
  actions: Object,
  accessToken: string,
  isLoggedIn: boolean,
}

const Index = (props: Props) => {
  if (props.isLoggedIn) {
    return (
      <Synchronization
        navigation={props.navigation}
        accessToken={props.accessToken}
      />
    )
  }
  return (
    <Login
      navigation={props.navigation}
      setAccessToken={token => props.actions.SetAccessToken(token)}
    />
  )
}

function mapStateToProps(state: ReduxState) {
  return {
    isLoggedIn: state.synchronization.userLoggedToDropbox,
    accessToken: state.synchronization.accessToken,
  }
}
export default connect(mapStateToProps, dispatch => ({
  actions: bindActionCreators(synchronizationActions, dispatch),
}))(Index)

Sauvegarde, Restauration et suppréssion des données :

Pour la sauvegarde des données ainsi que le téléchargement nous allons utiliser la librairie react-native-fetch-blob qui permet de simplifier l'accès au fichier ainsi que le transfert de données.

Commençons par ajouter cette librairie :

yarn add react-native-fetch-blob

Les données vont être sauvegardées au format JSON. Le fichier contiendra les mots de passe cryptés ansi que les réglages de l'utilisateur. Il faut aussi que nous exportions les données techniques tels que le salt, l'iv et le verificationToken afin de pouvoir décrypter les données lorsqu'on les récupères.

Voici le format de l'objet JSON exporté :

{
  "d": "les mots de passe cryptés",
  "st": "les réglages de l'utilisateur",
  "s": "le salt",
  "v": "l'iv",
  "vt": "le verificationToken"
}

Nous allons maintenant passer à la création des types d'actions. Dans le fichier types.js du dossier actions ajoutez les types suivants :

/*
**************
* Synchronization
**************
*/
...
export type DropboxStartAction = {
  type: 'DROPBOX_ACTION_START',
  infos:string,
};

export type DropboxSuccessAction = {
  type: 'DROPBOX_ACTION_SUCCESS',
  info: string,
};

export type DropboxFailAction = {
  type: 'DROPBOX_ACTION_FAIL',
  error: string,
};

export type Action =
  ...
  /** *** Synchronization **** */
  | SetAccessTokenAction
  | DropboxStartAction
  | DropboxSuccessAction
  | DropboxFailAction;

Ici nous avons créés trois types d'actions génériques qui seront utilisés pour les trois actions (sauvegarde,restauration,suppréssion).

Maintenant que les types d'actions sont créés nous allons modifier le reducer synchronization :

/*
* @flow
*/

import type { Action } from '../actions/types'
import type { SynchronizationState } from './types'

const initialState: SynchronizationState = {
  userLoggedToDropbox: false,
  accessToken: '',
  pendingAction: false,
  message: '',
  success: false,
}

const synchronizationState = (
  state: SynchronizationState = initialState,
  action: Action
): SynchronizationState => {
  switch (action.type) {
    case 'SET_ACCESS_TOKEN':
      return { ...state, userLoggedToDropbox: true, accessToken: action.token }

    case 'DROPBOX_ACTION_START':
      return {
        ...state,
        message: action.info,
        success: false,
        pendingAction: true,
      }

    case 'DROPBOX_ACTION_SUCCESS':
      return {
        ...state,
        message: action.info,
        success: true,
        pendingAction: false,
      }

    case 'DROPBOX_ACTION_FAIL':
      return {
        ...state,
        message: action.error,
        success: false,
        pendingAction: false,
      }

    default:
      return state
  }
}

export default synchronizationState

Il ne reste plus qu'à rajouter les nouveaux champs dans le type SynchronizatonState :

export type SynchronizationState = {
  +userLoggedToDropbox: boolean,
  +accessToken: string,
  +pendingAction: boolean,
  +message: string,
  +success: boolean,
}

Pour terminer nous allons ajouter l'ensemble des ressources que nous allons utilisées :

fr.js

publishPending: 'Publication en cours ...',
pullPending: 'Récupération en cours ...',
deletePending: 'Supression en cours ...',
unHandled: 'Une erreur est survenue. Rééssayer plus tard',
notFound: 'Aucune sauvegarde trouvée',
notSameVerification: "Le mot de passe des données n'est pas le même que celui actuelle. Impossible de récupérer les données",
uploadSuccess: 'Les données ont été sauvegarder sur Dropbox avec succès',
dowloadSuccess: 'Les données ont été restaurer avec succès',
deleteBackupSuccess: 'Votre sauvegarde a été supprimé avec succès',

en.js

publishPending: 'Publication in progress ...',
pullPending: 'Download in progress ...',
deletePending: 'Deleting ...',
unHandled: 'An error has occurred. Try again later',
notFound: 'No backup found',
notSameVerification: 'The data password is not the same as the current . Unable to retrieve the data',
uploadSuccess: 'The data was saved on Dropbox successfully',
dowloadSuccess: 'Data were successfully restore',
deleteBackupSuccess: 'Your backup has been deleted successfully',

strings.js

publishPending:I18n.t('publishPending'),
pullPending:I18n.t('pullPending'),
deletePending:I18n.t('deletePending'),
unHandled:I18n.t('unHandled'),
notFound:I18n.t('notFound'),
notSameVerification:I18n.t('notSameVerification'),
uploadSuccess:I18n.t('uploadSuccess'),
dowloadSuccess:I18n.t('dowloadSuccess'),
deleteBackupSuccess:I18n.t('deleteBackupSuccess'),

Sauvegarde des données

Passons à la création de l'action qui permettra de sauvegarder les données. Dans le fichier actions/synchronization.js :

/*
* @flow
*/
import RNFetchBlob from 'react-native-fetch-blob'
import Base64 from 'base-64'
import strings from '../locales/strings'

import type {
  ThunkAction,
  Dispatch,
  SetAccessTokenAction,
  DropboxStartAction,
  DropboxSuccessAction,
  DropboxFailAction,
} from './types'

/*
*** Actions ***
*/

export const SetAccessToken = (token: string): ThunkAction => (
  dispatch: Dispatch
) => {
  dispatch(setAccessToken(token))
}
// Actions
export function BackUpData(
  token: string,
  passwords: string,
  iv: string,
  salt: string,
  verificationToken: string,
  passwordLength: number,
  autoGeneration: boolean
): ThunkAction {
  return async (dispatch: Dispatch) => {
    dispatch(startDropboxAction(strings.publishPending))
    try {
      const settings = { passwordLength, autoGeneration }

      let data = {
        d: passwords,
        st: settings,
        s: salt,
        v: iv,
        vt: verificationToken,
      }
      data = JSON.stringify(data)
      const response = await RNFetchBlob.fetch(
        'POST',
        'https://content.dropboxapi.com/2/files/upload',
        {
          Authorization: `Bearer ${token}`,
          'Dropbox-API-Arg': JSON.stringify({
            path: '/data_backup.json',
            mode: 'overwrite',
          }),
          'Content-Type': 'application/octet-stream',
        },
        Base64.encode(data)
      )
      if (response.respInfo.status.toString() !== '200') {
        dispatch(dropboxActionFail(strings.unHandled))
      }
      dispatch(dropboxActionSuccess(strings.uploadSuccess))
    } catch (error) {
      dispatch(dropboxActionFail(error.toString()))
    }
  }
}

/*
*** Actions Creator ***
*/

const setAccessToken = (token: string): SetAccessTokenAction => ({
  type: 'SET_ACCESS_TOKEN',
  token,
})

const startDropboxAction = (info: string): DropboxStartAction => ({
  type: 'DROPBOX_ACTION_START',
  info,
})

const dropboxActionSuccess = (info: string): DropboxSuccessAction => ({
  type: 'DROPBOX_ACTION_SUCCESS',
  info,
})

const dropboxActionFail = (error: string): DropboxFailAction => ({
  type: 'DROPBOX_ACTION_FAIL',
  error,
})

Ensuite nous modifions les fichiers synchronization.js et index.js pour pouvoir executer l'action.

Premièrement nous allons définir le type Props de l'écran synchronization :

type Props = {
  uploadBackup: () => void,
  downloadBackup: () => void,
  deleteBackup: () => void,
  success: boolean,
  message: string,
  pendingAction: boolean,
  navigation: Object,
}

Puis nous modifions les méthodes pour la sauvegarde, restauration et suppression des données :

uploadBackup() {
    this.props.uploadBackup();
  }
  downloadBackup() {
    this.props.downloadBackup();
  }
  deleteBackup() {
    Alert.alert(strings.clear, strings.clearConfirmation, [
      { text: strings.cancel, style: 'cancel' },
      { text: strings.delete, onPress: () => this.props.deleteBackup() },
    ]);
  }

Nous allons rajouter à l'écran un indicateur permettant de visualiser si l'action est en train de s'executer ainsi qu'un texte permettant de voir le message une fois l'action terminée.

Ajoutez une fonction renderAction comme ceci :

renderAction() {
    if (this.props.pendingAction) {
      return (
        <View style={styles.loaderCtnr}>
          <ActivityIndicator size="large" color={WHITE} animating />
          <Text style={styles.msgPending}>{this.props.message}</Text>
        </View>
      );
    }
    const color = this.props.success ? PRIMARY : DELETE_COLOR;
    return <Text style={[styles.msg, { color }]}>{this.props.message}</Text>;
  }

Ensuite rajouter cette fonction à la suite des composant de l'écran comme ceci :

render() {
  return (
    <View style={styles.container}>
      ...
      {this.renderAction()}
    </View>
  );
}

Enfin ajoutez les styles manquants :

const styles = PlateformStyleSheet({
  ...
  loaderCtnr: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    backgroundColor: 'rgba( 0, 0, 0, 0.7 )',
    justifyContent: 'center',
    alignItems: 'center',
  },
  msg: {
    ios: {
      padding: IOS_MARGIN,
    },
    android: {
      padding: ANDROID_MARGIN,
    },
  },
  msgPending: {
    color: WHITE,
    fontWeight: 'bold',
    marginTop: 16,
  },
});

Maintenant que l'écran est prêt nous allons modifier le fichier index.js pour récupérer l'ensemble des propriétés définies dans l'écran Synchronization.

On commence par définir le type Props du fichier index.js :

type Props = {
  navigation: Object,
  actions: Object,
  accessToken: string,
  isLoggedIn: boolean,
  success: boolean,
  message: string,
  pendingAction: boolean,
  passwords: string,
  iv: string,
  salt: string,
  verificationToken: string,
  passwordLength: number,
  autoGeneration: boolean,
}

Puis on passe les propriétés nécessaires au composant Synchronization :

const Index = (props: Props) => {
 const {
   isLoggedIn,
   accessToken,
   passwords,
   iv,
   salt,
   verificationToken,
   passwordLength,
   autoGeneration,
 } = props;

 if (isLoggedIn) {
   return (
     <Synchronization
       success={props.success}
       pendingAction={props.pendingAction}
       message={props.message}
       navigation={props.navigation}
       accessToken={props.accessToken}
       uploadBackup={() =>
         props.actions.BackUpData(
           accessToken,
           passwords,
           iv,
           salt,
           verificationToken,
           passwordLength,
           autoGeneration,
         )}
     />
   );
 }
 return (
   <Login
     navigation={props.navigation}
     setAccessToken={token => props.actions.SetAccessToken(token)}
   />
 );

Nous ajouterons les propriétés downloadBackup et deleteBackup au fur et à mesure que nous ajouterons les actions.

Suppression des données

Nous pouvons passer à la suppression du fichier de sauvegarde sur Dropbox, ce qui va être relativement simple puisque nous avons juste à créer l'action et l'ajoutée au composant Synchronization.

Commencons par l'action :

export function DeleteData(token: string): ThunkAction {
  return async (dispatch: Dispatch) => {
    dispatch(startDropboxAction(strings.deletePending))
    try {
      const req = {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${token}`,
          'Content-Type': 'application/json',
        },
        body: '{"path": "/data_backup.json"}',
      }
      const response = await fetch(
        'https://api.dropboxapi.com/2/files/delete',
        req
      )
      if (response.status.toString() === '409') {
        dispatch(dropboxActionFail(strings.notFound))
      } else if (response.status.toString() !== '200') {
        dispatch(dropboxActionFail(strings.unHandled))
      } else {
        dispatch(dropboxActionSuccess(strings.deleteBackupSuccess))
      }
    } catch (error) {
      dispatch(dropboxActionFail(error.toString()))
    }
  }
}

Puis on rajoute la propriété sur le composant :

<Synchronization
  success={props.success}
  pendingAction={props.pendingAction}
  message={props.message}
  navigation={props.navigation}
  accessToken={props.accessToken}
  uploadBackup={() =>
    props.actions.BackUpData(
      accessToken,
      passwords,
      iv,
      salt,
      verificationToken,
      passwordLength,
      autoGeneration
    )
  }
  deleteBackup={() => props.actions.DeleteData(accessToken)}
/>

Récupération des données

Pour terminer cet article nous allons passer à la récupération des données. Cette action est plus complexe que les deux précédentes car nous allons devoir effectuer différentes actions au sein de celle-ci :

  • Récupérer les données depuis Dropbox
  • Vérifier que le verificationToken du backup est similaire à celui de l'application
  • Définir les mots de passe cryptées du state avec ceux du backup
  • Décrypter les mots de passe et définir les mots de passe du state avec ceux qu'on aura décryptés
  • Définir les réglages du state avec ceux que l'on vient de récupérer

Nous allons commencer par ajouter les types d'actions manquants et modifier les reducers.

Pour les réglages nous allons rajouter le type SetSettingsActions dans le fichier actions/types.js :

...
/*
**************
* Settings
**************
*/
...
export type SetSettingsAction = {
  type: 'SET_SETTINGS',
  length: number,
  autoGeneration: boolean,
};

...

export type Action =
  /** *** Settings **** */
  | SetPasswordLengthAction
  | SetAutoGenerationAction
  | SetSettingsAction
  ...

Et nous modifions le reducer settings de cette manière :

const settingsState = (
  state: SettingsState = initialState,
  action: Action
): SettingsState => {
  switch (action.type) {
    case 'SET_PASSWORD_LENGTH':
      return { ...state, passwordLength: action.length }
    case 'SET_AUTO_GENERATION':
      return { ...state, autoGeneration: action.autoGeneration }
    case 'SET_SETTINGS':
      return {
        ...state,
        autoGeneration: action.autoGeneration,
        passwordLength: action.length,
      }
    default:
      return state
  }
}

Passons aux mots de passe non cryptés rajoutez le type d'action suivant :

export type SetPasswordsAction = {
  type: 'SET_PASSWORDS',
  passwords: NormalizedState,
}

Puis ajoutez le nouveaux cas dans le reducer data :

case 'SET_PASSWORDS':
    return { ...state, passwords: action.passwords };
Maintenant nous allons pouvoir créer l'action :

export function DownloadData(
  token: string,
  verificationToken: string,
  key: CryptoJS.WordArray,
): ThunkAction {
  return async (dispatch: Dispatch) => {
    dispatch(startDropboxAction(strings.pullPending));
    try {
      const response = await RNFetchBlob.fetch(
        'POST',
        'https://content.dropboxapi.com/2/files/download',
        {
          Authorization: `Bearer ${token}`,
          'Dropbox-API-Arg': '{"path": "/data_backup.json"}',
        },
      );
      if (response.respInfo.status.toString() === '409') {
        dispatch(dropboxActionFail(strings.notFound));
      } else if (response.respInfo.status.toString() !== '200') {
        dispatch(dropboxActionFail(strings.unHandled));
      } else {
        const data = JSON.parse(response.data);
        if (verificationToken === data.vt) {
          let uncryptedPasswords = Decrypt(data.d, key, data.v);
          uncryptedPasswords = JSON.parse(uncryptedPasswords);
          dispatch(setSettings(data.st.passwordLength, data.st.autoGeneration));
          dispatch(setPasswords(uncryptedPasswords));
          dispatch(updateCryptedPasswords(data.d));
          dispatch(dropboxActionSuccess(strings.dowloadSuccess));
        } else {
          dispatch(dropboxActionFail(strings.notSameVerification));
        }
      }
    } catch (error) {
      dispatch(dropboxActionFail(error.toString()));
    }
  };
}

Ainsi que les actions creators manquants :

const setSettings = (
  length: number,
  autoGeneration: boolean
): SetSettingsAction => ({
  type: 'SET_SETTINGS',
  length,
  autoGeneration,
})

const setPasswords = (passwords: NormalizedState): SetPasswordsAction => ({
  type: 'SET_PASSWORDS',
  passwords,
})

const updateCryptedPasswords = (
  cryptedPassword: string
): UpdateCryptedPasswordsAction => ({
  type: 'UPDATE_CRYPTED_PASSWORDS',
  cryptedPassword,
})

Ensuite nous avons plus qu'a modifier le fichier index.js de Synchronization.

Voici le fichier index.js final :

/*
* @flow
*/
import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import CryptoJS from 'crypto-js'
import type { ReduxState } from '../../reducers/types'
import * as synchronizationActions from '../../actions/synchronization'
import Synchronization from './Synchronization'
import Login from './Login'

type Props = {
  navigation: Object,
  actions: Object,
  accessToken: string,
  isLoggedIn: boolean,
  success: boolean,
  message: string,
  pendingAction: boolean,
  passwords: string,
  iv: string,
  salt: string,
  verificationToken: string,
  passwordLength: number,
  autoGeneration: boolean,
  cryptoKey: CryptoJS.WordArray,
}

const Index = (props: Props) => {
  const {
    isLoggedIn,
    accessToken,
    passwords,
    iv,
    salt,
    verificationToken,
    passwordLength,
    autoGeneration,
    cryptoKey,
  } = props

  if (isLoggedIn) {
    return (
      <Synchronization
        success={props.success}
        pendingAction={props.pendingAction}
        message={props.message}
        navigation={props.navigation}
        accessToken={props.accessToken}
        uploadBackup={() =>
          props.actions.BackUpData(
            accessToken,
            passwords,
            iv,
            salt,
            verificationToken,
            passwordLength,
            autoGeneration
          )
        }
        deleteBackup={() => props.actions.DeleteData(accessToken)}
        downloadBackup={() =>
          props.actions.DownloadData(accessToken, verificationToken, cryptoKey)
        }
      />
    )
  }
  return (
    <Login
      navigation={props.navigation}
      setAccessToken={token => props.actions.SetAccessToken(token)}
    />
  )
}

function mapStateToProps(state: ReduxState) {
  return {
    isLoggedIn: state.synchronization.userLoggedToDropbox,
    accessToken: state.synchronization.accessToken,
    success: state.synchronization.success,
    message: state.synchronization.message,
    pendingAction: state.synchronization.pendingAction,
    passwords: state.cryptedData.passwords,
    iv: state.user.iv,
    salt: state.user.salt,
    cryptoKey: state.data.key,
    verificationToken: state.user.verificationToken,
    passwordLength: state.settings.passwordLength,
    autoGeneration: state.settings.autoGeneration,
  }
}
export default connect(mapStateToProps, dispatch => ({
  actions: bindActionCreators(synchronizationActions, dispatch),
}))(Index)

Cet article est maintenant terminé. Prochainement l'application sera publiée sur les stores d'application. Rejoignez le newsletter pour être avertit de sa sortie.

Bien sur le code source est toujours accessible ici.