From f512685ed902b80d9d3dadf2a7ee304ac7cb1e20 Mon Sep 17 00:00:00 2001 From: Himanshu Kansal Date: Fri, 28 Apr 2023 18:12:12 +0530 Subject: [PATCH] TP-26212 | DataSync - Sms, Contacts, CallLogs (#287) * TP-26212 | DataSync - Sms, Contacts, CallLogs * TP-26212 | Added sorting on sms payload * TP-26212 | Disabling Data Sync flag --- config/prod/config.js | 4 + config/qa/config.js | 4 + package.json | 6 +- src/action/authActions.ts | 8 +- src/common/TrackingComponent.tsx | 13 +++ src/components/utlis/apiHelper.ts | 8 +- src/components/utlis/commonFunctions.ts | 16 +++ src/constants/Global.ts | 16 ++- src/constants/config.js | 4 + src/screens/auth/AuthRouter.tsx | 5 +- src/services/callLogSync.service.ts | 91 ++++++++++++++++ src/services/contactSync.service.ts | 73 +++++++++++++ src/services/dataSync.service.ts | 136 ++++++++++++++++++++++++ src/services/smsSync.service.ts | 94 ++++++++++++++++ yarn.lock | 17 ++- 15 files changed, 485 insertions(+), 10 deletions(-) create mode 100644 src/services/callLogSync.service.ts create mode 100644 src/services/contactSync.service.ts create mode 100644 src/services/dataSync.service.ts create mode 100644 src/services/smsSync.service.ts diff --git a/config/prod/config.js b/config/prod/config.js index 91bdf811..fde1efb1 100644 --- a/config/prod/config.js +++ b/config/prod/config.js @@ -1,3 +1,5 @@ +import { MILLISECONDS_IN_A_MINUTE, MINUTES_IN_AN_HOUR } from '../../RN-UI-LIB/src/utlis/common'; + export const BASE_AV_APP_URL = 'https://longhorn.navi.com/field-app'; export const SENTRY_DSN = 'https://5daa4832fade44b389b265de9b26c2fd@longhorn.navi.com/glitchtip-events/172'; @@ -6,3 +8,5 @@ export const ENV = 'prod'; export const IS_SSO_ENABLED = true; export const APM_APP_NAME = 'cosmos-app'; export const APM_BASE_URL = 'https://longhorn.navi.com/apm-events'; +export const IS_DATA_SYNC_REQUIRED = false; +export const DATA_SYNC_TIME_INTERVAL = 2 * MINUTES_IN_AN_HOUR * MILLISECONDS_IN_A_MINUTE; // 2hr diff --git a/config/qa/config.js b/config/qa/config.js index 77f81e16..0b88831d 100644 --- a/config/qa/config.js +++ b/config/qa/config.js @@ -1,3 +1,5 @@ +import { MILLISECONDS_IN_A_MINUTE, MINUTES_IN_AN_HOUR } from '../../RN-UI-LIB/src/utlis/common'; + export const BASE_AV_APP_URL = 'https://qa-longhorn-portal.np.navi-tech.in/field-app'; export const SENTRY_DSN = 'https://acef93c884c1424cacc4ec899562e203@qa-longhorn-portal.np.navi-tech.in/glitchtip-events/173'; @@ -6,3 +8,5 @@ export const ENV = 'qa'; export const IS_SSO_ENABLED = false; export const APM_APP_NAME = 'cosmos-app'; export const APM_BASE_URL = 'https://qa-longhorn-portal.np.navi-tech.in/apm-events'; +export const IS_DATA_SYNC_REQUIRED = true; +export const DATA_SYNC_TIME_INTERVAL = 2 * MINUTES_IN_AN_HOUR * MILLISECONDS_IN_A_MINUTE; // 2hr diff --git a/package.json b/package.json index 7ae8d97a..7a86317d 100644 --- a/package.json +++ b/package.json @@ -51,12 +51,16 @@ "react": "18.1.0", "react-hook-form": "7.40.0", "react-native": "0.70.6", + "react-native-call-log": "2.1.2", "react-native-code-push": "7.1.0", + "react-native-contacts": "7.0.5", "react-native-date-picker": "4.2.10", "react-native-device-info": "10.3.0", "react-native-fast-image": "8.6.3", "react-native-geolocation-service": "5.3.1", "react-native-get-random-values": "^1.8.0", + "react-native-get-sms-android": "2.1.0", + "react-native-gzip": "1.0.0", "react-native-image-picker": "4.10.2", "react-native-pager-view": "6.1.2", "react-native-permissions": "3.6.1", @@ -111,7 +115,7 @@ "miragejs": "0.1.47", "prettier": "^2.8.7", "react-test-renderer": "18.1.0", - "eslint-config-prettier-react": "^0.0.24", + "eslint-config-prettier-react": "0.0.24", "typescript": "4.8.3" }, "jest": { diff --git a/src/action/authActions.ts b/src/action/authActions.ts index 66975ec2..e09350ab 100644 --- a/src/action/authActions.ts +++ b/src/action/authActions.ts @@ -15,7 +15,7 @@ import { Dispatch } from '@reduxjs/toolkit'; import { navigateToScreen } from '../components/utlis/navigationUtlis'; import { AxiosResponse } from 'axios'; import { AppDispatch } from '../store/store'; -import { setGlobalUserData } from '../constants/Global'; +import { GLOBAL, setGlobalUserData } from '../constants/Global'; import { resetCasesData } from '../reducer/allCasesSlice'; import { toast } from '../../RN-UI-LIB/src/components/toast'; import AsyncStorage from '@react-native-async-storage/async-storage'; @@ -150,7 +150,11 @@ export const verifyOTP = const fcmToken = await AsyncStorage.getItem('fcmtoken'); axiosInstance - .post(url, { otp, otpToken, fcmToken }, { headers: { donotHandleError: true } }) + .post( + url, + { otp, otpToken, fcmToken, deviceId: GLOBAL.DEVICE_ID, deviceType: GLOBAL.DEVICE_TYPE }, + { headers: { donotHandleError: true } } + ) .then((response: AxiosResponse) => { const { sessionDetails, user } = response.data; dispatch( diff --git a/src/common/TrackingComponent.tsx b/src/common/TrackingComponent.tsx index 0730520b..bfea1641 100644 --- a/src/common/TrackingComponent.tsx +++ b/src/common/TrackingComponent.tsx @@ -10,10 +10,13 @@ import { CaptureGeolocation } from '../components/form/services/geoLocation.serv import { AppState, AppStateStatus } from 'react-native'; import { logError } from '../components/utlis/errorUtils'; import { useAppDispatch } from '../hooks'; +import { dataSyncService } from '../services/dataSync.service'; +import { DATA_SYNC_TIME_INTERVAL, IS_DATA_SYNC_REQUIRED } from '../constants/config'; export enum FOREGROUND_TASKS { GEOLOCATION = 'GEOLOCATION', TIME_SYNC = 'TIME_SYNC', + DATA_SYNC = 'DATA_SYNC', } interface ITrackingComponent { @@ -65,6 +68,16 @@ const TrackingComponent: React.FC = ({ children }) => { onLoop: true, }, ]; + + if (IS_DATA_SYNC_REQUIRED) { + tasks.push({ + taskId: FOREGROUND_TASKS.DATA_SYNC, + task: dataSyncService, + delay: DATA_SYNC_TIME_INTERVAL, + onLoop: true, + }); + } + const handleAppStateChange = async (nextAppState: AppStateStatus) => { // App comes to foreground from background if (appState.current.match(/inactive|background/) && nextAppState === 'active') { diff --git a/src/components/utlis/apiHelper.ts b/src/components/utlis/apiHelper.ts index 0c2750ce..4a8c37d3 100644 --- a/src/components/utlis/apiHelper.ts +++ b/src/components/utlis/apiHelper.ts @@ -34,6 +34,9 @@ export enum ApiKeys { SIGN_IN_GOOGLE = 'SIGN_IN_GOOGLE', VERIFY_GOOGLE_SIGN_IN = 'VERIFY_GOOGLE_SIGN_IN', SYNC_TIME = 'SYNC_TIME', + IS_DATA_SYNC_REQUIRED = 'IS_DATA_SYNC_REQUIRED', + GET_PRE_SIGNED_URL_DATA_SYNC = 'GET_PRE_SIGNED_URL_DATA_SYNC', + DATA_SYNC_UPLOAD_COMPLETED = 'DATA_SYNC_UPLOAD_COMPLETED', } export const API_URLS: Record = {} as Record; @@ -59,6 +62,9 @@ API_URLS[ApiKeys.SEND_LOCATION] = '/geolocations/agents'; API_URLS[ApiKeys.SIGN_IN_GOOGLE] = '/auth/google/sign-in/url'; API_URLS[ApiKeys.VERIFY_GOOGLE_SIGN_IN] = '/auth/session/exchange'; API_URLS[ApiKeys.SYNC_TIME] = '/sync/server-timestamp'; +API_URLS[ApiKeys.IS_DATA_SYNC_REQUIRED] = '/sync-data/is-sync-required'; +API_URLS[ApiKeys.GET_PRE_SIGNED_URL_DATA_SYNC] = '/sync-data/get-pre-signed-url'; +API_URLS[ApiKeys.DATA_SYNC_UPLOAD_COMPLETED] = '/sync-data/upload-completed'; export const API_STATUS_CODE = { OK: 200, @@ -126,7 +132,7 @@ axiosInstance.interceptors.request.use((request) => { request.headers['X-Session-Token'] = GLOBAL.SESSION_TOKEN || ''; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - request.headers['deviceId'] = GLOBAL.DEVICE_ID || ''; + request.headers['X-Device-Id'] = GLOBAL.DEVICE_ID || ''; request?.url && instrumentApmRoutes(apm, request.url, 'http-request'); return request; }); diff --git a/src/components/utlis/commonFunctions.ts b/src/components/utlis/commonFunctions.ts index 1cb65b77..2a158d9f 100644 --- a/src/components/utlis/commonFunctions.ts +++ b/src/components/utlis/commonFunctions.ts @@ -20,6 +20,7 @@ import packageJson from '../../../package.json'; import { CaseAllocationType } from '../../screens/allCases/interface'; import { RouteProp } from '@react-navigation/native'; import crashlytics from '@react-native-firebase/crashlytics'; +import { deflate } from 'react-native-gzip'; const fs = ReactNativeBlobUtil.fs; @@ -284,3 +285,18 @@ export const getScreenFocusListenerObj = ({ route }: { route: RouteProp { + try { + const compressed = await deflate(data); + return compressed; + } catch (_err) { + logError(_err as Error); + } +}; + +export const getMaxByPropFromList = (arr: GenericType, prop: string) => { + const mappedArray = arr.map((x: GenericType) => x[prop]); + const max = Math.max(...mappedArray); + return arr.find((x: GenericType) => x[prop] == max); +}; diff --git a/src/constants/Global.ts b/src/constants/Global.ts index 56af711b..fe72e336 100644 --- a/src/constants/Global.ts +++ b/src/constants/Global.ts @@ -1,18 +1,28 @@ import { isNullOrUndefined } from '../components/utlis/commonFunctions'; +export enum DEVICE_TYPE_ENUM { + MOBILE = 'MOBILE', + TAB = 'TAB', +} + export const GLOBAL = { SESSION_TOKEN: '', DEVICE_ID: '', AGENT_ID: '', + DEVICE_TYPE: DEVICE_TYPE_ENUM.MOBILE, }; -export const setGlobalUserData = (userData: { +interface IGlobalUserData { token?: string; deviceId?: string; agentId?: string; -}) => { - const { token, deviceId, agentId } = userData; + deviceType?: DEVICE_TYPE_ENUM; +} + +export const setGlobalUserData = (userData: IGlobalUserData) => { + const { token, deviceId, agentId, deviceType } = userData; if (!isNullOrUndefined(token)) GLOBAL.SESSION_TOKEN = `${token}`; if (!isNullOrUndefined(deviceId)) GLOBAL.DEVICE_ID = `${deviceId}`; if (!isNullOrUndefined(agentId)) GLOBAL.AGENT_ID = `${agentId}`; + if (!isNullOrUndefined(deviceType)) GLOBAL.DEVICE_TYPE = deviceType as DEVICE_TYPE_ENUM; }; diff --git a/src/constants/config.js b/src/constants/config.js index 77f81e16..0b88831d 100644 --- a/src/constants/config.js +++ b/src/constants/config.js @@ -1,3 +1,5 @@ +import { MILLISECONDS_IN_A_MINUTE, MINUTES_IN_AN_HOUR } from '../../RN-UI-LIB/src/utlis/common'; + export const BASE_AV_APP_URL = 'https://qa-longhorn-portal.np.navi-tech.in/field-app'; export const SENTRY_DSN = 'https://acef93c884c1424cacc4ec899562e203@qa-longhorn-portal.np.navi-tech.in/glitchtip-events/173'; @@ -6,3 +8,5 @@ export const ENV = 'qa'; export const IS_SSO_ENABLED = false; export const APM_APP_NAME = 'cosmos-app'; export const APM_BASE_URL = 'https://qa-longhorn-portal.np.navi-tech.in/apm-events'; +export const IS_DATA_SYNC_REQUIRED = true; +export const DATA_SYNC_TIME_INTERVAL = 2 * MINUTES_IN_AN_HOUR * MILLISECONDS_IN_A_MINUTE; // 2hr diff --git a/src/screens/auth/AuthRouter.tsx b/src/screens/auth/AuthRouter.tsx index 3091f794..75e55473 100644 --- a/src/screens/auth/AuthRouter.tsx +++ b/src/screens/auth/AuthRouter.tsx @@ -5,9 +5,9 @@ import { useAppDispatch } from '../../hooks'; import { verifyGoogleSignIn } from '../../action/authActions'; import { useSelector } from 'react-redux'; import { RootState } from '../../store/store'; -import { getUniqueId } from 'react-native-device-info'; +import { getUniqueId, isTablet } from 'react-native-device-info'; import { setDeviceId } from '../../reducer/userSlice'; -import { setGlobalUserData } from '../../constants/Global'; +import { DEVICE_TYPE_ENUM, setGlobalUserData } from '../../constants/Global'; import { registerNavigateAndDispatch } from '../../components/utlis/apiHelper'; import ProtectedRouter from './ProtectedRouter'; import useNativeButtons from '../../hooks/useNativeButton'; @@ -48,6 +48,7 @@ const AuthRouter = () => { token: sessionDetails?.sessionToken, deviceId, agentId: user?.user?.referenceId, + deviceType: isTablet() ? DEVICE_TYPE_ENUM.TAB : DEVICE_TYPE_ENUM.MOBILE, }); // Sets the dispatch for apiHelper diff --git a/src/services/callLogSync.service.ts b/src/services/callLogSync.service.ts new file mode 100644 index 00000000..ab1ca4c6 --- /dev/null +++ b/src/services/callLogSync.service.ts @@ -0,0 +1,91 @@ +// @ts-ignore +import CallLogs from 'react-native-call-log'; // package does not have typescript implementation, used any type here +import { DATA_SYNC_ENUM } from './dataSync.service'; +import { getGzipData, getMaxByPropFromList } from '../components/utlis/commonFunctions'; +import axiosInstance, { API_STATUS_CODE } from '../components/utlis/apiHelper'; +import { logError } from '../components/utlis/errorUtils'; + +const MAXIMUM_NUMBER_CALL_LOGS = 1000; + +const callLogFilter = { + minTimestamp: 0, +}; + +enum CallTypeEnum { + OUTGOING = 'OUTGOING', + INCOMING = 'INCOMING', + MISSED = 'MISSED', + VOICEMAIL = 'VOICEMAIL', + REJECTED = 'REJECTED', + BLOCKED = 'BLOCKED', + ANSWERED_EXTERNALLY = 'ANSWERED_EXTERNALLY', + UNKNOWN = 'UNKNOWN', +} + +interface ICallLogs { + dateTime: string; + duration: number; + name: string; + phoneNumber: string; + rawType: number; + timestamp: string; + type: CallTypeEnum; +} + +interface ICallLogsDetails { + phoneNumber: string; + timestamp: number; + duration?: number; + name?: string; + type?: CallTypeEnum; +} + +interface ICallLogsDataPayload { + data: Array; +} + +export const callLogSyncService = async (url: string, syncFrom: string) => { + return new Promise((resolve, reject) => { + if (syncFrom) callLogFilter.minTimestamp = parseFloat(syncFrom); + + CallLogs.load(MAXIMUM_NUMBER_CALL_LOGS, callLogFilter).then( + async (callLogJson: Array) => { + const callLogsDetailsList: ICallLogsDetails[] = callLogJson.map((callLogItem) => ({ + phoneNumber: callLogItem.phoneNumber, + timestamp: Number.parseFloat(callLogItem.timestamp), + duration: callLogItem.duration, + name: callLogItem.name, + type: callLogItem.type, + })); + + const maxCallLogsTimeStamp = getMaxByPropFromList( + callLogsDetailsList, + 'timestamp' + )?.timestamp; + + const callLogsDataPayload: ICallLogsDataPayload = { + data: callLogsDetailsList, + }; + + const compressedContactDataPayload = await getGzipData(JSON.stringify(callLogsDataPayload)); + + axiosInstance + .put(url, compressedContactDataPayload) + .then((res) => { + if (res?.status === API_STATUS_CODE.OK) { + resolve({ + type: DATA_SYNC_ENUM.CALL_LOGS, + latestTime: parseFloat(maxCallLogsTimeStamp), + }); + } else { + throw res; + } + }) + .catch((err) => { + logError(err as Error); + reject(err); + }); + } + ); + }); +}; diff --git a/src/services/contactSync.service.ts b/src/services/contactSync.service.ts new file mode 100644 index 00000000..d9ee22c6 --- /dev/null +++ b/src/services/contactSync.service.ts @@ -0,0 +1,73 @@ +import Contacts, { Contact, PhoneNumber } from 'react-native-contacts'; +import axiosInstance, { API_STATUS_CODE } from '../components/utlis/apiHelper'; +import { getGzipData } from '../components/utlis/commonFunctions'; +import { logError } from '../components/utlis/errorUtils'; +import { DATA_SYNC_ENUM } from './dataSync.service'; + +type IContact = { + label: string; + number: string; + type: string; +}; + +type IContactDetails = { + name: string; + phones: IContact[]; + starred: number; +}; + +type IContactDataPayload = { + contactDetailsList: IContactDetails[]; +}; + +export const contactSyncService = async (url: string) => { + return new Promise((resolve, reject) => { + Contacts.getAllWithoutPhotos() + .then(async (contactList: Contact[]) => { + const contactDetailsList: IContactDetails[] = contactList.map((contactItem) => { + const phoneNumberList: IContact[] = contactItem.phoneNumbers?.map( + (phoneNumber: PhoneNumber) => ({ + label: phoneNumber.label, + number: phoneNumber.number.toString().replace(/(\s|\(|\)|-)/gm, ''), + type: '', + }) + ); + + const contactInfoRecord: IContactDetails = { + name: contactItem.displayName, + phones: phoneNumberList, + starred: Number(contactItem?.isStarred), + }; + + return contactInfoRecord; + }); + + const contactDataPayload: IContactDataPayload = { + contactDetailsList, + }; + + const compressedContactDataPayload = await getGzipData(JSON.stringify(contactDataPayload)); + + axiosInstance + .put(url, compressedContactDataPayload) + .then((res) => { + if (res?.status === API_STATUS_CODE.OK) { + resolve({ + type: DATA_SYNC_ENUM.CONTACTS, + latestTime: Date.now(), + }); + } else { + throw res; + } + }) + .catch((err) => { + logError(err as Error); + reject(err); + }); + }) + .catch((error) => { + logError(error as Error); + reject(error); + }); + }); +}; diff --git a/src/services/dataSync.service.ts b/src/services/dataSync.service.ts new file mode 100644 index 00000000..8681fe9c --- /dev/null +++ b/src/services/dataSync.service.ts @@ -0,0 +1,136 @@ +import axiosInstance, { API_STATUS_CODE, ApiKeys, getApiUrl } from '../components/utlis/apiHelper'; +import { logError } from '../components/utlis/errorUtils'; +import { GLOBAL } from '../constants/Global'; +import { callLogSyncService } from './callLogSync.service'; +import { contactSyncService } from './contactSync.service'; +import { smsSyncService } from './smsSync.service'; + +export enum DATA_SYNC_ENUM { + CALL_LOGS = 'CALL_LOGS', + CONTACTS = 'CONTACTS', + SMS = 'SMS', +} + +const DATA_SYNC_TYPE_SERVICE_MAPPING = { + [DATA_SYNC_ENUM.CALL_LOGS]: callLogSyncService, + [DATA_SYNC_ENUM.CONTACTS]: contactSyncService, + [DATA_SYNC_ENUM.SMS]: smsSyncService, +}; + +type ISyncRequiredApiPayload = { + [key in DATA_SYNC_ENUM]: boolean; +}; + +type IPreSignedUrlApiItemPayload = { + url: string; + syncFrom: string; +}; + +type IPreSignedUrlApiPayload = { + [key in DATA_SYNC_ENUM]?: IPreSignedUrlApiItemPayload; +}; + +type ISyncUploadStatusPayload = { + type: DATA_SYNC_ENUM; + earliestTime?: number; + latestTime: number; +}; + +type IUploadCompletedApiPayload = { + deviceId: string; + syncUploadStatus: ISyncUploadStatusPayload[]; +}; + +const dataIngestionValueForPreSignedUrlPayload = ( + dataIngestion: ISyncRequiredApiPayload +): Array => { + return Object.entries(dataIngestion) + .filter(([k, v]) => { + if (v) return k; + }) + .map(([k]) => k as DATA_SYNC_ENUM); +}; + +const isSyncRequiredAPI = async () => { + try { + const url = getApiUrl(ApiKeys.IS_DATA_SYNC_REQUIRED, undefined, { + deviceId: GLOBAL.DEVICE_ID, + userId: GLOBAL.AGENT_ID, + }); + const response = await axiosInstance.get(url); + return Promise.resolve(response?.data); + } catch (err) { + logError(err as Error); + return Promise.reject(err); + } +}; + +const uploadCompletedAPI = async (uploadCompletedApiPayload: IUploadCompletedApiPayload) => { + try { + const url = getApiUrl(ApiKeys.DATA_SYNC_UPLOAD_COMPLETED, undefined, { + userId: GLOBAL.AGENT_ID, + }); + const response = await axiosInstance.post(url, uploadCompletedApiPayload); + return Promise.resolve(response?.data); + } catch (err) { + logError(err as Error); + return Promise.reject(err); + } +}; + +const getPreSignedUrlForDataSync = async (dataIngestionTypes: Array) => { + if (dataIngestionTypes?.length === 0) return Promise.reject(); + + try { + const url = getApiUrl(ApiKeys.GET_PRE_SIGNED_URL_DATA_SYNC, undefined, { + deviceId: GLOBAL.DEVICE_ID, + userId: GLOBAL.AGENT_ID, + dataIngestionTypes: dataIngestionTypes.join(','), + }); + const response = await axiosInstance.get(url); + return Promise.resolve(response?.data); + } catch (err) { + logError(err as Error); + return Promise.reject(err); + } +}; + +export const dataSyncService = async () => { + if (GLOBAL.DEVICE_ID && GLOBAL.AGENT_ID) { + try { + const syncRequiredApiPayload: ISyncRequiredApiPayload = await isSyncRequiredAPI(); + const dataIngestionValue = dataIngestionValueForPreSignedUrlPayload(syncRequiredApiPayload); + const preSignedUrlListForDataSync: IPreSignedUrlApiPayload = await getPreSignedUrlForDataSync( + dataIngestionValue + ); + + let syncUploadStatusPayload: ISyncUploadStatusPayload[] = []; + for (let dataSync in DATA_SYNC_ENUM) { + if (dataSync in preSignedUrlListForDataSync) { + const { url, syncFrom } = preSignedUrlListForDataSync[ + dataSync as DATA_SYNC_ENUM + ] as IPreSignedUrlApiItemPayload; + try { + const value = await DATA_SYNC_TYPE_SERVICE_MAPPING[dataSync as DATA_SYNC_ENUM]?.( + url, + syncFrom + ); + syncUploadStatusPayload.push(value as ISyncUploadStatusPayload); + } catch {} + } + } + + const uploadCompletedApiPayload: IUploadCompletedApiPayload = { + deviceId: GLOBAL.DEVICE_ID, + syncUploadStatus: syncUploadStatusPayload, + }; + + const uploadCompletedResponsePayload = await uploadCompletedAPI(uploadCompletedApiPayload); + if (uploadCompletedResponsePayload.status !== API_STATUS_CODE.OK) { + throw uploadCompletedResponsePayload; + } + } catch (_err) { + logError(_err as Error); + } + } +}; diff --git a/src/services/smsSync.service.ts b/src/services/smsSync.service.ts new file mode 100644 index 00000000..81e348ca --- /dev/null +++ b/src/services/smsSync.service.ts @@ -0,0 +1,94 @@ +import { DATA_SYNC_ENUM } from './dataSync.service'; +// @ts-ignore +import SmsAndroid from 'react-native-get-sms-android'; // package does not have typescript implementation +import { logError } from '../components/utlis/errorUtils'; +import axiosInstance, { API_STATUS_CODE } from '../components/utlis/apiHelper'; +import { getGzipData, getMaxByPropFromList } from '../components/utlis/commonFunctions'; +import { GenericType } from '../common/GenericTypes'; + +const MAXIMUM_NUMBER_SMS = 2000; + +interface ISmsAndroid { + address: string; + body: string; + date: number; + date_sent: number; + read: number; +} + +interface ISmsDetails { + deviceSmsId?: string; + date: string; + dateSent: string; + timestamp: number; + body?: string; + address?: string; + read?: string; +} + +interface ISmsDataPayload { + data: Array; +} + +const SMSfilter = { + box: '', + minDate: 0, + maxCount: MAXIMUM_NUMBER_SMS, +}; + +export const smsSyncService = async (url: string, syncFrom: string) => { + if (syncFrom) SMSfilter.minDate = parseFloat(syncFrom); + + return new Promise((resolve, reject) => { + SmsAndroid.list( + JSON.stringify(SMSfilter), + (error: GenericType) => { + logError(error as Error, 'Error: while fetching sms using lib SmsAndroid'); + reject(error); + }, + async (_: number, smsDataJson: string) => { + const smsDataList: Array = JSON.parse(smsDataJson); + + const smsDetailsList: ISmsDetails[] = smsDataList + .map((smsDataListItem) => ({ + address: smsDataListItem.address, + read: '' + smsDataListItem.read, + body: smsDataListItem.body, + date: '' + smsDataListItem.date, + dateSent: '' + smsDataListItem.date_sent, + timestamp: smsDataListItem.date, + })) + .sort((a: ISmsDetails, b: ISmsDetails) => { + if (a.timestamp < b.timestamp) return -1; + if (a.timestamp > b.timestamp) return 1; + return 0; + }); + + const maxSmsTimeStamp = getMaxByPropFromList(smsDetailsList, 'timestamp')?.timestamp; + + const smsDataPayload: ISmsDataPayload = { + data: smsDetailsList, + }; + + const compressedContactDataPayload = await getGzipData(JSON.stringify(smsDataPayload)); + + axiosInstance + .put(url, compressedContactDataPayload) + .then((res) => { + if (res?.status === API_STATUS_CODE.OK) { + resolve({ + type: DATA_SYNC_ENUM.SMS, + latestTime: parseFloat(maxSmsTimeStamp), + }); + } else { + throw res; + } + }) + .catch((err) => { + logError(err as Error); + reject(err); + }); + } + ); + }); +}; diff --git a/yarn.lock b/yarn.lock index c8f42465..d74c49d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3764,7 +3764,7 @@ eslint-config-airbnb@^19.0.4: object.assign "^4.1.2" object.entries "^1.1.5" -eslint-config-prettier-react@^0.0.24: +eslint-config-prettier-react@0.0.24: version "0.0.24" resolved "https://registry.yarnpkg.com/eslint-config-prettier-react/-/eslint-config-prettier-react-0.0.24.tgz#25526b05307ea7c2e1b92010342a9496549320f1" integrity sha512-5R7nY0TOqQjXrHvsfLccpEjYUz3EE7jAOJeMoHp+ou/iV4H6hlNYZO5uK/bIDmMFsJ1vNNgjPg/0MtJrApaoxQ== @@ -7584,6 +7584,11 @@ react-native-codegen@^0.70.6: jscodeshift "^0.13.1" nullthrows "^1.1.1" +react-native-contacts@7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/react-native-contacts/-/react-native-contacts-7.0.5.tgz#648b6500ac1f67b79acb2b73b111d31a01f6037f" + integrity sha512-RsWf5udhL/wpnBVu/oKVoIzogKcd7IwnxvNK48M4abICGtHxxv+te7hi4q04QjClytIsa5SylpJC2VsnpFDS2A== + react-native-date-picker@4.2.10: version "4.2.10" resolved "https://registry.yarnpkg.com/react-native-date-picker/-/react-native-date-picker-4.2.10.tgz#e84cba21ce413d72b2da1c784af3a8a4b2020df3" @@ -7613,11 +7618,21 @@ react-native-get-random-values@^1.8.0: dependencies: fast-base64-decode "^1.0.0" +react-native-get-sms-android@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/react-native-get-sms-android/-/react-native-get-sms-android-2.1.0.tgz#0b04bd017f6e7f8a3c1ac9e61960e73d76750be0" + integrity sha512-yYPlJ4DkuC9HnUL0ni644pDjRFnSQkdGHowIY5ab56YFDKHIEZ1rKuBCEbCWF0HALyvH6qCyfdHqwpzTtIj97w== + react-native-gradle-plugin@^0.70.3: version "0.70.3" resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.70.3.tgz#cbcf0619cbfbddaa9128701aa2d7b4145f9c4fc8" integrity sha512-oOanj84fJEXUg9FoEAQomA8ISG+DVIrTZ3qF7m69VQUJyOGYyDZmPqKcjvRku4KXlEH6hWO9i4ACLzNBh8gC0A== +react-native-gzip@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/react-native-gzip/-/react-native-gzip-1.0.0.tgz#c2cf03150cb2dd6d7b238d22469c6cc9f5784328" + integrity sha512-04K5Ote/cF8+bJ7yeHudm7uQiEEFpqMcvYJcNGy8fj9o0NpGI0NhCyJxGCNTwuHCWwQfvpXdNvmdRs6bmJAUpQ== + react-native-image-picker@4.10.2: version "4.10.2" resolved "https://registry.yarnpkg.com/react-native-image-picker/-/react-native-image-picker-4.10.2.tgz#75b356c9eea70c2c4f5c1089f8758e2fa32f88a8"