Mobile App Documentation
| Verification | |
|---|---|
| Tags | |
| Last edited | |
| Last edited time | |
| Owner | |
| Person |
SPSI Mobile app documentation
Table of contents
1. Business requirements
- This app will be used by security officers who get deployed to client locations for a certain duration(not more than 8hrs).
- Officers look after the client location, guard them and attend to any unexpected situations.
- Admin creates shifts for officers from .Officers recieve notification regarding their assigned shift details.
- Officers can only start the shift from within the shift location. This is called . Once they start the shift they have to fill 3 types of reports which will be submitted to both admin and client.
- Courtesy report: Checklist created by admin for the officer to check in client location. Only one courtesy report per shift.
- Incident report: To report any unexpected situation. Officers can attach images, videos, doc files and audio files. Officer can create any number of incident report per shift.
- Daily activity report: Officer has to write entries every hour on what is happening in client location. Even here for each entry officer can attach images, videos, doc files and audio files. Only single DAR report per shift.
- Once the report is published it will be sent to client's email. Each report will be accessible to admin on the web app.
- Officer can take tea breaks and lunch breaks. When he/she does so, they have to use tea break or lunch break timers. These timings will be added into offcer's timesheet for that particular shift. Timesheet can be viewed by officer by going to timesheets tab. This timesheet will be used by admin to gauge how officer spends time during a shift.
- Officers can see upcoming shifts also in schedules tab.
- Officers apply for leave and monitor the approval status of it.
- Officers can view their profile details from profile screen but they cannot update them.
2. Environment setup and running the app
1. Setting up the environment
Follow this link to setup the environment.
Make sure to choose Development OS and Target OS to setup for android and iOS separately.

2. Running the application
Run start script to start the app in an emulator.
You can also choose to run the app on an actual device, Reference Link.
3. Code structure
1. Src folder structure

- index.js is the root component
- action folder contains all the redux actions
- assets folder holds assets like fonts and icons
- component folder holds all the components created
- container folder holds the screens of the app
- reducer folder holds all the redux reducers
- utils folder holds utility functions
- RootNavigation.js is the React navigation file which directs both stack and tab navigation
- NavigationService.js is a React navigation file to call navigation methods from out of React components
- store.js holds createstore method for redux
2. Redux setup
Lets see how redux is setup
1. Actions

Four action files are created.
- Auth.js deals with actions related to authentication(login, register, forgotpasswor, getting user info, update user info and logout)
- Common.js holds common actions like showing alerts, loaders which are common for the whole app.
- index.js holds actions for fetching common resources like shifts, leaves, timesheets etc.
- Reports.js holds all the actions related to reports
2. Reducers

Four reducer files are present
- Auth.js deals with authentication
- Common.js deals with common reducers general for the app
- index.js combines all the reducers to form a root reducer
- Reports.js deals with report reducers
- Shift.js deals with shift reducers
3. Action types
Action types are present in ActionType.js file. Path is /src/constant/ActionType.js
4. Store composer
All the reducers and middlewares are combined to form a configure store function.
Path: /src/store.js
3. Containers content

1. Attendance
attendance folder is not used, it is kept as a future implementation.
2. Auth
auth folder contains all the authentication screens.
| file | in use | description |
| About.js | yes | About app permissions and release details |
| Editprofile.js | no | Update user info |
| Forgotpassword.js | yes | Resetting the password |
| Profile.js | yes | View user info |
| Registration.js | yes | Registering the user |
| SignIn.js | yes | Loggin in the user |
| SignUp.js | no | User registering for being officer at spsi(Screen not used) |
| UpdatePassword.js | no | updating the password but not in use. About.js allows officer to update their password instead |
3. Dashboard
dashboard folder holds home screen, shift action and geofence not valid map screen.
| file | in use | description |
| AvailableShift.js | no | Shift actions page with access to reports and end shift. |
| availableShift2.js | no | backup file of older version of AvailableShift.js |
| index.js | yes | home screen displays upcoming shifts, active shifts, timesheet link and leave link |
| index2.js | no | older version of home screen |
| NotExactLocationWarning.js | yes | If geofence fails, map screen opens with client and user location with gps accuracy details. |
| ShiftActions.js | yes | Lunch and tea break timers, access links to individual reports and end shift option |
| ShiftDetails.js | no | Shift details screen with shift description and map with client location pointing. On click of map, location opens up in native navigation apps. |
4. Leave
This folder handles everything about leaves.
| file | in use | description |
| AddNewLeave.js | yes | Allows officer to apply for a leave |
| EditLeave.js | no | Allows officer to update leave information. |
| index.js | yes | Lists all the leaves with their respective status |
| index2.js | no | backup |
| ViewLeave.js | yes | Once a leave is created, it cannot be updated. Officer can check leave contents here. |
5. Notification
Screen that lists all the recent notifications.
6. Report
Screen that allows officer to perform all the actions related to reports.
Others folder contains backup files which are not in use
| file | in use | description |
| CourtesyReport.js | yes | Lists both published and unpublished courtesy reports for the past 30 days. Officer can create new one from here. |
| DailyReport.js | yes | Lists both published and unpublished dar reports for the past 30 days. Officer can create new one from here. |
| EditCourtesyReport.js | yes | Officer can edit the report and publish. |
| EditDailyReport.js | yes | Officer can edit the dar description and publish it. Can also create and edit entries from here |
| EditDailyReportEntry.js | yes | Officer can edit dar entries. They can attach medias from here. |
| EditIncidentReport.js | yes | Officer can edit incident report and attach medias to it. |
| IncidentReport.js | yes | Lists both published and unpublished incident reports for the past 30 days. Officer can create new one from here. |
| index.js | yes | Provides buttons to enter individual reports. |
| ViewCourtesyReport.js | yes | Published reports cannot be edited. They can viewed from here. |
| ViewDailyReport.js | yes | Published reports cannot be edited. They can viewed from here. |
| ViewIncidentReport.js | yes | Published reports cannot be edited. They can viewed from here. |
7. Schedule
Deals with shifts and its details
| file | in use | description |
| index.js | yes | lists all the shift from current day to future 30 days |
| index2.js | no | backup |
| ShiftDetails.js | yes | Shift details screen with shift description and map with client location pointing. On click of map, location opens up in native navigation apps. |
8. Timesheet
Holds screens related to timesheets
| file | in use | description |
| AddNewTimesheet.js | no | Allows officer to add new timesheet. Timesheets are created by backend so this screen is redundand. |
| EditTimesheet.js | no | Officer can edit timesheet. As per client requirements officer cannot update timesheet. Edit features are disabled. |
| EditTimesheet2.js | no | backup |
| Timesheet.js | yes | Lists all the timesheets |
| timesheet2.js | no | backup |
| ViewTimesheet.js | yes | Once shifts ends, timesheet's status changes to published. This screen allows officer to just the content. |

4. Implementations
1. Geolocation fetching
App requires gps cordinates to verify whether the officer is present within the client location geofence or not.
Package used: react-native-geolocation-services
Package configuration: Setup of this package
Sample code:
import Geolocation from 'react-native-geolocation-service';
Geolocation.getCurrentPosition(
position => {
let region = {
lat: position.coords.latitude,
lng: position.coords.longitude,
};
log.info('Location:', position.coords);
},
error => {
log.error('[container/dashboard/index] in get location:', error);
},
{enableHighAccuracy: true, timeout: 5000, maximumAge: 3000},
);Note:
- enableHighAccuracy: gives better gps accuracy
- timeout: closes gps request after 5 seconds
- maximumAge: New gps values will be fetches after every 3 seconds
2. Permissions
App requires permission for accessing gps, camera, microphone, file system and media files. In order to use those services user must provide permission for the app to use them. To get permission from the user we are using a package.
Package used: react-native-permissions
Package configuration: Configuration link
Sample code:
import { requestMultiple, PERMISSIONS } from "react-native-permissions";
//iOS multiple permission request
async function requestAndroidPermission() {
try {
let statuses = requestMultiple([
PERMISSIONS.ANDROID.CAMERA,
PERMISSIONS.ANDROID.RECORD_AUDIO,
PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE,
PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE,
PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION,
]);
if (
statuses[PERMISSIONS.ANDROID.CAMERA] == "granted" &&
statuses[PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE] == "granted" &&
statuses[PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE] == "granted" &&
statuses[PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION] == "granted" &&
statuses[PERMISSIONS.ANDROID.RECORD_AUDIO] == "granted"
) {
log.info("info", "SUCCESS:", "all android permissions granted");
}
} catch (error) {
log.error("[utils/permission] in request android permission:", error);
}
}
//android multiple permission requestNote: Permission names are different for both android and iOS. Check package documentation for exact names. Before using permissions check the type of OS. You cant ask iOS permissions in android. Use Platform module from react native to check the OS. Instead of multiple permissions you can ask for single permission also.
3. Notifications
App uses push notifications. When a new shift is created for that officer by admin, it should be notified in the app. Notifications work if the app is in background, foreground or even closed. We are using firebase for handling notifications.
Package used: react-native-push-notification for android, react-native-community/push-notification-ios for iOS and @react-native-firebase.
Package configuration: react-native-push-notification , react-native-community/push-notification-ios and @react-native-firebase
References: push notifications and firebase remote notifications
How it works: While logging in the user, fcm token is generated and its sent to backend to send notifications. While logging out, we remove the token from backend.
Files involved:
- /src/utils/LocalNotificationServices.js for notification services
- /src/utils/FCMServices.js for working with firebase
- /src/action/Auth.js while logging in fcm registration is made
- Sample code
// notification registration part present in login action const onRegister = async (token) => { await AsyncStorage.setItem( "notificationToken", JSON.stringify(token) ); let tokens = await dispatch(getNotificationToken()); let tokenPresent = tokens?.filter( (token) => token.token === token ); if (!tokenPresent.length) await dispatch(addNotificationToken(token)); }; const onNotification = async (notify) => { const options = {}; await localNotificationService.showNotification( 0, notify.title && notify.title, notify.body && notify.body, notify, options ); // await dispatch(getNotifications()); Alert.alert( notify.title, notify.body, [ { text: "Ok", onPress: () => console.log("ok"), style: "cancel", }, ], { cancelable: false } ); // showAlert("notifications received"); }; const onOpenNotification = async (notify) => { log.info("[App] onOpenNotification:", notify); }; fcmService.registerAppWithFCM(); fcmService.register( onRegister, onNotification, onOpenNotification ); localNotificationService.configure(onOpenNotification);
- Sample code
- index.js listens for remote notifications while app is in background.
- Sample code
// sample code present in root index.js file messaging().setBackgroundMessageHandler(async (remoteMessage) => { log.info("remoteMessage: message handled in the background", remoteMessage); }); function HeadlessCheck({ isHeadless }) { if (isHeadless) { //app has been launched by ios return null; } return <App />; } AppRegistry.registerComponent(appName, () => HeadlessCheck);
- Sample code
4. Media handling
Officers upload images, videos, audios and documents to attach to reports.
Package used: react-native-image-crop-picker for images and videos, react-native-document-picker for audio and document files
Package configuration: react-native-image-crop-picker , react-native-document-picker
How it works:
- Based on the type of the file clicked from report media upload section, respective packages handle file picking.
- Multipe files can be picked, Once the files are picked they are upload to AWS S3 bucket. We will see how that is done in later section.
- After uploading files to S3 bucket the response id is passed to media upload api to create a document. Which then gives a document id which will be attached with report.
- ScreenshotsMedia upload container in incident reportOn clicking file icon button, option to choose documet or audio fileChoose video from library or cameraOn click of image icon button, Option to choose image from library or take new one from camera




Files involved:
- /src/utils/func.js exposes to 2 functions namely handleMediaPicker and pickFile for image and video handling and document handling respectively.
Viewing uploaded media:
- /src/component/MediaComp.js component is responsible for viewing the uploaded media files
- Package used is refer docs for setup. It uses for downloading the file to local and then react-native-file-viewer opens the file.
5. Map display
Map is used to display the client location markers in shift details.
Package used: react-native-maps
Reference: using maps in react native
Files involved:
- /src/container/schedule/ShiftDetails.js gives shift details with client location marker
- /src/container/dashboard/NotExactLocationWarning.js shows both client and user location markers and also displays how accurate user gps values are
6. Remote config handling
If we store cofigurations of the app locally, if we have to make any changes we will have to make new release. So we are serving app configurations from one of the API. Config data is encoded and we have decode at mobile end to use it.
- When user logs in, API call is made to fetch config data. It is stored locally in AsyncStorage.
// getting config data in login action let configResp = await postRequest(CONFIG_API, "get"); log.info("configresp:", configResp); if (!configResp) { setTimeout(() => { showAlert("", "Config server not responding, contact admin"); }, 1000); dispatch(hideLoader()); return false; } if (configResp && configResp.status === 200) { let configData = configResp.data; configData = base64.decode(configData); await AsyncStorage.setItem("configs", configData);
- Before all other API calls getConfig() function is called, which gets the values from localstorage.
// getConfig function which is called before every API call export async function getConfig() { let configFromStorage = await AsyncStorage.getItem("configs"); configFromStorage = configFromStorage && JSON.parse(configFromStorage); return configFromStorage ? configFromStorage : false; }
7. Device online status finding
Before making any API call app checks whether user is connected to internet or not.
Package used: @react-native-community/netinfo
Reference: @react-native-community/netinfo
- Sample codeFile: /src/container/auth/SignIn.js on click of login button
import { useNetInfo } from "@react-native-community/netinfo"; const netInfo = useNetInfo(); const { isConnected } = await NetInfo.fetch();isConnected is a boolean value
8. Navigation
Lets see screens and tabs are navigated within the app. There are two types of navigations, 1: Stack navigation 2: Tab navigation.
package used: React navigation
Reference: React navigation
1. Stack navigation
Screens are stacked on one top of another to render a new screen view and removed from the stack to go back to the previous screen.
Reference: Stack navigation
Files involved:
- /src/RootNavigatin.js - This is the root file for all the navigation in the app. All the screens are defined in this file.
- Sample codeCreation of stack navigation screens
// creating stack navigation screens import React from 'react'; import {NavigationContainer} from '@react-navigation/native'; import {createStackNavigator} from '@react-navigation/stack'; import SpalshScreen from './utils/SplashScreen'; const Stack = createStackNavigator(); function RootNavigation() { return ( <NavigationContainer ref={navigationRef}> <Stack.Navigator initialRouteName="SplashScreen"> <Stack.Screen name="SplashScreen" component={SpalshScreen} options={{headerShown: false}} /> </Stack.Navigator> </NavigationContainer> ); } export default RootNavigation;Calling screen change from within the ui// calling screen change from screens // in class components this.props.navigation.navigate('ShiftActions', { // any data you want to pass to the new screen }); // in functional components props.navigation.navigate('ShiftActions', { // any data you want to pass to the new screen });
2. Tab navigation
Tab navigations are the ones that appear at the bottom the app screen. Within a tab we can use stack navigation.
Reference: Tab navigation
Bottom icons are called tab navigators

Files involved:
- /src/utils/BottomTabNavigation.js - This file is responsible for all the tab navigations.
- Sample code
import React from "react"; import { createStackNavigator } from "@react-navigation/stack"; import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; import Dashboard from "../container/dashboard"; const Tab = createBottomTabNavigator(); const Stack = createStackNavigator(); // creating a stack screen for tab navigation function DashboardStack(props) { return ( <Stack.Navigator initialRouteName="Home" screenOptions={{ headerShown: false, }} > <Stack.Screen name="Home" component={Dashboard} options={{ headerShown: false }} /> </Stack.Navigator> ); } export default function BottonTabNavigation() { return ( <Tab.Navigator initialRouteName="Home" tabBarOptions={{ activeTintColor: colors.whiteColor, style: { backgroundColor: colors.lightBlack, borderTopColor: colors.lightBlack, }, tabStyle: { marginTop: 5, }, }} > // using the above defined stack screen within tab container <Tab.Screen name="Home" component={DashboardStack} options={{ tabBarIcon: ({ focused }) => ( <Image source={focused ? home_tab_selected_ic : home_tab_ic} style={styles.tabIcon} /> ), }} /> </Tab.Navigator> ); }
After succesfully logging in the user, navigation is moved to tab navigator. In file /src/container/auth/SignIn.js. StackActions.replace replaces the current screen with new screen instead of stacking on top of it, due to that when the user presses back button it doesnt go to the previous screen.
import { StackActions } from "@react-navigation/native";
props.navigation.dispatch(StackActions.replace("TabNavigator"));9. AWS media upload
We are using AWS s3 buckets to store all the media files. Media files include images, videos, audios and documents.
Media upload flow:
Uploading files to aws s3
After selecting media files, they are uploaded to aws s3. S3 returns unique id for each file.
- Sample code
export const uploadS3 = async files => { let config = await getConfig(); try { AWS.config.update({ secretAccessKey: config.SECRET_ACCESS_KEY, accessKeyId: config.ACCESS_KEY_ID, region: config.REGION, signatureVersion: 'v4', //API version }); const s3 = new AWS.S3({signatureVersion: 'v4'}); let fileDetails = []; await Promise.all( files.map(async file => { const base64 = await fs.readFile(file.uri, 'base64'); let arrayBuffer = Base64Binary.decode(base64); let fileType = file.type; let tempType = file.name.split('.'); tempType = tempType[tempType.length - 1]; const fileName = `${uuid.v4()}.${tempType}`; const params = { Key: fileName, Bucket: config.BUCKET, Body: arrayBuffer, ContentType: fileType, }; await new Promise((resolve, reject) => { s3.upload(params, (err, uploadResp) => { if (err) { log.error( '[action/index/uploadS3] file upload to s3 failed:', err, ); reject(err); } else { log.info( '[action/index/uploadS3] file uploaded to s3 :', uploadResp, ); fileDetails.push({ name: uploadResp.Key, url: uploadResp.Location, format: file.type, size: file.size, }); log.info( 'file name after uploading to s3:', uploadResp.key, uploadResp.Key, uploadResp.Location, ); resolve(uploadResp); } }); }); }), ); log.info('fileDetails in upload func:', fileDetails); if (fileDetails.length) return fileDetails; else return false; } catch (error) { log.error('[action/index/uploadFile] :', error); return false; // reject(false); } };AWS S3 configs are being accessed from localstorage, which are present remotely.
Saving the file ids to database:
After uploading files to s3, all file unique ids are saved in the database linked to particular report.
- Sample code
export const saveFileDetail = files => async dispatch => { let userinfo = await AsyncStorage.getItem('userInfo'); if (userinfo !== null) userinfo = JSON.parse(userinfo); // let accessToken = await checkAuth(); let accessToken = await dispatch(checkAuth()); let config = await getConfig(); try { // dispatch(showLoader()); const headers = { Authorization: `Bearer ${accessToken}`, }; log.info('files in save file Detail action:', files); const resp = accessToken && (await postRequest( `${config.COMMON_URL}/uploadFiles`, 'post', files, headers, null, null, )); // log.info('RESP: in save file detail:', JSON.stringify(resp)); // dispatch(hideLoader()); // console.log("response from delete contact:", JSON.stringify(resp)); if ( resp && (resp.status === 200 || resp.status === 201 || resp.status === 202) ) { return resp.data; } else { if (resp === undefined) { dispatch(userLogout({sessionExpired: false})); log.error('[action/index/saveFileDetail] :', UNDEFINED_SERVER_RESPONSE); return false; } if (resp && resp.status === 500) { log.error( '[action/index/saveFileDetail] :', INTERNAL_SERVER_ERROR_ALERT, ); } const statusText = resp && (resp.status === 401 ? UNAUTHORIZED_ALERT : resp.status === 404 ? NO_RECORD_FOUND_ALERT : resp.status === 403 ? FORBIDDEN_ALERT : resp.status === 400 ? BAD_REQUEST_ALERT : resp.status === 500 ? INTERNAL_SERVER_ERROR_ALERT : undefined); accessToken && setTimeout(() => { showAlert('', statusText || UNDEFINED_RESPONSE_ALERT); }, 1000); return false; } } catch (error) { log.error('[action/index/saveFileDetail] :', error); // logger('error', 'ERROR:', 'in file upload') dispatch(hideLoader()); } };
10. Geofence verification
App uses geofence verification to check whether the officer is present within the client property before starting the shift. Geo fence verification is happening while starting shift, starting breaks and ending shift.
Package used: geolib
Reference: geolib
Files involved:
- /src/containers/dashboard/index.js - startShift function calls verfiyGeoFence function.
- Sample code
import { getCenterOfBounds, getDistance as calcDistance, orderByDistance, findNearest, getAreaOfPolygon, isPointWithinRadius, } from 'geolib'; export async function verifyGeoFence( currentLocation, shiftLocation, polygon, // offsetDistance, // disable ) { let status = false; let showCircle = false; let offsetDistance = 20; let radius = 20; try { let centerPoint = getCenterOfBounds(polygon); //in centerpoint lat and lng values or reverse let clientOfficerDistance = calcDistance(currentLocation, { latitude: shiftLocation.lat, longitude: shiftLocation.lng, }); let farthestPoint = orderByDistance(currentLocation, polygon); farthestPoint = farthestPoint[farthestPoint.length - 1]; // console.log("farthest dist:", farthestPoint); let maxDistance = calcDistance(centerPoint, farthestPoint); let fenceArea = getAreaOfPolygon(polygon); if (fenceArea <= 150) { status = await isPointWithinRadius( currentLocation, {latitude: centerPoint.longitude, longitude: centerPoint.latitude}, maxDistance + offsetDistance, ); showCircle = true; radius = maxDistance + offsetDistance; } else if (fenceArea > 150 && fenceArea <= 400) { status = await isPointWithinRadius( currentLocation, {latitude: centerPoint.longitude, longitude: centerPoint.latitude}, maxDistance, ); showCircle = true; radius = maxDistance; } else if (fenceArea > 400) { status = await pointInpolygon(currentLocation, polygon); showCircle = false; } log.info( '[Func] Area of circle:', 3.14 * (maxDistance + offsetDistance) ** 2, ); log.info('geofence status===', status); return { centerPoint, status, maxDistance, clientOfficerDistance, showCircle, radius, fenceArea, }; } catch (error) { log.error('[verifyGeoFence func] :', error); return false; } }User location, polygon of the client location and client location cordinates are passed to this function. First area of the client property is calculated with function getAreaOfPolygon(). Max distance from client properties center to farthest poligon point is calculated. Then based on the area of the polygon offset distance added to the maxDistance.
locationData = await verifyGeoFence(
{latitude: currentLocation.lat, longitude: currentLocation.lng},
shiftLocation,
fenceRadius,
);
if (
locationData.status ||
!isGeoFencingOn ||
(shiftActionAttempt &&
shiftActionAttempt === this.state.geoFenceAttemptConstant)
) {Based on the above if condition shift is started if not navigated to NotExactLocationWarning.js screen.
this.props.navigation.navigate('NotExactLocationWarning', {
nextShiftTime: this.state.nextShiftHours,
selectedShift: selectedShift,
currentLocation: this.state.currentLocation,
centerPoint: locationData.centerPoint,
// maxDistance: locationData.maxDistance,
radius: locationData.radius,
showCircle: locationData.showCircle,
getshifts: this.getShiftDetails,
});11. API call setup
REST API calls are made using axios npm package. Lets see how its setup and being used in actions.
Package used: Axios
Reference: Axios
Lets take an example of getCourtesyReports funtion from /src/actions/reports.js
- Getting config data
let userinfo = await AsyncStorage.getItem('userInfo'); if (userinfo !== null) userinfo = JSON.parse(userinfo); let accessToken = await dispatch(checkAuth()); let config = await getConfig();
- Setting up the query url
//headers and query params are constructed const headers = { Authorization: `Bearer ${accessToken}`, }; let params = { ...request.params, pageSize: config.PAGE_SIZE, sort: 'dateTime,d', }; const resp = accessToken && (await postRequest( `${config.BASE_URL}/officers/${ userinfo && userinfo.officerId }/courtesyreports`, 'get', null, headers, params, ));
- postRequest function construction
import Axios from 'axios'; export default async function postRequest( url, method, request, headers, params, noTimeout, ) { let {isConnected} = await NetInfo.fetch(); if (isConnected) { try { const result = await Axios({ method: method, url, data: request, headers: headers, timeout: noTimeout ? 0 : apiCallTimeout, params: params ? params : null, validateStatus: function (status) { return true; }, }); if (result === undefined) { } return result; } catch (error) { if (error.message === 'Network Error') { log.error('[postRequest func] Network error:', error); setTimeout(() => { showAlert('', 'Network Error'); }, 1000); return false; } if (error.code === 'ECONNABORTED') { log.error('[postRequest func] ECONNABORTED resp from axios:', error); setTimeout(() => { showAlert( 'Timeout', 'This may be due to slow internet. Please try again', ); }, 1000); } return false; } } else { setTimeout(() => { showAlert('', OFFLINE_ALERT); }, 1000); return false; } }From postRequest function network and connectivity errors are being handled. showAlert is a common function for showing alert messages.
- Handling API call response
if ( resp && (resp.status === 200 || resp.status === 201 || resp.status === 202) ) { dispatch({ type: type.SET_COURTESY_REPORT_LIST, payload: resp.data.items, }); return resp.data.items; } else { if (resp === undefined) { dispatch(userLogout({sessionExpired: false})); log.error( '[actions/reports/getCourtesyReports]', UNDEFINED_SERVER_RESPONSE, ); return false; } const statusText = resp && (resp.status === 401 ? UNAUTHORIZED_ALERT : resp.status === 404 ? NO_RECORD_FOUND_ALERT : resp.status === 403 ? FORBIDDEN_ALERT : resp.status === 400 ? BAD_REQUEST_ALERT : resp.status === 500 ? INTERNAL_SERVER_ERROR_ALERT : undefined); accessToken && setTimeout(() => { showAlert('', statusText || UNDEFINED_RESPONSE_ALERT); }, 1000); return false; } } catch (error) { log.error('[actions/reports/getCourtesyReports] :', error); dispatch(hideLoader()); dispatch({ type: type.SET_COURTESY_REPORT_LIST, payload: undefined, }); }Based on the status code appropriate alert messages are shown.Note: if the response is undefined, it can mean that API services are down. App shows contact admin alert for this.
12. Form validation
Form validations are very important. Without validating user inputs, data should never be send to backend.
Package used: React hook form
Reference: React hook form
Lets take file /src/container/leave/AddNewLeave.js for example
- Setting the validations
import { useForm, Controller } from "react-hook-form"; import { yupResolver } from "@hookform/resolvers/yup"; import * as yup from "yup"; const schema = yup.object().shape({ reason: yup.string().required().max(255, "Cannot exceed 255 characters"), description: yup .string() .required() .max(maxLength, `Cannot exceed ${maxLength} characters`), }); const { control, handleSubmit, errors, formState, getValues } = useForm({ resolver: yupResolver(schema), });Schema is the definition of all the validations of all the fields of our form.
- Using React hook form with React NativeController is from react-hook-form, it wraps the react native input component.
<Controller control={control} name="reason" defaultValue={state.reason} render={({ onChange, onBlur, value }) => ( <FloatingTextInput value={value} onBlur={onBlur} onChangeText={(value) => onChange(value)} maxLength={maxLength} helperText={errors.reason?.message} helperTextType="error" helperTextVisible={errors.reason} error={errors.reason} /> )} /><Button mode="contained" color={colors.yellowColor} onPress={handleSubmit(onAddLeave)} disabled={state.buttonDisable} > request leave </Button>
13. App update prompt
Whenever a new app release is available, app checks it and prompts the user to update it.
Package used: React native version check
Reference: React native version check
/src/index.js file is the root file. Here we check for new updates.
- Update processCurrent app version and latest app version is found.If current version not equal to latest version then, we get the app url from playstore or appstore conditionally based on the type of platform.Using React native’s Linking we open the appstore or playstore url, which opens the respective app for update.
import VersionCheck from 'react-native-version-check'; const versionCheckUpdate = async () => { const currentVersion = VersionCheck.getCurrentVersion(); let packageName = VersionCheck.getPackageName(); let appID = packageName; log.info('package name:', packageName); let updatedNeeded = await VersionCheck.needUpdate(); log.info('is update needed:', updatedNeeded); VersionCheck.getLatestVersion() // Automatically choose profer provider using `Platform.select` by device platform. .then(async latestVersion => { log.info('Latest version:', latestVersion); log.info('Current version:', currentVersion); if ( currentVersion && currentVersion !== latestVersion && latestVersion ) { log.info('update is needed'); let playstoreUrl = null; let appstoreUrl = null; if (Platform.OS == 'android') { await VersionCheck.getPlayStoreUrl({packageName}).then(url => { playstoreUrl = url; }); } if (Platform.OS == 'ios') { await VersionCheck.getAppStoreUrl({appID: packageName}).then( url => { appstoreUrl = url; }, ); } log.info('playstore url:', playstoreUrl); // log.info("app store url:", appstoreUrl); if (playstoreUrl || appstoreUrl) { Alert.alert( 'Update available', 'Please update the app to continue using it', [ { text: 'Update', onPress: () => { console.log('about to update'); // appstoreUrl ='itms-apps://apps.apple.com/IN/app/id=com.app.securityofficerapp' appstoreUrl = updatedNeeded.storeUrl; BackHandler.exitApp(); log.info('app store url :', appstoreUrl); if (Platform.OS === 'android' && playstoreUrl) { Linking.openURL(playstoreUrl); } if (Platform.OS === 'ios' && appstoreUrl) { Linking.openURL(appstoreUrl); } }, }, ], {cancelable: false}, ); } } }); };
14. Navigation service
We can use the React navigation’s navigation services only within react components. For being able to navigate from other than React components we create a navigation service.
- Creating navigation service
import * as React from "react"; export const navigationRef = React.createRef(); export function navigate(name, params) { navigationRef.current?.navigate(name, params); } export function replace(name, params) { navigationRef.current?.replace(name, params); }import {navigationRef} from './NavigationService'; <NavigationContainer ref={navigationRef}></NavigationContainer>
- Usage
navigationService.navigate("SignIn");
15. Splash screen
Splash screen is what we see when app is loaded closed state. We show SPSI logo on splash screen
Note: We setup splashscreens differently for both android and iOS refer this blog
Package used: React native splash screen
Setup Reference: React native splash screen
File /src/utils/SplashScreen.js is a stack screen which shows up and closes when initial app loading is done.
- We register it as any other stack screen in /src/RootNavigation.js
import SpalshScreen from './utils/SplashScreen'; <NavigationContainer ref={navigationRef}> <Stack.Navigator initialRouteName="SplashScreen"> <Stack.Screen name="SplashScreen" component={SpalshScreen} options={{headerShown: false}} /> </Stack.Navigator> </NavigationContainer>
- We can perform all the actions when components mounts and when all the loading operations are done, we can use StackAction.replace() to navigate to login screen
async componentDidMount() { try { let userInfo = await AsyncStorage.getItem("userInfo"); let isShiftEnd = await AsyncStorage.getItem("isShiftEnd"); let shiftStatus = await AsyncStorage.getItem("shiftStatus"); let breakStarted = await AsyncStorage.getItem("breakStarted"); if (shiftStatus) { shiftStatus = JSON.parse(shiftStatus); this.props.setShiftActiveStatus(shiftStatus); } if (breakStarted) { breakStarted = JSON.parse(breakStarted); this.props.setBreakStarted(breakStarted); } if (isShiftEnd) { isShiftEnd = JSON.parse(isShiftEnd); this.props.setShiftEndStatus(isShiftEnd); } let route = "SignIn"; if (userInfo !== null) { userInfo = JSON.parse(userInfo); this.props.setUserInfo(userInfo); route = "TabNavigator"; } return this._navigate(route); } catch (error) { log.error("[splashscreen] :", error); return this._navigate("SignIn"); } } _navigate = (route) => { SplashScreen.hide(); return this.props.navigation.dispatch(StackActions.replace(route)); };
16. App icon
App icon is the brand icon that appears on mobile screens which on click opens the actual app.
Note: Setup is different for both android and iOS
along with ic_launcher.png add ic_launcher_round.png aswell.

17. Allowing access to remote servers(Transport security setup)
To communicate with a remote host we have to specify that in app cofigurations.
- Android
<?xml version="1.0" encoding="utf-8"?> <network-security-config> <!-- deny cleartext traffic for React Native packager ips in release --> <domain-config cleartextTrafficPermitted="true"> <domain includeSubdomains="true">localhost</domain> <domain includeSubdomains="true">10.0.2.2</domain> <domain includeSubdomains="true">10.0.3.2</domain> <domain includeSubdomains="true">139.59.82.79</domain> <domain includeSubdomains="true">192.168.0.23</domain> <domain includeSubdomains="true">148.72.214.211</domain> <domain includeSubdomains="true">54.244.2.143</domain> <domain includeSubdomains="true">34.217.9.177</domain> <domain includeSubdomains="true">3.136.86.151</domain> <domain includeSubdomains="true">ec2-34-217-9-177.us-west-2.compute.amazonaws.com</domain> <domain includeSubdomains="true">ec2-3-136-86-151.us-east-2.compute.amazonaws.com</domain> <domain includeSubdomains="true">spsiapi.swayaan.com</domain> <trustkit-config enforcePinning="true"> </trustkit-config> </domain-config> </network-security-config>Mention this file in AndroidManifest.xml file<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false" android:theme="@style/AppTheme" android:networkSecurityConfig="@xml/network_security_config"> </application>
- iOS
<key>NSAppTransportSecurity</key> <dict> <key>NSExceptionDomains</key> <dict> <key>ec2-3-136-86-151.us-east-2.compute.amazonaws.com</key> <dict> <key>NSExceptionAllowsInsecureHTTPLoads</key> <true/> </dict> <key>localhost</key> <dict> <key>NSExceptionAllowsInsecureHTTPLoads</key> <true/> </dict> </dict> </dict>It can be done from xcode also
18. Setting up push notification icon
This icon appears in the notification.
- Generate notification icons in black and white for different resolutions same as app icon and place it in android/app/main/res/mipmap-* by the name
ic_notification
- In /src/utils/LocationNotificationService.js add this ic_notification name in configuration.
buildAndroidNotification = (id, title, message, data = {}, options = {}) => {
return {
id: id,
autoCancel: true,
largeIcon: options.largeIcon || "ic_launcher",
smallIcon: options.smallIcon || "ic_notification",
vibrate: options.vibrate || true,
vibration: options.vibration || 300,
priority: options.priority || "high",
importance: options.importance || "high",
color: options.color || "white",
};
};19 NotExactLocationWarning screen
This screen is rendered when the officer is not in the client location. It shows the positions of both officer and client locations. Buttons are provided to switch focus on client location and officer location. It also displays the gps accuracy of the officer.
As the officer starts moving a blue line is drawn to indicate officer’s direction of movement. This helps officer to know whether he is moving towards or away from client location.
5. Building and publishing the app
Note: You can find all the app release notes here
1. Android building and publishing
- Change the version code and version name in android/app/build.gradle file
defaultConfig { applicationId "com.securityofficerapp" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 20 versionName "2.8" }
- Go to android studio and open just the android folder of spsiofficerapp project. Let is start indexing. Once done go to build > signed bundle or APK >
- Select from project android folder. Current keystore password isFill key-alias as Click next.
my-release-keystore.keystore
murali.
my-key-alias.
- Select relase and click finish to build the app.
- After building you will find the app in folder.
android/app/release/
- Navigate to Select tab and click on create new release button. Upload the apk file and let the rest of the autofilled fields be same. Click on publish button.
.
Production
- Currently manual publish is setup, so reviewed apps will not avaiable on playstore automatically. We have publish from the button
Releases overview
2. iOS building and publishing
general app information

certificate signing tab

Selecting emulator device

- Open the project in xcode
- From general tab after selecting project name, you can see the app display name and version and build numbers. You have to updated version number from previous version number.
- In signing and capabilites tab you have to select signing certificate. If not present you can
.
- Select from select devices tab present at the top.
Any iOS device
- Click from top bar. Select
Product
Clean Build Folder.
- Once done, From select
Product
Archive.
- Once built Select the build from Archives list which you want to push to appstore and click .
Distribute App
- Select itll build and from check all the options and click next.
App Store Connect,
App Store Connect distribution options
- Click on
Automatically manage signing.
- Click upload button to upload it to appstore connect.
- Login in to
.
- Click on plus button to the right of Add the new version number of the app. Fill in whats new in this version in input under heading.
iOS app.
Version information
- Select the build from section. If the new build is not diplayed here, wait and try again after some time. Pushed app may take some time to reflect on appstore connect sometimes.
Build
- After filling up create a new release from the button at the top right corner.
- Published app will take between 2 to 4 days for review. Once reviewed succesfully we have to manually publish it from appstore connect console.
6. Walkaround of Playstore and Appstore console
Android
1. Releases overview: This tab will show latest app available on the playstore 1. Production: This tab will allow you to publish new apps 1. Publishing overview: We can setup manual or automated publishing of the app
iOS
1. iOS app menu from left top, will display all the general informations of the app from app store description, app store icon, build version etc. 1. In the same page, under Version Release heading we can set the app release type whether manual or automatic. Currently its set to manual in both appstore and playstore.