Skip to content

Commit

Permalink
Merge pull request #26546 from huzaifa-99/25855-get-user-location
Browse files Browse the repository at this point in the history
Added option to get user's current location
  • Loading branch information
tgolen authored Oct 5, 2023
2 parents 49da40c + c226384 commit 1c1d8b1
Show file tree
Hide file tree
Showing 25 changed files with 9,789 additions and 514 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ USE_WEB_PROXY=false
USE_WDYR=false
CAPTURE_METRICS=false
ONYX_METRICS=false
GOOGLE_GEOLOCATION_API_KEY=AIzaSyBqg6bMvQU7cPWDKhhzpYqJrTEnSorpiLI

EXPENSIFY_ACCOUNT_ID_ACCOUNTING=-1
EXPENSIFY_ACCOUNT_ID_ADMIN=-1
Expand Down
1 change: 1 addition & 0 deletions .env.production
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ PUSHER_APP_KEY=268df511a204fbb60884
USE_WEB_PROXY=false
ENVIRONMENT=production
SEND_CRASH_REPORTS=true
GOOGLE_GEOLOCATION_API_KEY=AIzaSyBFKujMpzExz0_z2pAGfPUwkmlaUc-uw1Q
1 change: 1 addition & 0 deletions .env.staging
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ PUSHER_APP_KEY=268df511a204fbb60884
USE_WEB_PROXY=false
ENVIRONMENT=staging
SEND_CRASH_REPORTS=true
GOOGLE_GEOLOCATION_API_KEY=AIzaSyD2T1mlByThbUN88O8OPOD8vKuMMwLD4-M
1 change: 1 addition & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.HIGH_SAMPLING_RATE_SENSORS"/>

<!-- android:hardwareAccelerated is essential for Android performance: https://developer.android.com/topic/performance/hardware-accel -->
Expand Down
5 changes: 5 additions & 0 deletions desktop/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ const Localize = require('../src/libs/Localize');
const port = process.env.PORT || 8082;
const {DESKTOP_SHORTCUT_ACCELERATOR, LOCALES} = CONST;

// Setup google api key in process environment, we are setting it this way intentionally. It is required by the
// geolocation api (window.navigator.geolocation.getCurrentPosition) to work on desktop.
// Source: https://github.com/electron/electron/blob/98cd16d336f512406eee3565be1cead86514db7b/docs/api/environment-variables.md#google_api_key
process.env.GOOGLE_API_KEY = CONFIG.GOOGLE_GEOLOCATION_API_KEY;

app.setName('New Expensify');

/**
Expand Down
6 changes: 6 additions & 0 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,8 @@ PODS:
- React-Core
- react-native-flipper (0.159.0):
- React-Core
- react-native-geolocation (3.0.6):
- React-Core
- react-native-image-manipulator (1.0.5):
- React
- react-native-image-picker (5.1.0):
Expand Down Expand Up @@ -892,6 +894,7 @@ DEPENDENCIES:
- react-native-config (from `../node_modules/react-native-config`)
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
- react-native-flipper (from `../node_modules/react-native-flipper`)
- "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)"
- "react-native-image-manipulator (from `../node_modules/@oguzhnatly/react-native-image-manipulator`)"
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
- react-native-key-command (from `../node_modules/react-native-key-command`)
Expand Down Expand Up @@ -1065,6 +1068,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-document-picker"
react-native-flipper:
:path: "../node_modules/react-native-flipper"
react-native-geolocation:
:path: "../node_modules/@react-native-community/geolocation"
react-native-image-manipulator:
:path: "../node_modules/@oguzhnatly/react-native-image-manipulator"
react-native-image-picker:
Expand Down Expand Up @@ -1249,6 +1254,7 @@ SPEC CHECKSUMS:
react-native-config: 7cd105e71d903104e8919261480858940a6b9c0e
react-native-document-picker: f68191637788994baed5f57d12994aa32cf8bf88
react-native-flipper: dc5290261fbeeb2faec1bdc57ae6dd8d562e1de4
react-native-geolocation: 0f7fe8a4c2de477e278b0365cce27d089a8c5903
react-native-image-manipulator: c48f64221cfcd46e9eec53619c4c0374f3328a56
react-native-image-picker: c33d4e79f0a14a2b66e5065e14946ae63749660b
react-native-key-command: c2645ec01eb1fa664606c09480c05cb4220ef67b
Expand Down
9,809 changes: 9,300 additions & 509 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"@react-native-camera-roll/camera-roll": "5.4.0",
"@react-native-community/clipboard": "^1.5.1",
"@react-native-community/datetimepicker": "^3.5.2",
"@react-native-community/geolocation": "^3.0.6",
"@react-native-community/netinfo": "^9.3.10",
"@react-native-firebase/analytics": "^12.3.0",
"@react-native-firebase/app": "^12.3.0",
Expand Down Expand Up @@ -116,6 +117,7 @@
"react-map-gl": "^7.1.3",
"react-error-boundary": "^4.0.11",
"react-native": "0.72.4",
"react-native-android-location-enabler": "^1.2.2",
"react-native-blob-util": "^0.17.3",
"react-native-collapsible": "^1.6.0",
"react-native-config": "^1.4.5",
Expand Down
2 changes: 2 additions & 0 deletions src/CONFIG.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const secureExpensifyUrl = Url.addTrailingForwardSlash(get(Config, 'SECURE_EXPEN
const useNgrok = get(Config, 'USE_NGROK', 'false') === 'true';
const useWebProxy = get(Config, 'USE_WEB_PROXY', 'true') === 'true';
const expensifyComWithProxy = getPlatform() === 'web' && useWebProxy ? '/' : expensifyURL;
const googleGeolocationAPIKey = get(Config, 'GOOGLE_GEOLOCATION_API_KEY', 'AIzaSyBqg6bMvQU7cPWDKhhzpYqJrTEnSorpiLI');

// Throw errors on dev if config variables are not set correctly
if (ENVIRONMENT === CONST.ENVIRONMENT.DEV) {
Expand Down Expand Up @@ -91,4 +92,5 @@ export default {
WEB_CLIENT_ID: '921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com',
IOS_CLIENT_ID: '921154746561-s3uqn2oe4m85tufi6mqflbfbuajrm2i3.apps.googleusercontent.com',
},
GOOGLE_GEOLOCATION_API_KEY: googleGeolocationAPIKey,
} as const;
2 changes: 2 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,8 @@ const CONST = {
DEFAULT: 'default',
},

YOUR_LOCATION_TEXT: 'Your Location',

ATTACHMENT_MESSAGE_TEXT: '[Attachment]',
// This is a placeholder for attachment which is uploading
ATTACHMENT_UPLOADING_MESSAGE_HTML: 'Uploading attachment...',
Expand Down
4 changes: 2 additions & 2 deletions src/components/AddressSearch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,8 +344,8 @@ function AddressSearch(props) {
props.onInputChange({street: text});
}

// If the text is empty, we set displayListViewBorder to false to prevent UI flickering
if (_.isEmpty(text)) {
// If the text is empty and we have no predefined places, we set displayListViewBorder to false to prevent UI flickering
if (_.isEmpty(text) && _.isEmpty(props.predefinedPlaces)) {
setDisplayListViewBorder(false);
}
},
Expand Down
77 changes: 77 additions & 0 deletions src/components/LocationErrorMessage/BaseLocationErrorMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
import CONST from '../../CONST';
import colors from '../../styles/colors';
import styles from '../../styles/styles';
import Icon from '../Icon';
import * as Expensicons from '../Icon/Expensicons';
import PressableWithoutFeedback from '../Pressable/PressableWithoutFeedback';
import Text from '../Text';
import TextLink from '../TextLink';
import Tooltip from '../Tooltip';
import withLocalize, {withLocalizePropTypes} from '../withLocalize';
import * as locationErrorMessagePropTypes from './locationErrorMessagePropTypes';

const propTypes = {
/** A callback that runs when 'allow location permission' link is pressed */
onAllowLocationLinkPress: PropTypes.func.isRequired,

// eslint-disable-next-line react/forbid-foreign-prop-types
...locationErrorMessagePropTypes.propTypes,

/* Onyx Props */
...withLocalizePropTypes,
};

function BaseLocationErrorMessage({onClose, onAllowLocationLinkPress, locationErrorCode, translate}) {
if (!locationErrorCode) {
return null;
}

const isPermissionDenied = locationErrorCode === 1;

return (
<View style={[styles.dotIndicatorMessage, styles.mt4]}>
<View style={styles.offlineFeedback.errorDot}>
<Icon
src={Expensicons.DotIndicator}
fill={colors.red}
/>
</View>
<View style={styles.offlineFeedback.textContainer}>
{isPermissionDenied ? (
<Text>
<Text style={[styles.offlineFeedback.text]}>{`${translate('location.permissionDenied')} ${translate('location.please')}`}</Text>
<TextLink
onPress={onAllowLocationLinkPress}
style={styles.locationErrorLinkText}
>
{` ${translate('location.allowPermission')} `}
</TextLink>
<Text style={[styles.offlineFeedback.text]}>{translate('location.tryAgain')}</Text>
</Text>
) : (
<Text style={styles.offlineFeedback.text}>{translate('location.notFound')}</Text>
)}
</View>
<View>
<Tooltip text={translate('common.close')}>
<PressableWithoutFeedback
onPress={onClose}
style={[styles.touchableButtonImage]}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON}
accessibilityLabel={translate('common.close')}
>
<Icon src={Expensicons.Close} />
</PressableWithoutFeedback>
</Tooltip>
</View>
</View>
);
}

BaseLocationErrorMessage.displayName = 'BaseLocationErrorMessage';
BaseLocationErrorMessage.propTypes = propTypes;
BaseLocationErrorMessage.defaultProps = locationErrorMessagePropTypes.defaultProps;
export default withLocalize(BaseLocationErrorMessage);
25 changes: 25 additions & 0 deletions src/components/LocationErrorMessage/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import {Linking} from 'react-native';
import CONST from '../../CONST';
import BaseLocationErrorMessage from './BaseLocationErrorMessage';
import * as locationErrorMessagePropTypes from './locationErrorMessagePropTypes';

/** Opens expensify help site in a new browser tab */
const navigateToExpensifyHelpSite = () => {
Linking.openURL(CONST.NEWHELP_URL);
};

function LocationErrorMessage(props) {
return (
<BaseLocationErrorMessage
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
onAllowLocationLinkPress={navigateToExpensifyHelpSite}
/>
);
}

LocationErrorMessage.displayName = 'LocationErrorMessage';
LocationErrorMessage.propTypes = locationErrorMessagePropTypes.propTypes;
LocationErrorMessage.defaultProps = locationErrorMessagePropTypes.defaultProps;
export default LocationErrorMessage;
24 changes: 24 additions & 0 deletions src/components/LocationErrorMessage/index.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import {Linking} from 'react-native';
import BaseLocationErrorMessage from './BaseLocationErrorMessage';
import * as locationErrorMessagePropTypes from './locationErrorMessagePropTypes';

/** Opens app level settings from the native system settings */
const openAppSettings = () => {
Linking.openSettings();
};

function LocationErrorMessage(props) {
return (
<BaseLocationErrorMessage
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
onAllowLocationLinkPress={openAppSettings}
/>
);
}

LocationErrorMessage.displayName = 'LocationErrorMessage';
LocationErrorMessage.propTypes = locationErrorMessagePropTypes.propTypes;
LocationErrorMessage.defaultProps = locationErrorMessagePropTypes.defaultProps;
export default LocationErrorMessage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import PropTypes from 'prop-types';

const propTypes = {
/** A callback that runs when close icon is pressed */
onClose: PropTypes.func.isRequired,

/**
* The location error code from onyx
* - code -1 = location not supported (web only)
* - code 1 = location permission is not enabled
* - code 2 = location is unavailable or there is some connection issue
* - code 3 = location fetch timeout
*/
locationErrorCode: PropTypes.oneOf([-1, 1, 2, 3]),
};

const defaultProps = {
locationErrorCode: null,
};

export {propTypes, defaultProps};
112 changes: 112 additions & 0 deletions src/components/UserCurrentLocationButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import PropTypes from 'prop-types';
import React, {useEffect, useRef, useState} from 'react';
import {Text} from 'react-native';
import getCurrentPosition from '../libs/getCurrentPosition';
import styles from '../styles/styles';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import LocationErrorMessage from './LocationErrorMessage';
import withLocalize, {withLocalizePropTypes} from './withLocalize';
import colors from '../styles/colors';
import PressableWithFeedback from './Pressable/PressableWithFeedback';

const propTypes = {
/** Callback that runs when location data is fetched */
onLocationFetched: PropTypes.func.isRequired,

/** Callback that runs when fetching location has errors */
onLocationError: PropTypes.func,

/** Callback that runs when location button is clicked */
onClick: PropTypes.func,

/** Boolean to indicate if the button is clickable */
isDisabled: PropTypes.bool,

...withLocalizePropTypes,
};

const defaultProps = {
isDisabled: false,
onLocationError: () => {},
onClick: () => {},
};

function UserCurrentLocationButton({onLocationFetched, onLocationError, onClick, isDisabled, translate}) {
const isFetchingLocation = useRef(false);
const shouldTriggerCallbacks = useRef(true);
const [locationErrorCode, setLocationErrorCode] = useState(null);

/** Gets the user's current location and registers success/error callbacks */
const getUserLocation = () => {
if (isFetchingLocation.current) {
return;
}

isFetchingLocation.current = true;

onClick();

getCurrentPosition(
(successData) => {
isFetchingLocation.current = false;
if (!shouldTriggerCallbacks.current) {
return;
}

setLocationErrorCode(null);
onLocationFetched(successData);
},
(errorData) => {
isFetchingLocation.current = false;
if (!shouldTriggerCallbacks.current) {
return;
}

setLocationErrorCode(errorData.code);
onLocationError(errorData);
},
{
maximumAge: 0, // No cache, always get fresh location info
timeout: 5000,
},
);
};

// eslint-disable-next-line arrow-body-style
useEffect(() => {
return () => {
// If the component unmounts we don't want any of the callback for geolocation to run.
shouldTriggerCallbacks.current = false;
};
}, []);

return (
<>
<PressableWithFeedback
style={[styles.flexRow, styles.mt4, styles.alignSelfStart, isDisabled && styles.buttonOpacityDisabled]}
onPress={getUserLocation}
accessibilityLabel={translate('location.useCurrent')}
disabled={isDisabled}
>
<Icon
src={Expensicons.Location}
fill={colors.green}
/>
<Text style={[styles.textLabel, styles.mh2, isDisabled && styles.userSelectNone]}>{translate('location.useCurrent')}</Text>
</PressableWithFeedback>
<LocationErrorMessage
onClose={() => setLocationErrorCode(null)}
locationErrorCode={locationErrorCode}
/>
</>
);
}

UserCurrentLocationButton.displayName = 'UserCurrentLocationButton';
UserCurrentLocationButton.propTypes = propTypes;
UserCurrentLocationButton.defaultProps = defaultProps;

// This components gets used inside <Form/>, we are using an HOC (withLocalize) as function components with
// hooks give hook errors when nested inside <Form/>.
export default withLocalize(UserCurrentLocationButton);
Loading

0 comments on commit 1c1d8b1

Please sign in to comment.