diff --git a/__mocks__/fileMock.js b/__mocks__/fileMock.js
deleted file mode 100644
index 86059f362924..000000000000
--- a/__mocks__/fileMock.js
+++ /dev/null
@@ -1 +0,0 @@
-module.exports = 'test-file-stub';
diff --git a/__mocks__/fileMock.ts b/__mocks__/fileMock.ts
new file mode 100644
index 000000000000..3592aaaaf26b
--- /dev/null
+++ b/__mocks__/fileMock.ts
@@ -0,0 +1,3 @@
+const fileMock = 'test-file-stub';
+
+export default fileMock;
diff --git a/__mocks__/react-native-config.js b/__mocks__/react-native-config.js
deleted file mode 100644
index 7c3900efa21e..000000000000
--- a/__mocks__/react-native-config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-const path = require('path');
-const dotenv = require('dotenv');
-
-const env = dotenv.config({path: path.resolve('./.env.example')}).parsed;
-
-export default env;
diff --git a/__mocks__/react-native-config.ts b/__mocks__/react-native-config.ts
new file mode 100644
index 000000000000..129d7c97ed43
--- /dev/null
+++ b/__mocks__/react-native-config.ts
@@ -0,0 +1,8 @@
+import dotenv from 'dotenv';
+import path from 'path';
+
+type ReactNativeConfigMock = dotenv.DotenvParseOutput | undefined;
+
+const reactNativeConfigMock: ReactNativeConfigMock = dotenv.config({path: path.resolve('./.env.example')}).parsed;
+
+export default reactNativeConfigMock;
diff --git a/__mocks__/react-native-image-picker.js b/__mocks__/react-native-image-picker.js
deleted file mode 100644
index 7e4c29fa79ac..000000000000
--- a/__mocks__/react-native-image-picker.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export default {
- showImagePicker: jest.fn(),
- launchCamera: jest.fn(),
- launchImageLibrary: jest.fn(),
-};
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 96e92df52404..7bfb88bde255 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -98,8 +98,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001043904
- versionName "1.4.39-4"
+ versionCode 1001044000
+ versionName "1.4.40-0"
}
flavorDimensions "default"
diff --git a/android/app/src/main/res/raw/attention.mp3 b/android/app/src/main/res/raw/attention.mp3
new file mode 100644
index 000000000000..854be9cbcf07
Binary files /dev/null and b/android/app/src/main/res/raw/attention.mp3 differ
diff --git a/android/app/src/main/res/raw/done.mp3 b/android/app/src/main/res/raw/done.mp3
new file mode 100644
index 000000000000..23d6a2b6bdb5
Binary files /dev/null and b/android/app/src/main/res/raw/done.mp3 differ
diff --git a/android/app/src/main/res/raw/receive.mp3 b/android/app/src/main/res/raw/receive.mp3
new file mode 100644
index 000000000000..28f03052a14b
Binary files /dev/null and b/android/app/src/main/res/raw/receive.mp3 differ
diff --git a/android/app/src/main/res/raw/success.mp3 b/android/app/src/main/res/raw/success.mp3
new file mode 100644
index 000000000000..bd1af6526e40
Binary files /dev/null and b/android/app/src/main/res/raw/success.mp3 differ
diff --git a/assets/images/chatbubble-add.svg b/assets/images/chatbubble-add.svg
index 047a43073b3c..48eebf863cc3 100644
--- a/assets/images/chatbubble-add.svg
+++ b/assets/images/chatbubble-add.svg
@@ -1,13 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/chatbubble-unread.svg b/assets/images/chatbubble-unread.svg
index 9da789510276..492616cf2ab5 100644
--- a/assets/images/chatbubble-unread.svg
+++ b/assets/images/chatbubble-unread.svg
@@ -1,12 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/home.svg b/assets/images/home.svg
index 6b2411407be7..d4e02b723fee 100644
--- a/assets/images/home.svg
+++ b/assets/images/home.svg
@@ -1,3 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/images/olddot-wireframe.svg b/assets/images/olddot-wireframe.svg
index ee9aa93be255..055059edfd70 100644
--- a/assets/images/olddot-wireframe.svg
+++ b/assets/images/olddot-wireframe.svg
@@ -1,3422 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__gears.svg b/assets/images/simple-illustrations/simple-illustration__gears.svg
index 3b4cbc001e3b..2798feb4e04d 100644
--- a/assets/images/simple-illustrations/simple-illustration__gears.svg
+++ b/assets/images/simple-illustrations/simple-illustration__gears.svg
@@ -1,101 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__lockclosed.svg b/assets/images/simple-illustrations/simple-illustration__lockclosed.svg
index 3779b92b0b0f..791500c28032 100644
--- a/assets/images/simple-illustrations/simple-illustration__lockclosed.svg
+++ b/assets/images/simple-illustrations/simple-illustration__lockclosed.svg
@@ -1,17 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__palmtree.svg b/assets/images/simple-illustrations/simple-illustration__palmtree.svg
index 2aef4956cde9..c67e871dc434 100644
--- a/assets/images/simple-illustrations/simple-illustration__palmtree.svg
+++ b/assets/images/simple-illustrations/simple-illustration__palmtree.svg
@@ -1,15 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__profile.svg b/assets/images/simple-illustrations/simple-illustration__profile.svg
index 85312f26e186..085f02822bc0 100644
--- a/assets/images/simple-illustrations/simple-illustration__profile.svg
+++ b/assets/images/simple-illustrations/simple-illustration__profile.svg
@@ -1,6 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__qr-code.svg b/assets/images/simple-illustrations/simple-illustration__qr-code.svg
index 10268d747588..7bd460d5f4e9 100644
--- a/assets/images/simple-illustrations/simple-illustration__qr-code.svg
+++ b/assets/images/simple-illustrations/simple-illustration__qr-code.svg
@@ -1,4 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/sounds/attention.mp3 b/assets/sounds/attention.mp3
new file mode 100644
index 000000000000..854be9cbcf07
Binary files /dev/null and b/assets/sounds/attention.mp3 differ
diff --git a/assets/sounds/done.mp3 b/assets/sounds/done.mp3
new file mode 100644
index 000000000000..23d6a2b6bdb5
Binary files /dev/null and b/assets/sounds/done.mp3 differ
diff --git a/assets/sounds/receive.mp3 b/assets/sounds/receive.mp3
new file mode 100644
index 000000000000..28f03052a14b
Binary files /dev/null and b/assets/sounds/receive.mp3 differ
diff --git a/assets/sounds/success.mp3 b/assets/sounds/success.mp3
new file mode 100644
index 000000000000..bd1af6526e40
Binary files /dev/null and b/assets/sounds/success.mp3 differ
diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js
index 9b1a28382fa2..9fcbec23b70e 100644
--- a/config/webpack/webpack.common.js
+++ b/config/webpack/webpack.common.js
@@ -97,6 +97,7 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({
{from: 'web/manifest.json'},
{from: 'assets/css', to: 'css'},
{from: 'assets/fonts/web', to: 'fonts'},
+ {from: 'assets/sounds', to: 'sounds'},
{from: 'node_modules/react-pdf/dist/esm/Page/AnnotationLayer.css', to: 'css/AnnotationLayer.css'},
{from: 'node_modules/react-pdf/dist/esm/Page/TextLayer.css', to: 'css/TextLayer.css'},
{from: 'assets/images/shadow.png', to: 'images/shadow.png'},
@@ -200,7 +201,7 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({
alias: {
'react-native-config': 'react-web-config',
'react-native$': 'react-native-web',
-
+ 'react-native-sound': 'react-native-web-sound',
// Module alias for web & desktop
// https://webpack.js.org/configuration/resolve/#resolvealias
'@assets': path.resolve(__dirname, '../../assets'),
@@ -239,6 +240,7 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({
'process/browser': require.resolve('process/browser'),
},
},
+
optimization: {
runtimeChunk: 'single',
splitChunks: {
diff --git a/docs/assets/images/info.svg b/docs/assets/images/info.svg
index 96924fbb6cf7..fbe9b3612667 100644
--- a/docs/assets/images/info.svg
+++ b/docs/assets/images/info.svg
@@ -1,9 +1 @@
-
+
\ No newline at end of file
diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj
index 5d3bf1c07985..9b1451b2bf94 100644
--- a/ios/NewExpensify.xcodeproj/project.pbxproj
+++ b/ios/NewExpensify.xcodeproj/project.pbxproj
@@ -8,6 +8,10 @@
/* Begin PBXBuildFile section */
059DC4EFD39EF39437E6823D /* libPods-NotificationServiceExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A997AA8204EA3D90907FA80 /* libPods-NotificationServiceExtension.a */; };
+ 083353EB2B5AB22A00C603C0 /* attention.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 083353E72B5AB22900C603C0 /* attention.mp3 */; };
+ 083353EC2B5AB22A00C603C0 /* done.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 083353E82B5AB22900C603C0 /* done.mp3 */; };
+ 083353ED2B5AB22A00C603C0 /* receive.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 083353E92B5AB22900C603C0 /* receive.mp3 */; };
+ 083353EE2B5AB22A00C603C0 /* success.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 083353EA2B5AB22900C603C0 /* success.mp3 */; };
0C7C65547D7346EB923BE808 /* ExpensifyMono-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = E704648954784DDFBAADF568 /* ExpensifyMono-Regular.otf */; };
0CDA8E34287DD650004ECBEC /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0CDA8E33287DD650004ECBEC /* AppDelegate.mm */; };
0CDA8E35287DD650004ECBEC /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0CDA8E33287DD650004ECBEC /* AppDelegate.mm */; };
@@ -79,6 +83,10 @@
00E356EE1AD99517003FC87E /* NewExpensifyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NewExpensifyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
076FD9E41E08971BBF51D580 /* libPods-NewExpensify-NewExpensifyTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NewExpensify-NewExpensifyTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+ 083353E72B5AB22900C603C0 /* attention.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = attention.mp3; path = ../assets/sounds/attention.mp3; sourceTree = ""; };
+ 083353E82B5AB22900C603C0 /* done.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = done.mp3; path = ../assets/sounds/done.mp3; sourceTree = ""; };
+ 083353E92B5AB22900C603C0 /* receive.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = receive.mp3; path = ../assets/sounds/receive.mp3; sourceTree = ""; };
+ 083353EA2B5AB22900C603C0 /* success.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = success.mp3; path = ../assets/sounds/success.mp3; sourceTree = ""; };
0CDA8E33287DD650004ECBEC /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = NewExpensify/AppDelegate.mm; sourceTree = ""; };
0CDA8E36287DD6A0004ECBEC /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = NewExpensify/Images.xcassets; sourceTree = ""; };
0F5BE0CD252686320097D869 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; };
@@ -311,6 +319,10 @@
A9EA265D209D4558995C9BD4 /* Resources */ = {
isa = PBXGroup;
children = (
+ 083353E72B5AB22900C603C0 /* attention.mp3 */,
+ 083353E82B5AB22900C603C0 /* done.mp3 */,
+ 083353E92B5AB22900C603C0 /* receive.mp3 */,
+ 083353EA2B5AB22900C603C0 /* success.mp3 */,
44BF435285B94E5B95F90994 /* ExpensifyNewKansas-Medium.otf */,
D2AFB39EC1D44BF9B91D3227 /* ExpensifyNewKansas-MediumItalic.otf */,
DCF33E34FFEC48128CDD41D4 /* ExpensifyMono-Bold.otf */,
@@ -496,13 +508,17 @@
0F5BE0CE252686330097D869 /* GoogleService-Info.plist in Resources */,
E9DF872D2525201700607FDC /* AirshipConfig.plist in Resources */,
F0C450EA2705020500FD2970 /* colors.json in Resources */,
+ 083353EB2B5AB22A00C603C0 /* attention.mp3 in Resources */,
0CDA8E37287DD6A0004ECBEC /* Images.xcassets in Resources */,
70CF6E82262E297300711ADC /* BootSplash.storyboard in Resources */,
FF941A8D48F849269AB85C9A /* ExpensifyNewKansas-Medium.otf in Resources */,
BDB853621F354EBB84E619C2 /* ExpensifyNewKansas-MediumItalic.otf in Resources */,
26AF3C3540374A9FACB6C19E /* ExpensifyMono-Bold.otf in Resources */,
+ 083353EE2B5AB22A00C603C0 /* success.mp3 in Resources */,
0C7C65547D7346EB923BE808 /* ExpensifyMono-Regular.otf in Resources */,
2A9F8CDA983746B0B9204209 /* ExpensifyNeue-Bold.otf in Resources */,
+ 083353EC2B5AB22A00C603C0 /* done.mp3 in Resources */,
+ 083353ED2B5AB22A00C603C0 /* receive.mp3 in Resources */,
ED222ED90E074A5481A854FA /* ExpensifyNeue-BoldItalic.otf in Resources */,
30581EA8AAFD4FCE88C5D191 /* ExpensifyNeue-Italic.otf in Resources */,
1246A3EF20E54E7A9494C8B9 /* ExpensifyNeue-Regular.otf in Resources */,
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index e46568920c19..094fa0ab6025 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.4.39
+ 1.4.40
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.39.4
+ 1.4.40.0
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index f1a55f6f34ff..b35e4393d66d 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.4.39
+ 1.4.40
CFBundleSignature
????
CFBundleVersion
- 1.4.39.4
+ 1.4.40.0
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index c9a62d0dd2e5..72e43a33d4f7 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName
$(PRODUCT_NAME)
CFBundleShortVersionString
- 1.4.39
+ 1.4.40
CFBundleVersion
- 1.4.39.4
+ 1.4.40.0
NSExtension
NSExtensionPointIdentifier
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 2c05bf4175d8..7212f0bd872e 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1438,6 +1438,11 @@ PODS:
- glog
- RCT-Folly (= 2022.05.16.00)
- React-Core
+ - RNSound (0.11.2):
+ - React-Core
+ - RNSound/Core (= 0.11.2)
+ - RNSound/Core (0.11.2):
+ - React-Core
- RNSVG (14.0.0):
- React-Core
- SDWebImage (5.17.0):
@@ -1580,6 +1585,7 @@ DEPENDENCIES:
- RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`)
- RNReanimated (from `../node_modules/react-native-reanimated`)
- RNScreens (from `../node_modules/react-native-screens`)
+ - RNSound (from `../node_modules/react-native-sound`)
- RNSVG (from `../node_modules/react-native-svg`)
- VisionCamera (from `../node_modules/react-native-vision-camera`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
@@ -1827,6 +1833,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-reanimated"
RNScreens:
:path: "../node_modules/react-native-screens"
+ RNSound:
+ :path: "../node_modules/react-native-sound"
RNSVG:
:path: "../node_modules/react-native-svg"
VisionCamera:
@@ -1973,6 +1981,7 @@ SPEC CHECKSUMS:
RNReactNativeHapticFeedback: 1e3efeca9628ff9876ee7cdd9edec1b336913f8c
RNReanimated: 57f436e7aa3d277fbfed05e003230b43428157c0
RNScreens: b582cb834dc4133307562e930e8fa914b8c04ef2
+ RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852
RNSVG: 255767813dac22db1ec2062c8b7e7b856d4e5ae6
SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9
SDWebImageAVIFCoder: 8348fef6d0ec69e129c66c9fe4d74fbfbf366112
diff --git a/jest.config.js b/jest.config.js
index b347db593d83..95ecc350ed9f 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -26,6 +26,6 @@ module.exports = {
setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect', '/jest/setupAfterEnv.ts', '/tests/perf-test/setupAfterEnv.js'],
cacheDirectory: '/.jest-cache',
moduleNameMapper: {
- '\\.(lottie)$': '/__mocks__/fileMock.js',
+ '\\.(lottie)$': '/__mocks__/fileMock.ts',
},
};
diff --git a/jest/setup.ts b/jest/setup.ts
index 68d904fac5be..55774ff136f1 100644
--- a/jest/setup.ts
+++ b/jest/setup.ts
@@ -38,3 +38,11 @@ jest.mock('react-native-fs', () => ({
unlink: jest.fn(() => new Promise((res) => res())),
CachesDirectoryPath: jest.fn(),
}));
+
+jest.mock('react-native-sound', () => {
+ class SoundMock {
+ play = jest.fn();
+ }
+
+ return SoundMock;
+});
diff --git a/package-lock.json b/package-lock.json
index 862bdca66463..eab50aa5df98 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.39-4",
+ "version": "1.4.40-0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.39-4",
+ "version": "1.4.40-0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -109,13 +109,15 @@
"react-native-render-html": "6.3.1",
"react-native-safe-area-context": "4.7.4",
"react-native-screens": "3.29.0",
- "react-native-svg": "14.0.0",
+ "react-native-sound": "^0.11.2",
+ "react-native-svg": "14.1.0",
"react-native-tab-view": "^3.5.2",
"react-native-url-polyfill": "^2.0.0",
"react-native-view-shot": "3.8.0",
"react-native-vision-camera": "2.16.8",
"react-native-web": "^0.19.9",
"react-native-web-linear-gradient": "^1.1.2",
+ "react-native-web-sound": "^0.1.3",
"react-native-webview": "13.6.3",
"react-pdf": "7.3.3",
"react-plaid-link": "3.3.2",
@@ -33754,6 +33756,11 @@
"node": ">=10"
}
},
+ "node_modules/howler": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.4.tgz",
+ "integrity": "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w=="
+ },
"node_modules/hpack.js": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
@@ -45343,10 +45350,18 @@
"react-native": "*"
}
},
+ "node_modules/react-native-sound": {
+ "version": "0.11.2",
+ "resolved": "https://registry.npmjs.org/react-native-sound/-/react-native-sound-0.11.2.tgz",
+ "integrity": "sha512-LmGc8lgOK3qecYMVQpyHvww/C+wgT6sWeMpVbOe4NCRGC2yKd4fo4U0KBUo9PO7AqKESO3I/2GZg1/C0+bwiiA==",
+ "peerDependencies": {
+ "react-native": ">=0.8.0"
+ }
+ },
"node_modules/react-native-svg": {
- "version": "14.0.0",
- "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-14.0.0.tgz",
- "integrity": "sha512-17W/gWXRUMS7p7PSHu/WyGkZUc1NzRTGxxXc0VqBLjzKSchyb0EmgsiWf9aKmfC6gmY0wcsmKZcGV41bCcNfBA==",
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-14.1.0.tgz",
+ "integrity": "sha512-HeseElmEk+AXGwFZl3h56s0LtYD9HyGdrpg8yd9QM26X+d7kjETrRQ9vCjtxuT5dCZEIQ5uggU1dQhzasnsCWA==",
"dependencies": {
"css-select": "^5.1.0",
"css-tree": "^1.1.3"
@@ -45428,6 +45443,17 @@
"react-native-web": "*"
}
},
+ "node_modules/react-native-web-sound": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/react-native-web-sound/-/react-native-web-sound-0.1.3.tgz",
+ "integrity": "sha512-aUWNZuRFM7aREePiDgsTaBaaKX+aHKF0cDXtfOwFn4fQXkN+w5Ny3HykFrbDMxZagXSav5QjPntcA51lpWnSgg==",
+ "dependencies": {
+ "howler": "^2.2.1"
+ },
+ "peerDependencies": {
+ "react-native-web": "*"
+ }
+ },
"node_modules/react-native-web/node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
diff --git a/package.json b/package.json
index 73b1ac1837df..ad26ff297966 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.39-4",
+ "version": "1.4.40-0",
"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.",
@@ -157,13 +157,15 @@
"react-native-render-html": "6.3.1",
"react-native-safe-area-context": "4.7.4",
"react-native-screens": "3.29.0",
- "react-native-svg": "14.0.0",
+ "react-native-sound": "^0.11.2",
+ "react-native-svg": "14.1.0",
"react-native-tab-view": "^3.5.2",
"react-native-url-polyfill": "^2.0.0",
"react-native-view-shot": "3.8.0",
"react-native-vision-camera": "2.16.8",
"react-native-web": "^0.19.9",
"react-native-web-linear-gradient": "^1.1.2",
+ "react-native-web-sound": "^0.1.3",
"react-native-webview": "13.6.3",
"react-pdf": "7.3.3",
"react-plaid-link": "3.3.2",
diff --git a/patches/react-native-sound+0.11.2.patch b/patches/react-native-sound+0.11.2.patch
new file mode 100644
index 000000000000..661e39263c43
--- /dev/null
+++ b/patches/react-native-sound+0.11.2.patch
@@ -0,0 +1,38 @@
+diff --git a/node_modules/react-native-sound/RNSound/RNSound.h b/node_modules/react-native-sound/RNSound/RNSound.h
+index 7f5b97b..1a3c840 100644
+--- a/node_modules/react-native-sound/RNSound/RNSound.h
++++ b/node_modules/react-native-sound/RNSound/RNSound.h
+@@ -1,17 +1,7 @@
+-#if __has_include()
+ #import
+-#else
+-#import "RCTBridgeModule.h"
+-#endif
+-
+ #import
+-
+-#if __has_include()
+ #import
+-#else
+-#import "RCTEventEmitter.h"
+-#endif
+
+ @interface RNSound : RCTEventEmitter
+-@property (nonatomic, weak) NSNumber *_key;
++@property(nonatomic, weak) NSNumber *_key;
+ @end
+diff --git a/node_modules/react-native-sound/RNSound/RNSound.m b/node_modules/react-native-sound/RNSound/RNSound.m
+index df3784e..d34ac01 100644
+--- a/node_modules/react-native-sound/RNSound/RNSound.m
++++ b/node_modules/react-native-sound/RNSound/RNSound.m
+@@ -1,10 +1,6 @@
+ #import "RNSound.h"
+
+-#if __has_include("RCTUtils.h")
+-#import "RCTUtils.h"
+-#else
+ #import
+-#endif
+
+ @implementation RNSound {
+ NSMutableDictionary *_playerPool;
diff --git a/patches/react-native-vision-camera+2.16.5.patch b/patches/react-native-vision-camera+2.16.5.patch
deleted file mode 100644
index d08f7c11f5f3..000000000000
--- a/patches/react-native-vision-camera+2.16.5.patch
+++ /dev/null
@@ -1,13 +0,0 @@
-diff --git a/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessorRuntimeManager.kt b/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessorRuntimeManager.kt
-index c0a8b23..653b51e 100644
---- a/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessorRuntimeManager.kt
-+++ b/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessorRuntimeManager.kt
-@@ -40,7 +40,7 @@ class FrameProcessorRuntimeManager(context: ReactApplicationContext, frameProces
- val holder = context.catalystInstance.jsCallInvokerHolder as CallInvokerHolderImpl
- mScheduler = VisionCameraScheduler(frameProcessorThread)
- mContext = WeakReference(context)
-- mHybridData = initHybrid(context.javaScriptContextHolder.get(), holder, mScheduler!!)
-+ mHybridData = initHybrid(context.javaScriptContextHolder!!.get(), holder, mScheduler!!)
- initializeRuntime()
-
- Log.i(TAG, "Installing JSI Bindings on JS Thread...")
diff --git a/patches/react-native-vision-camera+2.16.5+001+fix-boost-dependency.patch b/patches/react-native-vision-camera+2.16.8.patch
similarity index 100%
rename from patches/react-native-vision-camera+2.16.5+001+fix-boost-dependency.patch
rename to patches/react-native-vision-camera+2.16.8.patch
diff --git a/patches/react-native-web+0.19.9+003+fix-pointer-events.patch b/patches/react-native-web+0.19.9+003+fix-pointer-events.patch
deleted file mode 100644
index a457fbcfe36c..000000000000
--- a/patches/react-native-web+0.19.9+003+fix-pointer-events.patch
+++ /dev/null
@@ -1,22 +0,0 @@
-diff --git a/node_modules/react-native-web/dist/exports/StyleSheet/compiler/index.js b/node_modules/react-native-web/dist/exports/StyleSheet/compiler/index.js
-index bdcecc2..63f1364 100644
---- a/node_modules/react-native-web/dist/exports/StyleSheet/compiler/index.js
-+++ b/node_modules/react-native-web/dist/exports/StyleSheet/compiler/index.js
-@@ -353,7 +353,7 @@ function createAtomicRules(identifier, property, value) {
- var _block2 = createDeclarationBlock({
- pointerEvents: 'none'
- });
-- rules.push(selector + ">*" + _block2);
-+ rules.push(selector + " *" + _block2);
- }
- } else if (value === 'none' || value === 'box-none') {
- finalValue = 'none!important';
-@@ -361,7 +361,7 @@ function createAtomicRules(identifier, property, value) {
- var _block3 = createDeclarationBlock({
- pointerEvents: 'auto'
- });
-- rules.push(selector + ">*" + _block3);
-+ rules.push(selector + " *" + _block3);
- }
- }
- var _block4 = createDeclarationBlock({
diff --git a/src/CONST.ts b/src/CONST.ts
index 79895d20aa57..eae4b8ec7a2b 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1506,7 +1506,7 @@ const CONST = {
GUIDES_CALL_TASK_IDS: {
CONCIERGE_DM: 'NewExpensifyConciergeDM',
WORKSPACE_INITIAL: 'WorkspaceHome',
- WORKSPACE_OVERVIEW: 'WorkspaceOverview',
+ WORKSPACE_PROFILE: 'WorkspaceProfile',
WORKSPACE_CARD: 'WorkspaceCorporateCards',
WORKSPACE_REIMBURSE: 'WorkspaceReimburseReceipts',
WORKSPACE_BILLS: 'WorkspacePayBills',
@@ -1567,6 +1567,10 @@ const CONST = {
FORM_CHARACTER_LIMIT: 50,
LEGAL_NAMES_CHARACTER_LIMIT: 150,
LOGIN_CHARACTER_LIMIT: 254,
+
+ TITLE_CHARACTER_LIMIT: 100,
+ DESCRIPTION_LIMIT: 500,
+
WORKSPACE_NAME_CHARACTER_LIMIT: 80,
AVATAR_CROP_MODAL: {
// The next two constants control what is min and max value of the image crop scale.
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index e3a78cbff39d..3e0f1c5cb4dd 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -272,10 +272,6 @@ const ROUTES = {
route: ':iouType/new/confirmation/:reportID?',
getRoute: (iouType: string, reportID = '') => `${iouType}/new/confirmation/${reportID}` as const,
},
- MONEY_REQUEST_DATE: {
- route: ':iouType/new/date/:reportID?',
- getRoute: (iouType: string, reportID = '') => `${iouType}/new/date/${reportID}` as const,
- },
MONEY_REQUEST_CURRENCY: {
route: ':iouType/new/currency/:reportID?',
getRoute: (iouType: string, reportID: string, currency: string, backTo: string) => `${iouType}/new/currency/${reportID}?currency=${currency}&backTo=${backTo}` as const,
@@ -337,9 +333,9 @@ const ROUTES = {
getUrlWithBackToParam(`create/${iouType}/currency/${transactionID}/${reportID}/${pageIndex}`, backTo),
},
MONEY_REQUEST_STEP_DATE: {
- route: 'create/:iouType/date/:transactionID/:reportID',
- getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') =>
- getUrlWithBackToParam(`create/${iouType}/date/${transactionID}/${reportID}`, backTo),
+ route: ':action/:iouType/date/:transactionID/:reportID',
+ getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') =>
+ getUrlWithBackToParam(`${action}/${iouType}/date/${transactionID}/${reportID}`, backTo),
},
MONEY_REQUEST_STEP_DESCRIPTION: {
route: ':action/:iouType/description/:transactionID/:reportID',
@@ -436,17 +432,17 @@ const ROUTES = {
route: 'workspace/:policyID/invite-message',
getRoute: (policyID: string) => `workspace/${policyID}/invite-message` as const,
},
- WORKSPACE_OVERVIEW: {
- route: 'workspace/:policyID/overview',
- getRoute: (policyID: string) => `workspace/${policyID}/overview` as const,
+ WORKSPACE_PROFILE: {
+ route: 'workspace/:policyID/profile',
+ getRoute: (policyID: string) => `workspace/${policyID}/profile` as const,
},
- WORKSPACE_OVERVIEW_CURRENCY: {
- route: 'workspace/:policyID/overview/currency',
- getRoute: (policyID: string) => `workspace/${policyID}/overview/currency` as const,
+ WORKSPACE_PROFILE_CURRENCY: {
+ route: 'workspace/:policyID/profile/currency',
+ getRoute: (policyID: string) => `workspace/${policyID}/profile/currency` as const,
},
- WORKSPACE_OVERVIEW_NAME: {
- route: 'workspace/:policyID/overview/name',
- getRoute: (policyID: string) => `workspace/${policyID}/overview/name` as const,
+ WORKSPACE_PROFILE_NAME: {
+ route: 'workspace/:policyID/profile/name',
+ getRoute: (policyID: string) => `workspace/${policyID}/profile/name` as const,
},
WORKSPACE_AVATAR: {
route: 'workspace/:policyID/avatar',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index cd80937a3864..7cc80fa837a1 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -146,7 +146,6 @@ const SCREENS = {
PARTICIPANTS: 'Money_Request_Participants',
CONFIRMATION: 'Money_Request_Confirmation',
CURRENCY: 'Money_Request_Currency',
- DATE: 'Money_Request_Date',
CATEGORY: 'Money_Request_Category',
MERCHANT: 'Money_Request_Merchant',
WAYPOINT: 'Money_Request_Waypoint',
@@ -194,7 +193,7 @@ const SCREENS = {
WORKSPACE: {
INITIAL: 'Workspace_Initial',
- OVERVIEW: 'Workspace_Overview',
+ PROFILE: 'Workspace_Profile',
CARD: 'Workspace_Card',
REIMBURSE: 'Workspace_Reimburse',
RATE_AND_UNIT: 'Workspace_RateAndUnit',
@@ -204,8 +203,8 @@ const SCREENS = {
MEMBERS: 'Workspace_Members',
INVITE: 'Workspace_Invite',
INVITE_MESSAGE: 'Workspace_Invite_Message',
- CURRENCY: 'Workspace_Overview_Currency',
- NAME: 'Workspace_Overview_Name',
+ CURRENCY: 'Workspace_Profile_Currency',
+ NAME: 'Workspace_Profile_Name',
},
EDIT_REQUEST: {
diff --git a/src/components/IFrame.tsx b/src/components/IFrame.tsx
index ab27597aeebd..05da3a1edb9c 100644
--- a/src/components/IFrame.tsx
+++ b/src/components/IFrame.tsx
@@ -37,7 +37,7 @@ function getNewDotURL(url: string): string {
if (pathname === 'policy') {
const workspaceID = params.policyID || '';
- const section = urlObj.hash.slice(1) || 'overview';
+ const section = urlObj.hash.slice(1) || 'profile';
return `workspace/${workspaceID}/${section}`;
}
diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx
index 15c12afb2609..81ab1ae33268 100644
--- a/src/components/LHNOptionsList/LHNOptionsList.tsx
+++ b/src/components/LHNOptionsList/LHNOptionsList.tsx
@@ -7,6 +7,7 @@ import withCurrentReportID from '@components/withCurrentReportID';
import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import * as OptionsListUtils from '@libs/OptionsListUtils';
+import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
@@ -64,6 +65,16 @@ function LHNOptionsList({
const itemComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] ?? '';
const participants = [...ReportUtils.getParticipantsIDs(itemFullReport), itemFullReport?.ownerAccountID, itemParentReportAction?.actorAccountID].filter(Boolean) as number[];
const participantsPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, personalDetails);
+ const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions);
+ const lastReportAction = sortedReportActions[0];
+
+ // Get the transaction for the last report action
+ let lastReportActionTransactionID = '';
+
+ if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) {
+ lastReportActionTransactionID = lastReportAction.originalMessage?.IOUTransactionID ?? '';
+ }
+ const lastReportActionTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${lastReportActionTransactionID}`] ?? {};
return (
();
- const linkedTransaction = useMemo(() => {
- const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(reportActions);
- const lastReportAction = sortedReportActions[0];
- return TransactionUtils.getLinkedTransaction(lastReportAction);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [fullReport?.reportID, receiptTransactions, reportActions]);
const hasViolations = canUseViolations && ReportUtils.doesTransactionThreadHaveViolations(fullReport, transactionViolations, parentReportAction ?? null);
@@ -64,7 +57,19 @@ function OptionRowLHNData({
// Listen parentReportAction to update title of thread report when parentReportAction changed
// Listen to transaction to update title of transaction report when transaction changed
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [fullReport, linkedTransaction, reportActions, personalDetails, preferredLocale, policy, parentReportAction, transaction, transactionViolations, canUseViolations]);
+ }, [
+ fullReport,
+ lastReportActionTransaction,
+ reportActions,
+ personalDetails,
+ preferredLocale,
+ policy,
+ parentReportAction,
+ transaction,
+ transactionViolations,
+ canUseViolations,
+ receiptTransactions,
+ ]);
useEffect(() => {
if (!optionItem || !!optionItem.hasDraftComment || !comment || comment.length <= 0 || isFocused) {
diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts
index 1f2c98301f9a..58bea97f04c9 100644
--- a/src/components/LHNOptionsList/types.ts
+++ b/src/components/LHNOptionsList/types.ts
@@ -7,6 +7,7 @@ import type {CurrentReportIDContextValue} from '@components/withCurrentReportID'
import type CONST from '@src/CONST';
import type {OptionData} from '@src/libs/ReportUtils';
import type {Locale, PersonalDetailsList, Policy, Report, ReportAction, ReportActions, Transaction, TransactionViolation} from '@src/types/onyx';
+import type {EmptyObject} from '@src/types/utils/EmptyObject';
type OptionMode = ValueOf;
@@ -83,6 +84,9 @@ type OptionRowLHNDataProps = {
/** The transaction from the parent report action */
transaction: OnyxEntry;
+ /** The transaction linked to the report's last action */
+ lastReportActionTransaction?: OnyxEntry;
+
/** Comment added to report */
comment: string;
diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js
index 92656a7ad225..7608447a213e 100755
--- a/src/components/MoneyRequestConfirmationList.js
+++ b/src/components/MoneyRequestConfirmationList.js
@@ -286,14 +286,14 @@ function MoneyRequestConfirmationList(props) {
const shouldDisplayMerchantError = props.isPolicyExpenseChat && !props.isScanRequest && isMerchantEmpty;
useEffect(() => {
- if (shouldDisplayFieldError && props.hasSmartScanFailed) {
- setFormError('iou.receiptScanningFailed');
- return;
- }
if (shouldDisplayFieldError && didConfirmSplit) {
setFormError('iou.error.genericSmartscanFailureMessage');
return;
}
+ if (shouldDisplayFieldError && props.hasSmartScanFailed) {
+ setFormError('iou.receiptScanningFailed');
+ return;
+ }
// reset the form error whenever the screen gains or loses focus
setFormError('');
}, [isFocused, transaction, shouldDisplayFieldError, props.hasSmartScanFailed, didConfirmSplit]);
@@ -497,7 +497,6 @@ function MoneyRequestConfirmationList(props) {
if (props.isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction)) {
setDidConfirmSplit(true);
- setFormError('iou.error.genericSmartscanFailureMessage');
return;
}
@@ -704,11 +703,15 @@ function MoneyRequestConfirmationList(props) {
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
onPress={() => {
- if (props.isEditingSplitBill) {
- Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(props.reportID, props.reportActionID, CONST.EDIT_REQUEST_FIELD.DATE));
- return;
- }
- Navigation.navigate(ROUTES.MONEY_REQUEST_DATE.getRoute(props.iouType, props.reportID));
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(
+ CONST.IOU.ACTION.EDIT,
+ props.iouType,
+ transaction.transactionID,
+ props.reportID,
+ Navigation.getActiveRouteWithoutParams(),
+ ),
+ );
}}
disabled={didConfirm}
interactive={!props.isReadOnly}
diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
index a2f79d2696b8..418ba97a70b9 100755
--- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
+++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
@@ -21,6 +21,7 @@ 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 * as TransactionUtils from '@libs/TransactionUtils';
import * as IOU from '@userActions/IOU';
import CONST from '@src/CONST';
@@ -549,6 +550,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
return;
}
+ playSound(SOUNDS.DONE);
setDidConfirm(true);
onConfirm(selectedParticipants);
}
@@ -790,11 +792,9 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
onPress={() => {
- if (isEditingSplitBill) {
- Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID, CONST.EDIT_REQUEST_FIELD.DATE));
- return;
- }
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()));
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
+ );
}}
disabled={didConfirm}
interactive={!isReadOnly}
diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js
index bfdb80131aa6..3802fe7a2ea6 100644
--- a/src/components/PDFView/index.native.js
+++ b/src/components/PDFView/index.native.js
@@ -1,25 +1,21 @@
-import React, {Component} from 'react';
+import React, {useCallback, useEffect, useState} from 'react';
import {View} from 'react-native';
import PDF from 'react-native-pdf';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import KeyboardAvoidingView from '@components/KeyboardAvoidingView';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import Text from '@components/Text';
-import withKeyboardState, {keyboardStatePropTypes} from '@components/withKeyboardState';
-import withLocalize from '@components/withLocalize';
-import withStyleUtils, {withStyleUtilsPropTypes} from '@components/withStyleUtils';
-import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles';
-import withWindowDimensions from '@components/withWindowDimensions';
-import compose from '@libs/compose';
+import useKeyboardState from '@hooks/useKeyboardState';
+import useLocalize from '@hooks/useLocalize';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
import CONST from '@src/CONST';
import PDFPasswordForm from './PDFPasswordForm';
import {defaultProps, propTypes as pdfViewPropTypes} from './pdfViewPropTypes';
const propTypes = {
...pdfViewPropTypes,
- ...keyboardStatePropTypes,
- ...withThemeStylesPropTypes,
- ...withStyleUtilsPropTypes,
};
/**
@@ -36,41 +32,24 @@ const propTypes = {
* so that PDFPasswordForm doesn't bounce when react-native-pdf/PDF
* is (temporarily) rendered.
*/
-class PDFView extends Component {
- constructor(props) {
- super(props);
- this.state = {
- shouldRequestPassword: false,
- shouldAttemptPDFLoad: true,
- shouldShowLoadingIndicator: true,
- isPasswordInvalid: false,
- failedToLoadPDF: false,
- successToLoadPDF: false,
- password: '',
- };
- this.initiatePasswordChallenge = this.initiatePasswordChallenge.bind(this);
- this.attemptPDFLoadWithPassword = this.attemptPDFLoadWithPassword.bind(this);
- this.finishPDFLoad = this.finishPDFLoad.bind(this);
- this.handleFailureToLoadPDF = this.handleFailureToLoadPDF.bind(this);
- }
-
- componentDidUpdate() {
- this.props.onToggleKeyboard(this.props.isKeyboardShown);
- }
-
- handleFailureToLoadPDF(error) {
- if (error.message.match(/password/i)) {
- this.initiatePasswordChallenge();
- return;
- }
- this.setState({
- failedToLoadPDF: true,
- shouldAttemptPDFLoad: false,
- shouldRequestPassword: false,
- shouldShowLoadingIndicator: false,
- });
- }
+function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused, onScaleChanged, sourceURL, errorLabelStyles}) {
+ const [shouldRequestPassword, setShouldRequestPassword] = useState(false);
+ const [shouldAttemptPDFLoad, setShouldAttemptPDFLoad] = useState(true);
+ const [shouldShowLoadingIndicator, setShouldShowLoadingIndicator] = useState(true);
+ const [isPasswordInvalid, setIsPasswordInvalid] = useState(false);
+ const [failedToLoadPDF, setFailedToLoadPDF] = useState(false);
+ const [successToLoadPDF, setSuccessToLoadPDF] = useState(false);
+ const [password, setPassword] = useState('');
+ const {windowWidth, windowHeight, isSmallScreenWidth} = useWindowDimensions();
+ const {translate} = useLocalize();
+ const themeStyles = useThemeStyles();
+ const {isKeyboardShown} = useKeyboardState();
+ const StyleUtils = useStyleUtils();
+
+ useEffect(() => {
+ onToggleKeyboard(isKeyboardShown);
+ });
/**
* Initiate password challenge if message received from react-native-pdf/PDF
@@ -80,100 +59,101 @@ class PDFView extends Component {
* Note that the message doesn't specify whether the password is simply empty or
* invalid.
*/
- initiatePasswordChallenge() {
- this.setState({shouldShowLoadingIndicator: false});
+
+ const initiatePasswordChallenge = useCallback(() => {
+ setShouldShowLoadingIndicator(false);
// Render password form, and don't render PDF and loading indicator.
- this.setState({
- shouldRequestPassword: true,
- shouldAttemptPDFLoad: false,
- });
+ setShouldRequestPassword(true);
+ setShouldAttemptPDFLoad(false);
// The message provided by react-native-pdf doesn't indicate whether this
// is an initial password request or if the password is invalid. So we just assume
// that if a password was already entered then it's an invalid password error.
- if (this.state.password) {
- this.setState({isPasswordInvalid: true});
+ if (password) {
+ setIsPasswordInvalid(true);
}
- }
+ }, [password]);
+
+ const handleFailureToLoadPDF = (error) => {
+ if (error.message.match(/password/i)) {
+ initiatePasswordChallenge();
+ return;
+ }
+ setFailedToLoadPDF(true);
+ setShouldShowLoadingIndicator(false);
+ setShouldRequestPassword(false);
+ setShouldAttemptPDFLoad(false);
+ };
/**
* When the password is submitted via PDFPasswordForm, save the password
* in state and attempt to load the PDF. Also show the loading indicator
* since react-native-pdf/PDF will need to reload the PDF.
*
- * @param {String} password Password submitted via PDFPasswordForm
+ * @param {String} pdfPassword Password submitted via PDFPasswordForm
*/
- attemptPDFLoadWithPassword(password) {
+ const attemptPDFLoadWithPassword = (pdfPassword) => {
// Render react-native-pdf/PDF so that it can validate the password.
// Note that at this point in the password challenge, shouldRequestPassword is true.
// Thus react-native-pdf/PDF will be rendered - but not visible.
- this.setState({
- password,
- shouldAttemptPDFLoad: true,
- shouldShowLoadingIndicator: true,
- });
- }
-
+ setPassword(pdfPassword);
+ setShouldAttemptPDFLoad(true);
+ setShouldShowLoadingIndicator(true);
+ };
/**
* After the PDF is successfully loaded hide PDFPasswordForm and the loading
* indicator.
*/
- finishPDFLoad() {
- this.setState({
- shouldRequestPassword: false,
- shouldShowLoadingIndicator: false,
- successToLoadPDF: true,
- });
- this.props.onLoadComplete();
- }
+ const finishPDFLoad = () => {
+ setShouldRequestPassword(false);
+ setShouldShowLoadingIndicator(false);
+ setSuccessToLoadPDF(true);
+ onLoadComplete();
+ };
- renderPDFView() {
- const pdfStyles = [this.props.themeStyles.imageModalPDF, this.props.StyleUtils.getWidthAndHeightStyle(this.props.windowWidth, this.props.windowHeight)];
+ function renderPDFView() {
+ const pdfStyles = [themeStyles.imageModalPDF, StyleUtils.getWidthAndHeightStyle(windowWidth, windowHeight)];
// If we haven't yet successfully validated the password and loaded the PDF,
// then we need to hide the react-native-pdf/PDF component so that PDFPasswordForm
// is positioned nicely. We're specifically hiding it because we still need to render
// the PDF component so that it can validate the password.
- if (this.state.shouldRequestPassword) {
- pdfStyles.push(this.props.themeStyles.invisible);
+ if (shouldRequestPassword) {
+ pdfStyles.push(themeStyles.invisible);
}
- const containerStyles =
- this.state.shouldRequestPassword && this.props.isSmallScreenWidth
- ? [this.props.themeStyles.w100, this.props.themeStyles.flex1]
- : [this.props.themeStyles.alignItemsCenter, this.props.themeStyles.flex1];
+ const containerStyles = shouldRequestPassword && isSmallScreenWidth ? [themeStyles.w100, themeStyles.flex1] : [themeStyles.alignItemsCenter, themeStyles.flex1];
return (
- {this.state.failedToLoadPDF && (
-
- {this.props.translate('attachmentView.failedToLoadPDF')}
+ {failedToLoadPDF && (
+
+ {translate('attachmentView.failedToLoadPDF')}
)}
- {this.state.shouldAttemptPDFLoad && (
+ {shouldAttemptPDFLoad && (
}
- source={{uri: this.props.sourceURL}}
+ source={{uri: sourceURL}}
style={pdfStyles}
- onError={this.handleFailureToLoadPDF}
- password={this.state.password}
- onLoadComplete={this.finishPDFLoad}
- onPageSingleTap={this.props.onPress}
- onScaleChanged={this.props.onScaleChanged}
+ onError={handleFailureToLoadPDF}
+ password={password}
+ onLoadComplete={finishPDFLoad}
+ onPageSingleTap={onPress}
+ onScaleChanged={onScaleChanged}
/>
)}
-
- {this.state.shouldRequestPassword && (
-
+ {shouldRequestPassword && (
+
this.setState({isPasswordInvalid: false})}
- isPasswordInvalid={this.state.isPasswordInvalid}
- shouldShowLoadingIndicator={this.state.shouldShowLoadingIndicator}
+ isFocused={isFocused}
+ onSubmit={attemptPDFLoadWithPassword}
+ onPasswordUpdated={() => setIsPasswordInvalid(false)}
+ isPasswordInvalid={isPasswordInvalid}
+ shouldShowLoadingIndicator={shouldShowLoadingIndicator}
/>
)}
@@ -181,23 +161,22 @@ class PDFView extends Component {
);
}
- render() {
- return this.props.onPress && !this.state.successToLoadPDF ? (
-
- {this.renderPDFView()}
-
- ) : (
- this.renderPDFView()
- );
- }
+ return onPress && !successToLoadPDF ? (
+
+ {renderPDFView()}
+
+ ) : (
+ renderPDFView()
+ );
}
+PDFView.displayName = 'PDFView';
PDFView.propTypes = propTypes;
PDFView.defaultProps = defaultProps;
-export default compose(withWindowDimensions, withKeyboardState, withLocalize, withThemeStyles, withStyleUtils)(PDFView);
+export default PDFView;
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index 3e37b693ce7c..6b16f272e4c8 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -306,13 +306,7 @@ function MoneyRequestView({
titleStyle={styles.flex1}
onPress={() =>
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(
- CONST.IOU.ACTION.EDIT,
- CONST.IOU.TYPE.REQUEST,
- transaction?.transactionID ?? '',
- report.reportID,
- Navigation.getActiveRouteWithoutParams(),
- ),
+ ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(CONST.IOU.ACTION.EDIT, CONST.IOU.TYPE.REQUEST, transaction?.transactionID ?? '', report.reportID),
)
}
wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
@@ -353,7 +347,9 @@ function MoneyRequestView({
interactive={canEditDate}
shouldShowRightIcon={canEditDate}
titleStyle={styles.flex1}
- onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DATE))}
+ onPress={() =>
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(CONST.IOU.ACTION.EDIT, CONST.IOU.TYPE.REQUEST, transaction?.transactionID ?? '', report.reportID))
+ }
brickRoadIndicator={getErrorForField('date') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
error={getErrorForField('date')}
/>
diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx
index 80bedc84f069..50bfcd4cc8be 100644
--- a/src/components/SettlementButton.tsx
+++ b/src/components/SettlementButton.tsx
@@ -5,6 +5,7 @@ import {withOnyx} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import * as ReportUtils from '@libs/ReportUtils';
+import playSound, {SOUNDS} from '@libs/Sound';
import * as BankAccounts from '@userActions/BankAccounts';
import * as IOU from '@userActions/IOU';
import * as PaymentMethods from '@userActions/PaymentMethods';
@@ -201,6 +202,7 @@ function SettlementButton({
return;
}
+ playSound(SOUNDS.DONE);
onPress(iouPaymentType);
};
diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx
index c6429747bf97..9e169ab2464a 100644
--- a/src/components/TextInput/BaseTextInput/index.native.tsx
+++ b/src/components/TextInput/BaseTextInput/index.native.tsx
@@ -265,11 +265,12 @@ function BaseTextInput(
return (
<>
require('../../../pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage').default as React.ComponentType,
[SCREENS.MONEY_REQUEST.CONFIRMATION]: () => require('../../../pages/iou/steps/MoneyRequestConfirmPage').default as React.ComponentType,
[SCREENS.MONEY_REQUEST.CURRENCY]: () => require('../../../pages/iou/IOUCurrencySelection').default as React.ComponentType,
- [SCREENS.MONEY_REQUEST.DATE]: () => require('../../../pages/iou/MoneyRequestDatePage').default as React.ComponentType,
[SCREENS.MONEY_REQUEST.CATEGORY]: () => require('../../../pages/iou/MoneyRequestCategoryPage').default as React.ComponentType,
[SCREENS.MONEY_REQUEST.MERCHANT]: () => require('../../../pages/iou/MoneyRequestMerchantPage').default as React.ComponentType,
[SCREENS.IOU_SEND.ADD_BANK_ACCOUNT]: () => require('../../../pages/AddPersonalBankAccountPage').default as React.ComponentType,
@@ -242,7 +241,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/WorkspaceInvitePage').default as React.ComponentType,
[SCREENS.WORKSPACE.INVITE_MESSAGE]: () => require('../../../pages/workspace/WorkspaceInviteMessagePage').default as React.ComponentType,
[SCREENS.WORKSPACE.NAME]: () => require('../../../pages/workspace/WorkspaceNamePage').default as React.ComponentType,
- [SCREENS.WORKSPACE.CURRENCY]: () => require('../../../pages/workspace/WorkspaceOverviewCurrencyPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.CURRENCY]: () => require('../../../pages/workspace/WorkspaceProfileCurrencyPage').default as React.ComponentType,
[SCREENS.REIMBURSEMENT_ACCOUNT]: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default as React.ComponentType,
[SCREENS.GET_ASSISTANCE]: () => require('../../../pages/GetAssistancePage').default as React.ComponentType,
[SCREENS.SETTINGS.TWO_FACTOR_AUTH]: () => require('../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').default as React.ComponentType,
diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx
index 53928b71be4e..087e963b3892 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx
@@ -15,7 +15,7 @@ type Screens = Partial React.C
const workspaceSettingsScreens = {
[SCREENS.SETTINGS.WORKSPACES]: () => require('../../../../../pages/workspace/WorkspacesListPage').default as React.ComponentType,
- [SCREENS.WORKSPACE.OVERVIEW]: () => require('../../../../../pages/workspace/WorkspaceOverviewPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.PROFILE]: () => require('../../../../../pages/workspace/WorkspaceProfilePage').default as React.ComponentType,
[SCREENS.WORKSPACE.CARD]: () => require('../../../../../pages/workspace/card/WorkspaceCardPage').default as React.ComponentType,
[SCREENS.WORKSPACE.REIMBURSE]: () => require('../../../../../pages/workspace/reimburse/WorkspaceReimbursePage').default as React.ComponentType,
[SCREENS.WORKSPACE.BILLS]: () => require('../../../../../pages/workspace/bills/WorkspaceBillsPage').default as React.ComponentType,
diff --git a/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts b/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts
index a22185422a11..b0825b4b2991 100644
--- a/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts
+++ b/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts
@@ -26,9 +26,9 @@ type GetPartialStateDiffReturnType = {
* This function returns partial additive diff between the two states.
*
* Example: Let's start with state A on route /r/123. If the screen is wide we will have a HOME opened on bottom tab and REPORT on central pane.
- * Now let's say we want to navigate to /workspace/345/overview. We will generate state B from this path.
- * State B will have WORKSPACE_INITIAL on the bottom tab and WORKSPACE_OVERVIEW on the central pane.
- * Now we will generate partial diff between state A and state B. The diff will tell us that we need to push WORKSPACE_INITIAL on the bottom tab and WORKSPACE_OVERVIEW on the central pane.
+ * Now let's say we want to navigate to /workspace/345/profile. We will generate state B from this path.
+ * State B will have WORKSPACE_INITIAL on the bottom tab and WORKSPACE_PROFILE on the central pane.
+ * Now we will generate partial diff between state A and state B. The diff will tell us that we need to push WORKSPACE_INITIAL on the bottom tab and WORKSPACE_PROFILE on the central pane.
*
* Then we can generate actions from this diff and dispatch them to the linkTo function.
*
diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
index d61b36871434..d96ad416832d 100755
--- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
@@ -2,7 +2,7 @@ import type {CentralPaneName} from '@libs/Navigation/types';
import SCREENS from '@src/SCREENS';
const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = {
- [SCREENS.WORKSPACE.OVERVIEW]: [SCREENS.WORKSPACE.NAME, SCREENS.WORKSPACE.CURRENCY],
+ [SCREENS.WORKSPACE.PROFILE]: [SCREENS.WORKSPACE.NAME, SCREENS.WORKSPACE.CURRENCY],
[SCREENS.WORKSPACE.REIMBURSE]: [SCREENS.WORKSPACE.RATE_AND_UNIT],
[SCREENS.WORKSPACE.MEMBERS]: [SCREENS.WORKSPACE.INVITE, SCREENS.WORKSPACE.INVITE_MESSAGE],
};
diff --git a/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts b/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts
index 3344cffe94ae..446fb479ea09 100755
--- a/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts
@@ -5,7 +5,7 @@ const TAB_TO_CENTRAL_PANE_MAPPING: Record = {
[SCREENS.HOME]: [SCREENS.REPORT],
[SCREENS.ALL_SETTINGS]: [SCREENS.SETTINGS.WORKSPACES],
[SCREENS.WORKSPACE.INITIAL]: [
- SCREENS.WORKSPACE.OVERVIEW,
+ SCREENS.WORKSPACE.PROFILE,
SCREENS.WORKSPACE.CARD,
SCREENS.WORKSPACE.REIMBURSE,
SCREENS.WORKSPACE.BILLS,
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 12577e360784..e6ee00064d95 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -42,7 +42,7 @@ const config: LinkingOptions['config'] = {
[SCREENS.REPORT]: ROUTES.REPORT_WITH_ID.route,
[SCREENS.SETTINGS.WORKSPACES]: ROUTES.SETTINGS_WORKSPACES,
- [SCREENS.WORKSPACE.OVERVIEW]: ROUTES.WORKSPACE_OVERVIEW.route,
+ [SCREENS.WORKSPACE.PROFILE]: ROUTES.WORKSPACE_PROFILE.route,
[SCREENS.WORKSPACE.CARD]: {
path: ROUTES.WORKSPACE_CARD.route,
},
@@ -224,7 +224,7 @@ const config: LinkingOptions['config'] = {
path: ROUTES.SETTINGS_STATUS_CLEAR_AFTER_TIME,
},
[SCREENS.WORKSPACE.CURRENCY]: {
- path: ROUTES.WORKSPACE_OVERVIEW_CURRENCY.route,
+ path: ROUTES.WORKSPACE_PROFILE_CURRENCY.route,
},
[SCREENS.WORKSPACE.RATE_AND_UNIT]: {
path: ROUTES.WORKSPACE_RATE_AND_UNIT.route,
@@ -245,7 +245,7 @@ const config: LinkingOptions['config'] = {
[SCREENS.KEYBOARD_SHORTCUTS]: {
path: ROUTES.KEYBOARD_SHORTCUTS,
},
- [SCREENS.WORKSPACE.NAME]: ROUTES.WORKSPACE_OVERVIEW_NAME.route,
+ [SCREENS.WORKSPACE.NAME]: ROUTES.WORKSPACE_PROFILE_NAME.route,
},
},
[SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: {
@@ -405,7 +405,6 @@ const config: LinkingOptions['config'] = {
[SCREENS.MONEY_REQUEST.STEP_TAX_RATE]: ROUTES.MONEY_REQUEST_STEP_TAX_RATE.route,
[SCREENS.MONEY_REQUEST.PARTICIPANTS]: ROUTES.MONEY_REQUEST_PARTICIPANTS.route,
[SCREENS.MONEY_REQUEST.CONFIRMATION]: ROUTES.MONEY_REQUEST_CONFIRMATION.route,
- [SCREENS.MONEY_REQUEST.DATE]: ROUTES.MONEY_REQUEST_DATE.route,
[SCREENS.MONEY_REQUEST.CURRENCY]: ROUTES.MONEY_REQUEST_CURRENCY.route,
[SCREENS.MONEY_REQUEST.CATEGORY]: ROUTES.MONEY_REQUEST_CATEGORY.route,
[SCREENS.MONEY_REQUEST.MERCHANT]: ROUTES.MONEY_REQUEST_MERCHANT.route,
diff --git a/src/libs/Navigation/switchPolicyID.ts b/src/libs/Navigation/switchPolicyID.ts
index 9afb325eee99..72a7c3e32fb4 100644
--- a/src/libs/Navigation/switchPolicyID.ts
+++ b/src/libs/Navigation/switchPolicyID.ts
@@ -122,9 +122,9 @@ export default function switchPolicyID(navigation: NavigationContainerRef;
+ iouType: ValueOf;
+ transactionID: string;
reportID: string;
- field: string;
- threadReportID: string;
+ backTo: string;
};
[SCREENS.MONEY_REQUEST.STEP_DESCRIPTION]: {
action: ValueOf;
diff --git a/src/libs/Notification/LocalNotification/BrowserNotifications.ts b/src/libs/Notification/LocalNotification/BrowserNotifications.ts
index 911e665f3ff4..f44b6802b540 100644
--- a/src/libs/Notification/LocalNotification/BrowserNotifications.ts
+++ b/src/libs/Notification/LocalNotification/BrowserNotifications.ts
@@ -42,7 +42,7 @@ function canUseBrowserNotifications(): Promise {
* @param icon Path to icon
* @param data extra data to attach to the notification
*/
-function push(title: string, body = '', icon: string | ImageSourcePropType = '', data: LocalNotificationData = {}, onClick: LocalNotificationClickHandler = () => {}) {
+function push(title: string, body = '', icon: string | ImageSourcePropType = '', data: LocalNotificationData = {}, onClick: LocalNotificationClickHandler = () => {}, silent = false) {
canUseBrowserNotifications().then((canUseNotifications) => {
if (!canUseNotifications) {
return;
@@ -54,6 +54,7 @@ function push(title: string, body = '', icon: string | ImageSourcePropType = '',
body,
icon: String(icon),
data,
+ silent,
});
notificationCache[notificationID].onclick = () => {
onClick();
@@ -104,7 +105,7 @@ export default {
reportID: report.reportID,
};
- push(title, body, icon, data, onClick);
+ push(title, body, icon, data, onClick, true);
},
pushModifiedExpenseNotification(report: Report, reportAction: ReportAction, onClick: LocalNotificationClickHandler, usesIcon = false) {
diff --git a/src/libs/SessionUtils.ts b/src/libs/SessionUtils.ts
index c73513c747af..52521d5146cc 100644
--- a/src/libs/SessionUtils.ts
+++ b/src/libs/SessionUtils.ts
@@ -4,7 +4,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
/**
* Determine if the transitioning user is logging in as a new user.
*/
-function isLoggingInAsNewUser(transitionURL: string, sessionEmail: string): boolean {
+function isLoggingInAsNewUser(transitionURL?: string, sessionEmail?: string): boolean {
// The OldDot mobile app does not URL encode the parameters, but OldDot web
// does. We don't want to deploy OldDot mobile again, so as a work around we
// compare the session email to both the decoded and raw email from the transition link.
@@ -20,7 +20,7 @@ function isLoggingInAsNewUser(transitionURL: string, sessionEmail: string): bool
// If they do not match it might be due to encoding, so check the raw value
// Capture the un-encoded text in the email param
const emailParamRegex = /[?&]email=([^&]*)/g;
- const matches = emailParamRegex.exec(transitionURL);
+ const matches = emailParamRegex.exec(transitionURL ?? '');
const linkedEmail = matches?.[1] ?? null;
return linkedEmail !== sessionEmail;
}
diff --git a/src/libs/Sound/config/index.native.ts b/src/libs/Sound/config/index.native.ts
new file mode 100644
index 000000000000..e948c1e189e8
--- /dev/null
+++ b/src/libs/Sound/config/index.native.ts
@@ -0,0 +1,3 @@
+const config = {prefix: ''};
+
+export default config;
diff --git a/src/libs/Sound/config/index.ts b/src/libs/Sound/config/index.ts
new file mode 100644
index 000000000000..f58755750d9d
--- /dev/null
+++ b/src/libs/Sound/config/index.ts
@@ -0,0 +1,3 @@
+const config = {prefix: '/sounds/'};
+
+export default config;
diff --git a/src/libs/Sound/index.ts b/src/libs/Sound/index.ts
new file mode 100644
index 000000000000..4639887e831c
--- /dev/null
+++ b/src/libs/Sound/index.ts
@@ -0,0 +1,71 @@
+import Onyx from 'react-native-onyx';
+import Sound from 'react-native-sound';
+import type {ValueOf} from 'type-fest';
+import ONYXKEYS from '@src/ONYXKEYS';
+import config from './config';
+
+let isMuted = false;
+
+Onyx.connect({
+ key: ONYXKEYS.USER,
+ callback: (val) => (isMuted = !!val?.isMutedAllSounds),
+});
+
+const SOUNDS = {
+ DONE: 'done',
+ SUCCESS: 'success',
+ ATTENTION: 'attention',
+ RECEIVE: 'receive',
+} as const;
+
+/**
+ * Creates a version of the given function that, when called, queues the execution and ensures that
+ * calls are spaced out by at least the specified `minExecutionTime`, even if called more frequently. This allows
+ * for throttling frequent calls to a function, ensuring each is executed with a minimum `minExecutionTime` between calls.
+ * Each call returns a promise that resolves when the function call is executed, allowing for asynchronous handling.
+ */
+function withMinimalExecutionTime) => ReturnType>(func: F, minExecutionTime: number) {
+ const queue: Array<[() => ReturnType, (value?: unknown) => void]> = [];
+ let timerId: NodeJS.Timeout | null = null;
+
+ function processQueue() {
+ if (queue.length > 0) {
+ const next = queue.shift();
+
+ if (!next) {
+ return;
+ }
+
+ const [nextFunc, resolve] = next;
+ nextFunc();
+ resolve();
+ timerId = setTimeout(processQueue, minExecutionTime);
+ } else {
+ timerId = null;
+ }
+ }
+
+ return function (...args: Parameters) {
+ return new Promise((resolve) => {
+ queue.push([() => func(...args), resolve]);
+
+ if (!timerId) {
+ // If the timer isn't running, start processing the queue
+ processQueue();
+ }
+ });
+ };
+}
+
+const playSound = (soundFile: ValueOf) => {
+ const sound = new Sound(`${config.prefix}${soundFile}.mp3`, Sound.MAIN_BUNDLE, (error) => {
+ if (error || isMuted) {
+ return;
+ }
+
+ sound.play();
+ });
+};
+
+export {SOUNDS};
+export default withMinimalExecutionTime(playSound, 300);
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index d7ac6f3c9361..56cea2a6d4e8 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -269,8 +269,8 @@ function setMoneyRequestAmount_temporaryForRefactor(transactionID: string, amoun
}
// eslint-disable-next-line @typescript-eslint/naming-convention
-function setMoneyRequestCreated_temporaryForRefactor(transactionID: string, created: string) {
- Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {created});
+function setMoneyRequestCreated(transactionID: string, created: string, isDraft: boolean) {
+ Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {created});
}
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -3712,10 +3712,6 @@ function setMoneyRequestAmount(amount: number) {
Onyx.merge(ONYXKEYS.IOU, {amount});
}
-function setMoneyRequestCreated(created: string) {
- Onyx.merge(ONYXKEYS.IOU, {created});
-}
-
function setMoneyRequestCurrency(currency: string) {
Onyx.merge(ONYXKEYS.IOU, {currency});
}
@@ -3858,7 +3854,7 @@ export {
setMoneyRequestAmount_temporaryForRefactor,
setMoneyRequestBillable_temporaryForRefactor,
setMoneyRequestCategory_temporaryForRefactor,
- setMoneyRequestCreated_temporaryForRefactor,
+ setMoneyRequestCreated,
setMoneyRequestCurrency_temporaryForRefactor,
setMoneyRequestDescription,
setMoneyRequestOriginalCurrency_temporaryForRefactor,
@@ -3869,7 +3865,6 @@ export {
setMoneyRequestAmount,
setMoneyRequestBillable,
setMoneyRequestCategory,
- setMoneyRequestCreated,
setMoneyRequestCurrency,
setMoneyRequestId,
setMoneyRequestMerchant,
diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts
index 901e3698376b..28cecf460a5f 100644
--- a/src/libs/actions/Task.ts
+++ b/src/libs/actions/Task.ts
@@ -13,6 +13,7 @@ import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
+import playSound, {SOUNDS} from '@libs/Sound';
import * as UserUtils from '@libs/UserUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -309,6 +310,7 @@ function completeTask(taskReport: OnyxEntry) {
completedTaskReportActionID: completedTaskReportAction.reportActionID,
};
+ playSound(SOUNDS.SUCCESS);
API.write(WRITE_COMMANDS.COMPLETE_TASK, parameters, {optimisticData, successData, failureData});
}
diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index 09cc6e49e6cc..75c61d7fab5f 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -28,6 +28,7 @@ import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as Pusher from '@libs/Pusher/pusher';
import PusherUtils from '@libs/PusherUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
+import playSound, {SOUNDS} from '@libs/Sound';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -37,6 +38,7 @@ import type {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer';
import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails';
import type {Status} from '@src/types/onyx/PersonalDetails';
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 * as Link from './Link';
@@ -458,7 +460,7 @@ function isBlockedFromConcierge(blockedFromConciergeNVP: OnyxEntry {
- if (!update.shouldNotify) {
+ if (!update.shouldNotify && !update.shouldShowPushNotification) {
return;
}
@@ -469,6 +471,101 @@ function triggerNotifications(onyxUpdates: OnyxServerUpdate[]) {
});
}
+const isChannelMuted = (reportId: string) =>
+ new Promise((resolve) => {
+ const connectionId = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT}${reportId}`,
+ callback: (report) => {
+ Onyx.disconnect(connectionId);
+
+ resolve(
+ !report?.notificationPreference ||
+ report?.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE ||
+ report?.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN,
+ );
+ },
+ });
+ });
+
+function playSoundForMessageType(pushJSON: OnyxServerUpdate[]) {
+ const reportActionsOnly = pushJSON.filter((update) => update.key.includes('reportActions_'));
+ // "reportActions_5134363522480668" -> "5134363522480668"
+ const reportIDs = reportActionsOnly.map((value) => value.key.split('_')[1]);
+
+ Promise.all(reportIDs.map((reportID) => isChannelMuted(reportID)))
+ .then((muted) => muted.every((isMuted) => isMuted))
+ .then((isSoundMuted) => {
+ if (isSoundMuted) {
+ return;
+ }
+
+ try {
+ const flatten = reportActionsOnly.flatMap((update) => {
+ const value = update.value as OnyxCollection;
+
+ if (!value) {
+ return [];
+ }
+
+ return Object.values(value);
+ }) as ReportAction[];
+
+ for (const data of flatten) {
+ // Someone completes a task
+ if (data.actionName === 'TASKCOMPLETED') {
+ return playSound(SOUNDS.SUCCESS);
+ }
+ }
+
+ const types = flatten.map((data) => data?.originalMessage).filter(Boolean) as OriginalMessage[];
+
+ for (const message of types) {
+ // someone sent money
+ if ('IOUDetails' in message) {
+ return playSound(SOUNDS.SUCCESS);
+ }
+
+ // mention user
+ if ('html' in message && typeof message.html === 'string' && message.html.includes('')) {
+ return playSound(SOUNDS.ATTENTION);
+ }
+
+ // mention @here
+ if ('html' in message && typeof message.html === 'string' && message.html.includes('')) {
+ return playSound(SOUNDS.ATTENTION);
+ }
+
+ // assign a task
+ if ('taskReportID' in message) {
+ return playSound(SOUNDS.ATTENTION);
+ }
+
+ // request money
+ if ('IOUTransactionID' in message) {
+ return playSound(SOUNDS.ATTENTION);
+ }
+
+ // Someone completes a money request
+ if ('IOUReportID' in message) {
+ return playSound(SOUNDS.SUCCESS);
+ }
+
+ // plain message
+ if ('html' in message) {
+ return playSound(SOUNDS.RECEIVE);
+ }
+ }
+ } catch (e) {
+ let errorMessage = String(e);
+ if (e instanceof Error) {
+ errorMessage = e.message;
+ }
+
+ Log.client(`Unexpected error occurred while parsing the data to play a sound: ${errorMessage}`);
+ }
+ });
+}
+
/**
* Handles the newest events from Pusher where a single mega multipleEvents contains
* an array of singular events all in one event
@@ -514,8 +611,10 @@ function subscribeToUserEvents() {
});
// Handles Onyx updates coming from Pusher through the mega multipleEvents.
- PusherUtils.subscribeToMultiEvent(Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE, (pushJSON: OnyxServerUpdate[]) =>
- SequentialQueue.getCurrentRequest().then(() => {
+ PusherUtils.subscribeToMultiEvent(Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE, (pushJSON: OnyxServerUpdate[]) => {
+ playSoundForMessageType(pushJSON);
+
+ return SequentialQueue.getCurrentRequest().then(() => {
// If we don't have the currentUserAccountID (user is logged out) we don't want to update Onyx with data from Pusher
if (currentUserAccountID === -1) {
return;
@@ -528,8 +627,8 @@ function subscribeToUserEvents() {
// Return a promise when Onyx is done updating so that the OnyxUpdatesManager can properly apply all
// the onyx updates in order
return onyxUpdatePromise;
- }),
- );
+ });
+ });
}
/**
@@ -613,6 +712,10 @@ function clearUserErrorMessage() {
Onyx.merge(ONYXKEYS.USER, {error: ''});
}
+function setMuteAllSounds(isMutedAllSounds: boolean) {
+ Onyx.merge(ONYXKEYS.USER, {isMutedAllSounds});
+}
+
/**
* Clear the data about a screen share request from Onyx.
*/
@@ -880,6 +983,7 @@ export {
subscribeToUserEvents,
updatePreferredSkinTone,
setShouldUseStagingServer,
+ setMuteAllSounds,
clearUserErrorMessage,
updateFrequentlyUsedEmojis,
joinScreenShare,
diff --git a/src/libs/actions/Welcome.ts b/src/libs/actions/Welcome.ts
index 3f6b2dc99a8f..952d19117679 100644
--- a/src/libs/actions/Welcome.ts
+++ b/src/libs/actions/Welcome.ts
@@ -136,11 +136,13 @@ function show(routes: NavigationState['routes'], showEngagem
const transitionRoute = routes.find(
(route): route is NavigationState>['routes'][number] => route.name === SCREENS.TRANSITION_BETWEEN_APPS,
);
+ const activeRoute = Navigation.getActiveRouteWithoutParams();
+ const isOnWorkspaceOverviewPage = activeRoute?.startsWith('/workspace') && activeRoute?.endsWith('/overview');
const isExitingToWorkspaceRoute = transitionRoute?.params?.exitTo === 'workspace/new';
// If we already opened the workspace settings or want the admin room to stay open, do not
// navigate away to the workspace chat report
- const shouldNavigateToWorkspaceChat = !isExitingToWorkspaceRoute;
+ const shouldNavigateToWorkspaceChat = !isExitingToWorkspaceRoute && !isOnWorkspaceOverviewPage;
const workspaceChatReport = Object.values(allReports ?? {}).find((report) => {
if (report) {
diff --git a/src/pages/EditRequestCreatedPage.js b/src/pages/EditRequestCreatedPage.js
deleted file mode 100644
index 2b5a8abaa349..000000000000
--- a/src/pages/EditRequestCreatedPage.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import DatePicker from '@components/DatePicker';
-import FormProvider from '@components/Form/FormProvider';
-import InputWrapper from '@components/Form/InputWrapper';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
-import ScreenWrapper from '@components/ScreenWrapper';
-import useLocalize from '@hooks/useLocalize';
-import useThemeStyles from '@hooks/useThemeStyles';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-
-const propTypes = {
- /** Transaction defailt created value */
- defaultCreated: PropTypes.string.isRequired,
-
- /** Callback to fire when the Save button is pressed */
- onSubmit: PropTypes.func.isRequired,
-};
-
-function EditRequestCreatedPage({defaultCreated, onSubmit}) {
- const styles = useThemeStyles();
- const {translate} = useLocalize();
-
- return (
-
-
-
-
-
-
- );
-}
-
-EditRequestCreatedPage.propTypes = propTypes;
-EditRequestCreatedPage.displayName = 'EditRequestCreatedPage';
-
-export default EditRequestCreatedPage;
diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js
index 777321fc2068..aa22439dee70 100644
--- a/src/pages/EditRequestPage.js
+++ b/src/pages/EditRequestPage.js
@@ -21,7 +21,6 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import EditRequestAmountPage from './EditRequestAmountPage';
import EditRequestCategoryPage from './EditRequestCategoryPage';
-import EditRequestCreatedPage from './EditRequestCreatedPage';
import EditRequestDistancePage from './EditRequestDistancePage';
import EditRequestMerchantPage from './EditRequestMerchantPage';
import EditRequestReceiptPage from './EditRequestReceiptPage';
@@ -129,19 +128,6 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p
[transaction, report, policy, policyTags, policyCategories],
);
- const saveCreated = useCallback(
- ({created: newCreated}) => {
- // If the value hasn't changed, don't request to save changes on the server and just close the modal
- if (newCreated === TransactionUtils.getCreated(transaction)) {
- Navigation.dismissModal();
- return;
- }
- IOU.updateMoneyRequestDate(transaction.transactionID, report.reportID, newCreated, policy, policyTags, policyCategories);
- Navigation.dismissModal();
- },
- [transaction, report, policy, policyTags, policyCategories],
- );
-
const saveMerchant = useCallback(
({merchant: newMerchant}) => {
const newTrimmedMerchant = newMerchant.trim();
@@ -190,15 +176,6 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p
[transactionCategory, transaction.transactionID, report.reportID, policy, policyTags, policyCategories],
);
- if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.DATE) {
- return (
-
- );
- }
-
if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.AMOUNT) {
return (
- );
- }
-
if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.AMOUNT) {
return (
{
- Linking.getInitialURL().then((url) => {
- const sessionEmail = props.session.email;
- const transitionUrl = NativeModules.HybridAppModule ? CONST.DEEPLINK_BASE_URL + initUrl : url;
- const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(transitionUrl, sessionEmail);
-
- if (isLoggingInAsNewUser) {
- Session.signOutAndRedirectToSignIn();
- }
-
- // We need to signin and fetch a new authToken, if a user was already authenticated in NewDot, and was redirected to OldDot
- // and their authToken stored in Onyx becomes invalid.
- // This workflow is triggered while setting up VBBA. User is redirected from NewDot to OldDot to set up 2FA, and then redirected back to NewDot
- // On Enabling 2FA, authToken stored in Onyx becomes expired and hence we need to fetch new authToken
- const shouldForceLogin = lodashGet(props, 'route.params.shouldForceLogin', '') === 'true';
- if (shouldForceLogin) {
- Log.info('LogOutPreviousUserPage - forcing login with shortLivedAuthToken');
- const email = lodashGet(props, 'route.params.email', '');
- const shortLivedAuthToken = lodashGet(props, 'route.params.shortLivedAuthToken', '');
- Session.signInWithShortLivedAuthToken(email, shortLivedAuthToken);
- }
-
- const exitTo = lodashGet(props, 'route.params.exitTo', '');
- // We don't want to navigate to the exitTo route when creating a new workspace from a deep link,
- // because we already handle creating the optimistic policy and navigating to it in App.setUpPoliciesAndNavigate,
- // which is already called when AuthScreens mounts.
- if (exitTo && exitTo !== ROUTES.WORKSPACE_NEW && !props.account.isLoading && !isLoggingInAsNewUser) {
- Navigation.isNavigationReady().then(() => {
- // remove this screen and navigate to exit route
- const exitUrl = NativeModules.HybridAppModule ? Navigation.parseHybridAppUrl(exitTo) : exitTo;
- Navigation.goBack();
- Navigation.navigate(exitUrl);
- });
- }
- });
- }, [initUrl, props]);
-
- return ;
-}
-
-LogOutPreviousUserPage.propTypes = propTypes;
-LogOutPreviousUserPage.defaultProps = defaultProps;
-LogOutPreviousUserPage.displayName = 'LogOutPreviousUserPage';
-
-export default withOnyx({
- account: {
- key: ONYXKEYS.ACCOUNT,
- },
- session: {
- key: ONYXKEYS.SESSION,
- },
-})(LogOutPreviousUserPage);
diff --git a/src/pages/LogOutPreviousUserPage.tsx b/src/pages/LogOutPreviousUserPage.tsx
new file mode 100644
index 000000000000..f68344604dfa
--- /dev/null
+++ b/src/pages/LogOutPreviousUserPage.tsx
@@ -0,0 +1,60 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useEffect} from 'react';
+import {Linking} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
+import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
+import * as SessionUtils from '@libs/SessionUtils';
+import type {AuthScreensParamList} from '@navigation/types';
+import * as SessionActions from '@userActions/Session';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type SCREENS from '@src/SCREENS';
+import type {Session} from '@src/types/onyx';
+
+type LogOutPreviousUserPageOnyxProps = {
+ /** The data about the current session which will be set once the user is authenticated and we return to this component as an AuthScreen */
+ session: OnyxEntry;
+};
+
+type LogOutPreviousUserPageProps = LogOutPreviousUserPageOnyxProps & StackScreenProps;
+
+// This page is responsible for handling transitions from OldDot. Specifically, it logs the current user
+// out if the transition is for another user.
+//
+// This component should not do any other navigation as that handled in App.setUpPoliciesAndNavigate
+function LogOutPreviousUserPage({session, route}: LogOutPreviousUserPageProps) {
+ useEffect(() => {
+ Linking.getInitialURL().then((transitionURL) => {
+ const sessionEmail = session?.email;
+ const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(transitionURL ?? undefined, sessionEmail);
+
+ if (isLoggingInAsNewUser) {
+ SessionActions.signOutAndRedirectToSignIn();
+ }
+
+ // We need to signin and fetch a new authToken, if a user was already authenticated in NewDot, and was redirected to OldDot
+ // and their authToken stored in Onyx becomes invalid.
+ // This workflow is triggered while setting up VBBA. User is redirected from NewDot to OldDot to set up 2FA, and then redirected back to NewDot
+ // On Enabling 2FA, authToken stored in Onyx becomes expired and hence we need to fetch new authToken
+ const shouldForceLogin = route.params.shouldForceLogin === 'true';
+ if (shouldForceLogin) {
+ const email = route.params.email ?? '';
+ const shortLivedAuthToken = route.params.shortLivedAuthToken ?? '';
+ SessionActions.signInWithShortLivedAuthToken(email, shortLivedAuthToken);
+ }
+ });
+
+ // We only want to run this effect once on mount (when the page first loads after transitioning from OldDot)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return ;
+}
+
+LogOutPreviousUserPage.displayName = 'LogOutPreviousUserPage';
+
+export default withOnyx({
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+})(LogOutPreviousUserPage);
diff --git a/src/pages/ReportAvatar.tsx b/src/pages/ReportAvatar.tsx
index 29142294084c..121b238012bf 100644
--- a/src/pages/ReportAvatar.tsx
+++ b/src/pages/ReportAvatar.tsx
@@ -22,9 +22,8 @@ type ReportAvatarProps = ReportAvatarOnyxProps & StackScreenProps {
+ playSound(SOUNDS.DONE);
const newComment = composerRef.current.prepareCommentAndResetComposer();
Report.addAttachment(reportID, file, newComment);
setTextInputShouldClear(false);
@@ -287,6 +289,7 @@ function ReportActionCompose({
return;
}
+ playSound(SOUNDS.DONE);
onSubmit(newComment);
},
[onSubmit],
diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx
index afe97b2b95c1..1caa951d16a7 100644
--- a/src/pages/home/report/ReportActionItemCreated.tsx
+++ b/src/pages/home/report/ReportActionItemCreated.tsx
@@ -117,6 +117,7 @@ export default withOnyx;
+ /** Metadata of the report currently being looked at */
+ reportMetadata: OnyxEntry;
+
/** The policies which the user has access to */
policies: OnyxCollection;
@@ -42,28 +45,29 @@ export default function (
return function (WrappedComponent: ComponentType>) {
function WithReportOrNotFound(props: TProps, ref: ForwardedRef) {
const contentShown = React.useRef(false);
+ const isReportIdInRoute = !!props.route.params.reportID?.length;
+ const isReportLoaded = !isEmptyObject(props.report) && !!props.report?.reportID;
- const isReportIdInRoute = props.route.params.reportID?.length;
+ // The `isLoadingInitialReportActions` value will become `false` only after the first OpenReport API call is finished (either succeeded or failed)
+ const shouldFetchReport = isReportIdInRoute && props.reportMetadata?.isLoadingInitialReportActions !== false;
// When accessing certain report-dependant pages (e.g. Task Title) by deeplink, the OpenReport API is not called,
// So we need to call OpenReport API here to make sure the report data is loaded if it exists on the Server
useEffect(() => {
- if (!isReportIdInRoute || !isEmptyObject(props.report)) {
+ if (isReportLoaded || !shouldFetchReport) {
// If the report is not required or is already loaded, we don't need to call the API
return;
}
Report.openReport(props.route.params.reportID);
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [isReportIdInRoute, props.route.params.reportID]);
+ }, [shouldFetchReport, isReportLoaded, props.route.params.reportID]);
if (shouldRequireReportID || isReportIdInRoute) {
- const shouldShowFullScreenLoadingIndicator = props.isLoadingReportData !== false && (!Object.entries(props.report ?? {}).length || !props.report?.reportID);
-
- const shouldShowNotFoundPage =
- !Object.entries(props.report ?? {}).length || !props.report?.reportID || !ReportUtils.canAccessReport(props.report, props.policies, props.betas);
+ const shouldShowFullScreenLoadingIndicator = !isReportLoaded && (props.isLoadingReportData !== false || shouldFetchReport);
+ const shouldShowNotFoundPage = !isReportLoaded || !ReportUtils.canAccessReport(props.report, props.policies, props.betas);
- // If the content was shown but it's not anymore that means the report was deleted and we are probably navigating out of this screen.
+ // If the content was shown, but it's not anymore, that means the report was deleted, and we are probably navigating out of this screen.
// Return null for this case to avoid rendering FullScreenLoadingIndicator or NotFoundPage when animating transition.
if (shouldShowNotFoundPage && contentShown.current) {
return null;
@@ -97,6 +101,9 @@ export default function (
report: {
key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`,
},
+ reportMetadata: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_METADATA}${route.params.reportID}`,
+ },
isLoadingReportData: {
key: ONYXKEYS.IS_LOADING_REPORT_DATA,
},
diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js
index 773d6b6d4acc..2fe44639e184 100644
--- a/src/pages/home/sidebar/SidebarLinks.js
+++ b/src/pages/home/sidebar/SidebarLinks.js
@@ -1,7 +1,9 @@
/* eslint-disable rulesdir/onyx-props-must-have-default */
+import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useRef} from 'react';
import {InteractionManager, StyleSheet, View} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import Breadcrumbs from '@components/Breadcrumbs';
import LHNOptionsList from '@components/LHNOptionsList/LHNOptionsList';
@@ -42,7 +44,7 @@ const propTypes = {
isActiveReport: PropTypes.func.isRequired,
};
-function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priorityMode = CONST.PRIORITY_MODE.DEFAULT, isActiveReport, isCreateMenuOpen}) {
+function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priorityMode = CONST.PRIORITY_MODE.DEFAULT, isActiveReport, isCreateMenuOpen, activePolicy}) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const modal = useRef({});
@@ -133,9 +135,14 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority
`${ONYXKEYS.COLLECTION.POLICY}${activeWorkspaceID}`,
+ },
+})(SidebarLinks);
export {basePropTypes};
diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js
index abe6d66b3759..3bd538e8beab 100644
--- a/src/pages/home/sidebar/SidebarLinksData.js
+++ b/src/pages/home/sidebar/SidebarLinksData.js
@@ -208,6 +208,7 @@ function SidebarLinksData({
isActiveReport={isActiveReport}
isLoading={isLoading}
optionListItems={optionListItemsWithCurrentReport}
+ activeWorkspaceID={activeWorkspaceID}
/>
);
diff --git a/src/pages/iou/MoneyRequestDatePage.js b/src/pages/iou/MoneyRequestDatePage.js
deleted file mode 100644
index f6159abd73f6..000000000000
--- a/src/pages/iou/MoneyRequestDatePage.js
+++ /dev/null
@@ -1,126 +0,0 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
-import React, {useEffect} from 'react';
-import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
-import DatePicker from '@components/DatePicker';
-import FormProvider from '@components/Form/FormProvider';
-import InputWrapper from '@components/Form/InputWrapper';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
-import ScreenWrapper from '@components/ScreenWrapper';
-import useLocalize from '@hooks/useLocalize';
-import useThemeStyles from '@hooks/useThemeStyles';
-import * as MoneyRequestUtils from '@libs/MoneyRequestUtils';
-import Navigation from '@libs/Navigation/Navigation';
-import * as IOU from '@userActions/IOU';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
-import {iouDefaultProps, iouPropTypes} from './propTypes';
-
-const propTypes = {
- /** Onyx Props */
- /** Holds data related to Money Request view state, rather than the underlying Money Request data. */
- iou: iouPropTypes,
-
- /** Route from navigation */
- route: PropTypes.shape({
- /** Params from the route */
- params: PropTypes.shape({
- /** The type of IOU report, i.e. bill, request, send */
- iouType: PropTypes.string,
-
- /** The report ID of the IOU */
- reportID: PropTypes.string,
-
- /** Which field we are editing */
- field: PropTypes.string,
-
- /** reportID for the "transaction thread" */
- threadReportID: PropTypes.string,
- }),
- }).isRequired,
-
- /** The current tab we have navigated to in the request modal. String that corresponds to the request type. */
- selectedTab: PropTypes.oneOf(_.values(CONST.TAB_REQUEST)).isRequired,
-};
-
-const defaultProps = {
- iou: iouDefaultProps,
-};
-
-function MoneyRequestDatePage({iou, route, selectedTab}) {
- const styles = useThemeStyles();
- const {translate} = useLocalize();
- const iouType = lodashGet(route, 'params.iouType', '');
- const reportID = lodashGet(route, 'params.reportID', '');
- const isDistanceRequest = MoneyRequestUtils.isDistanceRequest(iouType, selectedTab);
-
- useEffect(() => {
- const moneyRequestId = `${iouType}${reportID}`;
- const shouldReset = iou.id !== moneyRequestId;
- if (shouldReset) {
- IOU.resetMoneyRequestInfo(moneyRequestId);
- }
-
- if (!isDistanceRequest && (_.isEmpty(iou.participants) || (iou.amount === 0 && !iou.receiptPath) || shouldReset)) {
- Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID), true);
- }
- }, [iou.id, iou.participants, iou.amount, iou.receiptPath, iouType, reportID, isDistanceRequest]);
-
- function navigateBack() {
- Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID));
- }
-
- /**
- * Sets the money request comment by saving it to Onyx.
- *
- * @param {Object} value
- * @param {String} value.moneyRequestCreated
- */
- function updateDate(value) {
- IOU.setMoneyRequestCreated(value.moneyRequestCreated);
- navigateBack();
- }
-
- return (
-
- navigateBack()}
- />
- updateDate(value)}
- submitButtonText={translate('common.save')}
- enabledWhenOffline
- >
-
-
-
- );
-}
-
-MoneyRequestDatePage.propTypes = propTypes;
-MoneyRequestDatePage.defaultProps = defaultProps;
-MoneyRequestDatePage.displayName = 'MoneyRequestDatePage';
-
-export default withOnyx({
- iou: {
- key: ONYXKEYS.IOU,
- },
- selectedTab: {
- key: `${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.RECEIPT_TAB_ID}`,
- },
-})(MoneyRequestDatePage);
diff --git a/src/pages/iou/request/step/IOURequestStepDate.js b/src/pages/iou/request/step/IOURequestStepDate.js
index 5677ef46fcfa..d25ca4d26706 100644
--- a/src/pages/iou/request/step/IOURequestStepDate.js
+++ b/src/pages/iou/request/step/IOURequestStepDate.js
@@ -1,16 +1,24 @@
+import lodashGet from 'lodash/get';
+import lodashIsEmpty from 'lodash/isEmpty';
+import PropTypes from 'prop-types';
import React from 'react';
+import {withOnyx} from 'react-native-onyx';
+import categoryPropTypes from '@components/categoryPropTypes';
import DatePicker from '@components/DatePicker';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
+import tagPropTypes from '@components/tagPropTypes';
import transactionPropTypes from '@components/transactionPropTypes';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
import * as IOUUtils from '@libs/IOUUtils';
import Navigation from '@libs/Navigation/Navigation';
+import * as TransactionUtils from '@libs/TransactionUtils';
import * as IOU from '@userActions/IOU';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import {policyPropTypes} from '@src/pages/workspace/withPolicy';
import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes';
import StepScreenWrapper from './StepScreenWrapper';
import withFullTransactionOrNotFound from './withFullTransactionOrNotFound';
@@ -23,20 +31,44 @@ const propTypes = {
/** Onyx Props */
/** Holds data related to Money Request view state, rather than the underlying Money Request data. */
transaction: transactionPropTypes,
+
+ /** The draft transaction that holds data to be persisted on the current transaction */
+ splitDraftTransaction: transactionPropTypes,
+
+ /** The policy of the report */
+ policy: policyPropTypes.policy,
+
+ /** Collection of categories attached to a policy */
+ policyCategories: PropTypes.objectOf(categoryPropTypes),
+
+ /** Collection of tags attached to a policy */
+ policyTags: tagPropTypes,
};
const defaultProps = {
transaction: {},
+ splitDraftTransaction: {},
+ policy: null,
+ policyTags: null,
+ policyCategories: null,
};
function IOURequestStepDate({
route: {
- params: {iouType, backTo, transactionID},
+ params: {action, iouType, reportID, backTo},
},
transaction,
+ splitDraftTransaction,
+ policy,
+ policyTags,
+ policyCategories,
}) {
const styles = useThemeStyles();
const {translate} = useLocalize();
+ const isEditing = action === CONST.IOU.ACTION.EDIT;
+ // In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value
+ const isEditingSplitBill = iouType === CONST.IOU.TYPE.SPLIT && isEditing;
+ const currentCreated = isEditingSplitBill && !lodashIsEmpty(splitDraftTransaction) ? TransactionUtils.getCreated(splitDraftTransaction) : TransactionUtils.getCreated(transaction);
const navigateBack = () => {
Navigation.goBack(backTo);
@@ -47,7 +79,27 @@ function IOURequestStepDate({
* @param {String} value.moneyRequestCreated
*/
const updateDate = (value) => {
- IOU.setMoneyRequestCreated_temporaryForRefactor(transactionID, value.moneyRequestCreated);
+ const newCreated = value.moneyRequestCreated;
+
+ // Only update created if it has changed
+ if (newCreated === currentCreated) {
+ navigateBack();
+ return;
+ }
+
+ // In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value
+ if (isEditingSplitBill) {
+ IOU.setDraftSplitTransaction(transaction.transactionID, {created: newCreated});
+ navigateBack();
+ return;
+ }
+
+ IOU.setMoneyRequestCreated(transaction.transactionID, newCreated, action === CONST.IOU.ACTION.CREATE);
+
+ if (isEditing) {
+ IOU.updateMoneyRequestDate(transaction.transactionID, reportID, newCreated, policy, policyTags, policyCategories);
+ }
+
navigateBack();
};
@@ -70,7 +122,7 @@ function IOURequestStepDate({
InputComponent={DatePicker}
inputID="moneyRequestCreated"
label={translate('common.date')}
- defaultValue={transaction.created}
+ defaultValue={currentCreated}
maxDate={CONST.CALENDAR_PICKER.MAX_DATE}
minDate={CONST.CALENDAR_PICKER.MIN_DATE}
/>
@@ -83,4 +135,24 @@ IOURequestStepDate.propTypes = propTypes;
IOURequestStepDate.defaultProps = defaultProps;
IOURequestStepDate.displayName = 'IOURequestStepDate';
-export default compose(withWritableReportOrNotFound, withFullTransactionOrNotFound)(IOURequestStepDate);
+export default compose(
+ withWritableReportOrNotFound,
+ withFullTransactionOrNotFound,
+ withOnyx({
+ splitDraftTransaction: {
+ key: ({route}) => {
+ const transactionID = lodashGet(route, 'params.transactionID', 0);
+ return `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`;
+ },
+ },
+ policy: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`,
+ },
+ policyCategories: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`,
+ },
+ policyTags: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`,
+ },
+ }),
+)(IOURequestStepDate);
diff --git a/src/pages/iou/request/step/IOURequestStepDescription.js b/src/pages/iou/request/step/IOURequestStepDescription.js
index 3a50eff13918..f3324a311732 100644
--- a/src/pages/iou/request/step/IOURequestStepDescription.js
+++ b/src/pages/iou/request/step/IOURequestStepDescription.js
@@ -14,6 +14,7 @@ import transactionPropTypes from '@components/transactionPropTypes';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
+import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import updateMultilineInputRange from '@libs/updateMultilineInputRange';
import * as IOU from '@userActions/IOU';
@@ -88,6 +89,24 @@ function IOURequestStepDescription({
}, []),
);
+ /**
+ * @param {Object} values
+ * @param {String} values.title
+ * @returns {Object} - An object containing the errors for each inputID
+ */
+ const validate = useCallback((values) => {
+ const errors = {};
+
+ if (values.moneyRequestComment.length > CONST.DESCRIPTION_LIMIT) {
+ ErrorUtils.addErrorMessage(errors, 'moneyRequestComment', [
+ 'common.error.characterLimitExceedCounter',
+ {length: values.moneyRequestComment.length, limit: CONST.DESCRIPTION_LIMIT},
+ ]);
+ }
+
+ return errors;
+ }, []);
+
const navigateBack = () => {
Navigation.goBack(backTo);
};
@@ -132,6 +151,7 @@ function IOURequestStepDescription({
style={[styles.flexGrow1, styles.ph5]}
formID={ONYXKEYS.FORMS.MONEY_REQUEST_DESCRIPTION_FORM}
onSubmit={updateComment}
+ validate={validate}
submitButtonText={translate('common.save')}
enabledWhenOffline
>
diff --git a/src/pages/settings/Preferences/PreferencesPage.js b/src/pages/settings/Preferences/PreferencesPage.js
index 6ea7e1eb5280..0fe643884f25 100755
--- a/src/pages/settings/Preferences/PreferencesPage.js
+++ b/src/pages/settings/Preferences/PreferencesPage.js
@@ -78,6 +78,18 @@ function PreferencesPage(props) {
/>
+
+
+ {translate('preferencesPage.muteAllSounds')}
+
+
+
+
+
CONST.TITLE_CHARACTER_LIMIT) {
+ ErrorUtils.addErrorMessage(errors, 'legalFirstName', ['common.error.characterLimitExceedCounter', {length: values.legalFirstName.length, limit: CONST.TITLE_CHARACTER_LIMIT}]);
}
if (ValidationUtils.doesContainReservedWord(values.legalFirstName, CONST.DISPLAY_NAME.RESERVED_NAMES)) {
ErrorUtils.addErrorMessage(errors, 'legalFirstName', 'personalDetails.error.containsReservedWord');
}
- if (values.legalFirstName.length > CONST.LEGAL_NAME.MAX_LENGTH) {
- ErrorUtils.addErrorMessage(errors, 'legalFirstName', ['common.error.characterLimitExceedCounter', {length: values.legalFirstName.length, limit: CONST.LEGAL_NAME.MAX_LENGTH}]);
- }
if (!ValidationUtils.isValidLegalName(values.legalLastName)) {
ErrorUtils.addErrorMessage(errors, 'legalLastName', 'privatePersonalDetails.error.hasInvalidCharacter');
} else if (_.isEmpty(values.legalLastName)) {
errors.legalLastName = 'common.error.fieldRequired';
+ } else if (values.legalLastName.length > CONST.TITLE_CHARACTER_LIMIT) {
+ ErrorUtils.addErrorMessage(errors, 'legalLastName', ['common.error.characterLimitExceedCounter', {length: values.legalLastName.length, limit: CONST.TITLE_CHARACTER_LIMIT}]);
}
if (ValidationUtils.doesContainReservedWord(values.legalLastName, CONST.DISPLAY_NAME.RESERVED_NAMES)) {
ErrorUtils.addErrorMessage(errors, 'legalLastName', 'personalDetails.error.containsReservedWord');
}
- if (values.legalLastName.length > CONST.LEGAL_NAME.MAX_LENGTH) {
- ErrorUtils.addErrorMessage(errors, 'legalLastName', ['common.error.characterLimitExceedCounter', {length: values.legalLastName.length, limit: CONST.LEGAL_NAME.MAX_LENGTH}]);
- }
return errors;
}, []);
@@ -111,7 +109,6 @@ function LegalNamePage(props) {
aria-label={props.translate('privatePersonalDetails.legalFirstName')}
role={CONST.ROLE.PRESENTATION}
defaultValue={legalFirstName}
- maxLength={CONST.LEGAL_NAME.MAX_LENGTH + CONST.SEARCH_MAX_LENGTH}
spellCheck={false}
/>
@@ -124,7 +121,6 @@ function LegalNamePage(props) {
aria-label={props.translate('privatePersonalDetails.legalLastName')}
role={CONST.ROLE.PRESENTATION}
defaultValue={legalLastName}
- maxLength={CONST.LEGAL_NAME.MAX_LENGTH + CONST.SEARCH_MAX_LENGTH}
spellCheck={false}
/>
diff --git a/src/pages/settings/Report/RoomNamePage.tsx b/src/pages/settings/Report/RoomNamePage.tsx
index 30226bc6f502..aa2aa7dd5c07 100644
--- a/src/pages/settings/Report/RoomNamePage.tsx
+++ b/src/pages/settings/Report/RoomNamePage.tsx
@@ -65,6 +65,8 @@ function RoomNamePage({report, policy, reports}: RoomNamePageProps) {
} else if (ValidationUtils.isExistingRoomName(values.roomName, reports, report?.policyID ?? '')) {
// The room name can't be set to one that already exists on the policy
ErrorUtils.addErrorMessage(errors, 'roomName', 'newRoomPage.roomAlreadyExistsError');
+ } else if (values.roomName.length > CONST.TITLE_CHARACTER_LIMIT) {
+ ErrorUtils.addErrorMessage(errors, 'roomName', ['common.error.characterLimitExceedCounter', {length: values.roomName.length, limit: CONST.TITLE_CHARACTER_LIMIT}]);
}
return errors;
diff --git a/src/pages/tasks/NewTaskDescriptionPage.js b/src/pages/tasks/NewTaskDescriptionPage.js
index 4d84cac90537..54b0094dfcf7 100644
--- a/src/pages/tasks/NewTaskDescriptionPage.js
+++ b/src/pages/tasks/NewTaskDescriptionPage.js
@@ -12,6 +12,7 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
+import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import updateMultilineInputRange from '@libs/updateMultilineInputRange';
import * as Task from '@userActions/Task';
@@ -46,6 +47,20 @@ function NewTaskDescriptionPage(props) {
Navigation.goBack(ROUTES.NEW_TASK);
};
+ /**
+ * @param {Object} values - form input values passed by the Form component
+ * @returns {Boolean}
+ */
+ function validate(values) {
+ const errors = {};
+
+ if (values.taskDescription.length > CONST.DESCRIPTION_LIMIT) {
+ ErrorUtils.addErrorMessage(errors, 'taskDescription', ['common.error.characterLimitExceedCounter', {length: values.taskDescription.length, limit: CONST.DESCRIPTION_LIMIT}]);
+ }
+
+ return errors;
+ }
+
return (
validate(values)}
onSubmit={(values) => onSubmit(values)}
enabledWhenOffline
>
diff --git a/src/pages/tasks/NewTaskDetailsPage.js b/src/pages/tasks/NewTaskDetailsPage.js
index 4f4f2560a0d9..9595e1adbe76 100644
--- a/src/pages/tasks/NewTaskDetailsPage.js
+++ b/src/pages/tasks/NewTaskDetailsPage.js
@@ -57,6 +57,11 @@ function NewTaskDetailsPage(props) {
if (!values.taskTitle) {
// We error if the user doesn't enter a task name
ErrorUtils.addErrorMessage(errors, 'taskTitle', 'newTaskPage.pleaseEnterTaskName');
+ } else if (values.taskTitle.length > CONST.TITLE_CHARACTER_LIMIT) {
+ ErrorUtils.addErrorMessage(errors, 'taskTitle', ['common.error.characterLimitExceedCounter', {length: values.taskTitle.length, limit: CONST.TITLE_CHARACTER_LIMIT}]);
+ }
+ if (values.taskDescription.length > CONST.DESCRIPTION_LIMIT) {
+ ErrorUtils.addErrorMessage(errors, 'taskDescription', ['common.error.characterLimitExceedCounter', {length: values.taskDescription.length, limit: CONST.DESCRIPTION_LIMIT}]);
}
return errors;
diff --git a/src/pages/tasks/NewTaskPage.js b/src/pages/tasks/NewTaskPage.js
index bf54d02f778f..f77285190e62 100644
--- a/src/pages/tasks/NewTaskPage.js
+++ b/src/pages/tasks/NewTaskPage.js
@@ -17,6 +17,7 @@ import * as LocalePhoneNumber from '@libs/LocalePhoneNumber';
import Navigation from '@libs/Navigation/Navigation';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as ReportUtils from '@libs/ReportUtils';
+import playSound, {SOUNDS} from '@libs/Sound';
import reportPropTypes from '@pages/reportPropTypes';
import * as Task from '@userActions/Task';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -124,6 +125,7 @@ function NewTaskPage(props) {
return;
}
+ playSound(SOUNDS.DONE);
Task.createTaskAndNavigate(
parentReport.reportID,
props.task.title,
diff --git a/src/pages/tasks/NewTaskTitlePage.js b/src/pages/tasks/NewTaskTitlePage.js
index 7bf6065625c0..e6da2a06435d 100644
--- a/src/pages/tasks/NewTaskTitlePage.js
+++ b/src/pages/tasks/NewTaskTitlePage.js
@@ -48,6 +48,8 @@ function NewTaskTitlePage(props) {
if (!values.taskTitle) {
// We error if the user doesn't enter a task name
ErrorUtils.addErrorMessage(errors, 'taskTitle', 'newTaskPage.pleaseEnterTaskName');
+ } else if (values.taskTitle.length > CONST.TITLE_CHARACTER_LIMIT) {
+ ErrorUtils.addErrorMessage(errors, 'taskTitle', ['common.error.characterLimitExceedCounter', {length: values.taskTitle.length, limit: CONST.TITLE_CHARACTER_LIMIT}]);
}
return errors;
diff --git a/src/pages/tasks/TaskDescriptionPage.js b/src/pages/tasks/TaskDescriptionPage.js
index 48be7022b187..add2cf3da057 100644
--- a/src/pages/tasks/TaskDescriptionPage.js
+++ b/src/pages/tasks/TaskDescriptionPage.js
@@ -13,6 +13,7 @@ import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalD
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
+import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as ReportUtils from '@libs/ReportUtils';
import StringUtils from '@libs/StringUtils';
@@ -38,7 +39,20 @@ const defaultProps = {
const parser = new ExpensiMark();
function TaskDescriptionPage(props) {
const styles = useThemeStyles();
- const validate = useCallback(() => ({}), []);
+
+ /**
+ * @param {Object} values - form input values passed by the Form component
+ * @returns {Boolean}
+ */
+ const validate = useCallback((values) => {
+ const errors = {};
+
+ if (values.description.length > CONST.DESCRIPTION_LIMIT) {
+ ErrorUtils.addErrorMessage(errors, 'description', ['common.error.characterLimitExceedCounter', {length: values.description.length, limit: CONST.DESCRIPTION_LIMIT}]);
+ }
+
+ return errors;
+ }, []);
const submit = useCallback(
(values) => {
diff --git a/src/pages/tasks/TaskTitlePage.js b/src/pages/tasks/TaskTitlePage.js
index 9b3d28a0d032..9fd1f29d3a0d 100644
--- a/src/pages/tasks/TaskTitlePage.js
+++ b/src/pages/tasks/TaskTitlePage.js
@@ -12,6 +12,7 @@ import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalD
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
+import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as ReportUtils from '@libs/ReportUtils';
import withReportOrNotFound from '@pages/home/report/withReportOrNotFound';
@@ -44,6 +45,8 @@ function TaskTitlePage(props) {
if (_.isEmpty(values.title)) {
errors.title = 'newTaskPage.pleaseEnterTaskName';
+ } else if (values.title.length > CONST.TITLE_CHARACTER_LIMIT) {
+ ErrorUtils.addErrorMessage(errors, 'title', ['common.error.characterLimitExceedCounter', {length: values.title.length, limit: CONST.TITLE_CHARACTER_LIMIT}]);
}
return errors;
diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx
index ea243c56ac76..3f7bfc92d48d 100644
--- a/src/pages/workspace/WorkspaceInitialPage.tsx
+++ b/src/pages/workspace/WorkspaceInitialPage.tsx
@@ -150,11 +150,11 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
const menuItems: WorkspaceMenuItem[] = [
{
- translationKey: 'workspace.common.overview',
+ translationKey: 'workspace.common.profile',
icon: Expensicons.Home,
- action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW.getRoute(policyID)))),
+ action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE.getRoute(policyID)))),
brickRoadIndicator: hasGeneralSettingsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
- routeName: SCREENS.WORKSPACE.OVERVIEW,
+ routeName: SCREENS.WORKSPACE.PROFILE,
},
...(shouldShowProtectedItems ? protectedMenuItems : []),
];
diff --git a/src/pages/workspace/WorkspaceNamePage.tsx b/src/pages/workspace/WorkspaceNamePage.tsx
index e9d1ddd021d0..59679456be56 100644
--- a/src/pages/workspace/WorkspaceNamePage.tsx
+++ b/src/pages/workspace/WorkspaceNamePage.tsx
@@ -8,6 +8,7 @@ import ScreenWrapper from '@components/ScreenWrapper';
import TextInput from '@components/TextInput';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
+import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as ValidationUtils from '@libs/ValidationUtils';
import * as Policy from '@userActions/Policy';
@@ -41,10 +42,10 @@ function WorkspaceNamePage({policy}: Props) {
if (!ValidationUtils.isRequiredFulfilled(name)) {
errors.name = 'workspace.editor.nameIsRequiredError';
- } else if ([...name].length > CONST.WORKSPACE_NAME_CHARACTER_LIMIT) {
+ } else if ([...name].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.
- errors.name = 'workspace.editor.nameIsTooLongError';
+ ErrorUtils.addErrorMessage(errors, 'name', ['common.error.characterLimitExceedCounter', {length: [...name].length, limit: CONST.TITLE_CHARACTER_LIMIT}]);
}
return errors;
diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js
index b8676faf0510..5c77f3a03191 100644
--- a/src/pages/workspace/WorkspaceNewRoomPage.js
+++ b/src/pages/workspace/WorkspaceNewRoomPage.js
@@ -211,6 +211,8 @@ function WorkspaceNewRoomPage(props) {
} else if (ValidationUtils.isExistingRoomName(values.roomName, props.reports, values.policyID)) {
// Certain names are reserved for default rooms and should not be used for policy rooms.
ErrorUtils.addErrorMessage(errors, 'roomName', 'newRoomPage.roomAlreadyExistsError');
+ } else if (values.roomName.length > CONST.TITLE_CHARACTER_LIMIT) {
+ ErrorUtils.addErrorMessage(errors, 'roomName', ['common.error.characterLimitExceedCounter', {length: values.roomName.length, limit: CONST.TITLE_CHARACTER_LIMIT}]);
}
if (!values.policyID) {
diff --git a/src/pages/workspace/WorkspaceOverviewCurrencyPage.js b/src/pages/workspace/WorkspaceProfileCurrencyPage.js
similarity index 100%
rename from src/pages/workspace/WorkspaceOverviewCurrencyPage.js
rename to src/pages/workspace/WorkspaceProfileCurrencyPage.js
diff --git a/src/pages/workspace/WorkspaceOverviewPage.js b/src/pages/workspace/WorkspaceProfilePage.js
similarity index 93%
rename from src/pages/workspace/WorkspaceOverviewPage.js
rename to src/pages/workspace/WorkspaceProfilePage.js
index 4e18d09c9137..c030e7e3841a 100644
--- a/src/pages/workspace/WorkspaceOverviewPage.js
+++ b/src/pages/workspace/WorkspaceProfilePage.js
@@ -50,23 +50,23 @@ const defaultProps = {
...policyDefaultProps,
};
-function WorkspaceOverviewPage({policy, currencyList, route}) {
+function WorkspaceProfilePage({policy, currencyList, route}) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const formattedCurrency = !_.isEmpty(policy) && !_.isEmpty(currencyList) ? `${policy.outputCurrency} - ${currencyList[policy.outputCurrency].symbol}` : '';
- const onPressCurrency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW_CURRENCY.getRoute(policy.id)), [policy.id]);
- const onPressName = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW_NAME.getRoute(policy.id)), [policy.id]);
+ const onPressCurrency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_CURRENCY.getRoute(policy.id)), [policy.id]);
+ const onPressName = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_NAME.getRoute(policy.id)), [policy.id]);
const policyName = lodashGet(policy, 'name', '');
const readOnly = !PolicyUtils.isPolicyAdmin(policy);
return (
+
>;
describe('generateMonthMatrix', () => {
it('returns the correct matrix for January 2022', () => {
- const expected = [
+ const expected: MonthMatrix = [
[null, null, null, null, null, 1, 2],
[3, 4, 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14, 15, 16],
@@ -14,7 +16,7 @@ describe('generateMonthMatrix', () => {
});
it('returns the correct matrix for February 2022', () => {
- const expected = [
+ const expected: MonthMatrix = [
[null, 1, 2, 3, 4, 5, 6],
[7, 8, 9, 10, 11, 12, 13],
[14, 15, 16, 17, 18, 19, 20],
@@ -25,7 +27,7 @@ describe('generateMonthMatrix', () => {
});
it('returns the correct matrix for leap year February 2020', () => {
- const expected = [
+ const expected: MonthMatrix = [
[null, null, null, null, null, 1, 2],
[3, 4, 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14, 15, 16],
@@ -36,7 +38,7 @@ describe('generateMonthMatrix', () => {
});
it('returns the correct matrix for March 2022', () => {
- const expected = [
+ const expected: MonthMatrix = [
[null, 1, 2, 3, 4, 5, 6],
[7, 8, 9, 10, 11, 12, 13],
[14, 15, 16, 17, 18, 19, 20],
@@ -47,7 +49,7 @@ describe('generateMonthMatrix', () => {
});
it('returns the correct matrix for April 2022', () => {
- const expected = [
+ const expected: MonthMatrix = [
[null, null, null, null, 1, 2, 3],
[4, 5, 6, 7, 8, 9, 10],
[11, 12, 13, 14, 15, 16, 17],
@@ -58,7 +60,7 @@ describe('generateMonthMatrix', () => {
});
it('returns the correct matrix for December 2022', () => {
- const expected = [
+ const expected: MonthMatrix = [
[null, null, null, 1, 2, 3, 4],
[5, 6, 7, 8, 9, 10, 11],
[12, 13, 14, 15, 16, 17, 18],
@@ -69,7 +71,7 @@ describe('generateMonthMatrix', () => {
});
it('returns the correct matrix for January 2025', () => {
- const expected = [
+ const expected: MonthMatrix = [
[null, null, 1, 2, 3, 4, 5],
[6, 7, 8, 9, 10, 11, 12],
[13, 14, 15, 16, 17, 18, 19],
@@ -80,7 +82,7 @@ describe('generateMonthMatrix', () => {
});
it('returns the correct matrix for February 2025', () => {
- const expected = [
+ const expected: MonthMatrix = [
[null, null, null, null, null, 1, 2],
[3, 4, 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14, 15, 16],
@@ -91,7 +93,7 @@ describe('generateMonthMatrix', () => {
});
it('returns the correct matrix for June 2025', () => {
- const expected = [
+ const expected: MonthMatrix = [
[null, null, null, null, null, null, 1],
[2, 3, 4, 5, 6, 7, 8],
[9, 10, 11, 12, 13, 14, 15],
@@ -103,7 +105,7 @@ describe('generateMonthMatrix', () => {
});
it('returns the correct matrix for December 2025', () => {
- const expected = [
+ const expected: MonthMatrix = [
[1, 2, 3, 4, 5, 6, 7],
[8, 9, 10, 11, 12, 13, 14],
[15, 16, 17, 18, 19, 20, 21],
@@ -125,13 +127,6 @@ describe('generateMonthMatrix', () => {
expect(() => generateMonthMatrix(-1, 0)).toThrow();
});
- it('throws an error if year or month is not a number', () => {
- expect(() => generateMonthMatrix()).toThrow();
- expect(() => generateMonthMatrix(2022, 'invalid')).toThrow();
- expect(() => generateMonthMatrix('2022', '0')).toThrow();
- expect(() => generateMonthMatrix(null, undefined)).toThrow();
- });
-
it('returns a matrix with 6 rows and 7 columns for January 2022', () => {
const matrix = generateMonthMatrix(2022, 0);
expect(matrix.length).toBe(6);