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 && ( + + )} + + +