cda-app-mobile-reactnative

cda-app-mobile-reactnative

React Native (Expo) mobile application with authentication, public/private navigation, and a full mobile session lifecycle (login, token storage, refresh flow, profile retrieval, sign-out). The project demonstrates a reusable mobile foundation with a clear separation between business screens and authentication flows.

🎯 Context and goals

  • Build a cross-platform API-driven mobile application with a secure authentication journey.
  • Deliver a complete UX flow: login/register, private tab navigation, user listing/search.
  • Structure network integration and local credential persistence to guarantee session continuity.

🛠️ Deliverables

🧩 Design

{
  "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"
  }
}
  • State architecture is based on an auth useReducer with explicit transitions (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 };
  }
};

💻 Development

  • Backend : The mobile client encapsulates auth API calls (/auth/login, /auth/token, /auth/me) and dynamically injects Authorization + x-xsrf-token headers. 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;
};

The login flow persists accessToken, refreshToken, xsrfToken, and email in AsyncStorage before updating global auth context. 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,
});

The refresh strategy triggers token renewal through /auth/token on expiration (401) and rewrites persisted credentials. 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 : Navigation nests Stack + Bottom Tabs and switches automatically between public and private routes based on token presence. 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>

The register screen uses Formik + Yup to validate fields (email, password length, password confirmation), then submits a normalized payload. 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();
};

The Search feature implements email-based lookup through API and conditionally renders returned user fields. 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 & Quality

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

📈 Results

  • Based on the analyzed Git history, the work spans 2021-10-05 -> 2026-02-14, with 18 commits across 2 contributors. The project delivers a mobile app with persisted session handling, token refresh logic, protected navigation, and API-driven user screens. The context + reducer + local storage structure supports stable user experience even with expired sessions. The technical benefit is a robust mobile baseline directly usable for authenticated real-world scenarios.

🔧 Technical environment

  • Mobile: React Native, Expo, React Navigation (Stack + Bottom Tabs), React Native Elements.
  • API/data: Axios, AsyncStorage, Authorization/x-xsrf-token headers.
  • Forms: Formik, Yup, custom validation utilities.
  • Architecture: AuthContext, session reducer, modular screens (Login, Register, Home, Search, Profile).
  • Tooling: Expo scripts (start, android, ios, web, eject).
🌐 View the project

Tech Stack

Outils / Environnement
Expo
Frontend
JavaScript
React
React Native