From 15d218ced2397f0457119c23f707902c15fa937d Mon Sep 17 00:00:00 2001 From: Aman Chaturvedi Date: Wed, 9 Oct 2024 18:47:49 +0530 Subject: [PATCH] NTP-6184 | Clickstream events added for app update --- src/action/appDownloadAction.ts | 33 +++++++++++++-- src/common/BlockerScreen.tsx | 75 +++++++++++++++++++++++++-------- src/common/Constants.ts | 38 +++++++++++++++++ src/reducer/appUpdateSlice.ts | 29 +++++++++++++ src/store/store.ts | 5 ++- 5 files changed, 157 insertions(+), 23 deletions(-) create mode 100644 src/reducer/appUpdateSlice.ts diff --git a/src/action/appDownloadAction.ts b/src/action/appDownloadAction.ts index 5203abbe..b81e8c94 100644 --- a/src/action/appDownloadAction.ts +++ b/src/action/appDownloadAction.ts @@ -1,6 +1,8 @@ -import { BuildFlavours } from '@common/Constants'; +import { BuildFlavours, CLICKSTREAM_EVENT_NAMES } from '@common/Constants'; import { ApiKeys, getApiUrl } from '@components/utlis/apiHelper'; +import { getBuildVersion } from '@components/utlis/commonFunctions'; import { logError } from '@components/utlis/errorUtils'; +import { addClickstreamEvent } from '@services/clickstreamEventService'; import axios from 'axios'; import { Linking, NativeModules } from 'react-native'; import RNFetchBlob from 'react-native-blob-util'; @@ -24,17 +26,33 @@ export const deleteCachedApkFiles = async () => { } }; -export const downloadApkFromS3 = async (s3Url: string, fileName: string) => { +export const downloadApkFromS3 = async (s3Url: string, fileName: string, newAppVersion: number) => { deleteCachedApkFiles(); const dirs = RNFetchBlob.fs.dirs; const pathToSaveAPK = `${dirs.CacheDir}/latest-app/${fileName}.apk`; + const oldAppVersion = getBuildVersion(); try { + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_APK_UPDATE_DOWNLOAD_STARTED, { + downloadPath: pathToSaveAPK, + oldAppVersion, + newAppVersion, + }); const res = await RNFetchBlob.config({ path: pathToSaveAPK, fileCache: true, }).fetch('GET', s3Url, {}); + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_APK_UPDATE_DOWNLOAD_SUCCESS, { + downloadPath: res.path(), + oldAppVersion, + newAppVersion, + }); return res.path(); } catch (err) { + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_APK_UPDATE_DOWNLOAD_FAILED, { + errorMessage: (err as Error)?.message, + oldAppVersion, + newAppVersion, + }); logError(err as Error, 'Error while downloading the latest app'); } }; @@ -56,18 +74,25 @@ const openApkDownloadLink = (url: string) => { export const openFallbackLonghornLink = (fallbackUrl?: string) => { if (fallbackUrl) { + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_APK_UPDATE_FALLBACK_TRIGGERED, { + fallbackUrl, + }); openApkDownloadLink(fallbackUrl); } }; -export const downloadLatestApkAndGetFilePath = async (buildFlavour: BuildFlavours) => { +export const downloadLatestApkAndGetFilePath = async ( + buildFlavour: BuildFlavours, + appVersion: number +) => { const appUrl = await downloadLatestAppS3Url(buildFlavour); if (!appUrl) { return ''; } const appFileUrl = await downloadApkFromS3( appUrl, - `Cosmos_${BuildFlavours.FIELD_AGENTS}_${Date.now()}` + `Cosmos_${BuildFlavours.FIELD_AGENTS}_${Date.now()}`, + appVersion ); if (!appFileUrl) { return ''; diff --git a/src/common/BlockerScreen.tsx b/src/common/BlockerScreen.tsx index 853e880e..5ee5b237 100644 --- a/src/common/BlockerScreen.tsx +++ b/src/common/BlockerScreen.tsx @@ -26,6 +26,8 @@ import { openFallbackLonghornLink, } from '@actions/appDownloadAction'; import AppUpdate from './AppUpdate'; +import ReactNativeBlobUtil from 'react-native-blob-util'; +import { setShouldUpdate } from '@reducers/appUpdateSlice'; interface IBlockerScreen { children?: ReactNode; @@ -42,15 +44,11 @@ const BlockerScreen = (props: IBlockerScreen) => { const approvalStatus = useAppSelector((state) => state.profile?.approvalStatus); const isLoading = useAppSelector((state) => state.profile?.isLoading); const roles = useAppSelector((state) => state.user?.agentRoles); + const shouldUpdate = useAppSelector((state) => state.appUpdate.shouldUpdate) || {}; const isFieldAgent = (roles?.length === 1 && roles.includes(IUserRole.ROLE_FIELD_AGENT)) || roles.includes(IUserRole.ROLE_OMA); - const [shouldUpdate, setShouldUpdate] = useState({ - newApkCachedUrl: '', - switchToFallback: false, - }); - const [showActionBtnLoader, setShowActionBtnLoader] = useState(false); const dispatch = useAppDispatch(); @@ -61,34 +59,71 @@ const BlockerScreen = (props: IBlockerScreen) => { let apkFileUrl; if (GLOBAL.BUILD_FLAVOUR.includes(BuildFlavours.FIELD_AGENTS)) { // Download app for Field agent - apkFileUrl = await downloadLatestApkAndGetFilePath(BuildFlavours.FIELD_AGENTS); + apkFileUrl = await downloadLatestApkAndGetFilePath( + BuildFlavours.FIELD_AGENTS, + appState?.fieldAgents?.version + ); } else { // Download app for Calling agent - apkFileUrl = await downloadLatestApkAndGetFilePath(BuildFlavours.CALLING_AGENTS); + apkFileUrl = await downloadLatestApkAndGetFilePath( + BuildFlavours.CALLING_AGENTS, + appState?.telecallingAgents?.version + ); } if (apkFileUrl) { - setShouldUpdate({ newApkCachedUrl: apkFileUrl, switchToFallback: false }); + dispatch(setShouldUpdate({ newApkCachedUrl: apkFileUrl, switchToFallback: false })); } else { - setShouldUpdate({ newApkCachedUrl: '', switchToFallback: true }); + dispatch(setShouldUpdate({ newApkCachedUrl: '', switchToFallback: true })); } }; const handleAppUpdate = () => { let fallbackLonghornUrl; + let oldAppVersion = getBuildVersion(); + let newAppVersion = appState?.fieldAgents?.version; if (GLOBAL.BUILD_FLAVOUR.includes('fieldAgents')) { fallbackLonghornUrl = appState?.fieldAgents?.currentProdAPK; } else { + newAppVersion = appState?.telecallingAgents?.version; fallbackLonghornUrl = appState?.telecallingAgents?.currentProdAPK; } + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_APK_UPDATE_BUTTON_CLICKED, { + apkPath: shouldUpdate.newApkCachedUrl, + oldAppVersion, + newAppVersion, + }); if (!shouldUpdate.newApkCachedUrl) { openFallbackLonghornLink(fallbackLonghornUrl); return; } - installApk(shouldUpdate.newApkCachedUrl, (error) => { - if (!error) { - return; + ReactNativeBlobUtil.fs.stat(shouldUpdate.newApkCachedUrl).then((res) => { + if (res.size < 10e6) { + // Temporary check: if size less than 10MB, then file is corrupted. Go to fallback link + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_APK_UPDATE_CORRUPTED_FILE_DOWNLOADED, { + apkPath: shouldUpdate.newApkCachedUrl, + oldAppVersion, + newAppVersion, + }); + openFallbackLonghornLink(fallbackLonghornUrl); + } else { + installApk(shouldUpdate.newApkCachedUrl, (error) => { + if (!error) { + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_APK_UPDATE_INSTALL_STARTED, { + apkPath: shouldUpdate.newApkCachedUrl, + oldAppVersion, + newAppVersion, + }); + return; + } + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_APK_UPDATE_INSTALL_FAILED, { + apkPath: shouldUpdate.newApkCachedUrl, + errorMessage: error, + oldAppVersion, + newAppVersion, + }); + openFallbackLonghornLink(fallbackLonghornUrl); + }); } - openFallbackLonghornLink(fallbackLonghornUrl); }); }; @@ -105,18 +140,22 @@ const BlockerScreen = (props: IBlockerScreen) => { if (!flavorToUpdate) return; const currentBuildNumber = getBuildVersion(); - if ( currentBuildNumber && !isNaN(currentBuildNumber) && currentBuildNumber < flavorToUpdate.version ) { downloadLatestApp(); - } else { - setShouldUpdate({ - newApkCachedUrl: '', - switchToFallback: false, + } else if (shouldUpdate.newApkCachedUrl) { + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_APK_UPDATE_INSTALL_SUCCESS, { + appVersion: currentBuildNumber, }); + dispatch( + setShouldUpdate({ + newApkCachedUrl: '', + switchToFallback: false, + }) + ); deleteCachedApkFiles(); } }, [appState]); diff --git a/src/common/Constants.ts b/src/common/Constants.ts index 7df85363..d48340be 100644 --- a/src/common/Constants.ts +++ b/src/common/Constants.ts @@ -1309,6 +1309,44 @@ export const CLICKSTREAM_EVENT_NAMES = { name: 'FA_UNSYNC_FEEDBACK_CAPTURED', description: 'Unsync feedback captured' }, + + // Apk Update + FA_APK_UPDATE_DOWNLOAD_STARTED: { + name: 'FA_APK_UPDATE_DOWNLOAD_STARTED', + description: 'APK update download started' + }, + FA_APK_UPDATE_DOWNLOAD_SUCCESS: { + name: 'FA_APK_UPDATE_DOWNLOAD_SUCCESS', + description: 'APK update download completed' + }, + FA_APK_UPDATE_DOWNLOAD_FAILED: { + name: 'FA_APK_UPDATE_DOWNLOAD_FAILED', + description: 'APK update download failed' + }, + FA_APK_UPDATE_BUTTON_CLICKED: { + name: 'FA_APK_UPDATE_BUTTON_CLICKED', + description: 'APK update button clicked' + }, + FA_APK_UPDATE_INSTALL_STARTED: { + name: 'FA_APK_UPDATE_INSTALL_STARTED', + description: 'APK update installation started' + }, + FA_APK_UPDATE_INSTALL_FAILED: { + name: 'FA_APK_UPDATE_INSTALL_FAILED', + description: 'APK update installation failed' + }, + FA_APK_UPDATE_FALLBACK_TRIGGERED: { + name: 'FA_APK_UPDATE_FALLBACK_TRIGGERED', + description: 'APK update fallback triggered' + }, + FA_APK_UPDATE_CORRUPTED_FILE_DOWNLOADED: { + name: 'FA_APK_UPDATE_CORRUPTED_FILE_DOWNLOADED', + description: 'APK update corrupted file downloaded' + }, + FA_APK_UPDATE_INSTALL_SUCCESS: { + name: 'FA_APK_UPDATE_INSTALL_SUCCESS', + description: 'APK update installation success' + }, } as const; export enum MimeType { diff --git a/src/reducer/appUpdateSlice.ts b/src/reducer/appUpdateSlice.ts new file mode 100644 index 00000000..c4d9cde6 --- /dev/null +++ b/src/reducer/appUpdateSlice.ts @@ -0,0 +1,29 @@ +import { createSlice } from '@reduxjs/toolkit'; + +interface IAppUpdateSlice { + shouldUpdate: { + newApkCachedUrl: string; + switchToFallback: boolean; + }; +} + +const initialState: IAppUpdateSlice = { + shouldUpdate: { + newApkCachedUrl: '', + switchToFallback: false, + }, +}; + +const AppUpdateSlice = createSlice({ + name: 'appUpdate', + initialState, + reducers: { + setShouldUpdate: (state, action) => { + state.shouldUpdate = action.payload; + }, + }, +}); + +export const { setShouldUpdate } = AppUpdateSlice.actions; + +export default AppUpdateSlice.reducer; diff --git a/src/store/store.ts b/src/store/store.ts index 2ecd7293..0c728e9a 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -16,6 +16,7 @@ import repaymentsSlice from '../reducer/repaymentsSlice'; import feedbackHistorySlice from '../reducer/feedbackHistorySlice'; import notificationsSlice from '../reducer/notificationsSlice'; import MetadataSlice from '../reducer/metadataSlice'; +import AppUpdateSlice from '../reducer/appUpdateSlice'; import foregroundServiceSlice from '../reducer/foregroundServiceSlice'; import feedbackImagesSlice from '../reducer/feedbackImagesSlice'; import configSlice from '../reducer/configSlice'; @@ -47,6 +48,7 @@ const rootReducer = combineReducers({ address: addressSlice, notifications: notificationsSlice, metadata: MetadataSlice, + appUpdate: AppUpdateSlice, foregroundService: foregroundServiceSlice, feedbackImages: feedbackImagesSlice, config: configSlice, @@ -86,7 +88,8 @@ const persistConfig = { 'foregroundService', 'feedbackFilters', 'litmusExperiment', - 'activeCall' + 'activeCall', + 'appUpdate' ], blackList: [ 'case',