cda-app-mobile-reactnative

cda-app-mobile-reactnative

Application mobile React Native (Expo) avec authentification, navigation privée/publique et gestion complète du cycle de session utilisateur (login, tokens, refresh, profil, déconnexion). Le projet démontre la mise en place d'un socle mobile réutilisable avec une séparation claire des écrans métier et des flux d'authentification.

🎯 Contexte et objectifs

  • Développer une application mobile cross-platform orientée API avec parcours d’authentification sécurisé.
  • Mettre en place un flux UX complet: connexion/inscription, navigation tabulée privée, consultation/recherche d’utilisateurs.
  • Structurer la couche d’intégration réseau et la persistance locale des credentials pour assurer la continuité de session.

🛠️ Réalisations

🧩 Conception

{
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "eject": "expo eject"
  },
  "dependencies": {
    "@react-native-async-storage/async-storage": "^1.15.9",
    "@react-navigation/bottom-tabs": "^6.0.7",
    "@react-navigation/native": "^6.0.4",
    "@react-navigation/stack": "^6.0.9",
    "axios": "^0.22.0",
    "expo": "~42.0.1",
    "formik": "^2.2.9",
    "react-native-elements": "^3.4.2",
    "yup": "^0.32.9"
  }
}
  • L’architecture d’état repose sur un useReducer d’authentification avec transitions explicites (LOGIN, LOGOUT, REFRESH_TOKEN, RETRIEVE_TOKEN). Source: cda-app-mobile-reactnative/App.js
const initialLoginState = {
  isLoading: false,
  userName: null,
  userToken: null,
  refreshToken: null,
  xsrfToken: null,
  email: null,
};

const loginReducer = (prevState, action) => {
  switch (action.type) {
    case "RETRIEVE_TOKEN":
      return { ...prevState, userName: action.username, userToken: action.token, isLoading: false };
    case "REFRESH_TOKEN":
      return { ...prevState, xsrfToken: action.xsrfToken, userToken: action.token, refreshToken: action.refreshToken, isLoading: false };
    case "LOGIN":
      return { ...prevState, xsrfToken: action.xsrfToken, userToken: action.token, refreshToken: action.refreshToken, email: action.email, isLoading: false };
    case "LOGOUT":
      return { ...prevState, userName: null, xsrfToken: null, userToken: null, refreshToken: null, email: null, isLoading: false };
  }
};

💻 Développement

  • Backend : Le client mobile encapsule les appels API d’authentification (/auth/login, /auth/token, /auth/me) et injecte dynamiquement Authorization + x-xsrf-token. Source: cda-app-mobile-reactnative/utils/api.js
const instance = axios.create({
  baseURL: API_BASE_URL,
});

export const addXsrfToken = async (token) => {
  instance.defaults.headers.common["Authorization"] = "Bearer " + token;
  const xsrfToken = await AsyncStorage.getItem("xsrfToken");
  instance.defaults.headers.common["x-xsrf-token"] = xsrfToken;
};

Le flux de login persiste accessToken, refreshToken, xsrfToken et email dans AsyncStorage avant mise à jour du contexte global. Source: cda-app-mobile-reactnative/App.js

const response = await client.post("auth/login", {
  ...foundUser.userInfo,
});
if (response.status === 200) {
  await AsyncStorage.setItem("email", email);
  await AsyncStorage.setItem("userToken", response.data.accessToken);
  await AsyncStorage.setItem("xsrfToken", response.data.xsrfToken);
  await AsyncStorage.setItem("refreshToken", response.data.refreshToken);
}

dispatch({
  type: "LOGIN",
  xsrfToken: xsrfToken,
  token: userToken,
  refreshToken: refreshToken,
  email: email,
});

La stratégie de refresh déclenche un renouvellement via /auth/token en cas d’expiration (401) puis remet à jour les tokens persistés. Source: cda-app-mobile-reactnative/App.js

if (err.response.data.statusCode === 401) {
  refreshToken = await AsyncStorage.getItem("refreshToken");
  email = await AsyncStorage.getItem("email");
  const refresh = await client.post("auth/token", {
    email: email,
    refresh_token: refreshToken,
  });

  await AsyncStorage.setItem("refreshToken", refresh.data.refreshToken);
  await AsyncStorage.setItem("userToken", refresh.data.accessToken);
  await AsyncStorage.setItem("xsrfToken", refresh.data.xsrfToken);

  dispatch({
    type: "REFRESH_TOKEN",
    xsrfToken: refresh.data.xsrfToken,
    userToken: refresh.data.accessToken,
    refreshToken: refresh.data.refreshToken,
  });
}
  • Frontend : La navigation imbrique Stack + Bottom Tabs et bascule automatiquement entre routes publiques et privées selon la présence du token. Source: cda-app-mobile-reactnative/App.js
<NavigationContainer>
  <Stack.Navigator>
    {loginState.userToken !== null ? (
      <Stack.Screen
        options={{ headerShown: false }}
        name="HomeTabs"
        component={HomeTabs}
      />
    ) : (
      <>
        <Stack.Screen options={{ headerShown: false }} name="Login" component={Login} />
        <Stack.Screen options={{ headerShown: true }} name="Register" component={Register} />
      </>
    )}
  </Stack.Navigator>
</NavigationContainer>

L’écran d’inscription utilise Formik + Yup pour valider les champs (email, longueur mot de passe, confirmation), puis envoie un payload normalisé. Source: cda-app-mobile-reactnative/Screens/Register.js

const validationSchema = Yup.object({
  firstname: Yup.string().trim().min(3, "Invalid name!").required("Name is required!"),
  email: Yup.string().email("Invalid email!").required("Email is required!"),
  password: Yup.string().trim().min(6, "Password is too short!").required("Password is required!"),
  confirmPassword: Yup.string().equals([Yup.ref("password"), null], "Password does not match!"),
});

const signUpForm = async (values, formikActions) => {
  const clone = (({ confirmPassword, ...o }) => o)(values);
  signUp({ clone });
  formikActions.resetForm();
};

La feature Search implémente une recherche par email via API et un rendu conditionnel des informations utilisateur. Source: cda-app-mobile-reactnative/Screens/Search.js

const [searchEmail, setSearchEmail] = useState("");
const [userFind, setUserFound] = useState({});

const search = async (email) => {
  const userEmail = await client.get("users/email/" + email);
  setUserFound(userEmail.data);
};

<Text>{userFind.firstname ? "Prénom " + userFind.firstname : ""}</Text>
<Text>{userFind.lastname ? "Nom " + userFind.lastname : ""}</Text>
<Text>{userFind.username ? "Pseudo " + userFind.username : ""}</Text>

🏗️ DevOps & Qualité

export const isValidObjField = (obj) => {
  return Object.values(obj).every((value) => value.trim());
};

export const updateError = (error, stateUpdater) => {
  stateUpdater(error);
  setTimeout(() => {
    stateUpdater("");
  }, 2500);
};

export const isValidEmail = (value) => {
  const regx = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/;
  return regx.test(value);
};

📈 Résultats

  • Sur la base de l’historique Git du dépôt analysé, le travail couvre la période 2021-10-05 -> 2026-02-14, avec 18 commits réalisés par 2 contributeurs. Le projet délivre une application mobile avec session persistée, rafraîchissement de tokens, navigation protégée et écrans connectés à l’API utilisateurs. La structure contexte + reducer + stockage local permet de maintenir une expérience fluide même avec expiration de session. Le bénéfice technique est une base mobile robuste, directement exploitable pour des usages authentifiés en environnement réel.

🔧 Environnement technique

  • Mobile: React Native, Expo, React Navigation (Stack + Bottom Tabs), React Native Elements.
  • Données/API: Axios, AsyncStorage, headers Authorization/x-xsrf-token.
  • Formulaires: Formik, Yup, validation utilitaire custom.
  • Architecture: AuthContext, reducer de session, écrans modulaires (Login, Register, Home, Search, Profile).
  • Outillage: scripts Expo (start, android, ios, web, eject).
🌐 Voir le projet

Technologies utilisées

Outils / Environnement
Expo
Frontend
JavaScript
React
React Native