From 1db16d9aa254f4bf0eaa2a067446196a1f61a28e Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Mon, 26 Feb 2024 17:14:29 +0100 Subject: [PATCH 001/198] feat: create new rate field --- .../MoneyRequestConfirmationList.js | 12 ++++++ ...oraryForRefactorRequestConfirmationList.js | 20 ++++++++++ src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/DistanceRequestUtils.ts | 39 ++++++++++++++++--- 5 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index df2781d3ea89..7bb4bb868c99 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -730,6 +730,18 @@ function MoneyRequestConfirmationList(props) { interactive={!props.isReadOnly} /> )} + {props.isDistanceRequest && ( + Navigation.navigate(ROUTES.MONEY_REQUEST_DISTANCE.getRoute(props.iouType, props.reportID))} + disabled={didConfirm || !isTypeRequest} + interactive={!props.isReadOnly} + /> + )} {shouldShowMerchant && ( {}} + // onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + disabled={didConfirm || !isTypeRequest} + interactive={!isReadOnly} + /> + ), + // TODO: hide when betas ready + shouldShow: isDistanceRequest, + isSupplementary: true, + }, { item: ( Date: Mon, 26 Feb 2024 19:20:47 +0100 Subject: [PATCH 002/198] feat: create rate selection page --- src/ROUTES.ts | 5 ++ src/SCREENS.ts | 1 + ...oraryForRefactorRequestConfirmationList.js | 23 +++---- .../AppNavigator/ModalStackNavigators.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 1 + .../iou/request/step/IOURequestStepRate.tsx | 65 +++++++++++++++++++ 6 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 src/pages/iou/request/step/IOURequestStepRate.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index a8786bda3ffb..c01fdac72aef 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -369,6 +369,11 @@ const ROUTES = { getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => getUrlWithBackToParam(`create/${iouType}/distance/${transactionID}/${reportID}`, backTo), }, + MONEY_REQUEST_STEP_RATE: { + route: ':action/:iouType/rate/:transactionID/:reportID', + getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`${action}/${iouType}/rate/${transactionID}/${reportID}`, backTo), + }, MONEY_REQUEST_STEP_MERCHANT: { route: ':action/:iouType/merchant/:transactionID/:reportID', getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 520895c89c98..ddaf52a03a59 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -143,6 +143,7 @@ const SCREENS = { STEP_DATE: 'Money_Request_Step_Date', STEP_DESCRIPTION: 'Money_Request_Step_Description', STEP_DISTANCE: 'Money_Request_Step_Distance', + STEP_RATE: 'Money_Request_Step_Rate', STEP_MERCHANT: 'Money_Request_Step_Merchant', STEP_PARTICIPANTS: 'Money_Request_Step_Participants', STEP_SCAN: 'Money_Request_Step_Scan', diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 6fb0e8bc0f08..f2fcb3ac96eb 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -1,10 +1,10 @@ -import {useIsFocused} from '@react-navigation/native'; -import {format} from 'date-fns'; +import { useIsFocused } from '@react-navigation/native'; +import { format } from 'date-fns'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {Fragment, useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react'; -import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import React, { Fragment, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'; +import { View } from 'react-native'; +import { withOnyx } from 'react-native-onyx'; import _ from 'underscore'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; @@ -21,9 +21,9 @@ import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import playSound, {SOUNDS} from '@libs/Sound'; +import playSound, { SOUNDS } from '@libs/Sound'; import * as TransactionUtils from '@libs/TransactionUtils'; -import {policyPropTypes} from '@pages/workspace/withPolicy'; +import { policyPropTypes } from '@pages/workspace/withPolicy'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -44,7 +44,8 @@ import Switch from './Switch'; import tagPropTypes from './tagPropTypes'; import Text from './Text'; import transactionPropTypes from './transactionPropTypes'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from './withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails, { withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes } from './withCurrentUserPersonalDetails'; + const propTypes = { /** Callback to inform parent modal of success */ @@ -701,9 +702,9 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ description={translate('common.rate')} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} - // TODO: Add the onPress function - onPress={() => {}} - // onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + onPress={() => { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_RATE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + }} disabled={didConfirm || !isTypeRequest} interactive={!isReadOnly} /> diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index 2be262aa5f0f..941900c3bf2c 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -89,6 +89,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../pages/iou/request/step/IOURequestStepDate').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_DESCRIPTION]: () => require('../../../pages/iou/request/step/IOURequestStepDescription').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_DISTANCE]: () => require('../../../pages/iou/request/step/IOURequestStepDistance').default as React.ComponentType, + [SCREENS.MONEY_REQUEST.STEP_RATE]: () => require('../../../pages/iou/request/step/IOURequestStepRate').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_MERCHANT]: () => require('../../../pages/iou/request/step/IOURequestStepMerchant').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS]: () => require('../../../pages/iou/request/step/IOURequestStepParticipants').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_SCAN]: () => require('../../../pages/iou/request/step/IOURequestStepScan').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 48d649cc4dd9..9163c0b9bfe5 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -416,6 +416,7 @@ const config: LinkingOptions['config'] = { [SCREENS.MONEY_REQUEST.STEP_DATE]: ROUTES.MONEY_REQUEST_STEP_DATE.route, [SCREENS.MONEY_REQUEST.STEP_DESCRIPTION]: ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.route, [SCREENS.MONEY_REQUEST.STEP_DISTANCE]: ROUTES.MONEY_REQUEST_STEP_DISTANCE.route, + [SCREENS.MONEY_REQUEST.STEP_RATE]: ROUTES.MONEY_REQUEST_STEP_RATE.route, [SCREENS.MONEY_REQUEST.HOLD]: ROUTES.MONEY_REQUEST_HOLD_REASON.route, [SCREENS.MONEY_REQUEST.STEP_MERCHANT]: ROUTES.MONEY_REQUEST_STEP_MERCHANT.route, [SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS]: ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.route, diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx new file mode 100644 index 000000000000..347e627624e9 --- /dev/null +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import {withOnyx} from 'react-native-onyx'; +import SelectionList from '@components/SelectionList'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import StepScreenWrapper from '@pages/iou/request/step/StepScreenWrapper'; +import type {Route} from '@src/ROUTES'; + +type Props = { + // eslint-disable-next-line react/no-unused-prop-types + lastSelectedDistanceRate: string; + /** The route object passed to this screen */ + route: { + /** The params passed to this screen */ + params: { + /** The route to go back to */ + backTo: Route; + }; + }; +}; + +const mockRates = [ + {text: 'Default Rate', alternateText: '0.656/mile', keyForList: 'DefaultRate'}, + {text: 'Custom Rate', alternateText: '0.700/mile', keyForList: 'CustomRate'}, +]; + +function IOURequestStepRate({ + route: { + params: {backTo}, + }, +}: Props) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + return ( + Navigation.goBack(backTo)} + shouldShowWrapper={Boolean(backTo)} + testID="rate" + > + {translate('themePage.chooseThemeBelowOrSync')} + + {}} + initiallyFocusedOptionKey="DefaultRate" + /> + + ); +} + +IOURequestStepRate.displayName = 'IOURequestStepRate'; + +// export default withOnyx({ +// lastSelectedDistanceRate: { +// key: 'ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATE', +// }, +// })(IOURequestStepRate); + +export default IOURequestStepRate; From b3e1f48577d3cd8e1b4c041ce661ae684507fdd0 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 29 Feb 2024 10:07:54 +0100 Subject: [PATCH 003/198] feat: add translation for the rate page --- src/languages/en.ts | 1 + src/languages/es.ts | 2 ++ src/pages/iou/request/step/IOURequestStepRate.tsx | 6 +++--- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 920705883b02..d904a52c1b7a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -696,6 +696,7 @@ export default { set: 'set', changed: 'changed', removed: 'removed', + chooseARate: 'Choose a rate to use below', }, notificationPreferencesPage: { header: 'Notification preferences', diff --git a/src/languages/es.ts b/src/languages/es.ts index 1b858f707851..842f357639c8 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -691,6 +691,8 @@ export default { set: 'estableció', changed: 'cambió', removed: 'eliminó', + // TODO: check if this is the correct translation + chooseARate: 'Elige una tarifa para utilizar a continuación', }, notificationPreferencesPage: { header: 'Preferencias de avisos', diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx index 347e627624e9..d82238f2c7ea 100644 --- a/src/pages/iou/request/step/IOURequestStepRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -23,8 +23,8 @@ type Props = { }; const mockRates = [ - {text: 'Default Rate', alternateText: '0.656/mile', keyForList: 'DefaultRate'}, - {text: 'Custom Rate', alternateText: '0.700/mile', keyForList: 'CustomRate'}, + {text: 'Default Rate', alternateText: '$0.656 / mile', keyForList: 'DefaultRate'}, + {text: 'Custom Rate', alternateText: '$0.700 / mile', keyForList: 'CustomRate'}, ]; function IOURequestStepRate({ @@ -42,7 +42,7 @@ function IOURequestStepRate({ shouldShowWrapper={Boolean(backTo)} testID="rate" > - {translate('themePage.chooseThemeBelowOrSync')} + {translate('iou.chooseARate')} Date: Thu, 29 Feb 2024 17:59:45 +0100 Subject: [PATCH 004/198] feat: wip - display rates for policy --- ...oraryForRefactorRequestConfirmationList.js | 4 +- src/libs/DistanceRequestUtils.ts | 119 +++++++++++++++--- .../iou/request/step/IOURequestStepRate.tsx | 49 +++++--- src/types/onyx/Policy.ts | 4 +- 4 files changed, 139 insertions(+), 37 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 46e4cb3d6733..267662f07157 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -681,7 +681,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY, + callback: (policy, key) => { + if (!policy || !key || !policy.name) { + return; + } + + policies[key] = policy; + }, +}); + /** * Retrieves the default mileage rate based on a given policy. * @@ -67,22 +81,21 @@ function convertDistanceUnit(distanceInMeters: number, unit: Unit): number { */ function getRoundedDistanceInUnits(distanceInMeters: number, unit: Unit): string { const convertedDistance = convertDistanceUnit(distanceInMeters, unit); - return convertedDistance.toFixed(2); + // TODO: add logic for currencies for which we need to round to 4 decimals + return convertedDistance.toFixed(3); } /** -* @param hasRoute Whether the route exists for the distance request -* @param distanceInMeters Distance traveled -* @param unit Unit that should be used to display the distance -* @param rate Expensable amount allowed per unit -* @param currency The currency associated with the rate -* @param translate Translate function -* @param toLocaleDigit Function to convert to localized digit -* @returns A string that describes the distance traveled and the rate used for expense calculation -*/ + * @param hasRoute Whether the route exists for the distance request + * @param unit Unit that should be used to display the distance + * @param rate Expensable amount allowed per unit + * @param currency The currency associated with the rate + * @param translate Translate function + * @param toLocaleDigit Function to convert to localized digit + * @returns A string that describes the distance traveled and the rate used for expense calculation + */ function getRateForDisplay( hasRoute: boolean, - distanceInMeters: number, unit: Unit, rate: number, currency: string, @@ -101,6 +114,28 @@ function getRateForDisplay( return `${currencySymbol}${ratePerUnit} / ${singularDistanceUnit}`; } +// TODO: I wonder if it would be better to rfactor these functions to pass params in an object +/** + * @param hasRoute Whether the route exists for the distance request + * @param distanceInMeters Distance traveled + * @param unit Unit that should be used to display the distance + * @param rate Expensable amount allowed per unit + * @param translate Translate function + * @returns A string that describes the distance traveled + */ +function getDistanceForDisplay(hasRoute: boolean, distanceInMeters: number, unit: Unit, translate: LocaleContextProps['translate']): string { + if (!hasRoute) { + return translate('iou.routePending'); + } + + const distanceInUnits = getRoundedDistanceInUnits(distanceInMeters, unit); + const distanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.miles') : translate('common.kilometers'); + const singularDistanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer'); + const unitString = distanceInUnits === '1' ? singularDistanceUnit : distanceUnit; + + return `${distanceInUnits} ${unitString}`; +} + /** * @param hasRoute Whether the route exists for the distance request * @param distanceInMeters Distance traveled @@ -124,13 +159,52 @@ function getDistanceMerchant( return translate('iou.routePending'); } - const distanceInUnits = getRoundedDistanceInUnits(distanceInMeters, unit); - const distanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.miles') : translate('common.kilometers'); - const singularDistanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer'); - const unitString = distanceInUnits === '1' ? singularDistanceUnit : distanceUnit; - const ratePerUnit = getRateForDisplay(hasRoute, distanceInMeters, unit, rate, currency, translate, toLocaleDigit); + const distanceInUnits = getDistanceForDisplay(hasRoute, distanceInMeters, unit, translate); + const ratePerUnit = getRateForDisplay(hasRoute, unit, rate, currency, translate, toLocaleDigit); + + return `${distanceInUnits} @ ${ratePerUnit}`; +} + +/** + * Retrieves the mileage rates for given policy. + * + * @param policyID - The policy ID from which to extract the mileage rates. + * + * @returns An array of mileage rates or an empty array if not found. + */ +function getMileageRates(policyID?: string): MileageRate[] | [] { + if (!policyID) { + return []; + } + + const mileageRates: MileageRate[] = []; + + const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? null; + + if (!policy || !policy?.customUnits) { + return mileageRates; + } + + const distanceUnit = Object.values(policy.customUnits).find((unit) => unit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE); + if (!distanceUnit?.rates) { + return mileageRates; + } + + const rates = Object.values(distanceUnit.rates); + + for (const rate of rates) { + if (rate.enabled) { + mileageRates.push({ + rate: rate.rate ?? 0, + name: rate.name, + currency: rate.currency ?? 'USD', + unit: distanceUnit.attributes.unit, + customUnitRateID: rate.customUnitRateID, + }); + } + } - return `${distanceInUnits} ${unitString} @ ${ratePerUnit}`; + return mileageRates; } /** @@ -147,4 +221,11 @@ function getDistanceRequestAmount(distance: number, unit: Unit, rate: number): n return Math.round(roundedDistance * rate); } -export default {getDefaultMileageRate, getDistanceMerchant, getDistanceRequestAmount, getRateForDisplay}; +export default { + getDefaultMileageRate, + getDistanceMerchant, + getDistanceRequestAmount, + getRateForDisplay, + getMileageRates, + getDistanceForDisplay, +}; diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx index d82238f2c7ea..3b7889de9651 100644 --- a/src/pages/iou/request/step/IOURequestStepRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -1,17 +1,27 @@ import React from 'react'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import compose from '@libs/compose'; +import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import StepScreenWrapper from '@pages/iou/request/step/StepScreenWrapper'; +import withWritableReportOrNotFound from '@pages/iou/request/step/withWritableReportOrNotFound'; +import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; +import type {Policy} from '@src/types/onyx'; type Props = { // eslint-disable-next-line react/no-unused-prop-types lastSelectedDistanceRate: string; + + /** Policy details */ + policy: OnyxEntry; + /** The route object passed to this screen */ route: { /** The params passed to this screen */ @@ -22,18 +32,22 @@ type Props = { }; }; -const mockRates = [ - {text: 'Default Rate', alternateText: '$0.656 / mile', keyForList: 'DefaultRate'}, - {text: 'Custom Rate', alternateText: '$0.700 / mile', keyForList: 'CustomRate'}, -]; - function IOURequestStepRate({ + policy, route: { params: {backTo}, }, }: Props) { const styles = useThemeStyles(); - const {translate} = useLocalize(); + const {translate, toLocaleDigit} = useLocalize(); + const rates = DistanceRequestUtils.getMileageRates(policy?.id); + + const data = rates.map((rate) => ({ + text: rate.name ?? '', + alternateText: DistanceRequestUtils.getRateForDisplay(true, rate.unit, rate.rate, rate.currency, translate, toLocaleDigit), + keyForList: rate.name ?? '', + value: rate.customUnitRateID, + })); return ( {translate('iou.chooseARate')} {}} - initiallyFocusedOptionKey="DefaultRate" + // TODO: change for lastSelectedDistanceRates + initiallyFocusedOptionKey="Default Rate" /> ); @@ -56,10 +71,14 @@ function IOURequestStepRate({ IOURequestStepRate.displayName = 'IOURequestStepRate'; -// export default withOnyx({ -// lastSelectedDistanceRate: { -// key: 'ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATE', -// }, -// })(IOURequestStepRate); - -export default IOURequestStepRate; +export default compose( + withWritableReportOrNotFound, + withOnyx({ + policy: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, + }, + // lastSelectedDistanceRates: { + // key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, + // }, + }), +)(IOURequestStepRate); diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 1eece2d3a1e0..8e408951a84f 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -19,8 +19,10 @@ type Attributes = { type MileageRate = { unit: Unit; - rate?: number; currency: string; + customUnitRateID?: string; + rate?: number; + name?: string; }; type CustomUnit = OnyxCommon.OnyxValueWithOfflineFeedback<{ From cde1d19d37cdd0203ca6023612dfe12ea0f46537 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 1 Mar 2024 16:12:00 +0100 Subject: [PATCH 005/198] feat: wip - get rate for p2p --- ...oraryForRefactorRequestConfirmationList.js | 52 ++++++++++++++++--- src/libs/DistanceRequestUtils.ts | 13 +++-- ...yForRefactorRequestParticipantsSelector.js | 4 +- 3 files changed, 55 insertions(+), 14 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 267662f07157..5a7019e63428 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -239,6 +239,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ session: {accountID}, shouldShowSmartScanFields, transaction, + lastSelectedDistanceRate = {}, }) { const theme = useTheme(); const styles = useThemeStyles(); @@ -250,6 +251,12 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const isTypeSend = iouType === CONST.IOU.TYPE.SEND; const {unit, rate, currency} = mileageRate; + + // const rate = transaction.comment.customUnit.customUnitRateID === '_FAKE_P2P_ID_' + + // const {unit, rate, currency} = lastSelectedDistanceRate || mileageRate; + + const distance = lodashGet(transaction, 'routes.route0.distance', 0); const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0; const taxRates = lodashGet(policy, 'taxRates', {}); @@ -266,6 +273,8 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const shouldShowDate = shouldShowSmartScanFields || isDistanceRequest; const shouldShowMerchant = shouldShowSmartScanFields && !isDistanceRequest; + console.log({transaction}); + const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); // A flag for showing the tags field @@ -622,6 +631,10 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ ); }, [isReadOnly, iouType, selectedParticipants.length, confirm, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, formError, styles.ph1, styles.mb2]); + // TODO: change for a value from usePermissions [will be added in this PR https://github.com/Expensify/App/pull/37185] + // change for true for development + const canUseP2PDistanceRequests = true; + // An intermediate structure that helps us classify the fields as "primary" and "supplementary". // The primary fields are always shown to the user, while an extra action is needed to reveal the supplementary ones. const classifiedFields = [ @@ -681,7 +694,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ ), - shouldShow: isDistanceRequest, + shouldShow: isDistanceRequest && !canUseP2PDistanceRequests, isSupplementary: true, }, { item: ( Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + disabled={didConfirm || !isTypeRequest} + interactive={!isReadOnly} + /> + ), + shouldShow: isDistanceRequest && canUseP2PDistanceRequests, + isSupplementary: true, + }, + { + item: ( + { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_RATE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_RATE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()), + ); }} disabled={didConfirm || !isTypeRequest} - interactive={!isReadOnly} + interactive={!isReadOnly && isPolicyExpenseChat} /> ), - // TODO: hide when betas ready - shouldShow: isDistanceRequest, + shouldShow: isDistanceRequest && canUseP2PDistanceRequests, isSupplementary: true, }, { @@ -950,12 +982,16 @@ export default compose( policyTags: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, }, + // TODO: add NVP_LAST_SELECTED_DISTANCE_RATES + // lastSelectedDistanceRates: { + // key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, + // }, mileageRate: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, selector: DistanceRequestUtils.getDefaultMileageRate, }, policy: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - }, + } }), )(MoneyTemporaryForRefactorRequestConfirmationList); diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index a605ace8da87..191edd93bfb9 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -12,6 +12,8 @@ type DefaultMileageRate = { rate?: number; currency?: string; unit: Unit; + name?: string; + customUnitRateID?: string; }; const policies: OnyxCollection = {}; @@ -55,6 +57,8 @@ function getDefaultMileageRate(policy: OnyxEntry): DefaultMileageRate | rate: distanceRate.rate, currency: distanceRate.currency, unit: distanceUnit.attributes.unit, + name: distanceRate.name, + customUnitRateID: distanceRate.customUnitRateID, }; } @@ -91,6 +95,7 @@ function getRoundedDistanceInUnits(distanceInMeters: number, unit: Unit): string return convertedDistance.toFixed(3); } +// TODO: I wonder if it would be better to refactor these functions to pass params in an object /** * @param hasRoute Whether the route exists for the distance request * @param unit Unit that should be used to display the distance @@ -120,7 +125,7 @@ function getRateForDisplay( return `${currencySymbol}${ratePerUnit} / ${singularDistanceUnit}`; } -// TODO: I wonder if it would be better to rfactor these functions to pass params in an object +// TODO: this function will be added in https://github.com/Expensify/App/pull/37185, remove it to avoid conflicts /** * @param hasRoute Whether the route exists for the distance request * @param distanceInMeters Distance traveled @@ -129,8 +134,8 @@ function getRateForDisplay( * @param translate Translate function * @returns A string that describes the distance traveled */ -function getDistanceForDisplay(hasRoute: boolean, distanceInMeters: number, unit: Unit, translate: LocaleContextProps['translate']): string { - if (!hasRoute) { +function getDistanceForDisplay(hasRoute: boolean, distanceInMeters: number, unit: Unit, rate: number, translate: LocaleContextProps['translate']): string { + if (!hasRoute || !rate) { return translate('iou.routePending'); } @@ -165,7 +170,7 @@ function getDistanceMerchant( return translate('iou.routePending'); } - const distanceInUnits = getDistanceForDisplay(hasRoute, distanceInMeters, unit, translate); + const distanceInUnits = getDistanceForDisplay(hasRoute, distanceInMeters, unit, rate, translate); const ratePerUnit = getRateForDisplay(hasRoute, unit, rate, currency, translate, toLocaleDigit); return `${distanceInUnits} @ ${ratePerUnit}`; diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 2865316b7fd5..b42b94afc686 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -121,7 +121,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ iouType === CONST.IOU.TYPE.REQUEST, // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features. - iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, + true, false, {}, [], @@ -131,7 +131,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ // We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now. // This functionality is being built here: https://github.com/Expensify/App/issues/23291 - iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, + true, false, ); From c3cb80bf507de6c4922b28e170893cdcaa126ebb Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 1 Mar 2024 17:38:59 +0100 Subject: [PATCH 006/198] feat: wip - new rate field logic --- src/CONST.ts | 7 ++++ ...oraryForRefactorRequestConfirmationList.js | 40 +++++++++---------- src/libs/DistanceRequestUtils.ts | 39 +++++++++--------- src/libs/actions/IOU.ts | 13 ++++++ .../iou/request/step/IOURequestStepRate.tsx | 12 +++++- 5 files changed, 69 insertions(+), 42 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 8d4eaac44a38..2bd047f6bf51 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1398,6 +1398,13 @@ const CONST = { RATE_DECIMALS: 3, }, + // TODO: remove this mock when https://github.com/Expensify/App/issues/36982 is done + CURRENCY_TO_DEFAULT_MILEAGE_RATE: { + USD: { unit: "mile", rate: 0.5 }, + EUR: { unit: "kilometer", rate: 0.8 }, + GBP: { unit: "mile", rate: 0.45 }, + }, + TERMS: { CFPB_PREPAID: 'cfpb.gov/prepaid', CFPB_COMPLAINT: 'cfpb.gov/complaint', diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 5a7019e63428..9c2d77239015 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -1,10 +1,10 @@ -import { useIsFocused } from '@react-navigation/native'; -import { format } from 'date-fns'; +import {useIsFocused} from '@react-navigation/native'; +import {format} from 'date-fns'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, { Fragment, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'; -import { View } from 'react-native'; -import { withOnyx } from 'react-native-onyx'; +import React, {useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react'; +import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; @@ -21,9 +21,9 @@ import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import playSound, { SOUNDS } from '@libs/Sound'; +import playSound, {SOUNDS} from '@libs/Sound'; import * as TransactionUtils from '@libs/TransactionUtils'; -import { policyPropTypes } from '@pages/workspace/withPolicy'; +import {policyPropTypes} from '@pages/workspace/withPolicy'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -44,8 +44,7 @@ import Switch from './Switch'; import tagPropTypes from './tagPropTypes'; import Text from './Text'; import transactionPropTypes from './transactionPropTypes'; -import withCurrentUserPersonalDetails, { withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes } from './withCurrentUserPersonalDetails'; - +import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from './withCurrentUserPersonalDetails'; const propTypes = { /** Callback to inform parent modal of success */ @@ -239,7 +238,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ session: {accountID}, shouldShowSmartScanFields, transaction, - lastSelectedDistanceRate = {}, + mileageRates, }) { const theme = useTheme(); const styles = useThemeStyles(); @@ -250,12 +249,11 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT; const isTypeSend = iouType === CONST.IOU.TYPE.SEND; + // TODO: uncomment after Splits and P2P are enabled https://github.com/Expensify/App/pull/37185, mileageRate prop should be be removed + // const mileageRate = transaction.comment.customUnit.customUnitRateID === '_FAKE_P2P_ID_' ? DistanceRequestUtils.getRateForP2P(policy.outputCurrency) : mileageRates[transaction.comment.customUnit.customUnitRateID]; const {unit, rate, currency} = mileageRate; - // const rate = transaction.comment.customUnit.customUnitRateID === '_FAKE_P2P_ID_' - - // const {unit, rate, currency} = lastSelectedDistanceRate || mileageRate; - + // will hardcoded rates will have a currency property? If not we have to get currency other way const distance = lodashGet(transaction, 'routes.route0.distance', 0); const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0; @@ -273,8 +271,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const shouldShowDate = shouldShowSmartScanFields || isDistanceRequest; const shouldShowMerchant = shouldShowSmartScanFields && !isDistanceRequest; - console.log({transaction}); - const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); // A flag for showing the tags field @@ -633,7 +629,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ // TODO: change for a value from usePermissions [will be added in this PR https://github.com/Expensify/App/pull/37185] // change for true for development - const canUseP2PDistanceRequests = true; + const canUseP2PDistanceRequests = false; // An intermediate structure that helps us classify the fields as "primary" and "supplementary". // The primary fields are always shown to the user, while an extra action is needed to reveal the supplementary ones. @@ -982,16 +978,16 @@ export default compose( policyTags: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, }, - // TODO: add NVP_LAST_SELECTED_DISTANCE_RATES - // lastSelectedDistanceRates: { - // key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, - // }, mileageRate: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, selector: DistanceRequestUtils.getDefaultMileageRate, }, + mileageRates: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + selector: (policy) => DistanceRequestUtils.getMileageRates(policy ? policy.id : ''), + }, policy: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - } + }, }), )(MoneyTemporaryForRefactorRequestConfirmationList); diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index 191edd93bfb9..dd95612af3e6 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -1,4 +1,4 @@ -import type {OnyxEntry, OnyxCollection} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; @@ -183,13 +183,13 @@ function getDistanceMerchant( * * @returns An array of mileage rates or an empty array if not found. */ -function getMileageRates(policyID?: string): DefaultMileageRate[] | [] { +function getMileageRates(policyID?: string): Record { + const mileageRates = {}; + if (!policyID) { - return []; + return mileageRates; } - const mileageRates: DefaultMileageRate[] = []; - const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? null; if (!policy || !policy?.customUnits) { @@ -201,23 +201,25 @@ function getMileageRates(policyID?: string): DefaultMileageRate[] | [] { return mileageRates; } - const rates = Object.values(distanceUnit.rates); - - for (const rate of rates) { - if (rate.enabled) { - mileageRates.push({ - rate: rate.rate ?? 0, - name: rate.name, - currency: rate.currency ?? 'USD', - unit: distanceUnit.attributes.unit, - customUnitRateID: rate.customUnitRateID, - }); - } - } + Object.entries(distanceUnit.rates).forEach(([rateID, rate]) => { + // TODO: fix TS error + mileageRates[rateID] = { + rate: rate.rate, + currency: rate.currency, + unit: distanceUnit.attributes.unit, + name: rate.name, + customUnitRateID: rate.customUnitRateID, + }; + }); return mileageRates; } +// TODO: probably will need to be changed +function getRateForP2P(currency) { + return CONST.CURRENCY_TO_DEFAULT_MILEAGE_RATE[currency] ?? CONST.CURRENCY_TO_DEFAULT_MILEAGE_RATE.USD; +} + /** * Calculates the request amount based on distance, unit, and rate. * @@ -239,4 +241,5 @@ export default { getRateForDisplay, getMileageRates, getDistanceForDisplay, + getRateForP2P, }; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 5f9657755b02..91dd95b12592 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -355,6 +355,17 @@ function setMoneyRequestReceipt(transactionID: string, source: string, filename: }); } +/** Set the last selected distance rate for policy */ +// TODO: probably need to be changed +function setLastSelectedDistanceRates(policyID: string, rateID: string) { + Onyx.merge('lastSelectedDistanceRates', {[policyID]: rateID}); +} + +/** Update transaction distance rate */ +function updateDistanceRequestRate(transactionID: string, rateID: string) { + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {comment: {customUnit: {customUnitRateID: rateID}}}); +} + /** Reset money request info from the store with its initial value */ function resetMoneyRequestInfo(id = '') { // Disabling this line since currentDate can be an empty string @@ -4327,4 +4338,6 @@ export { cancelPayment, navigateToStartStepIfScanFileCannotBeRead, savePreferredPaymentMethod, + setLastSelectedDistanceRates, + updateDistanceRequestRate, }; diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx index 3b7889de9651..a2cf7a0af1c8 100644 --- a/src/pages/iou/request/step/IOURequestStepRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -9,6 +9,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; +import * as IOU from '@libs/actions/IOU'; import StepScreenWrapper from '@pages/iou/request/step/StepScreenWrapper'; import withWritableReportOrNotFound from '@pages/iou/request/step/withWritableReportOrNotFound'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -42,13 +43,20 @@ function IOURequestStepRate({ const {translate, toLocaleDigit} = useLocalize(); const rates = DistanceRequestUtils.getMileageRates(policy?.id); - const data = rates.map((rate) => ({ + const data = Object.values(rates).map((rate) => ({ text: rate.name ?? '', alternateText: DistanceRequestUtils.getRateForDisplay(true, rate.unit, rate.rate, rate.currency, translate, toLocaleDigit), keyForList: rate.name ?? '', value: rate.customUnitRateID, })); + const selectDistanceRate = (customUnitRateID) => { + IOU.setLastSelectedDistanceRates(policy?.id ?? '', customUnitRateID); + // TODO: get a proper transaction ID + IOU.updateDistanceRequestRate('1', customUnitRateID); + Navigation.goBack(backTo); + } + return ( {}} + onSelectRow={({value}) => selectDistanceRate(value)} // TODO: change for lastSelectedDistanceRates initiallyFocusedOptionKey="Default Rate" /> From 5bf399af20bdb7816bedf1ed40ee37fa83e3fffa Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 1 Mar 2024 17:51:11 +0100 Subject: [PATCH 007/198] fix: revert filtering changes --- .../MoneyTemporaryForRefactorRequestParticipantsSelector.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index b42b94afc686..2865316b7fd5 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -121,7 +121,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ iouType === CONST.IOU.TYPE.REQUEST, // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features. - true, + iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, false, {}, [], @@ -131,7 +131,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ // We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now. // This functionality is being built here: https://github.com/Expensify/App/issues/23291 - true, + iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, false, ); From ef7b30e6a95bc95ed2ead63f27e25636398f7c6b Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 13 Mar 2024 10:19:52 +0100 Subject: [PATCH 008/198] refactor: use canUseP2PDistanceRequests from usePermissions --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 182aa5464772..7333d405363b 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -664,10 +664,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ ); }, [isReadOnly, iouType, selectedParticipants.length, confirm, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, formError, styles.ph1, styles.mb2]); - // TODO: change for a value from usePermissions [will be added in this PR https://github.com/Expensify/App/pull/37185] - // change for true for development - const canUseP2PDistanceRequests = false; - // An intermediate structure that helps us classify the fields as "primary" and "supplementary". // The primary fields are always shown to the user, while an extra action is needed to reveal the supplementary ones. const classifiedFields = [ From 6f3afddef1d036658a48b38ca8f0caf6c696c85f Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 13 Mar 2024 17:28:24 +0100 Subject: [PATCH 009/198] fix: remove mock rates --- src/CONST.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 9b2502cb17d1..d4fbd0ff6ef3 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1431,13 +1431,6 @@ const CONST = { FAKE_P2P_ID: '_FAKE_P2P_ID_', }, - // TODO: remove this mock when https://github.com/Expensify/App/issues/36982 is done - CURRENCY_TO_DEFAULT_MILEAGE_RATE: { - USD: { unit: "mile", rate: 0.5 }, - EUR: { unit: "kilometer", rate: 0.8 }, - GBP: { unit: "mile", rate: 0.45 }, - }, - TERMS: { CFPB_PREPAID: 'cfpb.gov/prepaid', CFPB_COMPLAINT: 'cfpb.gov/complaint', From 50174f38629f23ef3363e51613755b05e30ec3d0 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 13 Mar 2024 17:29:24 +0100 Subject: [PATCH 010/198] feat: create function for getting personal policy --- src/libs/PolicyUtils.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 4689fd03ebd0..6a3829b10934 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1,5 +1,6 @@ import Str from 'expensify-common/lib/str'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -12,6 +13,14 @@ import Navigation from './Navigation/Navigation'; type MemberEmailsToAccountIDs = Record; type UnitRate = {rate: number}; +let allPolicies: OnyxCollection; + +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY, + waitForCollectionCallback: true, + callback: (value) => (allPolicies = value), +}); + /** * Filter out the active policies, which will exclude policies with pending deletion * These are policies that we can use to create reports with in NewDot. @@ -265,6 +274,10 @@ function goBackFromInvalidPolicy() { Navigation.navigateWithSwitchPolicyID({route: ROUTES.ALL_SETTINGS}); } +function getPersonalPolicy() { + return Object.values(allPolicies ?? {}).find((policy) => policy?.type === CONST.POLICY.TYPE.PERSONAL); +} + export { getActivePolicies, hasAccountingConnections, @@ -295,6 +308,7 @@ export { getPathWithoutPolicyID, getPolicyMembersByIdWithoutCurrentUser, goBackFromInvalidPolicy, + getPersonalPolicy }; export type {MemberEmailsToAccountIDs}; From eb3aff8ee97f1f055d22376ea7004ba575ff1cb9 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 14 Mar 2024 19:15:31 +0100 Subject: [PATCH 011/198] feat: add rate field to MoneyRequestView --- .../ReportActionItem/MoneyRequestView.tsx | 68 +++++++++++++++---- src/libs/DistanceRequestUtils.ts | 17 ++--- 2 files changed, 63 insertions(+), 22 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 70c65d1d66ce..bcaa7f830925 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -21,6 +21,7 @@ import type {ViolationField} from '@hooks/useViolations'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CardUtils from '@libs/CardUtils'; import * as CurrencyUtils from '@libs/CurrencyUtils'; +import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; @@ -89,8 +90,8 @@ function MoneyRequestView({ const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {isSmallScreenWidth} = useWindowDimensions(); - const {translate} = useLocalize(); - const {canUseViolations} = usePermissions(); + const {translate, toLocaleDigit} = useLocalize(); + const {canUseViolations, canUseP2PDistanceRequests} = usePermissions(); const parentReportAction = parentReportActions?.[report.parentReportActionID ?? ''] ?? null; const moneyRequestReport = parentReport; const { @@ -151,6 +152,20 @@ function MoneyRequestView({ let amountDescription = `${translate('iou.amount')}`; + const hasRoute = TransactionUtils.hasRoute(transaction); + const distance = transaction?.routes?.route0?.distance ?? 0; + const rateID = transaction?.comment.customUnit?.customUnitRateID ?? '0'; + + const rates = DistanceRequestUtils.getMileageRates(policy?.id); + const {unit, currency, rate} = rates[rateID as string] ?? { + unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, + currency: CONST.CURRENCY.USD, + rate: 0, + }; + + const rateToDisplay = DistanceRequestUtils.getRateForDisplay(hasRoute, unit, rate, currency, translate, toLocaleDigit); + const distanceToDisplay = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate); + const saveBillable = useCallback( (newBillable: boolean) => { // If the value hasn't changed, don't request to save changes on the server and just close the modal @@ -236,6 +251,44 @@ function MoneyRequestView({ [transactionAmount, isSettled, isCancelled, isPolicyExpenseChat, isEmptyMerchant, transactionDate, hasErrors, canUseViolations, hasViolations, translate, getViolationsForField], ); + const distanceRequestFields = canUseP2PDistanceRequests ? ( + <> + + Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DISTANCE))} + /> + + + {}} + /> + + + ) : ( + + Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DISTANCE))} + /> + + ) + return ( @@ -320,16 +373,7 @@ function MoneyRequestView({ /> {isDistanceRequest ? ( - - Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DISTANCE))} - /> - + distanceRequestFields ) : ( { - const mileageRates = {}; + const mileageRates: Record = {}; if (!policyID) { return mileageRates; @@ -202,7 +201,6 @@ function getMileageRates(policyID?: string): Record } Object.entries(distanceUnit.rates).forEach(([rateID, rate]) => { - // TODO: fix TS error mileageRates[rateID] = { rate: rate.rate, currency: rate.currency, @@ -215,8 +213,7 @@ function getMileageRates(policyID?: string): Record return mileageRates; } -// TODO: probably will need to be changed -function getRateForP2P(currency) { +function getRateForP2P(currency: string) { return CONST.CURRENCY_TO_DEFAULT_MILEAGE_RATE[currency] ?? CONST.CURRENCY_TO_DEFAULT_MILEAGE_RATE.USD; } From 7dc206cf66ae8f96ddea9883b16334618b4a5379 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Mon, 18 Mar 2024 19:57:19 -0300 Subject: [PATCH 012/198] feat: change customRateID when participant changes, other improvements --- ...oraryForRefactorRequestConfirmationList.js | 25 ++++--- .../ReportActionItem/MoneyRequestView.tsx | 72 +++++++++---------- src/libs/DistanceRequestUtils.ts | 3 +- src/libs/TransactionUtils.ts | 9 +++ src/libs/actions/IOU.ts | 6 +- ...yForRefactorRequestParticipantsSelector.js | 2 +- .../step/IOURequestStepParticipants.js | 35 ++++++++- .../iou/request/step/IOURequestStepRate.tsx | 28 +++++--- 8 files changed, 116 insertions(+), 64 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 93831606416e..ce5668924271 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -224,7 +224,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ isReadOnly, isScanRequest, listStyles, - mileageRate, + mileageRates, onConfirm, onSelectParticipant, onSendMoney, @@ -242,7 +242,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ session: {accountID}, shouldShowSmartScanFields, transaction, - mileageRates, }) { const theme = useTheme(); const styles = useThemeStyles(); @@ -254,14 +253,19 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const isTypeSend = iouType === CONST.IOU.TYPE.SEND; const canEditDistance = isTypeRequest || (canUseP2PDistanceRequests && isTypeSplit); - // TODO: uncomment after Splits and P2P are enabled https://github.com/Expensify/App/pull/37185, mileageRate prop should be be removed - // const mileageRate = transaction.comment.customUnit.customUnitRateID === '_FAKE_P2P_ID_' ? DistanceRequestUtils.getRateForP2P(policy.outputCurrency) : mileageRates[transaction.comment.customUnit.customUnitRateID]; - const {unit, rate, currency} = mileageRate; + const personalPolicy = policyID === CONST.POLICY.ID_FAKE ? PolicyUtils.getPersonalPolicy() : policy; + const mileageRate = TransactionUtils.isCustomUnitRateIDForP2P(transaction) + ? DistanceRequestUtils.getRateForP2P(personalPolicy.outputCurrency) + : mileageRates[transaction.comment.customUnit.customUnitRateID]; - // will hardcoded rates will have a currency property? If not we have to get currency other way + const {unit, rate} = mileageRate || { + unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, + rate: CONST.CUSTOM_UNITS.MILEAGE_IRS_RATE * 100, + }; + + const currency = personalPolicy.outputCurrency; const distance = lodashGet(transaction, 'routes.route0.distance', 0); - const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0; const taxRates = lodashGet(policy, 'taxRates', {}); // A flag for showing the categories field @@ -292,7 +296,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const formattedAmount = isDistanceRequestWithPendingRoute ? '' : CurrencyUtils.convertToDisplayString( - shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : iouAmount, + isDistanceRequest && iouAmount === 0 ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : iouAmount, isDistanceRequest ? currency : iouCurrencyCode, ); const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction.taxAmount, iouCurrencyCode); @@ -359,13 +363,13 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ }, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit, isMerchantRequired, merchantError]); useEffect(() => { - if (!shouldCalculateDistanceAmount) { + if (!isDistanceRequest) { return; } const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate); IOU.setMoneyRequestAmount_temporaryForRefactor(transaction.transactionID, amount, currency); - }, [shouldCalculateDistanceAmount, distance, rate, unit, transaction, currency]); + }, [isDistanceRequest, distance, rate, unit, transaction, currency]); /** * Returns the participants with amount @@ -757,7 +761,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ item: ( - - Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DISTANCE))} - /> - - - {}} - /> - - - ) : ( - - Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DISTANCE))} - /> - - ) + <> + + Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DISTANCE))} + /> + + + {}} + /> + + + ) : ( + + Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DISTANCE))} + /> + + ); return ( diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index e954ce4d266d..28714a92ed84 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -91,8 +91,7 @@ function convertDistanceUnit(distanceInMeters: number, unit: Unit): number { */ function getRoundedDistanceInUnits(distanceInMeters: number, unit: Unit): string { const convertedDistance = convertDistanceUnit(distanceInMeters, unit); - // TODO: add logic for currencies for which we need to round to 4 decimals - return convertedDistance.toFixed(3); + return convertedDistance.toFixed(2); } // TODO: I wonder if it would be better to refactor these functions to pass params in an object diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index bc94c8fee8fc..f694199fbeb4 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -584,6 +584,14 @@ function getEnabledTaxRateCount(options: TaxRates) { return Object.values(options).filter((option: TaxRate) => !option.isDisabled).length; } +/** + * Check if the customUnitRateID has a value default for P2P distance requests + */ + +function isCustomUnitRateIDForP2P(transaction: Transaction): boolean { + return transaction?.comment?.customUnit?.customUnitRateID === CONST.CUSTOM_UNITS.FAKE_P2P_ID; +} + export { buildOptimisticTransaction, calculateTaxAmount, @@ -633,6 +641,7 @@ export { waypointHasValidAddress, getRecentTransactions, hasViolation, + isCustomUnitRateIDForP2P, }; export type {TransactionChanges}; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 477fb99900b8..31cd49269e2c 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -253,8 +253,9 @@ function initMoneyRequest(reportID: string, policy: OnyxEntry, waypoint1: {}, }; const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID; - if (ReportUtils.isPolicyExpenseChat(report)) { + if (ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isPolicyExpenseChat(parentReport)) { customUnitRateID = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? DistanceRequestUtils.getDefaultMileageRate(policy)?.customUnitRateID ?? ''; } comment.customUnit = {customUnitRateID}; @@ -373,9 +374,8 @@ function setMoneyRequestReceipt(transactionID: string, source: string, filename: } /** Set the last selected distance rate for policy */ -// TODO: probably need to be changed function setLastSelectedDistanceRates(policyID: string, rateID: string) { - Onyx.merge('lastSelectedDistanceRates', {[policyID]: rateID}); + Onyx.merge(ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, {[policyID]: rateID}); } /** Update transaction distance rate */ diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index cdc3e72f98f4..7bdcafef61e6 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -190,7 +190,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const addSingleParticipant = (option) => { onParticipantsAdded([ { - ..._.pick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText'), + ..._.pick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText', 'policyID'), selected: true, }, ]); diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index 5ca465d8fb78..8c92961a28c4 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -1,16 +1,21 @@ import {useNavigation} from '@react-navigation/native'; import lodashGet from 'lodash/get'; +import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; import compose from '@libs/compose'; +import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import * as IOUUtils from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; +import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import MoneyRequestParticipantsSelector from '@pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes'; import StepScreenWrapper from './StepScreenWrapper'; @@ -24,6 +29,9 @@ const propTypes = { /* Onyx Props */ /** The transaction object being modified in Onyx */ transaction: transactionPropTypes, + + // eslint-disable-next-line + lastSelectedDistanceRates: PropTypes.object, }; const defaultProps = { @@ -36,6 +44,7 @@ function IOURequestStepParticipants({ }, transaction, transaction: {participants = []}, + lastSelectedDistanceRates = {}, }) { const {translate} = useLocalize(); const navigation = useNavigation(); @@ -100,6 +109,22 @@ function IOURequestStepParticipants({ } IOU.setMoneyRequestParticipants_temporaryForRefactor(transactionID, val); + + // change customUnitRateID when choosing + if (val[0].isPolicyExpenseChat && TransactionUtils.isCustomUnitRateIDForP2P(transaction)) { + let customUnitRateID = ''; + if (val[0].policyID && lastSelectedDistanceRates[val[0].policyID]) { + customUnitRateID = lastSelectedDistanceRates[val[0].policyID]; + } else { + const policy = ReportUtils.getPolicy(val[0].policyID); + const defaultRate = DistanceRequestUtils.getDefaultMileageRate(policy); + customUnitRateID = defaultRate ? defaultRate.customUnitRateID : ''; + } + IOU.updateDistanceRequestRate(transactionID, customUnitRateID); + } else if (!TransactionUtils.isCustomUnitRateIDForP2P(transaction)) { + IOU.updateDistanceRequestRate(transactionID, CONST.CUSTOM_UNITS.FAKE_P2P_ID); + } + numberOfParticipants.current = val.length; // When multiple participants are selected, the reportID is generated at the end of the confirmation step. @@ -163,4 +188,12 @@ IOURequestStepParticipants.displayName = 'IOURequestStepParticipants'; IOURequestStepParticipants.propTypes = propTypes; IOURequestStepParticipants.defaultProps = defaultProps; -export default compose(withWritableReportOrNotFound, withFullTransactionOrNotFound)(IOURequestStepParticipants); +export default compose( + withWritableReportOrNotFound, + withFullTransactionOrNotFound, + withOnyx({ + lastSelectedDistanceRates: { + key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, + }, + }), +)(IOURequestStepParticipants); diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx index a2cf7a0af1c8..10513f7a44ca 100644 --- a/src/pages/iou/request/step/IOURequestStepRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -6,19 +6,21 @@ import RadioListItem from '@components/SelectionList/RadioListItem'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as IOU from '@libs/actions/IOU'; import compose from '@libs/compose'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; -import * as IOU from '@libs/actions/IOU'; import StepScreenWrapper from '@pages/iou/request/step/StepScreenWrapper'; import withWritableReportOrNotFound from '@pages/iou/request/step/withWritableReportOrNotFound'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import type {Policy} from '@src/types/onyx'; + type Props = { - // eslint-disable-next-line react/no-unused-prop-types - lastSelectedDistanceRate: string; + /** Object of last selected rates for the policies */ + lastSelectedDistanceRates: Record; /** Policy details */ policy: OnyxEntry; @@ -38,11 +40,14 @@ function IOURequestStepRate({ route: { params: {backTo}, }, + lastSelectedDistanceRates = {}, }: Props) { const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); const rates = DistanceRequestUtils.getMileageRates(policy?.id); + const lastSelectedRate = lastSelectedDistanceRates[policy?.id ?? '0'] ?? '0'; + const data = Object.values(rates).map((rate) => ({ text: rate.name ?? '', alternateText: DistanceRequestUtils.getRateForDisplay(true, rate.unit, rate.rate, rate.currency, translate, toLocaleDigit), @@ -50,9 +55,10 @@ function IOURequestStepRate({ value: rate.customUnitRateID, })); - const selectDistanceRate = (customUnitRateID) => { + const initiallyFocusedOption = rates[lastSelectedRate]?.name ?? CONST.CUSTOM_UNITS.DEFAULT_RATE; + + function selectDistanceRate(customUnitRateID = '0') { IOU.setLastSelectedDistanceRates(policy?.id ?? '', customUnitRateID); - // TODO: get a proper transaction ID IOU.updateDistanceRequestRate('1', customUnitRateID); Navigation.goBack(backTo); } @@ -70,8 +76,7 @@ function IOURequestStepRate({ sections={[{data}]} ListItem={RadioListItem} onSelectRow={({value}) => selectDistanceRate(value)} - // TODO: change for lastSelectedDistanceRates - initiallyFocusedOptionKey="Default Rate" + initiallyFocusedOptionKey={initiallyFocusedOption} /> ); @@ -82,11 +87,14 @@ IOURequestStepRate.displayName = 'IOURequestStepRate'; export default compose( withWritableReportOrNotFound, withOnyx({ + // @ts-expect-error TODO: fix when withWritableReportOrNotFound will be migrated to TS policy: { + // @ts-expect-error TODO: fix when withWritableReportOrNotFound will be migrated to TS key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, }, - // lastSelectedDistanceRates: { - // key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, - // }, + // @ts-expect-error TODO: fix when withWritableReportOrNotFound will be migrated to TS + lastSelectedDistanceRates: { + key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, + }, }), )(IOURequestStepRate); From a0227e07854c76bbc9039d5df2971de4c5294a5f Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 19 Mar 2024 09:42:20 -0300 Subject: [PATCH 013/198] fix: run prettier --- src/pages/iou/request/step/IOURequestStepRate.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx index 10513f7a44ca..583fa8c055c7 100644 --- a/src/pages/iou/request/step/IOURequestStepRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -17,7 +17,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import type {Policy} from '@src/types/onyx'; - type Props = { /** Object of last selected rates for the policies */ lastSelectedDistanceRates: Record; From eb350eec27ba9858929864cdc29aada4453b88e1 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 19 Mar 2024 09:53:29 -0300 Subject: [PATCH 014/198] fix: lint fix --- src/pages/iou/request/step/IOURequestStepParticipants.js | 2 +- src/pages/iou/request/step/IOURequestStepRate.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index 8c92961a28c4..a66387709f61 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -137,7 +137,7 @@ function IOURequestStepParticipants({ // When a participant is selected, the reportID needs to be saved because that's the reportID that will be used in the confirmation step. selectedReportID.current = lodashGet(val, '[0].reportID', reportID); }, - [reportID, transactionID, iouType, participants, updateRouteParams], + [iouType, participants, transactionID, transaction, reportID, updateRouteParams, lastSelectedDistanceRates], ); const goToNextStep = useCallback( diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx index 583fa8c055c7..de570ca3c67f 100644 --- a/src/pages/iou/request/step/IOURequestStepRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -10,12 +10,12 @@ import * as IOU from '@libs/actions/IOU'; import compose from '@libs/compose'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; -import StepScreenWrapper from '@pages/iou/request/step/StepScreenWrapper'; -import withWritableReportOrNotFound from '@pages/iou/request/step/withWritableReportOrNotFound'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import type {Policy} from '@src/types/onyx'; +import StepScreenWrapper from './StepScreenWrapper'; +import withWritableReportOrNotFound from './withWritableReportOrNotFound'; type Props = { /** Object of last selected rates for the policies */ From 4b823f46eeb833902249b682b7b211721df433c5 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 23 Mar 2024 21:50:16 +0800 Subject: [PATCH 015/198] only add the saved user on first render --- src/pages/workspace/WorkspaceInvitePage.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 00d7eaa33f6b..fe7d82658e82 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -1,6 +1,6 @@ import {useNavigation} from '@react-navigation/native'; import type {StackNavigationProp, StackScreenProps} from '@react-navigation/stack'; -import React, {useEffect, useMemo, useState} from 'react'; +import React, {useEffect, useMemo, useRef, useState} from 'react'; import type {SectionListData} from 'react-native'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -67,6 +67,7 @@ function WorkspaceInvitePage({ const [personalDetails, setPersonalDetails] = useState([]); const [usersToInvite, setUsersToInvite] = useState([]); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); + const firstRenderRef = useRef(true); const navigation = useNavigation>(); const openWorkspaceInvitePage = () => { const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policyMembers, personalDetailsProp); @@ -120,12 +121,16 @@ function WorkspaceInvitePage({ }); const newSelectedOptions: MemberForList[] = []; - Object.keys(invitedEmailsToAccountIDsDraft ?? {}).forEach((login) => { - if (!(login in detailsMap)) { - return; - } - newSelectedOptions.push({...detailsMap[login], isSelected: true}); - }); + if (firstRenderRef.current) { + // We only want to add the saved selected user on first render + firstRenderRef.current = false; + Object.keys(invitedEmailsToAccountIDsDraft ?? {}).forEach((login) => { + if (!(login in detailsMap)) { + return; + } + newSelectedOptions.push({...detailsMap[login], isSelected: true}); + }); + } selectedOptions.forEach((option) => { newSelectedOptions.push(option.login && option.login in detailsMap ? {...detailsMap[option.login], isSelected: true} : option); }); From 0682afbf2bd04faf5f56b44aae122d797a5983e4 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 28 Mar 2024 13:56:02 +0100 Subject: [PATCH 016/198] fix: add missing screen --- src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index ef1fc3c2dfb0..7fead4088bf1 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -81,6 +81,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../../pages/iou/request/step/IOURequestStepDate').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_DESCRIPTION]: () => require('../../../../pages/iou/request/step/IOURequestStepDescription').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_DISTANCE]: () => require('../../../../pages/iou/request/step/IOURequestStepDistance').default as React.ComponentType, + [SCREENS.MONEY_REQUEST.STEP_RATE]: () => require('../../../pages/iou/request/step/IOURequestStepRate').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_MERCHANT]: () => require('../../../../pages/iou/request/step/IOURequestStepMerchant').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS]: () => require('../../../../pages/iou/request/step/IOURequestStepParticipants').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_SCAN]: () => require('../../../../pages/iou/request/step/IOURequestStepScan').default as React.ComponentType, From 6105b5d917e8172779de97cca9947694ee3dcb4c Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 28 Mar 2024 17:05:33 +0100 Subject: [PATCH 017/198] fix: show check mark on lastSelectedDistanceRate --- src/pages/iou/request/step/IOURequestStepRate.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx index de570ca3c67f..c4e638a48c2f 100644 --- a/src/pages/iou/request/step/IOURequestStepRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -52,11 +52,12 @@ function IOURequestStepRate({ alternateText: DistanceRequestUtils.getRateForDisplay(true, rate.unit, rate.rate, rate.currency, translate, toLocaleDigit), keyForList: rate.name ?? '', value: rate.customUnitRateID, + isSelected: lastSelectedRate === rate.customUnitRateID, })); const initiallyFocusedOption = rates[lastSelectedRate]?.name ?? CONST.CUSTOM_UNITS.DEFAULT_RATE; - function selectDistanceRate(customUnitRateID = '0') { + function selectDistanceRate(customUnitRateID: string) { IOU.setLastSelectedDistanceRates(policy?.id ?? '', customUnitRateID); IOU.updateDistanceRequestRate('1', customUnitRateID); Navigation.goBack(backTo); @@ -74,7 +75,7 @@ function IOURequestStepRate({ selectDistanceRate(value)} + onSelectRow={({value}) => selectDistanceRate(value ?? '')} initiallyFocusedOptionKey={initiallyFocusedOption} /> @@ -96,4 +97,5 @@ export default compose( key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, }, }), + // @ts-expect-error TODO: fix when withWritableReportOrNotFound will be migrated to TS )(IOURequestStepRate); From 063a8b6464615d1d3e4bc252e86bef7ef5d5cdb1 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 28 Mar 2024 17:15:46 +0100 Subject: [PATCH 018/198] fix: fix distance edit route --- src/components/ReportActionItem/MoneyRequestView.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 8a2ad321cedc..9011e8193077 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -260,7 +260,9 @@ function MoneyRequestView({ interactive={canEditDistance} shouldShowRightIcon={canEditDistance} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DISTANCE))} + onPress={() => + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.EDIT, CONST.IOU.TYPE.REQUEST, transaction?.transactionID ?? '', report.reportID)) + } /> @@ -285,9 +287,7 @@ function MoneyRequestView({ shouldShowRightIcon={canEditDistance} titleStyle={styles.flex1} onPress={() => - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.EDIT, CONST.IOU.TYPE.REQUEST, transaction?.transactionID ?? '', report.reportID), - ) + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.EDIT, CONST.IOU.TYPE.REQUEST, transaction?.transactionID ?? '', report.reportID)) } /> From 7fe595a9f274790a987949ff4fcfb960647983f9 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 28 Mar 2024 18:46:11 +0100 Subject: [PATCH 019/198] fix: path fix --- src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 7fead4088bf1..75a9806e4ee8 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -81,7 +81,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../../pages/iou/request/step/IOURequestStepDate').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_DESCRIPTION]: () => require('../../../../pages/iou/request/step/IOURequestStepDescription').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_DISTANCE]: () => require('../../../../pages/iou/request/step/IOURequestStepDistance').default as React.ComponentType, - [SCREENS.MONEY_REQUEST.STEP_RATE]: () => require('../../../pages/iou/request/step/IOURequestStepRate').default as React.ComponentType, + [SCREENS.MONEY_REQUEST.STEP_RATE]: () => require('../../../../pages/iou/request/step/IOURequestStepRate').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_MERCHANT]: () => require('../../../../pages/iou/request/step/IOURequestStepMerchant').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS]: () => require('../../../../pages/iou/request/step/IOURequestStepParticipants').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_SCAN]: () => require('../../../../pages/iou/request/step/IOURequestStepScan').default as React.ComponentType, From 9e68814a1f476f205be5bb0a3188723ca38b568d Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 28 Mar 2024 19:08:20 +0100 Subject: [PATCH 020/198] fix: minor fix --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 394db0bddbe3..5dd94c331e80 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -254,7 +254,9 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const isTypeTrackExpense = iouType === CONST.IOU.TYPE.TRACK_EXPENSE; const canEditDistance = isTypeRequest || (canUseP2PDistanceRequests && isTypeSplit); - const personalPolicy = policyID === CONST.POLICY.ID_FAKE ? PolicyUtils.getPersonalPolicy() : policy; + // if there is no policyID that means that the transaction was started from Global Create and the participant is not a policy + const personalPolicy = policyID === CONST.POLICY.ID_FAKE || !policyID ? PolicyUtils.getPersonalPolicy() : policy; + const mileageRate = TransactionUtils.isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(personalPolicy.outputCurrency) : mileageRates[transaction.comment.customUnit.customUnitRateID]; From 68eecf0cb83aae10fd3c18ef735bd243986d3107 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 28 Mar 2024 19:17:55 +0100 Subject: [PATCH 021/198] fix: enable fields for split, fix route --- ...neyTemporaryForRefactorRequestConfirmationList.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 5dd94c331e80..e4bc65b9840d 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -757,13 +757,17 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ item: ( Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} - disabled={didConfirm || !isTypeRequest} + onPress={() => + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()), + ) + } + disabled={didConfirm} interactive={!isReadOnly} /> ), @@ -784,7 +788,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ ROUTES.MONEY_REQUEST_STEP_RATE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()), ); }} - disabled={didConfirm || !isTypeRequest} + disabled={didConfirm} interactive={!isReadOnly && isPolicyExpenseChat} /> ), From 35baf304a5f41f213f653836ec10378b173fbfd8 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 28 Mar 2024 19:29:58 +0100 Subject: [PATCH 022/198] fix: minor fix --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index e4bc65b9840d..5a28b8be1ef5 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -266,7 +266,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ rate: CONST.CUSTOM_UNITS.MILEAGE_IRS_RATE * 100, }; - const currency = personalPolicy.outputCurrency; + const currency = mileageRate.currency || personalPolicy.outputCurrency; const distance = lodashGet(transaction, 'routes.route0.distance', 0); const taxRates = lodashGet(policy, 'taxRates', {}); From 9803669d05f0ad55c5d47ec6215c0f09df88bd95 Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Thu, 28 Mar 2024 15:35:34 -0500 Subject: [PATCH 023/198] Create Enable-and-set-up-expense-violations.md New article for enabling and setting up expense violations --- .../Enable-and-set-up-expense-violations.md | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 docs/articles/expensify-classic/workspaces/Enable-and-set-up-expense-violations.md diff --git a/docs/articles/expensify-classic/workspaces/Enable-and-set-up-expense-violations.md b/docs/articles/expensify-classic/workspaces/Enable-and-set-up-expense-violations.md new file mode 100644 index 000000000000..b6cea251785b --- /dev/null +++ b/docs/articles/expensify-classic/workspaces/Enable-and-set-up-expense-violations.md @@ -0,0 +1,111 @@ +--- +title: Enable and set up expense violations +description: Set up rules for expenses and enable violations +--- +
+ +Expensify automatically detects expense errors or discrepancies as violations that must be corrected. You can also set rules for a workspace that will trigger a violation if the rule is not met. These rules can be set for categories, tags, and even for specific domain groups. + +When reviewing submitted expense reports, approvers will see violations highlighted with an exclamation mark. There are two types of violations: +- **Yellow**: Automated highlights that require attention but may not require corrective action. For example, if a receipt was SmartScanned and then the amount was modified, a yellow violation will be added to call out the change for review. +- **Red**: Violations directly tied to your workspace settings. These violations must be addressed before the report can be submitted and reimbursed. + +You can hover over the icon to see a brief description, and you can find more detailed information below the list of expenses. + +{% include info.html %} +If your workspace has automations set to automatically submit reports for approval, the report that contains violations will not be submitted automatically until the violations are corrected. (However, if a comment is added to an expense, it will override the violation as the member is providing a reason for submission *unless* domain workspace rules are set to be strictly enforced, as detailed near the bottom of this article.) +{% include end-info.html %} + +# Enable or disable expense violations + +1. Hover over Settings, then click **Workspaces**. +2. Click the **Group** tab on the left. +3. Click the desired workspace name. +4. Click the **Expenses** tab on the left. +5. Click the “Enable violations” toggle. +6. If desired, enter the expense rules that will be used to create violations: + - **Max expense age (days)**: How old an expense can be + - **Max expense amount**: How much a single expense can cost + - **Receipt required amount**: How much a single expense can cost before a receipt is required + +{% include info.html %} +Expensify includes certain system mandatory violations that can't be disabled, even if your policy has violations turned off. +{% include end-info.html %} + +# Set category rules + +Admin on a Control workspace can enable specific rules for each category, including setting expense caps for specific categories, requiring receipts, and more. These rules can allow you to have a default expense limit of $2,500 but to only allow a daily entertainment limit of $150 per person. You can also choose to not require receipts for mileage or per diem expenses. + +To set up category rules, +1. Hover over Settings, then click **Workspaces**. +2. Click the **Group** tab on the left. +3. Click the desired workspace name. +4. Click the **Categories** tab on the left. +5. Click **Edit** to the right of the category. +6. Enter your category rules, as desired: + - **GL Code and Payroll Code**: You can add general ledger (GL) or payroll codes to the category for accounting. GL codes populate automatically if you have an accounting integration connected with Expensify. + - **Max Amount**: You can set specific expense caps for the expense category. Use the Limit Type dropdown to determine if the amount is set per individual expense or per day, then enter the maximum amount into this field. + - **Receipts**: You can determine whether receipts are required for the category. For example, many companies disable receipt requirements for toll expenses. + - **Description**: You can determine whether a description is required for expenses under this category. + - **Description Hint**: You can add a hint in the description field to prompt the expense creator on what they should enter into the description field for expenses under this category. + - **Approver**: You can set a specific approver for expenses labeled with this category. + +If users are in violation of these rules, the violations will be shown in red on the report. + +{% include info.html %} +If Scheduled Submit is enabled on a workspace, expenses with category violations will not be auto-submitted unless the expense has a comment added. +{% include end-info.html %} + +# Make categories required + +This means all expenses must be coded with a Category. + +1. Hover over Settings, then click **Workspaces**. +2. Click the **Group** tab on the left. +3. Click the desired workspace name. +4. Click the **Categories** tab on the left. +5. Enable the “People must categorize expenses” toggle. + +Each Workspace Member will now be required to select a category for their expense. If they do not select a category, the report will receive a violation, which can prevent submission if Scheduled Submit is enabled. + +# Make tags required + +1. Hover over Settings, then click **Workspaces**. +2. Click the **Group** tab on the left. +3. Click the desired workspace name. +4. Click the **Tags** tab on the left. +5. Enable the “People must tag expenses” toggle. + +Each Workspace Member will now be required to select a tag for their expense before they’re able to submit it. + +# Require strict compliance by domain group + +You can require strict compliance to require members of a specific domain group to submit reports that meet **all** workspace rules before they can submit their expense report—even if they add a note. Every rule and regulation on the workspace must be met before a report can be submitted. + +{% include info.html %} +This will prevent members from submitting any reports where a manager has granted them a manual exception for any of the workspace rules. +{% include end-info.html %} + +To enable strict domain group compliance for reports, + +1. Hover over Settings, then click **Domains**. +2. Click the **Groups** tab on the left. +3. Click **Edit** to the right of the desired workspace name. +4. Enable the “Strictly enforce expense workspace rules” toggle. + +# FAQs + +**Why can’t my employees see the categories on their expenses?** + +The employee may have their default workspace set as their personal workspace. Look under the details section on top right of the report to ensure it is being reported under the correct workspace. + +**Will the account numbers from our accounting system (QuickBooks Online, Sage Intacct, etc.) show in the category list for employees?** + +The general ledger (GL) account numbers are visible only for Workspace Admins in the workspace settings when they are part of a control workspace. This information is not visible to other members of the workspace. However, if you wish to have this information available to your employees when they are categorizing their expenses, you can edit the account name in your accounting software to include the GL number (for example, Accounts Payable - 12345). + +**What causes a category violation?** + +- An expense is categorized with a category that is not included in the workspace's categories. This may happen if the employee creates an expense under the wrong workspace, which will cause a "category out of workspace" violation. +- If the workspace categories are being imported from an accounting integration and they’ve been updated in the accounting system but not in Expensify, this can cause an old category to still be in use on an open report which would throw a violation on submission. Simply reselect a proper category to clear violation. + +
From e9a15d7d7790bed485cc18ce40cddbcd45ac1d32 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 28 Mar 2024 22:19:08 +0100 Subject: [PATCH 024/198] fix: change copy on rate selection page --- src/languages/en.ts | 3 ++- src/languages/es.ts | 3 ++- src/languages/types.ts | 3 +++ src/pages/iou/request/step/IOURequestStepRate.tsx | 8 +++++--- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index de5d4d80ea48..f45e4bd1c555 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -49,6 +49,7 @@ import type { PayerPaidAmountParams, PayerPaidParams, PayerSettledParams, + ReimbursementRateUnit, RemovedTheRequestParams, RenamedRoomActionParams, ReportArchiveReasonsClosedParams, @@ -726,7 +727,7 @@ export default { set: 'set', changed: 'changed', removed: 'removed', - chooseARate: 'Choose a rate to use below', + chooseARate: ({unit}: ReimbursementRateUnit) => `Select a workspace reimbursement rate per ${unit}`, }, notificationPreferencesPage: { header: 'Notification preferences', diff --git a/src/languages/es.ts b/src/languages/es.ts index c00ec12216fd..7fc558f9673b 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -48,6 +48,7 @@ import type { PayerPaidAmountParams, PayerPaidParams, PayerSettledParams, + ReimbursementRateUnit, RemovedTheRequestParams, RenamedRoomActionParams, ReportArchiveReasonsClosedParams, @@ -724,7 +725,7 @@ export default { set: 'estableció', changed: 'cambió', removed: 'eliminó', - chooseARate: 'Elige una tarifa para utilizar a continuación', + chooseARate: ({unit}: ReimbursementRateUnit) => `Seleccione una tasa de reembolso del espacio de trabajo por ${unit}`, }, notificationPreferencesPage: { header: 'Preferencias de avisos', diff --git a/src/languages/types.ts b/src/languages/types.ts index c365363f84af..86f4b4709181 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -297,6 +297,8 @@ type HeldRequestParams = {comment: string}; type DistanceRateOperationsParams = {count: number}; +type ReimbursementRateUnit = {unit: string}; + export type { AdminCanceledRequestParams, ApprovedAmountParams, @@ -400,4 +402,5 @@ export type { ZipCodeExampleFormatParams, LogSizeParams, HeldRequestParams, + ReimbursementRateUnit, }; diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx index c4e638a48c2f..8395dbc60ffa 100644 --- a/src/pages/iou/request/step/IOURequestStepRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -45,16 +45,18 @@ function IOURequestStepRate({ const {translate, toLocaleDigit} = useLocalize(); const rates = DistanceRequestUtils.getMileageRates(policy?.id); - const lastSelectedRate = lastSelectedDistanceRates[policy?.id ?? '0'] ?? '0'; + const lastSelectedRate = lastSelectedDistanceRates[policy?.id ?? '0'] ?? rates[0]?.customUnitRateID; const data = Object.values(rates).map((rate) => ({ text: rate.name ?? '', alternateText: DistanceRequestUtils.getRateForDisplay(true, rate.unit, rate.rate, rate.currency, translate, toLocaleDigit), keyForList: rate.name ?? '', value: rate.customUnitRateID, - isSelected: lastSelectedRate === rate.customUnitRateID, + isSelected: lastSelectedRate ? lastSelectedRate === rate.customUnitRateID : Boolean(rate.name === CONST.CUSTOM_UNITS.DEFAULT_RATE), })); + const unit = Object.values(rates)[0]?.unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer'); + const initiallyFocusedOption = rates[lastSelectedRate]?.name ?? CONST.CUSTOM_UNITS.DEFAULT_RATE; function selectDistanceRate(customUnitRateID: string) { @@ -70,7 +72,7 @@ function IOURequestStepRate({ shouldShowWrapper={Boolean(backTo)} testID="rate" > - {translate('iou.chooseARate')} + {translate('iou.chooseARate', {unit})} Date: Fri, 29 Mar 2024 14:04:01 +0100 Subject: [PATCH 025/198] fix: minor fix --- src/pages/iou/request/step/IOURequestStepParticipants.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index a66387709f61..7511b2f8d06e 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -30,8 +30,8 @@ const propTypes = { /** The transaction object being modified in Onyx */ transaction: transactionPropTypes, - // eslint-disable-next-line - lastSelectedDistanceRates: PropTypes.object, + // eslint-disable-next-line react/require-default-props + lastSelectedDistanceRates: PropTypes.shape({}), }; const defaultProps = { From 4217a9838ef4002f76ec3d917f956e2ca5562ce1 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 29 Mar 2024 16:31:48 +0100 Subject: [PATCH 026/198] fix: remove a comment --- src/libs/DistanceRequestUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index a676e2617f72..0aa5a0c4b3a5 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -94,7 +94,6 @@ function getRoundedDistanceInUnits(distanceInMeters: number, unit: Unit): string return convertedDistance.toFixed(2); } -// TODO: I wonder if it would be better to refactor these functions to pass params in an object /** * @param hasRoute Whether the route exists for the distance request * @param unit Unit that should be used to display the distance From c7b8854b701d3a6c04a5d5cbf30c816c00a5651c Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 4 Apr 2024 21:48:45 +0200 Subject: [PATCH 027/198] fix: display distance and rate properly --- src/CONST.ts | 3 ++- src/components/ReportActionItem/MoneyRequestView.tsx | 9 ++++----- src/libs/TransactionUtils.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index d33cf174cf48..e1d055d89028 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3,11 +3,12 @@ import dateAdd from 'date-fns/add'; import dateSubtract from 'date-fns/sub'; import Config from 'react-native-config'; import * as KeyCommand from 'react-native-key-command'; +import type {Unit} from '@src/types/onyx/Policy'; import * as Url from './libs/Url'; import SCREENS from './SCREENS'; type RateAndUnit = { - unit: string; + unit: Unit; rate: number; }; type CurrencyDefaultMileageRate = Record; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 6f184c10f5d6..597b0494928d 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -176,11 +176,10 @@ function MoneyRequestView({ const rateID = transaction?.comment.customUnit?.customUnitRateID ?? '0'; const rates = DistanceRequestUtils.getMileageRates(policy?.id); - const {unit, currency, rate} = rates[rateID as string] ?? { - unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, - currency: CONST.CURRENCY.USD, - rate: 0, - }; + const currency = policy ? policy.outputCurrency : PolicyUtils.getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD; + + const mileageRate = TransactionUtils.isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(currency) : rates[rateID as string]; + const {unit, rate} = mileageRate; const rateToDisplay = DistanceRequestUtils.getRateForDisplay(hasRoute, unit, rate, currency, translate, toLocaleDigit); const distanceToDisplay = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate); diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index be6822b936c4..2b5f5aff4a1b 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -624,7 +624,7 @@ function getEnabledTaxRateCount(options: TaxRates) { * Check if the customUnitRateID has a value default for P2P distance requests */ -function isCustomUnitRateIDForP2P(transaction: Transaction): boolean { +function isCustomUnitRateIDForP2P(transaction: OnyxEntry): boolean { return transaction?.comment?.customUnit?.customUnitRateID === CONST.CUSTOM_UNITS.FAKE_P2P_ID; } From b16107aae43d7f37a835fe0cac32122b0fcf9814 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 4 Apr 2024 22:07:21 +0200 Subject: [PATCH 028/198] fix: minor fixes --- src/CONST.ts | 2 +- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index e1d055d89028..3ce64396a538 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3,9 +3,9 @@ import dateAdd from 'date-fns/add'; import dateSubtract from 'date-fns/sub'; import Config from 'react-native-config'; import * as KeyCommand from 'react-native-key-command'; -import type {Unit} from '@src/types/onyx/Policy'; import * as Url from './libs/Url'; import SCREENS from './SCREENS'; +import type {Unit} from './types/onyx/Policy'; type RateAndUnit = { unit: Unit; diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 4de7d52e9e81..91274c119625 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -790,7 +790,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ /> ), shouldShow: isDistanceRequest && canUseP2PDistanceRequests, - isSupplementary: true, + isSupplementary: false, }, { item: ( @@ -811,7 +811,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ /> ), shouldShow: isDistanceRequest && canUseP2PDistanceRequests, - isSupplementary: true, + isSupplementary: false, }, { item: ( From 53e0ca31da0f54d1e5f4ff08ed000bfdae48bbd7 Mon Sep 17 00:00:00 2001 From: bartektomczyk Date: Fri, 5 Apr 2024 12:52:45 +0200 Subject: [PATCH 029/198] feat: added new method for handliung onboarding, build new messages structure for onboarding --- src/CONST.ts | 240 +++++++++++++++++++++++++++++++++++++ src/libs/API/types.ts | 1 + src/libs/actions/Report.ts | 104 ++++++++++++++++ 3 files changed, 345 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index 6d1195ff5c79..b05e71fe7f19 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3541,6 +3541,246 @@ const CONST = { "Expensify is best known for expense and corporate card management, but we do a lot more than that. Let me know what you're interested in and I'll help get you started.", }, + ONBOARDING_MESSAGES: { + [onboardingChoices.TRACK]: { + message: 'Here are some essential tasks to keep your business spend in shape for tax season.', + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, + tasks: [ + { + title: 'Create a workspace', + subtitle: 'Create a workspace to track expenses, scan receipts, chat, and more. ', + message: + 'Here’s how to create a workspace:\n' + + '\n' + + '1. Click your profile picture.\n' + + '2. Click Workspaces > New workspace.\n' + + '\n' + + 'Your new workspace is ready! It’ll keep all of your spend (and chats) in one place.', + }, + { + title: 'Track an expense', + subtitle: 'Track an expense in any currency, in just a few clicks.', + message: + 'Here’s how to track an expense:\n' + + '\n' + + '1. Click the green + button.\n' + + '2. Choose Track expense.\n' + + '3. Enter an amount or scan a receipt.\n' + + '4. Click Track.\n' + + '\n' + + 'And you’re done! Yep, it’s that easy.', + }, + ], + }, + [onboardingChoices.EMPLOYER]: { + message: 'Getting paid back is as easy as sending a message. Let’s go over the basics.', + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, + tasks: [ + { + title: 'Submit an expense', + subtitle: 'Submit an expense by entering an amount or scanning a receipt.', + message: + 'Here’s how to submit an expense:\n' + + '\n' + + '1. Click the green + button.\n' + + '2. Choose Submit expense.\n' + + '3. Enter an amount or scan a receipt.\n' + + '4. Add your reimburser to the request.\n' + + '\n' + + 'Then, send your request and wait for that sweet “Cha-ching!” when it’s complete.', + }, + { + title: 'Enable your wallet', + subtitle: 'You’ll need to enable your Expensify Wallet to get paid back. Don’t worry, it’s easy!', + message: + 'Here’s how to set up your wallet:\n' + + '\n' + + '1. Click your profile picture.\n' + + '2. Click Wallet > Enable wallet.\n' + + '3. Connect your bank account.\n' + + '\n' + + 'Once that’s done, you can request money from anyone and get paid back right into your personal bank account.', + }, + ], + }, + [onboardingChoices.MANAGE_TEAM]: { + message: 'Here are some important tasks to help get your team’s expenses under control.', + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, + tasks: [ + { + title: 'Create a workspace', + subtitle: 'Create a workspace to track expenses, scan receipts, chat, and more. ', + message: + 'Here’s how to create a workspace:\n' + + '\n' + + '1. Click your profile picture.\n' + + '2. Click Workspaces > New workspace.\n' + + '\n' + + 'Your new workspace is ready! It’ll keep all of your spend (and chats) in one place.', + }, + { + title: 'Meet your setup specialist', + subtitle: '', + message: + 'Meet your setup specialist, {guideName}, who can answer any questions as you get started with Expensify. Yes, a real human!\n' + + '\n' + + 'Chat with {guideName} in your [admins room]({adminsRoomID}) or [schedule a call]({guideCalendarLink}) today', + }, + { + title: 'Set up categories', + subtitle: 'Set up categories so your team can code expenses for easy reporting.', + message: + 'Here’s how to set up categories:\n' + + '\n' + + '1. Click your profile picture.\n' + + '2. Go to Workspaces > [your workspace].\n' + + '3. Click Categories.\n' + + '4. Enable and disable default categories.\n' + + '5. Click Add categories to make your own.\n' + + '\n' + + 'For more controls like requiring a category for every expense, click Settings. ', + }, + { + title: 'Add expense approvals', + subtitle: 'Add expense approvals to review your team’s spend and keep it under control.', + message: + 'Here’s how to add expense approvals:\n' + + '\n' + + '1. Click your profile picture.\n' + + '2. Go to Workspaces > [your workspace].\n' + + '3. Click More features.\n' + + '4. Enable Workflows.\n' + + '5. In Workflows, enable Add approvals.\n' + + '\n' + + 'You’ll be set as the expense approver. You can change this to any admin once you invite your team. ', + }, + { + title: 'Invite your team', + subtitle: 'Invite your team to Expensify so they can start tracking expenses today.', + message: + 'Here’s how to invite your team:\n' + + '\n' + + '1. Click your profile picture.\n' + + '2. Go to Workspaces > [your workspace].\n' + + '3. Click Members > Invite member.\n' + + '4. Enter emails or phone numbers. \n' + + '5. Add an invite message if you want.\n' + + '\n' + + 'That’s it! Happy expensing :)', + }, + ], + }, + [onboardingChoices.PERSONAL_SPEND]: { + message: 'Here’s how to track your spend in a few clicks.', + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, + tasks: [ + { + title: 'Track an expense', + subtitle: 'Track an expense in any currency, whether you have a receipt or not.', + message: + 'Here’s how to track an expense:\n' + + '\n' + + '1. Click the green + button.\n' + + '2. Choose Track expense.\n' + + '3. Enter an amount or scan a receipt.\n' + + '4. Click Track.\n' + + '\n' + + 'And you’re done! Yep, it’s that easy.', + }, + ], + }, + [onboardingChoices.CHAT_SPLIT]: { + message: 'Splitting bills with friends is as easy as sending a message. Here’s how.', + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, + tasks: [ + { + title: 'Start a chat', + subtitle: 'Start a chat with a friend or group using their email or phone number.', + message: + 'Here’s how to start a chat:\n' + + '\n' + + '1. rClick the green + button.\n' + + '2. Choose Start chat.\n' + + '3. Enter emails or phone numbers.\n' + + '\n' + + 'If any of your friends aren’t using Expensify already, they’ll be invited automatically. \n' + + '\n' + + 'Every chat will also turn into an email or text that they can respond to directly.', + }, + { + title: 'Split an expense', + subtitle: 'Split an expense right in your chat with one or more friends.', + message: + 'Here’s how to request money:\n' + + '\n' + + '1. Click the green + button.\n' + + '2. Choose Split expense.\n' + + '3. Scan a receipt or enter an amount.\n' + + '4. Add your friend(s) to the request.\n' + + '\n' + + 'Feel free to add more details if you want, or just send it off. Let’s get you paid back!', + }, + { + title: 'Enable your wallet', + subtitle: 'You’ll need to enable your Expensify Wallet to get paid back. Don’t worry, it’s easy!', + message: + 'Here’s how to enable your wallet:\n' + + '\n' + + '1. Click your profile picture.\n' + + '2. Click Wallet > Enable wallet.\n' + + '3. Add your bank account.\n' + + '\n' + + 'Once that’s done, you can request money from anyone and get paid right into your personal bank account.', + }, + ], + }, + [onboardingChoices.LOOKING_AROUND]: { + message: + '# Welcome to Expensify!\n' + + "Hi there, I'm Concierge. Chat with me here for anything you need.\n" + + '\n' + + "Expensify is best known for expense and corporate card management, but we do a lot more than that. Let me know what you're interested in and I'll help get you started.", + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, + tasks: [], + }, + }, + REPORT_FIELD_TITLE_FIELD_ID: 'text_title', MOBILE_PAGINATION_SIZE: 15, diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 3cff726a530c..90e03188f3a3 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -134,6 +134,7 @@ const WRITE_COMMANDS = { REOPEN_TASK: 'ReopenTask', COMPLETE_TASK: 'CompleteTask', COMPLETE_ENGAGEMENT_MODAL: 'CompleteEngagementModal', + COMPLETE_GUIDED_SETUP: 'CompleteGuidedSetup', SET_NAME_VALUE_PAIR: 'SetNameValuePair', SET_REPORT_FIELD: 'Report_SetFields', SET_REPORT_NAME: 'RenameReport', diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 179ee87862ff..60cb81c9165a 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2712,6 +2712,109 @@ function getReportPrivateNote(reportID: string | undefined) { API.read(READ_COMMANDS.GET_REPORT_PRIVATE_NOTE, parameters, {optimisticData, successData, failureData}); } +function completeOnboarding(properties: { + data: ValueOf; + firstName: string; + lastName: string; + targetEmail: ValueOf; + engagementChoice: ValueOf; +}) { + const {data, engagementChoice, targetEmail, firstName, lastName} = properties; + const actorAccountID = PersonalDetailsUtils.getAccountIDsByLogins([targetEmail])[0]; + const targetChatReport = ReportUtils.getChatByParticipants([actorAccountID]); + const targetChatReportID = targetChatReport?.reportID ?? ''; + const targetChatPolicyID = targetChatReport?.policyID ?? ''; + + // Text message + const textComment = ReportUtils.buildOptimisticAddCommentReportAction(data.message, undefined); + const textCommentAction: OptimisticAddCommentReportAction = textComment.reportAction; + const textCommentText = textComment.commentText; + + const textMessage: AddCommentOrAttachementParams & {type: string} = { + reportID: targetChatReportID, + reportActionID: textCommentAction.reportActionID, + reportComment: textCommentText, + type: 'message', + }; + + // Video message + const videoComment = ReportUtils.buildOptimisticAddCommentReportAction(CONST.ATTACHMENT_MESSAGE_TEXT, undefined); + const videoCommentAction: OptimisticAddCommentReportAction = videoComment.reportAction; + const videoCommentText = videoComment.commentText; + + const videoMessage: AddCommentOrAttachementParams & {type: string} & typeof data.video = { + reportID: targetChatReportID, + reportActionID: videoCommentAction.reportActionID, + reportComment: videoCommentText, + type: 'video', + ...data.video, + }; + + // tasks + const tasks = data.tasks.reduce((acc, curr) => { + const currTask = ReportUtils.buildOptimisticTaskReport(actorAccountID, undefined, targetChatReportID, curr.title, undefined, targetChatPolicyID); + const taskCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(targetEmail); + const taskAddCommentReport = ReportUtils.buildOptimisticTaskCommentReportAction(currTask.reportID, curr.title, 0, `task for ${curr.title}`, targetChatReportID); + + // subtitle message + const subtitleComment = ReportUtils.buildOptimisticAddCommentReportAction(curr.subtitle, undefined, actorAccountID); + const subtitleCommentAction: OptimisticAddCommentReportAction = subtitleComment.reportAction; + const subtitleCommentText = subtitleComment.commentText; + + const subtitleMessage: AddCommentOrAttachementParams = { + reportID: currTask.reportID, + reportActionID: subtitleCommentAction.reportActionID, + reportComment: subtitleCommentText, + }; + + // instruction message + const instructionComment = ReportUtils.buildOptimisticAddCommentReportAction(curr.message, undefined, actorAccountID); + const instructionCommentAction: OptimisticAddCommentReportAction = instructionComment.reportAction; + const instructionCommentText = instructionComment.commentText; + + const instructionMessage: AddCommentOrAttachementParams = { + reportID: currTask.reportID, + reportActionID: instructionCommentAction.reportActionID, + reportComment: instructionCommentText, + }; + + return [ + ...acc, + { + parentReportActionID: taskAddCommentReport.reportAction.reportActionID, + parentReportID: currTask.parentReportID, + taskReportID: currTask.reportID, + createdTaskReportActionID: taskCreatedAction.reportActionID, + title: currTask.reportName, + description: currTask.description, + assigneeChatReportID: '', + type: 'task', + task: 'trackExpense', + }, + { + type: 'message', + ...subtitleMessage, + }, + { + type: 'message', + ...instructionMessage, + }, + ]; + }, []); + + const parameters = { + engagementChoice, + firstName, + lastName, + data: JSON.stringify([textMessage, videoMessage, ...tasks]), + }; + + // console.log('-------------------------------------'); + // console.log(JSON.stringify({...parameters, data: JSON.parse(parameters.data)}, ' ', 2)); + + API.write(WRITE_COMMANDS.COMPLETE_GUIDED_SETUP, parameters, {}); +} + /** * Completes the engagement modal that new NewDot users see when they first sign up/log in by doing the following: * @@ -3029,4 +3132,5 @@ export { setGroupDraft, clearGroupChat, startNewChat, + completeOnboarding, }; From c34e8a20a35940b035b987d18e9797211fd3526e Mon Sep 17 00:00:00 2001 From: bartektomczyk Date: Fri, 5 Apr 2024 16:31:47 +0200 Subject: [PATCH 030/198] feat: call method --- src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx index ae43f850018b..bd8a7ab40917 100644 --- a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx +++ b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx @@ -80,7 +80,14 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on return; } - Report.completeEngagementModal(CONST.ONBOARDING_CONCIERGE[selectedPurpose], selectedPurpose); + // Report.completeEngagementModal(CONST.ONBOARDING_CONCIERGE[selectedPurpose], selectedPurpose); + Report.completeOnboarding({ + data: CONST.ONBOARDING_MESSAGES[selectedPurpose], + engagementChoice: selectedPurpose, + targetEmail: CONST.EMAIL.CONCIERGE, + firstName: 'Test', + lastName: 'Testovsky', + }); Navigation.dismissModal(); // Only navigate to concierge chat when central pane is visible From 3bd9aad21f9666b9a863a41318bd5af05be1fdc8 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 5 Apr 2024 17:39:53 +0200 Subject: [PATCH 031/198] fix: apply requested changes --- ...neyTemporaryForRefactorRequestConfirmationList.js | 6 +++--- src/languages/en.ts | 4 ++-- src/languages/es.ts | 4 ++-- src/languages/types.ts | 5 +++-- .../iou/request/step/IOURequestStepParticipants.js | 12 +++++++----- src/pages/iou/request/step/IOURequestStepRate.tsx | 7 ++++--- 6 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 91274c119625..51436b5d8737 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -769,7 +769,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ /> ), shouldShow: isDistanceRequest && !canUseP2PDistanceRequests, - isSupplementary: true, + isSupplementary: false, }, { item: ( @@ -796,7 +796,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ item: ( ), shouldShow: isDistanceRequest && canUseP2PDistanceRequests, diff --git a/src/languages/en.ts b/src/languages/en.ts index 55313718222d..4c93e8bb6207 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -49,7 +49,7 @@ import type { PayerPaidAmountParams, PayerPaidParams, PayerSettledParams, - ReimbursementRateUnit, + ReimbursementRateParams, RemovedTheRequestParams, RenamedRoomActionParams, ReportArchiveReasonsClosedParams, @@ -737,7 +737,7 @@ export default { set: 'set', changed: 'changed', removed: 'removed', - chooseARate: ({unit}: ReimbursementRateUnit) => `Select a workspace reimbursement rate per ${unit}`, + chooseARate: ({unit}: ReimbursementRateParams) => `Select a workspace reimbursement rate per ${unit}`, }, notificationPreferencesPage: { header: 'Notification preferences', diff --git a/src/languages/es.ts b/src/languages/es.ts index 108a781bb298..406466d44489 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -48,7 +48,7 @@ import type { PayerPaidAmountParams, PayerPaidParams, PayerSettledParams, - ReimbursementRateUnit, + ReimbursementRateParams, RemovedTheRequestParams, RenamedRoomActionParams, ReportArchiveReasonsClosedParams, @@ -735,7 +735,7 @@ export default { set: 'estableció', changed: 'cambió', removed: 'eliminó', - chooseARate: ({unit}: ReimbursementRateUnit) => `Seleccione una tasa de reembolso del espacio de trabajo por ${unit}`, + chooseARate: ({unit}: ReimbursementRateParams) => `Seleccione una tasa de reembolso del espacio de trabajo por ${unit}`, }, notificationPreferencesPage: { header: 'Preferencias de avisos', diff --git a/src/languages/types.ts b/src/languages/types.ts index 86f4b4709181..59e248e4e43a 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -1,4 +1,5 @@ import type {ReportAction} from '@src/types/onyx'; +import type {Unit} from '@src/types/onyx/Policy'; import type en from './en'; type AddressLineParams = { @@ -297,7 +298,7 @@ type HeldRequestParams = {comment: string}; type DistanceRateOperationsParams = {count: number}; -type ReimbursementRateUnit = {unit: string}; +type ReimbursementRateParams = {unit: Unit}; export type { AdminCanceledRequestParams, @@ -402,5 +403,5 @@ export type { ZipCodeExampleFormatParams, LogSizeParams, HeldRequestParams, - ReimbursementRateUnit, + ReimbursementRateParams, }; diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index f66d998e1d1f..c6ae0c0fdb0e 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -110,13 +110,15 @@ function IOURequestStepParticipants({ IOU.setMoneyRequestParticipants_temporaryForRefactor(transactionID, val); + const participant = val[0]; + // change customUnitRateID when choosing - if (val[0].isPolicyExpenseChat && TransactionUtils.isCustomUnitRateIDForP2P(transaction)) { + if (participant.isPolicyExpenseChat && TransactionUtils.isCustomUnitRateIDForP2P(transaction)) { let customUnitRateID = ''; - if (val[0].policyID && lastSelectedDistanceRates[val[0].policyID]) { - customUnitRateID = lastSelectedDistanceRates[val[0].policyID]; + if (lastSelectedDistanceRates[participant.policyID]) { + customUnitRateID = lastSelectedDistanceRates[participant.policyID]; } else { - const policy = ReportUtils.getPolicy(val[0].policyID); + const policy = ReportUtils.getPolicy(participant.policyID); const defaultRate = DistanceRequestUtils.getDefaultMileageRate(policy); customUnitRateID = defaultRate ? defaultRate.customUnitRateID : ''; } @@ -135,7 +137,7 @@ function IOURequestStepParticipants({ } // When a participant is selected, the reportID needs to be saved because that's the reportID that will be used in the confirmation step. - selectedReportID.current = lodashGet(val, '[0].reportID', reportID); + selectedReportID.current = participant.reportID; }, [iouType, participants, transactionID, transaction, reportID, updateRouteParams, lastSelectedDistanceRates], ); diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx index 8395dbc60ffa..43f37a97eb76 100644 --- a/src/pages/iou/request/step/IOURequestStepRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -14,6 +14,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import type {Policy} from '@src/types/onyx'; +import type {Unit} from '@src/types/onyx/Policy'; import StepScreenWrapper from './StepScreenWrapper'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; @@ -47,7 +48,7 @@ function IOURequestStepRate({ const lastSelectedRate = lastSelectedDistanceRates[policy?.id ?? '0'] ?? rates[0]?.customUnitRateID; - const data = Object.values(rates).map((rate) => ({ + const sections = Object.values(rates).map((rate) => ({ text: rate.name ?? '', alternateText: DistanceRequestUtils.getRateForDisplay(true, rate.unit, rate.rate, rate.currency, translate, toLocaleDigit), keyForList: rate.name ?? '', @@ -55,7 +56,7 @@ function IOURequestStepRate({ isSelected: lastSelectedRate ? lastSelectedRate === rate.customUnitRateID : Boolean(rate.name === CONST.CUSTOM_UNITS.DEFAULT_RATE), })); - const unit = Object.values(rates)[0]?.unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer'); + const unit = (Object.values(rates)[0]?.unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer')) as Unit; const initiallyFocusedOption = rates[lastSelectedRate]?.name ?? CONST.CUSTOM_UNITS.DEFAULT_RATE; @@ -75,7 +76,7 @@ function IOURequestStepRate({ {translate('iou.chooseARate', {unit})} selectDistanceRate(value ?? '')} initiallyFocusedOptionKey={initiallyFocusedOption} From bd83588de6a8763f62aedbdbddc7e038640f8ad5 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 5 Apr 2024 17:40:48 +0200 Subject: [PATCH 032/198] fix: revert unnecessary change --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 51436b5d8737..755871c3d551 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -796,7 +796,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ item: ( ), shouldShow: isDistanceRequest && canUseP2PDistanceRequests, From 7712ceca4d980517d9330caf155ee50357923f19 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Mon, 8 Apr 2024 15:27:17 +0200 Subject: [PATCH 033/198] fix: get rates from withOnyx hoc, rename DefaultMileageRate --- src/components/MoneyRequestConfirmationList.tsx | 4 ++-- src/libs/DistanceRequestUtils.ts | 10 +++++----- src/pages/iou/request/step/IOURequestStepRate.tsx | 12 +++++++++++- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index e1e39279336f..86506933e6f4 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -12,7 +12,7 @@ import usePermissions from '@hooks/usePermissions'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; -import type {DefaultMileageRate} from '@libs/DistanceRequestUtils'; +import type {MileageRate} from '@libs/DistanceRequestUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import * as IOUUtils from '@libs/IOUUtils'; import Log from '@libs/Log'; @@ -64,7 +64,7 @@ type MoneyRequestConfirmationListOnyxProps = { session: OnyxEntry; /** Unit and rate used for if the money request is a distance request */ - mileageRate: OnyxEntry; + mileageRate: OnyxEntry; }; type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & { /** Callback to inform parent modal of success */ diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index 0aa5a0c4b3a5..c930e6ec9068 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -8,7 +8,7 @@ import type Policy from '@src/types/onyx/Policy'; import * as CurrencyUtils from './CurrencyUtils'; import * as PolicyUtils from './PolicyUtils'; -type DefaultMileageRate = { +type MileageRate = { customUnitRateID?: string; rate?: number; currency?: string; @@ -38,7 +38,7 @@ Onyx.connect({ * @returns [currency] - The currency associated with the rate. * @returns [unit] - The unit of measurement for the distance. */ -function getDefaultMileageRate(policy: OnyxEntry): DefaultMileageRate | null { +function getDefaultMileageRate(policy: OnyxEntry): MileageRate | null { if (!policy?.customUnits) { return null; } @@ -180,8 +180,8 @@ function getDistanceMerchant( * * @returns An array of mileage rates or an empty array if not found. */ -function getMileageRates(policyID?: string): Record { - const mileageRates: Record = {}; +function getMileageRates(policyID?: string): Record { + const mileageRates: Record = {}; if (!policyID) { return mileageRates; @@ -239,4 +239,4 @@ export default { getRateForP2P, }; -export type {DefaultMileageRate}; +export type {MileageRate}; diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx index 43f37a97eb76..3dee03fa0f60 100644 --- a/src/pages/iou/request/step/IOURequestStepRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -8,6 +8,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as IOU from '@libs/actions/IOU'; import compose from '@libs/compose'; +import type {MileageRate} from '@libs/DistanceRequestUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; @@ -33,6 +34,9 @@ type Props = { backTo: Route; }; }; + + /** Mileage rates */ + rates: Record; }; function IOURequestStepRate({ @@ -41,10 +45,10 @@ function IOURequestStepRate({ params: {backTo}, }, lastSelectedDistanceRates = {}, + rates, }: Props) { const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); - const rates = DistanceRequestUtils.getMileageRates(policy?.id); const lastSelectedRate = lastSelectedDistanceRates[policy?.id ?? '0'] ?? rates[0]?.customUnitRateID; @@ -99,6 +103,12 @@ export default compose( lastSelectedDistanceRates: { key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, }, + rates: { + // @ts-expect-error TODO: fix when withWritableReportOrNotFound will be migrated to TS + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, + // @ts-expect-error TODO: fix when withWritableReportOrNotFound will be migrated to TS + selector: (policy) => DistanceRequestUtils.getMileageRates(policy ? policy.id : ''), + }, }), // @ts-expect-error TODO: fix when withWritableReportOrNotFound will be migrated to TS )(IOURequestStepRate); From 72d31422b1fc1d6f1756cf4591f103a4975ec1c2 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 9 Apr 2024 01:13:41 +0200 Subject: [PATCH 034/198] fix: apply requested changes --- src/CONST.ts | 2 +- ...oraryForRefactorRequestConfirmationList.js | 12 +++--- .../ReportActionItem/MoneyRequestView.tsx | 12 +++++- src/libs/DistanceRequestUtils.ts | 42 ++++++++++++++----- src/libs/Permissions.ts | 1 + src/libs/actions/IOU.ts | 35 +++++++++++----- .../step/IOURequestStepParticipants.js | 41 ++---------------- .../iou/request/step/IOURequestStepRate.tsx | 6 +-- 8 files changed, 78 insertions(+), 73 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index a8584ab3859d..fb1d2ac7d288 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4286,6 +4286,6 @@ const CONST = { type Country = keyof typeof CONST.ALL_COUNTRIES; -export type {Country}; +export type {Country, RateAndUnit}; export default CONST; diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 1d23b80ff099..70f12af2dd42 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -261,19 +261,16 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const isTypeTrackExpense = iouType === CONST.IOU.TYPE.TRACK_EXPENSE; const canEditDistance = isTypeRequest || (canUseP2PDistanceRequests && isTypeSplit); - // if there is no policyID that means that the transaction was started from Global Create and the participant is not a policy - const personalPolicy = policyID === CONST.POLICY.ID_FAKE || !policyID ? PolicyUtils.getPersonalPolicy() : policy; + const customUnitRateID = lodashGet(transaction, 'comment.customUnit.customUnitRateID', ''); - const mileageRate = TransactionUtils.isCustomUnitRateIDForP2P(transaction) - ? DistanceRequestUtils.getRateForP2P(personalPolicy.outputCurrency) - : mileageRates[transaction.comment.customUnit.customUnitRateID]; + const mileageRate = TransactionUtils.isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(policy.outputCurrency) : mileageRates[customUnitRateID]; const {unit, rate} = mileageRate || { unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate: CONST.CUSTOM_UNITS.MILEAGE_IRS_RATE * 100, }; - const currency = mileageRate.currency || personalPolicy.outputCurrency; + const currency = mileageRate && mileageRate.currency ? mileageRate.currency : policy.outputCurrency; const distance = lodashGet(transaction, 'routes.route0.distance', 0); const taxRates = lodashGet(policy, 'taxRates', {}); @@ -1097,10 +1094,11 @@ export default compose( }, mileageRates: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - selector: (policy) => DistanceRequestUtils.getMileageRates(policy ? policy.id : ''), + selector: DistanceRequestUtils.getMileageRates, }, policy: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + selector: (policy, props) => (!policy || (props && props.policyID) === CONST.POLICY.ID_FAKE ? PolicyUtils.getPersonalPolicy() : policy), }, }), )(MoneyTemporaryForRefactorRequestConfirmationList); diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 0f48eb9d683f..2dff075afc14 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -21,6 +21,7 @@ import type {ViolationField} from '@hooks/useViolations'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CardUtils from '@libs/CardUtils'; import * as CurrencyUtils from '@libs/CurrencyUtils'; +import type {MileageRate} from '@libs/DistanceRequestUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; @@ -64,6 +65,9 @@ type MoneyRequestViewOnyxPropsWithoutTransaction = { /** The actions from the parent report */ parentReportActions: OnyxEntry; + + /** The rates for the policy */ + rates: Record; }; type MoneyRequestViewPropsWithoutTransaction = MoneyRequestViewOnyxPropsWithoutTransaction & { @@ -90,6 +94,7 @@ function MoneyRequestView({ policy, transactionViolations, shouldShowAnimatedBackground, + rates, }: MoneyRequestViewProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -172,15 +177,14 @@ function MoneyRequestView({ let amountDescription = `${translate('iou.amount')}`; const hasRoute = TransactionUtils.hasRoute(transaction); - const distance = transaction?.routes?.route0?.distance ?? 0; const rateID = transaction?.comment.customUnit?.customUnitRateID ?? '0'; - const rates = DistanceRequestUtils.getMileageRates(policy?.id); const currency = policy ? policy.outputCurrency : PolicyUtils.getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD; const mileageRate = TransactionUtils.isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(currency) : rates[rateID as string]; const {unit, rate} = mileageRate; + const distance = DistanceRequestUtils.getDistanceFromMerchant(transactionMerchant, unit); const rateToDisplay = DistanceRequestUtils.getRateForDisplay(hasRoute, unit, rate, currency, translate, toLocaleDigit); const distanceToDisplay = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate); @@ -554,6 +558,10 @@ export default withOnyx `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '0'}`, canEvict: false, }, + rates: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, + selector: DistanceRequestUtils.getMileageRates, + }, })( withOnyx({ transaction: { diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index c930e6ec9068..61629c5d58bf 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -1,10 +1,12 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; +import type {RateAndUnit} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Unit} from '@src/types/onyx/Policy'; import type Policy from '@src/types/onyx/Policy'; +import type {EmptyObject} from '@src/types/utils/EmptyObject'; import * as CurrencyUtils from './CurrencyUtils'; import * as PolicyUtils from './PolicyUtils'; @@ -28,6 +30,9 @@ Onyx.connect({ }, }); +const METERS_TO_KM = 0.001; // 1 kilometer is 1000 meters +const METERS_TO_MILES = 0.000621371; // There are approximately 0.000621371 miles in a meter + /** * Retrieves the default mileage rate based on a given policy. * @@ -38,7 +43,7 @@ Onyx.connect({ * @returns [currency] - The currency associated with the rate. * @returns [unit] - The unit of measurement for the distance. */ -function getDefaultMileageRate(policy: OnyxEntry): MileageRate | null { +function getDefaultMileageRate(policy: OnyxEntry | EmptyObject): MileageRate | null { if (!policy?.customUnits) { return null; } @@ -71,9 +76,6 @@ function getDefaultMileageRate(policy: OnyxEntry): MileageRate | null { * @returns The converted distance in the specified unit. */ function convertDistanceUnit(distanceInMeters: number, unit: Unit): number { - const METERS_TO_KM = 0.001; // 1 kilometer is 1000 meters - const METERS_TO_MILES = 0.000621371; // There are approximately 0.000621371 miles in a meter - switch (unit) { case CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS: return distanceInMeters * METERS_TO_KM; @@ -176,20 +178,18 @@ function getDistanceMerchant( /** * Retrieves the mileage rates for given policy. * - * @param policyID - The policy ID from which to extract the mileage rates. + * @param policy - The policy from which to extract the mileage rates. * * @returns An array of mileage rates or an empty array if not found. */ -function getMileageRates(policyID?: string): Record { +function getMileageRates(policy: OnyxEntry): Record { const mileageRates: Record = {}; - if (!policyID) { + if (!policy) { return mileageRates; } - const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? null; - - if (!policy || !policy?.customUnits) { + if (!policy?.customUnits) { return mileageRates; } @@ -211,7 +211,7 @@ function getMileageRates(policyID?: string): Record { return mileageRates; } -function getRateForP2P(currency: string) { +function getRateForP2P(currency: string): RateAndUnit { return CONST.CURRENCY_TO_DEFAULT_MILEAGE_RATE[currency] ?? CONST.CURRENCY_TO_DEFAULT_MILEAGE_RATE.USD; } @@ -229,6 +229,25 @@ function getDistanceRequestAmount(distance: number, unit: Unit, rate: number): n return Math.round(roundedDistance * rate); } +/** + * Extracts the distance from a merchant string. + * + * @param merchant - The merchant string containing the distance. + * @returns The distance extracted from the merchant string. + */ +function getDistanceFromMerchant(merchant: string | undefined, unit: Unit): number { + if (!merchant) { + return 0; + } + + const distance = Number(merchant.split(' ')[0]); + if (!distance) { + return 0; + } + + return unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS ? distance / METERS_TO_KM : distance / METERS_TO_MILES; +} + export default { getDefaultMileageRate, getDistanceMerchant, @@ -237,6 +256,7 @@ export default { getMileageRates, getDistanceForDisplay, getRateForP2P, + getDistanceFromMerchant, }; export type {MileageRate}; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 1973e665b20f..e71359724677 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -27,6 +27,7 @@ function canUseTrackExpense(betas: OnyxEntry): boolean { } function canUseP2PDistanceRequests(betas: OnyxEntry): boolean { + return true; return !!betas?.includes(CONST.BETAS.P2P_DISTANCE_REQUESTS) || canUseAllBetas(betas); } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 0240b711d804..e122078c17d9 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -284,13 +284,15 @@ function initMoneyRequest(reportID: string, policy: OnyxEntry, waypoint0: {}, waypoint1: {}, }; - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; - const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; - let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID; - if (ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isPolicyExpenseChat(parentReport)) { - customUnitRateID = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? DistanceRequestUtils.getDefaultMileageRate(policy)?.customUnitRateID ?? ''; + if (!isFromGlobalCreate) { + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; + let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID; + if (ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isPolicyExpenseChat(parentReport)) { + customUnitRateID = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? DistanceRequestUtils.getDefaultMileageRate(policy)?.customUnitRateID ?? ''; + } + comment.customUnit = {customUnitRateID}; } - comment.customUnit = {customUnitRateID}; } // Store the transaction in Onyx and mark it as not saved so it can be cleaned up later @@ -418,13 +420,24 @@ function setMoneyRequestReceipt(transactionID: string, source: string, filename: }); } -/** Set the last selected distance rate for policy */ -function setLastSelectedDistanceRates(policyID: string, rateID: string) { - Onyx.merge(ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, {[policyID]: rateID}); +/** Set custom unit rateID for the transaction draft */ +function setCustomUnitRateID(transactionID: string, reportID: string) { + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; + const policy = getPolicy(report?.policyID) ?? null; + + let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID; + + if (ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isPolicyExpenseChat(parentReport)) { + customUnitRateID = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? DistanceRequestUtils.getDefaultMileageRate(policy)?.customUnitRateID ?? ''; + } + + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {comment: {customUnit: {customUnitRateID}}}); } /** Update transaction distance rate */ -function updateDistanceRequestRate(transactionID: string, rateID: string) { +function updateDistanceRequestRate(transactionID: string, rateID: string, policyID: string) { + Onyx.merge(ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, {[policyID]: rateID}); Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {comment: {customUnit: {customUnitRateID: rateID}}}); } @@ -5296,6 +5309,7 @@ export { setMoneyRequestTaxAmount, setMoneyRequestTaxRate, setShownHoldUseExplanation, + setCustomUnitRateID, updateMoneyRequestDate, updateMoneyRequestBillable, updateMoneyRequestMerchant, @@ -5317,6 +5331,5 @@ export { trackExpense, canIOUBePaid, canApproveIOU, - setLastSelectedDistanceRates, updateDistanceRequestRate, }; diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index c6ae0c0fdb0e..734adfbc486c 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -1,21 +1,16 @@ import {useNavigation} from '@react-navigation/native'; import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; -import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; import compose from '@libs/compose'; -import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import * as IOUUtils from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; -import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import MoneyRequestParticipantsSelector from '@pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes'; import StepScreenWrapper from './StepScreenWrapper'; @@ -29,9 +24,6 @@ const propTypes = { /* Onyx Props */ /** The transaction object being modified in Onyx */ transaction: transactionPropTypes, - - // eslint-disable-next-line react/require-default-props - lastSelectedDistanceRates: PropTypes.shape({}), }; const defaultProps = { @@ -44,7 +36,6 @@ function IOURequestStepParticipants({ }, transaction, transaction: {participants = []}, - lastSelectedDistanceRates = {}, }) { const {translate} = useLocalize(); const navigation = useNavigation(); @@ -109,23 +100,7 @@ function IOURequestStepParticipants({ } IOU.setMoneyRequestParticipants_temporaryForRefactor(transactionID, val); - - const participant = val[0]; - - // change customUnitRateID when choosing - if (participant.isPolicyExpenseChat && TransactionUtils.isCustomUnitRateIDForP2P(transaction)) { - let customUnitRateID = ''; - if (lastSelectedDistanceRates[participant.policyID]) { - customUnitRateID = lastSelectedDistanceRates[participant.policyID]; - } else { - const policy = ReportUtils.getPolicy(participant.policyID); - const defaultRate = DistanceRequestUtils.getDefaultMileageRate(policy); - customUnitRateID = defaultRate ? defaultRate.customUnitRateID : ''; - } - IOU.updateDistanceRequestRate(transactionID, customUnitRateID); - } else if (!TransactionUtils.isCustomUnitRateIDForP2P(transaction)) { - IOU.updateDistanceRequestRate(transactionID, CONST.CUSTOM_UNITS.FAKE_P2P_ID); - } + IOU.setCustomUnitRateID(transactionID, reportID); numberOfParticipants.current = val.length; @@ -137,9 +112,9 @@ function IOURequestStepParticipants({ } // When a participant is selected, the reportID needs to be saved because that's the reportID that will be used in the confirmation step. - selectedReportID.current = participant.reportID; + selectedReportID.current = val[0].reportID; }, - [iouType, participants, transactionID, transaction, reportID, updateRouteParams, lastSelectedDistanceRates], + [iouType, participants, transactionID, reportID, updateRouteParams], ); const goToNextStep = useCallback( @@ -190,12 +165,4 @@ IOURequestStepParticipants.displayName = 'IOURequestStepParticipants'; IOURequestStepParticipants.propTypes = propTypes; IOURequestStepParticipants.defaultProps = defaultProps; -export default compose( - withWritableReportOrNotFound, - withFullTransactionOrNotFound, - withOnyx({ - lastSelectedDistanceRates: { - key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, - }, - }), -)(IOURequestStepParticipants); +export default compose(withWritableReportOrNotFound, withFullTransactionOrNotFound)(IOURequestStepParticipants); diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx index 3dee03fa0f60..59a5ab67a8a7 100644 --- a/src/pages/iou/request/step/IOURequestStepRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -65,8 +65,7 @@ function IOURequestStepRate({ const initiallyFocusedOption = rates[lastSelectedRate]?.name ?? CONST.CUSTOM_UNITS.DEFAULT_RATE; function selectDistanceRate(customUnitRateID: string) { - IOU.setLastSelectedDistanceRates(policy?.id ?? '', customUnitRateID); - IOU.updateDistanceRequestRate('1', customUnitRateID); + IOU.updateDistanceRequestRate('1', customUnitRateID, policy?.id ?? ''); Navigation.goBack(backTo); } @@ -106,8 +105,7 @@ export default compose( rates: { // @ts-expect-error TODO: fix when withWritableReportOrNotFound will be migrated to TS key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, - // @ts-expect-error TODO: fix when withWritableReportOrNotFound will be migrated to TS - selector: (policy) => DistanceRequestUtils.getMileageRates(policy ? policy.id : ''), + selector: DistanceRequestUtils.getMileageRates, }, }), // @ts-expect-error TODO: fix when withWritableReportOrNotFound will be migrated to TS From fcbdddd98235bd2e8b3004fe0f6681eb126f76f1 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 9 Apr 2024 21:09:49 +0200 Subject: [PATCH 035/198] fix: remove unnecessary line --- src/libs/Permissions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index e71359724677..1973e665b20f 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -27,7 +27,6 @@ function canUseTrackExpense(betas: OnyxEntry): boolean { } function canUseP2PDistanceRequests(betas: OnyxEntry): boolean { - return true; return !!betas?.includes(CONST.BETAS.P2P_DISTANCE_REQUESTS) || canUseAllBetas(betas); } From 7bf1e976e6e7db510ef1afaf1f0cc54604d423a5 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 9 Apr 2024 21:24:42 +0200 Subject: [PATCH 036/198] fix: change hardcoded transactionID value for the one from the route params --- src/pages/iou/request/step/IOURequestStepRate.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx index 59a5ab67a8a7..5952f37c81b3 100644 --- a/src/pages/iou/request/step/IOURequestStepRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -32,6 +32,9 @@ type Props = { params: { /** The route to go back to */ backTo: Route; + + /** The ID of the transaction being configured */ + transactionID: string; }; }; @@ -42,7 +45,7 @@ type Props = { function IOURequestStepRate({ policy, route: { - params: {backTo}, + params: {backTo, transactionID}, }, lastSelectedDistanceRates = {}, rates, @@ -65,7 +68,7 @@ function IOURequestStepRate({ const initiallyFocusedOption = rates[lastSelectedRate]?.name ?? CONST.CUSTOM_UNITS.DEFAULT_RATE; function selectDistanceRate(customUnitRateID: string) { - IOU.updateDistanceRequestRate('1', customUnitRateID, policy?.id ?? ''); + IOU.updateDistanceRequestRate(transactionID, customUnitRateID, policy?.id ?? ''); Navigation.goBack(backTo); } From 880a34a6d6266fbcee789255f7f05ad852436f44 Mon Sep 17 00:00:00 2001 From: bartektomczyk Date: Fri, 5 Apr 2024 12:52:45 +0200 Subject: [PATCH 037/198] feat: added new method for handliung onboarding, build new messages structure for onboarding --- src/CONST.ts | 240 +++++++++++++++++++++++++++++++++++++ src/libs/API/types.ts | 1 + src/libs/actions/Report.ts | 104 ++++++++++++++++ 3 files changed, 345 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index b07b622cec05..f317a2dca3fa 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3587,6 +3587,246 @@ const CONST = { "Expensify is best known for expense and corporate card management, but we do a lot more than that. Let me know what you're interested in and I'll help get you started.", }, + ONBOARDING_MESSAGES: { + [onboardingChoices.TRACK]: { + message: 'Here are some essential tasks to keep your business spend in shape for tax season.', + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, + tasks: [ + { + title: 'Create a workspace', + subtitle: 'Create a workspace to track expenses, scan receipts, chat, and more. ', + message: + 'Here’s how to create a workspace:\n' + + '\n' + + '1. Click your profile picture.\n' + + '2. Click Workspaces > New workspace.\n' + + '\n' + + 'Your new workspace is ready! It’ll keep all of your spend (and chats) in one place.', + }, + { + title: 'Track an expense', + subtitle: 'Track an expense in any currency, in just a few clicks.', + message: + 'Here’s how to track an expense:\n' + + '\n' + + '1. Click the green + button.\n' + + '2. Choose Track expense.\n' + + '3. Enter an amount or scan a receipt.\n' + + '4. Click Track.\n' + + '\n' + + 'And you’re done! Yep, it’s that easy.', + }, + ], + }, + [onboardingChoices.EMPLOYER]: { + message: 'Getting paid back is as easy as sending a message. Let’s go over the basics.', + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, + tasks: [ + { + title: 'Submit an expense', + subtitle: 'Submit an expense by entering an amount or scanning a receipt.', + message: + 'Here’s how to submit an expense:\n' + + '\n' + + '1. Click the green + button.\n' + + '2. Choose Submit expense.\n' + + '3. Enter an amount or scan a receipt.\n' + + '4. Add your reimburser to the request.\n' + + '\n' + + 'Then, send your request and wait for that sweet “Cha-ching!” when it’s complete.', + }, + { + title: 'Enable your wallet', + subtitle: 'You’ll need to enable your Expensify Wallet to get paid back. Don’t worry, it’s easy!', + message: + 'Here’s how to set up your wallet:\n' + + '\n' + + '1. Click your profile picture.\n' + + '2. Click Wallet > Enable wallet.\n' + + '3. Connect your bank account.\n' + + '\n' + + 'Once that’s done, you can request money from anyone and get paid back right into your personal bank account.', + }, + ], + }, + [onboardingChoices.MANAGE_TEAM]: { + message: 'Here are some important tasks to help get your team’s expenses under control.', + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, + tasks: [ + { + title: 'Create a workspace', + subtitle: 'Create a workspace to track expenses, scan receipts, chat, and more. ', + message: + 'Here’s how to create a workspace:\n' + + '\n' + + '1. Click your profile picture.\n' + + '2. Click Workspaces > New workspace.\n' + + '\n' + + 'Your new workspace is ready! It’ll keep all of your spend (and chats) in one place.', + }, + { + title: 'Meet your setup specialist', + subtitle: '', + message: + 'Meet your setup specialist, {guideName}, who can answer any questions as you get started with Expensify. Yes, a real human!\n' + + '\n' + + 'Chat with {guideName} in your [admins room]({adminsRoomID}) or [schedule a call]({guideCalendarLink}) today', + }, + { + title: 'Set up categories', + subtitle: 'Set up categories so your team can code expenses for easy reporting.', + message: + 'Here’s how to set up categories:\n' + + '\n' + + '1. Click your profile picture.\n' + + '2. Go to Workspaces > [your workspace].\n' + + '3. Click Categories.\n' + + '4. Enable and disable default categories.\n' + + '5. Click Add categories to make your own.\n' + + '\n' + + 'For more controls like requiring a category for every expense, click Settings. ', + }, + { + title: 'Add expense approvals', + subtitle: 'Add expense approvals to review your team’s spend and keep it under control.', + message: + 'Here’s how to add expense approvals:\n' + + '\n' + + '1. Click your profile picture.\n' + + '2. Go to Workspaces > [your workspace].\n' + + '3. Click More features.\n' + + '4. Enable Workflows.\n' + + '5. In Workflows, enable Add approvals.\n' + + '\n' + + 'You’ll be set as the expense approver. You can change this to any admin once you invite your team. ', + }, + { + title: 'Invite your team', + subtitle: 'Invite your team to Expensify so they can start tracking expenses today.', + message: + 'Here’s how to invite your team:\n' + + '\n' + + '1. Click your profile picture.\n' + + '2. Go to Workspaces > [your workspace].\n' + + '3. Click Members > Invite member.\n' + + '4. Enter emails or phone numbers. \n' + + '5. Add an invite message if you want.\n' + + '\n' + + 'That’s it! Happy expensing :)', + }, + ], + }, + [onboardingChoices.PERSONAL_SPEND]: { + message: 'Here’s how to track your spend in a few clicks.', + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, + tasks: [ + { + title: 'Track an expense', + subtitle: 'Track an expense in any currency, whether you have a receipt or not.', + message: + 'Here’s how to track an expense:\n' + + '\n' + + '1. Click the green + button.\n' + + '2. Choose Track expense.\n' + + '3. Enter an amount or scan a receipt.\n' + + '4. Click Track.\n' + + '\n' + + 'And you’re done! Yep, it’s that easy.', + }, + ], + }, + [onboardingChoices.CHAT_SPLIT]: { + message: 'Splitting bills with friends is as easy as sending a message. Here’s how.', + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, + tasks: [ + { + title: 'Start a chat', + subtitle: 'Start a chat with a friend or group using their email or phone number.', + message: + 'Here’s how to start a chat:\n' + + '\n' + + '1. rClick the green + button.\n' + + '2. Choose Start chat.\n' + + '3. Enter emails or phone numbers.\n' + + '\n' + + 'If any of your friends aren’t using Expensify already, they’ll be invited automatically. \n' + + '\n' + + 'Every chat will also turn into an email or text that they can respond to directly.', + }, + { + title: 'Split an expense', + subtitle: 'Split an expense right in your chat with one or more friends.', + message: + 'Here’s how to request money:\n' + + '\n' + + '1. Click the green + button.\n' + + '2. Choose Split expense.\n' + + '3. Scan a receipt or enter an amount.\n' + + '4. Add your friend(s) to the request.\n' + + '\n' + + 'Feel free to add more details if you want, or just send it off. Let’s get you paid back!', + }, + { + title: 'Enable your wallet', + subtitle: 'You’ll need to enable your Expensify Wallet to get paid back. Don’t worry, it’s easy!', + message: + 'Here’s how to enable your wallet:\n' + + '\n' + + '1. Click your profile picture.\n' + + '2. Click Wallet > Enable wallet.\n' + + '3. Add your bank account.\n' + + '\n' + + 'Once that’s done, you can request money from anyone and get paid right into your personal bank account.', + }, + ], + }, + [onboardingChoices.LOOKING_AROUND]: { + message: + '# Welcome to Expensify!\n' + + "Hi there, I'm Concierge. Chat with me here for anything you need.\n" + + '\n' + + "Expensify is best known for expense and corporate card management, but we do a lot more than that. Let me know what you're interested in and I'll help get you started.", + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, + tasks: [], + }, + }, + REPORT_FIELD_TITLE_FIELD_ID: 'text_title', MOBILE_PAGINATION_SIZE: 15, diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index fc19ba60693c..c33e96731bdc 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -134,6 +134,7 @@ const WRITE_COMMANDS = { REOPEN_TASK: 'ReopenTask', COMPLETE_TASK: 'CompleteTask', COMPLETE_ENGAGEMENT_MODAL: 'CompleteEngagementModal', + COMPLETE_GUIDED_SETUP: 'CompleteGuidedSetup', SET_NAME_VALUE_PAIR: 'SetNameValuePair', SET_REPORT_FIELD: 'Report_SetFields', DELETE_REPORT_FIELD: 'RemoveReportField', diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index d2f85362baf8..8fe56d93d634 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2774,6 +2774,109 @@ function getReportPrivateNote(reportID: string | undefined) { API.read(READ_COMMANDS.GET_REPORT_PRIVATE_NOTE, parameters, {optimisticData, successData, failureData}); } +function completeOnboarding(properties: { + data: ValueOf; + firstName: string; + lastName: string; + targetEmail: ValueOf; + engagementChoice: ValueOf; +}) { + const {data, engagementChoice, targetEmail, firstName, lastName} = properties; + const actorAccountID = PersonalDetailsUtils.getAccountIDsByLogins([targetEmail])[0]; + const targetChatReport = ReportUtils.getChatByParticipants([actorAccountID]); + const targetChatReportID = targetChatReport?.reportID ?? ''; + const targetChatPolicyID = targetChatReport?.policyID ?? ''; + + // Text message + const textComment = ReportUtils.buildOptimisticAddCommentReportAction(data.message, undefined); + const textCommentAction: OptimisticAddCommentReportAction = textComment.reportAction; + const textCommentText = textComment.commentText; + + const textMessage: AddCommentOrAttachementParams & {type: string} = { + reportID: targetChatReportID, + reportActionID: textCommentAction.reportActionID, + reportComment: textCommentText, + type: 'message', + }; + + // Video message + const videoComment = ReportUtils.buildOptimisticAddCommentReportAction(CONST.ATTACHMENT_MESSAGE_TEXT, undefined); + const videoCommentAction: OptimisticAddCommentReportAction = videoComment.reportAction; + const videoCommentText = videoComment.commentText; + + const videoMessage: AddCommentOrAttachementParams & {type: string} & typeof data.video = { + reportID: targetChatReportID, + reportActionID: videoCommentAction.reportActionID, + reportComment: videoCommentText, + type: 'video', + ...data.video, + }; + + // tasks + const tasks = data.tasks.reduce((acc, curr) => { + const currTask = ReportUtils.buildOptimisticTaskReport(actorAccountID, undefined, targetChatReportID, curr.title, undefined, targetChatPolicyID); + const taskCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(targetEmail); + const taskAddCommentReport = ReportUtils.buildOptimisticTaskCommentReportAction(currTask.reportID, curr.title, 0, `task for ${curr.title}`, targetChatReportID); + + // subtitle message + const subtitleComment = ReportUtils.buildOptimisticAddCommentReportAction(curr.subtitle, undefined, actorAccountID); + const subtitleCommentAction: OptimisticAddCommentReportAction = subtitleComment.reportAction; + const subtitleCommentText = subtitleComment.commentText; + + const subtitleMessage: AddCommentOrAttachementParams = { + reportID: currTask.reportID, + reportActionID: subtitleCommentAction.reportActionID, + reportComment: subtitleCommentText, + }; + + // instruction message + const instructionComment = ReportUtils.buildOptimisticAddCommentReportAction(curr.message, undefined, actorAccountID); + const instructionCommentAction: OptimisticAddCommentReportAction = instructionComment.reportAction; + const instructionCommentText = instructionComment.commentText; + + const instructionMessage: AddCommentOrAttachementParams = { + reportID: currTask.reportID, + reportActionID: instructionCommentAction.reportActionID, + reportComment: instructionCommentText, + }; + + return [ + ...acc, + { + parentReportActionID: taskAddCommentReport.reportAction.reportActionID, + parentReportID: currTask.parentReportID, + taskReportID: currTask.reportID, + createdTaskReportActionID: taskCreatedAction.reportActionID, + title: currTask.reportName, + description: currTask.description, + assigneeChatReportID: '', + type: 'task', + task: 'trackExpense', + }, + { + type: 'message', + ...subtitleMessage, + }, + { + type: 'message', + ...instructionMessage, + }, + ]; + }, []); + + const parameters = { + engagementChoice, + firstName, + lastName, + data: JSON.stringify([textMessage, videoMessage, ...tasks]), + }; + + // console.log('-------------------------------------'); + // console.log(JSON.stringify({...parameters, data: JSON.parse(parameters.data)}, ' ', 2)); + + API.write(WRITE_COMMANDS.COMPLETE_GUIDED_SETUP, parameters, {}); +} + /** * Completes the engagement modal that new NewDot users see when they first sign up/log in by doing the following: * @@ -3093,4 +3196,5 @@ export { setGroupDraft, clearGroupChat, startNewChat, + completeOnboarding, }; From c396f2af93022c2b13f38804517c122aa423a550 Mon Sep 17 00:00:00 2001 From: bartektomczyk Date: Fri, 5 Apr 2024 16:31:47 +0200 Subject: [PATCH 038/198] feat: call method --- src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx index ae43f850018b..bd8a7ab40917 100644 --- a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx +++ b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx @@ -80,7 +80,14 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on return; } - Report.completeEngagementModal(CONST.ONBOARDING_CONCIERGE[selectedPurpose], selectedPurpose); + // Report.completeEngagementModal(CONST.ONBOARDING_CONCIERGE[selectedPurpose], selectedPurpose); + Report.completeOnboarding({ + data: CONST.ONBOARDING_MESSAGES[selectedPurpose], + engagementChoice: selectedPurpose, + targetEmail: CONST.EMAIL.CONCIERGE, + firstName: 'Test', + lastName: 'Testovsky', + }); Navigation.dismissModal(); // Only navigate to concierge chat when central pane is visible From 9ad33297839823d773b05331be3ca8a522aae2eb Mon Sep 17 00:00:00 2001 From: bartektomczyk Date: Wed, 10 Apr 2024 08:41:54 +0200 Subject: [PATCH 039/198] feat: added optimistic data --- src/libs/actions/Report.ts | 133 ++++++++++++++++++++++++++++++++----- 1 file changed, 117 insertions(+), 16 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 8fe56d93d634..8b280e11973b 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -71,6 +71,7 @@ import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/NewRoomForm'; import type {PersonalDetails, PersonalDetailsList, PolicyReportField, RecentlyUsedReportFields, ReportActionReactions, ReportMetadata, ReportUserIsTyping} from '@src/types/onyx'; +import type * as OnyxTypes from '@src/types/onyx'; import type {Decision, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; import type {NotificationPreference, RoomVisibility, WriteCapability} from '@src/types/onyx/Report'; import type Report from '@src/types/onyx/Report'; @@ -2788,38 +2789,50 @@ function completeOnboarding(properties: { const targetChatPolicyID = targetChatReport?.policyID ?? ''; // Text message - const textComment = ReportUtils.buildOptimisticAddCommentReportAction(data.message, undefined); + const textComment = ReportUtils.buildOptimisticAddCommentReportAction(data.message, undefined, actorAccountID); const textCommentAction: OptimisticAddCommentReportAction = textComment.reportAction; const textCommentText = textComment.commentText; - const textMessage: AddCommentOrAttachementParams & {type: string} = { + const textMessage: AddCommentOrAttachementParams = { reportID: targetChatReportID, reportActionID: textCommentAction.reportActionID, reportComment: textCommentText, - type: 'message', }; // Video message - const videoComment = ReportUtils.buildOptimisticAddCommentReportAction(CONST.ATTACHMENT_MESSAGE_TEXT, undefined); + const videoComment = ReportUtils.buildOptimisticAddCommentReportAction(CONST.ATTACHMENT_MESSAGE_TEXT, undefined, actorAccountID); const videoCommentAction: OptimisticAddCommentReportAction = videoComment.reportAction; const videoCommentText = videoComment.commentText; - const videoMessage: AddCommentOrAttachementParams & {type: string} & typeof data.video = { + const videoMessage: AddCommentOrAttachementParams = { reportID: targetChatReportID, reportActionID: videoCommentAction.reportActionID, reportComment: videoCommentText, - type: 'video', - ...data.video, }; - // tasks - const tasks = data.tasks.reduce((acc, curr) => { - const currTask = ReportUtils.buildOptimisticTaskReport(actorAccountID, undefined, targetChatReportID, curr.title, undefined, targetChatPolicyID); + const tasksData = data.tasks.map((task) => { + const currTask = ReportUtils.buildOptimisticTaskReport(actorAccountID, undefined, targetChatReportID, task.title, undefined, targetChatPolicyID); const taskCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(targetEmail); - const taskAddCommentReport = ReportUtils.buildOptimisticTaskCommentReportAction(currTask.reportID, curr.title, 0, `task for ${curr.title}`, targetChatReportID); + const taskAddCommentReport = ReportUtils.buildOptimisticTaskCommentReportAction(currTask.reportID, task.title, 0, `task for ${task.title}`, targetChatReportID); // subtitle message - const subtitleComment = ReportUtils.buildOptimisticAddCommentReportAction(curr.subtitle, undefined, actorAccountID); + const subtitleComment = ReportUtils.buildOptimisticAddCommentReportAction(task.subtitle, undefined, actorAccountID); + + // instruction message + const instructionComment = ReportUtils.buildOptimisticAddCommentReportAction(task.message, undefined, actorAccountID); + + return { + currTask, + taskCreatedAction, + taskAddCommentReport, + subtitleComment, + instructionComment, + }; + }); + + // tasks + const tasksForParameters = tasksData.reduce((acc, {currTask, taskCreatedAction, taskAddCommentReport, subtitleComment, instructionComment}) => { + // subtitle message const subtitleCommentAction: OptimisticAddCommentReportAction = subtitleComment.reportAction; const subtitleCommentText = subtitleComment.commentText; @@ -2830,7 +2843,6 @@ function completeOnboarding(properties: { }; // instruction message - const instructionComment = ReportUtils.buildOptimisticAddCommentReportAction(curr.message, undefined, actorAccountID); const instructionCommentAction: OptimisticAddCommentReportAction = instructionComment.reportAction; const instructionCommentText = instructionComment.commentText; @@ -2851,7 +2863,7 @@ function completeOnboarding(properties: { description: currTask.description, assigneeChatReportID: '', type: 'task', - task: 'trackExpense', + task: engagementChoice, }, { type: 'message', @@ -2864,17 +2876,101 @@ function completeOnboarding(properties: { ]; }, []); + const tasksForOptimisticData = tasksData.reduce((acc, {currTask, taskCreatedAction, subtitleComment, instructionComment}) => { + // subtitle message + const subtitleCommentAction: OptimisticAddCommentReportAction = subtitleComment.reportAction; + const subtitleCommentText = subtitleComment.commentText; + + const subtitleMessage: AddCommentOrAttachementParams = { + reportID: currTask.reportID, + reportActionID: subtitleCommentAction.reportActionID, + reportComment: subtitleCommentText, + }; + + // instruction message + const instructionCommentAction: OptimisticAddCommentReportAction = instructionComment.reportAction; + const instructionCommentText = instructionComment.commentText; + + const instructionMessage: AddCommentOrAttachementParams = { + reportID: currTask.reportID, + reportActionID: instructionCommentAction.reportActionID, + reportComment: instructionCommentText, + }; + + return [ + ...acc, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${currTask.reportID}`, + value: { + ...currTask, + pendingFields: { + createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + reportName: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + description: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + managerID: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + isOptimisticReport: true, + }, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currTask.reportID}`, + value: {[taskCreatedAction.reportActionID]: taskCreatedAction as OnyxTypes.ReportAction}, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${currTask.reportID}`, + value: {[subtitleMessage.reportID ?? '']: subtitleMessage, [instructionMessage.reportID ?? '']: instructionMessage}, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`, + value: { + [subtitleCommentAction.reportActionID ?? '']: subtitleCommentAction as ReportAction, + [instructionCommentAction.reportActionID ?? '']: instructionCommentAction as ReportAction, + }, + }, + ]; + }, []); + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${targetChatReportID}`, + value: {[textMessage.reportID ?? '']: textMessage, [videoMessage.reportID ?? '']: videoMessage}, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`, + value: {[textCommentAction.reportActionID ?? '']: textCommentAction as ReportAction, [videoCommentAction.reportActionID ?? '']: videoCommentAction as ReportAction}, + }, + ...tasksForOptimisticData, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_INTRO_SELECTED, + value: {choice: engagementChoice}, + }, + ]; + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`, + value: {[textCommentAction.reportActionID ?? '']: {pendingAction: null}, [videoCommentAction.reportActionID ?? '']: {pendingAction: null}}, + }, + ]; + const parameters = { engagementChoice, firstName, lastName, - data: JSON.stringify([textMessage, videoMessage, ...tasks]), + data: JSON.stringify([{type: 'message', ...textMessage}, {type: 'video', ...data.video, ...videoMessage}, ...tasksForParameters]), }; // console.log('-------------------------------------'); // console.log(JSON.stringify({...parameters, data: JSON.parse(parameters.data)}, ' ', 2)); - API.write(WRITE_COMMANDS.COMPLETE_GUIDED_SETUP, parameters, {}); + API.write(WRITE_COMMANDS.COMPLETE_GUIDED_SETUP, parameters, {optimisticData, successData}); } /** @@ -2949,6 +3045,11 @@ function completeEngagementModal(text: string, choice: ValueOf Date: Wed, 10 Apr 2024 08:47:26 +0200 Subject: [PATCH 040/198] feat: added optimistic data --- src/libs/actions/Report.ts | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 8b280e11973b..3edd4515a1d2 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -726,6 +726,11 @@ function openReport( key: ONYXKEYS.PERSONAL_DETAILS_LIST, value: settledPersonalDetails, }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: settledPersonalDetails, + }); // Add the createdReportActionID parameter to the API call parameters.createdReportActionID = optimisticCreatedAction.reportActionID; @@ -1939,16 +1944,6 @@ function deleteReport(reportID: string) { Onyx.multiSet(onyxData); - // Clear the optimistic personal detail - const participantPersonalDetails: OnyxCollection = {}; - report?.participantAccountIDs?.forEach((accountID) => { - if (!allPersonalDetails?.[accountID]?.isOptimisticPersonalDetail) { - return; - } - participantPersonalDetails[accountID] = null; - }); - Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, participantPersonalDetails); - // Delete linked IOU report if (report?.iouReportID) { deleteReport(report.iouReportID); @@ -2900,7 +2895,7 @@ function completeOnboarding(properties: { return [ ...acc, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${currTask.reportID}`, value: { ...currTask, @@ -2967,9 +2962,6 @@ function completeOnboarding(properties: { data: JSON.stringify([{type: 'message', ...textMessage}, {type: 'video', ...data.video, ...videoMessage}, ...tasksForParameters]), }; - // console.log('-------------------------------------'); - // console.log(JSON.stringify({...parameters, data: JSON.parse(parameters.data)}, ' ', 2)); - API.write(WRITE_COMMANDS.COMPLETE_GUIDED_SETUP, parameters, {optimisticData, successData}); } From 1077782645bd53ca71eb3cc53d0cfbada1a29ec5 Mon Sep 17 00:00:00 2001 From: bartektomczyk Date: Wed, 10 Apr 2024 11:01:34 +0200 Subject: [PATCH 041/198] feat: update method --- src/libs/actions/Report.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 3edd4515a1d2..3f288c29723c 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2920,7 +2920,7 @@ function completeOnboarding(properties: { }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currTask.reportID}`, value: { [subtitleCommentAction.reportActionID ?? '']: subtitleCommentAction as ReportAction, [instructionCommentAction.reportActionID ?? '']: instructionCommentAction as ReportAction, @@ -2940,7 +2940,6 @@ function completeOnboarding(properties: { key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`, value: {[textCommentAction.reportActionID ?? '']: textCommentAction as ReportAction, [videoCommentAction.reportActionID ?? '']: videoCommentAction as ReportAction}, }, - ...tasksForOptimisticData, { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_INTRO_SELECTED, @@ -2962,7 +2961,7 @@ function completeOnboarding(properties: { data: JSON.stringify([{type: 'message', ...textMessage}, {type: 'video', ...data.video, ...videoMessage}, ...tasksForParameters]), }; - API.write(WRITE_COMMANDS.COMPLETE_GUIDED_SETUP, parameters, {optimisticData, successData}); + API.write(WRITE_COMMANDS.COMPLETE_GUIDED_SETUP, parameters, {}); } /** @@ -3037,11 +3036,6 @@ function completeEngagementModal(text: string, choice: ValueOf Date: Wed, 10 Apr 2024 14:44:40 +0200 Subject: [PATCH 042/198] fix: type IOURequestStepRate properly --- src/libs/Navigation/types.ts | 6 ++ .../iou/request/step/IOURequestStepRate.tsx | 64 +++++++------------ .../step/withWritableReportOrNotFound.tsx | 1 + 3 files changed, 31 insertions(+), 40 deletions(-) diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 1f1e7ec9a459..0bc20cb2baa1 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -439,6 +439,12 @@ type MoneyRequestNavigatorParamList = { iouType: string; reportID: string; }; + [SCREENS.MONEY_REQUEST.STEP_RATE]: { + iouType: ValueOf; + transactionID: string; + backTo: Routes; + reportID: string; + }; [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION]: { action: ValueOf; iouType: ValueOf; diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx index 5952f37c81b3..e56ea39e9b13 100644 --- a/src/pages/iou/request/step/IOURequestStepRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -7,41 +7,31 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as IOU from '@libs/actions/IOU'; -import compose from '@libs/compose'; import type {MileageRate} from '@libs/DistanceRequestUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Route} from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; import type {Policy} from '@src/types/onyx'; import type {Unit} from '@src/types/onyx/Policy'; import StepScreenWrapper from './StepScreenWrapper'; +import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; -type Props = { +type IOURequestStepRateOnyxProps = { /** Object of last selected rates for the policies */ - lastSelectedDistanceRates: Record; + lastSelectedDistanceRates: OnyxEntry>; /** Policy details */ policy: OnyxEntry; - /** The route object passed to this screen */ - route: { - /** The params passed to this screen */ - params: { - /** The route to go back to */ - backTo: Route; - - /** The ID of the transaction being configured */ - transactionID: string; - }; - }; - /** Mileage rates */ rates: Record; }; +type IOURequestStepRateProps = IOURequestStepRateOnyxProps & WithWritableReportOrNotFoundProps; + function IOURequestStepRate({ policy, route: { @@ -49,23 +39,23 @@ function IOURequestStepRate({ }, lastSelectedDistanceRates = {}, rates, -}: Props) { +}: IOURequestStepRateProps) { const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); - const lastSelectedRate = lastSelectedDistanceRates[policy?.id ?? '0'] ?? rates[0]?.customUnitRateID; + const lastSelectedRateID = lastSelectedDistanceRates?.[policy?.id ?? '0'] ?? rates[0]?.customUnitRateID ?? ''; const sections = Object.values(rates).map((rate) => ({ text: rate.name ?? '', alternateText: DistanceRequestUtils.getRateForDisplay(true, rate.unit, rate.rate, rate.currency, translate, toLocaleDigit), keyForList: rate.name ?? '', value: rate.customUnitRateID, - isSelected: lastSelectedRate ? lastSelectedRate === rate.customUnitRateID : Boolean(rate.name === CONST.CUSTOM_UNITS.DEFAULT_RATE), + isSelected: lastSelectedRateID ? lastSelectedRateID === rate.customUnitRateID : Boolean(rate.name === CONST.CUSTOM_UNITS.DEFAULT_RATE), })); const unit = (Object.values(rates)[0]?.unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer')) as Unit; - const initiallyFocusedOption = rates[lastSelectedRate]?.name ?? CONST.CUSTOM_UNITS.DEFAULT_RATE; + const initiallyFocusedOption = rates[lastSelectedRateID]?.name ?? CONST.CUSTOM_UNITS.DEFAULT_RATE; function selectDistanceRate(customUnitRateID: string) { IOU.updateDistanceRequestRate(transactionID, customUnitRateID, policy?.id ?? ''); @@ -93,23 +83,17 @@ function IOURequestStepRate({ IOURequestStepRate.displayName = 'IOURequestStepRate'; -export default compose( - withWritableReportOrNotFound, - withOnyx({ - // @ts-expect-error TODO: fix when withWritableReportOrNotFound will be migrated to TS - policy: { - // @ts-expect-error TODO: fix when withWritableReportOrNotFound will be migrated to TS - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, - }, - // @ts-expect-error TODO: fix when withWritableReportOrNotFound will be migrated to TS - lastSelectedDistanceRates: { - key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, - }, - rates: { - // @ts-expect-error TODO: fix when withWritableReportOrNotFound will be migrated to TS - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, - selector: DistanceRequestUtils.getMileageRates, - }, - }), - // @ts-expect-error TODO: fix when withWritableReportOrNotFound will be migrated to TS -)(IOURequestStepRate); +const IOURequestStepRateWithOnyx = withOnyx({ + policy: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, + }, + lastSelectedDistanceRates: { + key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, + }, + rates: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? '0'}`, + selector: DistanceRequestUtils.getMileageRates, + }, +})(IOURequestStepRate); + +export default withWritableReportOrNotFound(IOURequestStepRateWithOnyx); diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx index 515f6f97f280..b7cd77c6c8a6 100644 --- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx +++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx @@ -21,6 +21,7 @@ type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.STEP_WAYPOINT | typeof SCREENS.MONEY_REQUEST.STEP_DESCRIPTION | typeof SCREENS.MONEY_REQUEST.STEP_CATEGORY + | typeof SCREENS.MONEY_REQUEST.STEP_RATE | typeof SCREENS.MONEY_REQUEST.STEP_CONFIRMATION | typeof SCREENS.MONEY_REQUEST.STEP_TAX_RATE | typeof SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT; From 7fb35ab243994dac4573956f4eb317712d364bad Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 10 Apr 2024 16:46:30 +0200 Subject: [PATCH 043/198] fix: minor fix --- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 623de07a05f8..70b0b7e3e6b2 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -181,7 +181,7 @@ function MoneyRequestView({ const currency = policy ? policy.outputCurrency : PolicyUtils.getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD; - const mileageRate = TransactionUtils.isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(currency) : rates[rateID as string]; + const mileageRate = TransactionUtils.isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(currency) : rates[rateID as string] ?? {}; const {unit, rate} = mileageRate; const distance = DistanceRequestUtils.getDistanceFromMerchant(transactionMerchant, unit); From 994ea52e6f9cbcf3e2d44aef68c648a508c76c34 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Wed, 10 Apr 2024 11:57:17 -0400 Subject: [PATCH 044/198] return promises from push notification callbacks --- .../PushNotification/index.native.ts | 18 ++++++++++-------- ...ubscribeToReportCommentPushNotifications.ts | 7 ++++++- .../Notification/PushNotification/types.ts | 4 ++-- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/libs/Notification/PushNotification/index.native.ts b/src/libs/Notification/PushNotification/index.native.ts index 4e028ad82392..b15e30b94b85 100644 --- a/src/libs/Notification/PushNotification/index.native.ts +++ b/src/libs/Notification/PushNotification/index.native.ts @@ -10,7 +10,7 @@ import NotificationType from './NotificationType'; import type {ClearNotifications, Deregister, Init, OnReceived, OnSelected, Register} from './types'; import type PushNotificationType from './types'; -type NotificationEventActionCallback = (data: NotificationData) => void; +type NotificationEventActionCallback = (data: NotificationData) => Promise; type NotificationEventActionMap = Partial>>; @@ -56,7 +56,13 @@ function pushNotificationEventCallback(eventType: EventType, notification: PushP }); return; } - action(data); + + /** + * The action callback should return a promise. It's very important we return that promise so that + * when these callbacks are run in Android's background process (via Headless JS), the process waits + * for the promise to resolve before quitting + */ + return action(data); } /** @@ -83,15 +89,11 @@ function refreshNotificationOptInStatus() { */ const init: Init = () => { // Setup event listeners - Airship.addListener(EventType.PushReceived, (notification) => { - pushNotificationEventCallback(EventType.PushReceived, notification.pushPayload); - }); + Airship.addListener(EventType.PushReceived, (notification) => pushNotificationEventCallback(EventType.PushReceived, notification.pushPayload)); // Note: the NotificationResponse event has a nested PushReceived event, // so event.notification refers to the same thing as notification above ^ - Airship.addListener(EventType.NotificationResponse, (event) => { - pushNotificationEventCallback(EventType.NotificationResponse, event.pushPayload); - }); + Airship.addListener(EventType.NotificationResponse, (event) => pushNotificationEventCallback(EventType.NotificationResponse, event.pushPayload)); // Keep track of which users have enabled push notifications via an NVP. Airship.addListener(EventType.NotificationOptInStatus, refreshNotificationOptInStatus); diff --git a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts index 36051fa35c56..b31509f2aeb8 100644 --- a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts +++ b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts @@ -33,8 +33,9 @@ export default function subscribeToReportCommentPushNotifications() { PushNotification.onReceived(PushNotification.TYPE.REPORT_COMMENT, ({reportID, reportActionID, onyxData, lastUpdateID, previousUpdateID}) => { if (!ActiveClientManager.isClientTheLeader()) { Log.info('[PushNotification] received report comment notification, but ignoring it since this is not the active client'); - return; + return Promise.resolve(); } + Log.info(`[PushNotification] received report comment notification in the ${Visibility.isVisible() ? 'foreground' : 'background'}`, false, {reportID, reportActionID}); if (onyxData && lastUpdateID && previousUpdateID) { @@ -55,6 +56,8 @@ export default function subscribeToReportCommentPushNotifications() { } else { Log.hmmm("[PushNotification] Didn't apply onyx updates because some data is missing", {lastUpdateID, previousUpdateID, onyxDataCount: onyxData?.length ?? 0}); } + + return Promise.resolve(); }); // Open correct report when push notification is clicked @@ -96,5 +99,7 @@ export default function subscribeToReportCommentPushNotifications() { } }); }); + + return Promise.resolve(); }); } diff --git a/src/libs/Notification/PushNotification/types.ts b/src/libs/Notification/PushNotification/types.ts index 4399c10b4a95..cf7e54abd094 100644 --- a/src/libs/Notification/PushNotification/types.ts +++ b/src/libs/Notification/PushNotification/types.ts @@ -5,8 +5,8 @@ import type NotificationType from './NotificationType'; type Init = () => void; type Register = (notificationID: string | number) => void; type Deregister = () => void; -type OnReceived = >(notificationType: T, callback: (data: NotificationDataMap[T]) => void) => void; -type OnSelected = >(notificationType: T, callback: (data: NotificationDataMap[T]) => void) => void; +type OnReceived = >(notificationType: T, callback: (data: NotificationDataMap[T]) => Promise) => void; +type OnSelected = >(notificationType: T, callback: (data: NotificationDataMap[T]) => Promise) => void; type ClearNotifications = () => void; type PushNotification = { From 37b4241f4ddb878554ed35ce5f182a7e471c0a69 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Wed, 10 Apr 2024 12:20:15 -0400 Subject: [PATCH 045/198] optionally handle onyx update gaps synchronously --- src/libs/actions/OnyxUpdateManager.ts | 146 ++++++++++++++------------ src/libs/actions/OnyxUpdates.ts | 8 +- 2 files changed, 83 insertions(+), 71 deletions(-) diff --git a/src/libs/actions/OnyxUpdateManager.ts b/src/libs/actions/OnyxUpdateManager.ts index 40f522e215b5..54fcc22b1ad1 100644 --- a/src/libs/actions/OnyxUpdateManager.ts +++ b/src/libs/actions/OnyxUpdateManager.ts @@ -1,9 +1,11 @@ +import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as ActiveClientManager from '@libs/ActiveClientManager'; import Log from '@libs/Log'; import * as SequentialQueue from '@libs/Network/SequentialQueue'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {OnyxUpdatesFromServer} from '@src/types/onyx'; import * as App from './App'; import * as OnyxUpdates from './OnyxUpdates'; @@ -36,81 +38,85 @@ Onyx.connect({ }, }); -export default () => { - console.debug('[OnyxUpdateManager] Listening for updates from the server'); - Onyx.connect({ - key: ONYXKEYS.ONYX_UPDATES_FROM_SERVER, - callback: (value) => { - // When the OpenApp command hasn't finished yet, we should not process any updates. - if (!value || isLoadingApp) { - if (isLoadingApp) { - console.debug(`[OnyxUpdateManager] Ignoring Onyx updates while OpenApp hans't finished yet.`); - } - return; - } - // This key is shared across clients, thus every client/tab will have a copy and try to execute this method. - // It is very important to only process the missing onyx updates from leader client otherwise requests we'll execute - // several duplicated requests that are not controlled by the SequentialQueue. - if (!ActiveClientManager.isClientTheLeader()) { - return; - } +function handleOnyxUpdateGap(onyxUpdatesFromServer: OnyxEntry) { + // When the OpenApp command hasn't finished yet, we should not process any updates. + if (!onyxUpdatesFromServer || isLoadingApp) { + if (isLoadingApp) { + console.debug(`[OnyxUpdateManager] Ignoring Onyx updates while OpenApp hans't finished yet.`); + } + return; + } + // This key is shared across clients, thus every client/tab will have a copy and try to execute this method. + // It is very important to only process the missing onyx updates from leader client otherwise requests we'll execute + // several duplicated requests that are not controlled by the SequentialQueue. + if (!ActiveClientManager.isClientTheLeader()) { + return; + } - // Since we used the same key that used to store another object, let's confirm that the current object is - // following the new format before we proceed. If it isn't, then let's clear the object in Onyx. - if ( - !(typeof value === 'object' && !!value) || - !('type' in value) || - (!(value.type === CONST.ONYX_UPDATE_TYPES.HTTPS && value.request && value.response) && - !((value.type === CONST.ONYX_UPDATE_TYPES.PUSHER || value.type === CONST.ONYX_UPDATE_TYPES.AIRSHIP) && value.updates)) - ) { - console.debug('[OnyxUpdateManager] Invalid format found for updates, cleaning and unpausing the queue'); - Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, null); - SequentialQueue.unpause(); - return; - } + // Since we used the same key that used to store another object, let's confirm that the current object is + // following the new format before we proceed. If it isn't, then let's clear the object in Onyx. + if ( + !(typeof onyxUpdatesFromServer === 'object' && !!onyxUpdatesFromServer) || + !('type' in onyxUpdatesFromServer) || + (!(onyxUpdatesFromServer.type === CONST.ONYX_UPDATE_TYPES.HTTPS && onyxUpdatesFromServer.request && onyxUpdatesFromServer.response) && + !((onyxUpdatesFromServer.type === CONST.ONYX_UPDATE_TYPES.PUSHER || onyxUpdatesFromServer.type === CONST.ONYX_UPDATE_TYPES.AIRSHIP) && onyxUpdatesFromServer.updates)) + ) { + console.debug('[OnyxUpdateManager] Invalid format found for updates, cleaning and unpausing the queue'); + Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, null); + SequentialQueue.unpause(); + return; + } - const updateParams = value; - const lastUpdateIDFromServer = value.lastUpdateID; - const previousUpdateIDFromServer = value.previousUpdateID; + const updateParams = onyxUpdatesFromServer; + const lastUpdateIDFromServer = onyxUpdatesFromServer.lastUpdateID; + const previousUpdateIDFromServer = onyxUpdatesFromServer.previousUpdateID; - // In cases where we received a previousUpdateID and it doesn't match our lastUpdateIDAppliedToClient - // we need to perform one of the 2 possible cases: - // - // 1. This is the first time we're receiving an lastUpdateID, so we need to do a final reconnectApp before - // fully migrating to the reliable updates mode. - // 2. This client already has the reliable updates mode enabled, but it's missing some updates and it - // needs to fetch those. - // - // For both of those, we need to pause the sequential queue. This is important so that the updates are - // applied in their correct and specific order. If this queue was not paused, then there would be a lot of - // onyx data being applied while we are fetching the missing updates and that would put them all out of order. - SequentialQueue.pause(); - let canUnpauseQueuePromise; + // In cases where we received a previousUpdateID and it doesn't match our lastUpdateIDAppliedToClient + // we need to perform one of the 2 possible cases: + // + // 1. This is the first time we're receiving an lastUpdateID, so we need to do a final reconnectApp before + // fully migrating to the reliable updates mode. + // 2. This client already has the reliable updates mode enabled, but it's missing some updates and it + // needs to fetch those. + // + // For both of those, we need to pause the sequential queue. This is important so that the updates are + // applied in their correct and specific order. If this queue was not paused, then there would be a lot of + // onyx data being applied while we are fetching the missing updates and that would put them all out of order. + SequentialQueue.pause(); + let canUnpauseQueuePromise; - // The flow below is setting the promise to a reconnect app to address flow (1) explained above. - if (!lastUpdateIDAppliedToClient) { - Log.info('Client has not gotten reliable updates before so reconnecting the app to start the process'); + // The flow below is setting the promise to a reconnect app to address flow (1) explained above. + if (!lastUpdateIDAppliedToClient) { + Log.info('Client has not gotten reliable updates before so reconnecting the app to start the process'); - // Since this is a full reconnectApp, we'll not apply the updates we received - those will come in the reconnect app request. - canUnpauseQueuePromise = App.finalReconnectAppAfterActivatingReliableUpdates(); - } else { - // The flow below is setting the promise to a getMissingOnyxUpdates to address flow (2) explained above. - console.debug(`[OnyxUpdateManager] Client is behind the server by ${Number(previousUpdateIDFromServer) - lastUpdateIDAppliedToClient} so fetching incremental updates`); - Log.info('Gap detected in update IDs from server so fetching incremental updates', true, { - lastUpdateIDFromServer, - previousUpdateIDFromServer, - lastUpdateIDAppliedToClient, - }); - canUnpauseQueuePromise = App.getMissingOnyxUpdates(lastUpdateIDAppliedToClient, previousUpdateIDFromServer); - } + // Since this is a full reconnectApp, we'll not apply the updates we received - those will come in the reconnect app request. + canUnpauseQueuePromise = App.finalReconnectAppAfterActivatingReliableUpdates(); + } else { + // The flow below is setting the promise to a getMissingOnyxUpdates to address flow (2) explained above. + console.debug(`[OnyxUpdateManager] Client is behind the server by ${Number(previousUpdateIDFromServer) - lastUpdateIDAppliedToClient} so fetching incremental updates`); + Log.info('Gap detected in update IDs from server so fetching incremental updates', true, { + lastUpdateIDFromServer, + previousUpdateIDFromServer, + lastUpdateIDAppliedToClient, + }); + canUnpauseQueuePromise = App.getMissingOnyxUpdates(lastUpdateIDAppliedToClient, previousUpdateIDFromServer); + } - canUnpauseQueuePromise.finally(() => { - OnyxUpdates.apply(updateParams).finally(() => { - console.debug('[OnyxUpdateManager] Done applying all updates'); - Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, null); - SequentialQueue.unpause(); - }); - }); - }, + canUnpauseQueuePromise.finally(() => { + OnyxUpdates.apply(updateParams).finally(() => { + console.debug('[OnyxUpdateManager] Done applying all updates'); + Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, null); + SequentialQueue.unpause(); + }); + }); +} + +export default () => { + console.debug('[OnyxUpdateManager] Listening for updates from the server'); + Onyx.connect({ + key: ONYXKEYS.ONYX_UPDATES_FROM_SERVER, + callback: handleOnyxUpdateGap, }); }; + +export {handleOnyxUpdateGap}; diff --git a/src/libs/actions/OnyxUpdates.ts b/src/libs/actions/OnyxUpdates.ts index e093ce9a964b..157f7f16ae0d 100644 --- a/src/libs/actions/OnyxUpdates.ts +++ b/src/libs/actions/OnyxUpdates.ts @@ -9,6 +9,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxUpdateEvent, OnyxUpdatesFromServer, Request} from '@src/types/onyx'; import type Response from '@src/types/onyx/Response'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import {handleOnyxUpdateGap} from './OnyxUpdateManager'; import * as QueuedOnyxUpdates from './QueuedOnyxUpdates'; // This key needs to be separate from ONYXKEYS.ONYX_UPDATES_FROM_SERVER so that it can be updated without triggering the callback when the server IDs are updated. If that @@ -142,13 +143,18 @@ function doesClientNeedToBeUpdated(previousUpdateID = 0): boolean { return lastUpdateIDAppliedToClient < previousUpdateID; } -function applyOnyxUpdatesReliably(updates: OnyxUpdatesFromServer) { +function applyOnyxUpdatesReliably(updates: OnyxUpdatesFromServer, shouldRunSync = false) { const previousUpdateID = Number(updates.previousUpdateID) || 0; if (!doesClientNeedToBeUpdated(previousUpdateID)) { apply(updates); return; } + if (shouldRunSync) { + handleOnyxUpdateGap(updates); + return; + } + // If we reached this point, we need to pause the queue while we prepare to fetch older OnyxUpdates. SequentialQueue.pause(); saveUpdateInformation(updates); From 9e0ef4b8a97aca33da8d45e8c57a475fdd05fb57 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Wed, 10 Apr 2024 12:44:08 -0400 Subject: [PATCH 046/198] fix dependency cycle --- ...bscribeToReportCommentPushNotifications.ts | 4 +-- src/libs/actions/OnyxUpdates.ts | 21 +------------ src/libs/actions/User.ts | 4 +-- src/libs/actions/applyOnyxUpdatesReliably.ts | 30 +++++++++++++++++++ 4 files changed, 35 insertions(+), 24 deletions(-) create mode 100644 src/libs/actions/applyOnyxUpdatesReliably.ts diff --git a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts index b31509f2aeb8..7845aa6705ad 100644 --- a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts +++ b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts @@ -1,5 +1,5 @@ import Onyx from 'react-native-onyx'; -import * as OnyxUpdates from '@libs/actions/OnyxUpdates'; +import applyOnyxUpdatesReliably from '@libs/actions/applyOnyxUpdatesReliably'; import * as ActiveClientManager from '@libs/ActiveClientManager'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; @@ -52,7 +52,7 @@ export default function subscribeToReportCommentPushNotifications() { }, ], }; - OnyxUpdates.applyOnyxUpdatesReliably(updates); + applyOnyxUpdatesReliably(updates); } else { Log.hmmm("[PushNotification] Didn't apply onyx updates because some data is missing", {lastUpdateID, previousUpdateID, onyxDataCount: onyxData?.length ?? 0}); } diff --git a/src/libs/actions/OnyxUpdates.ts b/src/libs/actions/OnyxUpdates.ts index 157f7f16ae0d..e23ddaac33e6 100644 --- a/src/libs/actions/OnyxUpdates.ts +++ b/src/libs/actions/OnyxUpdates.ts @@ -2,14 +2,12 @@ import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {Merge} from 'type-fest'; import Log from '@libs/Log'; -import * as SequentialQueue from '@libs/Network/SequentialQueue'; import PusherUtils from '@libs/PusherUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxUpdateEvent, OnyxUpdatesFromServer, Request} from '@src/types/onyx'; import type Response from '@src/types/onyx/Response'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import {handleOnyxUpdateGap} from './OnyxUpdateManager'; import * as QueuedOnyxUpdates from './QueuedOnyxUpdates'; // This key needs to be separate from ONYXKEYS.ONYX_UPDATES_FROM_SERVER so that it can be updated without triggering the callback when the server IDs are updated. If that @@ -143,22 +141,5 @@ function doesClientNeedToBeUpdated(previousUpdateID = 0): boolean { return lastUpdateIDAppliedToClient < previousUpdateID; } -function applyOnyxUpdatesReliably(updates: OnyxUpdatesFromServer, shouldRunSync = false) { - const previousUpdateID = Number(updates.previousUpdateID) || 0; - if (!doesClientNeedToBeUpdated(previousUpdateID)) { - apply(updates); - return; - } - - if (shouldRunSync) { - handleOnyxUpdateGap(updates); - return; - } - - // If we reached this point, we need to pause the queue while we prepare to fetch older OnyxUpdates. - SequentialQueue.pause(); - saveUpdateInformation(updates); -} - // eslint-disable-next-line import/prefer-default-export -export {saveUpdateInformation, doesClientNeedToBeUpdated, apply, applyOnyxUpdatesReliably}; +export {saveUpdateInformation, doesClientNeedToBeUpdated, apply}; diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index dcd6e025e23b..4487dcf407c4 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -44,8 +44,8 @@ import type ReportAction from '@src/types/onyx/ReportAction'; import type {OriginalMessage} from '@src/types/onyx/ReportAction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; +import applyOnyxUpdatesReliably from './applyOnyxUpdatesReliably'; import * as Link from './Link'; -import * as OnyxUpdates from './OnyxUpdates'; import * as Report from './Report'; import * as Session from './Session'; @@ -597,7 +597,7 @@ function subscribeToUserEvents() { updates: pushJSON.updates ?? [], previousUpdateID: Number(pushJSON.previousUpdateID || 0), }; - OnyxUpdates.applyOnyxUpdatesReliably(updates); + applyOnyxUpdatesReliably(updates); }); // Handles Onyx updates coming from Pusher through the mega multipleEvents. diff --git a/src/libs/actions/applyOnyxUpdatesReliably.ts b/src/libs/actions/applyOnyxUpdatesReliably.ts new file mode 100644 index 000000000000..cae3e3a34d26 --- /dev/null +++ b/src/libs/actions/applyOnyxUpdatesReliably.ts @@ -0,0 +1,30 @@ +import * as SequentialQueue from '@libs/Network/SequentialQueue'; +import type {OnyxUpdatesFromServer} from '@src/types/onyx'; +import {handleOnyxUpdateGap} from './OnyxUpdateManager'; +import * as OnyxUpdates from './OnyxUpdates'; + +/** + * Checks for and handles gaps of onyx updates between the client and the given server updates before applying them + * + * This is in it's own lib to fix a dependency cycle from OnyxUpdateManager + * + * @param updates + * @param shouldRunSync + * @returns + */ +export default function applyOnyxUpdatesReliably(updates: OnyxUpdatesFromServer, shouldRunSync = false) { + const previousUpdateID = Number(updates.previousUpdateID) || 0; + if (!OnyxUpdates.doesClientNeedToBeUpdated(previousUpdateID)) { + OnyxUpdates.apply(updates); + return; + } + + if (shouldRunSync) { + handleOnyxUpdateGap(updates); + return; + } + + // If we reached this point, we need to pause the queue while we prepare to fetch older OnyxUpdates. + SequentialQueue.pause(); + OnyxUpdates.saveUpdateInformation(updates); +} From dc7ceb2a909c2a30a1b795eed910d620393833a5 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Wed, 10 Apr 2024 12:45:16 -0400 Subject: [PATCH 047/198] apply reliable update synchronously --- .../subscribeToReportCommentPushNotifications.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts index 7845aa6705ad..3d06ff1fb25a 100644 --- a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts +++ b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts @@ -52,7 +52,7 @@ export default function subscribeToReportCommentPushNotifications() { }, ], }; - applyOnyxUpdatesReliably(updates); + applyOnyxUpdatesReliably(updates, true); } else { Log.hmmm("[PushNotification] Didn't apply onyx updates because some data is missing", {lastUpdateID, previousUpdateID, onyxDataCount: onyxData?.length ?? 0}); } From 9f56daf34846c0868bee93831b313297146d288b Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Wed, 10 Apr 2024 13:04:53 -0400 Subject: [PATCH 048/198] simplify promise --- ...bscribeToReportCommentPushNotifications.ts | 57 ++++++++++--------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts index 3d06ff1fb25a..ad1637e07a4f 100644 --- a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts +++ b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts @@ -30,35 +30,40 @@ Onyx.connect({ * Setup reportComment push notification callbacks. */ export default function subscribeToReportCommentPushNotifications() { - PushNotification.onReceived(PushNotification.TYPE.REPORT_COMMENT, ({reportID, reportActionID, onyxData, lastUpdateID, previousUpdateID}) => { - if (!ActiveClientManager.isClientTheLeader()) { - Log.info('[PushNotification] received report comment notification, but ignoring it since this is not the active client'); - return Promise.resolve(); - } - - Log.info(`[PushNotification] received report comment notification in the ${Visibility.isVisible() ? 'foreground' : 'background'}`, false, {reportID, reportActionID}); + PushNotification.onReceived( + PushNotification.TYPE.REPORT_COMMENT, + ({reportID, reportActionID, onyxData, lastUpdateID, previousUpdateID}) => + new Promise((resolve) => { + Log.info(`[PushNotification] received report comment notification in the ${Visibility.isVisible() ? 'foreground' : 'background'}`, false, {reportID, reportActionID}); - if (onyxData && lastUpdateID && previousUpdateID) { - Log.info('[PushNotification] reliable onyx update received', false, {lastUpdateID, previousUpdateID, onyxDataCount: onyxData?.length ?? 0}); + if (!ActiveClientManager.isClientTheLeader()) { + Log.info('[PushNotification] received report comment notification, but ignoring it since this is not the active client'); + resolve(); + return; + } - const updates: OnyxUpdatesFromServer = { - type: CONST.ONYX_UPDATE_TYPES.AIRSHIP, - lastUpdateID, - previousUpdateID, - updates: [ - { - eventType: 'eventType', - data: onyxData, - }, - ], - }; - applyOnyxUpdatesReliably(updates, true); - } else { - Log.hmmm("[PushNotification] Didn't apply onyx updates because some data is missing", {lastUpdateID, previousUpdateID, onyxDataCount: onyxData?.length ?? 0}); - } + if (!onyxData || !lastUpdateID || !previousUpdateID) { + Log.hmmm("[PushNotification] didn't apply onyx updates because some data is missing", {lastUpdateID, previousUpdateID, onyxDataCount: onyxData?.length ?? 0}); + resolve(); + return; + } - return Promise.resolve(); - }); + Log.info('[PushNotification] reliable onyx update received', false, {lastUpdateID, previousUpdateID, onyxDataCount: onyxData?.length ?? 0}); + const updates: OnyxUpdatesFromServer = { + type: CONST.ONYX_UPDATE_TYPES.AIRSHIP, + lastUpdateID, + previousUpdateID, + updates: [ + { + eventType: 'eventType', + data: onyxData, + }, + ], + }; + applyOnyxUpdatesReliably(updates, true); + resolve(); + }), + ); // Open correct report when push notification is clicked PushNotification.onSelected(PushNotification.TYPE.REPORT_COMMENT, ({reportID, reportActionID}) => { From 7367ef3e3384a9f2bc2bc82e06582e8b285bdcfc Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Wed, 10 Apr 2024 13:23:21 -0400 Subject: [PATCH 049/198] add optional clientLastUpdateID to override lastUpdateIDAppliedToClient --- src/libs/actions/OnyxUpdateManager.ts | 19 +++++++++++++------ src/libs/actions/OnyxUpdates.ts | 11 +++++++---- src/libs/actions/applyOnyxUpdatesReliably.ts | 6 +++--- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/libs/actions/OnyxUpdateManager.ts b/src/libs/actions/OnyxUpdateManager.ts index 54fcc22b1ad1..2deaa134d30c 100644 --- a/src/libs/actions/OnyxUpdateManager.ts +++ b/src/libs/actions/OnyxUpdateManager.ts @@ -38,7 +38,13 @@ Onyx.connect({ }, }); -function handleOnyxUpdateGap(onyxUpdatesFromServer: OnyxEntry) { +/** + * + * @param onyxUpdatesFromServer + * @param clientLastUpdateID an optional override for the lastUpdateIDAppliedToClient + * @returns + */ +function handleOnyxUpdateGap(onyxUpdatesFromServer: OnyxEntry, clientLastUpdateID = 0) { // When the OpenApp command hasn't finished yet, we should not process any updates. if (!onyxUpdatesFromServer || isLoadingApp) { if (isLoadingApp) { @@ -70,6 +76,7 @@ function handleOnyxUpdateGap(onyxUpdatesFromServer: OnyxEntry { @@ -115,7 +122,7 @@ export default () => { console.debug('[OnyxUpdateManager] Listening for updates from the server'); Onyx.connect({ key: ONYXKEYS.ONYX_UPDATES_FROM_SERVER, - callback: handleOnyxUpdateGap, + callback: (value) => handleOnyxUpdateGap(value), }); }; diff --git a/src/libs/actions/OnyxUpdates.ts b/src/libs/actions/OnyxUpdates.ts index e23ddaac33e6..a20f615e545c 100644 --- a/src/libs/actions/OnyxUpdates.ts +++ b/src/libs/actions/OnyxUpdates.ts @@ -126,19 +126,22 @@ function saveUpdateInformation(updateParams: OnyxUpdatesFromServer) { * This function will receive the previousUpdateID from any request/pusher update that has it, compare to our current app state * and return if an update is needed * @param previousUpdateID The previousUpdateID contained in the response object + * @param clientLastUpdateID an optional override for the lastUpdateIDAppliedToClient */ -function doesClientNeedToBeUpdated(previousUpdateID = 0): boolean { +function doesClientNeedToBeUpdated(previousUpdateID = 0, clientLastUpdateID = 0): boolean { // If no previousUpdateID is sent, this is not a WRITE request so we don't need to update our current state if (!previousUpdateID) { return false; } - // If we don't have any value in lastUpdateIDAppliedToClient, this is the first time we're receiving anything, so we need to do a last reconnectApp - if (!lastUpdateIDAppliedToClient) { + const clientUpdateID = clientLastUpdateID || lastUpdateIDAppliedToClient; + + // If we don't have any value in clientUpdateID, this is the first time we're receiving anything, so we need to do a last reconnectApp + if (!clientUpdateID) { return true; } - return lastUpdateIDAppliedToClient < previousUpdateID; + return clientUpdateID < previousUpdateID; } // eslint-disable-next-line import/prefer-default-export diff --git a/src/libs/actions/applyOnyxUpdatesReliably.ts b/src/libs/actions/applyOnyxUpdatesReliably.ts index cae3e3a34d26..0dfba1388089 100644 --- a/src/libs/actions/applyOnyxUpdatesReliably.ts +++ b/src/libs/actions/applyOnyxUpdatesReliably.ts @@ -12,15 +12,15 @@ import * as OnyxUpdates from './OnyxUpdates'; * @param shouldRunSync * @returns */ -export default function applyOnyxUpdatesReliably(updates: OnyxUpdatesFromServer, shouldRunSync = false) { +export default function applyOnyxUpdatesReliably(updates: OnyxUpdatesFromServer, shouldRunSync = false, clientLastUpdateID = 0) { const previousUpdateID = Number(updates.previousUpdateID) || 0; - if (!OnyxUpdates.doesClientNeedToBeUpdated(previousUpdateID)) { + if (!OnyxUpdates.doesClientNeedToBeUpdated(previousUpdateID, clientLastUpdateID)) { OnyxUpdates.apply(updates); return; } if (shouldRunSync) { - handleOnyxUpdateGap(updates); + handleOnyxUpdateGap(updates, clientLastUpdateID); return; } From 89b0c3f5a84f81d362355b09e9eb56a4705c5f6f Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Wed, 10 Apr 2024 13:32:17 -0400 Subject: [PATCH 050/198] manually read lastUpdateIDAppliedToClient from onyx before applying updates --- ...bscribeToReportCommentPushNotifications.ts | 70 +++++++++++-------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts index ad1637e07a4f..cd34807a5735 100644 --- a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts +++ b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts @@ -26,44 +26,52 @@ Onyx.connect({ }, }); +function getLastUpdateIDAppliedToClient(): Promise { + return new Promise((resolve) => { + Onyx.connect({ + key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, + callback: (value) => resolve(value ?? 0), + }); + }); +} + /** * Setup reportComment push notification callbacks. */ export default function subscribeToReportCommentPushNotifications() { - PushNotification.onReceived( - PushNotification.TYPE.REPORT_COMMENT, - ({reportID, reportActionID, onyxData, lastUpdateID, previousUpdateID}) => - new Promise((resolve) => { - Log.info(`[PushNotification] received report comment notification in the ${Visibility.isVisible() ? 'foreground' : 'background'}`, false, {reportID, reportActionID}); + PushNotification.onReceived(PushNotification.TYPE.REPORT_COMMENT, ({reportID, reportActionID, onyxData, lastUpdateID, previousUpdateID}) => { + Log.info(`[PushNotification] received report comment notification in the ${Visibility.isVisible() ? 'foreground' : 'background'}`, false, {reportID, reportActionID}); - if (!ActiveClientManager.isClientTheLeader()) { - Log.info('[PushNotification] received report comment notification, but ignoring it since this is not the active client'); - resolve(); - return; - } + if (!ActiveClientManager.isClientTheLeader()) { + Log.info('[PushNotification] received report comment notification, but ignoring it since this is not the active client'); + return Promise.resolve(); + } - if (!onyxData || !lastUpdateID || !previousUpdateID) { - Log.hmmm("[PushNotification] didn't apply onyx updates because some data is missing", {lastUpdateID, previousUpdateID, onyxDataCount: onyxData?.length ?? 0}); - resolve(); - return; - } + if (!onyxData || !lastUpdateID || !previousUpdateID) { + Log.hmmm("[PushNotification] didn't apply onyx updates because some data is missing", {lastUpdateID, previousUpdateID, onyxDataCount: onyxData?.length ?? 0}); + return Promise.resolve(); + } + + Log.info('[PushNotification] reliable onyx update received', false, {lastUpdateID, previousUpdateID, onyxDataCount: onyxData?.length ?? 0}); + const updates: OnyxUpdatesFromServer = { + type: CONST.ONYX_UPDATE_TYPES.AIRSHIP, + lastUpdateID, + previousUpdateID, + updates: [ + { + eventType: 'eventType', + data: onyxData, + }, + ], + }; - Log.info('[PushNotification] reliable onyx update received', false, {lastUpdateID, previousUpdateID, onyxDataCount: onyxData?.length ?? 0}); - const updates: OnyxUpdatesFromServer = { - type: CONST.ONYX_UPDATE_TYPES.AIRSHIP, - lastUpdateID, - previousUpdateID, - updates: [ - { - eventType: 'eventType', - data: onyxData, - }, - ], - }; - applyOnyxUpdatesReliably(updates, true); - resolve(); - }), - ); + /** + * When this callback runs in the background on Android (via Headless JS), no other Onyx.connect callbacks will run. This means that + * lastUpdateIDAppliedToClient will NOT be populated in other libs. To workaround this, we manually read the value here + * and pass it as a param + */ + return getLastUpdateIDAppliedToClient().then((lastUpdateIDAppliedToClient) => applyOnyxUpdatesReliably(updates, true, lastUpdateIDAppliedToClient)); + }); // Open correct report when push notification is clicked PushNotification.onSelected(PushNotification.TYPE.REPORT_COMMENT, ({reportID, reportActionID}) => { From f62e13b30645bb2845e96ae575943d32cb5efed7 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Wed, 10 Apr 2024 13:37:58 -0400 Subject: [PATCH 051/198] use consistent naming --- src/libs/actions/OnyxUpdates.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/OnyxUpdates.ts b/src/libs/actions/OnyxUpdates.ts index a20f615e545c..5dc26e5e49a9 100644 --- a/src/libs/actions/OnyxUpdates.ts +++ b/src/libs/actions/OnyxUpdates.ts @@ -134,14 +134,14 @@ function doesClientNeedToBeUpdated(previousUpdateID = 0, clientLastUpdateID = 0) return false; } - const clientUpdateID = clientLastUpdateID || lastUpdateIDAppliedToClient; + const lastUpdateIDFromClient = clientLastUpdateID || lastUpdateIDAppliedToClient; - // If we don't have any value in clientUpdateID, this is the first time we're receiving anything, so we need to do a last reconnectApp - if (!clientUpdateID) { + // If we don't have any value in lastUpdateIDFromClient, this is the first time we're receiving anything, so we need to do a last reconnectApp + if (!lastUpdateIDFromClient) { return true; } - return clientUpdateID < previousUpdateID; + return lastUpdateIDFromClient < previousUpdateID; } // eslint-disable-next-line import/prefer-default-export From ddf117d2ae48aaf939f0b69e2c12ab3d4339fd83 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 10 Apr 2024 20:24:20 +0200 Subject: [PATCH 052/198] fix: minor fixes --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 2 +- src/pages/iou/request/step/IOURequestStepParticipants.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index ea783e6322b8..cda8a1a7048b 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -253,7 +253,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const theme = useTheme(); const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); - const {canUseViolations} = usePermissions(); + const {canUseP2PDistanceRequests, canUseViolations} = usePermissions(iouType); const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST; const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT; diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index 734adfbc486c..7dc94af66cd0 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -100,7 +100,7 @@ function IOURequestStepParticipants({ } IOU.setMoneyRequestParticipants_temporaryForRefactor(transactionID, val); - IOU.setCustomUnitRateID(transactionID, reportID); + IOU.setCustomUnitRateID(transactionID, val[0].reportID); numberOfParticipants.current = val.length; From 118aa7beef2563d65f1d84cb4dc44ecc5bd8ae12 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 10 Apr 2024 22:18:20 +0200 Subject: [PATCH 053/198] fix: add comment --- src/libs/DistanceRequestUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index 61629c5d58bf..28de14fba57d 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -244,7 +244,7 @@ function getDistanceFromMerchant(merchant: string | undefined, unit: Unit): numb if (!distance) { return 0; } - + // we need to convert the distance back to meters (it's saved in kilometers or miles in merchant) to pass it to getDistanceForDisplay return unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS ? distance / METERS_TO_KM : distance / METERS_TO_MILES; } From 1511b2e76d8bc6dab733af078c0b70c4c1e46493 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 11 Apr 2024 12:38:20 +0200 Subject: [PATCH 054/198] fix: display only rate when no rate name --- .../iou/request/step/IOURequestStepRate.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx index e56ea39e9b13..8d1d7a97af63 100644 --- a/src/pages/iou/request/step/IOURequestStepRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -45,13 +45,17 @@ function IOURequestStepRate({ const lastSelectedRateID = lastSelectedDistanceRates?.[policy?.id ?? '0'] ?? rates[0]?.customUnitRateID ?? ''; - const sections = Object.values(rates).map((rate) => ({ - text: rate.name ?? '', - alternateText: DistanceRequestUtils.getRateForDisplay(true, rate.unit, rate.rate, rate.currency, translate, toLocaleDigit), - keyForList: rate.name ?? '', - value: rate.customUnitRateID, - isSelected: lastSelectedRateID ? lastSelectedRateID === rate.customUnitRateID : Boolean(rate.name === CONST.CUSTOM_UNITS.DEFAULT_RATE), - })); + const sections = Object.values(rates).map((rate) => { + const rateForDisplay = DistanceRequestUtils.getRateForDisplay(true, rate.unit, rate.rate, rate.currency, translate, toLocaleDigit); + + return { + text: rate.name ?? rateForDisplay, + alternateText: rate.name ? rateForDisplay : '', + keyForList: rate.customUnitRateID, + value: rate.customUnitRateID, + isSelected: lastSelectedRateID ? lastSelectedRateID === rate.customUnitRateID : Boolean(rate.name === CONST.CUSTOM_UNITS.DEFAULT_RATE), + }; + }); const unit = (Object.values(rates)[0]?.unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer')) as Unit; From 5c03b38814489542fb45e354242efcec0c51585b Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 11 Apr 2024 13:03:38 +0200 Subject: [PATCH 055/198] fix: remove use of lastSelectedDistanceRate --- src/libs/TransactionUtils.ts | 8 +++++ .../iou/request/step/IOURequestStepRate.tsx | 35 ++++++++++++------- .../step/withFullTransactionOrNotFound.tsx | 1 + 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index d42d0775d4b4..831cc46ff553 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -638,6 +638,13 @@ function isCustomUnitRateIDForP2P(transaction: OnyxEntry): boolean return transaction?.comment?.customUnit?.customUnitRateID === CONST.CUSTOM_UNITS.FAKE_P2P_ID; } +/** + * Check if the customUnitRateID has a value default for P2P distance requests + */ +function getRateID(transaction: OnyxEntry): string | undefined { + return transaction?.comment?.customUnit?.customUnitRateID?.toString(); +} + /** * Gets the default tax name */ @@ -710,6 +717,7 @@ export { getRecentTransactions, hasViolation, isCustomUnitRateIDForP2P, + getRateID, }; export type {TransactionChanges}; diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx index 8d1d7a97af63..915bc0c5c8b5 100644 --- a/src/pages/iou/request/step/IOURequestStepRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -10,19 +10,18 @@ import * as IOU from '@libs/actions/IOU'; import type {MileageRate} from '@libs/DistanceRequestUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; +import * as TransactionUtils from '@libs/TransactionUtils'; +import withFullTransactionOrNotFound from '@pages/iou/request/step/withFullTransactionOrNotFound'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; -import type {Policy} from '@src/types/onyx'; +import type {Policy, Transaction} from '@src/types/onyx'; import type {Unit} from '@src/types/onyx/Policy'; import StepScreenWrapper from './StepScreenWrapper'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; type IOURequestStepRateOnyxProps = { - /** Object of last selected rates for the policies */ - lastSelectedDistanceRates: OnyxEntry>; - /** Policy details */ policy: OnyxEntry; @@ -30,20 +29,28 @@ type IOURequestStepRateOnyxProps = { rates: Record; }; -type IOURequestStepRateProps = IOURequestStepRateOnyxProps & WithWritableReportOrNotFoundProps; +type IOURequestStepRateProps = IOURequestStepRateOnyxProps & + WithWritableReportOrNotFoundProps & { + /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ + transaction: OnyxEntry; + }; function IOURequestStepRate({ policy, route: { params: {backTo, transactionID}, }, - lastSelectedDistanceRates = {}, + transaction, rates, }: IOURequestStepRateProps) { const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); - const lastSelectedRateID = lastSelectedDistanceRates?.[policy?.id ?? '0'] ?? rates[0]?.customUnitRateID ?? ''; + const lastSelectedRateID = TransactionUtils.getRateID(transaction) ?? ''; + + const navigateBack = () => { + Navigation.goBack(backTo); + }; const sections = Object.values(rates).map((rate) => { const rateForDisplay = DistanceRequestUtils.getRateForDisplay(true, rate.unit, rate.rate, rate.currency, translate, toLocaleDigit); @@ -63,13 +70,13 @@ function IOURequestStepRate({ function selectDistanceRate(customUnitRateID: string) { IOU.updateDistanceRequestRate(transactionID, customUnitRateID, policy?.id ?? ''); - Navigation.goBack(backTo); + navigateBack(); } return ( Navigation.goBack(backTo)} + onBackButtonPress={navigateBack} shouldShowWrapper={Boolean(backTo)} testID="rate" > @@ -91,13 +98,15 @@ const IOURequestStepRateWithOnyx = withOnyx `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, }, - lastSelectedDistanceRates: { - key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, - }, rates: { key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? '0'}`, selector: DistanceRequestUtils.getMileageRates, }, })(IOURequestStepRate); -export default withWritableReportOrNotFound(IOURequestStepRateWithOnyx); +// eslint-disable-next-line rulesdir/no-negated-variables +const IOURequestStepRateWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepRateWithOnyx); +// eslint-disable-next-line rulesdir/no-negated-variables +const IOURequestStepRateWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepRateWithWritableReportOrNotFound); + +export default IOURequestStepRateWithFullTransactionOrNotFound; diff --git a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx index 3d741725032b..d1bbf3956bc2 100644 --- a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx +++ b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx @@ -24,6 +24,7 @@ type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS | typeof SCREENS.MONEY_REQUEST.STEP_MERCHANT | typeof SCREENS.MONEY_REQUEST.STEP_TAG + | typeof SCREENS.MONEY_REQUEST.STEP_RATE | typeof SCREENS.MONEY_REQUEST.STEP_CONFIRMATION | typeof SCREENS.MONEY_REQUEST.STEP_CATEGORY | typeof SCREENS.MONEY_REQUEST.STEP_TAX_RATE; From 96c281d4682e765ca245a1a52bc0c3e2207c5128 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 11 Apr 2024 13:45:04 +0200 Subject: [PATCH 056/198] fix: minor fix --- src/pages/iou/request/step/IOURequestStepRate.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx index 915bc0c5c8b5..d7ae3ee0a19e 100644 --- a/src/pages/iou/request/step/IOURequestStepRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -11,13 +11,13 @@ import type {MileageRate} from '@libs/DistanceRequestUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as TransactionUtils from '@libs/TransactionUtils'; -import withFullTransactionOrNotFound from '@pages/iou/request/step/withFullTransactionOrNotFound'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type {Policy, Transaction} from '@src/types/onyx'; import type {Unit} from '@src/types/onyx/Policy'; import StepScreenWrapper from './StepScreenWrapper'; +import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; From 4f8f78e6593ca3a3d6215790703c72ed9c3f8d72 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 11 Apr 2024 17:00:33 +0200 Subject: [PATCH 057/198] minor fixes of onboarding messages --- src/CONST.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index f5b8ae572cea..65192eedfc94 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3610,12 +3610,12 @@ const CONST = { tasks: [ { title: 'Create a workspace', - subtitle: 'Create a workspace to track expenses, scan receipts, chat, and more. ', + subtitle: 'Create a workspace to track expenses, scan receipts, chat, and more.', message: 'Here’s how to create a workspace:\n' + '\n' + '1. Click your profile picture.\n' + - '2. Click Workspaces > New workspace.\n' + + '2. Click Workspaces > New workspace.\n' + '\n' + 'Your new workspace is ready! It’ll keep all of your spend (and chats) in one place.', }, @@ -3683,7 +3683,7 @@ const CONST = { tasks: [ { title: 'Create a workspace', - subtitle: 'Create a workspace to track expenses, scan receipts, chat, and more. ', + subtitle: 'Create a workspace to track expenses, scan receipts, chat, and more.', message: 'Here’s how to create a workspace:\n' + '\n' + @@ -3712,7 +3712,7 @@ const CONST = { '4. Enable and disable default categories.\n' + '5. Click Add categories to make your own.\n' + '\n' + - 'For more controls like requiring a category for every expense, click Settings. ', + 'For more controls like requiring a category for every expense, click Settings.', }, { title: 'Add expense approvals', @@ -3726,7 +3726,7 @@ const CONST = { '4. Enable Workflows.\n' + '5. In Workflows, enable Add approvals.\n' + '\n' + - 'You’ll be set as the expense approver. You can change this to any admin once you invite your team. ', + 'You’ll be set as the expense approver. You can change this to any admin once you invite your team.', }, { title: 'Invite your team', @@ -3785,7 +3785,7 @@ const CONST = { message: 'Here’s how to start a chat:\n' + '\n' + - '1. rClick the green + button.\n' + + '1. Click the green + button.\n' + '2. Choose Start chat.\n' + '3. Enter emails or phone numbers.\n' + '\n' + From e08f94d3239463edcbbdc47bc6d892120c2020a7 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 11 Apr 2024 17:00:57 +0200 Subject: [PATCH 058/198] create CompleteGuidedSetupParams --- src/libs/API/parameters/CompleteGuidedSetupParams.ts | 11 +++++++++++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 1 + 3 files changed, 13 insertions(+) create mode 100644 src/libs/API/parameters/CompleteGuidedSetupParams.ts diff --git a/src/libs/API/parameters/CompleteGuidedSetupParams.ts b/src/libs/API/parameters/CompleteGuidedSetupParams.ts new file mode 100644 index 000000000000..5ed7b18815af --- /dev/null +++ b/src/libs/API/parameters/CompleteGuidedSetupParams.ts @@ -0,0 +1,11 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type CompleteGuidedSetupParams = { + firstName: string; + lastName: string; + guidedSetupData: string; + engagementChoice: ValueOf; +}; + +export default CompleteGuidedSetupParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 6f5505b263fb..de12dfbe10d3 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -199,3 +199,4 @@ export type {default as SetPolicyForeignCurrencyDefaultParams} from './SetPolicy export type {default as SetPolicyCurrencyDefaultParams} from './SetPolicyCurrencyDefaultParams'; export type {default as UpdatePolicyConnectionConfigParams} from './UpdatePolicyConnectionConfigParams'; export type {default as RenamePolicyTaxParams} from './RenamePolicyTaxParams'; +export type {default as CompleteGuidedSetupParams} from './CompleteGuidedSetupParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index c33e96731bdc..ad48aecb08f0 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -324,6 +324,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.REOPEN_TASK]: Parameters.ReopenTaskParams; [WRITE_COMMANDS.COMPLETE_TASK]: Parameters.CompleteTaskParams; [WRITE_COMMANDS.COMPLETE_ENGAGEMENT_MODAL]: Parameters.CompleteEngagementModalParams; + [WRITE_COMMANDS.COMPLETE_GUIDED_SETUP]: Parameters.CompleteGuidedSetupParams; [WRITE_COMMANDS.SET_NAME_VALUE_PAIR]: Parameters.SetNameValuePairParams; [WRITE_COMMANDS.SET_REPORT_FIELD]: Parameters.SetReportFieldParams; [WRITE_COMMANDS.SET_REPORT_NAME]: Parameters.SetReportNameParams; From 202b1117f5c8949784ed1d1435955f16776e00c4 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 11 Apr 2024 17:01:37 +0200 Subject: [PATCH 059/198] integrate setDisplayName --- src/libs/actions/PersonalDetails.ts | 18 ++++++++++++++++++ .../BaseOnboardingPersonalDetails.tsx | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 8248b721c416..b9cea5c9447c 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -65,6 +65,23 @@ function updatePronouns(pronouns: string) { }); } +function setDisplayName(firstName: string, lastName: string) { + if (!currentUserAccountID) { + return; + } + + Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [currentUserAccountID]: { + firstName, + lastName, + displayName: PersonalDetailsUtils.createDisplayName(currentUserEmail ?? '', { + firstName, + lastName, + }), + }, + }); +} + function updateDisplayName(firstName: string, lastName: string) { if (!currentUserAccountID) { return; @@ -411,6 +428,7 @@ export { updateAutomaticTimezone, updateAvatar, updateDateOfBirth, + setDisplayName, updateDisplayName, updateLegalName, updatePronouns, diff --git a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx index f49c5c0f506e..248d4af4e9d0 100644 --- a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx +++ b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx @@ -37,7 +37,7 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat const {shouldUseNarrowLayout} = useOnboardingLayout(); const saveAndNavigate = useCallback((values: FormOnyxValues<'onboardingPersonalDetailsForm'>) => { - PersonalDetails.updateDisplayName(values.firstName.trim(), values.lastName.trim()); + PersonalDetails.setDisplayName(values.firstName.trim(), values.lastName.trim()); Navigation.navigate(ROUTES.ONBOARDING_PURPOSE); }, []); From fdb7f46dc1b5d612740e2da1ac4571bfd3ae1a54 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 11 Apr 2024 17:02:23 +0200 Subject: [PATCH 060/198] extend functions in ReportUtils --- src/libs/ReportUtils.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e49c2f25ccc4..48296cb03892 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3173,7 +3173,14 @@ function updateOptimisticParentReportAction(parentReportAction: OnyxEntry Date: Thu, 11 Apr 2024 17:03:51 +0200 Subject: [PATCH 061/198] improve completeOnboarding --- src/libs/actions/Report.ts | 128 ++++++++++++------ .../BaseOnboardingPurpose.tsx | 4 - 2 files changed, 89 insertions(+), 43 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 3f288c29723c..21217ee12bd6 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -15,6 +15,7 @@ import type { AddEmojiReactionParams, AddWorkspaceRoomParams, CompleteEngagementModalParams, + CompleteGuidedSetupParams, DeleteCommentParams, ExpandURLPreviewParams, FlagCommentParams, @@ -70,13 +71,20 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/NewRoomForm'; -import type {PersonalDetails, PersonalDetailsList, PolicyReportField, RecentlyUsedReportFields, ReportActionReactions, ReportMetadata, ReportUserIsTyping} from '@src/types/onyx'; -import type * as OnyxTypes from '@src/types/onyx'; +import type { + PersonalDetails, + PersonalDetailsList, + PolicyReportField, + RecentlyUsedReportFields, + ReportAction, + ReportActionReactions, + ReportMetadata, + ReportUserIsTyping, +} from '@src/types/onyx'; import type {Decision, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; import type {NotificationPreference, RoomVisibility, WriteCapability} from '@src/types/onyx/Report'; import type Report from '@src/types/onyx/Report'; import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; -import type ReportAction from '@src/types/onyx/ReportAction'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import * as CachedPDFPaths from './CachedPDFPaths'; @@ -2770,19 +2778,29 @@ function getReportPrivateNote(reportID: string | undefined) { API.read(READ_COMMANDS.GET_REPORT_PRIVATE_NOTE, parameters, {optimisticData, successData, failureData}); } -function completeOnboarding(properties: { - data: ValueOf; - firstName: string; - lastName: string; - targetEmail: ValueOf; - engagementChoice: ValueOf; -}) { - const {data, engagementChoice, targetEmail, firstName, lastName} = properties; +// #region completeOnboarding +function completeOnboarding(properties: {data: ValueOf; engagementChoice: ValueOf}) { + const {data, engagementChoice} = properties; + const targetEmail = CONST.EMAIL.CONCIERGE; + const login = allPersonalDetails?.[currentUserAccountID]?.login ?? ''; + const firstName = allPersonalDetails?.[currentUserAccountID]?.firstName ?? ''; + const lastName = allPersonalDetails?.[currentUserAccountID]?.lastName ?? ''; const actorAccountID = PersonalDetailsUtils.getAccountIDsByLogins([targetEmail])[0]; const targetChatReport = ReportUtils.getChatByParticipants([actorAccountID]); const targetChatReportID = targetChatReport?.reportID ?? ''; const targetChatPolicyID = targetChatReport?.policyID ?? ''; + // Mention message + const mentionComment = ReportUtils.buildOptimisticAddCommentReportAction(`Hey @${login} 👋`, undefined, actorAccountID); + const mentionCommentAction: OptimisticAddCommentReportAction = mentionComment.reportAction; + const mentionCommentText = mentionComment.commentText; + + const mentionMessage: AddCommentOrAttachementParams = { + reportID: targetChatReportID, + reportActionID: mentionCommentAction.reportActionID, + reportComment: mentionCommentText, + }; + // Text message const textComment = ReportUtils.buildOptimisticAddCommentReportAction(data.message, undefined, actorAccountID); const textCommentAction: OptimisticAddCommentReportAction = textComment.reportAction; @@ -2806,9 +2824,17 @@ function completeOnboarding(properties: { }; const tasksData = data.tasks.map((task) => { - const currTask = ReportUtils.buildOptimisticTaskReport(actorAccountID, undefined, targetChatReportID, task.title, undefined, targetChatPolicyID); + const currentTask = ReportUtils.buildOptimisticTaskReport( + actorAccountID, + undefined, + targetChatReportID, + task.title, + undefined, + targetChatPolicyID, + CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, + ); const taskCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(targetEmail); - const taskAddCommentReport = ReportUtils.buildOptimisticTaskCommentReportAction(currTask.reportID, task.title, 0, `task for ${task.title}`, targetChatReportID); + const taskReportAction = ReportUtils.buildOptimisticTaskCommentReportAction(currentTask.reportID, task.title, 0, `task for ${task.title}`, targetChatReportID, actorAccountID); // subtitle message const subtitleComment = ReportUtils.buildOptimisticAddCommentReportAction(task.subtitle, undefined, actorAccountID); @@ -2817,22 +2843,22 @@ function completeOnboarding(properties: { const instructionComment = ReportUtils.buildOptimisticAddCommentReportAction(task.message, undefined, actorAccountID); return { - currTask, + currentTask, taskCreatedAction, - taskAddCommentReport, + taskReportAction, subtitleComment, instructionComment, }; }); // tasks - const tasksForParameters = tasksData.reduce((acc, {currTask, taskCreatedAction, taskAddCommentReport, subtitleComment, instructionComment}) => { + const tasksForParameters = tasksData.reduce((acc, {currentTask, taskCreatedAction, taskReportAction, subtitleComment, instructionComment}) => { // subtitle message const subtitleCommentAction: OptimisticAddCommentReportAction = subtitleComment.reportAction; const subtitleCommentText = subtitleComment.commentText; const subtitleMessage: AddCommentOrAttachementParams = { - reportID: currTask.reportID, + reportID: currentTask.reportID, reportActionID: subtitleCommentAction.reportActionID, reportComment: subtitleCommentText, }; @@ -2842,7 +2868,7 @@ function completeOnboarding(properties: { const instructionCommentText = instructionComment.commentText; const instructionMessage: AddCommentOrAttachementParams = { - reportID: currTask.reportID, + reportID: currentTask.reportID, reportActionID: instructionCommentAction.reportActionID, reportComment: instructionCommentText, }; @@ -2850,12 +2876,12 @@ function completeOnboarding(properties: { return [ ...acc, { - parentReportActionID: taskAddCommentReport.reportAction.reportActionID, - parentReportID: currTask.parentReportID, - taskReportID: currTask.reportID, + parentReportActionID: taskReportAction.reportAction.reportActionID, + parentReportID: currentTask.parentReportID, + taskReportID: currentTask.reportID, createdTaskReportActionID: taskCreatedAction.reportActionID, - title: currTask.reportName, - description: currTask.description, + title: currentTask.reportName, + description: currentTask.description, assigneeChatReportID: '', type: 'task', task: engagementChoice, @@ -2871,13 +2897,13 @@ function completeOnboarding(properties: { ]; }, []); - const tasksForOptimisticData = tasksData.reduce((acc, {currTask, taskCreatedAction, subtitleComment, instructionComment}) => { + const tasksForOptimisticData = tasksData.reduce((acc, {currentTask, taskCreatedAction, taskReportAction, subtitleComment, instructionComment}) => { // subtitle message const subtitleCommentAction: OptimisticAddCommentReportAction = subtitleComment.reportAction; const subtitleCommentText = subtitleComment.commentText; const subtitleMessage: AddCommentOrAttachementParams = { - reportID: currTask.reportID, + reportID: currentTask.reportID, reportActionID: subtitleCommentAction.reportActionID, reportComment: subtitleCommentText, }; @@ -2887,18 +2913,25 @@ function completeOnboarding(properties: { const instructionCommentText = instructionComment.commentText; const instructionMessage: AddCommentOrAttachementParams = { - reportID: currTask.reportID, + reportID: currentTask.reportID, reportActionID: instructionCommentAction.reportActionID, reportComment: instructionCommentText, }; return [ ...acc, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`, + value: { + [taskReportAction.reportAction.reportActionID ?? '']: taskReportAction.reportAction as ReportAction, + }, + }, { onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${currTask.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${currentTask.reportID}`, value: { - ...currTask, + ...currentTask, pendingFields: { createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, reportName: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, @@ -2910,17 +2943,20 @@ function completeOnboarding(properties: { }, { onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currTask.reportID}`, - value: {[taskCreatedAction.reportActionID]: taskCreatedAction as OnyxTypes.ReportAction}, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentTask.reportID}`, + value: {[taskCreatedAction.reportActionID]: taskCreatedAction as ReportAction}, }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${currTask.reportID}`, - value: {[subtitleMessage.reportID ?? '']: subtitleMessage, [instructionMessage.reportID ?? '']: instructionMessage}, + key: `${ONYXKEYS.COLLECTION.REPORT}${currentTask.reportID}`, + value: { + [subtitleMessage.reportID ?? '']: subtitleMessage, + [instructionMessage.reportID ?? '']: instructionMessage, + }, }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currTask.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentTask.reportID}`, value: { [subtitleCommentAction.reportActionID ?? '']: subtitleCommentAction as ReportAction, [instructionCommentAction.reportActionID ?? '']: instructionCommentAction as ReportAction, @@ -2930,15 +2966,25 @@ function completeOnboarding(properties: { }, []); const optimisticData: OnyxUpdate[] = [ + ...tasksForOptimisticData, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${targetChatReportID}`, - value: {[textMessage.reportID ?? '']: textMessage, [videoMessage.reportID ?? '']: videoMessage}, + value: { + [mentionMessage.reportID ?? '']: mentionMessage, + [textMessage.reportID ?? '']: textMessage, + [videoMessage.reportID ?? '']: videoMessage, + lastMentionedTime: DateUtils.getDBTime(), + }, }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`, - value: {[textCommentAction.reportActionID ?? '']: textCommentAction as ReportAction, [videoCommentAction.reportActionID ?? '']: videoCommentAction as ReportAction}, + value: { + [mentionCommentAction.reportActionID ?? '']: mentionCommentAction as ReportAction, + [textCommentAction.reportActionID ?? '']: textCommentAction as ReportAction, + [videoCommentAction.reportActionID ?? '']: videoCommentAction as ReportAction, + }, }, { onyxMethod: Onyx.METHOD.MERGE, @@ -2950,18 +2996,22 @@ function completeOnboarding(properties: { { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`, - value: {[textCommentAction.reportActionID ?? '']: {pendingAction: null}, [videoCommentAction.reportActionID ?? '']: {pendingAction: null}}, + value: { + [mentionCommentAction.reportActionID ?? '']: {pendingAction: null}, + [textCommentAction.reportActionID ?? '']: {pendingAction: null}, + [videoCommentAction.reportActionID ?? '']: {pendingAction: null}, + }, }, ]; - const parameters = { + const parameters: CompleteGuidedSetupParams = { engagementChoice, firstName, lastName, - data: JSON.stringify([{type: 'message', ...textMessage}, {type: 'video', ...data.video, ...videoMessage}, ...tasksForParameters]), + guidedSetupData: JSON.stringify([{type: 'message', ...mentionMessage, ...textMessage}, {type: 'video', ...data.video, ...videoMessage}, ...tasksForParameters]), }; - API.write(WRITE_COMMANDS.COMPLETE_GUIDED_SETUP, parameters, {}); + API.write(WRITE_COMMANDS.COMPLETE_GUIDED_SETUP, parameters, {optimisticData, successData}); } /** diff --git a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx index bd8a7ab40917..62b8b072381b 100644 --- a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx +++ b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx @@ -80,13 +80,9 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on return; } - // Report.completeEngagementModal(CONST.ONBOARDING_CONCIERGE[selectedPurpose], selectedPurpose); Report.completeOnboarding({ data: CONST.ONBOARDING_MESSAGES[selectedPurpose], engagementChoice: selectedPurpose, - targetEmail: CONST.EMAIL.CONCIERGE, - firstName: 'Test', - lastName: 'Testovsky', }); Navigation.dismissModal(); From 4658af361ed628db87f0384d12cbe2d4c4423f77 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 11 Apr 2024 17:31:41 +0200 Subject: [PATCH 062/198] fix: amend comment --- src/libs/TransactionUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 831cc46ff553..1847e69a21e6 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -639,7 +639,7 @@ function isCustomUnitRateIDForP2P(transaction: OnyxEntry): boolean } /** - * Check if the customUnitRateID has a value default for P2P distance requests + * Get rate ID from the transaction object */ function getRateID(transaction: OnyxEntry): string | undefined { return transaction?.comment?.customUnit?.customUnitRateID?.toString(); From 6234c99ae920095d354dbdf64c897698d7383af9 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 11 Apr 2024 18:25:59 +0200 Subject: [PATCH 063/198] fix: lint --- ...eyTemporaryForRefactorRequestConfirmationList.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 7d382a10f30d..b8c5e5f2417e 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -335,7 +335,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate ?? 0); IOU.setMoneyRequestAmount_temporaryForRefactor(transactionID, amount, currency ?? ''); - }, [shouldCalculateDistanceAmount, distance, rate, unit, transaction, currency]); + }, [shouldCalculateDistanceAmount, distance, rate, unit, transaction, currency, transactionID]); // Calculate and set tax amount in transaction draft useEffect(() => { @@ -343,11 +343,11 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount)); if (transaction?.taxAmount && previousTransactionAmount === transaction?.amount) { - return IOU.setMoneyRequestTaxAmount(transaction?.transactionID, transaction?.taxAmount, true); + return IOU.setMoneyRequestTaxAmount(transactionID, transaction?.taxAmount, true); } IOU.setMoneyRequestTaxAmount(transactionID, amountInSmallestCurrencyUnits, true); - }, [taxRates?.defaultValue, transaction, previousTransactionAmount]); + }, [taxRates?.defaultValue, transaction, previousTransactionAmount, transactionID]); /** * Returns the participants with amount @@ -475,7 +475,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate ?? 0, currency ?? 'USD', translate, toLocaleDigit); IOU.setMoneyRequestMerchant(transactionID, distanceMerchant, true); - }, [isDistanceRequestWithPendingRoute, hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, isDistanceRequest, transaction]); + }, [isDistanceRequestWithPendingRoute, hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, isDistanceRequest, transactionID]); // Auto select the category if there is only one enabled category and it is required useEffect(() => { @@ -484,7 +484,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ return; } IOU.setMoneyRequestCategory(transactionID, enabledCategories[0].name); - }, [iouCategory, shouldShowCategories, policyCategories, transaction, isCategoryRequired]); + }, [iouCategory, shouldShowCategories, policyCategories, isCategoryRequired, transactionID]); // Auto select the tag if there is only one enabled tag and it is required useEffect(() => { @@ -500,7 +500,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ if (updatedTagsString !== TransactionUtils.getTag(transaction) && updatedTagsString) { IOU.setMoneyRequestTag(transactionID, updatedTagsString); } - }, [policyTagLists, transaction, policyTags, canUseViolations]); + }, [policyTagLists, transaction, policyTags, canUseViolations, transactionID]); /** */ From 96a70328ed190d8f2e51f724d16d6e6d65f0ac1b Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 11 Apr 2024 19:21:11 +0200 Subject: [PATCH 064/198] fix: add missing arg --- .../MoneyTemporaryForRefactorRequestConfirmationList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index b8c5e5f2417e..e7fc364799c8 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -210,7 +210,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {canUseP2PDistanceRequests, canUseViolations} = usePermissions(); + const {canUseP2PDistanceRequests, canUseViolations} = usePermissions(iouType); const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST; const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT; From 5de20289c5d4076b5a6dbfefca92a41dd1ef08c7 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 11 Apr 2024 20:39:08 +0200 Subject: [PATCH 065/198] fix: minor fix --- src/libs/actions/IOU.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 549e17aa8407..5a8514830571 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -425,7 +425,9 @@ function setMoneyRequestReceipt(transactionID: string, source: string, filename: }); } -/** Set custom unit rateID for the transaction draft */ +/** + * Set custom unit rateID for the transaction draft + */ function setCustomUnitRateID(transactionID: string, reportID: string) { const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; From 80f9b5e99c854023f93fe45c028cef5204c7130a Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 11 Apr 2024 22:58:16 +0200 Subject: [PATCH 066/198] fix: recalculate amount, fallback to default rate --- .../MoneyTemporaryForRefactorRequestConfirmationList.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index e7fc364799c8..e87516dc5e00 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -221,15 +221,16 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const mileageRate = TransactionUtils.isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(policy?.outputCurrency ?? CONST.CURRENCY.USD) - : mileageRates?.[customUnitRateID]; - - const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0; + : mileageRates?.[customUnitRateID] ?? DistanceRequestUtils.getDefaultMileageRate(policy); const {unit, rate} = mileageRate ?? { unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate: CONST.CUSTOM_UNITS.MILEAGE_IRS_RATE * 100, }; + const prevRate = usePrevious(rate); + const shouldCalculateDistanceAmount = isDistanceRequest && (iouAmount === 0 || prevRate !== rate); + const currency = (mileageRate as MileageRate)?.currency ?? policy?.outputCurrency ?? CONST.CURRENCY.USD; const distance = transaction?.routes?.route0?.distance ?? 0; From 4f291f065706f049c42249fc9458ecd7703cd8d7 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 12 Apr 2024 12:21:47 +0200 Subject: [PATCH 067/198] simplify CompleteGuidedSetupParams --- src/libs/API/parameters/CompleteGuidedSetupParams.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/libs/API/parameters/CompleteGuidedSetupParams.ts b/src/libs/API/parameters/CompleteGuidedSetupParams.ts index 5ed7b18815af..6835949382ca 100644 --- a/src/libs/API/parameters/CompleteGuidedSetupParams.ts +++ b/src/libs/API/parameters/CompleteGuidedSetupParams.ts @@ -1,11 +1,8 @@ -import type {ValueOf} from 'type-fest'; -import type CONST from '@src/CONST'; - type CompleteGuidedSetupParams = { firstName: string; lastName: string; guidedSetupData: string; - engagementChoice: ValueOf; + engagementChoice: string; }; export default CompleteGuidedSetupParams; From a32e5ea885d006af6852090fff52139e4d09ad32 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 12 Apr 2024 12:23:08 +0200 Subject: [PATCH 068/198] add todo --- src/libs/actions/Report.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 209ead38f4d7..b77979aadd45 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2788,6 +2788,7 @@ function getReportPrivateNote(reportID: string | undefined) { API.read(READ_COMMANDS.GET_REPORT_PRIVATE_NOTE, parameters, {optimisticData, successData, failureData}); } +// TODO: Clear region // #region completeOnboarding function completeOnboarding(properties: {data: ValueOf; engagementChoice: ValueOf}) { const {data, engagementChoice} = properties; From 30678e2d54a70d5cde2bddcd107d0c6884a1a1fa Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 12 Apr 2024 12:40:35 +0200 Subject: [PATCH 069/198] improve completeOnboarding --- src/libs/actions/Report.ts | 50 +++++++++++++------ .../BaseOnboardingPurpose.tsx | 5 +- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index b77979aadd45..34097945269e 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -99,6 +99,27 @@ type ActionSubscriber = { callback: SubscriberCallback; }; +type TaskForParameters = + | { + type: 'task'; + task: string; + taskReportID: string; + parentReportID: string; + parentReportActionID: string; + assigneeChatReportID: string; + createdTaskReportActionID: string; + title: string; + description: string; + } + | { + type: 'message'; + reportID: string; + reportActionID: string; + reportComment: string; + }; + +type TaskMessage = Required>; + let conciergeChatReportID: string | undefined; let currentUserAccountID = -1; let currentUserEmail: string | undefined; @@ -2790,12 +2811,9 @@ function getReportPrivateNote(reportID: string | undefined) { // TODO: Clear region // #region completeOnboarding -function completeOnboarding(properties: {data: ValueOf; engagementChoice: ValueOf}) { - const {data, engagementChoice} = properties; +function completeOnboarding(engagementChoice: string, data: ValueOf) { const targetEmail = CONST.EMAIL.CONCIERGE; - const login = allPersonalDetails?.[currentUserAccountID]?.login ?? ''; - const firstName = allPersonalDetails?.[currentUserAccountID]?.firstName ?? ''; - const lastName = allPersonalDetails?.[currentUserAccountID]?.lastName ?? ''; + const {login = '', firstName = '', lastName = ''} = allPersonalDetails?.[currentUserAccountID] ?? {}; const actorAccountID = PersonalDetailsUtils.getAccountIDsByLogins([targetEmail])[0]; const targetChatReport = ReportUtils.getChatByParticipants([actorAccountID]); const targetChatReportID = targetChatReport?.reportID ?? ''; @@ -2863,12 +2881,12 @@ function completeOnboarding(properties: {data: ValueOf((acc, {currentTask, taskCreatedAction, taskReportAction, subtitleComment, instructionComment}) => { + const tasksForParameters = tasksData.reduce((acc, {currentTask, taskCreatedAction, taskReportAction, subtitleComment, instructionComment}) => { // subtitle message const subtitleCommentAction: OptimisticAddCommentReportAction = subtitleComment.reportAction; const subtitleCommentText = subtitleComment.commentText; - const subtitleMessage: AddCommentOrAttachementParams = { + const subtitleMessage: TaskMessage = { reportID: currentTask.reportID, reportActionID: subtitleCommentAction.reportActionID, reportComment: subtitleCommentText, @@ -2878,7 +2896,7 @@ function completeOnboarding(properties: {data: ValueOf Date: Fri, 12 Apr 2024 13:02:34 +0200 Subject: [PATCH 070/198] fix: extract logic to a new function --- src/libs/actions/IOU.ts | 83 ++++++++++++++++++++++++++++++----------- 1 file changed, 61 insertions(+), 22 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 5a8514830571..50405164360f 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -268,6 +268,22 @@ function getPolicy(policyID: string | undefined): OnyxTypes.Policy | EmptyObject return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? {}; } +/** + * Returns custom unit rate ID for the transaction + */ +function getCustomUnitRateID(reportID: string) { + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; + const policy = getPolicy(report?.policyID) ?? null; + + let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID; + + if (ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isPolicyExpenseChat(parentReport)) { + customUnitRateID = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? DistanceRequestUtils.getDefaultMileageRate(policy)?.customUnitRateID ?? ''; + } + return customUnitRateID; +} + /** * Initialize money request info * @param reportID to attach the transaction to @@ -290,12 +306,7 @@ function initMoneyRequest(reportID: string, policy: OnyxEntry, waypoint1: {}, }; if (!isFromGlobalCreate) { - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; - const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; - let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID; - if (ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isPolicyExpenseChat(parentReport)) { - customUnitRateID = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? DistanceRequestUtils.getDefaultMileageRate(policy)?.customUnitRateID ?? ''; - } + const customUnitRateID = getCustomUnitRateID(reportID); comment.customUnit = {customUnitRateID}; } } @@ -362,7 +373,11 @@ function startMoneyRequest(iouType: ValueOf, reportID: st // eslint-disable-next-line @typescript-eslint/naming-convention function setMoneyRequestAmount_temporaryForRefactor(transactionID: string, amount: number, currency: string, removeOriginalCurrency = false) { if (removeOriginalCurrency) { - Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {amount, currency, originalCurrency: null}); + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, { + amount, + currency, + originalCurrency: null, + }); return; } Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {amount, currency}); @@ -377,7 +392,10 @@ function setMoneyRequestCreated(transactionID: string, created: string, isDraft: function setMoneyRequestCurrency_temporaryForRefactor(transactionID: string, currency: string, removeOriginalCurrency = false, isEditing = false) { const fieldToUpdate = isEditing ? 'modifiedCurrency' : 'currency'; if (removeOriginalCurrency) { - Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {[fieldToUpdate]: currency, originalCurrency: null}); + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, { + [fieldToUpdate]: currency, + originalCurrency: null, + }); return; } Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {[fieldToUpdate]: currency}); @@ -429,15 +447,7 @@ function setMoneyRequestReceipt(transactionID: string, source: string, filename: * Set custom unit rateID for the transaction draft */ function setCustomUnitRateID(transactionID: string, reportID: string) { - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; - const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; - const policy = getPolicy(report?.policyID) ?? null; - - let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID; - - if (ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isPolicyExpenseChat(parentReport)) { - customUnitRateID = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? DistanceRequestUtils.getDefaultMileageRate(policy)?.customUnitRateID ?? ''; - } + const customUnitRateID = getCustomUnitRateID(reportID); Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {comment: {customUnit: {customUnitRateID}}}); } @@ -475,7 +485,14 @@ function resetMoneyRequestInfo(id = '') { function getReceiptError(receipt?: Receipt, filename?: string, isScanRequest = true, errorKey?: number): Errors | ErrorFields { return isEmptyObject(receipt) || !isScanRequest ? ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage', false, errorKey) - : ErrorUtils.getMicroSecondOnyxErrorObject({error: CONST.IOU.RECEIPT_ERROR, source: receipt.source?.toString() ?? '', filename: filename ?? ''}, errorKey); + : ErrorUtils.getMicroSecondOnyxErrorObject( + { + error: CONST.IOU.RECEIPT_ERROR, + source: receipt.source?.toString() ?? '', + filename: filename ?? '', + }, + errorKey, + ); } /** Builds the Onyx data for a money request */ @@ -2711,7 +2728,13 @@ function createSplitsAndOnyxData( // Loop through participants creating individual chats, iouReports and reportActionIDs as needed const splitAmount = IOUUtils.calculateAmount(participants.length, amount, currency, false); - const splits: Split[] = [{email: currentUserEmailForIOUSplit, accountID: currentUserAccountID, amount: IOUUtils.calculateAmount(participants.length, amount, currency, true)}]; + const splits: Split[] = [ + { + email: currentUserEmailForIOUSplit, + accountID: currentUserAccountID, + amount: IOUUtils.calculateAmount(participants.length, amount, currency, true), + }, + ]; const hasMultipleParticipants = participants.length > 1; participants.forEach((participant) => { @@ -5307,10 +5330,25 @@ function setMoneyRequestParticipantsFromReport(transactionID: string, report: On const shouldAddAsReport = !isEmptyObject(chatReport) && ReportUtils.isSelfDM(chatReport); const participants: Participant[] = ReportUtils.isPolicyExpenseChat(chatReport) || shouldAddAsReport - ? [{accountID: 0, reportID: chatReport?.reportID, isPolicyExpenseChat: ReportUtils.isPolicyExpenseChat(chatReport), selected: true}] - : (chatReport?.participantAccountIDs ?? []).filter((accountID) => currentUserAccountID !== accountID).map((accountID) => ({accountID, selected: true})); + ? [ + { + accountID: 0, + reportID: chatReport?.reportID, + isPolicyExpenseChat: ReportUtils.isPolicyExpenseChat(chatReport), + selected: true, + }, + ] + : (chatReport?.participantAccountIDs ?? []) + .filter((accountID) => currentUserAccountID !== accountID) + .map((accountID) => ({ + accountID, + selected: true, + })); - Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {participants, participantsAutoAssigned: true}); + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, { + participants, + participantsAutoAssigned: true, + }); } function setMoneyRequestId(id: string) { @@ -5466,6 +5504,7 @@ function unholdRequest(transactionID: string, reportID: string) { {optimisticData, successData, failureData}, ); } + // eslint-disable-next-line rulesdir/no-negated-variables function navigateToStartStepIfScanFileCannotBeRead( receiptFilename: string | undefined, From 40c567696964dc381f8b1ea0c74c498fa25acfc4 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 12 Apr 2024 14:08:40 +0200 Subject: [PATCH 071/198] pass timestamp into getDBTimeWithSkew --- src/libs/DateUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 44c7682b47f2..cd0ad4dace94 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -378,11 +378,11 @@ function getDBTime(timestamp: string | number = ''): string { /** * Returns the current time plus skew in milliseconds in the format expected by the database */ -function getDBTimeWithSkew(): string { +function getDBTimeWithSkew(timestamp: string | number = ''): string { if (networkTimeSkew > 0) { - return getDBTime(new Date().valueOf() + networkTimeSkew); + return getDBTime(new Date(timestamp).valueOf() + networkTimeSkew); } - return getDBTime(); + return getDBTime(timestamp); } function subtractMillisecondsFromDateTime(dateTime: string, milliseconds: number): string { From 98175c1a989d5d0ec5c6c5261bdcd67b7a016d60 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 12 Apr 2024 14:08:59 +0200 Subject: [PATCH 072/198] pass createdOffset into buildOptimistic function --- src/libs/ReportUtils.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 24482de2c1d2..dd84cc7e2920 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3095,7 +3095,7 @@ function getPolicyDescriptionText(policy: OnyxEntry): string { return parser.htmlToText(policy.description); } -function buildOptimisticAddCommentReportAction(text?: string, file?: FileObject, actorAccountID?: number): OptimisticReportAction { +function buildOptimisticAddCommentReportAction(text?: string, file?: FileObject, actorAccountID?: number, createdOffset = 0): OptimisticReportAction { const parser = new ExpensiMark(); const commentText = getParsedComment(text ?? ''); const isAttachmentOnly = file && !text; @@ -3134,7 +3134,7 @@ function buildOptimisticAddCommentReportAction(text?: string, file?: FileObject, ], automatic: false, avatar: allPersonalDetails?.[accountID ?? -1]?.avatar ?? UserUtils.getDefaultAvatarURL(accountID), - created: DateUtils.getDBTimeWithSkew(), + created: DateUtils.getDBTimeWithSkew(Date.now() + createdOffset), message: [ { translationKey: isAttachmentOnly ? CONST.TRANSLATION_KEYS.ATTACHMENT : '', @@ -3202,6 +3202,7 @@ function updateOptimisticParentReportAction(parentReportAction: OnyxEntry Date: Fri, 12 Apr 2024 14:09:30 +0200 Subject: [PATCH 073/198] fix bugs and improve completeOnboarding --- src/libs/actions/Report.ts | 82 +++++++++++--------------------------- 1 file changed, 24 insertions(+), 58 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 34097945269e..0dbf8b561299 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2809,29 +2809,19 @@ function getReportPrivateNote(reportID: string | undefined) { API.read(READ_COMMANDS.GET_REPORT_PRIVATE_NOTE, parameters, {optimisticData, successData, failureData}); } -// TODO: Clear region -// #region completeOnboarding function completeOnboarding(engagementChoice: string, data: ValueOf) { const targetEmail = CONST.EMAIL.CONCIERGE; const {login = '', firstName = '', lastName = ''} = allPersonalDetails?.[currentUserAccountID] ?? {}; const actorAccountID = PersonalDetailsUtils.getAccountIDsByLogins([targetEmail])[0]; const targetChatReport = ReportUtils.getChatByParticipants([actorAccountID]); - const targetChatReportID = targetChatReport?.reportID ?? ''; - const targetChatPolicyID = targetChatReport?.policyID ?? ''; + const {reportID: targetChatReportID = '', policyID: targetChatPolicyID = ''} = targetChatReport ?? {}; // Mention message - const mentionComment = ReportUtils.buildOptimisticAddCommentReportAction(`Hey @${login} 👋`, undefined, actorAccountID); + const mentionComment = ReportUtils.buildOptimisticAddCommentReportAction(`Hey @${login.split('@')[0]} 👋`, undefined, actorAccountID); const mentionCommentAction: OptimisticAddCommentReportAction = mentionComment.reportAction; - const mentionCommentText = mentionComment.commentText; - - const mentionMessage: AddCommentOrAttachementParams = { - reportID: targetChatReportID, - reportActionID: mentionCommentAction.reportActionID, - reportComment: mentionCommentText, - }; // Text message - const textComment = ReportUtils.buildOptimisticAddCommentReportAction(data.message, undefined, actorAccountID); + const textComment = ReportUtils.buildOptimisticAddCommentReportAction(data.message, undefined, actorAccountID, 1); const textCommentAction: OptimisticAddCommentReportAction = textComment.reportAction; const textCommentText = textComment.commentText; @@ -2842,7 +2832,7 @@ function completeOnboarding(engagementChoice: string, data: ValueOf { + const tasksData = data.tasks.map((task, index) => { const currentTask = ReportUtils.buildOptimisticTaskReport( actorAccountID, undefined, @@ -2863,7 +2853,15 @@ function completeOnboarding(engagementChoice: string, data: ValueOf((acc, {currentTask, taskCreatedAction, taskReportAction, subtitleComment, instructionComment}) => { - // subtitle message const subtitleCommentAction: OptimisticAddCommentReportAction = subtitleComment.reportAction; - const subtitleCommentText = subtitleComment.commentText; - - const subtitleMessage: AddCommentOrAttachementParams = { - reportID: currentTask.reportID, - reportActionID: subtitleCommentAction.reportActionID, - reportComment: subtitleCommentText, - }; - - // instruction message const instructionCommentAction: OptimisticAddCommentReportAction = instructionComment.reportAction; - const instructionCommentText = instructionComment.commentText; - - const instructionMessage: AddCommentOrAttachementParams = { - reportID: currentTask.reportID, - reportActionID: instructionCommentAction.reportActionID, - reportComment: instructionCommentText, - }; return [ ...acc, @@ -2953,7 +2934,7 @@ function completeOnboarding(engagementChoice: string, data: ValueOf Date: Fri, 12 Apr 2024 15:08:58 +0200 Subject: [PATCH 074/198] avoid text escaping --- src/libs/ReportUtils.ts | 8 ++++---- src/libs/actions/Report.ts | 4 +--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index dd84cc7e2920..33e667e0b250 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3053,7 +3053,7 @@ function hasReportNameError(report: OnyxEntry): boolean { * For comments shorter than or equal to 10k chars, convert the comment from MD into HTML because that's how it is stored in the database * For longer comments, skip parsing, but still escape the text, and display plaintext for performance reasons. It takes over 40s to parse a 100k long string!! */ -function getParsedComment(text: string): string { +function getParsedComment(text: string, shouldEscapeText?: boolean): string { const parser = new ExpensiMark(); const textWithMention = text.replace(CONST.REGEX.SHORT_MENTION, (match) => { const mention = match.substring(1); @@ -3074,7 +3074,7 @@ function getParsedComment(text: string): string { return match; }); - return text.length <= CONST.MAX_MARKUP_LENGTH ? parser.replace(textWithMention, {shouldEscapeText: !shouldAllowRawHTMLMessages()}) : lodashEscape(text); + return text.length <= CONST.MAX_MARKUP_LENGTH ? parser.replace(textWithMention, {shouldEscapeText: shouldEscapeText ?? !shouldAllowRawHTMLMessages()}) : lodashEscape(text); } function getReportDescriptionText(report: Report): string { @@ -3095,9 +3095,9 @@ function getPolicyDescriptionText(policy: OnyxEntry): string { return parser.htmlToText(policy.description); } -function buildOptimisticAddCommentReportAction(text?: string, file?: FileObject, actorAccountID?: number, createdOffset = 0): OptimisticReportAction { +function buildOptimisticAddCommentReportAction(text?: string, file?: FileObject, actorAccountID?: number, createdOffset = 0, shouldEscapeText?: boolean): OptimisticReportAction { const parser = new ExpensiMark(); - const commentText = getParsedComment(text ?? ''); + const commentText = getParsedComment(text ?? '', shouldEscapeText); const isAttachmentOnly = file && !text; const isTextOnly = text && !file; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 0dbf8b561299..d62cd8498edd 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2865,9 +2865,7 @@ function completeOnboarding(engagementChoice: string, data: ValueOf Date: Fri, 12 Apr 2024 15:35:11 +0200 Subject: [PATCH 075/198] add replies config --- src/libs/ReportUtils.ts | 27 +++++++++++++++++++++++++++ src/libs/actions/Report.ts | 8 ++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 33e667e0b250..b5a180547a47 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -140,6 +140,10 @@ type OptimisticAddCommentReportAction = Pick< | 'childStatusNum' | 'childStateNum' | 'errors' + | 'childVisibleActionCount' + | 'childCommenterCount' + | 'childLastVisibleActionCreated' + | 'childOldestFourAccountIDs' > & {isOptimisticAction: boolean}; type OptimisticReportAction = { @@ -3212,6 +3216,12 @@ function buildOptimisticTaskCommentReportAction( parentReportID: string, actorAccountID?: number, createdOffset = 0, + repliesConfig?: { + childVisibleActionCount?: number; + childCommenterCount?: number; + childLastVisibleActionCreated?: string; + childOldestFourAccountIDs?: string; + }, ): OptimisticReportAction { const reportAction = buildOptimisticAddCommentReportAction(text, undefined, undefined, createdOffset); if (reportAction.reportAction.message?.[0]) { @@ -3231,10 +3241,27 @@ function buildOptimisticTaskCommentReportAction( reportAction.reportAction.childManagerAccountID = taskAssigneeAccountID; reportAction.reportAction.childStatusNum = CONST.REPORT.STATUS_NUM.OPEN; reportAction.reportAction.childStateNum = CONST.REPORT.STATE_NUM.OPEN; + if (actorAccountID) { reportAction.reportAction.actorAccountID = actorAccountID; } + if (repliesConfig?.childVisibleActionCount) { + reportAction.reportAction.childVisibleActionCount = repliesConfig.childVisibleActionCount; + } + + if (repliesConfig?.childCommenterCount) { + reportAction.reportAction.childCommenterCount = repliesConfig.childCommenterCount; + } + + if (repliesConfig?.childLastVisibleActionCreated) { + reportAction.reportAction.childLastVisibleActionCreated = repliesConfig.childLastVisibleActionCreated; + } + + if (repliesConfig?.childOldestFourAccountIDs) { + reportAction.reportAction.childOldestFourAccountIDs = repliesConfig.childOldestFourAccountIDs; + } + return reportAction; } diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index d62cd8498edd..4f7965b6839b 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2861,9 +2861,13 @@ function completeOnboarding(engagementChoice: string, data: ValueOf Date: Fri, 12 Apr 2024 16:02:30 +0200 Subject: [PATCH 076/198] pass mentionMessage and prettify --- src/libs/actions/Report.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 4f7965b6839b..31e0662501a9 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2819,27 +2819,28 @@ function completeOnboarding(engagementChoice: string, data: ValueOf { @@ -2880,22 +2881,17 @@ function completeOnboarding(engagementChoice: string, data: ValueOf((acc, {currentTask, taskCreatedAction, taskReportAction, subtitleComment, instructionComment}) => { - // subtitle message const subtitleCommentAction: OptimisticAddCommentReportAction = subtitleComment.reportAction; const subtitleCommentText = subtitleComment.commentText; - const subtitleMessage: TaskMessage = { reportID: currentTask.reportID, reportActionID: subtitleCommentAction.reportActionID, reportComment: subtitleCommentText, }; - // instruction message const instructionCommentAction: OptimisticAddCommentReportAction = instructionComment.reportAction; const instructionCommentText = instructionComment.commentText; - const instructionMessage: TaskMessage = { reportID: currentTask.reportID, reportActionID: instructionCommentAction.reportActionID, @@ -3005,7 +3001,7 @@ function completeOnboarding(engagementChoice: string, data: ValueOf Date: Fri, 12 Apr 2024 16:46:36 +0200 Subject: [PATCH 077/198] change ordering of pages --- src/components/TestToolMenu.tsx | 2 +- src/languages/en.ts | 1 - src/languages/es.ts | 1 - .../Navigators/OnboardingModalNavigator.tsx | 8 +-- .../BottomTabBar.tsx | 10 +-- src/libs/Navigation/linkingConfig/config.ts | 10 +-- .../BaseOnboardingPersonalDetails.tsx | 67 +++++++++++++------ .../index.native.tsx | 4 +- src/pages/OnboardingPersonalDetails/index.tsx | 4 +- .../BaseOnboardingPurpose.tsx | 34 ++-------- src/pages/OnboardingPurpose/types.ts | 23 ++++++- 11 files changed, 90 insertions(+), 74 deletions(-) diff --git a/src/components/TestToolMenu.tsx b/src/components/TestToolMenu.tsx index 527b92d4d7dc..4d1f7fe51a62 100644 --- a/src/components/TestToolMenu.tsx +++ b/src/components/TestToolMenu.tsx @@ -97,7 +97,7 @@ function TestToolMenu({user = USER_DEFAULT, network}: TestToolMenuProps) { text="Navigate" onPress={() => { Navigation.dismissModal(); - Navigation.navigate(ROUTES.ONBOARDING_PERSONAL_DETAILS); + Navigation.navigate(ROUTES.ONBOARDING_PURPOSE); }} /> diff --git a/src/languages/en.ts b/src/languages/en.ts index dbdda0d35635..076666fc595b 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1316,7 +1316,6 @@ export default { notYou: ({user}: NotYouParams) => `Not ${user}?`, }, onboarding: { - welcome: 'Welcome!', welcomeVideo: { title: 'Welcome to Expensify', description: 'Getting paid is as easy as sending a message.', diff --git a/src/languages/es.ts b/src/languages/es.ts index e81efa07a58c..30cada0d6534 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1318,7 +1318,6 @@ export default { notYou: ({user}: NotYouParams) => `¿No eres ${user}?`, }, onboarding: { - welcome: '¡Bienvenido!', welcomeVideo: { title: 'Bienvenido a Expensify', description: 'Cobrar es tan fácil como enviar un mensaje.', diff --git a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx index 6f4fbb08403b..3791cbc03641 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx @@ -23,14 +23,14 @@ function OnboardingModalNavigator() { - + diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx index 391468578fab..b48b54d22f89 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx @@ -48,16 +48,8 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps return; } - // Welcome.isOnboardingFlowCompleted({onNotCompleted: () => Navigation.navigate(ROUTES.ONBOARDING_PERSONAL_DETAILS)}); Welcome.isOnboardingFlowCompleted({ - onNotCompleted: () => - Navigation.navigate( - // Uncomment once Stage 1 Onboarding Flow is ready - // - // ROUTES.ONBOARDING_PERSONAL_DETAILS - // - ROUTES.ONBOARD, - ), + onNotCompleted: () => Navigation.navigate(ROUTES.ONBOARDING_PURPOSE), }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isLoadingApp]); diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 95294b7711b5..9401aa40281d 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -91,16 +91,16 @@ const config: LinkingOptions['config'] = { }, [NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR]: { path: ROUTES.ONBOARDING_ROOT, - initialRouteName: SCREENS.ONBOARDING.PERSONAL_DETAILS, + initialRouteName: SCREENS.ONBOARDING.PURPOSE, screens: { - [SCREENS.ONBOARDING.PERSONAL_DETAILS]: { - path: ROUTES.ONBOARDING_PERSONAL_DETAILS, - exact: true, - }, [SCREENS.ONBOARDING.PURPOSE]: { path: ROUTES.ONBOARDING_PURPOSE, exact: true, }, + [SCREENS.ONBOARDING.PERSONAL_DETAILS]: { + path: ROUTES.ONBOARDING_PERSONAL_DETAILS, + exact: true, + }, }, }, [NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: { diff --git a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx index 248d4af4e9d0..80d48a7cc29d 100644 --- a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx +++ b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx @@ -1,5 +1,6 @@ import React, {useCallback} from 'react'; import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormOnyxValues} from '@components/Form/types'; @@ -8,39 +9,56 @@ import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import OfflineIndicator from '@components/OfflineIndicator'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useOnboardingLayout from '@hooks/useOnboardingLayout'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ValidationUtils from '@libs/ValidationUtils'; +import type {BaseOnboardingPersonalDetailsOnyxProps, BaseOnboardingPersonalDetailsProps} from '@pages/OnboardingPurpose/types'; +import variables from '@styles/variables'; import * as PersonalDetails from '@userActions/PersonalDetails'; +import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/DisplayNameForm'; -type BaseOnboardingPersonalDetailsProps = { - /* Whether to use native styles tailored for native devices */ - shouldUseNativeStyles: boolean; -}; - -function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNativeStyles}: WithCurrentUserPersonalDetailsProps & BaseOnboardingPersonalDetailsProps) { - const theme = useTheme(); +function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNativeStyles, onboardingPurposeSelected}: BaseOnboardingPersonalDetailsProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); const {shouldUseNarrowLayout} = useOnboardingLayout(); - const saveAndNavigate = useCallback((values: FormOnyxValues<'onboardingPersonalDetailsForm'>) => { - PersonalDetails.setDisplayName(values.firstName.trim(), values.lastName.trim()); + const completeEngagement = useCallback( + (values: FormOnyxValues<'onboardingPersonalDetailsForm'>) => { + PersonalDetails.setDisplayName(values.firstName.trim(), values.lastName.trim()); - Navigation.navigate(ROUTES.ONBOARDING_PURPOSE); - }, []); + if (!onboardingPurposeSelected) { + return; + } + + Report.completeOnboarding(onboardingPurposeSelected, CONST.ONBOARDING_MESSAGES[onboardingPurposeSelected]); + + Navigation.dismissModal(); + // Only navigate to concierge chat when central pane is visible + // Otherwise stay on the chats screen. + if (isSmallScreenWidth) { + Navigation.navigate(ROUTES.HOME); + } else { + Report.navigateToConciergeChat(); + } + + // Small delay purely due to design considerations, + // no special technical reasons behind that. + setTimeout(() => { + Navigation.navigate(ROUTES.WELCOME_VIDEO_ROOT); + }, variables.welcomeVideoDelay); + }, + [isSmallScreenWidth, onboardingPurposeSelected], + ); const validate = (values: FormOnyxValues<'onboardingPersonalDetailsForm'>) => { const errors = {}; @@ -74,14 +92,18 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat return errors; }; + const handleGoBack = useCallback(() => { + Navigation.goBack(); + }, []); + const PersonalDetailsFooterInstance = ; return ( - {translate('onboarding.welcome')} {translate('onboarding.whatsYourName')} @@ -141,6 +162,10 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat BaseOnboardingPersonalDetails.displayName = 'BaseOnboardingPersonalDetails'; -export default withCurrentUserPersonalDetails(BaseOnboardingPersonalDetails); - -export type {BaseOnboardingPersonalDetailsProps}; +export default withCurrentUserPersonalDetails( + withOnyx({ + onboardingPurposeSelected: { + key: ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, + }, + })(BaseOnboardingPersonalDetails), +); diff --git a/src/pages/OnboardingPersonalDetails/index.native.tsx b/src/pages/OnboardingPersonalDetails/index.native.tsx index 3c49a13178e6..778d68e39458 100644 --- a/src/pages/OnboardingPersonalDetails/index.native.tsx +++ b/src/pages/OnboardingPersonalDetails/index.native.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import type {BaseOnboardingPersonalDetailsProps} from './BaseOnboardingPersonalDetails'; +import type {OnboardingPersonalDetailsProps} from '@pages/OnboardingPurpose/types'; import BaseOnboardingPersonalDetails from './BaseOnboardingPersonalDetails'; -function OnboardingPersonalDetails({...rest}: Omit) { +function OnboardingPersonalDetails({...rest}: Omit) { return ( ) { +function OnboardingPersonalDetails({...rest}: Omit) { return ( { - Navigation.goBack(); - }, []); - const selectedCheckboxIcon = useMemo( () => ( @@ -75,28 +70,13 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on [styles.pointerEventsAuto, styles.popoverMenuIcon, theme.success], ); - const completeEngagement = useCallback(() => { + const saveAndNavigate = useCallback(() => { if (selectedPurpose === undefined) { return; } - Report.completeOnboarding(selectedPurpose, CONST.ONBOARDING_MESSAGES[selectedPurpose]); - - Navigation.dismissModal(); - // Only navigate to concierge chat when central pane is visible - // Otherwise stay on the chats screen. - if (isSmallScreenWidth) { - Navigation.navigate(ROUTES.HOME); - } else { - Report.navigateToConciergeChat(); - } - - // Small delay purely due to design considerations, - // no special technical reasons behind that. - setTimeout(() => { - Navigation.navigate(ROUTES.WELCOME_VIDEO_ROOT); - }, variables.welcomeVideoDelay); - }, [isSmallScreenWidth, selectedPurpose]); + Navigation.navigate(ROUTES.ONBOARDING_PERSONAL_DETAILS); + }, [selectedPurpose]); const menuItems: MenuItemProps[] = Object.values(CONST.ONBOARDING_CHOICES).map((choice) => { const translationKey = `onboarding.purpose.${choice}` as const; @@ -126,9 +106,9 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on @@ -152,7 +132,7 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on return; } setError(false); - completeEngagement(); + saveAndNavigate(); }} message={errorMessage} isAlertVisible={error || Boolean(errorMessage)} diff --git a/src/pages/OnboardingPurpose/types.ts b/src/pages/OnboardingPurpose/types.ts index 586463a26bb0..15d6beec1f6f 100644 --- a/src/pages/OnboardingPurpose/types.ts +++ b/src/pages/OnboardingPurpose/types.ts @@ -1,4 +1,5 @@ import type {OnyxEntry} from 'react-native-onyx'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; type OnboardingPurposeProps = Record; @@ -16,4 +17,24 @@ type BaseOnboardingPurposeProps = OnboardingPurposeProps & shouldEnableMaxHeight: boolean; }; -export type {BaseOnboardingPurposeOnyxProps, BaseOnboardingPurposeProps, OnboardingPurposeProps}; +type OnboardingPersonalDetailsProps = Record; + +type BaseOnboardingPersonalDetailsOnyxProps = { + /** Saved onboarding purpose selected by the user */ + onboardingPurposeSelected: OnyxEntry; +}; + +type BaseOnboardingPersonalDetailsProps = WithCurrentUserPersonalDetailsProps & + BaseOnboardingPersonalDetailsOnyxProps & { + /* Whether to use native styles tailored for native devices */ + shouldUseNativeStyles: boolean; + }; + +export type { + BaseOnboardingPurposeOnyxProps, + BaseOnboardingPurposeProps, + OnboardingPurposeProps, + OnboardingPersonalDetailsProps, + BaseOnboardingPersonalDetailsOnyxProps, + BaseOnboardingPersonalDetailsProps, +}; From f5cd44bbba30f2294149d7f96454b67ca1b063d4 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 12 Apr 2024 16:52:00 +0200 Subject: [PATCH 078/198] fix bottom padding --- .../OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx index 80d48a7cc29d..bf39940e1536 100644 --- a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx +++ b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx @@ -110,7 +110,7 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat behavior="padding" > Date: Fri, 12 Apr 2024 16:56:50 +0200 Subject: [PATCH 079/198] pass current user info --- src/libs/actions/Report.ts | 14 +++++++++++++- .../BaseOnboardingPersonalDetails.tsx | 13 ++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 31e0662501a9..75ab3c530e36 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2809,7 +2809,19 @@ function getReportPrivateNote(reportID: string | undefined) { API.read(READ_COMMANDS.GET_REPORT_PRIVATE_NOTE, parameters, {optimisticData, successData, failureData}); } -function completeOnboarding(engagementChoice: string, data: ValueOf) { +function completeOnboarding( + engagementChoice: string, + data: ValueOf, + { + login, + firstName, + lastName, + }: { + login: string; + firstName: string; + lastName: string; + }, +) { const targetEmail = CONST.EMAIL.CONCIERGE; const {login = '', firstName = '', lastName = ''} = allPersonalDetails?.[currentUserAccountID] ?? {}; const actorAccountID = PersonalDetailsUtils.getAccountIDsByLogins([targetEmail])[0]; diff --git a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx index bf39940e1536..776050dc681d 100644 --- a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx +++ b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx @@ -34,13 +34,20 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat const completeEngagement = useCallback( (values: FormOnyxValues<'onboardingPersonalDetailsForm'>) => { - PersonalDetails.setDisplayName(values.firstName.trim(), values.lastName.trim()); + const firstName = values.firstName.trim(); + const lastName = values.lastName.trim(); + + PersonalDetails.setDisplayName(firstName, lastName); if (!onboardingPurposeSelected) { return; } - Report.completeOnboarding(onboardingPurposeSelected, CONST.ONBOARDING_MESSAGES[onboardingPurposeSelected]); + Report.completeOnboarding(onboardingPurposeSelected, CONST.ONBOARDING_MESSAGES[onboardingPurposeSelected], { + login: currentUserPersonalDetails.login ?? '', + firstName, + lastName, + }); Navigation.dismissModal(); // Only navigate to concierge chat when central pane is visible @@ -57,7 +64,7 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat Navigation.navigate(ROUTES.WELCOME_VIDEO_ROOT); }, variables.welcomeVideoDelay); }, - [isSmallScreenWidth, onboardingPurposeSelected], + [currentUserPersonalDetails.login, isSmallScreenWidth, onboardingPurposeSelected], ); const validate = (values: FormOnyxValues<'onboardingPersonalDetailsForm'>) => { From 1fb2411c29bb7e4c22e29a8aacbd0da230c2cf09 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 12 Apr 2024 17:13:20 +0200 Subject: [PATCH 080/198] remove line --- src/libs/actions/Report.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 75ab3c530e36..c8ba2b5c0593 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2823,7 +2823,6 @@ function completeOnboarding( }, ) { const targetEmail = CONST.EMAIL.CONCIERGE; - const {login = '', firstName = '', lastName = ''} = allPersonalDetails?.[currentUserAccountID] ?? {}; const actorAccountID = PersonalDetailsUtils.getAccountIDsByLogins([targetEmail])[0]; const targetChatReport = ReportUtils.getChatByParticipants([actorAccountID]); const {reportID: targetChatReportID = '', policyID: targetChatPolicyID = ''} = targetChatReport ?? {}; From 592bf7a542290977e5565931023eda7f6c99811f Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 12 Apr 2024 22:18:27 +0200 Subject: [PATCH 081/198] fix: fix problem with workspace customRateID --- ...raryForRefactorRequestConfirmationList.tsx | 27 ++++++++++++++++- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/DistanceRequestUtils.ts | 26 +++++++++++++++- src/libs/actions/IOU.ts | 30 ++----------------- .../step/IOURequestStepParticipants.tsx | 4 ++- 6 files changed, 58 insertions(+), 31 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index e87516dc5e00..44042a976230 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -66,6 +66,12 @@ type MoneyRequestConfirmationListOnyxProps = { /** Unit and rate used for if the money request is a distance request */ mileageRates: OnyxEntry>; + + /** Mileage rate default for the policy */ + defaultMileageRate: OnyxEntry; + + /** Last selected distance rates */ + lastSelectedDistanceRates: OnyxEntry>; }; type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & { @@ -205,6 +211,8 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ onToggleBillable, hasSmartScanFailed, reportActionID, + defaultMileageRate, + lastSelectedDistanceRates, }: MoneyRequestConfirmationListProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -217,8 +225,19 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const isTypeSend = iouType === CONST.IOU.TYPE.SEND; const isTypeTrackExpense = iouType === CONST.IOU.TYPE.TRACK_EXPENSE; + const transactionID = transaction?.transactionID ?? ''; const customUnitRateID = TransactionUtils.getRateID(transaction) ?? ''; + useEffect(() => { + if (customUnitRateID || !canUseP2PDistanceRequests) { + return; + } + if (!customUnitRateID && defaultMileageRate) { + const rateID = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? defaultMileageRate?.customUnitRateID ?? ''; + IOU.setCustomUnitRateID(transactionID, rateID); + } + }, [defaultMileageRate, customUnitRateID, lastSelectedDistanceRates, policy?.id, canUseP2PDistanceRequests, transactionID]); + const mileageRate = TransactionUtils.isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(policy?.outputCurrency ?? CONST.CURRENCY.USD) : mileageRates?.[customUnitRateID] ?? DistanceRequestUtils.getDefaultMileageRate(policy); @@ -235,7 +254,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const distance = transaction?.routes?.route0?.distance ?? 0; const taxRates = policy?.taxRates ?? null; - const transactionID = transaction?.transactionID ?? ''; // A flag for showing the categories field const shouldShowCategories = isPolicyExpenseChat && (!!iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); @@ -1031,6 +1049,10 @@ export default withOnyx `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, }, + defaultMileageRate: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + selector: DistanceRequestUtils.getDefaultMileageRate, + }, mileageRates: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, selector: DistanceRequestUtils.getMileageRates, @@ -1038,4 +1060,7 @@ export default withOnyx `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, }, + lastSelectedDistanceRates: { + key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, + }, })(MoneyTemporaryForRefactorRequestConfirmationList); diff --git a/src/languages/en.ts b/src/languages/en.ts index 5ee4be293ff5..bf41b8954e63 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -635,6 +635,7 @@ export default { posted: 'Posted', deleteReceipt: 'Delete receipt', routePending: 'Route pending...', + defaultRate: 'Default rate', receiptScanning: 'Scan in progress…', receiptMissingDetails: 'Receipt missing details', missingAmount: 'Missing amount', diff --git a/src/languages/es.ts b/src/languages/es.ts index ae732c6f6418..8423e045d437 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -631,6 +631,7 @@ export default { posted: 'Contabilizado', deleteReceipt: 'Eliminar recibo', routePending: 'Ruta pendiente...', + defaultRate: 'Tasa predeterminada', receiptScanning: 'Escaneo en curso…', receiptMissingDetails: 'Recibo con campos vacíos', missingAmount: 'Falta importe', diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index 28de14fba57d..c673245343b1 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -4,11 +4,13 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider'; import type {RateAndUnit} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report} from '@src/types/onyx'; import type {Unit} from '@src/types/onyx/Policy'; import type Policy from '@src/types/onyx/Policy'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import * as CurrencyUtils from './CurrencyUtils'; import * as PolicyUtils from './PolicyUtils'; +import * as ReportUtils from './ReportUtils'; type MileageRate = { customUnitRateID?: string; @@ -30,6 +32,13 @@ Onyx.connect({ }, }); +let allReports: OnyxCollection; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (value) => (allReports = value), +}); + const METERS_TO_KM = 0.001; // 1 kilometer is 1000 meters const METERS_TO_MILES = 0.000621371; // There are approximately 0.000621371 miles in a meter @@ -114,7 +123,7 @@ function getRateForDisplay( toLocaleDigit: LocaleContextProps['toLocaleDigit'], ): string { if (!hasRoute || !rate || !currency) { - return translate('iou.routePending'); + return translate('iou.defaultRate'); } const singularDistanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer'); @@ -248,6 +257,20 @@ function getDistanceFromMerchant(merchant: string | undefined, unit: Unit): numb return unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS ? distance / METERS_TO_KM : distance / METERS_TO_MILES; } +/** + * Returns custom unit rate ID for the distance transaction + */ +function getCustomUnitRateID(reportID: string) { + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; + + if (ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isPolicyExpenseChat(parentReport)) { + return ''; + } + + return CONST.CUSTOM_UNITS.FAKE_P2P_ID; +} + export default { getDefaultMileageRate, getDistanceMerchant, @@ -257,6 +280,7 @@ export default { getDistanceForDisplay, getRateForP2P, getDistanceFromMerchant, + getCustomUnitRateID, }; export type {MileageRate}; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 8dd13ebec7c4..8e9ea0e73726 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -236,14 +236,6 @@ Onyx.connect({ }, }); -let lastSelectedDistanceRates: OnyxEntry = {}; -Onyx.connect({ - key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, - callback: (value) => { - lastSelectedDistanceRates = value; - }, -}); - let quickAction: OnyxEntry = {}; Onyx.connect({ key: ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE, @@ -282,22 +274,6 @@ function getPolicy(policyID: string | undefined): OnyxTypes.Policy | EmptyObject return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? {}; } -/** - * Returns custom unit rate ID for the transaction - */ -function getCustomUnitRateID(reportID: string) { - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; - const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; - const policy = getPolicy(report?.policyID) ?? null; - - let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID; - - if (ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isPolicyExpenseChat(parentReport)) { - customUnitRateID = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? DistanceRequestUtils.getDefaultMileageRate(policy)?.customUnitRateID ?? ''; - } - return customUnitRateID; -} - /** * Initialize money request info * @param reportID to attach the transaction to @@ -320,7 +296,7 @@ function initMoneyRequest(reportID: string, policy: OnyxEntry, waypoint1: {}, }; if (!isFromGlobalCreate) { - const customUnitRateID = getCustomUnitRateID(reportID); + const customUnitRateID = DistanceRequestUtils.getCustomUnitRateID(reportID); comment.customUnit = {customUnitRateID}; } } @@ -460,9 +436,7 @@ function setMoneyRequestReceipt(transactionID: string, source: string, filename: /** * Set custom unit rateID for the transaction draft */ -function setCustomUnitRateID(transactionID: string, reportID: string) { - const customUnitRateID = getCustomUnitRateID(reportID); - +function setCustomUnitRateID(transactionID: string, customUnitRateID: string) { Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {comment: {customUnit: {customUnitRateID}}}); } diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index df57c4a8c53a..76a193a1d5de 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -4,6 +4,7 @@ import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import useLocalize from '@hooks/useLocalize'; +import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import * as IOUUtils from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as TransactionUtils from '@libs/TransactionUtils'; @@ -107,7 +108,8 @@ function IOURequestStepParticipants({ } IOU.setMoneyRequestParticipants_temporaryForRefactor(transactionID, val); - IOU.setCustomUnitRateID(transactionID, val[0]?.reportID ?? ''); + const rateID = DistanceRequestUtils.getCustomUnitRateID(val[0]?.reportID ?? ''); + IOU.setCustomUnitRateID(transactionID, rateID); numberOfParticipants.current = val.length; From 543113159f2f1330fb767d2cdd085fe4e51b3814 Mon Sep 17 00:00:00 2001 From: kmichel Date: Sun, 14 Apr 2024 00:39:55 -0700 Subject: [PATCH 082/198] fix issue --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 9380ce43c46a..80f0fd596ed6 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -2,7 +2,7 @@ import type {AVPlaybackStatus, VideoFullscreenUpdateEvent} from 'expo-av'; import {ResizeMode, Video, VideoFullscreenUpdate} from 'expo-av'; import type {MutableRefObject} from 'react'; -import React, {useCallback, useEffect, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react'; import type {GestureResponderEvent} from 'react-native'; import {View} from 'react-native'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; @@ -172,6 +172,15 @@ function BaseVideoPlayer({ }); }, [currentVideoPlayerRef, handleFullscreenUpdate, handlePlaybackStatusUpdate]); + useLayoutEffect(() => + () => { + if(shouldUseSharedVideoElement || videoPlayerRef.current !== currentVideoPlayerRef.current) { + return; + } + currentVideoPlayerRef.current = null; + } + , [currentVideoPlayerRef, shouldUseSharedVideoElement]); + useEffect(() => { if (!isUploading || !videoPlayerRef.current) { return; From dfa02a37496039f00ed9c35c881fa26c17e70ea8 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Mon, 15 Apr 2024 10:10:40 +0200 Subject: [PATCH 083/198] fix: minor fix --- .../MoneyTemporaryForRefactorRequestConfirmationList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 44042a976230..fc23af450687 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -232,7 +232,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ if (customUnitRateID || !canUseP2PDistanceRequests) { return; } - if (!customUnitRateID && defaultMileageRate) { + if (!customUnitRateID) { const rateID = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? defaultMileageRate?.customUnitRateID ?? ''; IOU.setCustomUnitRateID(transactionID, rateID); } From 0d23206b7144b63858a5fe52ad97008cafc38b4d Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 15 Apr 2024 17:28:08 +0200 Subject: [PATCH 084/198] integrate OnboardingWork page --- src/ONYXKEYS.ts | 3 + src/ROUTES.ts | 1 + src/SCREENS.ts | 1 + src/languages/en.ts | 2 + src/languages/es.ts | 2 + .../Navigators/OnboardingModalNavigator.tsx | 5 + src/libs/Navigation/linkingConfig/config.ts | 4 + src/libs/Navigation/types.ts | 1 + .../OnboardingWork/BaseOnboardingWork.tsx | 148 ++++++++++++++++++ src/pages/OnboardingWork/index.native.tsx | 17 ++ src/pages/OnboardingWork/index.tsx | 17 ++ src/pages/OnboardingWork/types.ts | 17 ++ src/types/form/WorkForm.ts | 18 +++ src/types/form/index.ts | 1 + 14 files changed, 237 insertions(+) create mode 100644 src/pages/OnboardingWork/BaseOnboardingWork.tsx create mode 100644 src/pages/OnboardingWork/index.native.tsx create mode 100644 src/pages/OnboardingWork/index.tsx create mode 100644 src/pages/OnboardingWork/types.ts create mode 100644 src/types/form/WorkForm.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index cda74da86a54..fe06f9ca71d8 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -384,6 +384,8 @@ const ONYXKEYS = { DISPLAY_NAME_FORM_DRAFT: 'displayNameFormDraft', ONBOARDING_PERSONAL_DETAILS_FORM: 'onboardingPersonalDetailsForm', ONBOARDING_PERSONAL_DETAILS_FORM_DRAFT: 'onboardingPersonalDetailsFormDraft', + ONBOARDING_PERSONAL_WORK: 'onboardingWorkForm', + ONBOARDING_PERSONAL_WORK_DRAFT: 'onboardingWorkFormDraft', ROOM_NAME_FORM: 'roomNameForm', ROOM_NAME_FORM_DRAFT: 'roomNameFormDraft', REPORT_DESCRIPTION_FORM: 'reportDescriptionForm', @@ -474,6 +476,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: FormTypes.ProfileSettingsForm; [ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: FormTypes.DisplayNameForm; [ONYXKEYS.FORMS.ONBOARDING_PERSONAL_DETAILS_FORM]: FormTypes.DisplayNameForm; + [ONYXKEYS.FORMS.ONBOARDING_PERSONAL_WORK]: FormTypes.WorkForm; [ONYXKEYS.FORMS.ROOM_NAME_FORM]: FormTypes.RoomNameForm; [ONYXKEYS.FORMS.REPORT_DESCRIPTION_FORM]: FormTypes.ReportDescriptionForm; [ONYXKEYS.FORMS.LEGAL_NAME_FORM]: FormTypes.LegalNameForm; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 8e4c84f24965..c924e8e86554 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -700,6 +700,7 @@ const ROUTES = { PROCESS_MONEY_REQUEST_HOLD: 'hold-request-educational', ONBOARDING_ROOT: 'onboarding', ONBOARDING_PERSONAL_DETAILS: 'onboarding/personal-details', + ONBOARDING_WORK: 'onboarding/work', ONBOARDING_PURPOSE: 'onboarding/purpose', WELCOME_VIDEO_ROOT: 'onboarding/welcome-video', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 96372d5bbabb..0c709dec955d 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -287,6 +287,7 @@ const SCREENS = { ONBOARDING: { PERSONAL_DETAILS: 'Onboarding_Personal_Details', PURPOSE: 'Onboarding_Purpose', + WORK: 'Onboarding_Work', }, ONBOARD_ENGAGEMENT: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 0793590f69b1..538b2f0b7965 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -320,6 +320,7 @@ export default { subtitleText2: 'button above, or create something using the', subtitleText3: 'button below.', }, + businessName: 'Business name', }, location: { useCurrent: 'Use current location', @@ -1335,6 +1336,7 @@ export default { button: "Let's go", }, whatsYourName: "What's your name?", + whereYouWork: 'Where do you work?', purpose: { title: 'What do you want to do today?', error: 'Please make a selection before continuing', diff --git a/src/languages/es.ts b/src/languages/es.ts index d7e60b53a55a..9a7dc6ffdcb4 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -310,6 +310,7 @@ export default { subtitleText2: 'o crea algo usando el botón', subtitleText3: '.', }, + businessName: 'Nombre del Negocio', }, location: { useCurrent: 'Usar ubicación actual', @@ -1337,6 +1338,7 @@ export default { button: 'Vámonos', }, whatsYourName: '¿Cómo te llamas?', + whereYouWork: '¿Dónde trabajas?', purpose: { title: '¿Qué quieres hacer hoy?', error: 'Por favor, haga una selección antes de continuar.', diff --git a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx index 3791cbc03641..ef3f6340f3e4 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx @@ -8,6 +8,7 @@ import OnboardingModalNavigatorScreenOptions from '@libs/Navigation/AppNavigator import type {OnboardingModalNavigatorParamList} from '@libs/Navigation/types'; import OnboardingPersonalDetails from '@pages/OnboardingPersonalDetails'; import OnboardingPurpose from '@pages/OnboardingPurpose'; +import OnboardingWork from '@pages/OnboardingWork'; import SCREENS from '@src/SCREENS'; import Overlay from './Overlay'; @@ -31,6 +32,10 @@ function OnboardingModalNavigator() { name={SCREENS.ONBOARDING.PERSONAL_DETAILS} component={OnboardingPersonalDetails} /> + diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index e577f6843d66..e060d86e0967 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -105,6 +105,10 @@ const config: LinkingOptions['config'] = { path: ROUTES.ONBOARDING_PERSONAL_DETAILS, exact: true, }, + [SCREENS.ONBOARDING.WORK]: { + path: ROUTES.ONBOARDING_WORK, + exact: true, + }, }, }, [NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 09591cfd78db..2520d81744e8 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -710,6 +710,7 @@ type OnboardingModalNavigatorParamList = { [SCREENS.ONBOARDING_MODAL.ONBOARDING]: undefined; [SCREENS.ONBOARDING.PERSONAL_DETAILS]: undefined; [SCREENS.ONBOARDING.PURPOSE]: undefined; + [SCREENS.ONBOARDING.WORK]: undefined; }; type WelcomeVideoModalNavigatorParamList = { diff --git a/src/pages/OnboardingWork/BaseOnboardingWork.tsx b/src/pages/OnboardingWork/BaseOnboardingWork.tsx new file mode 100644 index 000000000000..d00f17ad955b --- /dev/null +++ b/src/pages/OnboardingWork/BaseOnboardingWork.tsx @@ -0,0 +1,148 @@ +import React, {useCallback} from 'react'; +import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; +import OfflineIndicator from '@components/OfflineIndicator'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import useDisableModalDismissOnEscape from '@hooks/useDisableModalDismissOnEscape'; +import useLocalize from '@hooks/useLocalize'; +import useOnboardingLayout from '@hooks/useOnboardingLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import variables from '@styles/variables'; +import * as Policy from '@userActions/Policy'; +import * as Report from '@userActions/Report'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import INPUT_IDS from '@src/types/form/WorkForm'; +import type {BaseOnboardingWorkOnyxProps, BaseOnboardingWorkProps} from './types'; + +function BaseOnboardingWork({currentUserPersonalDetails, shouldUseNativeStyles, onboardingPurposeSelected}: BaseOnboardingWorkProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {isSmallScreenWidth} = useWindowDimensions(); + const {shouldUseNarrowLayout} = useOnboardingLayout(); + + useDisableModalDismissOnEscape(); + + const completeEngagement = useCallback( + (values: FormOnyxValues<'onboardingWorkForm'>) => { + if (!onboardingPurposeSelected) { + return; + } + + const work = values.work.trim(); + + Policy.createDraftInitialWorkspace(currentUserPersonalDetails.login ?? '', work, undefined, true); + + Report.completeOnboarding(onboardingPurposeSelected, CONST.ONBOARDING_MESSAGES[onboardingPurposeSelected], { + login: currentUserPersonalDetails.login ?? '', + firstName: currentUserPersonalDetails.firstName ?? '', + lastName: currentUserPersonalDetails.lastName ?? '', + }); + + Navigation.dismissModal(); + // Only navigate to concierge chat when central pane is visible + // Otherwise stay on the chats screen. + if (isSmallScreenWidth) { + Navigation.navigate(ROUTES.HOME); + } else { + Report.navigateToConciergeChat(); + } + + // Small delay purely due to design considerations, + // no special technical reasons behind that. + setTimeout(() => { + Navigation.navigate(ROUTES.WELCOME_VIDEO_ROOT); + }, variables.welcomeVideoDelay); + }, + [currentUserPersonalDetails.firstName, currentUserPersonalDetails.lastName, currentUserPersonalDetails.login, isSmallScreenWidth, onboardingPurposeSelected], + ); + + const validate = (values: FormOnyxValues<'onboardingWorkForm'>) => { + const errors: FormInputErrors = {}; + const work = values.work.trim(); + + if (!ValidationUtils.isRequiredFulfilled(work)) { + errors.work = 'workspace.editor.nameIsRequiredError'; + } else if ([...work].length > CONST.TITLE_CHARACTER_LIMIT) { + // Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 + // code units. + ErrorUtils.addErrorMessage(errors, 'work', ['common.error.characterLimitExceedCounter', {length: [...work].length, limit: CONST.TITLE_CHARACTER_LIMIT}]); + } + + return errors; + }; + + const handleGoBack = useCallback(() => { + Navigation.goBack(); + }, []); + + const WorkFooterInstance = ; + + return ( + + + + + + {translate('onboarding.whereYouWork')} + + + + + + + + ); +} + +BaseOnboardingWork.displayName = 'BaseOnboardingWork'; + +export default withCurrentUserPersonalDetails( + withOnyx({ + onboardingPurposeSelected: { + key: ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, + }, + })(BaseOnboardingWork), +); diff --git a/src/pages/OnboardingWork/index.native.tsx b/src/pages/OnboardingWork/index.native.tsx new file mode 100644 index 000000000000..3e69696aa45a --- /dev/null +++ b/src/pages/OnboardingWork/index.native.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import BaseOnboardingWork from './BaseOnboardingWork'; +import type {OnboardingWorkProps} from './types'; + +function OnboardingWork({...rest}: Omit) { + return ( + + ); +} + +OnboardingWork.displayName = 'OnboardingWork'; + +export default OnboardingWork; diff --git a/src/pages/OnboardingWork/index.tsx b/src/pages/OnboardingWork/index.tsx new file mode 100644 index 000000000000..ba1b8aaeb106 --- /dev/null +++ b/src/pages/OnboardingWork/index.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import BaseOnboardingWork from './BaseOnboardingWork'; +import type {OnboardingWorkProps} from './types'; + +function OnboardingWork({...rest}: Omit) { + return ( + + ); +} + +OnboardingWork.displayName = 'OnboardingPurpose'; + +export default OnboardingWork; diff --git a/src/pages/OnboardingWork/types.ts b/src/pages/OnboardingWork/types.ts new file mode 100644 index 000000000000..930a8081e9c4 --- /dev/null +++ b/src/pages/OnboardingWork/types.ts @@ -0,0 +1,17 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; + +type OnboardingWorkProps = Record; + +type BaseOnboardingWorkOnyxProps = { + /** Saved onboarding purpose selected by the user */ + onboardingPurposeSelected: OnyxEntry; +}; + +type BaseOnboardingWorkProps = WithCurrentUserPersonalDetailsProps & + BaseOnboardingWorkOnyxProps & { + /* Whether to use native styles tailored for native devices */ + shouldUseNativeStyles: boolean; + }; + +export type {OnboardingWorkProps, BaseOnboardingWorkOnyxProps, BaseOnboardingWorkProps}; diff --git a/src/types/form/WorkForm.ts b/src/types/form/WorkForm.ts new file mode 100644 index 000000000000..6e5fa8a89311 --- /dev/null +++ b/src/types/form/WorkForm.ts @@ -0,0 +1,18 @@ +import type {ValueOf} from 'type-fest'; +import type Form from './Form'; + +const INPUT_IDS = { + WORK: 'work', +} as const; + +type InputID = ValueOf; + +type WorkForm = Form< + InputID, + { + [INPUT_IDS.WORK]: string; + } +>; + +export type {WorkForm}; +export default INPUT_IDS; diff --git a/src/types/form/index.ts b/src/types/form/index.ts index ce3fcd428999..ddddb99ab89b 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -48,4 +48,5 @@ export type {WorkspaceTaxCustomName} from './WorkspaceTaxCustomName'; export type {PolicyCreateDistanceRateForm} from './PolicyCreateDistanceRateForm'; export type {PolicyDistanceRateEditForm} from './PolicyDistanceRateEditForm'; export type {NewChatNameForm} from './NewChatNameForm'; +export type {WorkForm} from './WorkForm'; export type {default as Form} from './Form'; From 19d7d50711aed59471d79c7dd6794ca887ff6fa5 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 15 Apr 2024 17:29:12 +0200 Subject: [PATCH 085/198] move types --- .../BaseOnboardingPersonalDetails.tsx | 2 +- .../index.native.tsx | 2 +- src/pages/OnboardingPersonalDetails/index.tsx | 2 +- src/pages/OnboardingPersonalDetails/types.ts | 17 ++++++++++++++ src/pages/OnboardingPurpose/types.ts | 23 +------------------ 5 files changed, 21 insertions(+), 25 deletions(-) create mode 100644 src/pages/OnboardingPersonalDetails/types.ts diff --git a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx index dac3b4e3d4c1..b1e14b077931 100644 --- a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx +++ b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx @@ -18,7 +18,6 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ValidationUtils from '@libs/ValidationUtils'; -import type {BaseOnboardingPersonalDetailsOnyxProps, BaseOnboardingPersonalDetailsProps} from '@pages/OnboardingPurpose/types'; import variables from '@styles/variables'; import * as PersonalDetails from '@userActions/PersonalDetails'; import * as Report from '@userActions/Report'; @@ -26,6 +25,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/DisplayNameForm'; +import type {BaseOnboardingPersonalDetailsOnyxProps, BaseOnboardingPersonalDetailsProps} from './types'; function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNativeStyles, onboardingPurposeSelected}: BaseOnboardingPersonalDetailsProps) { const styles = useThemeStyles(); diff --git a/src/pages/OnboardingPersonalDetails/index.native.tsx b/src/pages/OnboardingPersonalDetails/index.native.tsx index 778d68e39458..b5a4d42f0de1 100644 --- a/src/pages/OnboardingPersonalDetails/index.native.tsx +++ b/src/pages/OnboardingPersonalDetails/index.native.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import type {OnboardingPersonalDetailsProps} from '@pages/OnboardingPurpose/types'; import BaseOnboardingPersonalDetails from './BaseOnboardingPersonalDetails'; +import type {OnboardingPersonalDetailsProps} from './types'; function OnboardingPersonalDetails({...rest}: Omit) { return ( diff --git a/src/pages/OnboardingPersonalDetails/index.tsx b/src/pages/OnboardingPersonalDetails/index.tsx index 8409e7e8f0a2..d6a408ee7381 100644 --- a/src/pages/OnboardingPersonalDetails/index.tsx +++ b/src/pages/OnboardingPersonalDetails/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import type {OnboardingPersonalDetailsProps} from '@pages/OnboardingPurpose/types'; import BaseOnboardingPersonalDetails from './BaseOnboardingPersonalDetails'; +import type {OnboardingPersonalDetailsProps} from './types'; function OnboardingPersonalDetails({...rest}: Omit) { return ( diff --git a/src/pages/OnboardingPersonalDetails/types.ts b/src/pages/OnboardingPersonalDetails/types.ts new file mode 100644 index 000000000000..6d286373a1ac --- /dev/null +++ b/src/pages/OnboardingPersonalDetails/types.ts @@ -0,0 +1,17 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; + +type OnboardingPersonalDetailsProps = Record; + +type BaseOnboardingPersonalDetailsOnyxProps = { + /** Saved onboarding purpose selected by the user */ + onboardingPurposeSelected: OnyxEntry; +}; + +type BaseOnboardingPersonalDetailsProps = WithCurrentUserPersonalDetailsProps & + BaseOnboardingPersonalDetailsOnyxProps & { + /* Whether to use native styles tailored for native devices */ + shouldUseNativeStyles: boolean; + }; + +export type {OnboardingPersonalDetailsProps, BaseOnboardingPersonalDetailsOnyxProps, BaseOnboardingPersonalDetailsProps}; diff --git a/src/pages/OnboardingPurpose/types.ts b/src/pages/OnboardingPurpose/types.ts index 15d6beec1f6f..586463a26bb0 100644 --- a/src/pages/OnboardingPurpose/types.ts +++ b/src/pages/OnboardingPurpose/types.ts @@ -1,5 +1,4 @@ import type {OnyxEntry} from 'react-native-onyx'; -import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; type OnboardingPurposeProps = Record; @@ -17,24 +16,4 @@ type BaseOnboardingPurposeProps = OnboardingPurposeProps & shouldEnableMaxHeight: boolean; }; -type OnboardingPersonalDetailsProps = Record; - -type BaseOnboardingPersonalDetailsOnyxProps = { - /** Saved onboarding purpose selected by the user */ - onboardingPurposeSelected: OnyxEntry; -}; - -type BaseOnboardingPersonalDetailsProps = WithCurrentUserPersonalDetailsProps & - BaseOnboardingPersonalDetailsOnyxProps & { - /* Whether to use native styles tailored for native devices */ - shouldUseNativeStyles: boolean; - }; - -export type { - BaseOnboardingPurposeOnyxProps, - BaseOnboardingPurposeProps, - OnboardingPurposeProps, - OnboardingPersonalDetailsProps, - BaseOnboardingPersonalDetailsOnyxProps, - BaseOnboardingPersonalDetailsProps, -}; +export type {BaseOnboardingPurposeOnyxProps, BaseOnboardingPurposeProps, OnboardingPurposeProps}; From ed44ffdb16a55cc84abcaae96101951ed56f39c3 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 15 Apr 2024 17:43:08 +0200 Subject: [PATCH 086/198] integrate onboarding work --- .../BaseOnboardingPersonalDetails.tsx | 8 ++++++++ src/pages/OnboardingWork/BaseOnboardingWork.tsx | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx index b1e14b077931..a2e3765baa9c 100644 --- a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx +++ b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx @@ -46,6 +46,14 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat return; } + const openWorkPagePurposes = [CONST.ONBOARDING_CHOICES.EMPLOYER, CONST.ONBOARDING_CHOICES.MANAGE_TEAM]; + + if (openWorkPagePurposes.includes(onboardingPurposeSelected)) { + Navigation.navigate(ROUTES.ONBOARDING_WORK); + + return; + } + Report.completeOnboarding(onboardingPurposeSelected, CONST.ONBOARDING_MESSAGES[onboardingPurposeSelected], { login: currentUserPersonalDetails.login ?? '', firstName, diff --git a/src/pages/OnboardingWork/BaseOnboardingWork.tsx b/src/pages/OnboardingWork/BaseOnboardingWork.tsx index d00f17ad955b..01ec23c8315c 100644 --- a/src/pages/OnboardingWork/BaseOnboardingWork.tsx +++ b/src/pages/OnboardingWork/BaseOnboardingWork.tsx @@ -43,7 +43,7 @@ function BaseOnboardingWork({currentUserPersonalDetails, shouldUseNativeStyles, const work = values.work.trim(); - Policy.createDraftInitialWorkspace(currentUserPersonalDetails.login ?? '', work, undefined, true); + Policy.createWorkspace(currentUserPersonalDetails.login ?? '', true, work); Report.completeOnboarding(onboardingPurposeSelected, CONST.ONBOARDING_MESSAGES[onboardingPurposeSelected], { login: currentUserPersonalDetails.login ?? '', From 41851acf298e8146bb503bbbfcdd911d2e75ef70 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 15 Apr 2024 18:34:02 +0200 Subject: [PATCH 087/198] clarify work cases --- .../OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx index a2e3765baa9c..5657f8553113 100644 --- a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx +++ b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx @@ -46,7 +46,7 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat return; } - const openWorkPagePurposes = [CONST.ONBOARDING_CHOICES.EMPLOYER, CONST.ONBOARDING_CHOICES.MANAGE_TEAM]; + const openWorkPagePurposes = [CONST.ONBOARDING_CHOICES.TRACK, CONST.ONBOARDING_CHOICES.MANAGE_TEAM]; if (openWorkPagePurposes.includes(onboardingPurposeSelected)) { Navigation.navigate(ROUTES.ONBOARDING_WORK); From 410fdb17d092650dcc795b4edad2b03b40401b0e Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Mon, 15 Apr 2024 18:59:41 +0200 Subject: [PATCH 088/198] fix: apply requested changes --- src/ROUTES.ts | 6 ++--- src/SCREENS.ts | 2 +- ...raryForRefactorRequestConfirmationList.tsx | 21 +++++++++--------- .../ReportActionItem/MoneyRequestView.tsx | 13 +++++++++-- src/libs/DistanceRequestUtils.ts | 14 +++++++----- .../ModalStackNavigators/index.tsx | 2 +- src/libs/Navigation/linkingConfig/config.ts | 2 +- src/libs/Navigation/types.ts | 2 +- ...ate.tsx => IOURequestStepDistanceRate.tsx} | 22 +++++++++---------- .../step/withFullTransactionOrNotFound.tsx | 2 +- .../step/withWritableReportOrNotFound.tsx | 2 +- 11 files changed, 51 insertions(+), 37 deletions(-) rename src/pages/iou/request/step/{IOURequestStepRate.tsx => IOURequestStepDistanceRate.tsx} (82%) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index a0909b2c582f..e92b29464005 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -370,10 +370,10 @@ const ROUTES = { getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => getUrlWithBackToParam(`${action}/${iouType}/distance/${transactionID}/${reportID}`, backTo), }, - MONEY_REQUEST_STEP_RATE: { - route: ':action/:iouType/rate/:transactionID/:reportID', + MONEY_REQUEST_STEP_DISTANCE_RATE: { + route: ':action/:iouType/distanceRate/:transactionID/:reportID', getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`${action}/${iouType}/rate/${transactionID}/${reportID}`, backTo), + getUrlWithBackToParam(`${action}/${iouType}/distanceRate/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_MERCHANT: { route: ':action/:iouType/merchant/:transactionID/:reportID', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 303e6e322799..ac81bf1c5f92 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -148,7 +148,7 @@ const SCREENS = { STEP_DATE: 'Money_Request_Step_Date', STEP_DESCRIPTION: 'Money_Request_Step_Description', STEP_DISTANCE: 'Money_Request_Step_Distance', - STEP_RATE: 'Money_Request_Step_Rate', + STEP_DISTANCE_RATE: 'Money_Request_Step_Rate', STEP_MERCHANT: 'Money_Request_Step_Merchant', STEP_PARTICIPANTS: 'Money_Request_Step_Participants', STEP_SCAN: 'Money_Request_Step_Scan', diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index fc23af450687..66ba8b78a735 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -9,6 +9,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useTheme from '@hooks/useTheme'; @@ -219,6 +220,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const {translate, toLocaleDigit} = useLocalize(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const {canUseP2PDistanceRequests, canUseViolations} = usePermissions(iouType); + const {isOffline} = useNetwork(); const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST; const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT; @@ -242,10 +244,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ ? DistanceRequestUtils.getRateForP2P(policy?.outputCurrency ?? CONST.CURRENCY.USD) : mileageRates?.[customUnitRateID] ?? DistanceRequestUtils.getDefaultMileageRate(policy); - const {unit, rate} = mileageRate ?? { - unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, - rate: CONST.CUSTOM_UNITS.MILEAGE_IRS_RATE * 100, - }; + const {unit, rate} = mileageRate ?? {}; const prevRate = usePrevious(rate); const shouldCalculateDistanceAmount = isDistanceRequest && (iouAmount === 0 || prevRate !== rate); @@ -283,7 +282,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const formattedAmount = isDistanceRequestWithPendingRoute ? '' : CurrencyUtils.convertToDisplayString( - shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate ?? 0) : iouAmount, + shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate ?? 0) : iouAmount, isDistanceRequest ? currency : iouCurrencyCode, ); const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode); @@ -352,7 +351,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ return; } - const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate ?? 0); + const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate ?? 0); IOU.setMoneyRequestAmount_temporaryForRefactor(transactionID, amount, currency ?? ''); }, [shouldCalculateDistanceAmount, distance, rate, unit, transaction, currency, transactionID]); @@ -763,16 +762,18 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ item: ( { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_RATE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()), + ); }} disabled={didConfirm} - interactive={!isReadOnly && isPolicyExpenseChat} + interactive={Boolean(rate) && !isReadOnly && isPolicyExpenseChat} /> ), shouldShow: isDistanceRequest && canUseP2PDistanceRequests, diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 79f59d4e4868..74d04c8807e1 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -12,6 +12,7 @@ import Switch from '@components/Switch'; import Text from '@components/Text'; import ViolationMessages from '@components/ViolationMessages'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import usePermissions from '@hooks/usePermissions'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; @@ -99,6 +100,7 @@ function MoneyRequestView({ const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const {isOffline} = useNetwork(); const {isSmallScreenWidth} = useWindowDimensions(); const {translate, toLocaleDigit} = useLocalize(); const {canUseViolations, canUseP2PDistanceRequests} = usePermissions(); @@ -185,7 +187,7 @@ function MoneyRequestView({ const {unit, rate} = mileageRate; const distance = DistanceRequestUtils.getDistanceFromMerchant(transactionMerchant, unit); - const rateToDisplay = DistanceRequestUtils.getRateForDisplay(hasRoute, unit, rate, currency, translate, toLocaleDigit); + const rateToDisplay = DistanceRequestUtils.getRateForDisplay(hasRoute, unit, rate, currency, translate, toLocaleDigit, isOffline); const distanceToDisplay = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate); const saveBillable = useCallback( @@ -471,7 +473,14 @@ function MoneyRequestView({ ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(CONST.IOU.ACTION.EDIT, CONST.IOU.TYPE.REQUEST, orderWeight, transaction?.transactionID ?? '', report.reportID), ) } - brickRoadIndicator={getErrorForField('tag', {tagListIndex: index, tagListName: name}) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + brickRoadIndicator={ + getErrorForField('tag', { + tagListIndex: index, + tagListName: name, + }) + ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR + : undefined + } error={getErrorForField('tag', {tagListIndex: index, tagListName: name})} /> diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index c673245343b1..66ab9238be7c 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -116,15 +116,19 @@ function getRoundedDistanceInUnits(distanceInMeters: number, unit: Unit): string */ function getRateForDisplay( hasRoute: boolean, - unit: Unit, + unit: Unit | undefined, rate: number | undefined, currency: string | undefined, translate: LocaleContextProps['translate'], toLocaleDigit: LocaleContextProps['toLocaleDigit'], + isOffline?: boolean, ): string { - if (!hasRoute || !rate || !currency) { + if (isOffline) { return translate('iou.defaultRate'); } + if (!hasRoute || !rate || !currency || !unit) { + return translate('iou.routePending'); + } const singularDistanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer'); const ratePerUnit = PolicyUtils.getUnitRateValue(toLocaleDigit, {rate}); @@ -142,8 +146,8 @@ function getRateForDisplay( * @param translate Translate function * @returns A string that describes the distance traveled */ -function getDistanceForDisplay(hasRoute: boolean, distanceInMeters: number, unit: Unit, rate: number | undefined, translate: LocaleContextProps['translate']): string { - if (!hasRoute || !rate) { +function getDistanceForDisplay(hasRoute: boolean, distanceInMeters: number, unit: Unit | undefined, rate: number | undefined, translate: LocaleContextProps['translate']): string { + if (!hasRoute || !rate || !unit) { return translate('iou.routePending'); } @@ -168,7 +172,7 @@ function getDistanceForDisplay(hasRoute: boolean, distanceInMeters: number, unit function getDistanceMerchant( hasRoute: boolean, distanceInMeters: number, - unit: Unit, + unit: Unit | undefined, rate: number | undefined, currency: string, translate: LocaleContextProps['translate'], diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 062a146e2624..6568cab36a0d 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -81,7 +81,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../../pages/iou/request/step/IOURequestStepDate').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_DESCRIPTION]: () => require('../../../../pages/iou/request/step/IOURequestStepDescription').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_DISTANCE]: () => require('../../../../pages/iou/request/step/IOURequestStepDistance').default as React.ComponentType, - [SCREENS.MONEY_REQUEST.STEP_RATE]: () => require('../../../../pages/iou/request/step/IOURequestStepRate').default as React.ComponentType, + [SCREENS.MONEY_REQUEST.STEP_DISTANCE_RATE]: () => require('@pages/iou/request/step/IOURequestStepDistanceRate').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_MERCHANT]: () => require('../../../../pages/iou/request/step/IOURequestStepMerchant').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS]: () => require('../../../../pages/iou/request/step/IOURequestStepParticipants').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_SCAN]: () => require('../../../../pages/iou/request/step/IOURequestStepScan').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 67eab41ada5a..554dd963bcdc 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -568,7 +568,7 @@ const config: LinkingOptions['config'] = { [SCREENS.MONEY_REQUEST.STEP_DATE]: ROUTES.MONEY_REQUEST_STEP_DATE.route, [SCREENS.MONEY_REQUEST.STEP_DESCRIPTION]: ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.route, [SCREENS.MONEY_REQUEST.STEP_DISTANCE]: ROUTES.MONEY_REQUEST_STEP_DISTANCE.route, - [SCREENS.MONEY_REQUEST.STEP_RATE]: ROUTES.MONEY_REQUEST_STEP_RATE.route, + [SCREENS.MONEY_REQUEST.STEP_DISTANCE_RATE]: ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.route, [SCREENS.MONEY_REQUEST.HOLD]: ROUTES.MONEY_REQUEST_HOLD_REASON.route, [SCREENS.MONEY_REQUEST.STEP_MERCHANT]: ROUTES.MONEY_REQUEST_STEP_MERCHANT.route, [SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS]: ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index a48b2ba10e35..e89bb61fe566 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -478,7 +478,7 @@ type MoneyRequestNavigatorParamList = { transactionID: string; reportID: string; }; - [SCREENS.MONEY_REQUEST.STEP_RATE]: { + [SCREENS.MONEY_REQUEST.STEP_DISTANCE_RATE]: { iouType: ValueOf; transactionID: string; backTo: Routes; diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx similarity index 82% rename from src/pages/iou/request/step/IOURequestStepRate.tsx rename to src/pages/iou/request/step/IOURequestStepDistanceRate.tsx index d7ae3ee0a19e..dc1f6fff2968 100644 --- a/src/pages/iou/request/step/IOURequestStepRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx @@ -21,7 +21,7 @@ import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; -type IOURequestStepRateOnyxProps = { +type IOURequestStepDistanceRateOnyxProps = { /** Policy details */ policy: OnyxEntry; @@ -29,20 +29,20 @@ type IOURequestStepRateOnyxProps = { rates: Record; }; -type IOURequestStepRateProps = IOURequestStepRateOnyxProps & - WithWritableReportOrNotFoundProps & { +type IOURequestStepDistanceRateProps = IOURequestStepDistanceRateOnyxProps & + WithWritableReportOrNotFoundProps & { /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ transaction: OnyxEntry; }; -function IOURequestStepRate({ +function IOURequestStepDistanceRate({ policy, route: { params: {backTo, transactionID}, }, transaction, rates, -}: IOURequestStepRateProps) { +}: IOURequestStepDistanceRateProps) { const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); @@ -92,9 +92,9 @@ function IOURequestStepRate({ ); } -IOURequestStepRate.displayName = 'IOURequestStepRate'; +IOURequestStepDistanceRate.displayName = 'IOURequestStepDistanceRate'; -const IOURequestStepRateWithOnyx = withOnyx({ +const IOURequestStepDistanceRateWithOnyx = withOnyx({ policy: { key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, }, @@ -102,11 +102,11 @@ const IOURequestStepRateWithOnyx = withOnyx `${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? '0'}`, selector: DistanceRequestUtils.getMileageRates, }, -})(IOURequestStepRate); +})(IOURequestStepDistanceRate); // eslint-disable-next-line rulesdir/no-negated-variables -const IOURequestStepRateWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepRateWithOnyx); +const IOURequestStepDistanceRateWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepDistanceRateWithOnyx); // eslint-disable-next-line rulesdir/no-negated-variables -const IOURequestStepRateWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepRateWithWritableReportOrNotFound); +const IOURequestStepDistanceRateWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepDistanceRateWithWritableReportOrNotFound); -export default IOURequestStepRateWithFullTransactionOrNotFound; +export default IOURequestStepDistanceRateWithFullTransactionOrNotFound; diff --git a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx index 76050afaa734..70d5642f3873 100644 --- a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx +++ b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx @@ -27,7 +27,7 @@ type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS | typeof SCREENS.MONEY_REQUEST.STEP_MERCHANT | typeof SCREENS.MONEY_REQUEST.STEP_TAG - | typeof SCREENS.MONEY_REQUEST.STEP_RATE + | typeof SCREENS.MONEY_REQUEST.STEP_DISTANCE_RATE | typeof SCREENS.MONEY_REQUEST.STEP_CONFIRMATION | typeof SCREENS.MONEY_REQUEST.STEP_CATEGORY | typeof SCREENS.MONEY_REQUEST.STEP_TAX_RATE diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx index 723200fa0c12..ece519d87a1b 100644 --- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx +++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx @@ -21,7 +21,7 @@ type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.STEP_WAYPOINT | typeof SCREENS.MONEY_REQUEST.STEP_DESCRIPTION | typeof SCREENS.MONEY_REQUEST.STEP_CATEGORY - | typeof SCREENS.MONEY_REQUEST.STEP_RATE + | typeof SCREENS.MONEY_REQUEST.STEP_DISTANCE_RATE | typeof SCREENS.MONEY_REQUEST.STEP_CONFIRMATION | typeof SCREENS.MONEY_REQUEST.STEP_TAX_RATE | typeof SCREENS.MONEY_REQUEST.STEP_AMOUNT From 23268d16205517037012dd334129bc9426fd7893 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 15 Apr 2024 19:10:58 +0200 Subject: [PATCH 089/198] fix onboarding screen options --- src/libs/Navigation/AppNavigator/AuthScreens.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index fde0202d3d2f..0edceee43647 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -369,7 +369,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie /> Date: Mon, 15 Apr 2024 19:27:54 +0200 Subject: [PATCH 090/198] fix: restore setting customUnitRateID when data available --- src/libs/DistanceRequestUtils.ts | 17 ++++++++++++++--- src/libs/PolicyUtils.ts | 11 +++++++++++ src/libs/actions/IOU.ts | 23 +++-------------------- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index 66ab9238be7c..f827606ca927 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -4,7 +4,7 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider'; import type {RateAndUnit} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Report} from '@src/types/onyx'; +import type {LastSelectedDistanceRates, Report} from '@src/types/onyx'; import type {Unit} from '@src/types/onyx/Policy'; import type Policy from '@src/types/onyx/Policy'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; @@ -32,6 +32,14 @@ Onyx.connect({ }, }); +let lastSelectedDistanceRates: OnyxEntry = {}; +Onyx.connect({ + key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, + callback: (value) => { + lastSelectedDistanceRates = value; + }, +}); + let allReports: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, @@ -267,12 +275,15 @@ function getDistanceFromMerchant(merchant: string | undefined, unit: Unit): numb function getCustomUnitRateID(reportID: string) { const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; + const policy = PolicyUtils.getPolicy(report?.policyID ?? parentReport?.policyID ?? ''); + + let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID; if (ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isPolicyExpenseChat(parentReport)) { - return ''; + customUnitRateID = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? getDefaultMileageRate(policy)?.customUnitRateID ?? ''; } - return CONST.CUSTOM_UNITS.FAKE_P2P_ID; + return customUnitRateID; } export default { diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 8002ad5145df..7df88296651a 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -331,6 +331,16 @@ function getPolicyIDFromNavigationState() { return getPolicyIDFromState(navigationRef.getRootState() as State); } +/** + * Returns the policy of the report + */ +function getPolicy(policyID: string | undefined): Policy | EmptyObject { + if (!allPolicies || !policyID) { + return {}; + } + return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? {}; +} + export { getActivePolicies, hasAccountingConnections, @@ -371,6 +381,7 @@ export { getTaxByID, hasPolicyCategoriesError, getPolicyIDFromNavigationState, + getPolicy, }; export type {MemberEmailsToAccountIDs}; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 8e9ea0e73726..fde110776096 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -244,13 +244,6 @@ Onyx.connect({ }, }); -let allPolicies: OnyxCollection; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY, - waitForCollectionCallback: true, - callback: (value) => (allPolicies = value), -}); - const reportActionsByReport: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, @@ -264,16 +257,6 @@ Onyx.connect({ }, }); -/** - * Returns the policy of the report - */ -function getPolicy(policyID: string | undefined): OnyxTypes.Policy | EmptyObject { - if (!allPolicies || !policyID) { - return {}; - } - return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? {}; -} - /** * Initialize money request info * @param reportID to attach the transaction to @@ -4916,7 +4899,7 @@ function hasIOUToApproveOrPay(chatReport: OnyxEntry | EmptyObj return Object.values(chatReportActions).some((action) => { const iouReport = ReportUtils.getReport(action.childReportID ?? ''); - const policy = getPolicy(iouReport?.policyID); + const policy = PolicyUtils.getPolicy(iouReport?.policyID); const shouldShowSettlementButton = canIOUBePaid(iouReport, chatReport, policy) || canApproveIOU(iouReport, chatReport, policy); return action.childReportID?.toString() !== excludedIOUReportID && action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && shouldShowSettlementButton; }); @@ -5058,7 +5041,7 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject, full function submitReport(expenseReport: OnyxTypes.Report) { const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`] ?? null; const parentReport = ReportUtils.getReport(expenseReport.parentReportID); - const policy = getPolicy(expenseReport.policyID); + const policy = PolicyUtils.getPolicy(expenseReport.policyID); const isCurrentUserManager = currentUserPersonalDetails.accountID === expenseReport.managerID; const isSubmitAndClosePolicy = PolicyUtils.isSubmitAndClose(policy); const adminAccountID = policy.role === CONST.POLICY.ROLE.ADMIN ? currentUserPersonalDetails.accountID : undefined; @@ -5182,7 +5165,7 @@ function submitReport(expenseReport: OnyxTypes.Report) { function cancelPayment(expenseReport: OnyxTypes.Report, chatReport: OnyxTypes.Report) { const optimisticReportAction = ReportUtils.buildOptimisticCancelPaymentReportAction(expenseReport.reportID, -(expenseReport.total ?? 0), expenseReport.currency ?? ''); - const policy = getPolicy(chatReport.policyID); + const policy = PolicyUtils.getPolicy(chatReport.policyID); const isFree = policy && policy.type === CONST.POLICY.TYPE.FREE; const approvalMode = policy.approvalMode ?? CONST.POLICY.APPROVAL_MODE.BASIC; let stateNum: ValueOf = CONST.REPORT.STATE_NUM.SUBMITTED; From b5c4fc5e51b6147705ec03e2dd7a639a724a3834 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 16 Apr 2024 13:33:36 +0800 Subject: [PATCH 091/198] add pending fields to transaction thread --- src/libs/actions/IOU.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 85f4b74f3436..420d5e6a8e29 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -554,7 +554,10 @@ function buildOnyxDataForMoneyRequest( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, - value: transactionThreadReport, + value: { + ...transactionThreadReport, + pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}, + }, }, { onyxMethod: Onyx.METHOD.MERGE, @@ -737,6 +740,7 @@ function buildOnyxDataForMoneyRequest( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, value: { + pendingFields: null, errorFields: { createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'), }, @@ -895,7 +899,10 @@ function buildOnyxDataForTrackExpense( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, - value: transactionThreadReport, + value: { + ...transactionThreadReport, + pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}, + }, }, { onyxMethod: Onyx.METHOD.MERGE, @@ -1061,6 +1068,7 @@ function buildOnyxDataForTrackExpense( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, value: { + pendingFields: null, errorFields: { createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'), }, From 6afbf9a3072ef1012c0130dc79ce22b309f1b283 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 16 Apr 2024 15:40:28 +0700 Subject: [PATCH 092/198] fix Unsynchronized transition of left and right page when deleting workspace --- src/pages/workspace/WorkspaceInitialPage.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index a6a131f5372c..1f00e2fe58cc 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -247,6 +247,12 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r // We check isPendingDelete for both policy and prevPolicy to prevent the NotFound view from showing right after we delete the workspace (PolicyUtils.isPendingDeletePolicy(policy) && PolicyUtils.isPendingDeletePolicy(prevPolicy)); + useEffect(() => { + if (!isEmptyObject(prevPolicy) && !PolicyUtils.isPendingDeletePolicy(prevPolicy) && PolicyUtils.isPendingDeletePolicy(policy)) { + Navigation.navigateWithSwitchPolicyID({policyID: undefined}); + } + }, [policy, prevPolicy]); + // We are checking if the user can access the route. // If user can't access the route, we are dismissing any modals that are open when the NotFound view is shown const canAccessRoute = activeRoute && menuItems.some((item) => item.routeName === activeRoute); @@ -298,6 +304,8 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r onClose={() => dismissError(policyID)} errors={policy?.errors} errorRowStyles={[styles.ph5, styles.pv2]} + shouldDisableStrikeThrough={false} + shouldHideOnDelete={false} > {/* From d1ce7356e042390ee716fe6bc0ccf76b7a97019e Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 16 Apr 2024 16:28:25 +0700 Subject: [PATCH 093/198] fix navigate issue --- src/pages/workspace/WorkspaceInitialPage.tsx | 2 +- src/pages/workspace/WorkspaceProfilePage.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 1f00e2fe58cc..9acdc3f673da 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -249,7 +249,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r useEffect(() => { if (!isEmptyObject(prevPolicy) && !PolicyUtils.isPendingDeletePolicy(prevPolicy) && PolicyUtils.isPendingDeletePolicy(policy)) { - Navigation.navigateWithSwitchPolicyID({policyID: undefined}); + PolicyUtils.goBackFromInvalidPolicy(); } }, [policy, prevPolicy]); diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index 662335d0b358..b73c6c0d7062 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -97,7 +97,6 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi } Policy.deleteWorkspace(policy?.id, policyName); - PolicyUtils.goBackFromInvalidPolicy(); setIsDeleteModalOpen(false); // If the workspace being deleted is the active workspace, switch to the "All Workspaces" view From 804801138dd61f58f9d34c10f94710d80559cf8c Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 16 Apr 2024 16:38:54 +0700 Subject: [PATCH 094/198] fix lint --- src/pages/workspace/WorkspaceInitialPage.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 9acdc3f673da..7a627ae5015d 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -248,9 +248,10 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r (PolicyUtils.isPendingDeletePolicy(policy) && PolicyUtils.isPendingDeletePolicy(prevPolicy)); useEffect(() => { - if (!isEmptyObject(prevPolicy) && !PolicyUtils.isPendingDeletePolicy(prevPolicy) && PolicyUtils.isPendingDeletePolicy(policy)) { - PolicyUtils.goBackFromInvalidPolicy(); + if (isEmptyObject(prevPolicy) || PolicyUtils.isPendingDeletePolicy(prevPolicy) || !PolicyUtils.isPendingDeletePolicy(policy)) { + return; } + PolicyUtils.goBackFromInvalidPolicy(); }, [policy, prevPolicy]); // We are checking if the user can access the route. From 949bf4ebac3bd6d4ffde596fb43c3a107e896944 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 16 Apr 2024 12:26:43 +0200 Subject: [PATCH 095/198] fix: fallback to first rate when all custom named --- src/libs/DistanceRequestUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index f827606ca927..d35d7b0d711c 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -70,7 +70,7 @@ function getDefaultMileageRate(policy: OnyxEntry | EmptyObject): Mileage return null; } - const distanceRate = Object.values(distanceUnit.rates).find((rate) => rate.name === CONST.CUSTOM_UNITS.DEFAULT_RATE); + const distanceRate = Object.values(distanceUnit.rates).find((rate) => rate.name === CONST.CUSTOM_UNITS.DEFAULT_RATE) ?? Object.values(distanceUnit.rates)[0]; if (!distanceRate) { return null; } From 52fde4a3350d75b66084e80e69ce560d960a306d Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 16 Apr 2024 13:07:50 +0200 Subject: [PATCH 096/198] add video to tasks --- src/CONST.ts | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index a9d8ba6baece..039598dd2b8a 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3620,6 +3620,13 @@ const CONST = { '2. Click Workspaces > New workspace.\n' + '\n' + 'Your new workspace is ready! It’ll keep all of your spend (and chats) in one place.', + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, }, { title: 'Track an expense', @@ -3633,6 +3640,13 @@ const CONST = { '4. Click Track.\n' + '\n' + 'And you’re done! Yep, it’s that easy.', + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, }, ], }, @@ -3658,6 +3672,13 @@ const CONST = { '4. Add your reimburser to the request.\n' + '\n' + 'Then, send your request and wait for that sweet “Cha-ching!” when it’s complete.', + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, }, { title: 'Enable your wallet', @@ -3670,6 +3691,13 @@ const CONST = { '3. Connect your bank account.\n' + '\n' + 'Once that’s done, you can request money from anyone and get paid back right into your personal bank account.', + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, }, ], }, @@ -3693,6 +3721,13 @@ const CONST = { '2. Click Workspaces > New workspace.\n' + '\n' + 'Your new workspace is ready! It’ll keep all of your spend (and chats) in one place.', + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, }, { title: 'Meet your setup specialist', @@ -3701,6 +3736,13 @@ const CONST = { 'Meet your setup specialist, {guideName}, who can answer any questions as you get started with Expensify. Yes, a real human!\n' + '\n' + 'Chat with {guideName} in your [admins room]({adminsRoomID}) or [schedule a call]({guideCalendarLink}) today', + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, }, { title: 'Set up categories', @@ -3715,6 +3757,13 @@ const CONST = { '5. Click Add categories to make your own.\n' + '\n' + 'For more controls like requiring a category for every expense, click Settings.', + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, }, { title: 'Add expense approvals', @@ -3729,6 +3778,13 @@ const CONST = { '5. In Workflows, enable Add approvals.\n' + '\n' + 'You’ll be set as the expense approver. You can change this to any admin once you invite your team.', + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, }, { title: 'Invite your team', @@ -3743,6 +3799,13 @@ const CONST = { '5. Add an invite message if you want.\n' + '\n' + 'That’s it! Happy expensing :)', + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, }, ], }, @@ -3768,6 +3831,13 @@ const CONST = { '4. Click Track.\n' + '\n' + 'And you’re done! Yep, it’s that easy.', + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, }, ], }, @@ -3794,6 +3864,13 @@ const CONST = { 'If any of your friends aren’t using Expensify already, they’ll be invited automatically. \n' + '\n' + 'Every chat will also turn into an email or text that they can respond to directly.', + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, }, { title: 'Split an expense', @@ -3807,6 +3884,13 @@ const CONST = { '4. Add your friend(s) to the request.\n' + '\n' + 'Feel free to add more details if you want, or just send it off. Let’s get you paid back!', + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, }, { title: 'Enable your wallet', @@ -3819,6 +3903,13 @@ const CONST = { '3. Add your bank account.\n' + '\n' + 'Once that’s done, you can request money from anyone and get paid right into your personal bank account.', + video: { + url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, + duration: 55, + width: 1280, + height: 960, + }, }, ], }, From 5df95e03a0bf73c9c5238fcde740066204f04bac Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 16 Apr 2024 13:24:08 +0200 Subject: [PATCH 097/198] fix workspace creating --- src/pages/OnboardingWork/BaseOnboardingWork.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/OnboardingWork/BaseOnboardingWork.tsx b/src/pages/OnboardingWork/BaseOnboardingWork.tsx index 01ec23c8315c..22ebb57489c2 100644 --- a/src/pages/OnboardingWork/BaseOnboardingWork.tsx +++ b/src/pages/OnboardingWork/BaseOnboardingWork.tsx @@ -43,7 +43,7 @@ function BaseOnboardingWork({currentUserPersonalDetails, shouldUseNativeStyles, const work = values.work.trim(); - Policy.createWorkspace(currentUserPersonalDetails.login ?? '', true, work); + Policy.createWorkspace(undefined, true, work); Report.completeOnboarding(onboardingPurposeSelected, CONST.ONBOARDING_MESSAGES[onboardingPurposeSelected], { login: currentUserPersonalDetails.login ?? '', From ca821a012f3de8599139f740dfa846b58e77f124 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 16 Apr 2024 13:34:11 +0200 Subject: [PATCH 098/198] integrate video to tasks --- src/libs/actions/Report.ts | 106 +++++++++++++++++++++++-------------- 1 file changed, 67 insertions(+), 39 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index cdf38af7f3f2..4e2aa729c706 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -118,6 +118,13 @@ type TaskForParameters = createdTaskReportActionID: string; title: string; description: string; + video: { + url: string; + thumbnailUrl: string; + duration: number; + width: number; + height: number; + }; } | { type: 'message'; @@ -3019,60 +3026,80 @@ function completeOnboarding( }, ); const subtitleComment = ReportUtils.buildOptimisticAddCommentReportAction(task.subtitle, undefined, actorAccountID); + const taskVideoComment = ReportUtils.buildOptimisticAddCommentReportAction(CONST.ATTACHMENT_MESSAGE_TEXT, undefined, actorAccountID); const instructionComment = ReportUtils.buildOptimisticAddCommentReportAction(task.message, undefined, actorAccountID, undefined, false); return { + task, currentTask, taskCreatedAction, taskReportAction, subtitleComment, + taskVideoComment, instructionComment, }; }); - const tasksForParameters = tasksData.reduce((acc, {currentTask, taskCreatedAction, taskReportAction, subtitleComment, instructionComment}) => { - const subtitleCommentAction: OptimisticAddCommentReportAction = subtitleComment.reportAction; - const subtitleCommentText = subtitleComment.commentText; - const subtitleMessage: TaskMessage = { - reportID: currentTask.reportID, - reportActionID: subtitleCommentAction.reportActionID, - reportComment: subtitleCommentText, - }; + const tasksForParameters = tasksData.reduce( + (acc, {task, currentTask, taskCreatedAction, taskReportAction, subtitleComment, taskVideoComment, instructionComment}) => { + const subtitleCommentAction: OptimisticAddCommentReportAction = subtitleComment.reportAction; + const subtitleCommentText = subtitleComment.commentText; + const subtitleMessage: TaskMessage = { + reportID: currentTask.reportID, + reportActionID: subtitleCommentAction.reportActionID, + reportComment: subtitleCommentText, + }; - const instructionCommentAction: OptimisticAddCommentReportAction = instructionComment.reportAction; - const instructionCommentText = instructionComment.commentText; - const instructionMessage: TaskMessage = { - reportID: currentTask.reportID, - reportActionID: instructionCommentAction.reportActionID, - reportComment: instructionCommentText, - }; + const taskVideoCommentAction: OptimisticAddCommentReportAction = taskVideoComment.reportAction; + const taskVideoCommentText = instructionComment.commentText; + const taskVideoMessage: TaskMessage = { + reportID: currentTask.reportID, + reportActionID: taskVideoCommentAction.reportActionID, + reportComment: taskVideoCommentText, + }; - return [ - ...acc, - { - type: 'task', - task: engagementChoice, - taskReportID: currentTask.reportID, - parentReportID: currentTask.parentReportID ?? '', - parentReportActionID: taskReportAction.reportAction.reportActionID, - assigneeChatReportID: '', - createdTaskReportActionID: taskCreatedAction.reportActionID, - title: currentTask.reportName ?? '', - description: currentTask.description ?? '', - }, - { - type: 'message', - ...subtitleMessage, - }, - { - type: 'message', - ...instructionMessage, - }, - ]; - }, []); + const instructionCommentAction: OptimisticAddCommentReportAction = instructionComment.reportAction; + const instructionCommentText = instructionComment.commentText; + const instructionMessage: TaskMessage = { + reportID: currentTask.reportID, + reportActionID: instructionCommentAction.reportActionID, + reportComment: instructionCommentText, + }; + + return [ + ...acc, + { + type: 'task', + task: engagementChoice, + taskReportID: currentTask.reportID, + parentReportID: currentTask.parentReportID ?? '', + parentReportActionID: taskReportAction.reportAction.reportActionID, + assigneeChatReportID: '', + createdTaskReportActionID: taskCreatedAction.reportActionID, + title: currentTask.reportName ?? '', + description: currentTask.description ?? '', + video: task.video, + }, + { + type: 'message', + ...taskVideoMessage, + }, + { + type: 'message', + ...subtitleMessage, + }, + { + type: 'message', + ...instructionMessage, + }, + ]; + }, + [], + ); - const tasksForOptimisticData = tasksData.reduce((acc, {currentTask, taskCreatedAction, taskReportAction, subtitleComment, instructionComment}) => { + const tasksForOptimisticData = tasksData.reduce((acc, {currentTask, taskCreatedAction, taskReportAction, subtitleComment, taskVideoComment, instructionComment}) => { const subtitleCommentAction: OptimisticAddCommentReportAction = subtitleComment.reportAction; + const taskVideoCommentAction: OptimisticAddCommentReportAction = taskVideoComment.reportAction; const instructionCommentAction: OptimisticAddCommentReportAction = instructionComment.reportAction; return [ @@ -3104,6 +3131,7 @@ function completeOnboarding( value: { [taskCreatedAction.reportActionID]: taskCreatedAction as ReportAction, [subtitleCommentAction.reportActionID]: subtitleCommentAction as ReportAction, + [taskVideoCommentAction.reportActionID]: taskVideoCommentAction as ReportAction, [instructionCommentAction.reportActionID]: instructionCommentAction as ReportAction, }, }, From 9591026558a273e022ff1c97c17676af19f59546 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 16 Apr 2024 13:43:55 +0200 Subject: [PATCH 099/198] integrate OnboardingPurposeType type --- src/CONST.ts | 4 +++- src/libs/API/parameters/CompleteGuidedSetupParams.ts | 4 +++- src/libs/actions/Report.ts | 3 ++- src/pages/OnboardingPersonalDetails/types.ts | 3 ++- src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx | 8 +++----- src/pages/OnboardingPurpose/types.ts | 3 ++- src/pages/OnboardingWork/types.ts | 3 ++- src/types/onyx/IntroSelected.ts | 5 ++--- 8 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 039598dd2b8a..b91c1a06a0fb 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -66,6 +66,8 @@ const onboardingChoices = { LOOKING_AROUND: 'newDotLookingAround', }; +type OnboardingPurposeType = ValueOf; + const CONST = { MERGED_ACCOUNT_PREFIX: 'MERGED_', DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL], @@ -4673,6 +4675,6 @@ const CONST = { type Country = keyof typeof CONST.ALL_COUNTRIES; type IOUType = ValueOf; -export type {Country, IOUType}; +export type {Country, IOUType, OnboardingPurposeType}; export default CONST; diff --git a/src/libs/API/parameters/CompleteGuidedSetupParams.ts b/src/libs/API/parameters/CompleteGuidedSetupParams.ts index 6835949382ca..e3a0309d5113 100644 --- a/src/libs/API/parameters/CompleteGuidedSetupParams.ts +++ b/src/libs/API/parameters/CompleteGuidedSetupParams.ts @@ -1,8 +1,10 @@ +import type {OnboardingPurposeType} from '@src/CONST'; + type CompleteGuidedSetupParams = { firstName: string; lastName: string; guidedSetupData: string; - engagementChoice: string; + engagementChoice: OnboardingPurposeType; }; export default CompleteGuidedSetupParams; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 4e2aa729c706..e05dceb9fead 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -72,6 +72,7 @@ import shouldSkipDeepLinkNavigation from '@libs/shouldSkipDeepLinkNavigation'; import * as UserUtils from '@libs/UserUtils'; import Visibility from '@libs/Visibility'; import CONFIG from '@src/CONFIG'; +import type {OnboardingPurposeType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; @@ -3190,7 +3191,7 @@ function completeOnboarding( * - Sets the introSelected NVP to the choice the user made * - Creates an optimistic report comment from concierge */ -function completeEngagementModal(text: string, choice: ValueOf) { +function completeEngagementModal(text: string, choice: OnboardingPurposeType) { const conciergeAccountID = PersonalDetailsUtils.getAccountIDsByLogins([CONST.EMAIL.CONCIERGE])[0]; const reportComment = ReportUtils.buildOptimisticAddCommentReportAction(text, undefined, conciergeAccountID); const reportCommentAction: OptimisticAddCommentReportAction = reportComment.reportAction; diff --git a/src/pages/OnboardingPersonalDetails/types.ts b/src/pages/OnboardingPersonalDetails/types.ts index 6d286373a1ac..4828cc6e73bd 100644 --- a/src/pages/OnboardingPersonalDetails/types.ts +++ b/src/pages/OnboardingPersonalDetails/types.ts @@ -1,11 +1,12 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import type {OnboardingPurposeType} from '@src/CONST'; type OnboardingPersonalDetailsProps = Record; type BaseOnboardingPersonalDetailsOnyxProps = { /** Saved onboarding purpose selected by the user */ - onboardingPurposeSelected: OnyxEntry; + onboardingPurposeSelected: OnyxEntry; }; type BaseOnboardingPersonalDetailsProps = WithCurrentUserPersonalDetailsProps & diff --git a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx index 8ccbda809d97..a6aec605a8f0 100644 --- a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx +++ b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx @@ -21,14 +21,12 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import Navigation from '@libs/Navigation/Navigation'; import variables from '@styles/variables'; import * as Welcome from '@userActions/Welcome'; +import type {OnboardingPurposeType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {BaseOnboardingPurposeOnyxProps, BaseOnboardingPurposeProps} from './types'; -type ValuesType = T[keyof T]; -type SelectedPurposeType = ValuesType | undefined; - const menuIcons = { [CONST.ONBOARDING_CHOICES.TRACK]: Illustrations.CompanyCard, [CONST.ONBOARDING_CHOICES.EMPLOYER]: Illustrations.ReceiptUpload, @@ -42,7 +40,7 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on const styles = useThemeStyles(); const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useOnboardingLayout(); - const [selectedPurpose, setSelectedPurpose] = useState(undefined); + const [selectedPurpose, setSelectedPurpose] = useState(undefined); const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); const [error, setError] = useState(false); const theme = useTheme(); @@ -155,4 +153,4 @@ export default withOnyx; type BaseOnboardingPurposeOnyxProps = { /** Saved onboarding purpose selected by the user */ - onboardingPurposeSelected: OnyxEntry; + onboardingPurposeSelected: OnyxEntry; }; type BaseOnboardingPurposeProps = OnboardingPurposeProps & diff --git a/src/pages/OnboardingWork/types.ts b/src/pages/OnboardingWork/types.ts index 930a8081e9c4..5bef8048628d 100644 --- a/src/pages/OnboardingWork/types.ts +++ b/src/pages/OnboardingWork/types.ts @@ -1,11 +1,12 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import type {OnboardingPurposeType} from '@src/CONST'; type OnboardingWorkProps = Record; type BaseOnboardingWorkOnyxProps = { /** Saved onboarding purpose selected by the user */ - onboardingPurposeSelected: OnyxEntry; + onboardingPurposeSelected: OnyxEntry; }; type BaseOnboardingWorkProps = WithCurrentUserPersonalDetailsProps & diff --git a/src/types/onyx/IntroSelected.ts b/src/types/onyx/IntroSelected.ts index 9917f4b44550..14a0d2f70dfe 100644 --- a/src/types/onyx/IntroSelected.ts +++ b/src/types/onyx/IntroSelected.ts @@ -1,9 +1,8 @@ -import type {ValueOf} from 'type-fest'; -import type CONST from '@src/CONST'; +import type {OnboardingPurposeType} from '@src/CONST'; type IntroSelected = { /** The choice that the user selected in the engagement modal */ - choice: ValueOf; + choice: OnboardingPurposeType; }; export default IntroSelected; From d87069dc843ebf65a0e2f7dca51fedf45d590366 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 16 Apr 2024 13:48:19 +0200 Subject: [PATCH 100/198] integrate GuidedSetupData type --- src/libs/actions/Report.ts | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index e05dceb9fead..cea442efecf7 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -108,6 +108,14 @@ type ActionSubscriber = { callback: SubscriberCallback; }; +type Video = { + url: string; + thumbnailUrl: string; + duration: number; + width: number; + height: number; +}; + type TaskForParameters = | { type: 'task'; @@ -119,13 +127,7 @@ type TaskForParameters = createdTaskReportActionID: string; title: string; description: string; - video: { - url: string; - thumbnailUrl: string; - duration: number; - width: number; - height: number; - }; + video: Video; } | { type: 'message'; @@ -136,6 +138,15 @@ type TaskForParameters = type TaskMessage = Required>; +type GuidedSetupData = Array< + | AddCommentOrAttachementParams + | TaskForParameters + | ({ + type: 'video'; + } & Video & + AddCommentOrAttachementParams) +>; + let conciergeChatReportID: string | undefined; let currentUserAccountID = -1; let currentUserEmail: string | undefined; @@ -3175,11 +3186,18 @@ function completeOnboarding( }, ]; + const guidedSetupData: GuidedSetupData = [ + {type: 'message', ...mentionMessage}, + {type: 'message', ...textMessage}, + {type: 'video', ...data.video, ...videoMessage}, + ...tasksForParameters, + ]; + const parameters: CompleteGuidedSetupParams = { engagementChoice, firstName, lastName, - guidedSetupData: JSON.stringify([{type: 'message', ...mentionMessage}, {type: 'message', ...textMessage}, {type: 'video', ...data.video, ...videoMessage}, ...tasksForParameters]), + guidedSetupData: JSON.stringify(guidedSetupData), }; API.write(WRITE_COMMANDS.COMPLETE_GUIDED_SETUP, parameters, {optimisticData, successData}); From e4a4fa646d28ac9d3a60e83aba05220f1011a702 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 16 Apr 2024 14:04:30 +0200 Subject: [PATCH 101/198] use OnboardingPurposeType --- src/libs/actions/Welcome.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Welcome.ts b/src/libs/actions/Welcome.ts index 87762bc856ca..84066c05e80e 100644 --- a/src/libs/actions/Welcome.ts +++ b/src/libs/actions/Welcome.ts @@ -1,6 +1,6 @@ import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {SelectedPurposeType} from '@pages/OnboardingPurpose/BaseOnboardingPurpose'; +import type {OnboardingPurposeType} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type OnyxPolicy from '@src/types/onyx/Policy'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; @@ -112,7 +112,7 @@ function getPersonalDetails(accountID: number | undefined) { }); } -function setOnboardingPurposeSelected(value: SelectedPurposeType) { +function setOnboardingPurposeSelected(value: OnboardingPurposeType) { Onyx.set(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, value ?? null); } From ef7f8c6d779cb557db08b9e65d488672786b60f5 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 16 Apr 2024 14:31:20 +0200 Subject: [PATCH 102/198] change progress bar values --- .../OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx | 2 +- src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx index 5657f8553113..ee9693a950fd 100644 --- a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx +++ b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx @@ -120,7 +120,7 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat From 3c6e51247af49f731c7196ebd747fbc0a60b145a Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 16 Apr 2024 14:54:56 +0200 Subject: [PATCH 103/198] fix task actions ordering --- src/libs/actions/Report.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index cea442efecf7..9921816a3e5c 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3038,8 +3038,8 @@ function completeOnboarding( }, ); const subtitleComment = ReportUtils.buildOptimisticAddCommentReportAction(task.subtitle, undefined, actorAccountID); - const taskVideoComment = ReportUtils.buildOptimisticAddCommentReportAction(CONST.ATTACHMENT_MESSAGE_TEXT, undefined, actorAccountID); - const instructionComment = ReportUtils.buildOptimisticAddCommentReportAction(task.message, undefined, actorAccountID, undefined, false); + const taskVideoComment = ReportUtils.buildOptimisticAddCommentReportAction(CONST.ATTACHMENT_MESSAGE_TEXT, undefined, actorAccountID, 1); + const instructionComment = ReportUtils.buildOptimisticAddCommentReportAction(task.message, undefined, actorAccountID, 2, false); return { task, From 6eb2c290b7f7e42a43816add805784d35d7e2b40 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 16 Apr 2024 15:54:46 +0200 Subject: [PATCH 104/198] integrate addition options --- src/CONST.ts | 14 +-- src/libs/actions/Report.ts | 114 ++++++++++++------ .../OnboardingWork/BaseOnboardingWork.tsx | 19 +-- 3 files changed, 96 insertions(+), 51 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index b91c1a06a0fb..e6675916f15d 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3734,17 +3734,11 @@ const CONST = { { title: 'Meet your setup specialist', subtitle: '', - message: - 'Meet your setup specialist, {guideName}, who can answer any questions as you get started with Expensify. Yes, a real human!\n' + + message: ({adminsRoomLink, guideCalendarLink}: {adminsRoomLink: string; guideCalendarLink: string}) => + `Meet your setup specialist, who can answer any questions as you get started with Expensify. Yes, a real human!\n` + '\n' + - 'Chat with {guideName} in your [admins room]({adminsRoomID}) or [schedule a call]({guideCalendarLink}) today', - video: { - url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, - thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, - duration: 55, - width: 1280, - height: 960, - }, + `Chat with the specialist in your [#admins room](${adminsRoomLink}) or [schedule a call](${guideCalendarLink}) today.`, + video: null, }, { title: 'Set up categories', diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 9921816a3e5c..03636d869ed6 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -127,7 +127,7 @@ type TaskForParameters = createdTaskReportActionID: string; title: string; description: string; - video: Video; + video?: Video; } | { type: 'message'; @@ -163,6 +163,14 @@ Onyx.connect({ }, }); +let guideCalendarLink: string | undefined; +Onyx.connect({ + key: ONYXKEYS.ACCOUNT, + callback: (value) => { + guideCalendarLink = value?.guideCalendarLink ?? ''; + }, +}); + let preferredSkinTone: number = CONST.EMOJI_DEFAULT_SKIN_TONE; Onyx.connect({ key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, @@ -2978,6 +2986,7 @@ function completeOnboarding( firstName: string; lastName: string; }, + adminsChatReportID?: string, ) { const targetEmail = CONST.EMAIL.CONCIERGE; const actorAccountID = PersonalDetailsUtils.getAccountIDsByLogins([targetEmail])[0]; @@ -3037,9 +3046,16 @@ function completeOnboarding( childOldestFourAccountIDs: `${actorAccountID}`, }, ); - const subtitleComment = ReportUtils.buildOptimisticAddCommentReportAction(task.subtitle, undefined, actorAccountID); - const taskVideoComment = ReportUtils.buildOptimisticAddCommentReportAction(CONST.ATTACHMENT_MESSAGE_TEXT, undefined, actorAccountID, 1); - const instructionComment = ReportUtils.buildOptimisticAddCommentReportAction(task.message, undefined, actorAccountID, 2, false); + const subtitleComment = task.subtitle ? ReportUtils.buildOptimisticAddCommentReportAction(task.subtitle, undefined, actorAccountID) : null; + const taskVideoComment = task.video ? ReportUtils.buildOptimisticAddCommentReportAction(CONST.ATTACHMENT_MESSAGE_TEXT, undefined, actorAccountID, 1) : null; + const taskMessage = + typeof task.message === 'function' + ? task.message({ + adminsRoomLink: `${CONFIG.EXPENSIFY.NEW_EXPENSIFY_URL}${ROUTES.REPORT_WITH_ID.getRoute(adminsChatReportID ?? '')}`, + guideCalendarLink: guideCalendarLink ?? CONFIG.EXPENSIFY.NEW_EXPENSIFY_URL, + }) + : task.message; + const instructionComment = ReportUtils.buildOptimisticAddCommentReportAction(taskMessage, undefined, actorAccountID, 2, false); return { task, @@ -3054,22 +3070,6 @@ function completeOnboarding( const tasksForParameters = tasksData.reduce( (acc, {task, currentTask, taskCreatedAction, taskReportAction, subtitleComment, taskVideoComment, instructionComment}) => { - const subtitleCommentAction: OptimisticAddCommentReportAction = subtitleComment.reportAction; - const subtitleCommentText = subtitleComment.commentText; - const subtitleMessage: TaskMessage = { - reportID: currentTask.reportID, - reportActionID: subtitleCommentAction.reportActionID, - reportComment: subtitleCommentText, - }; - - const taskVideoCommentAction: OptimisticAddCommentReportAction = taskVideoComment.reportAction; - const taskVideoCommentText = instructionComment.commentText; - const taskVideoMessage: TaskMessage = { - reportID: currentTask.reportID, - reportActionID: taskVideoCommentAction.reportActionID, - reportComment: taskVideoCommentText, - }; - const instructionCommentAction: OptimisticAddCommentReportAction = instructionComment.reportAction; const instructionCommentText = instructionComment.commentText; const instructionMessage: TaskMessage = { @@ -3078,7 +3078,7 @@ function completeOnboarding( reportComment: instructionCommentText, }; - return [ + const tasksForParametersAcc: TaskForParameters[] = [ ...acc, { type: 'task', @@ -3090,31 +3090,53 @@ function completeOnboarding( createdTaskReportActionID: taskCreatedAction.reportActionID, title: currentTask.reportName ?? '', description: currentTask.description ?? '', - video: task.video, + video: task.video ?? undefined, }, { type: 'message', - ...taskVideoMessage, + ...instructionMessage, }, - { + ]; + + if (subtitleComment) { + const subtitleCommentAction: OptimisticAddCommentReportAction = subtitleComment.reportAction; + const subtitleCommentText = subtitleComment.commentText; + const subtitleMessage: TaskMessage = { + reportID: currentTask.reportID, + reportActionID: subtitleCommentAction.reportActionID, + reportComment: subtitleCommentText, + }; + + tasksForParametersAcc.push({ type: 'message', ...subtitleMessage, - }, - { + }); + } + + if (taskVideoComment) { + const taskVideoCommentAction: OptimisticAddCommentReportAction = taskVideoComment.reportAction; + const taskVideoCommentText = instructionComment.commentText; + const taskVideoMessage: TaskMessage = { + reportID: currentTask.reportID, + reportActionID: taskVideoCommentAction.reportActionID, + reportComment: taskVideoCommentText, + }; + + tasksForParametersAcc.push({ type: 'message', - ...instructionMessage, - }, - ]; + ...taskVideoMessage, + }); + } + + return tasksForParametersAcc; }, [], ); const tasksForOptimisticData = tasksData.reduce((acc, {currentTask, taskCreatedAction, taskReportAction, subtitleComment, taskVideoComment, instructionComment}) => { - const subtitleCommentAction: OptimisticAddCommentReportAction = subtitleComment.reportAction; - const taskVideoCommentAction: OptimisticAddCommentReportAction = taskVideoComment.reportAction; const instructionCommentAction: OptimisticAddCommentReportAction = instructionComment.reportAction; - return [ + const tasksForOptimisticDataAcc: OnyxUpdate[] = [ ...acc, { onyxMethod: Onyx.METHOD.MERGE, @@ -3142,12 +3164,36 @@ function completeOnboarding( key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentTask.reportID}`, value: { [taskCreatedAction.reportActionID]: taskCreatedAction as ReportAction, - [subtitleCommentAction.reportActionID]: subtitleCommentAction as ReportAction, - [taskVideoCommentAction.reportActionID]: taskVideoCommentAction as ReportAction, [instructionCommentAction.reportActionID]: instructionCommentAction as ReportAction, }, }, ]; + + if (subtitleComment) { + const subtitleCommentAction: OptimisticAddCommentReportAction = subtitleComment.reportAction; + + tasksForOptimisticDataAcc.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentTask.reportID}`, + value: { + [subtitleCommentAction.reportActionID]: subtitleCommentAction as ReportAction, + }, + }); + } + + if (taskVideoComment) { + const taskVideoCommentAction: OptimisticAddCommentReportAction = taskVideoComment.reportAction; + + tasksForOptimisticDataAcc.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentTask.reportID}`, + value: { + [taskVideoCommentAction.reportActionID]: taskVideoCommentAction as ReportAction, + }, + }); + } + + return tasksForOptimisticDataAcc; }, []); const optimisticData: OnyxUpdate[] = [ diff --git a/src/pages/OnboardingWork/BaseOnboardingWork.tsx b/src/pages/OnboardingWork/BaseOnboardingWork.tsx index 22ebb57489c2..81be2bcc6ac5 100644 --- a/src/pages/OnboardingWork/BaseOnboardingWork.tsx +++ b/src/pages/OnboardingWork/BaseOnboardingWork.tsx @@ -43,13 +43,18 @@ function BaseOnboardingWork({currentUserPersonalDetails, shouldUseNativeStyles, const work = values.work.trim(); - Policy.createWorkspace(undefined, true, work); - - Report.completeOnboarding(onboardingPurposeSelected, CONST.ONBOARDING_MESSAGES[onboardingPurposeSelected], { - login: currentUserPersonalDetails.login ?? '', - firstName: currentUserPersonalDetails.firstName ?? '', - lastName: currentUserPersonalDetails.lastName ?? '', - }); + const adminsChatReportID = Policy.createWorkspace(undefined, true, work); + + Report.completeOnboarding( + onboardingPurposeSelected, + CONST.ONBOARDING_MESSAGES[onboardingPurposeSelected], + { + login: currentUserPersonalDetails.login ?? '', + firstName: currentUserPersonalDetails.firstName ?? '', + lastName: currentUserPersonalDetails.lastName ?? '', + }, + adminsChatReportID, + ); Navigation.dismissModal(); // Only navigate to concierge chat when central pane is visible From 820360ffa66ef70375250f73f1e8bf0d36d2735f Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 16 Apr 2024 15:56:46 +0200 Subject: [PATCH 105/198] fix default value of guideCalendarLink --- src/libs/actions/Report.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 03636d869ed6..c08e4928eea1 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -167,7 +167,7 @@ let guideCalendarLink: string | undefined; Onyx.connect({ key: ONYXKEYS.ACCOUNT, callback: (value) => { - guideCalendarLink = value?.guideCalendarLink ?? ''; + guideCalendarLink = value?.guideCalendarLink ?? undefined; }, }); From 28948abcf110c4edc6a616f1df87c323d7a3dc15 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 16 Apr 2024 18:30:15 +0200 Subject: [PATCH 106/198] fix: apply requested changes --- src/libs/DistanceRequestUtils.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index d35d7b0d711c..6f82ec155c2d 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -71,9 +71,6 @@ function getDefaultMileageRate(policy: OnyxEntry | EmptyObject): Mileage } const distanceRate = Object.values(distanceUnit.rates).find((rate) => rate.name === CONST.CUSTOM_UNITS.DEFAULT_RATE) ?? Object.values(distanceUnit.rates)[0]; - if (!distanceRate) { - return null; - } return { customUnitRateID: distanceRate.customUnitRateID, @@ -131,7 +128,7 @@ function getRateForDisplay( toLocaleDigit: LocaleContextProps['toLocaleDigit'], isOffline?: boolean, ): string { - if (isOffline) { + if (isOffline && !rate) { return translate('iou.defaultRate'); } if (!hasRoute || !rate || !currency || !unit) { From 6bcd2570348c1f220c7e00888193ccd5bc09c6b2 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 16 Apr 2024 18:55:14 +0200 Subject: [PATCH 107/198] fix: check for quantity instead of route --- src/components/MoneyRequestConfirmationList.tsx | 2 +- .../MoneyTemporaryForRefactorRequestConfirmationList.tsx | 2 +- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- src/libs/TransactionUtils.ts | 4 ++-- src/pages/iou/request/step/IOURequestStepDistance.tsx | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index aaf86a052960..035bd440e256 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -264,7 +264,7 @@ function MoneyRequestConfirmationList({ // A flag for showing the billable field const shouldShowBillable = !(policy?.disabledFields?.defaultBillable ?? true); - const hasRoute = TransactionUtils.hasRoute(transaction); + const hasRoute = TransactionUtils.hasRoute(transaction, isDistanceRequest); const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate); const formattedAmount = isDistanceRequestWithPendingRoute ? '' diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 66ba8b78a735..e43a2db22ec0 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -277,7 +277,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ // A flag for showing the billable field const shouldShowBillable = policy?.disabledFields?.defaultBillable === false; - const hasRoute = TransactionUtils.hasRoute(transaction); + const hasRoute = TransactionUtils.hasRoute(transaction, isDistanceRequest); const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate); const formattedAmount = isDistanceRequestWithPendingRoute ? '' diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 74d04c8807e1..2e148292c0d7 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -178,7 +178,7 @@ function MoneyRequestView({ let amountDescription = `${translate('iou.amount')}`; - const hasRoute = TransactionUtils.hasRoute(transaction); + const hasRoute = TransactionUtils.hasRoute(transaction, isDistanceRequest); const rateID = transaction?.comment.customUnit?.customUnitRateID ?? '0'; const currency = policy ? policy.outputCurrency : PolicyUtils.getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 39edac4d5c17..a54ec416cad0 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -517,8 +517,8 @@ function hasMissingSmartscanFields(transaction: OnyxEntry): boolean /** * Check if the transaction has a defined route */ -function hasRoute(transaction: OnyxEntry): boolean { - return !!transaction?.routes?.route0?.geometry?.coordinates; +function hasRoute(transaction: OnyxEntry, isDistanceRequestType: boolean): boolean { + return isDistanceRequestType && !!transaction?.comment?.customUnit?.quantity; } function getAllReportTransactions(reportID?: string): Transaction[] { diff --git a/src/pages/iou/request/step/IOURequestStepDistance.tsx b/src/pages/iou/request/step/IOURequestStepDistance.tsx index fae07ad2249c..45da24c55c5c 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistance.tsx @@ -67,7 +67,7 @@ function IOURequestStepDistance({ const isLoadingRoute = transaction?.comment?.isLoading ?? false; const isLoading = transaction?.isLoading ?? false; const hasRouteError = !!transaction?.errorFields?.route; - const hasRoute = TransactionUtils.hasRoute(transaction); + const hasRoute = TransactionUtils.hasRoute(transaction, true); const validatedWaypoints = TransactionUtils.getValidWaypoints(waypoints); const previousValidatedWaypoints = usePrevious(validatedWaypoints); const haveValidatedWaypointsChanged = !isEqual(previousValidatedWaypoints, validatedWaypoints); From 520cf1f022739c66476f775d2416e7082c719658 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 16 Apr 2024 19:39:07 +0200 Subject: [PATCH 108/198] fix: minor fix --- src/libs/TransactionUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index a54ec416cad0..806c879a3709 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -518,7 +518,7 @@ function hasMissingSmartscanFields(transaction: OnyxEntry): boolean * Check if the transaction has a defined route */ function hasRoute(transaction: OnyxEntry, isDistanceRequestType: boolean): boolean { - return isDistanceRequestType && !!transaction?.comment?.customUnit?.quantity; + return !!transaction?.routes?.route0?.geometry?.coordinates || (isDistanceRequestType && !!transaction?.comment?.customUnit?.quantity); } function getAllReportTransactions(reportID?: string): Transaction[] { From 706ba6fc51b70a6087dbf583a98c4409c8cef7c1 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 16 Apr 2024 20:47:03 +0200 Subject: [PATCH 109/198] fix: resolve conflicts --- ...raryForRefactorRequestConfirmationList.tsx | 72 +++++++++++-------- 1 file changed, 43 insertions(+), 29 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 5abb602df995..a8c1c22093ab 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -709,9 +709,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ title={iouComment} description={translate('common.description')} onPress={() => { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()), - ); + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); }} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} @@ -732,17 +730,49 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ description={translate('common.distance')} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} - onPress={() => - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()), - ) - } + onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing disabled={didConfirm} // todo: handle edit for transaction while moving from track expense interactive={!isReadOnly && !isMovingTransactionFromTrackExpense} /> ), + shouldShow: isDistanceRequest && !canUseP2PDistanceRequests, + isSupplementary: false, + }, + { + item: ( + + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())) + } + disabled={didConfirm} + interactive={!isReadOnly} + /> + ), + shouldShow: isDistanceRequest && canUseP2PDistanceRequests, + isSupplementary: false, + }, + { + item: ( + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + disabled={didConfirm} + interactive={Boolean(rate) && !isReadOnly && isPolicyExpenseChat} + /> + ), shouldShow: isDistanceRequest, isSupplementary: true, }, @@ -798,9 +828,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ title={iouCategory} description={translate('common.category')} numberOfLinesTitle={2} - onPress={() => - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())) - } + onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} disabled={didConfirm} @@ -821,11 +849,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ title={TransactionUtils.getTagForDisplay(transaction, index)} description={name} numberOfLinesTitle={2} - onPress={() => - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(action, iouType, index, transactionID, reportID, Navigation.getActiveRouteWithoutParams()), - ) - } + onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(action, iouType, index, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} style={[styles.moneyRequestMenuItem]} disabled={didConfirm} interactive={!isReadOnly} @@ -845,9 +869,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ description={taxRates?.name} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} - onPress={() => - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())) - } + onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} disabled={didConfirm} interactive={!isReadOnly} /> @@ -864,11 +886,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ description={translate('iou.taxAmount')} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} - onPress={() => - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()), - ) - } + onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} disabled={didConfirm} interactive={!isReadOnly} /> @@ -970,11 +988,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ !isDistanceRequest && iouType === CONST.IOU.TYPE.REQUEST && ( - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()), - ) - } + onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} /> ))} {primaryFields} From fb59ef104cc108b2ad99cf66234c7b2958a95f69 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 16 Apr 2024 20:52:10 +0200 Subject: [PATCH 110/198] fix: navigation fix --- .../MoneyTemporaryForRefactorRequestConfirmationList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index a8c1c22093ab..14a061b59c21 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -768,7 +768,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ description={translate('common.rate')} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} disabled={didConfirm} interactive={Boolean(rate) && !isReadOnly && isPolicyExpenseChat} /> From 2955ee9aae1d449fbf3260e7ad66dd76c07cc4f1 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 16 Apr 2024 22:20:40 +0200 Subject: [PATCH 111/198] fix: translation --- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 9f0022c41ff0..2450ce6a3c7a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -635,7 +635,7 @@ export default { canceled: 'Canceled', posted: 'Posted', deleteReceipt: 'Delete receipt', - routePending: 'Route pending...', + routePending: 'Pending...', defaultRate: 'Default rate', receiptScanning: 'Scan in progress…', receiptMissingDetails: 'Receipt missing details', diff --git a/src/languages/es.ts b/src/languages/es.ts index a0cedeb5fd82..ee4b20479ee7 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -631,7 +631,7 @@ export default { canceled: 'Canceló', posted: 'Contabilizado', deleteReceipt: 'Eliminar recibo', - routePending: 'Ruta pendiente...', + routePending: 'Pendiente...', defaultRate: 'Tasa predeterminada', receiptScanning: 'Escaneo en curso…', receiptMissingDetails: 'Recibo con campos vacíos', From 068f3abceefcb559685c3175e29b6fec5814f8d3 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 17 Apr 2024 12:12:27 +0700 Subject: [PATCH 112/198] fix bump enpensify-common --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 478ae3c12b3c..46d9e3c8aef2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,7 +57,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#13de5b0606662df33fa1392ad82cc11daadff52e", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#c0f7f3b6558fbeda0527c80d68460d418afef219", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -20212,8 +20212,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#13de5b0606662df33fa1392ad82cc11daadff52e", - "integrity": "sha512-/NAZoAXqeqFWHvC61dueqq9VjRugF69urUtDdDhsfvu1sQE2PCnBoM7a+ACoAEWRYrnP82cyHHhdSA8e7fPuAg==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#c0f7f3b6558fbeda0527c80d68460d418afef219", + "integrity": "sha512-zz0/y0apISP1orxXEQOgn+Uod45O4wVypwwtaqcDPV4dH1tC3i4L98NoLSZvLn7Y17EcceSkfN6QCEsscgFTDQ==", "license": "MIT", "dependencies": { "classnames": "2.5.0", diff --git a/package.json b/package.json index e5092e132eae..83b140cd23f5 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#13de5b0606662df33fa1392ad82cc11daadff52e", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#c0f7f3b6558fbeda0527c80d68460d418afef219", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", From 80efdea3e2f6766123ef518ac1372c45c2dc6342 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 17 Apr 2024 12:38:20 +0700 Subject: [PATCH 113/198] Display violation when tax rate is invalid --- src/components/ReportActionItem/MoneyRequestView.tsx | 2 ++ src/libs/Permissions.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 73fc7e9bae6e..6d4a0bd7c308 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -451,6 +451,8 @@ function MoneyRequestView({ ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(CONST.IOU.ACTION.EDIT, CONST.IOU.TYPE.REQUEST, transaction?.transactionID ?? '', report.reportID), ) } + brickRoadIndicator={getErrorForField('tax') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + error={getErrorForField('tax')} /> )} diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 105736faeba0..80916a1c7bb2 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -4,6 +4,7 @@ import type {IOUType} from '@src/CONST'; import type Beta from '@src/types/onyx/Beta'; function canUseAllBetas(betas: OnyxEntry): boolean { + return true; return !!betas?.includes(CONST.BETAS.ALL); } From f6ef05fdd6160654ee814967ae9aecc5471efbc6 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 17 Apr 2024 11:26:26 +0200 Subject: [PATCH 114/198] fix: minor fix --- .../MoneyTemporaryForRefactorRequestConfirmationList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index bbcb1dea5365..4d536a13a619 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -253,7 +253,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const prevRate = usePrevious(rate); const shouldCalculateDistanceAmount = isDistanceRequest && (iouAmount === 0 || prevRate !== rate); - const currency = (mileageRate as MileageRate)?.currency ?? policy?.outputCurrency ?? CONST.CURRENCY.USD; + const currency = (mileageRate as MileageRate)?.currency ?? policy?.outputCurrency ?? PolicyUtils.getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD; const distance = transaction?.routes?.route0?.distance ?? 0; const taxRates = policy?.taxRates ?? null; From a73490b51c6bccf48b43df791bfb8c4188d3d9c6 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 17 Apr 2024 11:41:17 +0200 Subject: [PATCH 115/198] fix: currency and rate for p2p --- .../MoneyTemporaryForRefactorRequestConfirmationList.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 4d536a13a619..6a5a1ad8104f 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -244,8 +244,10 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ } }, [defaultMileageRate, customUnitRateID, lastSelectedDistanceRates, policy?.id, canUseP2PDistanceRequests, transactionID]); + const policyCurrency = policy?.outputCurrency ?? PolicyUtils.getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD; + const mileageRate = TransactionUtils.isCustomUnitRateIDForP2P(transaction) - ? DistanceRequestUtils.getRateForP2P(policy?.outputCurrency ?? CONST.CURRENCY.USD) + ? DistanceRequestUtils.getRateForP2P(policyCurrency) : mileageRates?.[customUnitRateID] ?? DistanceRequestUtils.getDefaultMileageRate(policy); const {unit, rate} = mileageRate ?? {}; @@ -253,7 +255,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const prevRate = usePrevious(rate); const shouldCalculateDistanceAmount = isDistanceRequest && (iouAmount === 0 || prevRate !== rate); - const currency = (mileageRate as MileageRate)?.currency ?? policy?.outputCurrency ?? PolicyUtils.getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD; + const currency = (mileageRate as MileageRate).currency ?? policyCurrency; const distance = transaction?.routes?.route0?.distance ?? 0; const taxRates = policy?.taxRates ?? null; From 2e7d5c072cd802d2b92bb9d9519b4730d1b3be29 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Apr 2024 15:30:05 +0200 Subject: [PATCH 116/198] fix task video assigning --- src/libs/actions/Report.ts | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index e95132418068..9530fe40d1e7 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -116,6 +116,8 @@ type Video = { height: number; }; +type TaskMessage = Required>; + type TaskForParameters = | { type: 'task'; @@ -127,25 +129,16 @@ type TaskForParameters = createdTaskReportActionID: string; title: string; description: string; - video?: Video; } - | { + | ({ type: 'message'; - reportID: string; - reportActionID: string; - reportComment: string; - }; - -type TaskMessage = Required>; - -type GuidedSetupData = Array< - | AddCommentOrAttachementParams - | TaskForParameters + } & TaskMessage) | ({ type: 'video'; - } & Video & - AddCommentOrAttachementParams) ->; + } & TaskMessage & + Video); + +type GuidedSetupData = Array; let conciergeChatReportID: string | undefined; let currentUserAccountID = -1; @@ -3095,7 +3088,6 @@ function completeOnboarding( createdTaskReportActionID: taskCreatedAction.reportActionID, title: currentTask.reportName ?? '', description: currentTask.description ?? '', - video: task.video ?? undefined, }, { type: 'message', @@ -3118,9 +3110,9 @@ function completeOnboarding( }); } - if (taskVideoComment) { + if (taskVideoComment && task.video) { const taskVideoCommentAction: OptimisticAddCommentReportAction = taskVideoComment.reportAction; - const taskVideoCommentText = instructionComment.commentText; + const taskVideoCommentText = taskVideoComment.commentText; const taskVideoMessage: TaskMessage = { reportID: currentTask.reportID, reportActionID: taskVideoCommentAction.reportActionID, @@ -3128,7 +3120,8 @@ function completeOnboarding( }; tasksForParametersAcc.push({ - type: 'message', + type: 'video', + ...task.video, ...taskVideoMessage, }); } From cb102de3d9c50658c3f52af431eb1e23f7dd4f14 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Apr 2024 15:40:12 +0200 Subject: [PATCH 117/198] minor improvement --- src/libs/actions/Report.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index e89b3048d8cb..82e4eab5c2df 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2973,7 +2973,7 @@ function getReportPrivateNote(reportID: string | undefined) { } function completeOnboarding( - engagementChoice: string, + engagementChoice: OnboardingPurposeType, data: ValueOf, { login, From d6d6f96144378e98b3ea9b6bc19c946c0a44ee32 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 17 Apr 2024 18:10:31 +0200 Subject: [PATCH 118/198] fix: remove hasRoute condition from getRateForDisplay --- .../MoneyTemporaryForRefactorRequestConfirmationList.tsx | 2 +- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- src/libs/DistanceRequestUtils.ts | 6 ++---- src/pages/iou/request/step/IOURequestStepDistanceRate.tsx | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 6a5a1ad8104f..ce50eb949574 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -768,7 +768,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ { - const rateForDisplay = DistanceRequestUtils.getRateForDisplay(true, rate.unit, rate.rate, rate.currency, translate, toLocaleDigit); + const rateForDisplay = DistanceRequestUtils.getRateForDisplay(rate.unit, rate.rate, rate.currency, translate, toLocaleDigit); return { text: rate.name ?? rateForDisplay, From 6c2c0a1c4d0008670440654051bf01bbe0e19822 Mon Sep 17 00:00:00 2001 From: kmichel Date: Wed, 17 Apr 2024 17:51:23 -0700 Subject: [PATCH 119/198] add comment --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 80f0fd596ed6..0480ee4314e9 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -172,6 +172,8 @@ function BaseVideoPlayer({ }); }, [currentVideoPlayerRef, handleFullscreenUpdate, handlePlaybackStatusUpdate]); + // use `useLayoutEffect` instead of `useEffect` because ref is null when unmount in `useEffect` hook + // ref url: https://reactjs.org/blog/2020/08/10/react-v17-rc.html#effect-cleanup-timing useLayoutEffect(() => () => { if(shouldUseSharedVideoElement || videoPlayerRef.current !== currentVideoPlayerRef.current) { @@ -212,7 +214,7 @@ function BaseVideoPlayer({ } return; } - + videoPlayerRef.current = currentVideoPlayerRef.current; if (currentlyPlayingURL === url && newParentRef && 'appendChild' in newParentRef) { newParentRef.appendChild(sharedElement as HTMLDivElement); From 6341e96ae37cc46ebfe2feb4983c75e56cc96560 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 18 Apr 2024 13:01:31 +0700 Subject: [PATCH 120/198] fix add bold style guided setup --- src/CONST.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index a6df33987c8d..60b2b57b7935 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3552,7 +3552,7 @@ const CONST = { "# Let's start tracking your expenses!\n" + '\n' + "To track your expenses, create a workspace to keep everything in one place. Here's how:\n" + - '1. From the home screen, click the green + button > New Workspace\n' + + '1. From the home screen, click the green + button > *New Workspace*\n' + '2. Give your workspace a name (e.g. "My business expenses").\n' + '\n' + 'Then, add expenses to your workspace:\n' + @@ -3565,7 +3565,7 @@ const CONST = { '# Expensify is the fastest way to get paid back!\n' + '\n' + 'To submit expenses for reimbursement:\n' + - '1. From the home screen, click the green + button > Request money.\n' + + '1. From the home screen, click the green + button > *Request money*.\n' + "2. Enter an amount or scan a receipt, then input your boss's email.\n" + '\n' + "That'll send a request to get you paid back. Let me know if you have any questions!", @@ -3573,7 +3573,7 @@ const CONST = { "# Let's start managing your team's expenses!\n" + '\n' + "To manage your team's expenses, create a workspace to keep everything in one place. Here's how:\n" + - '1. From the home screen, click the green + button > New Workspace\n' + + '1. From the home screen, click the green + button > *New Workspace*\n' + '2. Give your workspace a name (e.g. "Sales team expenses").\n' + '\n' + 'Then, invite your team to your workspace via the Members pane and [connect a business bank account](https://help.expensify.com/articles/new-expensify/bank-accounts/Connect-a-Bank-Account) to reimburse them. Let me know if you have any questions!', @@ -3581,7 +3581,7 @@ const CONST = { "# Let's start tracking your expenses! \n" + '\n' + "To track your expenses, create a workspace to keep everything in one place. Here's how:\n" + - '1. From the home screen, click the green + button > New Workspace\n' + + '1. From the home screen, click the green + button > *New Workspace*\n' + '2. Give your workspace a name (e.g. "My expenses").\n' + '\n' + 'Then, add expenses to your workspace:\n' + @@ -3594,7 +3594,7 @@ const CONST = { '# Splitting the bill is as easy as a conversation!\n' + '\n' + 'To split an expense:\n' + - '1. From the home screen, click the green + button > Request money.\n' + + '1. From the home screen, click the green + button > *Request money*.\n' + '2. Enter an amount or scan a receipt, then choose who you want to split it with.\n' + '\n' + "We'll send a request to each person so they can pay you back. Let me know if you have any questions!", From f31b294a2df9819f4504c3d511d0276acc5a60ed Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 18 Apr 2024 12:25:17 +0530 Subject: [PATCH 121/198] link verify redirect --- .github/scripts/detectRedirectCycle.ts | 4 ++++ .github/scripts/verifyRedirect.sh | 19 +++++++++++++++---- .github/workflows/deployExpensifyHelp.yml | 4 ++-- 3 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 .github/scripts/detectRedirectCycle.ts diff --git a/.github/scripts/detectRedirectCycle.ts b/.github/scripts/detectRedirectCycle.ts new file mode 100644 index 000000000000..d0d8ed4eb923 --- /dev/null +++ b/.github/scripts/detectRedirectCycle.ts @@ -0,0 +1,4 @@ +import fs from 'fs'; + +const docsDir = `${process.cwd()}/docs`; + diff --git a/.github/scripts/verifyRedirect.sh b/.github/scripts/verifyRedirect.sh index 737d9bffacf9..b8942cd5b23d 100644 --- a/.github/scripts/verifyRedirect.sh +++ b/.github/scripts/verifyRedirect.sh @@ -5,11 +5,22 @@ declare -r REDIRECTS_FILE="docs/redirects.csv" +declare -r RED='\033[0;31m' +declare -r GREEN='\033[0;32m' +declare -r NC='\033[0m' + duplicates=$(awk -F, 'a[$1]++{print $1}' $REDIRECTS_FILE) +if [[ -n "$duplicates" ]]; then + echo "${RED}duplicate redirects are not allowed: $duplicates ${NC}" + exit 1 +fi -if [[ -z "$duplicates" ]]; then - exit 0 +npm run detectRedirectCycle +DETECT_CYCLE_EXIT_CODE=$? +if [[ DETECT_CYCLE_EXIT_CODE -eq 1 ]]; then + echo -e "${RED}The redirects.csv has a cycle. Please remove the redirect cycle because it will cause an infinite redirect loop ${NC}" + exit 1 fi -echo "duplicate redirects are not allowed: $duplicates" -exit 1 +echo -e "${GREEN}The redirects.csv is valid!${NC}" +exit 0 diff --git a/.github/workflows/deployExpensifyHelp.yml b/.github/workflows/deployExpensifyHelp.yml index 699bd379fb77..f16aab1caaa7 100644 --- a/.github/workflows/deployExpensifyHelp.yml +++ b/.github/workflows/deployExpensifyHelp.yml @@ -36,8 +36,8 @@ jobs: - name: Create docs routes file run: ./.github/scripts/createDocsRoutes.sh - - - name: Check duplicates in redirect.csv + + - name: Check duplicates in redirects.csv run: ./.github/scripts/verifyRedirect.sh - name: Build with Jekyll From 7b8f8b9fa1bd912224fc18497ef45f19ea19a198 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 18 Apr 2024 12:43:15 +0530 Subject: [PATCH 122/198] add csv-parse dev dependency --- package-lock.json | 7 +++++++ package.json | 1 + 2 files changed, 8 insertions(+) diff --git a/package-lock.json b/package-lock.json index 64dd4fb0c885..fcce92f9d2f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -201,6 +201,7 @@ "concurrently": "^8.2.2", "copy-webpack-plugin": "^10.1.0", "css-loader": "^6.7.2", + "csv-parse": "^5.5.5", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", "electron": "^29.2.0", @@ -17740,6 +17741,12 @@ "version": "3.1.1", "license": "MIT" }, + "node_modules/csv-parse": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.5.tgz", + "integrity": "sha512-erCk7tyU3yLWAhk6wvKxnyPtftuy/6Ak622gOO7BCJ05+TYffnPCJF905wmOQm+BpkX54OdAl8pveJwUdpnCXQ==", + "dev": true + }, "node_modules/dag-map": { "version": "1.0.2", "license": "MIT" diff --git a/package.json b/package.json index 78e1a3a13e2c..fdcf786b52b1 100644 --- a/package.json +++ b/package.json @@ -252,6 +252,7 @@ "concurrently": "^8.2.2", "copy-webpack-plugin": "^10.1.0", "css-loader": "^6.7.2", + "csv-parse": "^5.5.5", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", "electron": "^29.2.0", From 459d4e8e47eb8f2c39421fae32f1f679c166a887 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 18 Apr 2024 16:32:49 +0700 Subject: [PATCH 123/198] fix just display track expense of selfDM is loaded --- .../sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index b55fecf48ffb..eb8408998fa2 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -260,6 +260,8 @@ function FloatingActionButtonAndPopover( } }; + const selfDMReportID = useMemo(() => ReportUtils.findSelfDMReportID(), [isLoading]); + return ( interceptAnonymousUser(Report.startNewChat), }, - ...(canUseTrackExpense + ...(canUseTrackExpense && selfDMReportID ? [ { icon: Expensicons.DocumentPlus, From f81ba49eb72a92f118facf2ef3ed4d4932c752b1 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 18 Apr 2024 12:06:39 +0200 Subject: [PATCH 124/198] cut task videos off --- src/libs/actions/Report.ts | 133 ++++++++++++++----------------------- 1 file changed, 51 insertions(+), 82 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 412beb3187d2..1faf850b6d76 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -132,13 +132,16 @@ type TaskForParameters = } | ({ type: 'message'; - } & TaskMessage) + } & TaskMessage); + +type GuidedSetupData = Array< + | ({type: 'message'} & AddCommentOrAttachementParams) + | TaskForParameters | ({ type: 'video'; - } & TaskMessage & - Video); - -type GuidedSetupData = Array; + } & Video & + AddCommentOrAttachementParams) +>; let conciergeChatReportID: string | undefined; let currentUserAccountID = -1; @@ -3051,7 +3054,6 @@ function completeOnboarding( }, ); const subtitleComment = task.subtitle ? ReportUtils.buildOptimisticAddCommentReportAction(task.subtitle, undefined, actorAccountID) : null; - const taskVideoComment = task.video ? ReportUtils.buildOptimisticAddCommentReportAction(CONST.ATTACHMENT_MESSAGE_TEXT, undefined, actorAccountID, 1) : null; const taskMessage = typeof task.message === 'function' ? task.message({ @@ -3059,85 +3061,64 @@ function completeOnboarding( guideCalendarLink: guideCalendarLink ?? CONFIG.EXPENSIFY.NEW_EXPENSIFY_URL, }) : task.message; - const instructionComment = ReportUtils.buildOptimisticAddCommentReportAction(taskMessage, undefined, actorAccountID, 2, false); + const instructionComment = ReportUtils.buildOptimisticAddCommentReportAction(taskMessage, undefined, actorAccountID, 1, false); return { - task, currentTask, taskCreatedAction, taskReportAction, subtitleComment, - taskVideoComment, instructionComment, }; }); - const tasksForParameters = tasksData.reduce( - (acc, {task, currentTask, taskCreatedAction, taskReportAction, subtitleComment, taskVideoComment, instructionComment}) => { - const instructionCommentAction: OptimisticAddCommentReportAction = instructionComment.reportAction; - const instructionCommentText = instructionComment.commentText; - const instructionMessage: TaskMessage = { + const tasksForParameters = tasksData.reduce((acc, {currentTask, taskCreatedAction, taskReportAction, subtitleComment, instructionComment}) => { + const instructionCommentAction: OptimisticAddCommentReportAction = instructionComment.reportAction; + const instructionCommentText = instructionComment.commentText; + const instructionMessage: TaskMessage = { + reportID: currentTask.reportID, + reportActionID: instructionCommentAction.reportActionID, + reportComment: instructionCommentText, + }; + + const tasksForParametersAcc: TaskForParameters[] = [ + ...acc, + { + type: 'task', + task: engagementChoice, + taskReportID: currentTask.reportID, + parentReportID: currentTask.parentReportID ?? '', + parentReportActionID: taskReportAction.reportAction.reportActionID, + assigneeChatReportID: '', + createdTaskReportActionID: taskCreatedAction.reportActionID, + title: currentTask.reportName ?? '', + description: currentTask.description ?? '', + }, + { + type: 'message', + ...instructionMessage, + }, + ]; + + if (subtitleComment) { + const subtitleCommentAction: OptimisticAddCommentReportAction = subtitleComment.reportAction; + const subtitleCommentText = subtitleComment.commentText; + const subtitleMessage: TaskMessage = { reportID: currentTask.reportID, - reportActionID: instructionCommentAction.reportActionID, - reportComment: instructionCommentText, + reportActionID: subtitleCommentAction.reportActionID, + reportComment: subtitleCommentText, }; - const tasksForParametersAcc: TaskForParameters[] = [ - ...acc, - { - type: 'task', - task: engagementChoice, - taskReportID: currentTask.reportID, - parentReportID: currentTask.parentReportID ?? '', - parentReportActionID: taskReportAction.reportAction.reportActionID, - assigneeChatReportID: '', - createdTaskReportActionID: taskCreatedAction.reportActionID, - title: currentTask.reportName ?? '', - description: currentTask.description ?? '', - }, - { - type: 'message', - ...instructionMessage, - }, - ]; - - if (subtitleComment) { - const subtitleCommentAction: OptimisticAddCommentReportAction = subtitleComment.reportAction; - const subtitleCommentText = subtitleComment.commentText; - const subtitleMessage: TaskMessage = { - reportID: currentTask.reportID, - reportActionID: subtitleCommentAction.reportActionID, - reportComment: subtitleCommentText, - }; - - tasksForParametersAcc.push({ - type: 'message', - ...subtitleMessage, - }); - } - - if (taskVideoComment && task.video) { - const taskVideoCommentAction: OptimisticAddCommentReportAction = taskVideoComment.reportAction; - const taskVideoCommentText = taskVideoComment.commentText; - const taskVideoMessage: TaskMessage = { - reportID: currentTask.reportID, - reportActionID: taskVideoCommentAction.reportActionID, - reportComment: taskVideoCommentText, - }; - - tasksForParametersAcc.push({ - type: 'video', - ...task.video, - ...taskVideoMessage, - }); - } + tasksForParametersAcc.push({ + type: 'message', + ...subtitleMessage, + }); + } - return tasksForParametersAcc; - }, - [], - ); + return tasksForParametersAcc; + }, []); - const tasksForOptimisticData = tasksData.reduce((acc, {currentTask, taskCreatedAction, taskReportAction, subtitleComment, taskVideoComment, instructionComment}) => { + const tasksForOptimisticData = tasksData.reduce((acc, {currentTask, taskCreatedAction, taskReportAction, subtitleComment, instructionComment}) => { const instructionCommentAction: OptimisticAddCommentReportAction = instructionComment.reportAction; const tasksForOptimisticDataAcc: OnyxUpdate[] = [ @@ -3185,18 +3166,6 @@ function completeOnboarding( }); } - if (taskVideoComment) { - const taskVideoCommentAction: OptimisticAddCommentReportAction = taskVideoComment.reportAction; - - tasksForOptimisticDataAcc.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentTask.reportID}`, - value: { - [taskVideoCommentAction.reportActionID]: taskVideoCommentAction as ReportAction, - }, - }); - } - return tasksForOptimisticDataAcc; }, []); From 55b4f52ef2c9c290701268a9ba5b9e7bd3e67e04 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 18 Apr 2024 17:11:41 +0700 Subject: [PATCH 125/198] fix lint --- .../sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index eb8408998fa2..b74a98a1a94a 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -260,7 +260,10 @@ function FloatingActionButtonAndPopover( } }; - const selfDMReportID = useMemo(() => ReportUtils.findSelfDMReportID(), [isLoading]); + const selfDMReportID = useMemo(() => { + return ReportUtils.findSelfDMReportID(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isLoading]); return ( From 0b61a9df1a444d9240664fd17d8d7661a6e7865f Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 18 Apr 2024 17:19:29 +0700 Subject: [PATCH 126/198] fix lint --- .../SidebarScreen/FloatingActionButtonAndPopover.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index b74a98a1a94a..76ef4aff1ab4 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -259,11 +259,8 @@ function FloatingActionButtonAndPopover( showCreateMenu(); } }; - - const selfDMReportID = useMemo(() => { - return ReportUtils.findSelfDMReportID(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isLoading]); + // eslint-disable-next-line react-hooks/exhaustive-deps + const selfDMReportID = useMemo(() => ReportUtils.findSelfDMReportID(), [isLoading]); return ( From bdfbbdbc9289515c3baa36c77e3d8ee82a8134fd Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 18 Apr 2024 12:28:02 +0200 Subject: [PATCH 127/198] fix: prettier --- src/languages/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 4177f16e9c59..cd26914b52ad 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -50,8 +50,8 @@ import type { PayerPaidAmountParams, PayerPaidParams, PayerSettledParams, - ReimbursementRateParams, PaySomeoneParams, + ReimbursementRateParams, RemovedTheRequestParams, RenamedRoomActionParams, ReportArchiveReasonsClosedParams, From a640d416a5e36c4fdf2ddb89557b15886d111451 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 18 Apr 2024 18:16:45 +0530 Subject: [PATCH 128/198] detect cycle in redirects.csv --- .github/scripts/detectRedirectCycle.ts | 57 +++++++++++++++++++++++++- package.json | 1 + 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/.github/scripts/detectRedirectCycle.ts b/.github/scripts/detectRedirectCycle.ts index d0d8ed4eb923..90191f947162 100644 --- a/.github/scripts/detectRedirectCycle.ts +++ b/.github/scripts/detectRedirectCycle.ts @@ -1,4 +1,59 @@ +import {parse} from 'csv-parse'; import fs from 'fs'; -const docsDir = `${process.cwd()}/docs`; +const parser = parse({skip_empty_lines: true}); +const adjacencyList: any = {}; +function addEdge(source: string, target: string) { + if (!adjacencyList[source]) { + adjacencyList[source] = []; + } + adjacencyList[source].push(target); +} + +function isCyclic(currentNode: string, visited: Map, backEdges: Map): boolean { + visited.set(currentNode, true); + backEdges.set(currentNode, true); + + // Do a depth first search for all the neighbours. If a node is found in backedge, a cycle is detected. + const neighbours = adjacencyList[currentNode]; + if (neighbours) { + for (const node of neighbours) { + if (!visited.has(node)) { + if (isCyclic(node, visited, backEdges)) return true; + } else if (backEdges.has(node)) return true; + } + } + + backEdges.delete(currentNode); + + return false; +} + +function detectCycle() { + const visited: Map = new Map(); + const backEdges: Map = new Map(); + + for (let node in adjacencyList) { + if (visited.has(node)) { + continue; + } + if (isCyclic(node, visited, backEdges)) { + const cycle = Array.from(backEdges.keys()); + console.log(`Infinite redirect found in cycle: ${cycle.join(' -> ')} -> ${node}`); + return true; + } + } +} + +fs.createReadStream(`${process.cwd()}/docs/redirects.csv`) + .pipe(parser) + .on('data', async (row) => { + addEdge(row[0], row[1]); + }) + .on('end', async () => { + if (detectCycle()) { + process.exit(1); + } + process.exit(0); + }); diff --git a/package.json b/package.json index fdcf786b52b1..abf4376ab683 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "desktop-build": "scripts/build-desktop.sh production", "desktop-build-staging": "scripts/build-desktop.sh staging", "createDocsRoutes": "ts-node .github/scripts/createDocsRoutes.ts", + "detectRedirectCycle": "ts-node .github/scripts/detectRedirectCycle.ts", "desktop-build-adhoc": "scripts/build-desktop.sh adhoc", "ios-build": "fastlane ios build", "android-build": "fastlane android build", From 104e9e4b6185863a9f747b9988beac6cb49670d5 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 18 Apr 2024 18:24:30 +0530 Subject: [PATCH 129/198] remove the found cycle in redirects.csv --- .github/scripts/detectRedirectCycle.ts | 2 +- docs/redirects.csv | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/scripts/detectRedirectCycle.ts b/.github/scripts/detectRedirectCycle.ts index 90191f947162..d2e13f061eee 100644 --- a/.github/scripts/detectRedirectCycle.ts +++ b/.github/scripts/detectRedirectCycle.ts @@ -40,7 +40,7 @@ function detectCycle() { } if (isCyclic(node, visited, backEdges)) { const cycle = Array.from(backEdges.keys()); - console.log(`Infinite redirect found in cycle: ${cycle.join(' -> ')} -> ${node}`); + console.log(`Infinite redirect found in the cycle: ${cycle.join(' -> ')} -> ${node}`); return true; } } diff --git a/docs/redirects.csv b/docs/redirects.csv index af595ecc5f83..3f90265b5f6b 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -123,7 +123,6 @@ https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/ https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Tax-Exempt,https://help.expensify.com/articles/expensify-classic/expensify-billing/Tax-Exempt https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Approving-Reports,https://help.expensify.com/expensify-classic/hubs/reports/ https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Invite-Members,https://help.expensify.com/articles/expensify-classic/workspaces/Invite-members-and-assign-roles -https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Removing-Members,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Removing-Members https://help.expensify.com/articles/expensify-classic/expense-and-report-features/Attendee-Tracking,https://help.expensify.com/articles/expensify-classic/expenses/Track-group-expenses https://help.expensify.com/articles/expensify-classic/expense-and-report-features/Currency,https://help.expensify.com/articles/expensify-classic/workspaces/Currency https://help.expensify.com/articles/expensify-classic/expense-and-report-features/Expense-Rules,https://help.expensify.com/articles/expensify-classic/expenses/Expense-Rules From 6637db8498cca22940eea69f868bc0fd52095db6 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 18 Apr 2024 18:43:27 +0530 Subject: [PATCH 130/198] add redirects --- docs/redirects.csv | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/redirects.csv b/docs/redirects.csv index 3f90265b5f6b..e442bd32fea9 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -157,3 +157,5 @@ https://help.expensify.com/articles/expensify-classic/workspaces/Budgets,https:/ https://help.expensify.com/articles/expensify-classic/workspaces/Categories,https://help.expensify.com/articles/expensify-classic/workspaces/Create-categories https://help.expensify.com/articles/expensify-classic/workspaces/Tags,https://help.expensify.com/articles/expensify-classic/workspaces/Create-tags https://help.expensify.com/expensify-classic/hubs/manage-employees-and-report-approvals,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Approval-Workflows +https://help.expensify.com/articles/expensify-classic/reports/Report-Audit-Log-and-Comments,https://help.expensify.com/articles/expensify-classic/reports/Print-or-download-a-report +https://help.expensify.com/articles/expensify-classic/reports/The-Reports-Page,https://help.expensify.com/articles/expensify-classic/reports/Report-statuses From 18cf4a82cbd17513db3bebb6c8b11eed4a3966e9 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 18 Apr 2024 15:27:00 +0200 Subject: [PATCH 131/198] Revert "add video to tasks" This reverts commit 52fde4a3350d75b66084e80e69ce560d960a306d. --- src/CONST.ts | 85 ---------------------------------------------------- 1 file changed, 85 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index f96c3087dff7..9d2992859efc 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3623,13 +3623,6 @@ const CONST = { '2. Click Workspaces > New workspace.\n' + '\n' + 'Your new workspace is ready! It’ll keep all of your spend (and chats) in one place.', - video: { - url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, - thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, - duration: 55, - width: 1280, - height: 960, - }, }, { title: 'Track an expense', @@ -3643,13 +3636,6 @@ const CONST = { '4. Click Track.\n' + '\n' + 'And you’re done! Yep, it’s that easy.', - video: { - url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, - thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, - duration: 55, - width: 1280, - height: 960, - }, }, ], }, @@ -3675,13 +3661,6 @@ const CONST = { '4. Add your reimburser to the request.\n' + '\n' + 'Then, send your request and wait for that sweet “Cha-ching!” when it’s complete.', - video: { - url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, - thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, - duration: 55, - width: 1280, - height: 960, - }, }, { title: 'Enable your wallet', @@ -3694,13 +3673,6 @@ const CONST = { '3. Connect your bank account.\n' + '\n' + 'Once that’s done, you can request money from anyone and get paid back right into your personal bank account.', - video: { - url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, - thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, - duration: 55, - width: 1280, - height: 960, - }, }, ], }, @@ -3724,13 +3696,6 @@ const CONST = { '2. Click Workspaces > New workspace.\n' + '\n' + 'Your new workspace is ready! It’ll keep all of your spend (and chats) in one place.', - video: { - url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, - thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, - duration: 55, - width: 1280, - height: 960, - }, }, { title: 'Meet your setup specialist', @@ -3739,7 +3704,6 @@ const CONST = { `Meet your setup specialist, who can answer any questions as you get started with Expensify. Yes, a real human!\n` + '\n' + `Chat with the specialist in your [#admins room](${adminsRoomLink}) or [schedule a call](${guideCalendarLink}) today.`, - video: null, }, { title: 'Set up categories', @@ -3754,13 +3718,6 @@ const CONST = { '5. Click Add categories to make your own.\n' + '\n' + 'For more controls like requiring a category for every expense, click Settings.', - video: { - url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, - thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, - duration: 55, - width: 1280, - height: 960, - }, }, { title: 'Add expense approvals', @@ -3775,13 +3732,6 @@ const CONST = { '5. In Workflows, enable Add approvals.\n' + '\n' + 'You’ll be set as the expense approver. You can change this to any admin once you invite your team.', - video: { - url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, - thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, - duration: 55, - width: 1280, - height: 960, - }, }, { title: 'Invite your team', @@ -3796,13 +3746,6 @@ const CONST = { '5. Add an invite message if you want.\n' + '\n' + 'That’s it! Happy expensing :)', - video: { - url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, - thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, - duration: 55, - width: 1280, - height: 960, - }, }, ], }, @@ -3828,13 +3771,6 @@ const CONST = { '4. Click Track.\n' + '\n' + 'And you’re done! Yep, it’s that easy.', - video: { - url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, - thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, - duration: 55, - width: 1280, - height: 960, - }, }, ], }, @@ -3861,13 +3797,6 @@ const CONST = { 'If any of your friends aren’t using Expensify already, they’ll be invited automatically. \n' + '\n' + 'Every chat will also turn into an email or text that they can respond to directly.', - video: { - url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, - thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, - duration: 55, - width: 1280, - height: 960, - }, }, { title: 'Split an expense', @@ -3881,13 +3810,6 @@ const CONST = { '4. Add your friend(s) to the request.\n' + '\n' + 'Feel free to add more details if you want, or just send it off. Let’s get you paid back!', - video: { - url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, - thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, - duration: 55, - width: 1280, - height: 960, - }, }, { title: 'Enable your wallet', @@ -3900,13 +3822,6 @@ const CONST = { '3. Add your bank account.\n' + '\n' + 'Once that’s done, you can request money from anyone and get paid right into your personal bank account.', - video: { - url: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, - thumbnailUrl: `${CLOUDFRONT_URL}/images/expensify__favicon.png`, - duration: 55, - width: 1280, - height: 960, - }, }, ], }, From 26d8d5e74d1b7042f0f2dbca2c149e088687254c Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 18 Apr 2024 18:57:50 +0530 Subject: [PATCH 132/198] rm async --- .github/scripts/detectRedirectCycle.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/scripts/detectRedirectCycle.ts b/.github/scripts/detectRedirectCycle.ts index d2e13f061eee..cc4d147b5b20 100644 --- a/.github/scripts/detectRedirectCycle.ts +++ b/.github/scripts/detectRedirectCycle.ts @@ -48,10 +48,10 @@ function detectCycle() { fs.createReadStream(`${process.cwd()}/docs/redirects.csv`) .pipe(parser) - .on('data', async (row) => { + .on('data', (row) => { addEdge(row[0], row[1]); }) - .on('end', async () => { + .on('end', () => { if (detectCycle()) { process.exit(1); } From 22795da4f9897f7e3b72b685aad5475a2861d521 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 18 Apr 2024 18:58:11 +0530 Subject: [PATCH 133/198] fix lint --- .github/scripts/detectRedirectCycle.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/scripts/detectRedirectCycle.ts b/.github/scripts/detectRedirectCycle.ts index cc4d147b5b20..a02021b3be55 100644 --- a/.github/scripts/detectRedirectCycle.ts +++ b/.github/scripts/detectRedirectCycle.ts @@ -20,8 +20,12 @@ function isCyclic(currentNode: string, visited: Map, backEdges: if (neighbours) { for (const node of neighbours) { if (!visited.has(node)) { - if (isCyclic(node, visited, backEdges)) return true; - } else if (backEdges.has(node)) return true; + if (isCyclic(node, visited, backEdges)) { + return true; + } + } else if (backEdges.has(node)) { + return true; + } } } From 1972309453d77826d7de1aca81a1a5b053e9a85f Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 18 Apr 2024 18:59:12 +0530 Subject: [PATCH 134/198] fix lint --- .github/scripts/detectRedirectCycle.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/scripts/detectRedirectCycle.ts b/.github/scripts/detectRedirectCycle.ts index a02021b3be55..b4dabaac110f 100644 --- a/.github/scripts/detectRedirectCycle.ts +++ b/.github/scripts/detectRedirectCycle.ts @@ -39,13 +39,12 @@ function detectCycle() { const backEdges: Map = new Map(); for (let node in adjacencyList) { - if (visited.has(node)) { - continue; - } - if (isCyclic(node, visited, backEdges)) { - const cycle = Array.from(backEdges.keys()); - console.log(`Infinite redirect found in the cycle: ${cycle.join(' -> ')} -> ${node}`); - return true; + if (!visited.has(node)) { + if (isCyclic(node, visited, backEdges)) { + const cycle = Array.from(backEdges.keys()); + console.log(`Infinite redirect found in the cycle: ${cycle.join(' -> ')} -> ${node}`); + return true; + } } } } From 32c569cae4726fe5d7fe3aec4f159812e4e82ccc Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 18 Apr 2024 18:59:51 +0530 Subject: [PATCH 135/198] fix lint --- .github/scripts/detectRedirectCycle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/detectRedirectCycle.ts b/.github/scripts/detectRedirectCycle.ts index b4dabaac110f..fdce5b4f080a 100644 --- a/.github/scripts/detectRedirectCycle.ts +++ b/.github/scripts/detectRedirectCycle.ts @@ -38,7 +38,7 @@ function detectCycle() { const visited: Map = new Map(); const backEdges: Map = new Map(); - for (let node in adjacencyList) { + for (const node in adjacencyList) { if (!visited.has(node)) { if (isCyclic(node, visited, backEdges)) { const cycle = Array.from(backEdges.keys()); From 22075e5cbbb79852a8e522543c4c4ab9be34dd6d Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 18 Apr 2024 19:02:02 +0530 Subject: [PATCH 136/198] fix type --- .github/scripts/detectRedirectCycle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/detectRedirectCycle.ts b/.github/scripts/detectRedirectCycle.ts index fdce5b4f080a..28474ce21ab1 100644 --- a/.github/scripts/detectRedirectCycle.ts +++ b/.github/scripts/detectRedirectCycle.ts @@ -2,7 +2,7 @@ import {parse} from 'csv-parse'; import fs from 'fs'; const parser = parse({skip_empty_lines: true}); -const adjacencyList: any = {}; +const adjacencyList: Record> = {}; function addEdge(source: string, target: string) { if (!adjacencyList[source]) { From 033e6c0aa4d1d999b1007254833a8d33a89056a6 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 18 Apr 2024 19:03:07 +0530 Subject: [PATCH 137/198] fix type --- .github/scripts/detectRedirectCycle.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/scripts/detectRedirectCycle.ts b/.github/scripts/detectRedirectCycle.ts index 28474ce21ab1..97a49d131604 100644 --- a/.github/scripts/detectRedirectCycle.ts +++ b/.github/scripts/detectRedirectCycle.ts @@ -34,7 +34,7 @@ function isCyclic(currentNode: string, visited: Map, backEdges: return false; } -function detectCycle() { +function detectCycle(): boolean { const visited: Map = new Map(); const backEdges: Map = new Map(); @@ -47,6 +47,7 @@ function detectCycle() { } } } + return false; } fs.createReadStream(`${process.cwd()}/docs/redirects.csv`) From e430bb08514459e484f313cb182ca5647bf2d717 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 18 Apr 2024 19:10:33 +0530 Subject: [PATCH 138/198] fix lint --- .github/scripts/detectRedirectCycle.ts | 5 +++-- .github/workflows/deployExpensifyHelp.yml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/scripts/detectRedirectCycle.ts b/.github/scripts/detectRedirectCycle.ts index 97a49d131604..ab526d598fef 100644 --- a/.github/scripts/detectRedirectCycle.ts +++ b/.github/scripts/detectRedirectCycle.ts @@ -2,7 +2,7 @@ import {parse} from 'csv-parse'; import fs from 'fs'; const parser = parse({skip_empty_lines: true}); -const adjacencyList: Record> = {}; +const adjacencyList: Record = {}; function addEdge(source: string, target: string) { if (!adjacencyList[source]) { @@ -38,7 +38,7 @@ function detectCycle(): boolean { const visited: Map = new Map(); const backEdges: Map = new Map(); - for (const node in adjacencyList) { + for (const [node, _] of Object.entries(adjacencyList)) { if (!visited.has(node)) { if (isCyclic(node, visited, backEdges)) { const cycle = Array.from(backEdges.keys()); @@ -53,6 +53,7 @@ function detectCycle(): boolean { fs.createReadStream(`${process.cwd()}/docs/redirects.csv`) .pipe(parser) .on('data', (row) => { + // Create a directed graph of sourceURL -> targetURL addEdge(row[0], row[1]); }) .on('end', () => { diff --git a/.github/workflows/deployExpensifyHelp.yml b/.github/workflows/deployExpensifyHelp.yml index f16aab1caaa7..f7f826d66f9b 100644 --- a/.github/workflows/deployExpensifyHelp.yml +++ b/.github/workflows/deployExpensifyHelp.yml @@ -37,7 +37,7 @@ jobs: - name: Create docs routes file run: ./.github/scripts/createDocsRoutes.sh - - name: Check duplicates in redirects.csv + - name: Check for duplicates and cycles in redirects.csv run: ./.github/scripts/verifyRedirect.sh - name: Build with Jekyll From 458bc3503cf12ae8bfea7e55e7562ff8d40080f2 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 18 Apr 2024 19:11:24 +0530 Subject: [PATCH 139/198] fix lint --- .github/scripts/detectRedirectCycle.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/scripts/detectRedirectCycle.ts b/.github/scripts/detectRedirectCycle.ts index ab526d598fef..d78358271261 100644 --- a/.github/scripts/detectRedirectCycle.ts +++ b/.github/scripts/detectRedirectCycle.ts @@ -1,6 +1,7 @@ import {parse} from 'csv-parse'; import fs from 'fs'; +// eslint-disable-next-line @typescript-eslint/naming-convention const parser = parse({skip_empty_lines: true}); const adjacencyList: Record = {}; From 08ee70495cbad141e2d5657c32f2ecae96c8e9cd Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 18 Apr 2024 19:15:14 +0530 Subject: [PATCH 140/198] make script executable --- .github/scripts/verifyRedirect.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/scripts/verifyRedirect.sh b/.github/scripts/verifyRedirect.sh index b8942cd5b23d..3a687391b394 100644 --- a/.github/scripts/verifyRedirect.sh +++ b/.github/scripts/verifyRedirect.sh @@ -24,3 +24,4 @@ fi echo -e "${GREEN}The redirects.csv is valid!${NC}" exit 0 + From da441105403dd6167d63216527e97f5100d0f7e3 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 18 Apr 2024 19:19:51 +0530 Subject: [PATCH 141/198] debug: --- .github/scripts/verifyRedirect.sh | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/scripts/verifyRedirect.sh b/.github/scripts/verifyRedirect.sh index 3a687391b394..79f8f25f3cac 100644 --- a/.github/scripts/verifyRedirect.sh +++ b/.github/scripts/verifyRedirect.sh @@ -9,11 +9,11 @@ declare -r RED='\033[0;31m' declare -r GREEN='\033[0;32m' declare -r NC='\033[0m' -duplicates=$(awk -F, 'a[$1]++{print $1}' $REDIRECTS_FILE) -if [[ -n "$duplicates" ]]; then - echo "${RED}duplicate redirects are not allowed: $duplicates ${NC}" - exit 1 -fi +# duplicates=$(awk -F, 'a[$1]++{print $1}' $REDIRECTS_FILE) +# if [[ -n "$duplicates" ]]; then +# echo "${RED}duplicate redirects are not allowed: $duplicates ${NC}" +# exit 1 +# fi npm run detectRedirectCycle DETECT_CYCLE_EXIT_CODE=$? @@ -24,4 +24,3 @@ fi echo -e "${GREEN}The redirects.csv is valid!${NC}" exit 0 - From bfe2096162498b6835fe8bc9181e9649c25530b1 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 18 Apr 2024 19:21:48 +0530 Subject: [PATCH 142/198] debug: --- .github/scripts/verifyRedirect.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/scripts/verifyRedirect.sh b/.github/scripts/verifyRedirect.sh index 79f8f25f3cac..dfe9492b2112 100644 --- a/.github/scripts/verifyRedirect.sh +++ b/.github/scripts/verifyRedirect.sh @@ -15,12 +15,12 @@ declare -r NC='\033[0m' # exit 1 # fi -npm run detectRedirectCycle -DETECT_CYCLE_EXIT_CODE=$? -if [[ DETECT_CYCLE_EXIT_CODE -eq 1 ]]; then - echo -e "${RED}The redirects.csv has a cycle. Please remove the redirect cycle because it will cause an infinite redirect loop ${NC}" - exit 1 -fi +# npm run detectRedirectCycle +# DETECT_CYCLE_EXIT_CODE=$? +# if [[ DETECT_CYCLE_EXIT_CODE -eq 1 ]]; then +# echo -e "${RED}The redirects.csv has a cycle. Please remove the redirect cycle because it will cause an infinite redirect loop ${NC}" +# exit 1 +# fi echo -e "${GREEN}The redirects.csv is valid!${NC}" exit 0 From 070e9339104cab33bdc14f20f2d9661bdef24786 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 18 Apr 2024 15:54:29 +0200 Subject: [PATCH 143/198] fix mentionHandle --- src/libs/actions/Report.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 1faf850b6d76..b4beb9cb287f 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -56,6 +56,7 @@ import * as EmojiUtils from '@libs/EmojiUtils'; import * as Environment from '@libs/Environment/Environment'; import * as ErrorUtils from '@libs/ErrorUtils'; import Log from '@libs/Log'; +import * as LoginUtils from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import LocalNotification from '@libs/Notification/LocalNotification'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; @@ -3001,7 +3002,8 @@ function completeOnboarding( const {reportID: targetChatReportID = '', policyID: targetChatPolicyID = ''} = targetChatReport ?? {}; // Mention message - const mentionComment = ReportUtils.buildOptimisticAddCommentReportAction(`Hey @${login.split('@')[0]} 👋`, undefined, actorAccountID); + const mentionHandle = LoginUtils.isEmailPublicDomain(login) ? login : login.split('@')[0]; + const mentionComment = ReportUtils.buildOptimisticAddCommentReportAction(`Hey @${mentionHandle} 👋`, undefined, actorAccountID); const mentionCommentAction: OptimisticAddCommentReportAction = mentionComment.reportAction; const mentionMessage: AddCommentOrAttachementParams = { reportID: targetChatReportID, From 0232f44c4abcf3a4843ab8c811e6ee402ea111df Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 18 Apr 2024 19:25:51 +0530 Subject: [PATCH 144/198] provide permission --- .github/scripts/detectRedirectCycle.ts | 0 .github/scripts/verifyRedirect.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .github/scripts/detectRedirectCycle.ts mode change 100644 => 100755 .github/scripts/verifyRedirect.sh diff --git a/.github/scripts/detectRedirectCycle.ts b/.github/scripts/detectRedirectCycle.ts old mode 100644 new mode 100755 diff --git a/.github/scripts/verifyRedirect.sh b/.github/scripts/verifyRedirect.sh old mode 100644 new mode 100755 From 409b1c1c215fb1c16e5c0a075b834cccbebf1f71 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 18 Apr 2024 19:26:27 +0530 Subject: [PATCH 145/198] provide permission --- .github/scripts/detectRedirectCycle.ts | 0 .github/scripts/verifyRedirect.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 .github/scripts/detectRedirectCycle.ts mode change 100755 => 100644 .github/scripts/verifyRedirect.sh diff --git a/.github/scripts/detectRedirectCycle.ts b/.github/scripts/detectRedirectCycle.ts old mode 100755 new mode 100644 diff --git a/.github/scripts/verifyRedirect.sh b/.github/scripts/verifyRedirect.sh old mode 100755 new mode 100644 From eb6c38eda2b01f3fd1704a0eaf6280639a3631bf Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 18 Apr 2024 19:28:49 +0530 Subject: [PATCH 146/198] delete file --- .github/scripts/verifyRedirect.sh | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 .github/scripts/verifyRedirect.sh diff --git a/.github/scripts/verifyRedirect.sh b/.github/scripts/verifyRedirect.sh deleted file mode 100644 index dfe9492b2112..000000000000 --- a/.github/scripts/verifyRedirect.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -# HelpDot - Verifies that redirects.csv does not have any duplicates -# Duplicate sourceURLs break redirection on cloudflare pages - -declare -r REDIRECTS_FILE="docs/redirects.csv" - -declare -r RED='\033[0;31m' -declare -r GREEN='\033[0;32m' -declare -r NC='\033[0m' - -# duplicates=$(awk -F, 'a[$1]++{print $1}' $REDIRECTS_FILE) -# if [[ -n "$duplicates" ]]; then -# echo "${RED}duplicate redirects are not allowed: $duplicates ${NC}" -# exit 1 -# fi - -# npm run detectRedirectCycle -# DETECT_CYCLE_EXIT_CODE=$? -# if [[ DETECT_CYCLE_EXIT_CODE -eq 1 ]]; then -# echo -e "${RED}The redirects.csv has a cycle. Please remove the redirect cycle because it will cause an infinite redirect loop ${NC}" -# exit 1 -# fi - -echo -e "${GREEN}The redirects.csv is valid!${NC}" -exit 0 From 08e2545fb1ef2008d4c2e3caf44334cfd598b12a Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 18 Apr 2024 19:30:58 +0530 Subject: [PATCH 147/198] add file as executable --- .github/scripts/verifyRedirect.sh | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100755 .github/scripts/verifyRedirect.sh diff --git a/.github/scripts/verifyRedirect.sh b/.github/scripts/verifyRedirect.sh new file mode 100755 index 000000000000..b8942cd5b23d --- /dev/null +++ b/.github/scripts/verifyRedirect.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# HelpDot - Verifies that redirects.csv does not have any duplicates +# Duplicate sourceURLs break redirection on cloudflare pages + +declare -r REDIRECTS_FILE="docs/redirects.csv" + +declare -r RED='\033[0;31m' +declare -r GREEN='\033[0;32m' +declare -r NC='\033[0m' + +duplicates=$(awk -F, 'a[$1]++{print $1}' $REDIRECTS_FILE) +if [[ -n "$duplicates" ]]; then + echo "${RED}duplicate redirects are not allowed: $duplicates ${NC}" + exit 1 +fi + +npm run detectRedirectCycle +DETECT_CYCLE_EXIT_CODE=$? +if [[ DETECT_CYCLE_EXIT_CODE -eq 1 ]]; then + echo -e "${RED}The redirects.csv has a cycle. Please remove the redirect cycle because it will cause an infinite redirect loop ${NC}" + exit 1 +fi + +echo -e "${GREEN}The redirects.csv is valid!${NC}" +exit 0 From 2b5015fd36b39668a8ee15f0319e0c53bfb53eeb Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 18 Apr 2024 19:45:06 +0530 Subject: [PATCH 148/198] fix lint --- .github/scripts/detectRedirectCycle.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/scripts/detectRedirectCycle.ts b/.github/scripts/detectRedirectCycle.ts index d78358271261..220f59f9c170 100644 --- a/.github/scripts/detectRedirectCycle.ts +++ b/.github/scripts/detectRedirectCycle.ts @@ -1,8 +1,10 @@ import {parse} from 'csv-parse'; import fs from 'fs'; -// eslint-disable-next-line @typescript-eslint/naming-convention -const parser = parse({skip_empty_lines: true}); +const parser = parse({ + // eslint-disable-next-line @typescript-eslint/naming-convention + skip_empty_lines: true, +}); const adjacencyList: Record = {}; function addEdge(source: string, target: string) { @@ -39,7 +41,7 @@ function detectCycle(): boolean { const visited: Map = new Map(); const backEdges: Map = new Map(); - for (const [node, _] of Object.entries(adjacencyList)) { + for (const [node] of Object.entries(adjacencyList)) { if (!visited.has(node)) { if (isCyclic(node, visited, backEdges)) { const cycle = Array.from(backEdges.keys()); From 3295539764f4c8a8b93172c02b9f1d1f19b8771f Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 18 Apr 2024 19:46:35 +0530 Subject: [PATCH 149/198] fix lint --- .github/scripts/detectRedirectCycle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/detectRedirectCycle.ts b/.github/scripts/detectRedirectCycle.ts index 220f59f9c170..3443ad746709 100644 --- a/.github/scripts/detectRedirectCycle.ts +++ b/.github/scripts/detectRedirectCycle.ts @@ -2,7 +2,7 @@ import {parse} from 'csv-parse'; import fs from 'fs'; const parser = parse({ - // eslint-disable-next-line @typescript-eslint/naming-convention + // eslint-disable-next-line @typescript-eslint/camelcase skip_empty_lines: true, }); const adjacencyList: Record = {}; From 1f2abbd03c82e00a6a965b2366d81103de124eb0 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Thu, 18 Apr 2024 19:56:21 +0530 Subject: [PATCH 150/198] remove: skip empty lines --- .github/scripts/detectRedirectCycle.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/scripts/detectRedirectCycle.ts b/.github/scripts/detectRedirectCycle.ts index 3443ad746709..9df86d28fc6f 100644 --- a/.github/scripts/detectRedirectCycle.ts +++ b/.github/scripts/detectRedirectCycle.ts @@ -1,10 +1,7 @@ import {parse} from 'csv-parse'; import fs from 'fs'; -const parser = parse({ - // eslint-disable-next-line @typescript-eslint/camelcase - skip_empty_lines: true, -}); +const parser = parse(); const adjacencyList: Record = {}; function addEdge(source: string, target: string) { From aa4301fc6ee8d56377a05b792269aef1b3ea1067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Thu, 18 Apr 2024 16:42:39 +0200 Subject: [PATCH 151/198] fix --- src/components/Form/FormWrapper.tsx | 29 ++++++++++--------- .../request/step/IOURequestStepMerchant.tsx | 1 + 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 902a96b1bcaf..1ba633e5c9fe 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -102,7 +102,7 @@ function FormWrapper({ {children} {isSubmitButtonVisible && ( @@ -123,26 +123,27 @@ function FormWrapper({ ), [ - children, - enabledWhenOffline, - errorMessage, - errors, - footerContent, formID, - formState?.errorFields, - formState?.isLoading, - isSubmitActionDangerous, - isSubmitButtonVisible, - onSubmit, style, - styles.flex1, + styles.pb5, styles.mh0, styles.mt5, - submitButtonStyles, - submitFlexEnabled, + styles.flex1, + children, + isSubmitButtonVisible, submitButtonText, + errors, + formState?.errorFields, + formState?.isLoading, shouldHideFixErrorsAlert, + errorMessage, + onSubmit, + footerContent, onFixTheErrorsLinkPressed, + submitFlexEnabled, + submitButtonStyles, + enabledWhenOffline, + isSubmitActionDangerous, disablePressOnEnter, ], ); diff --git a/src/pages/iou/request/step/IOURequestStepMerchant.tsx b/src/pages/iou/request/step/IOURequestStepMerchant.tsx index d731f56614a5..b50495ac47bd 100644 --- a/src/pages/iou/request/step/IOURequestStepMerchant.tsx +++ b/src/pages/iou/request/step/IOURequestStepMerchant.tsx @@ -111,6 +111,7 @@ function IOURequestStepMerchant({ onBackButtonPress={navigateBack} shouldShowWrapper testID={IOURequestStepMerchant.displayName} + includeSafeAreaPaddingBottom > Date: Thu, 18 Apr 2024 21:08:13 +0530 Subject: [PATCH 152/198] fix: Submit Expense - Error message about corrupted file when dragging and dropping image receipt. Signed-off-by: Krishna Gupta --- src/pages/iou/request/step/IOURequestStepScan/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index 654f9e9d9f91..b44aa633d27f 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -481,6 +481,7 @@ function IOURequestStepScan({ onDrop={(e) => { const file = e?.dataTransfer?.files[0]; if (file) { + file.uri = URL.createObjectURL(file); setReceiptAndNavigate(file); } }} From 41e7786f101eb0ebf954c6728d24b57dbc93958f Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 18 Apr 2024 17:42:41 +0200 Subject: [PATCH 153/198] fix link of message --- src/libs/actions/Report.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index b4beb9cb287f..9f6a6cfb7468 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3063,7 +3063,7 @@ function completeOnboarding( guideCalendarLink: guideCalendarLink ?? CONFIG.EXPENSIFY.NEW_EXPENSIFY_URL, }) : task.message; - const instructionComment = ReportUtils.buildOptimisticAddCommentReportAction(taskMessage, undefined, actorAccountID, 1, false); + const instructionComment = ReportUtils.buildOptimisticAddCommentReportAction(taskMessage, undefined, actorAccountID, 1); return { currentTask, From 5229462382bb70d6b3d49f05f131d58f81bf2075 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 18 Apr 2024 19:06:41 +0200 Subject: [PATCH 154/198] fix: add missing export --- src/libs/actions/IOU.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index f6cee42538fe..2ba6feea635f 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -6039,6 +6039,7 @@ export { submitReport, trackExpense, unholdRequest, + updateDistanceRequestRate, updateMoneyRequestAmountAndCurrency, updateMoneyRequestBillable, updateMoneyRequestCategory, From 60f2822b4aabf19a4142d996b0660d169bafa58f Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Fri, 19 Apr 2024 01:03:31 +0700 Subject: [PATCH 155/198] revert hard code --- src/libs/Permissions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 80916a1c7bb2..105736faeba0 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -4,7 +4,6 @@ import type {IOUType} from '@src/CONST'; import type Beta from '@src/types/onyx/Beta'; function canUseAllBetas(betas: OnyxEntry): boolean { - return true; return !!betas?.includes(CONST.BETAS.ALL); } From 9c8fbdf0ea9bc2911e94df237ae12b473e3df11b Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 18 Apr 2024 20:49:20 +0200 Subject: [PATCH 156/198] fix: minor fix --- .../MoneyTemporaryForRefactorRequestConfirmationList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 0701f2ff1db4..b9b657988712 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -255,7 +255,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const prevRate = usePrevious(rate); const shouldCalculateDistanceAmount = isDistanceRequest && (iouAmount === 0 || prevRate !== rate); - const currency = (mileageRate as MileageRate).currency ?? policyCurrency; + const currency = (mileageRate as MileageRate)?.currency ?? policyCurrency; const distance = transaction?.routes?.route0?.distance ?? 0; const taxRates = policy?.taxRates ?? null; From 63c5eddc276135c799d6404199b3567e89db76cc Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 18 Apr 2024 20:56:35 +0200 Subject: [PATCH 157/198] fix: copy temporary confirmation list content to the original file --- .../MoneyRequestConfirmationList.tsx | 980 ++++++++++-------- 1 file changed, 565 insertions(+), 415 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index fa6af89e5bfd..b9b657988712 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -1,13 +1,16 @@ import {useIsFocused} from '@react-navigation/native'; import {format} from 'date-fns'; -import React, {useCallback, useEffect, useMemo, useReducer, useState} from 'react'; +import Str from 'expensify-common/lib/str'; +import React, {useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import usePermissions from '@hooks/usePermissions'; +import usePrevious from '@hooks/usePrevious'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; @@ -19,11 +22,14 @@ import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; +import {isTaxTrackingEnabled} from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import playSound, {SOUNDS} from '@libs/Sound'; import * as TransactionUtils from '@libs/TransactionUtils'; +import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import * as IOU from '@userActions/IOU'; -import type {IOUType} from '@src/CONST'; +import type {IOUAction, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; @@ -31,17 +37,19 @@ import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; -import type {ReceiptSource} from '@src/types/onyx/Transaction'; +import Button from './Button'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; import type {DropdownOption} from './ButtonWithDropdownMenu/types'; import ConfirmedRoute from './ConfirmedRoute'; +import ConfirmModal from './ConfirmModal'; import FormHelpMessage from './FormHelpMessage'; +import * as Expensicons from './Icon/Expensicons'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import OptionsSelector from './OptionsSelector'; +import PDFThumbnail from './PDFThumbnail'; import ReceiptEmptyState from './ReceiptEmptyState'; import ReceiptImage from './ReceiptImage'; import SettlementButton from './SettlementButton'; -import ShowMoreButton from './ShowMoreButton'; import Switch from './Switch'; import Text from './Text'; @@ -55,21 +63,25 @@ type MoneyRequestConfirmationListOnyxProps = { /** The policy of the report */ policy: OnyxEntry; - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - iou: OnyxEntry; - /** The session of the logged in user */ session: OnyxEntry; - /** Unit and rate used for if the money request is a distance request */ - mileageRate: OnyxEntry; + /** Unit and rate used for if the expense is a distance request */ + mileageRates: OnyxEntry>; + + /** Mileage rate default for the policy */ + defaultMileageRate: OnyxEntry; + + /** Last selected distance rates */ + lastSelectedDistanceRates: OnyxEntry>; }; + type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & { /** Callback to inform parent modal of success */ - onConfirm?: (selectedParticipants: Array) => void; + onConfirm?: (selectedParticipants: Participant[]) => void; - /** Callback to parent modal to send money */ - onSendMoney?: (paymentMethod: IOUType | PaymentMethodType | undefined) => void; + /** Callback to parent modal to pay someone */ + onSendMoney?: (paymentMethod: PaymentMethodType | undefined) => void; /** Callback to inform a participant is selected */ onSelectParticipant?: (option: Participant) => void; @@ -98,9 +110,6 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & /** IOU Category */ iouCategory?: string; - /** IOU Tag */ - iouTag?: string; - /** IOU isBillable */ iouIsBillable?: boolean; @@ -108,10 +117,10 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & onToggleBillable?: (isOn: boolean) => void; /** Selected participants from MoneyRequestModal with login / accountID */ - selectedParticipants: Array; + selectedParticipants: Participant[]; - /** Payee of the money request with login */ - payeePersonalDetails?: OnyxEntry; + /** Payee of the expense with login */ + payeePersonalDetails?: OnyxTypes.PersonalDetails; /** Can the participants be modified or not */ canModifyParticipants?: boolean; @@ -129,7 +138,7 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & reportID?: string; /** File path of the receipt */ - receiptPath?: ReceiptSource; + receiptPath?: string; /** File name of the receipt */ receiptFilename?: string; @@ -137,19 +146,16 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & /** List styles for OptionsSelector */ listStyles?: StyleProp; - /** ID of the transaction that represents the money request */ - transactionID?: string; - - /** Transaction that represents the money request */ + /** Transaction that represents the expense */ transaction?: OnyxEntry; - /** Whether the money request is a distance request */ + /** Whether the expense is a distance expense */ isDistanceRequest?: boolean; - /** Whether the money request is a scan request */ + /** Whether the expense is a scan expense */ isScanRequest?: boolean; - /** Whether we're editing a split bill */ + /** Whether we're editing a split expense */ isEditingSplitBill?: boolean; /** Whether we should show the amount, date, and merchant fields. */ @@ -161,11 +167,18 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & /** Whether smart scan failed */ hasSmartScanFailed?: boolean; - /** The ID of the report action */ reportActionID?: string; + + action?: IOUAction; }; -function MoneyRequestConfirmationList({ +const getTaxAmount = (transaction: OnyxEntry, defaultTaxValue: string) => { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const percentage = (transaction?.taxRate ? transaction?.taxRate?.data?.value : defaultTaxValue) || ''; + return TransactionUtils.calculateTaxAmount(percentage, transaction?.amount ?? 0); +}; + +function MoneyTemporaryForRefactorRequestConfirmationList({ transaction = null, onSendMoney, onConfirm, @@ -174,7 +187,7 @@ function MoneyRequestConfirmationList({ isScanRequest = false, iouAmount, policyCategories, - mileageRate, + mileageRates, isDistanceRequest = false, policy, isPolicyExpenseChat = false, @@ -185,22 +198,9 @@ function MoneyRequestConfirmationList({ iouCurrencyCode, iouMerchant, hasMultipleParticipants, - selectedParticipants: selectedParticipantsProp, - payeePersonalDetails: payeePersonalDetailsProp, - iou = { - id: '', - amount: 0, - currency: CONST.CURRENCY.USD, - comment: '', - merchant: '', - category: '', - tag: '', - billable: false, - created: '', - participants: [], - receiptPath: '', - }, - canModifyParticipants: canModifyParticipantsProp = false, + selectedParticipants: pickedParticipants, + payeePersonalDetails, + canModifyParticipants = false, session, isReadOnly = false, bankAccountRoute = '', @@ -213,69 +213,88 @@ function MoneyRequestConfirmationList({ iouCreated, iouIsBillable = false, onToggleBillable, - iouTag = '', - transactionID = '', hasSmartScanFailed, reportActionID, + defaultMileageRate, + lastSelectedDistanceRates, + action = CONST.IOU.ACTION.CREATE, }: MoneyRequestConfirmationListProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); - const {canUseViolations} = usePermissions(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const {canUseP2PDistanceRequests, canUseViolations} = usePermissions(iouType); + const {isOffline} = useNetwork(); const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST; - const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; + const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT; const isTypeSend = iouType === CONST.IOU.TYPE.SEND; + const isTypeTrackExpense = iouType === CONST.IOU.TYPE.TRACK_EXPENSE; - const isSplitWithScan = isSplitBill && isScanRequest; + const transactionID = transaction?.transactionID ?? ''; + const customUnitRateID = TransactionUtils.getRateID(transaction) ?? ''; - const {unit, rate, currency} = mileageRate ?? { - unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, - rate: 0, - currency: CONST.CURRENCY.USD, - }; - const distance = transaction?.routes?.route0.distance ?? 0; - const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0; - const taxRates = policy?.taxRates; + useEffect(() => { + if (customUnitRateID || !canUseP2PDistanceRequests) { + return; + } + if (!customUnitRateID) { + const rateID = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? defaultMileageRate?.customUnitRateID ?? ''; + IOU.setCustomUnitRateID(transactionID, rateID); + } + }, [defaultMileageRate, customUnitRateID, lastSelectedDistanceRates, policy?.id, canUseP2PDistanceRequests, transactionID]); + + const policyCurrency = policy?.outputCurrency ?? PolicyUtils.getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD; + + const mileageRate = TransactionUtils.isCustomUnitRateIDForP2P(transaction) + ? DistanceRequestUtils.getRateForP2P(policyCurrency) + : mileageRates?.[customUnitRateID] ?? DistanceRequestUtils.getDefaultMileageRate(policy); + + const {unit, rate} = mileageRate ?? {}; + + const prevRate = usePrevious(rate); + const shouldCalculateDistanceAmount = isDistanceRequest && (iouAmount === 0 || prevRate !== rate); + + const currency = (mileageRate as MileageRate)?.currency ?? policyCurrency; + + const distance = transaction?.routes?.route0?.distance ?? 0; + const taxRates = policy?.taxRates ?? null; // A flag for showing the categories field const shouldShowCategories = isPolicyExpenseChat && (!!iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); + // A flag and a toggler for showing the rest of the form fields const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); - // Do not hide fields in case of send money request - const shouldShowAllFields = !!isDistanceRequest || shouldExpandFields || !shouldShowSmartScanFields || isTypeSend || isEditingSplitBill; + // Do not hide fields in case of paying someone + const shouldShowAllFields = isDistanceRequest || shouldExpandFields || !shouldShowSmartScanFields || isTypeSend || isEditingSplitBill; - // In Send Money and Split Bill with Scan flow, we don't allow the Merchant or Date to be edited. For distance requests, don't show the merchant as there's already another "Distance" menu item - const shouldShowDate = shouldShowAllFields && !isTypeSend && !isSplitWithScan; - const shouldShowMerchant = shouldShowAllFields && !isTypeSend && !isDistanceRequest && !isSplitWithScan; + const shouldShowDate = (shouldShowSmartScanFields || isDistanceRequest) && !isTypeSend; + const shouldShowMerchant = shouldShowSmartScanFields && !isDistanceRequest && !isTypeSend; const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); // A flag for showing the tags field - const shouldShowTags = isPolicyExpenseChat && (!!iouTag || OptionsListUtils.hasEnabledTags(policyTagLists)); + const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]); - // A flag for showing tax fields - tax rate and tax amount - const shouldShowTax = isPolicyExpenseChat && (policy?.tax?.trackingEnabled ?? policy?.isTaxTrackingEnabled); + // A flag for showing tax rate + const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy); // A flag for showing the billable field - const shouldShowBillable = !(policy?.disabledFields?.defaultBillable ?? true); - + const shouldShowBillable = policy?.disabledFields?.defaultBillable === false; + const isMovingTransactionFromTrackExpense = IOUUtils.isMovingTransactionFromTrackExpense(action); const hasRoute = TransactionUtils.hasRoute(transaction, isDistanceRequest); - const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate); + const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate) && !isMovingTransactionFromTrackExpense; const formattedAmount = isDistanceRequestWithPendingRoute ? '' : CurrencyUtils.convertToDisplayString( - shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate ?? 0) : iouAmount, + shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate ?? 0) : iouAmount, isDistanceRequest ? currency : iouCurrencyCode, ); const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode); + const taxRateTitle = taxRates && transaction ? TransactionUtils.getDefaultTaxName(taxRates, transaction) : ''; - const defaultTaxKey = taxRates?.defaultExternalID; - const defaultTaxName = (defaultTaxKey && `${taxRates.taxes[defaultTaxKey].name} (${taxRates.taxes[defaultTaxKey].value}) ${CONST.DOT_SEPARATOR} ${translate('common.default')}`) ?? ''; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing is not working when a left hand side value is '' - const taxRateTitle = transaction?.taxRate?.text || defaultTaxName; + const previousTransactionAmount = usePrevious(transaction?.amount); const isFocused = useIsFocused(); const [formError, setFormError] = useState(''); @@ -283,6 +302,14 @@ function MoneyRequestConfirmationList({ const [didConfirm, setDidConfirm] = useState(false); const [didConfirmSplit, setDidConfirmSplit] = useState(false); + const [merchantError, setMerchantError] = useState(false); + + const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false); + + const navigateBack = () => { + Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); + }; + const shouldDisplayFieldError: boolean = useMemo(() => { if (!isEditingSplitBill) { return false; @@ -292,60 +319,89 @@ function MoneyRequestConfirmationList({ }, [isEditingSplitBill, hasSmartScanFailed, transaction, didConfirmSplit]); const isMerchantEmpty = !iouMerchant || iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; - const shouldDisplayMerchantError = isPolicyExpenseChat && shouldDisplayFieldError && isMerchantEmpty; + const isMerchantRequired = isPolicyExpenseChat && !isScanRequest && shouldShowMerchant; + + const isCategoryRequired = canUseViolations && !!policy?.requiresCategory; useEffect(() => { - if (shouldDisplayFieldError && didConfirmSplit) { - setFormError('iou.error.genericSmartscanFailureMessage'); + if ((!isMerchantRequired && isMerchantEmpty) || !merchantError) { return; } + if (!isMerchantEmpty && merchantError) { + setMerchantError(false); + if (formError === 'iou.error.invalidMerchant') { + setFormError(''); + } + } + }, [formError, isMerchantEmpty, merchantError, isMerchantRequired]); + + useEffect(() => { if (shouldDisplayFieldError && hasSmartScanFailed) { setFormError('iou.receiptScanningFailed'); return; } + if (shouldDisplayFieldError && didConfirmSplit) { + setFormError('iou.error.genericSmartscanFailureMessage'); + return; + } + if (merchantError) { + setFormError('iou.error.invalidMerchant'); + return; + } // reset the form error whenever the screen gains or loses focus setFormError(''); - }, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit]); + }, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit, isMerchantRequired, merchantError]); useEffect(() => { if (!shouldCalculateDistanceAmount) { return; } - const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate ?? 0); - IOU.setMoneyRequestAmount(amount); - }, [shouldCalculateDistanceAmount, distance, rate, unit]); + const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate ?? 0); + IOU.setMoneyRequestAmount_temporaryForRefactor(transactionID, amount, currency ?? ''); + }, [shouldCalculateDistanceAmount, distance, rate, unit, transaction, currency, transactionID]); + + // Calculate and set tax amount in transaction draft + useEffect(() => { + const taxAmount = getTaxAmount(transaction, taxRates?.defaultValue ?? '').toString(); + const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount)); + + if (transaction?.taxAmount && previousTransactionAmount === transaction?.amount) { + return IOU.setMoneyRequestTaxAmount(transactionID, transaction?.taxAmount, true); + } + + IOU.setMoneyRequestTaxAmount(transactionID, amountInSmallestCurrencyUnits, true); + }, [taxRates?.defaultValue, transaction, previousTransactionAmount, transactionID]); /** * Returns the participants with amount */ const getParticipantsWithAmount = useCallback( - (participantsList: Array): Array => { - const calculatedIouAmount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode ?? ''); - return OptionsListUtils.getIOUConfirmationOptionsFromParticipants( - participantsList, - iouAmount > 0 ? CurrencyUtils.convertToDisplayString(calculatedIouAmount, iouCurrencyCode) : '', - ); + (participantsList: Participant[]) => { + const amount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode ?? ''); + return OptionsListUtils.getIOUConfirmationOptionsFromParticipants(participantsList, amount > 0 ? CurrencyUtils.convertToDisplayString(amount, iouCurrencyCode) : ''); }, [iouAmount, iouCurrencyCode], ); - // If completing a split bill fails, set didConfirm to false to allow the user to edit the fields again + // If completing a split expense fails, set didConfirm to false to allow the user to edit the fields again if (isEditingSplitBill && didConfirm) { setDidConfirm(false); } - const splitOrRequestOptions: Array> = useMemo(() => { + const splitOrRequestOptions: Array> = useMemo(() => { let text; - if (isSplitBill && iouAmount === 0) { - text = translate('iou.split'); - } else if ((!!receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { - text = translate('iou.expense'); + if (isTypeTrackExpense) { + text = translate('iou.trackExpense'); + } else if (isTypeSplit && iouAmount === 0) { + text = translate('iou.splitExpense'); + } else if ((receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { + text = translate('iou.submitExpense'); if (iouAmount !== 0) { text = translate('iou.submitAmount', {amount: formattedAmount}); } } else { - const translationKey = isSplitBill ? 'iou.splitAmount' : 'iou.submitAmount'; + const translationKey = isTypeSplit ? 'iou.splitAmount' : 'iou.submitAmount'; text = translate(translationKey, {amount: formattedAmount}); } return [ @@ -354,19 +410,19 @@ function MoneyRequestConfirmationList({ value: iouType, }, ]; - }, [isSplitBill, isTypeRequest, iouType, iouAmount, receiptPath, formattedAmount, isDistanceRequestWithPendingRoute, translate]); + }, [isTypeTrackExpense, isTypeSplit, iouAmount, receiptPath, isTypeRequest, isDistanceRequestWithPendingRoute, iouType, translate, formattedAmount]); - const selectedParticipants: Array = useMemo( - () => selectedParticipantsProp.filter((participant) => participant.selected), - [selectedParticipantsProp], - ); - const payeePersonalDetails = useMemo(() => payeePersonalDetailsProp ?? currentUserPersonalDetails, [payeePersonalDetailsProp, currentUserPersonalDetails]); - const canModifyParticipants = !isReadOnly && canModifyParticipantsProp && hasMultipleParticipants; - const shouldDisablePaidBySection = canModifyParticipants; + const selectedParticipants = useMemo(() => pickedParticipants.filter((participant) => participant.selected), [pickedParticipants]); + const personalDetailsOfPayee = useMemo(() => payeePersonalDetails ?? currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]); + const userCanModifyParticipants = useRef(!isReadOnly && canModifyParticipants && hasMultipleParticipants); + useEffect(() => { + userCanModifyParticipants.current = !isReadOnly && canModifyParticipants && hasMultipleParticipants; + }, [isReadOnly, canModifyParticipants, hasMultipleParticipants]); + const shouldDisablePaidBySection = userCanModifyParticipants.current; - const optionSelectorSections: OptionsListUtils.CategorySection[] = useMemo(() => { + const optionSelectorSections = useMemo(() => { const sections = []; - const unselectedParticipants = selectedParticipantsProp.filter((participant) => !participant.selected); + const unselectedParticipants = pickedParticipants.filter((participant) => !participant.selected); if (hasMultipleParticipants) { const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants); let formattedParticipantsList = [...new Set([...formattedSelectedParticipants, ...unselectedParticipants])]; @@ -380,7 +436,7 @@ function MoneyRequestConfirmationList({ const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode ?? '', true); const formattedPayeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail( - payeePersonalDetails, + personalDetailsOfPayee, iouAmount > 0 ? CurrencyUtils.convertToDisplayString(myIOUAmount, iouCurrencyCode) : '', ); @@ -398,9 +454,9 @@ function MoneyRequestConfirmationList({ }, ); } else { - const formattedSelectedParticipants = selectedParticipantsProp.map((participant) => ({ + const formattedSelectedParticipants = selectedParticipants.map((participant) => ({ ...participant, - isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), + isDisabled: !participant.isPolicyExpenseChat && !participant.isSelfDM && ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), })); sections.push({ title: translate('common.to'), @@ -411,26 +467,26 @@ function MoneyRequestConfirmationList({ return sections; }, [ selectedParticipants, + pickedParticipants, hasMultipleParticipants, iouAmount, iouCurrencyCode, getParticipantsWithAmount, - selectedParticipantsProp, - payeePersonalDetails, + personalDetailsOfPayee, translate, shouldDisablePaidBySection, canModifyParticipants, ]); - const selectedOptions: Array = useMemo(() => { + const selectedOptions = useMemo(() => { if (!hasMultipleParticipants) { return []; } - return [...selectedParticipants, OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetails)]; - }, [selectedParticipants, hasMultipleParticipants, payeePersonalDetails]); + return [...selectedParticipants, OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetailsOfPayee)]; + }, [selectedParticipants, hasMultipleParticipants, personalDetailsOfPayee]); useEffect(() => { - if (!isDistanceRequest) { + if (!isDistanceRequest || isMovingTransactionFromTrackExpense) { return; } @@ -441,10 +497,39 @@ function MoneyRequestConfirmationList({ */ IOU.setMoneyRequestPendingFields(transactionID, {waypoints: isDistanceRequestWithPendingRoute ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null}); - const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate ?? 0, currency ?? CONST.CURRENCY.USD, translate, toLocaleDigit); - IOU.setMoneyRequestMerchant(transactionID, distanceMerchant, false); - }, [isDistanceRequestWithPendingRoute, hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, isDistanceRequest, transactionID]); + const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate ?? 0, currency ?? 'USD', translate, toLocaleDigit); + IOU.setMoneyRequestMerchant(transactionID, distanceMerchant, true); + }, [isDistanceRequestWithPendingRoute, hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, isDistanceRequest, transactionID, isMovingTransactionFromTrackExpense]); + // Auto select the category if there is only one enabled category and it is required + useEffect(() => { + const enabledCategories = Object.values(policyCategories ?? {}).filter((category) => category.enabled); + if (iouCategory || !shouldShowCategories || enabledCategories.length !== 1 || !isCategoryRequired) { + return; + } + IOU.setMoneyRequestCategory(transactionID, enabledCategories[0].name); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [iouCategory, shouldShowCategories, policyCategories, isCategoryRequired]); + + // Auto select the tag if there is only one enabled tag and it is required + useEffect(() => { + let updatedTagsString = TransactionUtils.getTag(transaction); + policyTagLists.forEach((tagList, index) => { + const enabledTags = Object.values(tagList.tags).filter((tag) => tag.enabled); + const isTagListRequired = tagList.required === undefined ? false : tagList.required && canUseViolations; + if (!isTagListRequired || enabledTags.length !== 1 || TransactionUtils.getTag(transaction, index)) { + return; + } + updatedTagsString = IOUUtils.insertTagIntoTransactionTagsString(updatedTagsString, enabledTags[0] ? enabledTags[0].name : '', index); + }); + if (updatedTagsString !== TransactionUtils.getTag(transaction) && updatedTagsString) { + IOU.setMoneyRequestTag(transactionID, updatedTagsString); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [policyTagLists, policyTags, canUseViolations]); + + /** + */ const selectParticipant = useCallback( (option: Participant) => { // Return early if selected option is currently logged in user. @@ -460,24 +545,33 @@ function MoneyRequestConfirmationList({ * Navigate to report details or profile of selected user */ const navigateToReportOrUserDetail = (option: ReportUtils.OptionData) => { - if (option.accountID) { - const activeRoute = Navigation.getActiveRouteWithoutParams(); + const activeRoute = Navigation.getActiveRouteWithoutParams(); + + if (option.isSelfDM) { + Navigation.navigate(ROUTES.PROFILE.getRoute(currentUserPersonalDetails.accountID, activeRoute)); + return; + } + if (option.accountID) { Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, activeRoute)); } else if (option.reportID) { - Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(option.reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(option.reportID, activeRoute)); } }; + /** + * @param {String} paymentMethod + */ const confirm = useCallback( - (paymentMethod: IOUType | PaymentMethodType | undefined) => { - if (!selectedParticipants.length) { + (paymentMethod: PaymentMethodType | undefined) => { + if (selectedParticipants.length === 0) { return; } - if (iouCategory.length > CONST.API_TRANSACTION_CATEGORY_MAX_LENGTH) { - setFormError('iou.error.invalidCategoryLength'); + if ((isMerchantRequired && isMerchantEmpty) || (shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction ?? null))) { + setMerchantError(true); return; } + if (iouType === CONST.IOU.TYPE.SEND) { if (!paymentMethod) { return; @@ -488,34 +582,38 @@ function MoneyRequestConfirmationList({ Log.info(`[IOU] Sending money via: ${paymentMethod}`); onSendMoney?.(paymentMethod); } else { - // validate the amount for distance requests + // validate the amount for distance expenses const decimals = CurrencyUtils.getCurrencyDecimals(iouCurrencyCode); if (isDistanceRequest && !isDistanceRequestWithPendingRoute && !MoneyRequestUtils.validateAmount(String(iouAmount), decimals)) { setFormError('common.error.invalidAmount'); return; } - if (isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction)) { + if (isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction ?? null)) { setDidConfirmSplit(true); + setFormError('iou.error.genericSmartscanFailureMessage'); return; } + playSound(SOUNDS.DONE); setDidConfirm(true); onConfirm?.(selectedParticipants); } }, [ selectedParticipants, - onSendMoney, - onConfirm, - isEditingSplitBill, + isMerchantRequired, + isMerchantEmpty, + shouldDisplayFieldError, + transaction, iouType, + onSendMoney, + iouCurrencyCode, isDistanceRequest, - iouCategory, isDistanceRequestWithPendingRoute, - iouCurrencyCode, iouAmount, - transaction, + isEditingSplitBill, + onConfirm, ], ); @@ -534,6 +632,7 @@ function MoneyRequestConfirmationList({ onPress={confirm} enablePaymentsRoute={ROUTES.IOU_SEND_ENABLE_PAYMENTS} addBankAccountRoute={bankAccountRoute} + shouldShowPersonalBankAccountOption currency={iouCurrencyCode} policyID={policyID} buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE} @@ -545,7 +644,6 @@ function MoneyRequestConfirmationList({ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, }} - shouldShowPersonalBankAccountOption enterKeyEventListenerPriority={1} /> ) : ( @@ -553,333 +651,381 @@ function MoneyRequestConfirmationList({ success pressOnEnter isDisabled={shouldDisableButton} - // eslint-disable-next-line @typescript-eslint/naming-convention - onPress={(_event, value) => confirm(value)} + onPress={(event, value) => confirm(value as PaymentMethodType)} options={splitOrRequestOptions} buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE} enterKeyEventListenerPriority={1} /> ); + return ( <> - {!!formError.length && ( + {!!formError && ( )} + {button} ); - }, [isReadOnly, iouType, bankAccountRoute, iouCurrencyCode, policyID, selectedParticipants.length, confirm, splitOrRequestOptions, formError, styles.ph1, styles.mb2]); + }, [isReadOnly, iouType, selectedParticipants.length, confirm, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, formError, styles.ph1, styles.mb2]); + + // An intermediate structure that helps us classify the fields as "primary" and "supplementary". + // The primary fields are always shown to the user, while an extra action is needed to reveal the supplementary ones. + const classifiedFields = [ + { + item: ( + { + if (isDistanceRequest) { + return; + } + if (isEditingSplitBill) { + Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.AMOUNT)); + return; + } + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + }} + style={[styles.moneyRequestMenuItem, styles.mt2]} + titleStyle={styles.moneyRequestConfirmationAmount} + disabled={didConfirm} + brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + error={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? translate('common.error.enterAmount') : ''} + /> + ), + shouldShow: shouldShowSmartScanFields, + isSupplementary: false, + }, + { + item: ( + { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + }} + style={[styles.moneyRequestMenuItem]} + titleStyle={styles.flex1} + disabled={didConfirm} + interactive={!isReadOnly} + numberOfLinesTitle={2} + /> + ), + shouldShow: true, + isSupplementary: false, + }, + { + item: ( + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + disabled={didConfirm} + // todo: handle edit for transaction while moving from track expense + interactive={!isReadOnly && !isMovingTransactionFromTrackExpense} + /> + ), + shouldShow: isDistanceRequest && !canUseP2PDistanceRequests, + isSupplementary: false, + }, + { + item: ( + + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())) + } + disabled={didConfirm} + interactive={!isReadOnly} + /> + ), + shouldShow: isDistanceRequest && canUseP2PDistanceRequests, + isSupplementary: false, + }, + { + item: ( + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + disabled={didConfirm} + interactive={Boolean(rate) && !isReadOnly && isPolicyExpenseChat} + /> + ), + shouldShow: isDistanceRequest, + isSupplementary: true, + }, + { + item: ( + { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + }} + disabled={didConfirm} + interactive={!isReadOnly} + brickRoadIndicator={merchantError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + error={merchantError ? translate('common.error.fieldRequired') : ''} + rightLabel={isMerchantRequired ? translate('common.required') : ''} + /> + ), + shouldShow: shouldShowMerchant, + isSupplementary: !isMerchantRequired, + }, + { + item: ( + { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + }} + disabled={didConfirm} + interactive={!isReadOnly} + brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + error={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? translate('common.error.enterDate') : ''} + /> + ), + shouldShow: shouldShowDate, + isSupplementary: true, + }, + { + item: ( + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + style={[styles.moneyRequestMenuItem]} + titleStyle={styles.flex1} + disabled={didConfirm} + interactive={!isReadOnly} + rightLabel={isCategoryRequired ? translate('common.required') : ''} + /> + ), + shouldShow: shouldShowCategories, + isSupplementary: action === CONST.IOU.ACTION.CATEGORIZE ? false : !isCategoryRequired, + }, + ...policyTagLists.map(({name, required}, index) => { + const isTagRequired = required === undefined ? false : canUseViolations && required; + return { + item: ( + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(action, iouType, index, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + style={[styles.moneyRequestMenuItem]} + disabled={didConfirm} + interactive={!isReadOnly} + rightLabel={isTagRequired ? translate('common.required') : ''} + /> + ), + shouldShow: shouldShowTags, + isSupplementary: !isTagRequired, + }; + }), + { + item: ( + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + disabled={didConfirm} + interactive={!isReadOnly} + /> + ), + shouldShow: shouldShowTax, + isSupplementary: true, + }, + { + item: ( + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + disabled={didConfirm} + interactive={!isReadOnly} + /> + ), + shouldShow: shouldShowTax, + isSupplementary: true, + }, + { + item: ( + + {translate('common.billable')} + onToggleBillable?.(isOn)} + /> + + ), + shouldShow: shouldShowBillable, + isSupplementary: true, + }, + ]; + + const primaryFields = classifiedFields.filter((classifiedField) => classifiedField.shouldShow && !classifiedField.isSupplementary).map((primaryField) => primaryField.item); + + const supplementaryFields = classifiedFields + .filter((classifiedField) => classifiedField.shouldShow && classifiedField.isSupplementary) + .map((supplementaryField) => supplementaryField.item); const { image: receiptImage, thumbnail: receiptThumbnail, isThumbnail, fileExtension, - } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI); + isLocalFile, + } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction ?? null, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI); + + const resolvedThumbnail = isLocalFile ? receiptThumbnail : tryResolveUrlFromApiRoot(receiptThumbnail ?? ''); + const resolvedReceiptImage = isLocalFile ? receiptImage : tryResolveUrlFromApiRoot(receiptImage ?? ''); + + const receiptThumbnailContent = useMemo( + () => + isLocalFile && Str.isPDF(receiptFilename) ? ( + setIsAttachmentInvalid(true)} + /> + ) : ( + + ), + [isLocalFile, receiptFilename, resolvedThumbnail, styles.moneyRequestImage, isAttachmentInvalid, isThumbnail, resolvedReceiptImage, receiptThumbnail, fileExtension], + ); + return ( // @ts-expect-error This component is deprecated and will not be migrated to TypeScript (context: https://expensify.slack.com/archives/C01GTK53T8Q/p1709232289899589?thread_ts=1709156803.359359&cid=C01GTK53T8Q) {isDistanceRequest && ( - + )} - {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */} - {receiptImage || receiptThumbnail ? ( - - ) : ( - // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") - PolicyUtils.isPaidGroupPolicy(policy) && - !isDistanceRequest && - iouType === CONST.IOU.TYPE.REQUEST && ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute( - CONST.IOU.ACTION.CREATE, - iouType, - transaction?.transactionID ?? '', - reportID, - Navigation.getActiveRouteWithoutParams(), - ), - ) - } + {(!isMovingTransactionFromTrackExpense || !hasRoute) && + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + (receiptImage || receiptThumbnail + ? receiptThumbnailContent + : // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") + PolicyUtils.isPaidGroupPolicy(policy) && + !isDistanceRequest && + iouType === CONST.IOU.TYPE.REQUEST && ( + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + /> + ))} + {primaryFields} + {!shouldShowAllFields && ( + + +