diff --git a/.github/ISSUE_TEMPLATE/DesignDoc.md b/.github/ISSUE_TEMPLATE/DesignDoc.md
index 424b549a0940..2fbdcf7a65d5 100644
--- a/.github/ISSUE_TEMPLATE/DesignDoc.md
+++ b/.github/ISSUE_TEMPLATE/DesignDoc.md
@@ -27,6 +27,7 @@ labels: Daily, NewFeature
- [ ] Confirm that the doc has the minimum necessary number of reviews before proceeding
- [ ] Email `strategy@expensify.com` one last time to let them know the Design Doc is moving into the implementation phase
- [ ] Implement the changes
+- [ ] Add regression tests so that QA can test your feature with every deploy ([instructions](https://stackoverflowteams.com/c/expensify/questions/363))
- [ ] Send out a follow up email to `strategy@expensify.com` once everything has been implemented and do a **Project Wrap-Up** retrospective that provides:
- Summary of what we accomplished with this project
- What went well?
diff --git a/.github/workflows/README.md b/.github/workflows/README.md
index e1b1696411b1..e432d9291f45 100644
--- a/.github/workflows/README.md
+++ b/.github/workflows/README.md
@@ -85,7 +85,7 @@ The GitHub workflows require a large list of secrets to deploy, notify and test
1. `LARGE_SECRET_PASSPHRASE` - decrypts secrets stored in various encrypted files stored in GitHub repository. To create updated versions of these encrypted files, refer to steps 1-4 of [this encrypted secrets help page](https://docs.github.com/en/actions/reference/encrypted-secrets#limits-for-secrets) using the `LARGE_SECRET_PASSPHRASE`.
1. `android/app/my-upload-key.keystore.gpg`
1. `android/app/android-fastlane-json-key.json.gpg`
- 1. `ios/chat_expensify_adhoc.mobileprovision.gpg`
+ 1. `ios/expensify_chat_adhoc.mobileprovision.gpg`
1. `ios/chat_expensify_appstore.mobileprovision.gpg`
1. `ios/Certificates.p12.gpg`
1. `SLACK_WEBHOOK` - Sends Slack notifications via Slack WebHook https://expensify.slack.com/services/B01AX48D7MM
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 795271cab60a..1983e406c77b 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -15,7 +15,7 @@ jobs:
- uses: Expensify/App/.github/actions/composite/setupNode@main
- - name: Lint JavaScript with ESLint
+ - name: Lint JavaScript and Typescript with ESLint
run: npm run lint
env:
CI: true
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 911ef16fa865..7cba91e7b0a9 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -90,8 +90,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001036200
- versionName "1.3.62-0"
+ versionCode 1001036603
+ versionName "1.3.66-3"
}
flavorDimensions "default"
diff --git a/android/app/src/main/res/values-large/orientation.xml b/android/app/src/main/res/values-large/orientation.xml
index c06e0147ee73..9f60d109a2fc 100644
--- a/android/app/src/main/res/values-large/orientation.xml
+++ b/android/app/src/main/res/values-large/orientation.xml
@@ -1,4 +1,4 @@
- false
+ true
diff --git a/android/app/src/main/res/values-sw600dp/orientation.xml b/android/app/src/main/res/values-sw600dp/orientation.xml
index c06e0147ee73..9f60d109a2fc 100644
--- a/android/app/src/main/res/values-sw600dp/orientation.xml
+++ b/android/app/src/main/res/values-sw600dp/orientation.xml
@@ -1,4 +1,4 @@
- false
+ true
diff --git a/contributingGuides/FORMS.md b/contributingGuides/FORMS.md
index 01f145dafbc6..661c700130c7 100644
--- a/contributingGuides/FORMS.md
+++ b/contributingGuides/FORMS.md
@@ -274,6 +274,7 @@ Form.js will automatically provide the following props to any input with the inp
- onBlur: An onBlur handler that calls validate.
- onTouched: An onTouched handler that marks the input as touched.
- onInputChange: An onChange handler that saves draft values and calls validate for that input (inputA). Passing an inputID as a second param allows inputA to manipulate the input value of the provided inputID (inputB).
+- onFocus: An onFocus handler that marks the input as focused.
## Dynamic Form Inputs
diff --git a/docs/articles/request-money/Request-and-Split-Bills.md b/docs/articles/request-money/Request-and-Split-Bills.md
index a2c63cf6f8f7..bb27cd75c742 100644
--- a/docs/articles/request-money/Request-and-Split-Bills.md
+++ b/docs/articles/request-money/Request-and-Split-Bills.md
@@ -16,7 +16,10 @@ These two features ensure you can live in the moment and settle up afterward.
# How to Request Money
- Select the Green **+** button and choose **Request Money**
-- Enter the amount **$** they owe and click **Next**
+- Select the relevant option:
+ - **Manual:** Enter the merchant and amount manually.
+ - **Scan:** Take a photo of the receipt to have the merchant and amount auto-filled.
+ - **Distance:** Enter the details of your trip, plus any stops along the way, and the mileage and amount will be automatically calculated.
- Search for the user or enter their email!
- Enter a reason for the request (optional)
- Click **Request!**
diff --git a/docs/assets/images/insights-chart.png b/docs/assets/images/insights-chart.png
index 7b10c8c92d8d..4b21b8d70a09 100644
Binary files a/docs/assets/images/insights-chart.png and b/docs/assets/images/insights-chart.png differ
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 92c61cb81b2c..ecec05f1cec1 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -224,11 +224,11 @@ platform :ios do
contact_phone: ENV["APPLE_CONTACT_PHONE"],
demo_account_name: ENV["APPLE_DEMO_EMAIL"],
demo_account_password: ENV["APPLE_DEMO_PASSWORD"],
- notes: "1. Log into the Expensify app using the provided email
- 2. Now, you have to log in to this gmail account on https://mail.google.com/ so you can retrieve a One-Time-Password
- 3. To log in to the gmail account, use the password above (That's NOT a password for the Expensify app but for the Gmail account)
- 4. At the Gmail inbox, you should have received a one-time 6 digit magic code
- 5. Use that to sign in"
+ notes: "1. In the Expensify app, enter the email 'appletest.expensify@proton.me'. This will trigger a sign-in link to be sent to 'appletest.expensify@proton.me'
+ 2. Navigate to https://account.proton.me/login, log into Proton Mail using 'appletest.expensify@proton.me' as email and the password associated with 'appletest.expensify@proton.me', provided above
+ 3. Once logged into Proton Mail, navigate to your inbox and locate the email triggered in step 1. The email subject should be 'Your magic sign-in link for Expensify'
+ 4. Open the email and copy the 6-digit sign-in code provided within
+ 5. Return to the Expensify app and enter the copied 6-digit code in the designated login field"
}
)
rescue Exception => e
diff --git a/ios/Certificates.p12.gpg b/ios/Certificates.p12.gpg
index c4a68891f6e4..f63d6861f888 100644
Binary files a/ios/Certificates.p12.gpg and b/ios/Certificates.p12.gpg differ
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index c0b4110bddd4..e9e9394fcaae 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.3.62
+ 1.3.66
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.3.62.0
+ 1.3.66.3
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
@@ -108,6 +108,8 @@
armv7
+ UIRequiresFullScreen
+
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
@@ -117,8 +119,6 @@
UIInterfaceOrientationPortrait
UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeRight
- UIInterfaceOrientationLandscapeLeft
UIUserInterfaceStyle
Dark
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 97287c3c3086..7286b383a0c7 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.3.62
+ 1.3.66
CFBundleSignature
????
CFBundleVersion
- 1.3.62.0
+ 1.3.66.3
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 16ed1e05dc64..2bea672171fe 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -591,7 +591,7 @@ PODS:
- React-Core
- react-native-pager-view (6.2.0):
- React-Core
- - react-native-pdf (6.6.2):
+ - react-native-pdf (6.7.1):
- React-Core
- react-native-performance (4.0.0):
- React-Core
@@ -1254,7 +1254,7 @@ SPEC CHECKSUMS:
react-native-key-command: c2645ec01eb1fa664606c09480c05cb4220ef67b
react-native-netinfo: ccbe1085dffd16592791d550189772e13bf479e2
react-native-pager-view: 0ccb8bf60e2ebd38b1f3669fa3650ecce81db2df
- react-native-pdf: 33c622cbdf776a649929e8b9d1ce2d313347c4fa
+ react-native-pdf: 7c0e91ada997bac8bac3bb5bea5b6b81f5a3caae
react-native-performance: 224bd53e6a835fda4353302cf891d088a0af7406
react-native-plaid-link-sdk: 9eb0f71dad94b3bdde649c7a384cba93024af46c
react-native-quick-sqlite: bcc7a7a250a40222f18913a97cd356bf82d0a6c4
diff --git a/ios/chat_expensify_adhoc.mobileprovision.gpg b/ios/chat_expensify_adhoc.mobileprovision.gpg
deleted file mode 100644
index 97179c8a65ac..000000000000
Binary files a/ios/chat_expensify_adhoc.mobileprovision.gpg and /dev/null differ
diff --git a/ios/chat_expensify_appstore.mobileprovision.gpg b/ios/chat_expensify_appstore.mobileprovision.gpg
index 39137ea24a07..246f5f0ec99e 100644
Binary files a/ios/chat_expensify_appstore.mobileprovision.gpg and b/ios/chat_expensify_appstore.mobileprovision.gpg differ
diff --git a/ios/expensify_chat_adhoc.mobileprovision.gpg b/ios/expensify_chat_adhoc.mobileprovision.gpg
index 1464356e423e..8160fba0cfa9 100644
Binary files a/ios/expensify_chat_adhoc.mobileprovision.gpg and b/ios/expensify_chat_adhoc.mobileprovision.gpg differ
diff --git a/ios/expensify_chat_dev.mobileprovision.gpg b/ios/expensify_chat_dev.mobileprovision.gpg
deleted file mode 100644
index 3b8b96b2c142..000000000000
Binary files a/ios/expensify_chat_dev.mobileprovision.gpg and /dev/null differ
diff --git a/package-lock.json b/package-lock.json
index d1f030de301e..4128a740b360 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.3.62-0",
+ "version": "1.3.66-3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.3.62-0",
+ "version": "1.3.66-3",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -85,7 +85,7 @@
"react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "1.0.70",
+ "react-native-onyx": "1.0.72",
"react-native-pager-view": "^6.2.0",
"react-native-pdf": "^6.7.1",
"react-native-performance": "^4.0.0",
@@ -40884,9 +40884,9 @@
}
},
"node_modules/react-native-onyx": {
- "version": "1.0.70",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.70.tgz",
- "integrity": "sha512-bc/u4kkcwbrN6kLxXprZbwYqApYJ7G07IKteJhRuIjXi1hMPxOznRxxqMaOTELgET9y5LezUOB2QOwfEZ59FLg==",
+ "version": "1.0.72",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.72.tgz",
+ "integrity": "sha512-roJuA92qZH2PLYSqBhSPCse+Ra2EJu4FBpVqguwJRp6oaLNHR1CtPTgU1xMh/kj2nWmdpcqKoOc3nS35asb80g==",
"dependencies": {
"ascii-table": "0.0.9",
"fast-equals": "^4.0.3",
@@ -76679,9 +76679,9 @@
}
},
"react-native-onyx": {
- "version": "1.0.70",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.70.tgz",
- "integrity": "sha512-bc/u4kkcwbrN6kLxXprZbwYqApYJ7G07IKteJhRuIjXi1hMPxOznRxxqMaOTELgET9y5LezUOB2QOwfEZ59FLg==",
+ "version": "1.0.72",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.72.tgz",
+ "integrity": "sha512-roJuA92qZH2PLYSqBhSPCse+Ra2EJu4FBpVqguwJRp6oaLNHR1CtPTgU1xMh/kj2nWmdpcqKoOc3nS35asb80g==",
"requires": {
"ascii-table": "0.0.9",
"fast-equals": "^4.0.3",
diff --git a/package.json b/package.json
index dcfb9078f996..6574bcc4b77c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.3.62-0",
+ "version": "1.3.66-3",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
@@ -125,7 +125,7 @@
"react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "1.0.70",
+ "react-native-onyx": "1.0.72",
"react-native-pager-view": "^6.2.0",
"react-native-pdf": "^6.7.1",
"react-native-performance": "^4.0.0",
diff --git a/src/CONST.ts b/src/CONST.ts
index 666d7db19a15..56f61536b3cb 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -235,7 +235,6 @@ const CONST = {
TASKS: 'tasks',
THREADS: 'threads',
CUSTOM_STATUS: 'customStatus',
- DISTANCE_REQUESTS: 'distanceRequests',
},
BUTTON_STATES: {
DEFAULT: 'default',
@@ -453,7 +452,7 @@ const CONST = {
},
RECEIPT: {
ICON_SIZE: 164,
- PERMISSION_AUTHORIZED: 'authorized',
+ PERMISSION_GRANTED: 'granted',
HAND_ICON_HEIGHT: 152,
HAND_ICON_WIDTH: 200,
SHUTTER_SIZE: 90,
@@ -666,6 +665,7 @@ const CONST = {
TRANSACTION: {
DEFAULT_MERCHANT: 'Request',
UNKNOWN_MERCHANT: 'Unknown Merchant',
+ PARTIAL_TRANSACTION_MERCHANT: '(none)',
TYPE: {
CUSTOM_UNIT: 'customUnit',
},
diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js
index e761ebeed26b..1697dddba805 100644
--- a/src/components/AddressSearch/index.js
+++ b/src/components/AddressSearch/index.js
@@ -95,6 +95,9 @@ const propTypes = {
/** Maximum number of characters allowed in search input */
maxInputLength: PropTypes.number,
+ /** The result types to return from the Google Places Autocomplete request */
+ resultTypes: PropTypes.string,
+
/** Information about the network */
network: networkPropTypes.isRequired,
@@ -123,6 +126,7 @@ const defaultProps = {
},
maxInputLength: undefined,
predefinedPlaces: [],
+ resultTypes: 'address',
};
// Do not convert to class component! It's been tried before and presents more challenges than it's worth.
@@ -134,10 +138,10 @@ function AddressSearch(props) {
const query = useMemo(
() => ({
language: props.preferredLocale,
- types: 'address',
+ types: props.resultTypes,
components: props.isLimitedToUSA ? 'country:us' : undefined,
}),
- [props.preferredLocale, props.isLimitedToUSA],
+ [props.preferredLocale, props.resultTypes, props.isLimitedToUSA],
);
const saveLocationDetails = (autocompleteData, details) => {
diff --git a/src/components/ConfirmedRoute.js b/src/components/ConfirmedRoute.js
index f57fac050c65..aa7c38e0c535 100644
--- a/src/components/ConfirmedRoute.js
+++ b/src/components/ConfirmedRoute.js
@@ -93,6 +93,10 @@ function ConfirmedRoute({mapboxAccessToken, transaction}) {
accessToken={mapboxAccessToken.token}
mapPadding={CONST.MAP_PADDING}
pitchEnabled={false}
+ initialState={{
+ zoom: CONST.MAPBOX.DEFAULT_ZOOM,
+ location: lodashGet(waypointMarkers, [0, 'coordinate'], CONST.MAPBOX.DEFAULT_COORDINATE),
+ }}
directionCoordinates={coordinates}
style={styles.mapView}
waypoints={waypointMarkers}
diff --git a/src/components/DistanceRequest.js b/src/components/DistanceRequest.js
index ce6b9880a82c..c966cf62be96 100644
--- a/src/components/DistanceRequest.js
+++ b/src/components/DistanceRequest.js
@@ -1,8 +1,9 @@
-import React, {useEffect, useMemo, useState} from 'react';
+import React, {useEffect, useMemo, useState, useRef} from 'react';
import {ScrollView, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import lodashGet from 'lodash/get';
import lodashHas from 'lodash/has';
+import lodashIsNull from 'lodash/isNull';
import PropTypes from 'prop-types';
import _ from 'underscore';
@@ -36,6 +37,7 @@ import MenuItemWithTopDescription from './MenuItemWithTopDescription';
import {iouPropTypes} from '../pages/iou/propTypes';
import reportPropTypes from '../pages/reportPropTypes';
import * as IOU from '../libs/actions/IOU';
+import * as StyleUtils from '../styles/StyleUtils';
const MAX_WAYPOINTS = 25;
const MAX_WAYPOINTS_TO_DISPLAY = 4;
@@ -82,21 +84,23 @@ function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken})
const reportID = lodashGet(report, 'reportID', '');
const waypoints = useMemo(() => lodashGet(transaction, 'comment.waypoints', {}), [transaction]);
+ const previousWaypoints = usePrevious(waypoints);
const numberOfWaypoints = _.size(waypoints);
+ const numberOfPreviousWaypoints = _.size(previousWaypoints);
+ const scrollViewRef = useRef(null);
const lastWaypointIndex = numberOfWaypoints - 1;
const isLoadingRoute = lodashGet(transaction, 'comment.isLoading', false);
const hasRouteError = lodashHas(transaction, 'errorFields.route');
- const previousWaypoints = usePrevious(waypoints);
const haveWaypointsChanged = !_.isEqual(previousWaypoints, waypoints);
const doesRouteExist = lodashHas(transaction, 'routes.route0.geometry.coordinates');
- const shouldFetchRoute = (!doesRouteExist || haveWaypointsChanged) && !isLoadingRoute && TransactionUtils.validateWaypoints(waypoints);
-
+ const validatedWaypoints = TransactionUtils.getValidWaypoints(waypoints);
+ const shouldFetchRoute = (!doesRouteExist || haveWaypointsChanged) && !isLoadingRoute && _.size(validatedWaypoints) > 1;
const waypointMarkers = useMemo(
() =>
_.filter(
_.map(waypoints, (waypoint, key) => {
- if (!waypoint || !lodashHas(waypoint, 'lat') || !lodashHas(waypoint, 'lng')) {
+ if (!waypoint || !lodashHas(waypoint, 'lat') || !lodashHas(waypoint, 'lng') || lodashIsNull(waypoint.lat) || lodashIsNull(waypoint.lng)) {
return;
}
@@ -128,8 +132,8 @@ function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken})
);
// 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;
+ const halfMenuItemHeight = Math.floor(variables.optionRowHeight / 2);
+ const scrollContainerMaxHeight = variables.optionRowHeight * MAX_WAYPOINTS_TO_DISPLAY + halfMenuItemHeight;
useEffect(() => {
MapboxToken.init();
@@ -149,27 +153,32 @@ function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken})
const visibleAreaEnd = lodashGet(event, 'nativeEvent.contentOffset.y', 0) + scrollContainerHeight;
setShouldShowGradient(visibleAreaEnd < scrollContentHeight);
};
-
useEffect(() => {
if (isOffline || !shouldFetchRoute) {
return;
}
- Transaction.getRoute(iou.transactionID, waypoints);
- }, [shouldFetchRoute, iou.transactionID, waypoints, isOffline]);
+ Transaction.getRoute(iou.transactionID, validatedWaypoints);
+ }, [shouldFetchRoute, iou.transactionID, validatedWaypoints, isOffline]);
useEffect(updateGradientVisibility, [scrollContainerHeight, scrollContentHeight]);
return (
- <>
+
setScrollContainerHeight(lodashGet(event, 'nativeEvent.layout.height', 0))}
>
setScrollContentHeight(height)}
+ onContentSizeChange={(width, height) => {
+ if (scrollContentHeight < height && numberOfWaypoints > numberOfPreviousWaypoints) {
+ scrollViewRef.current.scrollToEnd({animated: true});
+ }
+ setScrollContentHeight(height);
+ }}
onScroll={updateGradientVisibility}
- scrollEventThrottle={16}
+ scrollEventThrottle={variables.distanceScrollEventThrottle}
+ ref={scrollViewRef}
>
{_.map(waypoints, (waypoint, key) => {
// key is of the form waypoint0, waypoint1, ...
@@ -204,7 +213,7 @@ function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken})
{shouldShowGradient && (
)}
{hasRouteError && (
@@ -255,11 +264,10 @@ function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken})
success
style={[styles.w100, styles.mb4, styles.ph4, styles.flexShrink0]}
onPress={() => IOU.navigateToNextPage(iou, iouType, reportID, report)}
- pressOnEnter
- isDisabled={waypointMarkers.length < 2}
+ isDisabled={_.size(validatedWaypoints) < 2}
text={translate('common.next')}
/>
- >
+
);
}
diff --git a/src/components/DownloadAppModal.js b/src/components/DownloadAppModal.js
index ffa933708e4c..c96c6b3d28c0 100644
--- a/src/components/DownloadAppModal.js
+++ b/src/components/DownloadAppModal.js
@@ -26,13 +26,13 @@ const defaultProps = {
};
function DownloadAppModal({isAuthenticated, showDownloadAppBanner}) {
- const [shouldShowBanner, setshouldShowBanner] = useState(Browser.isMobile() && isAuthenticated && showDownloadAppBanner);
+ const [shouldShowBanner, setShouldShowBanner] = useState(Browser.isMobile() && isAuthenticated && showDownloadAppBanner);
const {translate} = useLocalize();
const handleCloseBanner = () => {
setShowDownloadAppModal(false);
- setshouldShowBanner(false);
+ setShouldShowBanner(false);
};
let link = '';
@@ -44,6 +44,8 @@ function DownloadAppModal({isAuthenticated, showDownloadAppBanner}) {
}
const handleOpenAppStore = () => {
+ setShowDownloadAppModal(false);
+ setShouldShowBanner(false);
Link.openExternalLink(link, true);
};
diff --git a/src/components/DragAndDrop/Provider/dragAndDropProviderPropTypes.js b/src/components/DragAndDrop/Provider/dragAndDropProviderPropTypes.js
index d9cc806e9012..82e503456f7d 100644
--- a/src/components/DragAndDrop/Provider/dragAndDropProviderPropTypes.js
+++ b/src/components/DragAndDrop/Provider/dragAndDropProviderPropTypes.js
@@ -6,4 +6,7 @@ export default {
/** Should this dropZone be disabled? */
isDisabled: PropTypes.bool,
+
+ /** Indicate that users are dragging file or not */
+ setIsDraggingOver: PropTypes.func,
};
diff --git a/src/components/DragAndDrop/Provider/index.js b/src/components/DragAndDrop/Provider/index.js
index 89b0f47a830d..6408f6dbfbfa 100644
--- a/src/components/DragAndDrop/Provider/index.js
+++ b/src/components/DragAndDrop/Provider/index.js
@@ -1,5 +1,5 @@
import _ from 'underscore';
-import React, {useRef, useCallback} from 'react';
+import React, {useRef, useCallback, useEffect} from 'react';
import {View} from 'react-native';
import {PortalHost} from '@gorhom/portal';
import Str from 'expensify-common/lib/str';
@@ -17,7 +17,7 @@ function shouldAcceptDrop(event) {
return _.some(event.dataTransfer.types, (type) => type === 'Files');
}
-function DragAndDropProvider({children, isDisabled = false}) {
+function DragAndDropProvider({children, isDisabled = false, setIsDraggingOver = () => {}}) {
const dropZone = useRef(null);
const dropZoneID = useRef(Str.guid('drag-n-drop'));
@@ -33,6 +33,10 @@ function DragAndDropProvider({children, isDisabled = false}) {
isDisabled,
});
+ useEffect(() => {
+ setIsDraggingOver(isDraggingOver);
+ }, [isDraggingOver, setIsDraggingOver]);
+
return (
= CONST.NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT) {
- targetOffset = offsetAtEmojiBottom - CONST.NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT;
- } else if (offsetAtEmojiTop - CONST.EMOJI_PICKER_HEADER_HEIGHT <= this.currentScrollOffset) {
- // There is always a sticky header on the top, subtract the EMOJI_PICKER_HEADER_HEIGHT from offsetAtEmojiTop to get the correct scroll position.
- targetOffset = offsetAtEmojiTop - CONST.EMOJI_PICKER_HEADER_HEIGHT;
- }
- if (targetOffset !== this.currentScrollOffset) {
- // Disable pointer events so that onHover doesn't get triggered when the items move while we're scrolling
- if (!this.state.arePointerEventsDisabled) {
- this.setState({arePointerEventsDisabled: true});
- }
- this.emojiList.scrollToOffset({offset: targetOffset, animated: false});
- }
- }
-
/**
* Filter the entire list of emojis to only emojis that have the search term in their keywords
*
@@ -530,6 +496,7 @@ class EmojiPickerMenu extends Component {
return (
@@ -566,10 +533,11 @@ class EmojiPickerMenu extends Component {
{overscrollBehaviorY: 'contain'},
// Set overflow to hidden to prevent elastic scrolling when there are not enough contents to scroll in FlatList
{overflowY: this.state.filteredEmojis.length > overflowLimit ? 'auto' : 'hidden'},
+ // Set scrollPaddingTop to consider sticky headers while scrolling
+ {scrollPaddingTop: isFiltered ? 0 : CONST.EMOJI_PICKER_ITEM_HEIGHT},
]}
extraData={[this.state.filteredEmojis, this.state.highlightedIndex, this.props.preferredSkinTone]}
stickyHeaderIndices={this.state.headerIndices}
- onScroll={(e) => (this.currentScrollOffset = e.nativeEvent.contentOffset.y)}
getItemLayout={this.getItemLayout}
contentContainerStyle={styles.flexGrow1}
ListEmptyComponent={{this.props.translate('common.noResultsFound')}}
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
index a794d4aa4bad..bfdaf1c13d1b 100644
--- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
+++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
@@ -19,6 +19,7 @@ import * as User from '../../../libs/actions/User';
import TextInput from '../../TextInput';
import CategoryShortcutBar from '../CategoryShortcutBar';
import * as StyleUtils from '../../../styles/StyleUtils';
+import useSingleExecution from '../../../hooks/useSingleExecution';
const propTypes = {
/** Function to add the selected emoji to the main compose text input */
@@ -49,6 +50,7 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t
const [filteredEmojis, setFilteredEmojis] = useState(allEmojis);
const [headerIndices, setHeaderIndices] = useState(headerRowIndices);
const {windowWidth} = useWindowDimensions();
+ const {singleExecution} = useSingleExecution();
useEffect(() => {
setFilteredEmojis(allEmojis);
@@ -150,7 +152,7 @@ function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, t
return (
addToFrequentAndSelectEmoji(emoji, item)}
+ onPress={singleExecution((emoji) => addToFrequentAndSelectEmoji(emoji, item))}
emoji={emojiCode}
/>
);
diff --git a/src/components/EmojiPicker/EmojiPickerMenuItem/index.js b/src/components/EmojiPicker/EmojiPickerMenuItem/index.js
index 37e90f01c707..728e56792ddb 100644
--- a/src/components/EmojiPicker/EmojiPickerMenuItem/index.js
+++ b/src/components/EmojiPicker/EmojiPickerMenuItem/index.js
@@ -42,13 +42,14 @@ class EmojiPickerMenuItem extends PureComponent {
super(props);
this.ref = null;
+ this.focusAndScroll = this.focusAndScroll.bind(this);
}
componentDidMount() {
if (!this.props.isFocused) {
return;
}
- this.ref.focus();
+ this.focusAndScroll();
}
componentDidUpdate(prevProps) {
@@ -58,7 +59,12 @@ class EmojiPickerMenuItem extends PureComponent {
if (!this.props.isFocused) {
return;
}
- this.ref.focus();
+ this.focusAndScroll();
+ }
+
+ focusAndScroll() {
+ this.ref.focus({preventScroll: true});
+ this.ref.scrollIntoView({block: 'nearest'});
}
render() {
diff --git a/src/components/Form.js b/src/components/Form.js
index eb6945f6ec78..a45c6d769d57 100644
--- a/src/components/Form.js
+++ b/src/components/Form.js
@@ -108,6 +108,7 @@ function Form(props) {
const formContentRef = useRef(null);
const inputRefs = useRef({});
const touchedInputs = useRef({});
+ const focusedInput = useRef(null);
const isFirstRender = useRef(true);
const {validate, onSubmit, children} = props;
@@ -305,6 +306,12 @@ function Form(props) {
// as this is already happening by the value prop.
defaultValue: undefined,
errorText: errors[inputID] || fieldErrorMessage,
+ onFocus: (event) => {
+ focusedInput.current = inputID;
+ if (_.isFunction(child.props.onFocus)) {
+ child.props.onFocus(event);
+ }
+ },
onBlur: (event) => {
// Only run validation when user proactively blurs the input.
if (Visibility.isVisible() && Visibility.hasFocus()) {
@@ -328,6 +335,11 @@ function Form(props) {
},
onInputChange: (value, key) => {
const inputKey = key || inputID;
+
+ if (focusedInput.current && focusedInput.current !== inputKey) {
+ setTouchedInput(focusedInput.current);
+ }
+
setInputValues((prevState) => {
const newState = {
...prevState,
diff --git a/src/components/HeaderGap/index.desktop.js b/src/components/HeaderGap/index.desktop.js
index 10974aa9f5ee..6b47f56516de 100644
--- a/src/components/HeaderGap/index.desktop.js
+++ b/src/components/HeaderGap/index.desktop.js
@@ -1,9 +1,22 @@
import React, {PureComponent} from 'react';
import {View} from 'react-native';
+import PropTypes from 'prop-types';
import styles from '../../styles/styles';
-export default class HeaderGap extends PureComponent {
+const propTypes = {
+ /** Styles to apply to the HeaderGap */
+ // eslint-disable-next-line react/forbid-prop-types
+ styles: PropTypes.arrayOf(PropTypes.object),
+};
+
+class HeaderGap extends PureComponent {
render() {
- return ;
+ return ;
}
}
+
+HeaderGap.propTypes = propTypes;
+HeaderGap.defaultProps = {
+ styles: [],
+};
+export default HeaderGap;
diff --git a/src/components/Image/imagePropTypes.js b/src/components/Image/imagePropTypes.js
index 3e9293bd2437..46ec83384d62 100644
--- a/src/components/Image/imagePropTypes.js
+++ b/src/components/Image/imagePropTypes.js
@@ -10,7 +10,7 @@ const imagePropTypes = {
source: PropTypes.oneOfType([
PropTypes.number,
PropTypes.shape({
- uri: PropTypes.string.isRequired,
+ uri: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
// eslint-disable-next-line react/forbid-prop-types
headers: PropTypes.object,
}),
diff --git a/src/components/Image/index.native.js b/src/components/Image/index.native.js
index 0713fa6c7fe2..9d9ad600b1d4 100644
--- a/src/components/Image/index.native.js
+++ b/src/components/Image/index.native.js
@@ -18,7 +18,10 @@ function Image(props) {
const {source, isAuthTokenRequired, session, ...rest} = props;
let imageSource = source;
- if (typeof source !== 'number' && isAuthTokenRequired) {
+ if (source && source.uri && typeof source.uri === 'number') {
+ imageSource = source.uri;
+ }
+ if (typeof imageSource !== 'number' && isAuthTokenRequired) {
const authToken = lodashGet(props, 'session.encryptedAuthToken', null);
imageSource = {
...source,
diff --git a/src/components/LHNOptionsList/OptionRowLHNData.js b/src/components/LHNOptionsList/OptionRowLHNData.js
index 21fade6eb942..4c7bd54efa18 100644
--- a/src/components/LHNOptionsList/OptionRowLHNData.js
+++ b/src/components/LHNOptionsList/OptionRowLHNData.js
@@ -12,6 +12,9 @@ import withCurrentReportID, {withCurrentReportIDPropTypes, withCurrentReportIDDe
import OptionRowLHN, {propTypes as basePropTypes, defaultProps as baseDefaultProps} from './OptionRowLHN';
import * as Report from '../../libs/actions/Report';
import * as UserUtils from '../../libs/UserUtils';
+import * as ReportActionsUtils from '../../libs/ReportActionsUtils';
+import * as TransactionUtils from '../../libs/TransactionUtils';
+
import participantPropTypes from '../participantPropTypes';
import CONST from '../../CONST';
import reportActionPropTypes from '../../pages/home/report/reportActionPropTypes';
@@ -75,6 +78,7 @@ function OptionRowLHNData({
preferredLocale,
comment,
policies,
+ receiptTransactions,
parentReportActions,
...propsToForward
}) {
@@ -88,6 +92,14 @@ function OptionRowLHNData({
const parentReportAction = parentReportActions[fullReport.parentReportActionID];
const optionItemRef = useRef();
+
+ const linkedTransaction = useMemo(() => {
+ const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(reportActions);
+ const lastReportAction = _.first(sortedReportActions);
+ return TransactionUtils.getLinkedTransaction(lastReportAction);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [fullReport.reportID, receiptTransactions, reportActions]);
+
const optionItem = useMemo(() => {
// Note: ideally we'd have this as a dependent selector in onyx!
const item = SidebarUtils.getOptionData(fullReport, reportActions, personalDetails, preferredLocale, policy);
@@ -98,7 +110,7 @@ function OptionRowLHNData({
return item;
// Listen parentReportAction to update title of thread report when parentReportAction changed
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [fullReport, reportActions, personalDetails, preferredLocale, policy, parentReportAction]);
+ }, [fullReport, linkedTransaction, reportActions, personalDetails, preferredLocale, policy, parentReportAction]);
useEffect(() => {
if (!optionItem || optionItem.hasDraftComment || !comment || comment.length <= 0 || isFocused) {
@@ -186,6 +198,11 @@ export default React.memo(
key: ({fullReport}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${fullReport.parentReportID}`,
canEvict: false,
},
+ // Ideally, we aim to access only the last transaction for the current report by listening to changes in reportActions.
+ // In some scenarios, a transaction might be created after reportActions have been modified.
+ // This can lead to situations where `lastTransaction` doesn't update and retains the previous value.
+ // However, performance overhead of this is minimized by using memos inside the component.
+ receiptTransactions: {key: ONYXKEYS.COLLECTION.TRANSACTION},
}),
)(OptionRowLHNData),
);
diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx
index c1dd064127a9..aebd63edf559 100644
--- a/src/components/MapView/MapView.tsx
+++ b/src/components/MapView/MapView.tsx
@@ -3,6 +3,7 @@ import {useFocusEffect} from '@react-navigation/native';
import Mapbox, {MapState, MarkerView, setAccessToken} from '@rnmapbox/maps';
import {forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
+import responder from './responder';
import utils from './utils';
import Direction from './Direction';
import CONST from '../../CONST';
@@ -63,6 +64,8 @@ const MapView = forwardRef(({accessToken, style, ma
styleURL={styleURL}
onMapIdle={setMapIdle}
pitchEnabled={pitchEnabled}
+ // eslint-disable-next-line
+ {...responder.panHandlers}
>
(
);
return (
-
+