diff --git a/.well-known/apple-app-site-association b/.well-known/apple-app-site-association
index cf6bdf1dedef..9274dd8c1382 100644
--- a/.well-known/apple-app-site-association
+++ b/.well-known/apple-app-site-association
@@ -32,6 +32,10 @@
"/": "/iou/*",
"comment": "I Owe You reports"
},
+ {
+ "/": "/request/*",
+ "comment": "Money request"
+ },
{
"/": "/enable-payments/*",
"comment": "Payments setup"
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index f7b50f62369c..f1c7f65757d6 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -60,6 +60,7 @@
+
@@ -75,6 +76,7 @@
+
diff --git a/assets/images/dot-indicator-unfilled.svg b/assets/images/dot-indicator-unfilled.svg
new file mode 100644
index 000000000000..ae131b1c2cba
--- /dev/null
+++ b/assets/images/dot-indicator-unfilled.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/assets/images/drag-handles.svg b/assets/images/drag-handles.svg
new file mode 100644
index 000000000000..ec4fc4ccc672
--- /dev/null
+++ b/assets/images/drag-handles.svg
@@ -0,0 +1,7 @@
+
+
+
diff --git a/assets/images/location.svg b/assets/images/location.svg
new file mode 100644
index 000000000000..ad8102051e26
--- /dev/null
+++ b/assets/images/location.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index a486465b0a29..5a4054150e06 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -21,6 +21,8 @@ PODS:
- Airship/MessageCenter (= 16.11.3)
- Airship/PreferenceCenter (= 16.11.3)
- boost (1.76.0)
+ - BVLinearGradient (2.8.1):
+ - React-Core
- CocoaAsyncSocket (7.6.5)
- DoubleConversion (1.1.6)
- FBLazyVector (0.72.3)
@@ -793,6 +795,7 @@ PODS:
DEPENDENCIES:
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
+ - BVLinearGradient (from `../node_modules/react-native-linear-gradient`)
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`)
@@ -943,6 +946,8 @@ SPEC REPOS:
EXTERNAL SOURCES:
boost:
:podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec"
+ BVLinearGradient:
+ :path: "../node_modules/react-native-linear-gradient"
DoubleConversion:
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
FBLazyVector:
@@ -1115,6 +1120,7 @@ SPEC CHECKSUMS:
Airship: c70eed50e429f97f5adb285423c7291fb7a032ae
AirshipFrameworkProxy: 7bc4130c668c6c98e2d4c60fe4c9eb61a999be99
boost: 57d2868c099736d80fcd648bf211b4431e51a558
+ BVLinearGradient: 421743791a59d259aec53f4c58793aad031da2ca
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
FBLazyVector: 4cce221dd782d3ff7c4172167bba09d58af67ccb
diff --git a/package-lock.json b/package-lock.json
index 8f23fe160a6b..4ee7e1baf1ff 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -78,6 +78,7 @@
"react-native-image-picker": "^5.1.0",
"react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#8393b7e58df6ff65fd41f60aee8ece8822c91e2b",
"react-native-key-command": "^1.0.1",
+ "react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
"react-native-onyx": "1.0.52",
@@ -97,6 +98,7 @@
"react-native-tab-view": "^3.5.2",
"react-native-view-shot": "^3.6.0",
"react-native-vision-camera": "^2.15.4",
+ "react-native-web-linear-gradient": "^1.1.2",
"react-native-web-lottie": "^1.4.4",
"react-native-webview": "^11.17.2",
"react-pdf": "^6.2.2",
@@ -42615,6 +42617,15 @@
"react-native-web": "^0.18.1"
}
},
+ "node_modules/react-native-linear-gradient": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.8.1.tgz",
+ "integrity": "sha512-934R4Bnjo7mYT38W9ypS1Dq/YW6TgyGdkHg+w72HNxN0ZDKG1GqAnZ6XlicMUYJDh7ViiJAKN8eOF3Ho0N4J0Q==",
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/react-native-localize": {
"version": "2.2.6",
"license": "MIT",
@@ -42930,6 +42941,14 @@
"react-dom": "^17.0.2 || ^18.0.0"
}
},
+ "node_modules/react-native-web-linear-gradient": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/react-native-web-linear-gradient/-/react-native-web-linear-gradient-1.1.2.tgz",
+ "integrity": "sha512-SmUnpwT49CEe78pXvIvYf72Es8Pv+ZYKCnEOgb2zAKpEUDMo0+xElfRJhwt5nfI8krJ5WbFPKnoDgD0uUjAN1A==",
+ "peerDependencies": {
+ "react-native-web": "*"
+ }
+ },
"node_modules/react-native-web-lottie": {
"version": "1.4.4",
"license": "MIT",
@@ -79109,6 +79128,12 @@
"underscore": "^1.13.4"
}
},
+ "react-native-linear-gradient": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.8.1.tgz",
+ "integrity": "sha512-934R4Bnjo7mYT38W9ypS1Dq/YW6TgyGdkHg+w72HNxN0ZDKG1GqAnZ6XlicMUYJDh7ViiJAKN8eOF3Ho0N4J0Q==",
+ "requires": {}
+ },
"react-native-localize": {
"version": "2.2.6",
"requires": {}
@@ -79288,6 +79313,12 @@
"styleq": "^0.1.2"
}
},
+ "react-native-web-linear-gradient": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/react-native-web-linear-gradient/-/react-native-web-linear-gradient-1.1.2.tgz",
+ "integrity": "sha512-SmUnpwT49CEe78pXvIvYf72Es8Pv+ZYKCnEOgb2zAKpEUDMo0+xElfRJhwt5nfI8krJ5WbFPKnoDgD0uUjAN1A==",
+ "requires": {}
+ },
"react-native-web-lottie": {
"version": "1.4.4",
"requires": {
diff --git a/package.json b/package.json
index ac69af4c760b..8c1f9444e54d 100644
--- a/package.json
+++ b/package.json
@@ -117,6 +117,7 @@
"react-native-image-picker": "^5.1.0",
"react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#8393b7e58df6ff65fd41f60aee8ece8822c91e2b",
"react-native-key-command": "^1.0.1",
+ "react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
"react-native-onyx": "1.0.52",
@@ -136,6 +137,7 @@
"react-native-tab-view": "^3.5.2",
"react-native-view-shot": "^3.6.0",
"react-native-vision-camera": "^2.15.4",
+ "react-native-web-linear-gradient": "^1.1.2",
"react-native-web-lottie": "^1.4.4",
"react-native-webview": "^11.17.2",
"react-pdf": "^6.2.2",
diff --git a/src/components/DistanceRequest.js b/src/components/DistanceRequest.js
index 7bbbfe3f3a01..f499042f8887 100644
--- a/src/components/DistanceRequest.js
+++ b/src/components/DistanceRequest.js
@@ -1,40 +1,155 @@
-import React, {useEffect} from 'react';
-import {View} from 'react-native';
+import React, {useEffect, useState} from 'react';
+import {ScrollView, View} from 'react-native';
+import lodashGet from 'lodash/get';
+import _ from 'underscore';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
-import Text from './Text';
-import * as IOU from '../libs/actions/IOU';
-import styles from '../styles/styles';
import ONYXKEYS from '../ONYXKEYS';
+import * as Transaction from '../libs/actions/Transaction';
+import MenuItemWithTopDescription from './MenuItemWithTopDescription';
+import withLocalize, {withLocalizePropTypes} from './withLocalize';
+import compose from '../libs/compose';
+import * as Expensicons from './Icon/Expensicons';
+import theme from '../styles/themes/default';
+import Button from './Button';
+import styles from '../styles/styles';
+import variables from '../styles/variables';
+import LinearGradient from './LinearGradient';
+
+const MAX_WAYPOINTS = 25;
+const MAX_WAYPOINTS_TO_DISPLAY = 4;
const propTypes = {
/** The transactionID of this request */
transactionID: PropTypes.string,
+
+ /** The optimistic transaction for this request */
+ transaction: PropTypes.shape({
+ /** The transactionID of this request */
+ transactionID: PropTypes.string,
+
+ /** The comment object on the transaction */
+ comment: PropTypes.shape({
+ /** The waypoints defining the distance request */
+ waypoints: PropTypes.shape({
+ /** The latitude of the waypoint */
+ lat: PropTypes.number,
+
+ /** The longitude of the waypoint */
+ lng: PropTypes.number,
+
+ /** The address of the waypoint */
+ address: PropTypes.string,
+ }),
+ }),
+ }),
+
+ ...withLocalizePropTypes,
};
const defaultProps = {
transactionID: '',
+ transaction: {},
};
-function DistanceRequest(props) {
+function DistanceRequest({transactionID, transaction, translate}) {
+ const [shouldShowGradient, setShouldShowGradient] = useState(false);
+ const [scrollContainerHeight, setScrollContainerHeight] = useState(0);
+ const [scrollContentHeight, setScrollContentHeight] = useState(0);
+
+ const waypoints = lodashGet(transaction, 'comment.waypoints', {});
+ const numberOfWaypoints = _.size(waypoints);
+ const lastWaypointIndex = numberOfWaypoints - 1;
+
+ // Show up to the max number of waypoints plus 1/2 of one to hint at scrolling
+ const halfMenuItemHeight = Math.floor(variables.baseMenuItemHeight / 2);
+ const scrollContainerMaxHeight = variables.baseMenuItemHeight * MAX_WAYPOINTS_TO_DISPLAY + halfMenuItemHeight;
+
useEffect(() => {
- if (props.transactionID) {
+ if (!transaction.transactionID || !_.isEmpty(waypoints)) {
return;
}
- IOU.createEmptyTransaction();
- }, [props.transactionID]);
+ // Create the initial start and stop waypoints
+ Transaction.createInitialWaypoints(transaction.transactionID);
+ }, [transaction.transactionID, waypoints]);
+
+ const updateGradientVisibility = (event = {}) => {
+ // If a waypoint extends past the bottom of the visible area show the gradient, else hide it.
+ const visibleAreaEnd = lodashGet(event, 'nativeEvent.contentOffset.y', 0) + scrollContainerHeight;
+ setShouldShowGradient(visibleAreaEnd < scrollContentHeight);
+ };
+
+ useEffect(updateGradientVisibility, [scrollContainerHeight, scrollContentHeight]);
return (
-
- Distance Request
- transactionID: {props.transactionID}
-
+ <>
+ setScrollContainerHeight(lodashGet(event, 'nativeEvent.layout.height', 0))}
+ >
+ setScrollContentHeight(height)}
+ onScroll={updateGradientVisibility}
+ scrollEventThrottle={16}
+ >
+ {_.map(waypoints, (waypoint, key) => {
+ // key is of the form waypoint0, waypoint1, ...
+ const index = Number(key.replace('waypoint', ''));
+ let descriptionKey = 'distance.waypointDescription.';
+ let waypointIcon;
+ if (index === 0) {
+ descriptionKey += 'start';
+ waypointIcon = Expensicons.DotIndicatorUnfilled;
+ } else if (index === lastWaypointIndex) {
+ descriptionKey += 'finish';
+ waypointIcon = Expensicons.Location;
+ } else {
+ descriptionKey += 'stop';
+ waypointIcon = Expensicons.DotIndicator;
+ }
+
+ return (
+
+ );
+ })}
+
+ {shouldShowGradient && (
+
+ )}
+
+
+
+ >
);
}
DistanceRequest.displayName = 'DistanceRequest';
DistanceRequest.propTypes = propTypes;
DistanceRequest.defaultProps = defaultProps;
-export default withOnyx({
- transactionID: {key: ONYXKEYS.IOU, selector: (iou) => (iou && iou.transactionID) || ''},
-})(DistanceRequest);
+export default compose(
+ withLocalize,
+ withOnyx({
+ transaction: {
+ key: (props) => `${ONYXKEYS.COLLECTION.TRANSACTION}${props.transactionID}`,
+ selector: (transaction) => (transaction ? {transactionID: transaction.transactionID, comment: {waypoints: lodashGet(transaction, 'comment.waypoints')}} : null),
+ },
+ }),
+)(DistanceRequest);
diff --git a/src/components/Icon/Expensicons.js b/src/components/Icon/Expensicons.js
index b0c1d5a9249c..2f04c0199587 100644
--- a/src/components/Icon/Expensicons.js
+++ b/src/components/Icon/Expensicons.js
@@ -32,8 +32,10 @@ import Document from '../../../assets/images/document.svg';
import DeletedRoomAvatar from '../../../assets/images/avatars/deleted-room.svg';
import DomainRoomAvatar from '../../../assets/images/avatars/domain-room.svg';
import DotIndicator from '../../../assets/images/dot-indicator.svg';
+import DotIndicatorUnfilled from '../../../assets/images/dot-indicator-unfilled.svg';
import DownArrow from '../../../assets/images/down.svg';
import Download from '../../../assets/images/download.svg';
+import DragHandles from '../../../assets/images/drag-handles.svg';
import Emoji from '../../../assets/images/emoji.svg';
import Exclamation from '../../../assets/images/exclamation.svg';
import Exit from '../../../assets/images/exit.svg';
@@ -60,6 +62,7 @@ import Key from '../../../assets/images/key.svg';
import Keyboard from '../../../assets/images/keyboard.svg';
import Link from '../../../assets/images/link.svg';
import LinkCopy from '../../../assets/images/link-copy.svg';
+import Location from '../../../assets/images/location.svg';
import Lock from '../../../assets/images/lock.svg';
import LoungeAccess from '../../../assets/images/lounge-access.svg';
import Luggage from '../../../assets/images/luggage.svg';
@@ -154,9 +157,11 @@ export {
Document,
DomainRoomAvatar,
DotIndicator,
+ DotIndicatorUnfilled,
DownArrow,
Download,
DragAndDrop,
+ DragHandles,
Emoji,
Exclamation,
Exit,
@@ -187,6 +192,7 @@ export {
Keyboard,
Link,
LinkCopy,
+ Location,
Lock,
LoungeAccess,
Luggage,
diff --git a/src/components/LinearGradient/index.js b/src/components/LinearGradient/index.js
new file mode 100644
index 000000000000..8270681641d0
--- /dev/null
+++ b/src/components/LinearGradient/index.js
@@ -0,0 +1,3 @@
+import LinearGradient from 'react-native-web-linear-gradient';
+
+export default LinearGradient;
diff --git a/src/components/LinearGradient/index.native.js b/src/components/LinearGradient/index.native.js
new file mode 100644
index 000000000000..c8d5af2646b2
--- /dev/null
+++ b/src/components/LinearGradient/index.native.js
@@ -0,0 +1,3 @@
+import LinearGradient from 'react-native-linear-gradient';
+
+export default LinearGradient;
diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js
index c280a75a8ef3..9795e576e5d5 100644
--- a/src/components/MenuItem.js
+++ b/src/components/MenuItem.js
@@ -42,12 +42,14 @@ const defaultProps = {
descriptionTextStyle: styles.breakWord,
success: false,
icon: undefined,
+ secondaryIcon: undefined,
iconWidth: undefined,
iconHeight: undefined,
description: undefined,
iconRight: Expensicons.ArrowRight,
iconStyles: [],
iconFill: undefined,
+ secondaryIconFill: undefined,
focused: false,
disabled: false,
isSelected: false,
@@ -193,6 +195,19 @@ const MenuItem = React.forwardRef((props, ref) => {
)}
)}
+ {Boolean(props.secondaryIcon) && (
+
+
+
+ )}
{Boolean(props.description) && props.shouldShowDescriptionOnTop && (
{
+ if (transactionID) {
+ return;
+ }
+ IOU.createEmptyTransaction();
+ }, [transactionID]);
+
+ return (
+
+ );
+}
+
+DistanceRequestPage.displayName = 'DistanceRequestPage';
+DistanceRequestPage.propTypes = propTypes;
+DistanceRequestPage.defaultProps = defaultProps;
+export default withOnyx({
+ // We must provide a default value for transactionID here, otherwise the component won't mount
+ // because withOnyx returns null until all the keys are defined
+ transactionID: {key: ONYXKEYS.IOU, selector: (iou) => (iou && iou.transactionID) || ''},
+})(DistanceRequestPage);
diff --git a/src/pages/iou/MoneyRequestSelectorPage.js b/src/pages/iou/MoneyRequestSelectorPage.js
index 307d2aeb35a9..f1633d62e490 100644
--- a/src/pages/iou/MoneyRequestSelectorPage.js
+++ b/src/pages/iou/MoneyRequestSelectorPage.js
@@ -15,7 +15,7 @@ import Navigation from '../../libs/Navigation/Navigation';
import styles from '../../styles/styles';
import ReceiptSelector from './ReceiptSelector';
import * as IOU from '../../libs/actions/IOU';
-import DistanceRequest from '../../components/DistanceRequest';
+import DistanceRequestPage from './DistanceRequestPage';
import DragAndDropProvider from '../../components/DragAndDrop/Provider';
import usePermissions from '../../hooks/usePermissions';
import OnyxTabNavigator, {TopTab} from '../../libs/Navigation/OnyxTabNavigator';
@@ -112,7 +112,7 @@ function MoneyRequestSelectorPage(props) {
{canUseDistanceRequests && (
)}
diff --git a/src/styles/variables.js b/src/styles/variables.js
index a771aa95906e..af4d81c93c79 100644
--- a/src/styles/variables.js
+++ b/src/styles/variables.js
@@ -150,4 +150,6 @@ export default {
hoverDimValue: 1,
pressDimValue: 0.8,
qrShareHorizontalPadding: 32,
+
+ baseMenuItemHeight: 64,
};