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
- The app foundation combines Expo/React Native, React Navigation, AsyncStorage, Axios, and form validation (Formik/Yup). Source: cda-app-mobile-reactnative/package.json
{
"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
useReducerwith 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 injectsAuthorization+x-xsrf-tokenheaders. 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
- The project includes reusable local validation helpers for forms (email format, mandatory fields, temporary UI error handling). Source: cda-app-mobile-reactnative/utils/methods.js
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-tokenheaders. - 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).