diff --git a/App.tsx b/App.tsx index ef759204..ded6be88 100644 --- a/App.tsx +++ b/App.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { KeyboardAvoidingView,LogBox } from 'react-native'; +import { LogBox, PermissionsAndroid, Platform , KeyboardAvoidingView, PermissionStatus} from 'react-native'; import {Provider} from 'react-redux'; import store, {persistor} from './src/store/store'; import {PersistGate} from 'redux-persist/integration/react'; @@ -8,7 +8,7 @@ import {NavigationContainer} from '@react-navigation/native'; import {navigationRef} from './src/components/utlis/navigationUtlis'; import FullScreenLoader from './RN-UI-LIB/src/components/FullScreenLoader'; import ProtectedRouter from './ProtectedRouter'; -import { toastConfigs, ToastContainer } from './RN-UI-LIB/src/components/toast'; +import { toast, toastConfigs, ToastContainer } from './RN-UI-LIB/src/components/toast'; import ErrorBoundary from './src/common/ErrorBoundary'; @@ -20,6 +20,8 @@ import { COLORS } from './RN-UI-LIB/src/styles/colors'; import codePush from 'react-native-code-push'; import { ENV } from './src/constants/config'; import { mockApiServer } from './src/mock-api/server'; +import Text from './RN-UI-LIB/src/components/Text'; +import { PermissionStatusEnum } from './src/services/geolocation.service'; Sentry.init({ dsn: SENTRY_DSN }); if (ENV !== 'prod') { @@ -28,8 +30,38 @@ if (ENV !== 'prod') { LogBox.ignoreAllLogs(); + +const askForPermissions = (setPermissions:React.Dispatch> ) => { + if (Platform.OS === 'android') { + PermissionsAndroid.requestMultiple( + [PermissionsAndroid.PERMISSIONS.CAMERA, + PermissionsAndroid.PERMISSIONS.READ_CONTACTS, + PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION, + PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION, + PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE, + PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE] + ).then((result) => { + if (result['android.permission.ACCESS_COARSE_LOCATION'] + && result['android.permission.CAMERA'] + && result['android.permission.READ_CONTACTS'] + && result['android.permission.ACCESS_FINE_LOCATION'] + && result['android.permission.READ_EXTERNAL_STORAGE'] + && result['android.permission.WRITE_EXTERNAL_STORAGE'] === PermissionStatusEnum.GRANTED) { + setPermissions(true) + } else { + toast({type:'info', text1:'Please Go into Settings -> Applications -> Field App -> Permissions and Allow permissions to continue'}); + setPermissions(false) + } + }); + } +} + const App = () => { useNativeButtons(); + const [permissions, setPermissions] = React.useState(true); + React.useEffect(()=>{ + askForPermissions(setPermissions); + }, []) return ( { - + {/* TODO: ASK Adhya for designs its been more than 1 month */} + {permissions ? : Please grant Permisisons} { diff --git a/package.json b/package.json index 78fc2379..f50de454 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "react-native": "0.70.6", "react-native-code-push": "7.1.0", "react-native-device-info": "10.3.0", + "react-native-geolocation-service": "5.3.1", "react-native-image-picker": "4.10.2", "react-native-pager-view": "6.1.2", "react-native-permissions": "3.6.1", diff --git a/src/components/form/index.tsx b/src/components/form/index.tsx index 18fd309e..48bb314e 100644 --- a/src/components/form/index.tsx +++ b/src/components/form/index.tsx @@ -4,11 +4,10 @@ import { ScrollView, StyleSheet, TouchableHighlight, - View, + View } from 'react-native'; +import Geolocation from 'react-native-geolocation-service'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { useNavigationState } from '@react-navigation/native'; -import { useDispatch } from 'react-redux'; import Button from '../../../RN-UI-LIB/src/components/Button'; import Heading from '../../../RN-UI-LIB/src/components/Heading'; import Text from '../../../RN-UI-LIB/src/components/Text'; @@ -16,22 +15,24 @@ import ArrowSolidIcon from '../../../RN-UI-LIB/src/Icons/ArrowSolidIcon'; import CloseIcon from '../../../RN-UI-LIB/src/Icons/CloseIcon'; import { GenericStyles, getShadowStyle } from '../../../RN-UI-LIB/src/styles'; import { COLORS } from '../../../RN-UI-LIB/src/styles/colors'; -import { useAppDispatch, useAppSelector } from '../../hooks'; -import { getUpdatedAVCaseDetail, getUpdatedCollectionCaseDetail, updateCaseDetail } from '../../reducer/allCasesSlice'; -import { deleteInteraction, deleteJourney, updateInteraction, } from '../../reducer/caseReducer'; -import { getTemplateRoute, getWidgetNameFromRoute, goBack, navigateToScreen, } from '../utlis/navigationUtlis'; -import RenderQuestion from './RenderQuestion'; -import Submit from './Submit'; -import { addClickstreamEvent } from '../../services/clickstreamEventService'; -import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants'; -import Layout from '../../screens/layout/Layout'; -import { getNextJourneyActions, getNextWidget } from "./services/forms.service"; -import { FormTemplateV1 } from "../../types/template.types"; -import { CaseAllocationType } from "../../screens/allCases/interface"; -import useIsOnline from '../../hooks/useIsOnline'; -import { getUnSyncedCase } from '../../screens/caseDetails/interactionsHandler'; -import { getTransformedAvCase, getTransformedCollectionCaseItem } from '../../services/casePayload.transformer'; import { syncCaseDetail } from '../../action/dataActions'; +import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants'; +import { useAppDispatch, useAppSelector } from '../../hooks'; +import useIsOnline from '../../hooks/useIsOnline'; +import { getUpdatedAVCaseDetail, getUpdatedCollectionCaseDetail, updateCaseDetail } from '../../reducer/allCasesSlice'; +import { deleteInteraction, deleteJourney, updateInteraction } from '../../reducer/caseReducer'; +import { CaseAllocationType } from "../../screens/allCases/interface"; +import { getUnSyncedCase } from '../../screens/caseDetails/interactionsHandler'; +import Layout from '../../screens/layout/Layout'; +import { getTransformedAvCase, getTransformedCollectionCaseItem } from '../../services/casePayload.transformer'; +import { addClickstreamEvent } from '../../services/clickstreamEventService'; +import { FormTemplateV1 } from "../../types/template.types"; +import { logError } from '../utlis/errorUtils'; +import { getTemplateRoute, getWidgetNameFromRoute, goBack, navigateToScreen } from '../utlis/navigationUtlis'; +import RenderQuestion from './RenderQuestion'; +import { getNextJourneyActions, getNextWidget } from "./services/forms.service"; +import { requestLocationPermission } from './services/geoLocation.service'; +import Submit from './Submit'; import { toast } from '../../../RN-UI-LIB/src/components/toast'; interface IWidget { @@ -134,11 +135,27 @@ const Widget: React.FC = props => { ); } - const handleSubmitJourney = async(data: any) => { + const submitJourneyWithGeoLocation = (data: any) => { addClickstreamEvent( CLICKSTREAM_EVENT_NAMES.AV_FORM_SUBMIT_BUTTON_CLICKED, { caseId, journeyId: journey, widgetId: name }, ); + const isLocationOn = requestLocationPermission(); + if(!isLocationOn) return; + Geolocation.getCurrentPosition( + position => { + return handleSubmitJourney(data, position.coords); + }, + error => { + logError((error as any), "Unable to get location") + return; + }, + { enableHighAccuracy: true, timeout: 15e3, maximumAge: 1e4 }, + ); + + } + + const handleSubmitJourney = async(data: any, coords: Geolocation.GeoCoordinates) => { dispatch( updateInteraction({ caseId, @@ -146,9 +163,9 @@ const Widget: React.FC = props => { widgetId: name, answer: data, }), - ); + ); if (caseType === CaseAllocationType.COLLECTION_CASE) { - const updatedCase = getUpdatedCollectionCaseDetail({caseData, answer: data}); + const updatedCase = getUpdatedCollectionCaseDetail({caseData, answer: data, coords}); if(isOnline) { setShowSubmitLoader(true); const unsyncedCase = getUnSyncedCase(updatedCase); @@ -196,6 +213,7 @@ const Widget: React.FC = props => { caseData, nextActions, templateId, + coords }); if(isOnline) { @@ -373,7 +391,7 @@ const Widget: React.FC = props => { showLoader={isLeaf && showSubmitLoader} onPress={ isLeaf - ? handleSubmit(handleSubmitJourney, onError) + ? handleSubmit(submitJourneyWithGeoLocation, onError) : handleSubmit(onSubmit, onError) } rightIcon={ diff --git a/src/components/form/services/geoLocation.service.ts b/src/components/form/services/geoLocation.service.ts new file mode 100644 index 00000000..f71f32c9 --- /dev/null +++ b/src/components/form/services/geoLocation.service.ts @@ -0,0 +1,19 @@ +import { PermissionsAndroid } from "react-native"; + +export const requestLocationPermission = async () => { + try { + const granted = await PermissionsAndroid.request( + PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION, + { + title: 'Geolocation Permission', + message: 'Can we access your location?', + buttonNeutral: 'Ask Me Later', + buttonNegative: 'Cancel', + buttonPositive: 'OK', + }, + ); + return granted === 'granted'; + } catch (err) { + return false; + } +}; diff --git a/src/reducer/allCasesSlice.ts b/src/reducer/allCasesSlice.ts index eb0ed3d2..f607e05d 100644 --- a/src/reducer/allCasesSlice.ts +++ b/src/reducer/allCasesSlice.ts @@ -66,7 +66,24 @@ const initialState: IAllCasesSlice = { visitPlansUpdating: false, }; -export const getUpdatedCollectionCaseDetail = ({caseData, answer}: any) => { +const getPinnedListDetails = (casesList: ICaseItem[]) => { + let maxPinnedRank = 0; + const pinnedList: ICaseItem[] = []; + casesList.forEach(caseItem => { + const { pinRank } = caseItem; + if (pinRank === null || pinRank === undefined) { + return; + } + pinnedList.push(caseItem); + if (pinRank > maxPinnedRank) { + maxPinnedRank = pinRank; + } + }); + return { pinnedList, maxPinnedRank }; +}; + +export const getUpdatedCollectionCaseDetail = ({caseData, answer, coords}: any) => { + const updatedValue = { ...caseData }; updatedValue.isSynced = false; updatedValue.isApiCalled = false; @@ -83,10 +100,11 @@ export const getUpdatedCollectionCaseDetail = ({caseData, answer}: any) => { allocationReferenceId: answer.allocationReferenceId, }; updatedValue.answer = newAnswer; + updatedValue.coords = coords; return updatedValue; }; -export const getUpdatedAVCaseDetail = ({journeyId, answer, caseData, nextActions, templateId}: any) => { +export const getUpdatedAVCaseDetail = ({journeyId, answer, caseData, nextActions, templateId, coords}: any) => { const updatedValue = { ...caseData }; updatedValue.isSynced = false; updatedValue.isApiCalled = false; @@ -170,6 +188,7 @@ export const getUpdatedAVCaseDetail = ({journeyId, answer, caseData, nextActions updatedValue.caseStatus = CaseStatuses.CLOSED; updatedValue.caseVerdict = caseVerdict.EXHAUSTED; } + updatedValue.coords = coords; return updatedValue; }; diff --git a/src/services/casePayload.transformer.ts b/src/services/casePayload.transformer.ts index cbd37e05..dd22b495 100644 --- a/src/services/casePayload.transformer.ts +++ b/src/services/casePayload.transformer.ts @@ -2,6 +2,7 @@ import { CaseDetail } from './../screens/caseDetails/interface'; import { AnswerType } from '../components/form/interface'; import OfflineImageDAO from '../wmDB/dao/OfflineImageDAO'; import { CaseAllocationType } from '../screens/allCases/interface'; +import Geolocation from 'react-native-geolocation-service'; interface QuestionContext { answer: string; @@ -129,10 +130,10 @@ export const getImageFromOfflineDb = async (imageId: string) => { export interface IGetTransformedCaseItem extends CaseDetail { answer: any; caseId: string; + coords: Geolocation.GeoCoordinates } export const getTransformedCollectionCaseItem = async ( - caseItem: IGetTransformedCaseItem, -) => { + caseItem: IGetTransformedCaseItem) => { let cloneCaseItem = { ...caseItem }; let answerContextArray = await extractQuestionContext( cloneCaseItem?.answer, @@ -140,13 +141,13 @@ export const getTransformedCollectionCaseItem = async ( let data = { caseReferenceId: caseItem.caseReferenceId, answers: answerContextArray, - location: undefined, + location: cloneCaseItem?.coords, }; return { caseType: caseItem.caseType, data }; }; export const getTransformedAvCase = async(caseItem: IGetTransformedCaseItem, templateId: string) => { - const { caseType, allocationReferenceId, caseId } = caseItem; + const { caseType, allocationReferenceId, caseId, coords } = caseItem; const transformedAvCase: any = { caseType: caseType || CaseAllocationType.ADDRESS_VERIFICATION_CASE, data: { @@ -155,6 +156,7 @@ export const getTransformedAvCase = async(caseItem: IGetTransformedCaseItem, tem caseReferenceId: caseId, taskStatuses: {}, answers: [], + location: coords, } } const taskContext = caseItem?.context?.taskContext; diff --git a/src/services/geolocation.service.ts b/src/services/geolocation.service.ts index 3f7f5649..949eee5d 100644 --- a/src/services/geolocation.service.ts +++ b/src/services/geolocation.service.ts @@ -6,7 +6,7 @@ import { logError } from '../components/utlis/errorUtils'; const { RNFusedLocation } = NativeModules; -enum PermissionStatusEnum { +export enum PermissionStatusEnum { GRANTED = 'granted', DENIED = 'denied', NEVER_ASK = 'never_ask_again' diff --git a/yarn.lock b/yarn.lock index b2d5a967..a8fe630a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7255,6 +7255,11 @@ react-native-device-info@10.3.0: resolved "https://registry.yarnpkg.com/react-native-device-info/-/react-native-device-info-10.3.0.tgz#6bab64d84d3415dd00cc446c73ec5e2e61fddbe7" integrity sha512-/ziZN1sA1REbJTv5mQZ4tXggcTvSbct+u5kCaze8BmN//lbxcTvWsU6NQd4IihLt89VkbX+14IGc9sVApSxd/w== +react-native-geolocation-service@5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/react-native-geolocation-service/-/react-native-geolocation-service-5.3.1.tgz#4ce1017789da6fdfcf7576eb6f59435622af4289" + integrity sha512-LTXPtPNmrdhx+yeWG47sAaCgQc3nG1z+HLLHlhK/5YfOgfLcAb9HAkhREPjQKPZOUx8pKZMIpdGFUGfJYtimXQ== + 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"