app update architecture| Aman Singh (#685)

This commit is contained in:
Aman Singh
2024-01-04 22:45:34 +05:30
committed by GitHub
14 changed files with 130 additions and 14 deletions

View File

@@ -45,6 +45,8 @@ import analytics from '@react-native-firebase/analytics';
import fetchUpdatedRemoteConfig from './src/services/firebaseFetchAndUpdate.service';
import { addClickstreamEvent } from './src/services/clickstreamEventService';
import ScreenshotBlocker from './src/components/utlis/ScreenshotBlocker';
import { getBuildFlavour } from '@components/utlis/DeviceUtils';
import { setGlobalBuildFlavour } from '@constants/Global';
initSentry();
@@ -127,6 +129,9 @@ function App() {
useEffect(() => {
ScreenshotBlocker.unblockScreenshots();
getBuildFlavour().then((flavour) => {
setGlobalBuildFlavour(flavour);
});
}, []);
React.useEffect(() => {

View File

@@ -37,4 +37,4 @@
}
],
"configuration_version": "1"
}
}

View File

@@ -315,4 +315,14 @@ public class DeviceUtilsModule extends ReactContextBaseJavaModule {
}
}
@ReactMethod
public void getBuildFlavour(Promise promise) {
try {
String buildFlavour = BuildConfig.FLAVOR;
promise.resolve(buildFlavour);
} catch (Exception err) {
promise.reject(err);
}
}
}

View File

@@ -1,6 +1,7 @@
{
"name": "AV_APP",
"version": "2.6.9",
"buildNumber": "116",
"private": true,
"scripts": {
"android:dev": "yarn move:dev && react-native run-android",

View File

@@ -2,8 +2,8 @@ import React, { ReactNode, useCallback, useState } from 'react';
import { AppState, Linking } from 'react-native';
import { useSelector } from 'react-redux';
import { RootState } from '../store/store';
import { UninstallInformation } from '../reducer/metadataSlice';
import { getAppVersion } from '../components/utlis/commonFunctions';
import { IAppState, UninstallInformation } from '../reducer/metadataSlice';
import { getAppVersion, getBuildVersion } from '../components/utlis/commonFunctions';
import BlockerInstructions from './BlockerInstructions';
import { BLOCKER_SCREEN_DATA, CLICKSTREAM_EVENT_NAMES } from './Constants';
import { useAppDispatch, useAppSelector } from '../hooks';
@@ -18,6 +18,7 @@ import handleBlacklistedAppsForBlockingCosmos, {
import { addClickstreamEvent } from '@services/clickstreamEventService';
import { setBlacklistedAppsInstalledData } from '@reducers/blacklistedAppsInstalledSlice';
import perf from '@react-native-firebase/perf';
import { GLOBAL } from '@constants/Global';
interface IBlockerScreen {
children?: ReactNode;
@@ -26,12 +27,17 @@ interface IBlockerScreen {
const RETRY_GEOLOCATION_STEPS =
'Unable to retrieve location. Kindly follow the given steps and try again.';
const BUILD_FOR_FIELD = 'fieldAgents';
const BlockerScreen = (props: IBlockerScreen) => {
const [forceReinstallData, setForceReinstallData] = useState<UninstallInformation>();
const { isTimeSynced, isDeviceLocationEnabled } = useAppSelector(
(state) => state.foregroundService
);
const { isWifiOrCellularOn } = useAppSelector((state) => state.metadata);
const { isWifiOrCellularOn, appState } = useAppSelector((state) => state.metadata);
const [shouldUpdate, setShouldUpdate] = useState<boolean>();
const [showActionBtnLoader, setShowActionBtnLoader] = useState(false);
const dispatch = useAppDispatch();
@@ -66,6 +72,28 @@ const BlockerScreen = (props: IBlockerScreen) => {
setForceReinstallData(undefined);
}, [JSON.stringify(forceUninstallData || {})]);
React.useEffect(() => {
if (!appState) return;
const buildToCompare = GLOBAL.BUILD_FLAVOUR;
if (!buildToCompare) return;
let flavorToUpdate: IAppState;
if(GLOBAL.BUILD_FLAVOUR.includes('fieldAgents')){
flavorToUpdate = appState?.fieldAgent;
}else {
flavorToUpdate = appState?.telecallingAgents;
}
if (!flavorToUpdate) return;
const currentBuildNumber = getBuildVersion();
if (currentBuildNumber && currentBuildNumber < flavorToUpdate.buildNumber) {
setShouldUpdate(true);
} else {
setShouldUpdate(false);
}
}, [appState]);
React.useEffect(() => {
const appStateChange = AppState.addEventListener('change', async (change) => {
setTimeout(async () => {
@@ -92,6 +120,18 @@ const BlockerScreen = (props: IBlockerScreen) => {
await Linking.openSettings();
}, []);
const handleAppUpdate = () => {
let appUpdateUrl;
if(GLOBAL.BUILD_FLAVOUR.includes(BUILD_FOR_FIELD)){
appUpdateUrl = appState?.fieldAgent.currentProdAPK;
}else {
appUpdateUrl = appState?.telecallingAgents.currentProdAPK;
}
if (appUpdateUrl) {
openApkDownloadLink(appUpdateUrl);
}
};
const handleLocationAccess = async () => {
setShowActionBtnLoader(true);
const isLocationEnabled = await locationEnabled();
@@ -103,6 +143,18 @@ const BlockerScreen = (props: IBlockerScreen) => {
}
};
if (shouldUpdate) {
const { heading, instructions } = BLOCKER_SCREEN_DATA.UNINSTALL_APP;
return (
<BlockerInstructions
heading={heading}
instructions={instructions}
actionBtn={{ title: 'Download New App', action: handleAppUpdate }}
/>
);
}
if (forceReinstallData?.reinstall_endpoint) {
const { heading, instructions } = BLOCKER_SCREEN_DATA.UNINSTALL_APP;
return (

View File

@@ -1,3 +1,4 @@
import { buildFlavour } from '@reducers/metadataSlice';
import { NativeModules } from 'react-native';
const { DeviceUtilsModule } = NativeModules; // this is the same name we returned in getName function.
@@ -71,4 +72,6 @@ export const setBottomSheetView = (id: number | null) => noop();
export const clearBottomSheet = () => noop();
export const alfredSetEmailId = (emailId: string) => noop();
export const alfredSetEmailId = (emailId: string) => noop();
export const getBuildFlavour = (): Promise<buildFlavour> => DeviceUtilsModule.getBuildFlavour();

View File

@@ -26,6 +26,7 @@ import { IGeolocationCoordinate } from '../../types/addressGeolocation.types';
import { toast } from '@rn-ui-lib/components/toast';
import { sendContentToWhatsapp } from './DeviceUtils';
import { ToastMessages } from '@screens/allCases/constants';
import VersionNumber from 'react-native-version-number';
const fs = ReactNativeBlobUtil.fs;
@@ -243,6 +244,10 @@ export function getAppVersion(): string {
return packageJson.version;
}
export const getBuildVersion = (): string => {
return packageJson?.buildNumber || VersionNumber.buildVersion;
}
export const getDocumentList = (caseDetails: CaseDetail) => {
return caseDetails.caseType === CaseAllocationType.ADDRESS_VERIFICATION_CASE
? caseDetails.customerInfo?.documents

View File

@@ -1,3 +1,4 @@
import { buildFlavour } from '@reducers/metadataSlice';
import { isNullOrUndefined } from '../components/utlis/commonFunctions';
export enum DEVICE_TYPE_ENUM {
@@ -12,6 +13,7 @@ export const GLOBAL = {
DEVICE_TYPE: DEVICE_TYPE_ENUM.MOBILE,
IS_IMPERSONATED: false,
SELECTED_AGENT_ID: '',
BUILD_FLAVOUR: ''
};
interface IGlobalUserData {
@@ -32,3 +34,7 @@ export const setGlobalUserData = (userData: IGlobalUserData) => {
if (!isNullOrUndefined(isImpersonated)) GLOBAL.IS_IMPERSONATED = isImpersonated ?? false;
if (!isNullOrUndefined(selectedAgentId)) GLOBAL.SELECTED_AGENT_ID = `${selectedAgentId}`;
};
export const setGlobalBuildFlavour = (buildFlavour: buildFlavour) => {
if (buildFlavour) GLOBAL.BUILD_FLAVOUR = buildFlavour;
};

View File

@@ -15,7 +15,7 @@ import { type ILockData, MY_CASE_ITEM, setLockData, VisitPlanStatus } from '../r
import { setFilters } from '../reducer/filtersSlice';
import { type FormTemplateV1 } from '../types/template.types';
import { ToastMessages } from '../screens/allCases/constants';
import { setForceUninstallData } from '../reducer/metadataSlice';
import { setCurrentProdAPK, setForceUninstallData } from '../reducer/metadataSlice';
import { logError } from '../components/utlis/errorUtils';
import { type GenericFunctionArgs } from '../common/GenericTypes';
import { addClickstreamEvent } from '@services/clickstreamEventService';
@@ -57,7 +57,7 @@ const useFirestoreUpdates = () => {
let forceUninstallUnsubscribe: GenericFunctionArgs;
let lockUnsubscribe: GenericFunctionArgs;
let feedbackFiltersUnsubscribe: GenericFunctionArgs;
let appUpdateUnsubscribe: GenericFunctionArgs;
const dispatch = useAppDispatch();
const showCaseUpdationToast = (newlyAddedCases: number, deletedCases: number) => {
@@ -187,6 +187,18 @@ const useFirestoreUpdates = () => {
logError(err as Error, errMsg);
};
const handleAppUpdate = (
snapshot: FirebaseFirestoreTypes.DocumentSnapshot<FirebaseFirestoreTypes.DocumentData>
) => {
const configData = snapshot.data();
dispatch(setCurrentProdAPK(configData));
};
const subscribeToAppUpdate = () => {
const collectionPath = 'app-state/app-update';
return subscribeToDoc(handleAppUpdate, collectionPath);
};
const signInUserToFirebase = () => {
if (!sessionDetails) {
return;
@@ -290,6 +302,7 @@ const useFirestoreUpdates = () => {
collectionTemplateUnsubscribe = subscribeToCollectionTemplate();
lockUnsubscribe = subscribeToLocks();
feedbackFiltersUnsubscribe = subscribeToFeedbackFilters();
appUpdateUnsubscribe = subscribeToAppUpdate();
}
useEffect(() => {
@@ -345,6 +358,7 @@ const useFirestoreUpdates = () => {
useEffect(() => {
forceUninstallUnsubscribe = subscribeToForceUninstall();
subscribeToAppUpdate();
}, []);
};

View File

@@ -8,17 +8,29 @@ export interface UninstallInformation {
reinstall_endpoint: string;
}
export interface IAppState {
buildNumber: string;
currentProdAPK?: string;
}
interface IMetadata {
isOnline: boolean;
forceUninstall: Record<string, UninstallInformation>;
isWifiOrCellularOn: boolean;
lastFirebaseResyncTimestamp: number;
appState: {
fieldAgent: IAppState,
telecallingAgents: IAppState
};
lastFirebaseResyncTimestamp: string;
}
export type buildFlavour = 'fieldAgents' | 'callingAgents';
const initialState = {
isOnline: true,
forceUninstall: {},
isWifiOrCellularOn: true,
appState: {},
lastFirebaseResyncTimestamp: dayJs(FIRST_DATE).toString(),
} as IMetadata;
@@ -35,17 +47,23 @@ const MetadataSlice = createSlice({
setIsWifiOrCellularOn: (state, action) => {
state.isWifiOrCellularOn = action.payload;
},
setCurrentProdAPK(state, action) {
state.appState = action.payload;
},
setLastFirebaseResyncTimestamp: (state, action) => {
state.lastFirebaseResyncTimestamp = action.payload;
},
},
});
export const {
setIsOnline,
setForceUninstallData,
setIsWifiOrCellularOn,
setLastFirebaseResyncTimestamp,
setCurrentProdAPK
} = MetadataSlice.actions;
export default MetadataSlice.reducer;

View File

@@ -1,6 +1,7 @@
import { createSlice } from '@reduxjs/toolkit';
import { setGlobalUserData } from '../constants/Global';
import { IReportee } from '../screens/allCases/interface';
import { getAppVersion, getBuildVersion } from '@components/utlis/commonFunctions';
interface ISessionDetails {
sessionToken: string;
@@ -67,7 +68,7 @@ export interface IUserSlice extends IUser {
agentAttendance: {
showAttendanceBanner: boolean;
attendanceDate: string;
};
}
}
const initialState: IUserSlice = {
@@ -140,7 +141,7 @@ export const userSlice = createSlice({
},
setAgentAttendance: (state, action) => {
state.agentAttendance = action.payload;
},
}
},
});

View File

@@ -25,7 +25,7 @@ import { COLORS } from '../../../RN-UI-LIB/src/styles/colors';
import Button from '../../../RN-UI-LIB/src/components/Button';
import { navigateToScreen } from '../../components/utlis/navigationUtlis';
import GroupIcon from '../../../RN-UI-LIB/src/Icons/GroupIcon';
import { getAppVersion } from '../../components/utlis/commonFunctions';
import { getAppVersion, getBuildVersion } from '../../components/utlis/commonFunctions';
import VersionNumber from 'react-native-version-number';
import { useFocusEffect } from '@react-navigation/native';
import { CaseDetail } from '../caseDetails/interface';
@@ -261,7 +261,7 @@ const Profile: React.FC = () => {
>
<Text bold dark style={styles.version}>
App Version: {getAppVersion()} Gradle Version: {VersionNumber.appVersion} Gradle Build
No: {VersionNumber.buildVersion}
No: {getBuildVersion()}
</Text>
</Pressable>
</ScrollView>

View File

@@ -4,7 +4,7 @@ import { useSelector } from 'react-redux';
import { getUniqueId, isTablet } from 'react-native-device-info';
import { useAppDispatch } from '../../hooks';
import { type RootState } from '../../store/store';
import { setAgentAttendance, setDeviceId } from '../../reducer/userSlice';
import { setAgentAttendance, setBuildInfo, setDeviceId } from '../../reducer/userSlice';
import { DEVICE_TYPE_ENUM, setGlobalUserData } from '../../constants/Global';
import { registerNavigateAndDispatch } from '../../components/utlis/apiHelper';
import ProtectedRouter from './ProtectedRouter';
@@ -26,6 +26,7 @@ import AnswerRender from '../../components/form/AnswerRender';
import useScreenshotTracking from '../../hooks/useScreenshotTracking';
import { getSyncTime } from '@hooks/capturingApi';
function AuthRouter() {
const dispatch = useAppDispatch();
const user = useSelector((state: RootState) => state.user);