diff --git a/android/app/build.gradle b/android/app/build.gradle index c9d156afc122..d2fdd9f36df3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -155,8 +155,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001019704 - versionName "1.1.97-4" + versionCode 1001019703 + versionName "1.1.97-3" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index fcc229635f36..2e639bb7da1d 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -30,7 +30,7 @@ CFBundleVersion - 1.1.97.4 + 1.1.97.3 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index cfc97f8196dc..7dc667bb5cdc 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.1.97.4 + 1.1.97.3 diff --git a/package-lock.json b/package-lock.json index 0d250578dd52..82c66db4d582 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.1.97-4", + "version": "1.1.97-3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.1.97-4", + "version": "1.1.97-3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 3a17c64c5c97..b6747f2b9b6b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.1.97-4", + "version": "1.1.97-3", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/CONST.js b/src/CONST.js index 32847df3ab37..1c2770a69a81 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -667,6 +667,7 @@ const CONST = { ADMIN: 'admin', }, ROOM_PREFIX: '#', + CUSTOM_UNIT_RATE_BASE_OFFSET: 100, }, CUSTOM_UNITS: { diff --git a/src/components/DotIndicatorMessage.js b/src/components/DotIndicatorMessage.js index 2b073627ef51..53a2ec337809 100644 --- a/src/components/DotIndicatorMessage.js +++ b/src/components/DotIndicatorMessage.js @@ -45,6 +45,10 @@ const DotIndicatorMessage = (props) => { .keys() .sortBy() .map(key => props.messages[key]) + + // Using uniq here since some fields are wrapped by the same OfflineWithFeedback component (e.g. WorkspaceReimburseView) + // and can potentially pass the same error. + .uniq() .value(); return ( diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index d38846c9aa8a..d921fd41fae3 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -580,14 +580,21 @@ function setWorkspaceErrors(policyID, errors) { /** * @param {String} policyID - * @param {Number} customUnitID + * @param {String} customUnitID + * @param {String} customUnitRateID */ -function removeUnitError(policyID, customUnitID) { +function clearCustomUnitErrors(policyID, customUnitID, customUnitRateID) { Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { customUnits: { [customUnitID]: { errors: null, pendingAction: null, + onyxRates: { + [customUnitRateID]: { + errors: null, + pendingAction: null, + }, + }, }, }, }); @@ -603,17 +610,17 @@ function hideWorkspaceAlertMessage(policyID) { /** * @param {String} policyID * @param {Object} currentCustomUnit - * @param {Object} values The new custom unit values + * @param {Object} newCustomUnit */ -function updateWorkspaceCustomUnit(policyID, currentCustomUnit, values) { +function updateWorkspaceCustomUnit(policyID, currentCustomUnit, newCustomUnit) { const optimisticData = [ { onyxMethod: 'merge', key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { customUnits: { - [values.customUnitID]: { - ...values, + [newCustomUnit.customUnitID]: { + ...newCustomUnit, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, }, }, @@ -627,7 +634,7 @@ function updateWorkspaceCustomUnit(policyID, currentCustomUnit, values) { key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { customUnits: { - [values.customUnitID]: { + [newCustomUnit.customUnitID]: { pendingAction: null, errors: null, }, @@ -657,40 +664,81 @@ function updateWorkspaceCustomUnit(policyID, currentCustomUnit, values) { API.write('UpdateWorkspaceCustomUnit', { policyID, - customUnit: JSON.stringify(values), + customUnit: JSON.stringify(newCustomUnit), }, {optimisticData, successData, failureData}); } /** * @param {String} policyID + * @param {Object} currentCustomUnitRate * @param {String} customUnitID - * @param {Object} values + * @param {Object} newCustomUnitRate */ -function setCustomUnitRate(policyID, customUnitID, values) { - DeprecatedAPI.Policy_CustomUnitRate_Update({ - policyID: policyID.toString(), - customUnitID: customUnitID.toString(), - customUnitRate: JSON.stringify(values), - lastModified: null, - }) - .then((response) => { - if (response.jsonCode !== 200) { - throw new Error(); - } +function updateCustomUnitRate(policyID, currentCustomUnitRate, customUnitID, newCustomUnitRate) { + const optimisticData = [ + { + onyxMethod: 'merge', + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnitID]: { + onyxRates: { + [newCustomUnitRate.customUnitRateID]: { + ...newCustomUnitRate, + errors: null, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }, + }, + }, + }, + ]; + + const successData = [ + { + onyxMethod: 'merge', + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnitID]: { + onyxRates: { + [newCustomUnitRate.customUnitRateID]: { + pendingAction: null, + }, + }, + }, + }, + }, + }, + ]; - updateLocalPolicyValues(policyID, { - customUnit: { - rate: { - id: values.customUnitRateID, - name: values.name, - value: Number(values.rate), + const failureData = [ + { + onyxMethod: 'merge', + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnitID]: { + onyxRates: { + [currentCustomUnitRate.customUnitRateID]: { + ...currentCustomUnitRate, + errors: { + [DateUtils.getMicroseconds()]: Localize.translateLocal('workspace.reimburse.updateCustomUnitError'), + }, + }, + }, }, }, - }); - }).catch(() => { - // Show the user feedback - Growl.error(Localize.translateLocal('workspace.editor.genericFailureMessage'), 5000); - }); + }, + }, + ]; + + API.write('UpdateWorkspaceCustomUnitRate', { + policyID, + customUnitID, + customUnitRate: JSON.stringify(newCustomUnitRate), + }, {optimisticData, successData, failureData}); } /** @@ -815,13 +863,13 @@ export { create, update, setWorkspaceErrors, - removeUnitError, + clearCustomUnitErrors, hideWorkspaceAlertMessage, deletePolicy, createAndNavigate, createAndGetPolicyList, updateWorkspaceCustomUnit, - setCustomUnitRate, + updateCustomUnitRate, updateLastAccessedWorkspace, subscribeToPolicyEvents, clearDeleteMemberError, diff --git a/src/pages/workspace/reimburse/WorkspaceReimburseView.js b/src/pages/workspace/reimburse/WorkspaceReimburseView.js index e4899ce9426b..29c04b7b8921 100644 --- a/src/pages/workspace/reimburse/WorkspaceReimburseView.js +++ b/src/pages/workspace/reimburse/WorkspaceReimburseView.js @@ -39,7 +39,7 @@ const propTypes = { attributes: PropTypes.shape({ unit: PropTypes.string, }), - rates: PropTypes.arrayOf( + onyxRates: PropTypes.objectOf( PropTypes.shape({ customUnitRateID: PropTypes.string, name: PropTypes.string, @@ -62,14 +62,14 @@ class WorkspaceReimburseView extends React.Component { constructor(props) { super(props); const distanceCustomUnit = _.find(lodashGet(props, 'policy.customUnits', {}), unit => unit.name === 'Distance'); + const customUnitRate = _.find(lodashGet(distanceCustomUnit, 'onyxRates', {}), rate => rate.name === 'Default Rate'); this.state = { unitID: lodashGet(distanceCustomUnit, 'customUnitID', ''), unitName: lodashGet(distanceCustomUnit, 'name', ''), unitValue: lodashGet(distanceCustomUnit, 'attributes.unit', 'mi'), - rateID: lodashGet(distanceCustomUnit, 'rates[0].customUnitRateID', ''), - rateName: lodashGet(distanceCustomUnit, 'rates[0].name', ''), - rateValue: this.getRateDisplayValue(lodashGet(distanceCustomUnit, 'rates[0].rate', 0) / 100), + unitRateID: lodashGet(customUnitRate, 'customUnitRateID', ''), + unitRateValue: this.getRateDisplayValue(lodashGet(customUnitRate, 'rate', 0) / CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET), outputCurrency: lodashGet(props, 'policy.outputCurrency', ''), }; @@ -98,14 +98,13 @@ class WorkspaceReimburseView extends React.Component { .values() .findWhere({name: CONST.CUSTOM_UNITS.NAME_DISTANCE}) .value(); - + const customUnitRate = _.find(lodashGet(distanceCustomUnit, 'onyxRates', {}), rate => rate.name === 'Default Rate'); this.setState({ unitID: lodashGet(distanceCustomUnit, 'customUnitID', ''), unitName: lodashGet(distanceCustomUnit, 'name', ''), unitValue: lodashGet(distanceCustomUnit, 'attributes.unit', 'mi'), - rateID: lodashGet(distanceCustomUnit, 'rates[0].customUnitRateID', ''), - rateName: lodashGet(distanceCustomUnit, 'rates[0].name', ''), - rateValue: this.getRateDisplayValue(lodashGet(distanceCustomUnit, 'rates[0].rate', 0) / 100), + unitRateID: lodashGet(customUnitRate, 'customUnitRateID'), + unitRateValue: this.getRateDisplayValue(lodashGet(customUnitRate, 'rate', 0) / 100), }); } @@ -130,10 +129,10 @@ class WorkspaceReimburseView extends React.Component { const isInvalidRateValue = value !== '' && !CONST.REGEX.RATE_VALUE.test(value); this.setState(prevState => ({ - rateValue: !isInvalidRateValue ? value : prevState.rateValue, + unitRateValue: !isInvalidRateValue ? value : prevState.unitRateValue, }), () => { // Set the corrected value with a delay and sync to the server - this.updateRateValueDebounced(this.state.rateValue); + this.updateRateValueDebounced(this.state.unitRateValue); }); } @@ -156,7 +155,7 @@ class WorkspaceReimburseView extends React.Component { return; } - this.updateRateValueDebounced(this.state.rateValue); + this.updateRateValueDebounced(this.state.unitRateValue); } updateRateValue(value) { @@ -167,14 +166,15 @@ class WorkspaceReimburseView extends React.Component { } this.setState({ - rateValue: numValue.toFixed(3), + unitRateValue: numValue.toFixed(3), }); - Policy.setCustomUnitRate(this.props.policyID, this.state.unitID, { - customUnitRateID: this.state.rateID, - name: this.state.rateName, - rate: numValue.toFixed(3) * 100, - }, null); + const distanceCustomUnit = _.find(lodashGet(this.props, 'policy.customUnits', {}), unit => unit.name === 'Distance'); + const currentCustomUnitRate = lodashGet(distanceCustomUnit, ['onyxRates', this.state.unitRateID], {}); + Policy.updateCustomUnitRate(this.props.policyID, currentCustomUnitRate, this.state.unitID, { + ...currentCustomUnitRate, + rate: numValue.toFixed(3) * CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET, + }); } render() { @@ -214,9 +214,13 @@ class WorkspaceReimburseView extends React.Component { {this.props.translate('workspace.reimburse.trackDistanceCopy')} Policy.removeUnitError(this.props.policyID, this.state.unitID)} + errors={{ + ...lodashGet(this.props, ['policy', 'customUnits', this.state.unitID, 'errors'], {}), + ...lodashGet(this.props, ['policy', 'customUnits', this.state.unitID, 'onyxRates', this.state.unitRateID, 'errors'], {}), + }} + pendingAction={lodashGet(this.props, ['policy', 'customUnits', this.state.unitID, 'pendingAction']) + || lodashGet(this.props, ['policy', 'customUnits', this.state.unitID, 'onyxRates', this.state.unitRateID, 'pendingAction'])} + onClose={() => Policy.clearCustomUnitErrors(this.props.policyID, this.state.unitID, this.state.unitRateID)} > @@ -224,7 +228,7 @@ class WorkspaceReimburseView extends React.Component { label={this.props.translate('workspace.reimburse.trackDistanceRate')} placeholder={this.state.outputCurrency} onChangeText={value => this.setRate(value)} - value={this.state.rateValue} + value={this.state.unitRateValue} autoCompleteType="off" autoCorrect={false} keyboardType={CONST.KEYBOARD_TYPE.DECIMAL_PAD}