diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 4f78ee6b69bf..e9e4da575b4b 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,2 +1,6 @@
# Every PR gets a review from an internal Expensify engineer
* @Expensify/pullerbear
+
+# Assign the Design team to review changes to our styles & assets
+src/styles/ @Expensify/design @Expensify/pullerbear
+assets/ @Expensify/design @Expensify/pullerbear
diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml
index 9887943c77e0..98f876fd60e2 100644
--- a/.github/workflows/reassurePerformanceTests.yml
+++ b/.github/workflows/reassurePerformanceTests.yml
@@ -1,8 +1,11 @@
name: Reassure Performance Tests
on:
+ push:
+ branches: [main]
+ paths-ignore: [docs/**, contributingGuides/**, jest/**, workflow_tests/**]
pull_request:
- types: [opened, synchronize, closed]
+ types: [opened, synchronize]
branches-ignore: [staging, production]
paths-ignore: [docs/**, .github/**, contributingGuides/**, tests/**, workflow_tests/**, '**.md', '**.sh']
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 13a6a9d3cc38..c9e5c5a4c15c 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 1001045807
- versionName "1.4.58-7"
+ versionCode 1001045902
+ versionName "1.4.59-2"
}
flavorDimensions "default"
diff --git a/android/app/src/main/java/com/expensify/chat/MainActivity.kt b/android/app/src/main/java/com/expensify/chat/MainActivity.kt
index 935ba8c8825f..2daebb9b1c00 100644
--- a/android/app/src/main/java/com/expensify/chat/MainActivity.kt
+++ b/android/app/src/main/java/com/expensify/chat/MainActivity.kt
@@ -14,6 +14,8 @@ import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
+import com.oblador.performance.RNPerformance
+
class MainActivity : ReactActivity() {
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
@@ -82,4 +84,9 @@ class MainActivity : ReactActivity() {
KeyCommandModule.getInstance().onKeyDownEvent(keyCode, event)
return super.onKeyUp(keyCode, event)
}
+
+ override fun onStart() {
+ super.onStart()
+ RNPerformance.getInstance().mark("appCreationEnd", false);
+ }
}
diff --git a/android/app/src/main/java/com/expensify/chat/MainApplication.kt b/android/app/src/main/java/com/expensify/chat/MainApplication.kt
index 2362af009979..e660a871359d 100644
--- a/android/app/src/main/java/com/expensify/chat/MainApplication.kt
+++ b/android/app/src/main/java/com/expensify/chat/MainApplication.kt
@@ -15,6 +15,7 @@ import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.modules.i18nmanager.I18nUtil
import com.facebook.soloader.SoLoader
import com.google.firebase.crashlytics.FirebaseCrashlytics
+import com.oblador.performance.RNPerformance
import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ReactNativeHostWrapper
@@ -42,6 +43,8 @@ class MainApplication : MultiDexApplication(), ReactApplication {
override fun onCreate() {
super.onCreate()
+ RNPerformance.getInstance().mark("appCreationStart", false);
+
if (isOnfidoProcess()) {
return
}
diff --git a/docs/redirects.csv b/docs/redirects.csv
index f239d95187c8..9c12c4b0048a 100644
--- a/docs/redirects.csv
+++ b/docs/redirects.csv
@@ -67,6 +67,12 @@ https://help.expensify.com/articles/expensify-classic/settings/Merge-Accounts,ht
https://help.expensify.com/articles/expensify-classic/settings/Preferences,https://help.expensify.com/expensify-classic/hubs/settings/account-settings
https://help.expensify.com/articles/expensify-classic/getting-started/support/Your-Expensify-Account-Manager,https://use.expensify.com/support
https://help.expensify.com/articles/expensify-classic/settings/Copilot,https://help.expensify.com/expensify-classic/hubs/copilots-and-delegates/
+https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/reports/Currency,https://help.expensify.com/articles/expensify-classic/reports/Currency
+https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/reports/Report-Fields-And-Titles,https://help.expensify.com/articles/expensify-classic/workspaces/reports/Report-Fields-And-Titles
+https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/reports/Scheduled-Submit,https://help.expensify.com/articles/expensify-classic/workspaces/reports/Scheduled-Submit
+https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/Categories,https://help.expensify.com/articles/expensify-classic/workspaces/Categori
+https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/Expenses,https://help.expensify.com/expensify-classic/hubs/expenses/
+https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/Per-Diem,https://help.expensify.com/articles/expensify-classic/expenses/Per-Diem-Expenses
https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/Budgets,https://help.expensify.com/articles/expensify-classic/workspaces/Budgets
https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/Reimbursement,https://help.expensify.com/articles/expensify-classic/send-payments/Reimbursing-Reports
https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/Tags,https://help.expensify.com/articles/expensify-classic/workspaces/Tags
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 059fe63535b7..1b5bbff8ef99 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.4.58
+ 1.4.59
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.58.7
+ 1.4.59.2
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index c98b0058973d..d19d1c9bd131 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.4.58
+ 1.4.59
CFBundleSignature
????
CFBundleVersion
- 1.4.58.7
+ 1.4.59.2
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index b8a7167bba64..ac74ce3219a0 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName
$(PRODUCT_NAME)
CFBundleShortVersionString
- 1.4.58
+ 1.4.59
CFBundleVersion
- 1.4.58.7
+ 1.4.59.2
NSExtension
NSExtensionPointIdentifier
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index b2995c35c9bc..24ef0704be25 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1415,7 +1415,7 @@ PODS:
- SDWebImage/Core (~> 5.17)
- SocketRocket (0.6.1)
- Turf (2.7.0)
- - VisionCamera (2.16.8):
+ - VisionCamera (4.0.0-beta.11):
- React
- React-callinvoker
- React-Core
@@ -1920,7 +1920,7 @@ SPEC CHECKSUMS:
SDWebImageWebPCoder: af09429398d99d524cae2fe00f6f0f6e491ed102
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2
- VisionCamera: 0a6794d1974aed5d653d0d0cb900493e2583e35a
+ VisionCamera: b6b6f46949eae83b71429c971162af337ef34fa3
Yoga: e64aa65de36c0832d04e8c7bd614396c77a80047
PODFILE CHECKSUM: a431c146e1501391834a2f299a74093bac53b530
diff --git a/package-lock.json b/package-lock.json
index eb1d8e0c3aec..095c444cdf91 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.58-7",
+ "version": "1.4.59-2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.58-7",
+ "version": "1.4.59-2",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -114,7 +114,7 @@
"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-vision-camera": "^4.0.0-beta.11",
"react-native-web": "^0.19.9",
"react-native-web-linear-gradient": "^1.1.2",
"react-native-web-sound": "^0.1.3",
@@ -39769,11 +39769,18 @@
}
},
"node_modules/react-native-vision-camera": {
- "version": "2.16.8",
- "license": "MIT",
+ "version": "4.0.0-beta.11",
+ "resolved": "https://registry.npmjs.org/react-native-vision-camera/-/react-native-vision-camera-4.0.0-beta.11.tgz",
+ "integrity": "sha512-cKg/nwT0q0H1ivEVG+PQt/QxhFgf/dd7SiMm7bCzlSCmt0T2tXyIdLsuY312iKBB2qasQZOYtzRsoQjipHQkDw==",
"peerDependencies": {
"react": "*",
- "react-native": "*"
+ "react-native": "*",
+ "react-native-worklets-core": "*"
+ },
+ "peerDependenciesMeta": {
+ "react-native-worklets-core": {
+ "optional": true
+ }
}
},
"node_modules/react-native-web": {
diff --git a/package.json b/package.json
index 0b464b3801e0..1b44c77e0a48 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.58-7",
+ "version": "1.4.59-2",
"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.",
@@ -165,7 +165,7 @@
"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-vision-camera": "^4.0.0-beta.11",
"react-native-web": "^0.19.9",
"react-native-web-linear-gradient": "^1.1.2",
"react-native-web-sound": "^0.1.3",
diff --git a/patches/react-native-vision-camera+2.16.8.patch b/patches/react-native-vision-camera+2.16.8.patch
deleted file mode 100644
index 3afc4573985d..000000000000
--- a/patches/react-native-vision-camera+2.16.8.patch
+++ /dev/null
@@ -1,22 +0,0 @@
-diff --git a/node_modules/react-native-vision-camera/android/build.gradle b/node_modules/react-native-vision-camera/android/build.gradle
-index ddfa243..bafffc3 100644
---- a/node_modules/react-native-vision-camera/android/build.gradle
-+++ b/node_modules/react-native-vision-camera/android/build.gradle
-@@ -334,7 +334,7 @@ if (ENABLE_FRAME_PROCESSORS) {
- def thirdPartyVersions = new Properties()
- thirdPartyVersions.load(new FileInputStream(thirdPartyVersionsFile))
-
-- def BOOST_VERSION = thirdPartyVersions["BOOST_VERSION"]
-+ def BOOST_VERSION = thirdPartyVersions["BOOST_VERSION"] ?: "1.83.0"
- def boost_file = new File(downloadsDir, "boost_${BOOST_VERSION}.tar.gz")
- def DOUBLE_CONVERSION_VERSION = thirdPartyVersions["DOUBLE_CONVERSION_VERSION"]
- def double_conversion_file = new File(downloadsDir, "double-conversion-${DOUBLE_CONVERSION_VERSION}.tar.gz")
-@@ -352,7 +352,7 @@ if (ENABLE_FRAME_PROCESSORS) {
-
- task downloadBoost(dependsOn: createNativeDepsDirectories, type: Download) {
- def transformedVersion = BOOST_VERSION.replace("_", ".")
-- def srcUrl = "https://boostorg.jfrog.io/artifactory/main/release/${transformedVersion}/source/boost_${BOOST_VERSION}.tar.gz"
-+ def srcUrl = "https://archives.boost.io/release/${transformedVersion}/source/boost_${BOOST_VERSION}.tar.gz"
- if (REACT_NATIVE_VERSION < 69) {
- srcUrl = "https://github.com/react-native-community/boost-for-react-native/releases/download/v${transformedVersion}-0/boost_${BOOST_VERSION}.tar.gz"
- }
diff --git a/src/CONST.ts b/src/CONST.ts
index f4c7ecb5215a..e7358b382f14 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -334,7 +334,6 @@ const CONST = {
ALL: 'all',
CHRONOS_IN_CASH: 'chronosInCash',
DEFAULT_ROOMS: 'defaultRooms',
- BETA_COMMENT_LINKING: 'commentLinking',
VIOLATIONS: 'violations',
REPORT_FIELDS: 'reportFields',
TRACK_EXPENSE: 'trackExpense',
@@ -1827,6 +1826,8 @@ const CONST = {
RECEIPT: 'receipt',
DISTANCE: 'distance',
TAG: 'tag',
+ TAX_RATE: 'taxRate',
+ TAX_AMOUNT: 'taxAmount',
},
FOOTER: {
EXPENSE_MANAGEMENT_URL: `${USE_EXPENSIFY_URL}/expense-management`,
@@ -3335,7 +3336,16 @@ const CONST = {
},
/**
- * Constants for types of violations.
+ * Constants for types of violation.
+ */
+ VIOLATION_TYPES: {
+ VIOLATION: 'violation',
+ NOTICE: 'notice',
+ WARNING: 'warning',
+ },
+
+ /**
+ * Constants for types of violation names.
* Defined here because they need to be referenced by the type system to generate the
* ViolationNames type.
*/
diff --git a/src/Expensify.tsx b/src/Expensify.tsx
index 026025593aef..6a57d6fdcc10 100644
--- a/src/Expensify.tsx
+++ b/src/Expensify.tsx
@@ -26,6 +26,7 @@ import NavigationRoot from './libs/Navigation/NavigationRoot';
import NetworkConnection from './libs/NetworkConnection';
import PushNotification from './libs/Notification/PushNotification';
import './libs/Notification/PushNotification/subscribePushNotification';
+import Performance from './libs/Performance';
import StartupTimer from './libs/StartupTimer';
// This lib needs to be imported, but it has nothing to export since all it contains is an Onyx connection
import './libs/UnreadIndicatorUpdater';
@@ -130,6 +131,7 @@ function Expensify({
const onSplashHide = useCallback(() => {
setIsSplashHidden(true);
+ Performance.markEnd(CONST.TIMING.SIDEBAR_LOADED);
}, []);
useLayoutEffect(() => {
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index c134d2a65db2..46b2c5f8055c 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -632,7 +632,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.RECENTLY_USED_REPORT_FIELDS]: OnyxTypes.RecentlyUsedReportFields;
[ONYXKEYS.UPDATE_REQUIRED]: boolean;
[ONYXKEYS.PLAID_CURRENT_EVENT]: string;
- [ONYXKEYS.LOGS]: Record;
+ [ONYXKEYS.LOGS]: OnyxTypes.CapturedLogs;
[ONYXKEYS.SHOULD_STORE_LOGS]: boolean;
[ONYXKEYS.CACHED_PDF_PATHS]: Record;
[ONYXKEYS.POLICY_OWNERSHIP_CHANGE_CHECKS]: Record;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 7050360c2e8e..8130c271a2db 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -131,6 +131,16 @@ const ROUTES = {
route: 'settings/profile/address/country',
getRoute: (country: string, backTo?: string) => getUrlWithBackToParam(`settings/profile/address/country?country=${country}`, backTo),
},
+ SETTINGS_ADDRESS_STATE: {
+ route: 'settings/profile/address/state',
+
+ getRoute: (state?: string, backTo?: string, label?: string) =>
+ `${getUrlWithBackToParam(`settings/profile/address/state${state ? `?state=${encodeURIComponent(state)}` : ''}`, backTo)}${
+ // the label param can be an empty string so we cannot use a nullish ?? operator
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ label ? `${backTo || state ? '&' : '?'}label=${encodeURIComponent(label)}` : ''
+ }` as const,
+ },
SETTINGS_CONTACT_METHODS: {
route: 'settings/profile/contact-methods',
getRoute: (backTo?: string) => getUrlWithBackToParam('settings/profile/contact-methods', backTo),
@@ -401,6 +411,16 @@ const ROUTES = {
`create/${iouType}/start/${transactionID}/${reportID}/scan` as const,
},
+ MONEY_REQUEST_STATE_SELECTOR: {
+ route: 'request/state',
+
+ getRoute: (state?: string, backTo?: string, label?: string) =>
+ `${getUrlWithBackToParam(`request/state${state ? `?state=${encodeURIComponent(state)}` : ''}`, backTo)}${
+ // the label param can be an empty string so we cannot use a nullish ?? operator
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ label ? `${backTo || state ? '&' : '?'}label=${encodeURIComponent(label)}` : ''
+ }` as const,
+ },
IOU_REQUEST: 'request/new',
IOU_SEND: 'send/new',
IOU_SEND_ADD_BANK_ACCOUNT: 'send/new/add-bank-account',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index c732594cdcbe..cf864fd96b3e 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -56,6 +56,7 @@ const SCREENS = {
DATE_OF_BIRTH: 'Settings_DateOfBirth',
ADDRESS: 'Settings_Address',
ADDRESS_COUNTRY: 'Settings_Address_Country',
+ ADDRESS_STATE: 'Settings_Address_State',
},
PREFERENCES: {
@@ -154,6 +155,7 @@ const SCREENS = {
WAYPOINT: 'Money_Request_Waypoint',
EDIT_WAYPOINT: 'Money_Request_Edit_Waypoint',
RECEIPT: 'Money_Request_Receipt',
+ STATE_SELECTOR: 'Money_Request_State_Selector',
},
IOU_SEND: {
diff --git a/src/components/AddressForm.tsx b/src/components/AddressForm.tsx
index 626da4cfd5d4..a4d1c2dd6bd6 100644
--- a/src/components/AddressForm.tsx
+++ b/src/components/AddressForm.tsx
@@ -16,7 +16,7 @@ import CountrySelector from './CountrySelector';
import FormProvider from './Form/FormProvider';
import InputWrapper from './Form/InputWrapper';
import type {FormOnyxValues} from './Form/types';
-import StatePicker from './StatePicker';
+import StateSelector from './StateSelector';
import TextInput from './TextInput';
type CountryZipRegex = {
@@ -190,7 +190,7 @@ function AddressForm({
{isUSAForm ? (
;
+
+ /** Whether or not logs should be stored */
+ shouldStoreLogs: OnyxEntry;
+};
+
+type BaseClientSideLoggingToolProps = {
+ /** Locally created file */
+ file?: {path: string; newFileName: string; size: number};
+ /** Action to run when pressing Share button */
+ onShareLogs?: () => void;
+ /** Action to run when disabling the switch */
+ onDisableLogging: (logs: Log[]) => void;
+ /** Action to run when enabling logging */
+ onEnableLogging?: () => void;
+} & BaseClientSideLoggingToolMenuOnyxProps;
+
+function BaseClientSideLoggingToolMenu({shouldStoreLogs, capturedLogs, file, onShareLogs, onDisableLogging, onEnableLogging}: BaseClientSideLoggingToolProps) {
+ const {translate} = useLocalize();
+
+ const onToggle = () => {
+ if (!shouldStoreLogs) {
+ Console.setShouldStoreLogs(true);
+
+ if (onEnableLogging) {
+ onEnableLogging();
+ }
+
+ return;
+ }
+
+ if (!capturedLogs) {
+ Alert.alert(translate('initialSettingsPage.troubleshoot.noLogsToShare'));
+ Console.disableLoggingAndFlushLogs();
+ return;
+ }
+
+ const logs = Object.values(capturedLogs);
+ const logsWithParsedMessages = parseStringifiedMessages(logs);
+
+ onDisableLogging(logsWithParsedMessages);
+ Console.disableLoggingAndFlushLogs();
+ };
+ const styles = useThemeStyles();
+ return (
+ <>
+
+
+
+ {!!file && (
+ <>
+ {`path: ${file.path}`}
+
+
+
+ >
+ )}
+ >
+ );
+}
+
+BaseClientSideLoggingToolMenu.displayName = 'BaseClientSideLoggingToolMenu';
+
+export default withOnyx({
+ capturedLogs: {
+ key: ONYXKEYS.LOGS,
+ },
+ shouldStoreLogs: {
+ key: ONYXKEYS.SHOULD_STORE_LOGS,
+ },
+})(BaseClientSideLoggingToolMenu);
diff --git a/src/components/ClientSideLoggingToolMenu/index.android.tsx b/src/components/ClientSideLoggingToolMenu/index.android.tsx
new file mode 100644
index 000000000000..0be6e96fcafe
--- /dev/null
+++ b/src/components/ClientSideLoggingToolMenu/index.android.tsx
@@ -0,0 +1,47 @@
+import React, {useState} from 'react';
+import RNFetchBlob from 'react-native-blob-util';
+import Share from 'react-native-share';
+import type {Log} from '@libs/Console';
+import localFileCreate from '@libs/localFileCreate';
+import BaseClientSideLoggingToolMenu from './BaseClientSideLoggingToolMenu';
+
+function ClientSideLoggingToolMenu() {
+ const [file, setFile] = useState<{path: string; newFileName: string; size: number}>();
+
+ const createAndSaveFile = (logs: Log[]) => {
+ localFileCreate('logs', JSON.stringify(logs, null, 2)).then((localFile) => {
+ RNFetchBlob.MediaCollection.copyToMediaStore(
+ {
+ name: localFile.newFileName,
+ parentFolder: '',
+ mimeType: 'text/plain',
+ },
+ 'Download',
+ localFile.path,
+ );
+ setFile(localFile);
+ });
+ };
+
+ const shareLogs = () => {
+ if (!file) {
+ return;
+ }
+ Share.open({
+ url: `file://${file.path}`,
+ });
+ };
+
+ return (
+ setFile(undefined)}
+ onDisableLogging={createAndSaveFile}
+ onShareLogs={shareLogs}
+ />
+ );
+}
+
+ClientSideLoggingToolMenu.displayName = 'ClientSideLoggingToolMenu';
+
+export default ClientSideLoggingToolMenu;
diff --git a/src/components/ClientSideLoggingToolMenu/index.ios.tsx b/src/components/ClientSideLoggingToolMenu/index.ios.tsx
new file mode 100644
index 000000000000..cc596e54a973
--- /dev/null
+++ b/src/components/ClientSideLoggingToolMenu/index.ios.tsx
@@ -0,0 +1,37 @@
+import React, {useState} from 'react';
+import Share from 'react-native-share';
+import type {Log} from '@libs/Console';
+import localFileCreate from '@libs/localFileCreate';
+import BaseClientSideLoggingToolMenu from './BaseClientSideLoggingToolMenu';
+
+function ClientSideLoggingToolMenu() {
+ const [file, setFile] = useState<{path: string; newFileName: string; size: number}>();
+
+ const createFile = (logs: Log[]) => {
+ localFileCreate('logs', JSON.stringify(logs, null, 2)).then((localFile) => {
+ setFile(localFile);
+ });
+ };
+
+ const shareLogs = () => {
+ if (!file) {
+ return;
+ }
+ Share.open({
+ url: `file://${file.path}`,
+ });
+ };
+
+ return (
+ setFile(undefined)}
+ onDisableLogging={createFile}
+ onShareLogs={shareLogs}
+ />
+ );
+}
+
+ClientSideLoggingToolMenu.displayName = 'ClientSideLoggingToolMenu';
+
+export default ClientSideLoggingToolMenu;
diff --git a/src/components/ClientSideLoggingToolMenu/index.tsx b/src/components/ClientSideLoggingToolMenu/index.tsx
new file mode 100644
index 000000000000..182c3bf99b74
--- /dev/null
+++ b/src/components/ClientSideLoggingToolMenu/index.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import type {Log} from '@libs/Console';
+import localFileDownload from '@libs/localFileDownload';
+import BaseClientSideLoggingToolMenu from './BaseClientSideLoggingToolMenu';
+
+function ClientSideLoggingToolMenu() {
+ const downloadFile = (logs: Log[]) => {
+ localFileDownload('logs', JSON.stringify(logs, null, 2));
+ };
+
+ return ;
+}
+
+ClientSideLoggingToolMenu.displayName = 'ClientSideLoggingToolMenu';
+
+export default ClientSideLoggingToolMenu;
diff --git a/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx b/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx
index c88364b7e8f7..c7816b710692 100644
--- a/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx
+++ b/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx
@@ -65,7 +65,6 @@ function YearPickerModal({isVisible, years, currentYear = new Date().getFullYear
onBackButtonPress={onClose}
/>
;
+
+ /** The transaction thread report associated with the current report, if any */
+ transactionThreadReport: OnyxEntry;
};
type MoneyReportHeaderProps = MoneyReportHeaderOnyxProps & {
@@ -40,14 +45,35 @@ type MoneyReportHeaderProps = MoneyReportHeaderOnyxProps & {
/** The policy tied to the money request report */
policy: OnyxEntry;
+
+ /** Array of report actions for the report */
+ reportActions: OnyxTypes.ReportAction[];
+
+ /** The reportID of the transaction thread report associated with this current report, if any */
+ // eslint-disable-next-line react/no-unused-prop-types
+ transactionThreadReportID?: string | null;
};
-function MoneyReportHeader({session, policy, chatReport, nextStep, report: moneyRequestReport}: MoneyReportHeaderProps) {
+function MoneyReportHeader({session, policy, chatReport, nextStep, report: moneyRequestReport, transactionThreadReport, reportActions}: MoneyReportHeaderProps) {
const styles = useThemeStyles();
+ const [isDeleteRequestModalVisible, setIsDeleteRequestModalVisible] = useState(false);
const {translate} = useLocalize();
const {windowWidth, isSmallScreenWidth} = useWindowDimensions();
const {reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(moneyRequestReport);
const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID);
+ const requestParentReportAction = useMemo(() => {
+ if (!reportActions || !transactionThreadReport?.parentReportActionID) {
+ return null;
+ }
+ return reportActions.find((action) => action.reportActionID === transactionThreadReport.parentReportActionID);
+ }, [reportActions, transactionThreadReport?.parentReportActionID]);
+ const isDeletedParentAction = ReportActionsUtils.isDeletedAction(requestParentReportAction as OnyxTypes.ReportAction);
+
+ // Only the requestor can delete the request, admins can only edit it.
+ const isActionOwner =
+ typeof requestParentReportAction?.actorAccountID === 'number' && typeof session?.accountID === 'number' && requestParentReportAction.actorAccountID === session?.accountID;
+ const canDeleteRequest =
+ isActionOwner && (ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) || ReportUtils.isTrackExpenseReport(transactionThreadReport)) && !isDeletedParentAction;
const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false);
const [paymentType, setPaymentType] = useState();
const [requestType, setRequestType] = useState<'pay' | 'approve'>();
@@ -106,6 +132,19 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money
}
};
+ const deleteTransaction = useCallback(() => {
+ if (requestParentReportAction) {
+ const iouTransactionID = requestParentReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? requestParentReportAction.originalMessage?.IOUTransactionID ?? '' : '';
+ if (ReportActionsUtils.isTrackExpenseAction(requestParentReportAction)) {
+ IOU.deleteTrackExpense(moneyRequestReport?.reportID ?? '', iouTransactionID, requestParentReportAction, true);
+ return;
+ }
+ IOU.deleteMoneyRequest(iouTransactionID, requestParentReportAction, true);
+ }
+
+ setIsDeleteRequestModalVisible(false);
+ }, [moneyRequestReport?.reportID, requestParentReportAction, setIsDeleteRequestModalVisible]);
+
// The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on
const isWaitingForSubmissionFromCurrentUser = useMemo(
() => chatReport?.isOwnPolicyExpenseChat && !policy?.harvesting?.enabled,
@@ -121,6 +160,23 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money
});
}
+ // If the report supports adding transactions to it, then it also supports deleting transactions from it.
+ if (canDeleteRequest && !isEmptyObject(transactionThreadReport)) {
+ threeDotsMenuItems.push({
+ icon: Expensicons.Trashcan,
+ text: translate('reportActionContextMenu.deleteAction', {action: requestParentReportAction}),
+ onSelected: () => setIsDeleteRequestModalVisible(true),
+ });
+ }
+
+ useEffect(() => {
+ if (canDeleteRequest) {
+ return;
+ }
+
+ setIsDeleteRequestModalVisible(false);
+ }, [canDeleteRequest]);
+
return (
+ setIsDeleteRequestModalVisible(false)}
+ prompt={translate('iou.deleteConfirmation')}
+ confirmText={translate('common.delete')}
+ cancelText={translate('common.cancel')}
+ danger
+ />
);
}
@@ -244,6 +310,9 @@ export default withOnyx({
nextStep: {
key: ({report}) => `${ONYXKEYS.COLLECTION.NEXT_STEP}${report.reportID}`,
},
+ transactionThreadReport: {
+ key: ({transactionThreadReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`,
+ },
session: {
key: ONYXKEYS.SESSION,
},
diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx
index 5d3231ca0a41..f451f5f15581 100644
--- a/src/components/MoneyRequestHeader.tsx
+++ b/src/components/MoneyRequestHeader.tsx
@@ -222,7 +222,7 @@ const MoneyRequestHeaderWithTransaction = withOnyx {
const parentReportAction = (report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : {}) as ReportAction & OriginalMessageIOU;
- return `${ONYXKEYS.COLLECTION.TRANSACTION}${parentReportAction.originalMessage.IOUTransactionID ?? 0}`;
+ return `${ONYXKEYS.COLLECTION.TRANSACTION}${parentReportAction?.originalMessage?.IOUTransactionID ?? 0}`;
},
},
shownHoldUseExplanation: {
diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
index afc0c7b703dc..546b2885e24f 100755
--- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
+++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
@@ -10,6 +10,7 @@ import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import useLocalize from '@hooks/useLocalize';
import usePermissions from '@hooks/usePermissions';
+import usePrevious from '@hooks/usePrevious';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
@@ -21,6 +22,7 @@ import * as MoneyRequestUtils from '@libs/MoneyRequestUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
+import {isTaxPolicyEnabled} from '@libs/PolicyUtils';
import * as ReceiptUtils from '@libs/ReceiptUtils';
import * as ReportUtils from '@libs/ReportUtils';
import playSound, {SOUNDS} from '@libs/Sound';
@@ -204,6 +206,11 @@ const defaultProps = {
isPolicyExpenseChat: false,
};
+const getTaxAmount = (transaction, defaultTaxValue) => {
+ const percentage = (transaction.taxRate ? transaction.taxRate.data.value : defaultTaxValue) || '';
+ return TransactionUtils.calculateTaxAmount(percentage, transaction.amount);
+};
+
function MoneyTemporaryForRefactorRequestConfirmationList({
bankAccountRoute,
canModifyParticipants,
@@ -277,7 +284,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]);
// A flag for showing tax rate
- const shouldShowTax = isPolicyExpenseChat && policy && lodashGet(policy, 'tax.trackingEnabled', policy.isTaxTrackingEnabled);
+ const shouldShowTax = isTaxPolicyEnabled(isPolicyExpenseChat, policy);
// A flag for showing the billable field
const shouldShowBillable = !lodashGet(policy, 'disabledFields.defaultBillable', true);
@@ -292,9 +299,9 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
);
const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction.taxAmount, iouCurrencyCode);
- const defaultTaxKey = taxRates.defaultExternalID;
- const defaultTaxName = (defaultTaxKey && `${taxRates.taxes[defaultTaxKey].name} (${taxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || '';
- const taxRateTitle = (transaction.taxRate && transaction.taxRate.text) || defaultTaxName;
+ const taxRateTitle = TransactionUtils.getDefaultTaxName(taxRates, transaction);
+
+ const previousTransactionAmount = usePrevious(transaction.amount);
const isFocused = useIsFocused();
const [formError, setFormError] = useState('');
@@ -362,6 +369,18 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
IOU.setMoneyRequestAmount_temporaryForRefactor(transaction.transactionID, amount, currency);
}, [shouldCalculateDistanceAmount, distance, rate, unit, transaction, currency]);
+ // Calculate and set tax amount in transaction draft
+ useEffect(() => {
+ const taxAmount = getTaxAmount(transaction, taxRates.defaultValue);
+ const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount));
+
+ if (transaction.taxAmount && previousTransactionAmount === transaction.amount) {
+ return IOU.setMoneyRequestTaxAmount(transaction.transactionID, transaction.taxAmount, true);
+ }
+
+ IOU.setMoneyRequestTaxAmount(transaction.transactionID, amountInSmallestCurrencyUnits, true);
+ }, [taxRates.defaultValue, transaction, previousTransactionAmount]);
+
/**
* Returns the participants with amount
* @param {Array} participants
@@ -855,7 +874,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
key={`${taxRates.name}${formattedTaxAmount}`}
shouldShowRightIcon={!isReadOnly}
title={formattedTaxAmount}
- description={taxRates.name}
+ description={translate('iou.taxAmount')}
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()))}
diff --git a/src/components/ProfilingToolMenu/index.native.tsx b/src/components/ProfilingToolMenu/index.native.tsx
index e6a89a317ac7..a202a39032ba 100644
--- a/src/components/ProfilingToolMenu/index.native.tsx
+++ b/src/components/ProfilingToolMenu/index.native.tsx
@@ -9,6 +9,7 @@ import Button from '@components/Button';
import Switch from '@components/Switch';
import TestToolRow from '@components/TestToolRow';
import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import toggleProfileTool from '@libs/actions/ProfilingTool';
import getPlatform from '@libs/getPlatform';
@@ -44,6 +45,7 @@ function ProfilingToolMenu({isProfilingInProgress = false}: ProfilingToolMenuPro
const [sharePath, setSharePath] = useState('');
const [totalMemory, setTotalMemory] = useState(0);
const [usedMemory, setUsedMemory] = useState(0);
+ const {translate} = useLocalize();
// eslint-disable-next-line @lwc/lwc/no-async-await
const stop = useCallback(async () => {
@@ -142,29 +144,24 @@ function ProfilingToolMenu({isProfilingInProgress = false}: ProfilingToolMenuPro
return (
<>
-
- Release options
-
-
-
+
- {!!pathIOS && `path: ${pathIOS}`}
{!!pathIOS && (
-
-
-
+ <>
+ {`path: ${pathIOS}`}
+
+
+
+ >
)}
>
);
diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
index 2e9f9a553b71..2c6f14cec4c2 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
+++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
@@ -160,7 +160,7 @@ function MoneyRequestPreviewContent({
const violations = TransactionUtils.getTransactionViolations(transaction.transactionID, transactionViolations);
if (violations?.[0]) {
const violationMessage = ViolationsUtils.getViolationTranslation(violations[0], translate);
- const violationsCount = violations.filter((v) => v.type === 'violation').length;
+ const violationsCount = violations.filter((v) => v.type === CONST.VIOLATION_TYPES.VIOLATION).length;
const isTooLong = violationsCount > 1 || violationMessage.length > 15;
const hasViolationsAndFieldErrors = violationsCount > 0 && hasFieldErrors;
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index 074baa586dba..c052843fefe2 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -23,6 +23,7 @@ import * as CardUtils from '@libs/CardUtils';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
+import {isTaxPolicyEnabled} from '@libs/PolicyUtils';
import * as ReceiptUtils from '@libs/ReceiptUtils';
import * as ReportUtils from '@libs/ReportUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
@@ -100,6 +101,8 @@ function MoneyRequestView({
const {
created: transactionDate,
amount: transactionAmount,
+ taxAmount: transactionTaxAmount,
+ taxCode: transactionTaxCode,
currency: transactionCurrency,
comment: transactionDescription,
merchant: transactionMerchant,
@@ -119,6 +122,15 @@ function MoneyRequestView({
const isCardTransaction = TransactionUtils.isCardTransaction(transaction);
const cardProgramName = isCardTransaction && transactionCardID !== undefined ? CardUtils.getCardDescription(transactionCardID) : '';
const isApproved = ReportUtils.isReportApproved(moneyRequestReport);
+ const taxRates = policy?.taxRates;
+ const formattedTaxAmount = transactionTaxAmount ? CurrencyUtils.convertToDisplayString(transactionTaxAmount, transactionCurrency) : '';
+
+ const taxRatesDescription = taxRates?.name;
+ const taxRateTitle =
+ taxRates &&
+ (transactionTaxCode === taxRates?.defaultExternalID
+ ? transaction && TransactionUtils.getDefaultTaxName(taxRates, transaction)
+ : transactionTaxCode && TransactionUtils.getTaxName(taxRates?.taxes, transactionTaxCode));
// Flags for allowing or disallowing editing a money request
const isSettled = ReportUtils.isSettled(moneyRequestReport?.reportID);
@@ -147,6 +159,9 @@ function MoneyRequestView({
const shouldShowTag = isPolicyExpenseChat && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists));
const shouldShowBillable = isPolicyExpenseChat && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true));
+ // A flag for showing tax rate
+ const shouldShowTax = isTaxPolicyEnabled(isPolicyExpenseChat, policy) && transactionTaxCode && transactionTaxAmount;
+
const {getViolationsForField} = useViolations(transactionViolations ?? []);
const hasViolations = useCallback(
(field: ViolationField, data?: OnyxTypes.TransactionViolation['data']): boolean => !!canUseViolations && getViolationsForField(field, data).length > 0,
@@ -423,6 +438,31 @@ function MoneyRequestView({
/>
)}
+ {shouldShowTax && (
+
+ Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.TAX_RATE))}
+ />
+
+ )}
+
+ {shouldShowTax && (
+
+ Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.TAX_AMOUNT))}
+ />
+
+ )}
{shouldShowBillable && (
diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx
index 42fdc7dc575e..cd1a40b5ef5d 100644
--- a/src/components/SelectionList/BaseListItem.tsx
+++ b/src/components/SelectionList/BaseListItem.tsx
@@ -4,9 +4,7 @@ import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
-import SelectCircle from '@components/SelectCircle';
import useHover from '@hooks/useHover';
-import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
@@ -17,15 +15,12 @@ function BaseListItem({
pressableStyle,
wrapperStyle,
containerStyle,
- selectMultipleStyle,
isDisabled = false,
shouldPreventDefaultFocusOnSelectRow = false,
canSelectMultiple = false,
onSelectRow,
- onCheckboxPress,
onDismissError = () => {},
rightHandSideComponent,
- checkmarkPosition = CONST.DIRECTION.LEFT,
keyForList,
errors,
pendingAction,
@@ -34,7 +29,6 @@ function BaseListItem({
}: BaseListItemProps) {
const theme = useTheme();
const styles = useThemeStyles();
- const StyleUtils = useStyleUtils();
const {hovered, bind} = useHover();
const rightHandSideComponentRender = () => {
@@ -49,14 +43,6 @@ function BaseListItem({
return rightHandSideComponent;
};
- const handleCheckboxPress = () => {
- if (onCheckboxPress) {
- onCheckboxPress(item);
- } else {
- onSelectRow(item);
- }
- };
-
return (
onDismissError(item)}
@@ -80,45 +66,8 @@ function BaseListItem({
style={pressableStyle}
>
- {canSelectMultiple && checkmarkPosition === CONST.DIRECTION.LEFT && (
-
-
- {item.isSelected && (
-
- )}
-
-
- )}
-
{typeof children === 'function' ? children(hovered) : children}
- {canSelectMultiple && checkmarkPosition === CONST.DIRECTION.RIGHT && (
-
-
-
- )}
-
{!canSelectMultiple && item.isSelected && !rightHandSideComponent && (
(
shouldShowTooltips = true,
shouldUseDynamicMaxToRenderPerBatch = false,
rightHandSideComponent,
- checkmarkPosition,
isLoadingNewOptions = false,
onLayout,
customListHeader,
listHeaderWrapperStyle,
isRowMultilineSupported = false,
textInputRef,
+ headerMessageStyle,
}: BaseSelectionListProps,
ref: ForwardedRef,
) {
@@ -334,7 +334,6 @@ function BaseSelectionList(
onDismissError={() => onDismissError?.(item)}
shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow}
rightHandSideComponent={rightHandSideComponent}
- checkmarkPosition={checkmarkPosition}
keyForList={item.keyForList ?? ''}
isMultilineSupported={isRowMultilineSupported}
/>
@@ -516,7 +515,7 @@ function BaseSelectionList(
)}
{!!headerMessage && (
-
+
{headerMessage}
)}
diff --git a/src/components/SelectionList/InviteMemberListItem.tsx b/src/components/SelectionList/InviteMemberListItem.tsx
new file mode 100644
index 000000000000..aabde59ac374
--- /dev/null
+++ b/src/components/SelectionList/InviteMemberListItem.tsx
@@ -0,0 +1,134 @@
+import Str from 'expensify-common/lib/str';
+import React, {useCallback} from 'react';
+import {View} from 'react-native';
+import MultipleAvatars from '@components/MultipleAvatars';
+import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
+import SelectCircle from '@components/SelectCircle';
+import SubscriptAvatar from '@components/SubscriptAvatar';
+import Text from '@components/Text';
+import TextWithTooltip from '@components/TextWithTooltip';
+import useLocalize from '@hooks/useLocalize';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
+import BaseListItem from './BaseListItem';
+import type {InviteMemberListItemProps} from './types';
+
+function InviteMemberListItem({
+ item,
+ isFocused,
+ showTooltip,
+ isDisabled,
+ canSelectMultiple,
+ onSelectRow,
+ onCheckboxPress,
+ onDismissError,
+ shouldPreventDefaultFocusOnSelectRow,
+ rightHandSideComponent,
+}: InviteMemberListItemProps) {
+ const styles = useThemeStyles();
+ const theme = useTheme();
+ const StyleUtils = useStyleUtils();
+ const {translate} = useLocalize();
+
+ const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor;
+ const subscriptAvatarBorderColor = isFocused ? focusedBackgroundColor : theme.sidebar;
+ const hoveredBackgroundColor = !!styles.sidebarLinkHover && 'backgroundColor' in styles.sidebarLinkHover ? styles.sidebarLinkHover.backgroundColor : theme.sidebar;
+
+ const handleCheckboxPress = useCallback(() => {
+ if (onCheckboxPress) {
+ onCheckboxPress(item);
+ } else {
+ onSelectRow(item);
+ }
+ }, [item, onCheckboxPress, onSelectRow]);
+
+ return (
+
+ {translate('workspace.people.invitedBySecondaryLogin', {secondaryLogin: item.invitedSecondaryLogin})}
+
+ ) : undefined
+ }
+ keyForList={item.keyForList}
+ >
+ {(hovered?: boolean) => (
+ <>
+ {!!item.icons &&
+ (item.shouldShowSubscript ? (
+
+ ) : (
+
+ ))}
+
+
+ {!!item.alternateText && (
+
+ )}
+
+ {!!item.rightElement && item.rightElement}
+ {canSelectMultiple && (
+
+
+
+ )}
+ >
+ )}
+
+ );
+}
+
+InviteMemberListItem.displayName = 'InviteMemberListItem';
+
+export default InviteMemberListItem;
diff --git a/src/components/SelectionList/RadioListItem.tsx b/src/components/SelectionList/RadioListItem.tsx
index e7823209cf7e..b6b11c2f273c 100644
--- a/src/components/SelectionList/RadioListItem.tsx
+++ b/src/components/SelectionList/RadioListItem.tsx
@@ -1,7 +1,6 @@
import React from 'react';
import {View} from 'react-native';
import TextWithTooltip from '@components/TextWithTooltip';
-import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import BaseListItem from './BaseListItem';
@@ -12,17 +11,13 @@ function RadioListItem({
isFocused,
showTooltip,
isDisabled,
- canSelectMultiple,
onSelectRow,
- onCheckboxPress,
onDismissError,
shouldPreventDefaultFocusOnSelectRow,
rightHandSideComponent,
- checkmarkPosition,
isMultilineSupported = false,
}: RadioListItemProps) {
const styles = useThemeStyles();
- const StyleUtils = useStyleUtils();
const fullTitle = isMultilineSupported ? item.text?.trimStart() : item.text;
const indentsLength = (item.text?.length ?? 0) - (fullTitle?.length ?? 0);
const paddingLeft = Math.floor(indentsLength / CONST.INDENTS.length) * styles.ml3.marginLeft;
@@ -31,17 +26,13 @@ function RadioListItem({
<>
diff --git a/src/components/SelectionList/TableListItem.tsx b/src/components/SelectionList/TableListItem.tsx
index b7c3ed549d82..a233f5dd83fd 100644
--- a/src/components/SelectionList/TableListItem.tsx
+++ b/src/components/SelectionList/TableListItem.tsx
@@ -1,10 +1,14 @@
-import React from 'react';
+import React, {useCallback} from 'react';
import {View} from 'react-native';
+import Icon from '@components/Icon';
+import * as Expensicons from '@components/Icon/Expensicons';
import MultipleAvatars from '@components/MultipleAvatars';
+import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import TextWithTooltip from '@components/TextWithTooltip';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
import BaseListItem from './BaseListItem';
import type {TableListItemProps} from './types';
@@ -19,7 +23,6 @@ function TableListItem({
onDismissError,
shouldPreventDefaultFocusOnSelectRow,
rightHandSideComponent,
- checkmarkPosition,
}: TableListItemProps) {
const styles = useThemeStyles();
const theme = useTheme();
@@ -28,29 +31,55 @@ function TableListItem({
const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor;
const hoveredBackgroundColor = styles.sidebarLinkHover?.backgroundColor ? styles.sidebarLinkHover.backgroundColor : theme.sidebar;
+ const handleCheckboxPress = useCallback(() => {
+ if (onCheckboxPress) {
+ onCheckboxPress(item);
+ } else {
+ onSelectRow(item);
+ }
+ }, [item, onCheckboxPress, onSelectRow]);
+
return (
{(hovered) => (
<>
+ {canSelectMultiple && (
+
+
+ {item.isSelected && (
+
+ )}
+
+
+ )}
{!!item.icons && (
{
+ if (onCheckboxPress) {
+ onCheckboxPress(item);
+ } else {
+ onSelectRow(item);
+ }
+ }, [item, onCheckboxPress, onSelectRow]);
+
return (
{(hovered?: boolean) => (
<>
+ {canSelectMultiple && (
+
+
+ {item.isSelected && (
+
+ )}
+
+
+ )}
{!!item.icons &&
(item.shouldShowSubscript ? (
(props: BaseSelectionListProps, ref: ForwardedRef) {
- return (
-
- // eslint-disable-next-line react/jsx-props-no-spreading
- {...props}
- ref={ref}
- onScrollBeginDrag={() => Keyboard.dismiss()}
- />
- );
-}
-
-SelectionList.displayName = 'SelectionList';
-
-export default forwardRef(SelectionList);
diff --git a/src/components/SelectionList/index.android.tsx b/src/components/SelectionList/index.native.tsx
similarity index 96%
rename from src/components/SelectionList/index.android.tsx
rename to src/components/SelectionList/index.native.tsx
index f8e54b219f5b..baccdf7c6024 100644
--- a/src/components/SelectionList/index.android.tsx
+++ b/src/components/SelectionList/index.native.tsx
@@ -10,7 +10,6 @@ function SelectionList(props: BaseSelectionListProps Keyboard.dismiss()}
/>
);
diff --git a/src/components/SelectionList/selectionListPropTypes.js b/src/components/SelectionList/selectionListPropTypes.js
deleted file mode 100644
index eed1966f8222..000000000000
--- a/src/components/SelectionList/selectionListPropTypes.js
+++ /dev/null
@@ -1,200 +0,0 @@
-import PropTypes from 'prop-types';
-import _ from 'underscore';
-import sourcePropTypes from '@components/Image/sourcePropTypes';
-import CONST from '@src/CONST';
-
-const commonListItemPropTypes = {
- /** Whether this item is focused (for arrow key controls) */
- isFocused: PropTypes.bool,
-
- /** Style to be applied to Text */
- textStyles: PropTypes.arrayOf(PropTypes.object),
-
- /** Style to be applied on the alternate text */
- alternateTextStyles: PropTypes.arrayOf(PropTypes.object),
-
- /** Whether this item is disabled */
- isDisabled: PropTypes.bool,
-
- /** Whether this item should show Tooltip */
- showTooltip: PropTypes.bool.isRequired,
-
- /** Whether to use the Checkbox (multiple selection) instead of the Checkmark (single selection) */
- canSelectMultiple: PropTypes.bool,
-
- /** Callback to fire when the item is selected */
- onSelectRow: PropTypes.func.isRequired,
-
- /** Callback to fire when an error is dismissed */
- onDismissError: PropTypes.func,
-};
-
-const userListItemPropTypes = {
- ...commonListItemPropTypes,
-
- /** The section list item */
- item: PropTypes.shape({
- /** Text to display */
- text: PropTypes.string.isRequired,
-
- /** Alternate text to display */
- alternateText: PropTypes.string,
-
- /** Key used internally by React */
- keyForList: PropTypes.string.isRequired,
-
- /** Whether this option is selected */
- isSelected: PropTypes.bool,
-
- /** Whether this option is disabled for selection */
- isDisabled: PropTypes.bool,
-
- /** User accountID */
- accountID: PropTypes.number,
-
- /** User login */
- login: PropTypes.string,
-
- /** Element to show on the right side of the item */
- rightElement: PropTypes.element,
-
- /** Icons for the user (can be multiple if it's a Workspace) */
- icons: PropTypes.arrayOf(
- PropTypes.shape({
- source: PropTypes.oneOfType([PropTypes.string, sourcePropTypes]).isRequired,
- name: PropTypes.string,
- type: PropTypes.string,
- }),
- ),
-
- /** Errors that this user may contain */
- errors: PropTypes.objectOf(PropTypes.string),
-
- /** The type of action that's pending */
- pendingAction: PropTypes.oneOf(_.values(CONST.RED_BRICK_ROAD_PENDING_ACTION)),
- }).isRequired,
-};
-
-const radioListItemPropTypes = {
- ...commonListItemPropTypes,
-
- /** The section list item */
- item: PropTypes.shape({
- /** Text to display */
- text: PropTypes.string.isRequired,
-
- /** Alternate text to display */
- alternateText: PropTypes.string,
-
- /** Key used internally by React */
- keyForList: PropTypes.string.isRequired,
-
- /** Whether this option is selected */
- isSelected: PropTypes.bool,
- }).isRequired,
-};
-
-const baseListItemPropTypes = {
- ...commonListItemPropTypes,
- item: PropTypes.oneOfType([PropTypes.shape(userListItemPropTypes.item), PropTypes.shape(radioListItemPropTypes.item)]),
- shouldPreventDefaultFocusOnSelectRow: PropTypes.bool,
-};
-
-const propTypes = {
- /** Sections for the section list */
- sections: PropTypes.arrayOf(
- PropTypes.shape({
- /** Title of the section */
- title: PropTypes.string,
-
- /** Array of options */
- data: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.shape(userListItemPropTypes.item), PropTypes.shape(radioListItemPropTypes.item)])),
-
- /** Whether this section items disabled for selection */
- isDisabled: PropTypes.bool,
- }),
- ).isRequired,
-
- /** Whether this is a multi-select list */
- canSelectMultiple: PropTypes.bool,
-
- /** Callback to fire when a row is pressed */
- onSelectRow: PropTypes.func.isRequired,
-
- /** Callback to fire when "Select All" checkbox is pressed. Only use along with `canSelectMultiple` */
- onSelectAll: PropTypes.func,
-
- /** Callback to fire when an error is dismissed */
- onDismissError: PropTypes.func,
-
- /** Label for the text input */
- textInputLabel: PropTypes.string,
-
- /** Placeholder for the text input */
- textInputPlaceholder: PropTypes.string,
-
- /** Value for the text input */
- textInputValue: PropTypes.string,
-
- /** Max length for the text input */
- textInputMaxLength: PropTypes.number,
-
- /** Callback to fire when the text input changes */
- onChangeText: PropTypes.func,
-
- /** Input mode for the text input */
- inputMode: PropTypes.string,
-
- /** Item `keyForList` to focus initially */
- initiallyFocusedOptionKey: PropTypes.string,
-
- /** Callback to fire when the list is scrolled */
- onScroll: PropTypes.func,
-
- /** Callback to fire when the list is scrolled and the user begins dragging */
- onScrollBeginDrag: PropTypes.func,
-
- /** Message to display at the top of the list */
- headerMessage: PropTypes.string,
-
- /** Text to display on the confirm button */
- confirmButtonText: PropTypes.string,
-
- /** Callback to fire when the confirm button is pressed */
- onConfirm: PropTypes.func,
-
- /** Whether to show the vertical scroll indicator */
- showScrollIndicator: PropTypes.bool,
-
- /** Whether to show the loading placeholder */
- showLoadingPlaceholder: PropTypes.bool,
-
- /** Whether to show the default confirm button */
- showConfirmButton: PropTypes.bool,
-
- /** Whether to stop automatic form submission on pressing enter key or not */
- shouldStopPropagation: PropTypes.bool,
-
- /** Whether to prevent default focusing of options and focus the textinput when selecting an option */
- shouldPreventDefaultFocusOnSelectRow: PropTypes.bool,
-
- /** A ref to forward to the TextInput */
- inputRef: PropTypes.oneOfType([PropTypes.object]),
-
- /** Custom content to display in the header */
- headerContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
-
- /** Custom content to display in the footer */
- footerContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
-
- /** Whether to show the tooltip text */
- shouldShowTooltips: PropTypes.bool,
-
- /** Whether to use dynamic maxToRenderPerBatch depending on the visible number of elements */
- shouldUseDynamicMaxToRenderPerBatch: PropTypes.bool,
-
- /** Right hand side component to display in the list item. Function has list item passed as the param */
- rightHandSideComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
-};
-
-export {propTypes, baseListItemPropTypes, radioListItemPropTypes, userListItemPropTypes};
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index 88a864af2728..9e7e64896f4f 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -1,11 +1,11 @@
import type {MutableRefObject, ReactElement, ReactNode} from 'react';
import type {GestureResponderEvent, InputModeOptions, LayoutChangeEvent, SectionListData, StyleProp, TextInput, TextStyle, ViewStyle} from 'react-native';
-import type {ValueOf} from 'type-fest';
import type {MaybePhraseKey} from '@libs/Localize';
import type CONST from '@src/CONST';
import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon';
import type {ReceiptErrors} from '@src/types/onyx/Transaction';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
+import type InviteMemberListItem from './InviteMemberListItem';
import type RadioListItem from './RadioListItem';
import type TableListItem from './TableListItem';
import type UserListItem from './UserListItem';
@@ -35,9 +35,6 @@ type CommonListItemProps = {
/** Component to display on the right side */
rightHandSideComponent?: ((item: TItem) => ReactElement) | ReactElement | null;
- /** Direction of checkmark to show */
- checkmarkPosition?: ValueOf;
-
/** Styles for the pressable component */
pressableStyle?: StyleProp;
@@ -153,10 +150,14 @@ type UserListItemProps = ListItemProps & {
FooterComponent?: ReactElement;
};
+type InviteMemberListItemProps = UserListItemProps;
+
type RadioListItemProps = ListItemProps;
type TableListItemProps = ListItemProps;
+type ValidListItem = typeof RadioListItem | typeof UserListItem | typeof TableListItem | typeof InviteMemberListItem;
+
type Section = {
/** Title of the section */
title?: string;
@@ -181,7 +182,7 @@ type BaseSelectionListProps = Partial & {
sections: Array>> | typeof CONST.EMPTY_ARRAY;
/** Default renderer for every item in the list */
- ListItem: typeof RadioListItem | typeof UserListItem | typeof TableListItem;
+ ListItem: ValidListItem;
/** Whether this is a multi-select list */
canSelectMultiple?: boolean;
@@ -231,6 +232,9 @@ type BaseSelectionListProps = Partial & {
/** Message to display at the top of the list */
headerMessage?: string;
+ /** Styles to apply to the header message */
+ headerMessageStyle?: StyleProp;
+
/** Text to display on the confirm button */
confirmButtonText?: string;
@@ -267,24 +271,15 @@ type BaseSelectionListProps = Partial & {
/** Whether keyboard shortcuts should be disabled */
disableKeyboardShortcuts?: boolean;
- /** Whether to disable initial styling for focused option */
- disableInitialFocusOptionStyle?: boolean;
-
/** Styles to apply to SelectionList container */
containerStyle?: ViewStyle;
/** Whether keyboard is visible on the screen */
isKeyboardShown?: boolean;
- /** Whether focus event should be delayed */
- shouldDelayFocus?: boolean;
-
/** Component to display on the right side of each child */
rightHandSideComponent?: ((item: ListItem) => ReactElement) | ReactElement | null;
- /** Direction of checkmark to show */
- checkmarkPosition?: ValueOf;
-
/** Whether to show the loading indicator for new options */
isLoadingNewOptions?: boolean;
@@ -297,9 +292,6 @@ type BaseSelectionListProps = Partial & {
/** Styles for the list header wrapper */
listHeaderWrapperStyle?: StyleProp;
- /** Whether to auto focus the Search Input */
- autoFocus?: boolean;
-
/** Whether to wrap long text up to 2 lines */
isRowMultilineSupported?: boolean;
@@ -337,6 +329,7 @@ export type {
UserListItemProps,
RadioListItemProps,
TableListItemProps,
+ InviteMemberListItemProps,
ListItem,
ListItemProps,
FlattenedSectionsReturn,
@@ -344,4 +337,5 @@ export type {
ButtonOrCheckBoxRoles,
SectionListDataType,
SelectionListHandle,
+ ValidListItem,
};
diff --git a/src/components/StatePicker/StateSelectorModal.tsx b/src/components/StatePicker/StateSelectorModal.tsx
deleted file mode 100644
index 11cd38056f0c..000000000000
--- a/src/components/StatePicker/StateSelectorModal.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST';
-import React, {useEffect, useMemo} from 'react';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
-import Modal from '@components/Modal';
-import ScreenWrapper from '@components/ScreenWrapper';
-import SelectionList from '@components/SelectionList';
-import RadioListItem from '@components/SelectionList/RadioListItem';
-import useLocalize from '@hooks/useLocalize';
-import useThemeStyles from '@hooks/useThemeStyles';
-import searchCountryOptions from '@libs/searchCountryOptions';
-import type {CountryData} from '@libs/searchCountryOptions';
-import StringUtils from '@libs/StringUtils';
-import CONST from '@src/CONST';
-
-type State = keyof typeof COMMON_CONST.STATES;
-
-type StateSelectorModalProps = {
- /** Whether the modal is visible */
- isVisible: boolean;
-
- /** State value selected */
- currentState?: State;
-
- /** Function to call when the user selects a State */
- onStateSelected?: (state: CountryData) => void;
-
- /** Function to call when the user closes the State modal */
- onClose?: () => void;
-
- /** The search value from the selection list */
- searchValue: string;
-
- /** Function to call when the user types in the search input */
- setSearchValue: (value: string) => void;
-
- /** Label to display on field */
- label?: string;
-};
-
-function StateSelectorModal({currentState, isVisible, onClose = () => {}, onStateSelected = () => {}, searchValue, setSearchValue, label}: StateSelectorModalProps) {
- const styles = useThemeStyles();
- const {translate} = useLocalize();
-
- useEffect(() => {
- if (isVisible) {
- return;
- }
- setSearchValue('');
- }, [isVisible, setSearchValue]);
-
- const countryStates: CountryData[] = useMemo(
- () =>
- Object.keys(COMMON_CONST.STATES).map((state) => {
- const stateName = translate(`allStates.${state as State}.stateName`);
- const stateISO = translate(`allStates.${state as State}.stateISO`);
- return {
- value: stateISO,
- keyForList: stateISO,
- text: stateName,
- isSelected: currentState === stateISO,
- searchValue: StringUtils.sanitizeString(`${stateISO}${stateName}`),
- };
- }),
- [translate, currentState],
- );
-
- const searchResults = searchCountryOptions(searchValue, countryStates);
- const headerMessage = searchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : '';
-
- return (
-
-
-
-
-
-
- );
-}
-
-StateSelectorModal.displayName = 'StateSelectorModal';
-
-export default StateSelectorModal;
-export type {State};
diff --git a/src/components/StatePicker/index.tsx b/src/components/StatePicker/index.tsx
deleted file mode 100644
index 72262346b0d7..000000000000
--- a/src/components/StatePicker/index.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST';
-import React, {useState} from 'react';
-import type {ForwardedRef} from 'react';
-import {View} from 'react-native';
-import FormHelpMessage from '@components/FormHelpMessage';
-import type {MenuItemProps} from '@components/MenuItem';
-import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
-import useLocalize from '@hooks/useLocalize';
-import useThemeStyles from '@hooks/useThemeStyles';
-import type {MaybePhraseKey} from '@libs/Localize';
-import type {CountryData} from '@libs/searchCountryOptions';
-import StateSelectorModal from './StateSelectorModal';
-import type {State} from './StateSelectorModal';
-
-type StatePickerProps = {
- /** Error text to display */
- errorText?: MaybePhraseKey;
-
- /** State to display */
- value?: State;
-
- /** Callback to call when the input changes */
- onInputChange?: (value: string) => void;
-
- /** Label to display on field */
- label?: string;
-
- /** Any additional styles to apply */
- wrapperStyle?: MenuItemProps['wrapperStyle'];
-
- /** Callback to call when the picker modal is dismissed */
- onBlur?: () => void;
-};
-
-function StatePicker({value, onInputChange, label, onBlur, errorText = '', wrapperStyle}: StatePickerProps, ref: ForwardedRef) {
- const styles = useThemeStyles();
- const {translate} = useLocalize();
- const [isPickerVisible, setIsPickerVisible] = useState(false);
- const [searchValue, setSearchValue] = useState('');
-
- const showPickerModal = () => {
- setIsPickerVisible(true);
- };
-
- const hidePickerModal = (shouldBlur = true) => {
- if (shouldBlur) {
- onBlur?.();
- }
- setIsPickerVisible(false);
- };
-
- const updateStateInput = (state: CountryData) => {
- if (state.value !== value) {
- onInputChange?.(state.value);
- }
- // If the user selects any state, call the hidePickerModal function with shouldBlur = false
- // to prevent the onBlur function from being called.
- hidePickerModal(false);
- };
-
- const title = value && Object.keys(COMMON_CONST.STATES).includes(value) ? translate(`allStates.${value}.stateName`) : '';
- const descStyle = title.length === 0 ? styles.textNormal : null;
-
- return (
-
-
-
-
-
-
-
- );
-}
-
-StatePicker.displayName = 'StatePicker';
-
-export default React.forwardRef(StatePicker);
diff --git a/src/components/StateSelector.tsx b/src/components/StateSelector.tsx
new file mode 100644
index 000000000000..aa458834c8ad
--- /dev/null
+++ b/src/components/StateSelector.tsx
@@ -0,0 +1,107 @@
+import {useIsFocused} from '@react-navigation/native';
+import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST';
+import React, {useEffect, useRef} from 'react';
+import type {ForwardedRef} from 'react';
+import {View} from 'react-native';
+import useGeographicalStateFromRoute from '@hooks/useGeographicalStateFromRoute';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import type {MaybePhraseKey} from '@libs/Localize';
+import Navigation from '@libs/Navigation/Navigation';
+import ROUTES from '@src/ROUTES';
+import FormHelpMessage from './FormHelpMessage';
+import type {MenuItemProps} from './MenuItem';
+import MenuItemWithTopDescription from './MenuItemWithTopDescription';
+
+type State = keyof typeof COMMON_CONST.STATES;
+
+type StateSelectorProps = {
+ /** Form error text. e.g when no state is selected */
+ errorText?: MaybePhraseKey;
+
+ /** Current selected state */
+ value?: State | '';
+
+ /** Callback to call when the input changes */
+ onInputChange?: (value: string) => void;
+
+ /** Label to display on field */
+ label?: string;
+
+ /** Any additional styles to apply */
+ wrapperStyle?: MenuItemProps['wrapperStyle'];
+
+ /** Callback to call when the picker modal is dismissed */
+ onBlur?: () => void;
+
+ /** object to get route details from */
+ stateSelectorRoute?: typeof ROUTES.SETTINGS_ADDRESS_STATE | typeof ROUTES.MONEY_REQUEST_STATE_SELECTOR;
+};
+
+function StateSelector(
+ {errorText, onBlur, value: stateCode, label, onInputChange, wrapperStyle, stateSelectorRoute = ROUTES.SETTINGS_ADDRESS_STATE}: StateSelectorProps,
+ ref: ForwardedRef,
+) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const stateFromUrl = useGeographicalStateFromRoute();
+
+ const didOpenStateSelector = useRef(false);
+ const isFocused = useIsFocused();
+
+ useEffect(() => {
+ // Check if the state selector was opened and no value was selected, triggering onBlur to display an error
+ if (isFocused && didOpenStateSelector.current) {
+ didOpenStateSelector.current = false;
+ if (!stateFromUrl) {
+ onBlur?.();
+ }
+ }
+
+ // If no state is selected from the URL, exit the effect early to avoid further processing.
+ if (!stateFromUrl) {
+ return;
+ }
+
+ // If a state is selected, invoke `onInputChange` to update the form and clear any validation errors related to the state selection.
+ if (onInputChange) {
+ onInputChange(stateFromUrl);
+ }
+
+ // Clears the `state` parameter from the URL to ensure the component state is driven by the parent component rather than URL parameters.
+ // This helps prevent issues where the component might not update correctly if the state is controlled by both the parent and the URL.
+ Navigation.setParams({state: undefined});
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [stateFromUrl, onBlur, isFocused]);
+
+ const title = stateCode && Object.keys(COMMON_CONST.STATES).includes(stateCode) ? translate(`allStates.${stateCode}.stateName`) : '';
+ const descStyle = title.length === 0 ? styles.textNormal : null;
+
+ return (
+
+ {
+ const activeRoute = Navigation.getActiveRoute();
+ didOpenStateSelector.current = true;
+ Navigation.navigate(stateSelectorRoute.getRoute(stateCode, activeRoute, label));
+ }}
+ wrapperStyle={wrapperStyle}
+ />
+
+
+
+
+ );
+}
+
+StateSelector.displayName = 'StateSelector';
+
+export default React.forwardRef(StateSelector);
diff --git a/src/components/TaxPicker.tsx b/src/components/TaxPicker.tsx
index 936bd23b530d..4980025024ed 100644
--- a/src/components/TaxPicker.tsx
+++ b/src/components/TaxPicker.tsx
@@ -1,22 +1,31 @@
-import React, {useCallback, useMemo, useState} from 'react';
+import React, {useMemo, useState} from 'react';
+import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
import type {EdgeInsets} from 'react-native-safe-area-context';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import CONST from '@src/CONST';
-import type {TaxRatesWithDefault} from '@src/types/onyx';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Policy} from '@src/types/onyx';
import SelectionList from './SelectionList';
import RadioListItem from './SelectionList/RadioListItem';
import type {ListItem} from './SelectionList/types';
-type TaxPickerProps = {
- /** Collection of tax rates attached to a policy */
- taxRates?: TaxRatesWithDefault;
+type TaxPickerOnyxProps = {
+ /** The policy which the user has access to and which the report is tied to */
+ policy: OnyxEntry;
+};
+type TaxPickerProps = TaxPickerOnyxProps & {
/** The selected tax rate of an expense */
selectedTaxRate?: string;
+ /** ID of the policy */
+ // eslint-disable-next-line react/no-unused-prop-types
+ policyID?: string;
+
/**
* Safe area insets required for reflecting the portion of the view,
* that is not covered by navigation bars, tab bars, toolbars, and other ancestor views.
@@ -27,18 +36,17 @@ type TaxPickerProps = {
onSubmit: (tax: ListItem) => void;
};
-function TaxPicker({selectedTaxRate = '', taxRates, insets, onSubmit}: TaxPickerProps) {
+function TaxPicker({selectedTaxRate = '', policy, insets, onSubmit}: TaxPickerProps) {
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const [searchValue, setSearchValue] = useState('');
+ const taxRates = policy?.taxRates;
const taxRatesCount = TransactionUtils.getEnabledTaxRateCount(taxRates?.taxes ?? {});
const isTaxRatesCountBelowThreshold = taxRatesCount < CONST.TAX_RATES_LIST_THRESHOLD;
const shouldShowTextInput = !isTaxRatesCountBelowThreshold;
- const getTaxName = useCallback((key: string) => taxRates?.taxes[key]?.name, [taxRates?.taxes]);
-
const selectedOptions = useMemo(() => {
if (!selectedTaxRate) {
return [];
@@ -46,36 +54,39 @@ function TaxPicker({selectedTaxRate = '', taxRates, insets, onSubmit}: TaxPicker
return [
{
- name: getTaxName(selectedTaxRate),
+ name: selectedTaxRate,
enabled: true,
accountID: null,
},
];
- }, [selectedTaxRate, getTaxName]);
+ }, [selectedTaxRate]);
- const sections = useMemo(
- () => OptionsListUtils.getTaxRatesSection(taxRates, selectedOptions as OptionsListUtils.Category[], searchValue, selectedTaxRate),
- [taxRates, searchValue, selectedOptions, selectedTaxRate],
- );
+ const sections = useMemo(() => OptionsListUtils.getTaxRatesSection(taxRates, selectedOptions as OptionsListUtils.Category[], searchValue), [taxRates, searchValue, selectedOptions]);
const headerMessage = OptionsListUtils.getHeaderMessageForNonUserList(sections[0].data.length > 0, searchValue);
+ const selectedOptionKey = useMemo(() => sections?.[0]?.data?.find((taxRate) => taxRate.searchText === selectedTaxRate)?.keyForList, [sections, selectedTaxRate]);
+
return (
);
}
TaxPicker.displayName = 'TaxPicker';
-export default TaxPicker;
+export default withOnyx({
+ policy: {
+ key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ },
+})(TaxPicker);
diff --git a/src/components/TestToolsModal.tsx b/src/components/TestToolsModal.tsx
index 824162e63c51..026eafc7a13d 100644
--- a/src/components/TestToolsModal.tsx
+++ b/src/components/TestToolsModal.tsx
@@ -3,14 +3,18 @@ import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import useEnvironment from '@hooks/useEnvironment';
+import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
+import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import toggleTestToolsModal from '@userActions/TestTool';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import ClientSideLoggingToolMenu from './ClientSideLoggingToolMenu';
import Modal from './Modal';
import ProfilingToolMenu from './ProfilingToolMenu';
import TestToolMenu from './TestToolMenu';
+import Text from './Text';
type TestToolsModalOnyxProps = {
/** Whether the test tools modal is open */
@@ -23,6 +27,8 @@ function TestToolsModal({isTestToolsModalOpen = false}: TestToolsModalProps) {
const {isDevelopment} = useEnvironment();
const {windowWidth} = useWindowDimensions();
const StyleUtils = useStyleUtils();
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
return (
{isDevelopment && }
+
+ {translate('initialSettingsPage.troubleshoot.releaseOptions')}
+
+
);
diff --git a/src/components/VideoPlayer/BaseVideoPlayer.js b/src/components/VideoPlayer/BaseVideoPlayer.js
index 91737ad3938a..7016de3fa86c 100644
--- a/src/components/VideoPlayer/BaseVideoPlayer.js
+++ b/src/components/VideoPlayer/BaseVideoPlayer.js
@@ -114,7 +114,7 @@ function BaseVideoPlayer({
const currentDuration = e.durationMillis || videoDuration * 1000;
const currentPositon = e.positionMillis || 0;
- if (shouldReplayVideo(e, isVideoPlaying, currentDuration, currentPositon)) {
+ if (shouldReplayVideo(e, isPlaying, currentDuration, currentPositon)) {
videoPlayerRef.current.setStatusAsync({positionMillis: 0, shouldPlay: true});
}
@@ -127,6 +127,7 @@ function BaseVideoPlayer({
videoStateRef.current = e;
onPlaybackStatusUpdate(e);
},
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to trigger this when isPlaying changes because isPlaying is only used inside shouldReplayVideo
[onPlaybackStatusUpdate, preventPausingWhenExitingFullscreen, videoDuration],
);
diff --git a/src/hooks/useGeographicalStateFromRoute.ts b/src/hooks/useGeographicalStateFromRoute.ts
new file mode 100644
index 000000000000..13936ee78f5b
--- /dev/null
+++ b/src/hooks/useGeographicalStateFromRoute.ts
@@ -0,0 +1,23 @@
+import {useRoute} from '@react-navigation/native';
+import type {ParamListBase, RouteProp} from '@react-navigation/native';
+import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST';
+
+type CustomParamList = ParamListBase & Record>;
+type State = keyof typeof COMMON_CONST.STATES;
+
+/**
+ * Extracts the 'state' (default) query parameter from the route/ url and validates it against COMMON_CONST.STATES, returning its ISO code or `undefined`.
+ * Example 1: Url: https://new.expensify.com/settings/profile/address?state=MO Returns: MO
+ * Example 2: Url: https://new.expensify.com/settings/profile/address?state=ASDF Returns: undefined
+ * Example 3: Url: https://new.expensify.com/settings/profile/address Returns: undefined
+ * Example 4: Url: https://new.expensify.com/settings/profile/address?state=MO-hash-a12341 Returns: MO
+ */
+export default function useGeographicalStateFromRoute(stateParamName = 'state'): State | undefined {
+ const route = useRoute>();
+ const stateFromUrlTemp = route.params?.[stateParamName] as string | undefined;
+
+ if (!stateFromUrlTemp) {
+ return;
+ }
+ return COMMON_CONST.STATES[stateFromUrlTemp as State]?.stateISO;
+}
diff --git a/src/hooks/useViolations.ts b/src/hooks/useViolations.ts
index 3df457f1205a..7aadb4ce4ca3 100644
--- a/src/hooks/useViolations.ts
+++ b/src/hooks/useViolations.ts
@@ -1,4 +1,5 @@
import {useCallback, useMemo} from 'react';
+import CONST from '@src/CONST';
import type {TransactionViolation, ViolationName} from '@src/types/onyx';
/**
@@ -49,7 +50,7 @@ type ViolationsMap = Map;
function useViolations(violations: TransactionViolation[]) {
const violationsByField = useMemo((): ViolationsMap => {
- const filteredViolations = violations.filter((violation) => violation.type === 'violation');
+ const filteredViolations = violations.filter((violation) => violation.type === CONST.VIOLATION_TYPES.VIOLATION);
const violationGroups = new Map();
for (const violation of filteredViolations) {
const field = violationFields[violation.name];
diff --git a/src/languages/en.ts b/src/languages/en.ts
index fd2daa50942f..752d17e37d03 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -891,6 +891,11 @@ export default {
submitBug: 'submit a bug',
confirmResetDescription: 'All unsent draft messages will be lost, but the rest of your data is safe.',
resetAndRefresh: 'Reset and refresh',
+ clientSideLogging: 'Client side logging',
+ noLogsToShare: 'No logs to share',
+ useProfiling: 'Use profiling',
+ profileTrace: 'Profile trace',
+ releaseOptions: 'Release options',
},
debugConsole: {
saveLog: 'Save log',
@@ -899,6 +904,7 @@ export default {
execute: 'Execute',
noLogsAvailable: 'No logs available',
logSizeTooLarge: ({size}: LogSizeParams) => `Log size exceeds the limit of ${size} MB. Please use "Save log" to download the log file instead.`,
+ logs: 'Logs',
},
security: 'Security',
signOut: 'Sign out',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 1482ad5e0c5c..53ee6d3fba79 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -889,6 +889,11 @@ export default {
submitBug: 'envíe un error',
confirmResetDescription: 'Todos los borradores no enviados se perderán, pero el resto de tus datos estarán a salvo.',
resetAndRefresh: 'Restablecer y actualizar',
+ clientSideLogging: 'Logs del cliente',
+ noLogsToShare: 'No hay logs que compartir',
+ useProfiling: 'Usar el trazado',
+ profileTrace: 'Traza de ejecución',
+ releaseOptions: 'Opciones de publicación',
},
debugConsole: {
saveLog: 'Guardar registro',
@@ -897,6 +902,7 @@ export default {
execute: 'Ejecutar',
noLogsAvailable: 'No hay registros disponibles',
logSizeTooLarge: ({size}: LogSizeParams) => `El tamaño del registro excede el límite de ${size} MB. Utilice "Guardar registro" para descargar el archivo de registro.`,
+ logs: 'Logs',
},
security: 'Seguridad',
restoreStashed: 'Restablecer login guardado',
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 10ba657d397c..3cff726a530c 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -142,6 +142,8 @@ const WRITE_COMMANDS = {
UPDATE_MONEY_REQUEST_BILLABLE: 'UpdateMoneyRequestBillable',
UPDATE_MONEY_REQUEST_MERCHANT: 'UpdateMoneyRequestMerchant',
UPDATE_MONEY_REQUEST_TAG: 'UpdateMoneyRequestTag',
+ UPDATE_MONEY_REQUEST_TAX_AMOUNT: 'UpdateMoneyRequestTaxAmount',
+ UPDATE_MONEY_REQUEST_TAX_RATE: 'UpdateMoneyRequestTaxRate',
UPDATE_MONEY_REQUEST_DISTANCE: 'UpdateMoneyRequestDistance',
UPDATE_MONEY_REQUEST_CATEGORY: 'UpdateMoneyRequestCategory',
UPDATE_MONEY_REQUEST_DESCRIPTION: 'UpdateMoneyRequestDescription',
@@ -327,6 +329,8 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_MERCHANT]: Parameters.UpdateMoneyRequestParams;
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_BILLABLE]: Parameters.UpdateMoneyRequestParams;
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_TAG]: Parameters.UpdateMoneyRequestParams;
+ [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_TAX_AMOUNT]: Parameters.UpdateMoneyRequestParams;
+ [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_TAX_RATE]: Parameters.UpdateMoneyRequestParams;
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DISTANCE]: Parameters.UpdateMoneyRequestParams;
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_CATEGORY]: Parameters.UpdateMoneyRequestParams;
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DESCRIPTION]: Parameters.UpdateMoneyRequestParams;
diff --git a/src/libs/Console/index.ts b/src/libs/Console/index.ts
index bf7aa246aaf4..423e1037a605 100644
--- a/src/libs/Console/index.ts
+++ b/src/libs/Console/index.ts
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/naming-convention */
+import isEmpty from 'lodash/isEmpty';
import Onyx from 'react-native-onyx';
import {addLog} from '@libs/actions/Console';
import CONFIG from '@src/CONFIG';
@@ -118,5 +119,29 @@ function createLog(text: string) {
}
}
-export {sanitizeConsoleInput, createLog, shouldAttachLog};
+/**
+ * Loops through all the logs and parses the message if it's a stringified JSON
+ * @param logs Logs captured on the current device
+ * @returns CapturedLogs with parsed messages
+ */
+function parseStringifiedMessages(logs: Log[]): Log[] {
+ if (isEmpty(logs)) {
+ return logs;
+ }
+
+ return logs.map((log) => {
+ try {
+ const parsedMessage = JSON.parse(log.message);
+ return {
+ ...log,
+ message: parsedMessage,
+ };
+ } catch {
+ // If the message can't be parsed, just return the original log
+ return log;
+ }
+ });
+}
+
+export {sanitizeConsoleInput, createLog, shouldAttachLog, parseStringifiedMessages};
export type {Log};
diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts
index 8f1cb89d695b..0d961ea27115 100644
--- a/src/libs/ModifiedExpenseMessage.ts
+++ b/src/libs/ModifiedExpenseMessage.ts
@@ -55,6 +55,14 @@ function buildMessageFragmentForValue(
}
}
+/**
+ * Get the absolute value for a tax amount.
+ */
+function getTaxAmountAbsValue(taxAmount: number): number {
+ // IOU requests cannot have negative values but they can be stored as negative values, let's return absolute value
+ return Math.abs(taxAmount ?? 0);
+}
+
/**
* Get the message line for a modified expense.
*/
@@ -116,6 +124,7 @@ function getForReportAction(reportID: string | undefined, reportAction: OnyxEntr
'currency' in reportActionOriginalMessage;
const hasModifiedMerchant = reportActionOriginalMessage && 'oldMerchant' in reportActionOriginalMessage && 'merchant' in reportActionOriginalMessage;
+
if (hasModifiedAmount) {
const oldCurrency = reportActionOriginalMessage?.oldCurrency ?? '';
const oldAmountValue = reportActionOriginalMessage?.oldAmount ?? 0;
@@ -216,6 +225,29 @@ function getForReportAction(reportID: string | undefined, reportAction: OnyxEntr
});
}
+ const hasModifiedTaxAmount = reportActionOriginalMessage && 'oldTaxAmount' in reportActionOriginalMessage && 'taxAmount' in reportActionOriginalMessage;
+ if (hasModifiedTaxAmount) {
+ const currency = reportActionOriginalMessage?.currency;
+
+ const taxAmount = CurrencyUtils.convertToDisplayString(getTaxAmountAbsValue(reportActionOriginalMessage?.taxAmount ?? 0), currency);
+ const oldTaxAmountValue = getTaxAmountAbsValue(reportActionOriginalMessage?.oldTaxAmount ?? 0);
+ const oldTaxAmount = oldTaxAmountValue > 0 ? CurrencyUtils.convertToDisplayString(oldTaxAmountValue, currency) : '';
+ buildMessageFragmentForValue(taxAmount, oldTaxAmount, Localize.translateLocal('iou.taxAmount'), false, setFragments, removalFragments, changeFragments);
+ }
+
+ const hasModifiedTaxRate = reportActionOriginalMessage && 'oldTaxRate' in reportActionOriginalMessage && 'taxRate' in reportActionOriginalMessage;
+ if (hasModifiedTaxRate) {
+ buildMessageFragmentForValue(
+ reportActionOriginalMessage?.taxRate ?? '',
+ reportActionOriginalMessage?.oldTaxRate ?? '',
+ Localize.translateLocal('iou.taxRate'),
+ false,
+ setFragments,
+ removalFragments,
+ changeFragments,
+ );
+ }
+
const hasModifiedBillable = reportActionOriginalMessage && 'oldBillable' in reportActionOriginalMessage && 'billable' in reportActionOriginalMessage;
if (hasModifiedBillable) {
buildMessageFragmentForValue(
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index 55c58290b1cd..0d1b9b59a089 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -94,6 +94,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../../pages/EnablePayments/EnablePaymentsPage').default as React.ComponentType,
[SCREENS.MONEY_REQUEST.WAYPOINT]: () => require('../../../../pages/iou/MoneyRequestWaypointPage').default as React.ComponentType,
[SCREENS.MONEY_REQUEST.RECEIPT]: () => require('../../../../pages/EditRequestReceiptPage').default as React.ComponentType,
+ [SCREENS.MONEY_REQUEST.STATE_SELECTOR]: () => require('../../../../pages/settings/Profile/PersonalDetails/StateSelectionPage').default as React.ComponentType,
});
const SplitDetailsModalStackNavigator = createModalStackNavigator({
@@ -189,6 +190,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Profile/PersonalDetails/DateOfBirthPage').default as React.ComponentType,
[SCREENS.SETTINGS.PROFILE.ADDRESS]: () => require('../../../../pages/settings/Profile/PersonalDetails/AddressPage').default as React.ComponentType,
[SCREENS.SETTINGS.PROFILE.ADDRESS_COUNTRY]: () => require('../../../../pages/settings/Profile/PersonalDetails/CountrySelectionPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.PROFILE.ADDRESS_STATE]: () => require('../../../../pages/settings/Profile/PersonalDetails/StateSelectionPage').default as React.ComponentType,
[SCREENS.SETTINGS.PROFILE.CONTACT_METHODS]: () => require('../../../../pages/settings/Profile/Contacts/ContactMethodsPage').default as React.ComponentType,
[SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_DETAILS]: () => require('../../../../pages/settings/Profile/Contacts/ContactMethodDetailsPage').default as React.ComponentType,
[SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD]: () => require('../../../../pages/settings/Profile/Contacts/NewContactMethodPage').default as React.ComponentType,
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index f272ae24973a..823b6514c42b 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -212,6 +212,10 @@ const config: LinkingOptions['config'] = {
path: ROUTES.SETTINGS_ADDRESS_COUNTRY.route,
exact: true,
},
+ [SCREENS.SETTINGS.PROFILE.ADDRESS_STATE]: {
+ path: ROUTES.SETTINGS_ADDRESS_STATE.route,
+ exact: true,
+ },
[SCREENS.SETTINGS.TWO_FACTOR_AUTH]: {
path: ROUTES.SETTINGS_2FA.route,
exact: true,
@@ -527,6 +531,7 @@ const config: LinkingOptions['config'] = {
[SCREENS.MONEY_REQUEST.PARTICIPANTS]: ROUTES.MONEY_REQUEST_PARTICIPANTS.route,
[SCREENS.MONEY_REQUEST.CURRENCY]: ROUTES.MONEY_REQUEST_CURRENCY.route,
[SCREENS.MONEY_REQUEST.RECEIPT]: ROUTES.MONEY_REQUEST_RECEIPT.route,
+ [SCREENS.MONEY_REQUEST.STATE_SELECTOR]: {path: ROUTES.MONEY_REQUEST_STATE_SELECTOR.route, exact: true},
[SCREENS.IOU_SEND.ENABLE_PAYMENTS]: ROUTES.IOU_SEND_ENABLE_PAYMENTS,
[SCREENS.IOU_SEND.ADD_BANK_ACCOUNT]: ROUTES.IOU_SEND_ADD_BANK_ACCOUNT,
[SCREENS.IOU_SEND.ADD_DEBIT_CARD]: ROUTES.IOU_SEND_ADD_DEBIT_CARD,
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index dac3a98f4d7c..b88c44b9aa70 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -357,47 +357,48 @@ type MoneyRequestNavigatorParamList = {
iouType: string;
reportID: string;
currency: string;
- backTo: string;
+ backTo: Routes;
};
[SCREENS.MONEY_REQUEST.STEP_DATE]: {
action: ValueOf;
iouType: ValueOf;
transactionID: string;
reportID: string;
- backTo: string;
+ backTo: Routes;
};
[SCREENS.MONEY_REQUEST.STEP_DESCRIPTION]: {
action: ValueOf;
iouType: ValueOf;
transactionID: string;
reportID: string;
- backTo: string;
+ backTo: Routes;
+ reportActionID: string;
};
[SCREENS.MONEY_REQUEST.STEP_CATEGORY]: {
action: ValueOf;
iouType: ValueOf;
transactionID: string;
reportID: string;
- backTo: string;
+ backTo: Routes;
};
[SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT]: {
iouType: string;
transactionID: string;
reportID: string;
- backTo: string;
+ backTo: Routes;
};
[SCREENS.MONEY_REQUEST.STEP_TAG]: {
action: ValueOf;
iouType: ValueOf;
transactionID: string;
reportID: string;
- backTo: string;
+ backTo: Routes;
};
[SCREENS.MONEY_REQUEST.STEP_TAX_RATE]: {
iouType: string;
transactionID: string;
reportID: string;
- backTo: string;
+ backTo: Routes;
};
[SCREENS.MONEY_REQUEST.STEP_WAYPOINT]: {
iouType: ValueOf;
@@ -412,7 +413,7 @@ type MoneyRequestNavigatorParamList = {
iouType: ValueOf;
transactionID: string;
reportID: string;
- backTo: string;
+ backTo: Routes;
};
[SCREENS.IOU_SEND.ENABLE_PAYMENTS]: undefined;
[SCREENS.IOU_SEND.ADD_BANK_ACCOUNT]: undefined;
@@ -428,7 +429,7 @@ type MoneyRequestNavigatorParamList = {
iouType: ValueOf;
transactionID: string;
reportID: string;
- backTo: string;
+ backTo: Routes;
};
[SCREENS.MONEY_REQUEST.RECEIPT]: {
iouType: string;
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index 7e4082bff481..ca44931e7e8e 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -1191,8 +1191,8 @@ function hasEnabledTags(policyTagList: Array
* @param taxRates - The original tax rates object.
* @returns The transformed tax rates object.g
*/
-function transformedTaxRates(taxRates: TaxRatesWithDefault | undefined, defaultKey?: string): Record {
- const defaultTaxKey = defaultKey ?? taxRates?.defaultExternalID;
+function transformedTaxRates(taxRates: TaxRatesWithDefault | undefined): Record {
+ const defaultTaxKey = taxRates?.defaultExternalID;
const getModifiedName = (data: TaxRate, code: string) => `${data.name} (${data.value})${defaultTaxKey === code ? ` • ${Localize.translateLocal('common.default')}` : ''}`;
const taxes = Object.fromEntries(Object.entries(taxRates?.taxes ?? {}).map(([code, data]) => [code, {...data, code, modifiedName: getModifiedName(data, code), name: data.name}]));
return taxes;
@@ -1212,7 +1212,7 @@ function sortTaxRates(taxRates: TaxRates): TaxRate[] {
function getTaxRatesOptions(taxRates: Array>): Option[] {
return taxRates.map((taxRate) => ({
text: taxRate.modifiedName,
- keyForList: taxRate.code,
+ keyForList: taxRate.modifiedName,
searchText: taxRate.modifiedName,
tooltipText: taxRate.modifiedName,
isDisabled: taxRate.isDisabled,
@@ -1223,10 +1223,10 @@ function getTaxRatesOptions(taxRates: Array>): Option[] {
/**
* Builds the section list for tax rates
*/
-function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedOptions: Category[], searchInputValue: string, defaultTaxKey?: string): CategorySection[] {
+function getTaxRatesSection(taxRates: TaxRatesWithDefault | undefined, selectedOptions: Category[], searchInputValue: string): CategorySection[] {
const policyRatesSections = [];
- const taxes = transformedTaxRates(taxRates, defaultTaxKey);
+ const taxes = transformedTaxRates(taxRates);
const sortedTaxRates = sortTaxRates(taxes);
const enabledTaxRates = sortedTaxRates.filter((taxRate) => !taxRate.isDisabled);
diff --git a/src/libs/Performance.tsx b/src/libs/Performance.tsx
index 8d812014dbf8..9302b538a621 100644
--- a/src/libs/Performance.tsx
+++ b/src/libs/Performance.tsx
@@ -134,6 +134,13 @@ if (Metrics.canCapturePerformanceMetrics()) {
if (entry.name === 'runJsBundleEnd') {
Performance.measureFailSafe('runJsBundle', 'runJsBundleStart', 'runJsBundleEnd');
}
+ if (entry.name === 'appCreationEnd') {
+ Performance.measureFailSafe('appCreation', 'appCreationStart', 'appCreationEnd');
+ Performance.measureFailSafe('nativeLaunchEnd_To_appCreationStart', 'nativeLaunchEnd', 'appCreationStart');
+ }
+ if (entry.name === 'contentAppeared') {
+ Performance.measureFailSafe('appCreationEnd_To_contentAppeared', 'appCreationEnd', 'contentAppeared');
+ }
// We don't need to keep the observer past this point
if (entry.name === 'runJsBundleEnd' || entry.name === 'downloadEnd') {
@@ -154,6 +161,7 @@ if (Metrics.canCapturePerformanceMetrics()) {
// Capture any custom measures or metrics below
if (mark.name === `${CONST.TIMING.SIDEBAR_LOADED}_end`) {
+ Performance.measureFailSafe('contentAppeared_To_screenTTI', 'contentAppeared', mark.name);
Performance.measureTTI(mark.name);
}
});
@@ -163,6 +171,10 @@ if (Metrics.canCapturePerformanceMetrics()) {
Performance.getPerformanceMetrics = (): PerformanceEntry[] =>
[
...rnPerformance.getEntriesByName('nativeLaunch'),
+ ...rnPerformance.getEntriesByName('nativeLaunchEnd_To_appCreationStart'),
+ ...rnPerformance.getEntriesByName('appCreation'),
+ ...rnPerformance.getEntriesByName('appCreationEnd_To_contentAppeared'),
+ ...rnPerformance.getEntriesByName('contentAppeared_To_screenTTI'),
...rnPerformance.getEntriesByName('runJsBundle'),
...rnPerformance.getEntriesByName('jsBundleDownload'),
...rnPerformance.getEntriesByName('TTI'),
diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts
index effec73f10a9..1973e665b20f 100644
--- a/src/libs/Permissions.ts
+++ b/src/libs/Permissions.ts
@@ -14,10 +14,6 @@ function canUseDefaultRooms(betas: OnyxEntry): boolean {
return !!betas?.includes(CONST.BETAS.DEFAULT_ROOMS) || canUseAllBetas(betas);
}
-function canUseCommentLinking(betas: OnyxEntry): boolean {
- return !!betas?.includes(CONST.BETAS.BETA_COMMENT_LINKING) || canUseAllBetas(betas);
-}
-
function canUseReportFields(betas: OnyxEntry): boolean {
return !!betas?.includes(CONST.BETAS.REPORT_FIELDS) || canUseAllBetas(betas);
}
@@ -52,7 +48,6 @@ function canUseLinkPreviews(): boolean {
export default {
canUseChronos,
canUseDefaultRooms,
- canUseCommentLinking,
canUseLinkPreviews,
canUseViolations,
canUseTrackExpense,
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index 0a8437a5afaf..d393f8e64fba 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -244,6 +244,10 @@ function isPaidGroupPolicy(policy: OnyxEntry | EmptyObject): boolean {
return policy?.type === CONST.POLICY.TYPE.TEAM || policy?.type === CONST.POLICY.TYPE.CORPORATE;
}
+function isTaxPolicyEnabled(isPolicyExpenseChat: boolean, policy: OnyxEntry): boolean {
+ return (isPolicyExpenseChat && (policy?.tax?.trackingEnabled ?? policy?.isTaxTrackingEnabled)) ?? false;
+}
+
/**
* Checks if policy's scheduled submit / auto reporting frequency is "instant".
* Note: Free policies have "instant" submit always enabled.
@@ -328,6 +332,7 @@ export {
isInstantSubmitEnabled,
isFreeGroupPolicy,
isPolicyAdmin,
+ isTaxPolicyEnabled,
isSubmitAndClose,
getMemberAccountIDsForWorkspace,
getIneligibleInvitees,
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index a2bf892b96b4..8296e38411be 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -99,6 +99,10 @@ type ExpenseOriginalMessage = {
oldTag?: string;
billable?: string;
oldBillable?: string;
+ oldTaxAmount?: number;
+ taxAmount?: number;
+ taxRate?: string;
+ oldTaxRate?: string;
};
type SpendBreakdown = {
@@ -327,6 +331,8 @@ type OptimisticTaskReport = Pick<
type TransactionDetails = {
created: string;
amount: number;
+ taxAmount?: number;
+ taxCode?: string;
currency: string;
merchant: string;
waypoints?: WaypointCollection | string;
@@ -2290,6 +2296,8 @@ function getTransactionDetails(transaction: OnyxEntry, createdDateF
return {
created: TransactionUtils.getCreated(transaction, createdDateFormat),
amount: TransactionUtils.getAmount(transaction, !isEmptyObject(report) && isExpenseReport(report)),
+ taxAmount: TransactionUtils.getTaxAmount(transaction, !isEmptyObject(report) && isExpenseReport(report)),
+ taxCode: TransactionUtils.getTaxCode(transaction),
currency: TransactionUtils.getCurrency(transaction),
comment: TransactionUtils.getDescription(transaction),
merchant: TransactionUtils.getMerchant(transaction),
@@ -2714,7 +2722,12 @@ function getReportPreviewMessage(
*
* At the moment, we only allow changing one transaction field at a time.
*/
-function getModifiedExpenseOriginalMessage(oldTransaction: OnyxEntry, transactionChanges: TransactionChanges, isFromExpenseReport: boolean): ExpenseOriginalMessage {
+function getModifiedExpenseOriginalMessage(
+ oldTransaction: OnyxEntry,
+ transactionChanges: TransactionChanges,
+ isFromExpenseReport: boolean,
+ policy: OnyxEntry,
+): ExpenseOriginalMessage {
const originalMessage: ExpenseOriginalMessage = {};
// Remark: Comment field is the only one which has new/old prefixes for the keys (newComment/ oldComment),
// all others have old/- pattern such as oldCreated/created
@@ -2750,6 +2763,16 @@ function getModifiedExpenseOriginalMessage(oldTransaction: OnyxEntry,
transactionChanges: TransactionChanges,
isFromExpenseReport: boolean,
+ policy: OnyxEntry,
): OptimisticModifiedExpenseReportAction {
- const originalMessage = getModifiedExpenseOriginalMessage(oldTransaction, transactionChanges, isFromExpenseReport);
+ const originalMessage = getModifiedExpenseOriginalMessage(oldTransaction, transactionChanges, isFromExpenseReport, policy);
return {
actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE,
actorAccountID: currentUserAccountID,
diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts
index 907edc208570..430100e84b2f 100644
--- a/src/libs/TransactionUtils.ts
+++ b/src/libs/TransactionUtils.ts
@@ -4,11 +4,12 @@ import Onyx from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {RecentWaypoint, Report, TaxRate, TaxRates, Transaction, TransactionViolation} from '@src/types/onyx';
+import type {RecentWaypoint, Report, TaxRate, TaxRates, TaxRatesWithDefault, Transaction, TransactionViolation} from '@src/types/onyx';
import type {Comment, Receipt, TransactionChanges, TransactionPendingFieldsKey, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import {isCorporateCard, isExpensifyCard} from './CardUtils';
import DateUtils from './DateUtils';
+import * as Localize from './Localize';
import * as NumberUtils from './NumberUtils';
import {getCleanedTagName} from './PolicyUtils';
@@ -207,6 +208,16 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra
shouldStopSmartscan = true;
}
+ if (Object.hasOwn(transactionChanges, 'taxAmount') && typeof transactionChanges.taxAmount === 'number') {
+ updatedTransaction.taxAmount = isFromExpenseReport ? -transactionChanges.taxAmount : transactionChanges.taxAmount;
+ shouldStopSmartscan = true;
+ }
+
+ if (Object.hasOwn(transactionChanges, 'taxCode') && typeof transactionChanges.taxCode === 'string') {
+ updatedTransaction.taxCode = transactionChanges.taxCode;
+ shouldStopSmartscan = true;
+ }
+
if (Object.hasOwn(transactionChanges, 'billable') && typeof transactionChanges.billable === 'boolean') {
updatedTransaction.billable = transactionChanges.billable;
}
@@ -240,6 +251,8 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra
...(Object.hasOwn(transactionChanges, 'billable') && {billable: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
...(Object.hasOwn(transactionChanges, 'category') && {category: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
...(Object.hasOwn(transactionChanges, 'tag') && {tag: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
+ ...(Object.hasOwn(transactionChanges, 'taxAmount') && {taxAmount: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
+ ...(Object.hasOwn(transactionChanges, 'taxCode') && {taxCode: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
};
return updatedTransaction;
@@ -280,6 +293,27 @@ function getAmount(transaction: OnyxEntry, isFromExpenseReport = fa
return amount ? -amount : 0;
}
+/**
+ * Return the tax amount field from the transaction.
+ */
+function getTaxAmount(transaction: OnyxEntry, isFromExpenseReport: boolean): number {
+ // IOU requests cannot have negative values but they can be stored as negative values, let's return absolute value
+ if (!isFromExpenseReport) {
+ return Math.abs(transaction?.taxAmount ?? 0);
+ }
+
+ // To avoid -0 being shown, lets only change the sign if the value is other than 0.
+ const amount = transaction?.taxAmount ?? 0;
+ return amount ? -amount : 0;
+}
+
+/**
+ * Return the tax code from the transaction.
+ */
+function getTaxCode(transaction: OnyxEntry): string {
+ return transaction?.taxCode ?? '';
+}
+
/**
* Return the currency field from the transaction, return the modifiedCurrency if present.
*/
@@ -562,7 +596,9 @@ function isOnHold(transaction: OnyxEntry): boolean {
* Checks if any violations for the provided transaction are of type 'violation'
*/
function hasViolation(transactionID: string, transactionViolations: OnyxCollection): boolean {
- return Boolean(transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some((violation: TransactionViolation) => violation.type === 'violation'));
+ return Boolean(
+ transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some((violation: TransactionViolation) => violation.type === CONST.VIOLATION_TYPES.VIOLATION),
+ );
}
function getTransactionViolations(transactionID: string, transactionViolations: OnyxCollection): TransactionViolation[] | null {
@@ -584,9 +620,29 @@ function getEnabledTaxRateCount(options: TaxRates) {
return Object.values(options).filter((option: TaxRate) => !option.isDisabled).length;
}
+/**
+ * Gets the default tax name
+ */
+function getDefaultTaxName(taxRates: TaxRatesWithDefault, transaction: Transaction) {
+ const defaultTaxKey = taxRates.defaultExternalID;
+ const defaultTaxName = (defaultTaxKey && `${taxRates.taxes[defaultTaxKey].name} (${taxRates.taxes[defaultTaxKey].value}) • ${Localize.translateLocal('common.default')}`) || '';
+ return transaction?.taxRate?.text ?? defaultTaxName;
+}
+
+/**
+ * Gets the tax name
+ */
+function getTaxName(taxes: TaxRates, transactionTaxCode: string) {
+ const taxName = `${taxes[transactionTaxCode].name}`;
+ const taxValue = `${taxes[transactionTaxCode].value}`;
+ return transactionTaxCode ? `${taxName} (${taxValue})` : '';
+}
+
export {
buildOptimisticTransaction,
calculateTaxAmount,
+ getTaxName,
+ getDefaultTaxName,
getEnabledTaxRateCount,
getUpdatedTransaction,
getDescription,
@@ -595,6 +651,8 @@ export {
isManualRequest,
isScanRequest,
getAmount,
+ getTaxAmount,
+ getTaxCode,
getCurrency,
getDistance,
getCardID,
diff --git a/src/libs/Url.ts b/src/libs/Url.ts
index a21f007e8468..4e3282e7bdb3 100644
--- a/src/libs/Url.ts
+++ b/src/libs/Url.ts
@@ -41,4 +41,19 @@ function hasSameExpensifyOrigin(url1: string, url2: string): boolean {
}
}
-export {addTrailingForwardSlash, hasSameExpensifyOrigin, getPathFromURL};
+/**
+ * Appends or updates a query parameter in a given URL.
+ */
+function appendParam(url: string, paramName: string, paramValue: string) {
+ // If parameter exists, replace it
+ if (url.includes(`${paramName}=`)) {
+ const regex = new RegExp(`${paramName}=([^&]*)`);
+ return url.replace(regex, `${paramName}=${paramValue}`);
+ }
+
+ // If parameter doesn't exist, append it
+ const separator = url.includes('?') ? '&' : '?';
+ return `${url}${separator}${paramName}=${paramValue}`;
+}
+
+export {addTrailingForwardSlash, hasSameExpensifyOrigin, getPathFromURL, appendParam};
diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts
index 83f3b2fcc154..42f58be1d699 100644
--- a/src/libs/Violations/ViolationsUtils.ts
+++ b/src/libs/Violations/ViolationsUtils.ts
@@ -28,7 +28,7 @@ function getTagViolationsForSingleLevelTags(
// Add 'tagOutOfPolicy' violation if tag is not in policy
if (!hasTagOutOfPolicyViolation && updatedTransaction.tag && !isTagInPolicy) {
- newTransactionViolations.push({name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY, type: 'violation'});
+ newTransactionViolations.push({name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY, type: CONST.VIOLATION_TYPES.VIOLATION});
}
// Remove 'tagOutOfPolicy' violation if tag is in policy
@@ -43,7 +43,7 @@ function getTagViolationsForSingleLevelTags(
// Add 'missingTag violation' if tag is required and not set
if (!hasMissingTagViolation && !updatedTransaction.tag && policyRequiresTags) {
- newTransactionViolations.push({name: CONST.VIOLATIONS.MISSING_TAG, type: 'violation'});
+ newTransactionViolations.push({name: CONST.VIOLATIONS.MISSING_TAG, type: CONST.VIOLATION_TYPES.VIOLATION});
}
return newTransactionViolations;
}
@@ -77,7 +77,7 @@ function getTagViolationsForMultiLevelTags(
if (errorIndexes.length !== 0) {
newTransactionViolations.push({
name: CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED,
- type: 'violation',
+ type: CONST.VIOLATION_TYPES.VIOLATION,
data: {
errorIndexes,
},
@@ -91,7 +91,7 @@ function getTagViolationsForMultiLevelTags(
if (!isTagInPolicy) {
newTransactionViolations.push({
name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY,
- type: 'violation',
+ type: CONST.VIOLATION_TYPES.VIOLATION,
data: {
tagName: policyTagKeys[i],
},
@@ -142,7 +142,7 @@ const ViolationsUtils = {
// Add 'categoryOutOfPolicy' violation if category is not in policy
if (!hasCategoryOutOfPolicyViolation && categoryKey && !isCategoryInPolicy) {
- newTransactionViolations.push({name: 'categoryOutOfPolicy', type: 'violation'});
+ newTransactionViolations.push({name: 'categoryOutOfPolicy', type: CONST.VIOLATION_TYPES.VIOLATION});
}
// Remove 'categoryOutOfPolicy' violation if category is in policy
@@ -157,7 +157,7 @@ const ViolationsUtils = {
// Add 'missingCategory' violation if category is required and not set
if (!hasMissingCategoryViolation && policyRequiresCategories && !categoryKey) {
- newTransactionViolations.push({name: 'missingCategory', type: 'violation'});
+ newTransactionViolations.push({name: 'missingCategory', type: CONST.VIOLATION_TYPES.VIOLATION});
}
}
diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts
index 3a9ab4c52b3c..11bbc5c12f53 100644
--- a/src/libs/actions/App.ts
+++ b/src/libs/actions/App.ts
@@ -131,7 +131,6 @@ function setSidebarLoaded() {
}
Onyx.set(ONYXKEYS.IS_SIDEBAR_LOADED, true);
- Performance.markEnd(CONST.TIMING.SIDEBAR_LOADED);
Performance.markStart(CONST.TIMING.REPORT_INITIAL_RENDER);
}
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index f99942a3d7a8..761ae02ea4c6 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -1533,7 +1533,7 @@ function getUpdateMoneyRequestParams(
// We don't create a modified report action if we're updating the waypoints,
// since there isn't actually any optimistic data we can create for them and the report action is created on the server
// with the response from the MapBox API
- const updatedReportAction = ReportUtils.buildOptimisticModifiedExpenseReportAction(transactionThread, transaction, transactionChanges, isFromExpenseReport);
+ const updatedReportAction = ReportUtils.buildOptimisticModifiedExpenseReportAction(transactionThread, transaction, transactionChanges, isFromExpenseReport, policy);
if (!hasPendingWaypoints) {
params.reportActionID = updatedReportAction.reportActionID;
@@ -1743,6 +1743,7 @@ function getUpdateTrackExpenseParams(
transactionThreadReportID: string,
transactionChanges: TransactionChanges,
onlyIncludeChangedFields: boolean,
+ policy: OnyxEntry,
): UpdateMoneyRequestData {
const optimisticData: OnyxUpdate[] = [];
const successData: OnyxUpdate[] = [];
@@ -1809,7 +1810,7 @@ function getUpdateTrackExpenseParams(
// We don't create a modified report action if we're updating the waypoints,
// since there isn't actually any optimistic data we can create for them and the report action is created on the server
// with the response from the MapBox API
- const updatedReportAction = ReportUtils.buildOptimisticModifiedExpenseReportAction(transactionThread, transaction, transactionChanges, false);
+ const updatedReportAction = ReportUtils.buildOptimisticModifiedExpenseReportAction(transactionThread, transaction, transactionChanges, false, policy);
if (!hasPendingWaypoints) {
params.reportActionID = updatedReportAction.reportActionID;
@@ -1922,7 +1923,7 @@ function updateMoneyRequestDate(
const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null;
let data: UpdateMoneyRequestData;
if (ReportUtils.isTrackExpenseReport(transactionThreadReport)) {
- data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true);
+ data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true, policy);
} else {
data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, true);
}
@@ -1961,7 +1962,7 @@ function updateMoneyRequestMerchant(
const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null;
let data: UpdateMoneyRequestData;
if (ReportUtils.isTrackExpenseReport(transactionThreadReport)) {
- data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true);
+ data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true, policy);
} else {
data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true);
}
@@ -1985,6 +1986,38 @@ function updateMoneyRequestTag(
API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_TAG, params, onyxData);
}
+/** Updates the created tax amount of a money request */
+function updateMoneyRequestTaxAmount(
+ transactionID: string,
+ optimisticReportActionID: string,
+ taxAmount: number,
+ policy: OnyxEntry,
+ policyTagList: OnyxEntry,
+ policyCategories: OnyxEntry,
+) {
+ const transactionChanges = {
+ taxAmount,
+ };
+ const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, optimisticReportActionID, transactionChanges, policy, policyTagList, policyCategories, true);
+ API.write('UpdateMoneyRequestTaxAmount', params, onyxData);
+}
+
+/** Updates the created tax rate of a money request */
+function updateMoneyRequestTaxRate(
+ transactionID: string,
+ optimisticReportActionID: string,
+ taxCode: string,
+ policy: OnyxEntry,
+ policyTagList: OnyxEntry,
+ policyCategories: OnyxEntry,
+) {
+ const transactionChanges = {
+ taxCode,
+ };
+ const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, optimisticReportActionID, transactionChanges, policy, policyTagList, policyCategories, true);
+ API.write('UpdateMoneyRequestTaxRate', params, onyxData);
+}
+
/** Updates the waypoints of a distance money request */
function updateMoneyRequestDistance(
transactionID: string,
@@ -2000,7 +2033,7 @@ function updateMoneyRequestDistance(
const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null;
let data: UpdateMoneyRequestData;
if (ReportUtils.isTrackExpenseReport(transactionThreadReport)) {
- data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true);
+ data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true, policy);
} else {
data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true);
}
@@ -2039,7 +2072,7 @@ function updateMoneyRequestDescription(
const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null;
let data: UpdateMoneyRequestData;
if (ReportUtils.isTrackExpenseReport(transactionThreadReport)) {
- data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true);
+ data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true, policy);
} else {
data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true);
}
@@ -3330,7 +3363,7 @@ function editRegularMoneyRequest(
const isFromExpenseReport = ReportUtils.isExpenseReport(iouReport);
// STEP 2: Build new modified expense report action.
- const updatedReportAction = ReportUtils.buildOptimisticModifiedExpenseReportAction(transactionThread, transaction, transactionChanges, isFromExpenseReport);
+ const updatedReportAction = ReportUtils.buildOptimisticModifiedExpenseReportAction(transactionThread, transaction, transactionChanges, isFromExpenseReport, policy);
const updatedTransaction = transaction ? TransactionUtils.getUpdatedTransaction(transaction, transactionChanges, isFromExpenseReport) : null;
// STEP 3: Compute the IOU total and update the report preview message so LHN amount owed is correct
@@ -4574,8 +4607,9 @@ function canApproveIOU(iouReport: OnyxEntry | EmptyObject, cha
const isOpenExpenseReport = isPolicyExpenseChat && ReportUtils.isOpenExpenseReport(iouReport);
const isApproved = ReportUtils.isReportApproved(iouReport);
const iouSettled = ReportUtils.isSettled(iouReport?.reportID);
+ const isArchivedReport = ReportUtils.isArchivedRoom(iouReport);
- return isCurrentUserManager && !isOpenExpenseReport && !isApproved && !iouSettled;
+ return isCurrentUserManager && !isOpenExpenseReport && !isApproved && !iouSettled && !isArchivedReport;
}
function canIOUBePaid(iouReport: OnyxEntry | EmptyObject, chatReport: OnyxEntry | EmptyObject, policy: OnyxEntry | EmptyObject) {
@@ -4733,35 +4767,48 @@ function submitReport(expenseReport: OnyxTypes.Report) {
const policy = ReportUtils.getPolicy(expenseReport.policyID);
const isCurrentUserManager = currentUserPersonalDetails.accountID === expenseReport.managerID;
const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, CONST.REPORT.STATUS_NUM.SUBMITTED);
+ const isSubmitAndClosePolicy = PolicyUtils.isSubmitAndClose(policy);
- const optimisticData: OnyxUpdate[] = [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`,
- value: {
- [optimisticSubmittedReportAction.reportActionID]: {
- ...(optimisticSubmittedReportAction as OnyxTypes.ReportAction),
- pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
- },
- },
- },
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`,
- value: {
- ...expenseReport,
- lastMessageText: optimisticSubmittedReportAction.message?.[0].text ?? '',
- lastMessageHtml: optimisticSubmittedReportAction.message?.[0].html ?? '',
- stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
- statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
- },
- },
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`,
- value: optimisticNextStep,
- },
- ];
+ const optimisticData: OnyxUpdate[] = !isSubmitAndClosePolicy
+ ? [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`,
+ value: {
+ [optimisticSubmittedReportAction.reportActionID]: {
+ ...(optimisticSubmittedReportAction as OnyxTypes.ReportAction),
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ },
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`,
+ value: {
+ ...expenseReport,
+ lastMessageText: optimisticSubmittedReportAction.message?.[0].text ?? '',
+ lastMessageHtml: optimisticSubmittedReportAction.message?.[0].html ?? '',
+ stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+ statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`,
+ value: optimisticNextStep,
+ },
+ ]
+ : [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`,
+ value: {
+ ...expenseReport,
+ stateNum: CONST.REPORT.STATE_NUM.APPROVED,
+ statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
+ },
+ },
+ ];
if (parentReport?.reportID) {
optimisticData.push({
@@ -4776,8 +4823,9 @@ function submitReport(expenseReport: OnyxTypes.Report) {
});
}
- const successData: OnyxUpdate[] = [
- {
+ const successData: OnyxUpdate[] = [];
+ if (!isSubmitAndClosePolicy) {
+ successData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`,
value: {
@@ -4785,19 +4833,10 @@ function submitReport(expenseReport: OnyxTypes.Report) {
pendingAction: null,
},
},
- },
- ];
+ });
+ }
const failureData: OnyxUpdate[] = [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`,
- value: {
- [optimisticSubmittedReportAction.reportActionID]: {
- errors: ErrorUtils.getMicroSecondOnyxError('iou.error.other'),
- },
- },
- },
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`,
@@ -4806,12 +4845,25 @@ function submitReport(expenseReport: OnyxTypes.Report) {
stateNum: CONST.REPORT.STATE_NUM.OPEN,
},
},
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`,
- value: currentNextStep,
- },
];
+ if (!isSubmitAndClosePolicy) {
+ failureData.push(
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`,
+ value: {
+ [optimisticSubmittedReportAction.reportActionID]: {
+ errors: ErrorUtils.getMicroSecondOnyxError('iou.error.other'),
+ },
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`,
+ value: currentNextStep,
+ },
+ );
+ }
if (parentReport?.reportID) {
failureData.push({
@@ -5048,8 +5100,8 @@ function setMoneyRequestTaxRate(transactionID: string, taxRate: TaxRate) {
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {taxRate});
}
-function setMoneyRequestTaxAmount(transactionID: string, taxAmount: number) {
- Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {taxAmount});
+function setMoneyRequestTaxAmount(transactionID: string, taxAmount: number, isDraft: boolean) {
+ Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {taxAmount});
}
function setMoneyRequestBillable(billable: boolean) {
@@ -5261,6 +5313,8 @@ export {
updateMoneyRequestBillable,
updateMoneyRequestMerchant,
updateMoneyRequestTag,
+ updateMoneyRequestTaxAmount,
+ updateMoneyRequestTaxRate,
updateMoneyRequestDistance,
updateMoneyRequestCategory,
updateMoneyRequestAmountAndCurrency,
diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js
index d3941dca044e..ff2c2c5ce6ea 100644
--- a/src/pages/EditRequestPage.js
+++ b/src/pages/EditRequestPage.js
@@ -8,10 +8,12 @@ import ScreenWrapper from '@components/ScreenWrapper';
import tagPropTypes from '@components/tagPropTypes';
import transactionPropTypes from '@components/transactionPropTypes';
import compose from '@libs/compose';
+import * as CurrencyUtils from '@libs/CurrencyUtils';
import * as IOUUtils from '@libs/IOUUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
+import {isTaxPolicyEnabled} from '@libs/PolicyUtils';
import * as ReportUtils from '@libs/ReportUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import * as IOU from '@userActions/IOU';
@@ -19,6 +21,8 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import EditRequestReceiptPage from './EditRequestReceiptPage';
import EditRequestTagPage from './EditRequestTagPage';
+import EditRequestTaxAmountPage from './EditRequestTaxAmountPage';
+import EditRequestTaxRatePage from './EditRequestTaxRatePage';
import reportActionPropTypes from './home/report/reportActionPropTypes';
import reportPropTypes from './reportPropTypes';
import {policyPropTypes} from './workspace/withPolicy';
@@ -68,10 +72,17 @@ const defaultProps = {
transaction: {},
};
+const getTaxAmount = (transactionAmount, transactionTaxCode, taxRates) => {
+ const percentage = (transactionTaxCode ? taxRates.taxes[transactionTaxCode].value : taxRates.defaultValue) || '';
+ return CurrencyUtils.convertToBackendAmount(Number.parseFloat(TransactionUtils.calculateTaxAmount(percentage, transactionAmount)));
+};
+
function EditRequestPage({report, route, policy, policyCategories, policyTags, parentReportActions, transaction}) {
const parentReportActionID = lodashGet(report, 'parentReportActionID', '0');
const parentReportAction = lodashGet(parentReportActions, parentReportActionID, {});
- const {tag: transactionTag} = ReportUtils.getTransactionDetails(transaction);
+ const {taxAmount: transactionTaxAmount, taxCode: transactionTaxCode, currency: transactionCurrency, tag: transactionTag} = ReportUtils.getTransactionDetails(transaction);
+
+ const defaultCurrency = lodashGet(route, 'params.currency', '') || transactionCurrency;
const fieldToEdit = lodashGet(route, ['params', 'field'], '');
const tagListIndex = Number(lodashGet(route, ['params', 'tagIndex'], undefined));
@@ -80,12 +91,23 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p
const policyTagListName = PolicyUtils.getTagListName(policyTags, tagListIndex);
const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]);
+ const taxRates = lodashGet(policy, 'taxRates', {});
+
+ const taxRateTitle =
+ taxRates &&
+ (transactionTaxCode === taxRates.defaultExternalID
+ ? transaction && TransactionUtils.getDefaultTaxName(taxRates, transaction)
+ : transactionTaxCode && TransactionUtils.getTaxName(taxRates.taxes, transactionTaxCode));
+
// A flag for verifying that the current report is a sub-report of a workspace chat
const isPolicyExpenseChat = ReportUtils.isGroupPolicy(report);
// A flag for showing the tags page
const shouldShowTags = useMemo(() => isPolicyExpenseChat && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists)), [isPolicyExpenseChat, policyTagLists, transactionTag]);
+ // A flag for showing tax rate
+ const shouldShowTax = isTaxPolicyEnabled(isPolicyExpenseChat, policy);
+
// Decides whether to allow or disallow editing a money request
useEffect(() => {
// Do not dismiss the modal, when a current user can edit this property of the money request.
@@ -99,6 +121,35 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p
});
}, [parentReportAction, fieldToEdit]);
+ const updateTaxAmount = useCallback(
+ (transactionChanges) => {
+ const newTaxAmount = CurrencyUtils.convertToBackendAmount(Number.parseFloat(transactionChanges.amount));
+
+ if (newTaxAmount === TransactionUtils.getTaxAmount(transaction)) {
+ Navigation.dismissModal();
+ return;
+ }
+ IOU.updateMoneyRequestTaxAmount(transaction.transactionID, report.reportID, newTaxAmount, policy, policyTags, policyCategories);
+ Navigation.dismissModal(report.reportID);
+ },
+ [transaction, report, policy, policyTags, policyCategories],
+ );
+
+ const updateTaxRate = useCallback(
+ (transactionChanges) => {
+ const newTaxCode = transactionChanges.data.code;
+
+ if (newTaxCode === undefined || newTaxCode === TransactionUtils.getTaxCode(transaction)) {
+ Navigation.dismissModal();
+ return;
+ }
+
+ IOU.updateMoneyRequestTaxRate(transaction.transactionID, report.reportID, newTaxCode, policy, policyTags, policyCategories);
+ Navigation.dismissModal(report.reportID);
+ },
+ [transaction, report, policy, policyTags, policyCategories],
+ );
+
const saveTag = useCallback(
({tag: newTag}) => {
let updatedTag = newTag;
@@ -131,6 +182,27 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p
);
}
+ if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.TAX_AMOUNT && shouldShowTax) {
+ return (
+
+ );
+ }
+
+ if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.TAX_RATE && shouldShowTax) {
+ return (
+
+ );
+ }
+
if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT) {
return (
void;
+};
+
+function EditRequestTaxAmountPage({defaultAmount, defaultTaxAmount, defaultCurrency, onSubmit}: EditRequestTaxAmountPageProps) {
+ const {translate} = useLocalize();
+ const textInput = useRef(null);
+
+ const focusTimeoutRef = useRef(null);
+
+ useFocusEffect(
+ useCallback(() => {
+ focusTimeoutRef.current = setTimeout(() => textInput.current && textInput.current.focus(), CONST.ANIMATED_TRANSITION);
+ return () => {
+ if (!focusTimeoutRef.current) {
+ return;
+ }
+ clearTimeout(focusTimeoutRef.current);
+ };
+ }, []),
+ );
+
+ return (
+
+
+
+
+ );
+}
+
+EditRequestTaxAmountPage.displayName = 'EditRequestTaxAmountPage';
+
+export default EditRequestTaxAmountPage;
diff --git a/src/pages/EditRequestTaxRatePage.tsx b/src/pages/EditRequestTaxRatePage.tsx
new file mode 100644
index 000000000000..099851e92209
--- /dev/null
+++ b/src/pages/EditRequestTaxRatePage.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import TaxPicker from '@components/TaxPicker';
+import useLocalize from '@hooks/useLocalize';
+
+type EditRequestTaxRatePageProps = {
+ /** Transaction default tax Rate value */
+ defaultTaxRate: string;
+
+ /** The policyID we are getting categories for */
+ policyID: string;
+
+ /** Callback to fire when the Save button is pressed */
+ onSubmit: () => void;
+};
+
+function EditRequestTaxRatePage({defaultTaxRate, policyID, onSubmit}: EditRequestTaxRatePageProps) {
+ const {translate} = useLocalize();
+
+ return (
+
+ {({insets}) => (
+ <>
+
+
+ >
+ )}
+
+ );
+}
+
+EditRequestTaxRatePage.displayName = 'EditRequestTaxRatePage';
+
+export default EditRequestTaxRatePage;
diff --git a/src/pages/ReimbursementAccount/AddressForm.js b/src/pages/ReimbursementAccount/AddressForm.js
index dc95d62b6afa..d20d9df1d189 100644
--- a/src/pages/ReimbursementAccount/AddressForm.js
+++ b/src/pages/ReimbursementAccount/AddressForm.js
@@ -3,7 +3,7 @@ import React from 'react';
import {View} from 'react-native';
import AddressSearch from '@components/AddressSearch';
import InputWrapper from '@components/Form/InputWrapper';
-import StatePicker from '@components/StatePicker';
+import StateSelector from '@components/StateSelector';
import TextInput from '@components/TextInput';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
@@ -128,7 +128,7 @@ function AddressForm(props) {
{translate('businessInfoStep.pleaseSelectTheStateYourCompanyWasIncorporatedIn')}
);
}
diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx
index 152b02366227..ef277984b4e9 100644
--- a/src/pages/home/ReportScreen.tsx
+++ b/src/pages/home/ReportScreen.tsx
@@ -283,6 +283,10 @@ function ReportScreen({
const screenWrapperStyle: ViewStyle[] = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}];
const isEmptyChat = useMemo((): boolean => reportActions.length === 0, [reportActions]);
const isOptimisticDelete = report.statusNum === CONST.REPORT.STATUS_NUM.CLOSED;
+ const isLinkedMessageAvailable = useMemo(
+ (): boolean => sortedAllReportActions.findIndex((obj) => String(obj.reportActionID) === String(reportActionIDFromRoute)) > -1,
+ [sortedAllReportActions, reportActionIDFromRoute],
+ );
// If there's a non-404 error for the report we should show it instead of blocking the screen
const hasHelpfulErrors = Object.keys(report?.errorFields ?? {}).some((key) => key !== 'notFound');
@@ -331,11 +335,14 @@ function ReportScreen({
);
}
+ const transactionThreadReportID = useMemo(() => ReportActionsUtils.getOneTransactionThreadReportID(reportActions ?? []), [reportActions]);
if (ReportUtils.isMoneyRequestReport(report)) {
headerView = (
);
}
@@ -351,11 +358,12 @@ function ReportScreen({
const isLoading = !ReportUtils.isValidReportIDFromPath(reportIDFromRoute) || !isSidebarLoaded || PersonalDetailsUtils.isPersonalDetailsEmpty();
const shouldShowSkeleton =
- isLinkingToMessage ||
- !isCurrentReportLoadedFromOnyx ||
- (reportActions.length === 0 && !!reportMetadata?.isLoadingInitialReportActions) ||
- isLoading ||
- (!!reportActionIDFromRoute && reportMetadata?.isLoadingInitialReportActions);
+ !isLinkedMessageAvailable &&
+ (isLinkingToMessage ||
+ !isCurrentReportLoadedFromOnyx ||
+ (reportActions.length === 0 && !!reportMetadata?.isLoadingInitialReportActions) ||
+ isLoading ||
+ (!!reportActionIDFromRoute && reportMetadata?.isLoadingInitialReportActions));
const shouldShowReportActionList = isCurrentReportLoadedFromOnyx && !isLoading;
// eslint-disable-next-line rulesdir/no-negated-variables
const shouldShowNotFoundPage = useMemo(
@@ -654,7 +662,7 @@ function ReportScreen({
isLoadingNewerReportActions={reportMetadata?.isLoadingNewerReportActions}
isLoadingOlderReportActions={reportMetadata?.isLoadingOlderReportActions}
isReadyForCommentLinking={!shouldShowSkeleton}
- transactionThreadReportID={ReportActionsUtils.getOneTransactionThreadReportID(reportActions ?? [])}
+ transactionThreadReportID={transactionThreadReportID}
/>
)}
diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
index ea65ab40ac0a..8ed62ad9e03c 100644
--- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
@@ -17,7 +17,6 @@ import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails';
import * as Localize from '@libs/Localize';
import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage';
import Navigation from '@libs/Navigation/Navigation';
-import Permissions from '@libs/Permissions';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
@@ -406,7 +405,7 @@ const ContextMenuActions: ContextMenuAction[] = [
// Only hide the copylink menu item when context menu is opened over img element.
const isAttachmentTarget = menuTarget?.current && 'tagName' in menuTarget.current && menuTarget?.current.tagName === 'IMG' && isAttachment;
- return Permissions.canUseCommentLinking(betas) && type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !isAttachmentTarget && !ReportActionsUtils.isMessageDeleted(reportAction);
+ return type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !isAttachmentTarget && !ReportActionsUtils.isMessageDeleted(reportAction);
},
onPress: (closePopover, {reportAction, reportID}) => {
Environment.getEnvironmentURL().then((environmentURL) => {
diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx
index 7dac3f6bb497..e7bf0c1337cd 100755
--- a/src/pages/home/report/ReportActionsView.tsx
+++ b/src/pages/home/report/ReportActionsView.tsx
@@ -133,7 +133,7 @@ function ReportActionsView({
listOldID = newID;
return newID;
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [route, isLoadingInitialReportActions]);
+ }, [route, isLoadingInitialReportActions, reportActionID]);
// Get a sorted array of reportActions for both the current report and the transaction thread report associated with this report (if there is one)
// so that we display transaction-level and report-level report actions in order in the one-transaction view
@@ -153,18 +153,16 @@ function ReportActionsView({
}, [allReportActions, transactionThreadReportActions]);
const indexOfLinkedAction = useMemo(() => {
- if (!reportActionID || isLoading) {
+ if (!reportActionID) {
return -1;
}
-
return combinedReportActions.findIndex((obj) => String(obj.reportActionID) === String(isFirstLinkedActionRender.current ? reportActionID : currentReportActionID));
- }, [combinedReportActions, currentReportActionID, reportActionID, isLoading]);
+ }, [combinedReportActions, currentReportActionID, reportActionID]);
const reportActions = useMemo(() => {
if (!reportActionID) {
return combinedReportActions;
}
-
if (isLoading || indexOfLinkedAction === -1) {
return [];
}
@@ -255,7 +253,7 @@ function ReportActionsView({
}, []);
useEffect(() => {
- if (!reportActionID) {
+ if (!reportActionID || indexOfLinkedAction > -1) {
return;
}
@@ -264,7 +262,7 @@ function ReportActionsView({
// There should be only one openReport execution per page start or navigating
Report.openReport(reportID, reportActionID);
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [route]);
+ }, [route, indexOfLinkedAction]);
useEffect(() => {
const wasLoginChangedDetected = prevAuthTokenType === CONST.AUTH_TOKEN_TYPES.ANONYMOUS && !session?.authTokenType;
@@ -466,10 +464,15 @@ function ReportActionsView({
const reportPreviewAction = ReportActionsUtils.getReportPreviewAction(report.chatReportID ?? '', report.reportID);
const moneyRequestActions = reportActions.filter(
- (action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && action.originalMessage && action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE,
+ (action) =>
+ action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU &&
+ action.originalMessage &&
+ (action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE ||
+ Boolean(action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && action.originalMessage.IOUDetails) ||
+ action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK),
);
- if (report.total && moneyRequestActions.length < (reportPreviewAction?.childMoneyRequestCount ?? 0)) {
+ if (report.total && moneyRequestActions.length < (reportPreviewAction?.childMoneyRequestCount ?? 0) && isEmptyObject(transactionThreadReport)) {
const optimisticIOUAction = ReportUtils.buildOptimisticIOUReportAction(
CONST.IOU.REPORT_ACTION_TYPE.CREATE,
0,
@@ -497,7 +500,7 @@ function ReportActionsView({
}
return [...actions, createdAction];
- }, [reportActions, report]);
+ }, [reportActions, report, transactionThreadReport]);
// Comments have not loaded at all yet do nothing
if (!reportActions.length) {
diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
index be8a43b1a483..faf9a7c53cf8 100644
--- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
+++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
@@ -280,6 +280,23 @@ function FloatingActionButtonAndPopover(props) {
text: translate('sidebarScreen.fabNewChat'),
onSelected: () => interceptAnonymousUser(Report.startNewChat),
},
+ ...(canUseTrackExpense
+ ? [
+ {
+ icon: Expensicons.DocumentPlus,
+ text: translate('iou.trackExpense'),
+ onSelected: () =>
+ interceptAnonymousUser(() =>
+ IOU.startMoneyRequest(
+ CONST.IOU.TYPE.TRACK_EXPENSE,
+ // When starting to create a track expense from the global FAB, we need to retrieve selfDM reportID.
+ // If it doesn't exist, we generate a random optimistic reportID and use it for all of the routes in the creation flow.
+ ReportUtils.findSelfDMReportID() || ReportUtils.generateReportID(),
+ ),
+ ),
+ },
+ ]
+ : []),
{
icon: Expensicons.MoneyCircle,
text: translate('iou.requestMoney'),
@@ -306,23 +323,6 @@ function FloatingActionButtonAndPopover(props) {
),
),
},
- ...(canUseTrackExpense
- ? [
- {
- icon: Expensicons.DocumentPlus,
- text: translate('iou.trackExpense'),
- onSelected: () =>
- interceptAnonymousUser(() =>
- IOU.startMoneyRequest(
- CONST.IOU.TYPE.TRACK_EXPENSE,
- // When starting to create a track expense from the global FAB, we need to retrieve selfDM reportID.
- // If it doesn't exist, we generate a random optimistic reportID and use it for all of the routes in the creation flow.
- ReportUtils.findSelfDMReportID() || ReportUtils.generateReportID(),
- ),
- ),
- },
- ]
- : []),
{
icon: Expensicons.Task,
text: translate('newTaskPage.assignTask'),
diff --git a/src/pages/iou/request/step/IOURequestStepAmount.js b/src/pages/iou/request/step/IOURequestStepAmount.js
index 9465c7e3edae..1c3cdefd4392 100644
--- a/src/pages/iou/request/step/IOURequestStepAmount.js
+++ b/src/pages/iou/request/step/IOURequestStepAmount.js
@@ -1,10 +1,8 @@
import {useFocusEffect} from '@react-navigation/native';
import lodashGet from 'lodash/get';
import lodashIsEmpty from 'lodash/isEmpty';
-import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useRef} from 'react';
import {withOnyx} from 'react-native-onyx';
-import taxPropTypes from '@components/taxPropTypes';
import transactionPropTypes from '@components/transactionPropTypes';
import useLocalize from '@hooks/useLocalize';
import * as TransactionEdit from '@libs/actions/TransactionEdit';
@@ -41,24 +39,6 @@ const propTypes = {
/** The draft transaction object being modified in Onyx */
draftTransaction: transactionPropTypes,
-
- /** The policy of the report */
- policy: PropTypes.shape({
- /**
- * Whether or not the policy has tax tracking enabled
- *
- * @deprecated - use tax.trackingEnabled instead
- */
- isTaxTrackingEnabled: PropTypes.bool,
-
- /** Whether or not the policy has tax tracking enabled */
- tax: PropTypes.shape({
- trackingEnabled: PropTypes.bool,
- }),
-
- /** Collection of tax rates attached to a policy */
- taxRates: taxPropTypes,
- }),
};
const defaultProps = {
@@ -66,12 +46,6 @@ const defaultProps = {
transaction: {},
splitDraftTransaction: {},
draftTransaction: {},
- policy: {},
-};
-
-const getTaxAmount = (transaction, defaultTaxValue, amount) => {
- const percentage = (transaction.taxRate ? transaction.taxRate.data.value : defaultTaxValue) || '';
- return TransactionUtils.calculateTaxAmount(percentage, amount);
};
function IOURequestStepAmount({
@@ -82,7 +56,6 @@ function IOURequestStepAmount({
transaction,
splitDraftTransaction,
draftTransaction,
- policy,
}) {
const {translate} = useLocalize();
const textInput = useRef(null);
@@ -96,10 +69,6 @@ function IOURequestStepAmount({
const {amount: transactionAmount} = ReportUtils.getTransactionDetails(isEditingSplitBill && !lodashIsEmpty(splitDraftTransaction) ? splitDraftTransaction : transaction);
const {currency} = ReportUtils.getTransactionDetails(isEditing ? draftTransaction : transaction);
- const taxRates = lodashGet(policy, 'taxRates', {});
- const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report));
- const isTaxTrackingEnabled = isPolicyExpenseChat && lodashGet(policy, 'tax.trackingEnabled', policy.isTaxTrackingEnabled);
-
useFocusEffect(
useCallback(() => {
focusTimeoutRef.current = setTimeout(() => textInput.current && textInput.current.focus(), CONST.ANIMATED_TRANSITION);
@@ -156,12 +125,6 @@ function IOURequestStepAmount({
isSaveButtonPressed.current = true;
const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(amount));
- if ((iouRequestType === CONST.IOU.REQUEST_TYPE.MANUAL || backTo) && isTaxTrackingEnabled) {
- const taxAmount = getTaxAmount(transaction, taxRates.defaultValue, amountInSmallestCurrencyUnits);
- const taxAmountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount));
- IOU.setMoneyRequestTaxAmount(transaction.transactionID, taxAmountInSmallestCurrencyUnits);
- }
-
IOU.setMoneyRequestAmount_temporaryForRefactor(transactionID, amountInSmallestCurrencyUnits, currency || CONST.CURRENCY.USD, true);
if (backTo) {
@@ -242,9 +205,6 @@ export default compose(
return `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`;
},
},
- policy: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`,
- },
draftTransaction: {
key: ({route}) => {
const transactionID = lodashGet(route, 'params.transactionID', 0);
diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js
index 61ae0d2b67b7..0df9a7333e7a 100644
--- a/src/pages/iou/request/step/IOURequestStepConfirmation.js
+++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js
@@ -90,7 +90,8 @@ function IOURequestStepConfirmation({
const receiptFilename = lodashGet(transaction, 'filename');
const receiptPath = lodashGet(transaction, 'receipt.source');
const receiptType = lodashGet(transaction, 'receipt.type');
- const transactionTaxCode = transaction.taxRate && transaction.taxRate.keyForList;
+ const foreignTaxDefault = lodashGet(policy, 'taxRates.foreignTaxDefault');
+ const transactionTaxCode = transaction.taxRate ? transaction.taxRate.data.code : foreignTaxDefault;
const transactionTaxAmount = transaction.taxAmount;
const requestType = TransactionUtils.getRequestType(transaction);
const headerTitle = useMemo(() => {
diff --git a/src/pages/iou/request/step/IOURequestStepDate.js b/src/pages/iou/request/step/IOURequestStepDate.tsx
similarity index 57%
rename from src/pages/iou/request/step/IOURequestStepDate.js
rename to src/pages/iou/request/step/IOURequestStepDate.tsx
index f7b8b1ca3869..682204a4510f 100644
--- a/src/pages/iou/request/step/IOURequestStepDate.js
+++ b/src/pages/iou/request/step/IOURequestStepDate.tsx
@@ -1,58 +1,46 @@
-import lodashGet from 'lodash/get';
import lodashIsEmpty from 'lodash/isEmpty';
-import PropTypes from 'prop-types';
import React from 'react';
+import type {OnyxEntry} from 'react-native-onyx';
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 type {FormOnyxValues} from '@components/Form/types';
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 type SCREENS from '@src/SCREENS';
import INPUT_IDS from '@src/types/form/MoneyRequestDateForm';
-import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes';
+import type * as OnyxTypes from '@src/types/onyx';
import StepScreenWrapper from './StepScreenWrapper';
import withFullTransactionOrNotFound from './withFullTransactionOrNotFound';
+import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound';
import withWritableReportOrNotFound from './withWritableReportOrNotFound';
-const propTypes = {
- /** Navigation route context info provided by react navigation */
- route: IOURequestStepRoutePropTypes.isRequired,
-
- /** Onyx Props */
- /** Holds data related to Money Request view state, rather than the underlying Money Request data. */
- transaction: transactionPropTypes,
-
+type IOURequestStepDateOnyxProps = {
/** The draft transaction that holds data to be persisted on the current transaction */
- splitDraftTransaction: transactionPropTypes,
+ splitDraftTransaction: OnyxEntry;
/** The policy of the report */
- policy: policyPropTypes.policy,
+ policy: OnyxEntry;
/** Collection of categories attached to a policy */
- policyCategories: PropTypes.objectOf(categoryPropTypes),
+ policyCategories: OnyxEntry;
/** Collection of tags attached to a policy */
- policyTags: tagPropTypes,
+ policyTags: OnyxEntry;
};
-const defaultProps = {
- transaction: {},
- splitDraftTransaction: {},
- policy: null,
- policyTags: null,
- policyCategories: null,
-};
+type IOURequestStepDateProps = IOURequestStepDateOnyxProps &
+ WithWritableReportOrNotFoundProps & {
+ /** Holds data related to Money Request view state, rather than the underlying Money Request data. */
+ transaction: OnyxEntry;
+ };
function IOURequestStepDate({
route: {
@@ -63,7 +51,7 @@ function IOURequestStepDate({
policy,
policyTags,
policyCategories,
-}) {
+}: IOURequestStepDateProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const isEditing = action === CONST.IOU.ACTION.EDIT;
@@ -75,11 +63,7 @@ function IOURequestStepDate({
Navigation.goBack(backTo);
};
- /**
- * @param {Object} value
- * @param {String} value.moneyRequestCreated
- */
- const updateDate = (value) => {
+ const updateDate = (value: FormOnyxValues) => {
const newCreated = value.moneyRequestCreated;
// Only update created if it has changed
@@ -90,15 +74,15 @@ function IOURequestStepDate({
// In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value
if (isEditingSplitBill) {
- IOU.setDraftSplitTransaction(transaction.transactionID, {created: newCreated});
+ IOU.setDraftSplitTransaction(transaction?.transactionID ?? '0', {created: newCreated});
navigateBack();
return;
}
- IOU.setMoneyRequestCreated(transaction.transactionID, newCreated, action === CONST.IOU.ACTION.CREATE);
+ IOU.setMoneyRequestCreated(transaction?.transactionID ?? '0', newCreated, action === CONST.IOU.ACTION.CREATE);
if (isEditing) {
- IOU.updateMoneyRequestDate(transaction.transactionID, reportID, newCreated, policy, policyTags, policyCategories);
+ IOU.updateMoneyRequestDate(transaction?.transactionID ?? '0', reportID, newCreated, policy, policyTags, policyCategories);
}
navigateBack();
@@ -108,7 +92,7 @@ function IOURequestStepDate({
@@ -132,28 +116,29 @@ function IOURequestStepDate({
);
}
-IOURequestStepDate.propTypes = propTypes;
-IOURequestStepDate.defaultProps = defaultProps;
IOURequestStepDate.displayName = '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'}`,
+const IOURequestStepDateWithOnyx = withOnyx({
+ splitDraftTransaction: {
+ key: ({route}) => {
+ const transactionID = route?.params.transactionID ?? 0;
+ return `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`;
},
- }),
-)(IOURequestStepDate);
+ },
+ 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);
+
+// eslint-disable-next-line rulesdir/no-negated-variables
+const IOURequestStepDateWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepDateWithOnyx);
+// eslint-disable-next-line rulesdir/no-negated-variables
+const IOURequestStepDateWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepDateWithWritableReportOrNotFound);
+
+export default IOURequestStepDateWithFullTransactionOrNotFound;
diff --git a/src/pages/iou/request/step/IOURequestStepDescription.js b/src/pages/iou/request/step/IOURequestStepDescription.tsx
similarity index 57%
rename from src/pages/iou/request/step/IOURequestStepDescription.js
rename to src/pages/iou/request/step/IOURequestStepDescription.tsx
index 8c3327a5ee5c..d075ed81c956 100644
--- a/src/pages/iou/request/step/IOURequestStepDescription.js
+++ b/src/pages/iou/request/step/IOURequestStepDescription.tsx
@@ -1,81 +1,57 @@
import {useFocusEffect} from '@react-navigation/native';
-import lodashGet from 'lodash/get';
import lodashIsEmpty from 'lodash/isEmpty';
-import PropTypes from 'prop-types';
import React, {useCallback, useRef} from 'react';
import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
-import categoryPropTypes from '@components/categoryPropTypes';
import FormProvider from '@components/Form/FormProvider';
import InputWrapperWithRef from '@components/Form/InputWrapper';
-import tagPropTypes from '@components/tagPropTypes';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
+import type {AnimatedTextInputRef} from '@components/RNTextInput';
import TextInput from '@components/TextInput';
-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 * as ReportUtils from '@libs/ReportUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import updateMultilineInputRange from '@libs/updateMultilineInputRange';
-import reportActionPropTypes from '@pages/home/report/reportActionPropTypes';
-import reportPropTypes from '@pages/reportPropTypes';
import * as IOU from '@userActions/IOU';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import {policyPropTypes} from '@src/pages/workspace/withPolicy';
+import type SCREENS from '@src/SCREENS';
import INPUT_IDS from '@src/types/form/MoneyRequestDescriptionForm';
-import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes';
+import type * as OnyxTypes from '@src/types/onyx';
import StepScreenWrapper from './StepScreenWrapper';
import withFullTransactionOrNotFound from './withFullTransactionOrNotFound';
+import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound';
import withWritableReportOrNotFound from './withWritableReportOrNotFound';
-const propTypes = {
- /** Navigation route context info provided by react navigation */
- route: IOURequestStepRoutePropTypes.isRequired,
-
- /** Onyx Props */
- /** Holds data related to Money Request view state, rather than the underlying Money Request data. */
- transaction: transactionPropTypes,
-
+type IOURequestStepDescriptionOnyxProps = {
/** The draft transaction that holds data to be persisted on the current transaction */
- splitDraftTransaction: transactionPropTypes,
+ splitDraftTransaction: OnyxEntry;
/** The policy of the report */
- policy: policyPropTypes.policy,
+ policy: OnyxEntry;
/** Collection of categories attached to a policy */
- policyCategories: PropTypes.objectOf(categoryPropTypes),
+ policyCategories: OnyxEntry;
/** Collection of tags attached to a policy */
- policyTags: tagPropTypes,
+ policyTags: OnyxEntry;
/** The actions from the parent report */
- reportActions: PropTypes.shape(reportActionPropTypes),
+ reportActions: OnyxEntry;
/** Session info for the currently logged in user. */
- session: PropTypes.shape({
- /** Currently logged in user accountID */
- accountID: PropTypes.number,
-
- /** Currently logged in user email */
- email: PropTypes.string,
- }).isRequired,
-
- /** The report attached to the transaction */
- report: reportPropTypes,
+ session: OnyxEntry;
};
-const defaultProps = {
- transaction: {},
- splitDraftTransaction: {},
- policy: null,
- policyTags: null,
- policyCategories: null,
- reportActions: {},
- report: {},
-};
+type IOURequestStepDescriptionProps = IOURequestStepDescriptionOnyxProps &
+ WithWritableReportOrNotFoundProps & {
+ /** Holds data related to Money Request view state, rather than the underlying Money Request data. */
+ transaction: OnyxEntry;
+ };
function IOURequestStepDescription({
route: {
@@ -89,15 +65,14 @@ function IOURequestStepDescription({
reportActions,
session,
report,
-}) {
+}: IOURequestStepDescriptionProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
- const inputRef = useRef(null);
- const focusTimeoutRef = useRef(null);
+ const inputRef = useRef(null);
+ const focusTimeoutRef = useRef(null);
// In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value
const isEditingSplitBill = iouType === CONST.IOU.TYPE.SPLIT && action === CONST.IOU.ACTION.EDIT;
- const currentDescription =
- isEditingSplitBill && !lodashIsEmpty(splitDraftTransaction) ? lodashGet(splitDraftTransaction, 'comment.comment', '') : lodashGet(transaction, 'comment.comment', '');
+ const currentDescription = isEditingSplitBill && !lodashIsEmpty(splitDraftTransaction) ? splitDraftTransaction?.comment.comment ?? '' : transaction?.comment.comment ?? '';
useFocusEffect(
useCallback(() => {
focusTimeoutRef.current = setTimeout(() => {
@@ -115,11 +90,9 @@ function IOURequestStepDescription({
);
/**
- * @param {Object} values
- * @param {String} values.title
- * @returns {Object} - An object containing the errors for each inputID
+ * @returns - An object containing the errors for each inputID
*/
- const validate = useCallback((values) => {
+ const validate = useCallback((values: FormOnyxValues): FormInputErrors => {
const errors = {};
if (values.moneyRequestComment.length > CONST.DESCRIPTION_LIMIT) {
@@ -136,11 +109,7 @@ function IOURequestStepDescription({
Navigation.goBack(backTo);
};
- /**
- * @param {Object} value
- * @param {String} value.moneyRequestComment
- */
- const updateComment = (value) => {
+ const updateComment = (value: FormOnyxValues) => {
const newComment = value.moneyRequestComment.trim();
// Only update comment if it has changed
@@ -151,24 +120,25 @@ function IOURequestStepDescription({
// In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value
if (isEditingSplitBill) {
- IOU.setDraftSplitTransaction(transaction.transactionID, {comment: newComment});
+ IOU.setDraftSplitTransaction(transaction?.transactionID ?? '0', {comment: newComment});
navigateBack();
return;
}
- IOU.setMoneyRequestDescription(transaction.transactionID, newComment, action === CONST.IOU.ACTION.CREATE);
+ IOU.setMoneyRequestDescription(transaction?.transactionID ?? '0', newComment, action === CONST.IOU.ACTION.CREATE);
if (action === CONST.IOU.ACTION.EDIT) {
- IOU.updateMoneyRequestDescription(transaction.transactionID, reportID, newComment, policy, policyTags, policyCategories);
+ IOU.updateMoneyRequestDescription(transaction?.transactionID ?? '0', reportID, newComment, policy, policyTags, policyCategories);
}
navigateBack();
};
- const reportAction = reportActions[report.parentReportActionID || reportActionID];
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing doesn't achieve the same result in this case
+ const reportAction = reportActions?.[report?.parentReportActionID || reportActionID] ?? null;
const isEditing = action === CONST.IOU.ACTION.EDIT;
const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT;
- const canEditSplitBill = isSplitBill && reportAction && session.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction);
+ const canEditSplitBill = isSplitBill && reportAction && session?.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction);
// eslint-disable-next-line rulesdir/no-negated-variables
const shouldShowNotFoundPage = isEditing && (isSplitBill ? !canEditSplitBill : !ReportUtils.canEditMoneyRequest(reportAction));
return (
@@ -213,46 +183,47 @@ function IOURequestStepDescription({
);
}
-IOURequestStepDescription.propTypes = propTypes;
-IOURequestStepDescription.defaultProps = defaultProps;
IOURequestStepDescription.displayName = 'IOURequestStepDescription';
-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'}`,
+const IOURequestStepDescriptionWithOnyx = withOnyx({
+ splitDraftTransaction: {
+ key: ({route}) => {
+ const transactionID = route?.params.transactionID ?? 0;
+ return `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`;
},
- policyTags: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`,
- },
- reportActions: {
- key: ({
- report,
- route: {
- params: {action, iouType},
- },
- }) => {
- let reportID = '0';
- if (action === CONST.IOU.ACTION.EDIT) {
- reportID = iouType === CONST.IOU.TYPE.SPLIT ? report.reportID : report.parentReportID;
- }
- return `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`;
+ },
+ 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'}`,
+ },
+ reportActions: {
+ key: ({
+ report,
+ route: {
+ params: {action, iouType},
},
- canEvict: false,
+ }) => {
+ let reportID = '0';
+ if (action === CONST.IOU.ACTION.EDIT) {
+ reportID = iouType === CONST.IOU.TYPE.SPLIT ? report?.reportID ?? '0' : report?.parentReportID ?? '0';
+ }
+ return `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`;
},
- session: {
- key: ONYXKEYS.SESSION,
- },
- }),
-)(IOURequestStepDescription);
+ canEvict: false,
+ },
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+})(IOURequestStepDescription);
+
+// eslint-disable-next-line rulesdir/no-negated-variables
+const IOURequestStepDescriptionWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepDescriptionWithOnyx);
+// eslint-disable-next-line rulesdir/no-negated-variables
+const IOURequestStepDescriptionWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepDescriptionWithWritableReportOrNotFound);
+
+export default IOURequestStepDescriptionWithFullTransactionOrNotFound;
diff --git a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.native.js b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.native.js
index 65c17d3cb7ab..64fa291b2003 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.native.js
+++ b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.native.js
@@ -15,6 +15,7 @@ const NavigationAwareCamera = React.forwardRef(({cameraTabIndex, ...props}, ref)
return (
{
// Store the receipt on the transaction object in Onyx
@@ -257,7 +258,7 @@ function IOURequestStepScan({
showCameraAlert();
Log.warn('Error taking photo', error);
});
- }, [flash, action, translate, transactionID, updateScanAndNavigate, navigateToConfirmationStep, cameraPermissionStatus]);
+ }, [flash, hasFlash, action, translate, transactionID, updateScanAndNavigate, navigateToConfirmationStep, cameraPermissionStatus]);
// Wait for camera permission status to render
if (cameraPermissionStatus == null) {
@@ -356,20 +357,22 @@ function IOURequestStepScan({
height={CONST.RECEIPT.SHUTTER_SIZE}
/>
- setFlash((prevFlash) => !prevFlash)}
- >
-
-
+ {hasFlash && (
+ setFlash((prevFlash) => !prevFlash)}
+ >
+
+
+ )}
);
diff --git a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js
index fb984cb801c1..3d5ddcc1a47f 100644
--- a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js
+++ b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js
@@ -2,18 +2,12 @@ import {useFocusEffect} from '@react-navigation/native';
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useRef} from 'react';
-import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
-import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
-import ScreenWrapper from '@components/ScreenWrapper';
import taxPropTypes from '@components/taxPropTypes';
import transactionPropTypes from '@components/transactionPropTypes';
import useLocalize from '@hooks/useLocalize';
-import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
import * as CurrencyUtils from '@libs/CurrencyUtils';
-import * as IOUUtils from '@libs/IOUUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as TransactionUtils from '@libs/TransactionUtils';
import MoneyRequestAmountForm from '@pages/iou/steps/MoneyRequestAmountForm';
@@ -23,6 +17,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes';
+import StepScreenWrapper from './StepScreenWrapper';
import withFullTransactionOrNotFound from './withFullTransactionOrNotFound';
import withWritableReportOrNotFound from './withWritableReportOrNotFound';
@@ -47,8 +42,8 @@ const propTypes = {
const defaultProps = {
report: {},
- transaction: {},
policy: {},
+ transaction: {},
};
const getTaxAmount = (transaction, defaultTaxValue) => {
@@ -66,7 +61,6 @@ function IOURequestStepTaxAmountPage({
policy,
}) {
const {translate} = useLocalize();
- const styles = useThemeStyles();
const textInput = useRef(null);
const isEditing = Navigation.getActiveRoute().includes('taxAmount');
@@ -120,7 +114,7 @@ function IOURequestStepTaxAmountPage({
const updateTaxAmount = (currentAmount) => {
isSaveButtonPressed.current = true;
const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(currentAmount.amount));
- IOU.setMoneyRequestTaxAmount(transactionID, amountInSmallestCurrencyUnits);
+ IOU.setMoneyRequestTaxAmount(transactionID, amountInSmallestCurrencyUnits, true);
IOU.setMoneyRequestCurrency_temporaryForRefactor(transactionID, currency || CONST.CURRENCY.USD, true);
@@ -144,37 +138,24 @@ function IOURequestStepTaxAmountPage({
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID));
};
- const content = (
- (textInput.current = e)}
- onCurrencyButtonPress={navigateToCurrencySelectionPage}
- onSubmitButtonPress={updateTaxAmount}
- />
- );
-
return (
-
- {({safeAreaPaddingBottomStyle}) => (
-
-
-
- {content}
-
-
- )}
-
+ (textInput.current = e)}
+ onCurrencyButtonPress={navigateToCurrencySelectionPage}
+ onSubmitButtonPress={updateTaxAmount}
+ />
+
);
}
diff --git a/src/pages/iou/request/step/IOURequestStepTaxRatePage.js b/src/pages/iou/request/step/IOURequestStepTaxRatePage.js
index 335964adf309..d4a2c10d24b0 100644
--- a/src/pages/iou/request/step/IOURequestStepTaxRatePage.js
+++ b/src/pages/iou/request/step/IOURequestStepTaxRatePage.js
@@ -3,8 +3,6 @@ import PropTypes from 'prop-types';
import React from 'react';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
-import ScreenWrapper from '@components/ScreenWrapper';
import TaxPicker from '@components/TaxPicker';
import taxPropTypes from '@components/taxPropTypes';
import transactionPropTypes from '@components/transactionPropTypes';
@@ -14,9 +12,11 @@ import * as CurrencyUtils from '@libs/CurrencyUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
+import reportPropTypes from '@pages/reportPropTypes';
import * as IOU from '@userActions/IOU';
import ONYXKEYS from '@src/ONYXKEYS';
import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes';
+import StepScreenWrapper from './StepScreenWrapper';
import withFullTransactionOrNotFound from './withFullTransactionOrNotFound';
import withWritableReportOrNotFound from './withWritableReportOrNotFound';
@@ -27,6 +27,9 @@ const propTypes = {
/** The transaction object being modified in Onyx */
transaction: transactionPropTypes,
+ /** The report attached to the transaction */
+ report: reportPropTypes,
+
/* Onyx Props */
/** The policy of the report */
policy: PropTypes.shape({
@@ -36,6 +39,7 @@ const propTypes = {
};
const defaultProps = {
+ report: {},
policy: {},
transaction: {},
};
@@ -51,46 +55,40 @@ function IOURequestStepTaxRatePage({
},
policy,
transaction,
+ report,
}) {
const {translate} = useLocalize();
+ const taxRates = lodashGet(policy, 'taxRates', {});
+
const navigateBack = () => {
Navigation.goBack(backTo);
};
- const taxRates = lodashGet(policy, 'taxRates', {});
- const defaultTaxKey = taxRates.defaultExternalID;
- const selectedTaxRate = (transaction.taxRate && transaction.taxRate.keyForList) || defaultTaxKey;
+
+ const selectedTaxRate = TransactionUtils.getDefaultTaxName(taxRates, transaction);
const updateTaxRates = (taxes) => {
const taxAmount = getTaxAmount(taxRates, taxes.text, transaction.amount);
const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount));
IOU.setMoneyRequestTaxRate(transaction.transactionID, taxes);
- IOU.setMoneyRequestTaxAmount(transaction.transactionID, amountInSmallestCurrencyUnits);
+ IOU.setMoneyRequestTaxAmount(transaction.transactionID, amountInSmallestCurrencyUnits, true);
Navigation.goBack(backTo);
};
return (
-
- {({insets}) => (
- <>
- navigateBack()}
- />
-
- >
- )}
-
+
+
);
}
diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx
index 4e61ac944aac..a186288e8a27 100644
--- a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx
+++ b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx
@@ -27,6 +27,7 @@ import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';
import type {Waypoint} from '@src/types/onyx/Transaction';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -41,10 +42,10 @@ type IOURequestStepWaypointOnyxProps = {
userLocation: OnyxEntry;
};
-type IOURequestStepWaypointProps = {
- transaction: OnyxEntry;
-} & IOURequestStepWaypointOnyxProps &
- WithWritableReportOrNotFoundProps;
+type IOURequestStepWaypointProps = IOURequestStepWaypointOnyxProps &
+ WithWritableReportOrNotFoundProps & {
+ transaction: OnyxEntry;
+ };
function IOURequestStepWaypoint({
route: {
diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx
index d5d27d8268b1..de49a451da08 100644
--- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx
+++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx
@@ -17,11 +17,13 @@ type WithWritableReportOrNotFoundOnyxProps = {
report: OnyxEntry;
};
-type Route = RouteProp;
+type MoneyRequestRouteName = typeof SCREENS.MONEY_REQUEST.STEP_WAYPOINT | typeof SCREENS.MONEY_REQUEST.STEP_DESCRIPTION;
-type WithWritableReportOrNotFoundProps = WithWritableReportOrNotFoundOnyxProps & {route: Route};
+type Route = RouteProp;
-export default function (
+type WithWritableReportOrNotFoundProps = WithWritableReportOrNotFoundOnyxProps & {route: Route};
+
+export default function , TRef>(
WrappedComponent: ComponentType>,
): React.ComponentType, keyof WithWritableReportOrNotFoundOnyxProps>> {
// eslint-disable-next-line rulesdir/no-negated-variables
diff --git a/src/pages/iou/steps/MoneyRequestAmountForm.tsx b/src/pages/iou/steps/MoneyRequestAmountForm.tsx
index a010e13ff496..00970455fb8a 100644
--- a/src/pages/iou/steps/MoneyRequestAmountForm.tsx
+++ b/src/pages/iou/steps/MoneyRequestAmountForm.tsx
@@ -34,8 +34,11 @@ type MoneyRequestAmountFormProps = {
/** Whether the amount is being edited or not */
isEditing?: boolean;
+ /** Whether the currency symbol is pressable */
+ isCurrencyPressable?: boolean;
+
/** Fired when back button pressed, navigates to currency selection page */
- onCurrencyButtonPress: () => void;
+ onCurrencyButtonPress?: () => void;
/** Fired when submit button pressed, saves the given amount and navigates to the next page */
onSubmitButtonPress: ({amount, currency}: {amount: string; currency: string}) => void;
@@ -59,7 +62,7 @@ const getNewSelection = (oldSelection: Selection, prevLength: number, newLength:
const isAmountInvalid = (amount: string) => !amount.length || parseFloat(amount) < 0.01;
const isTaxAmountInvalid = (currentAmount: string, taxAmount: number, isTaxAmountForm: boolean) =>
- isTaxAmountForm && Number.parseFloat(currentAmount) > CurrencyUtils.convertToFrontendAmount(taxAmount);
+ isTaxAmountForm && Number.parseFloat(currentAmount) > CurrencyUtils.convertToFrontendAmount(Math.abs(taxAmount));
const AMOUNT_VIEW_ID = 'amountView';
const NUM_PAD_CONTAINER_VIEW_ID = 'numPadContainerView';
@@ -70,6 +73,7 @@ function MoneyRequestAmountForm(
amount = 0,
taxAmount = 0,
currency = CONST.CURRENCY.USD,
+ isCurrencyPressable = true,
isEditing = false,
onCurrencyButtonPress,
onSubmitButtonPress,
@@ -98,7 +102,7 @@ function MoneyRequestAmountForm(
const forwardDeletePressedRef = useRef(false);
- const formattedTaxAmount = CurrencyUtils.convertToDisplayString(taxAmount, currency);
+ const formattedTaxAmount = CurrencyUtils.convertToDisplayString(Math.abs(taxAmount), currency);
/**
* Event occurs when a user presses a mouse button over an DOM element.
@@ -301,7 +305,7 @@ function MoneyRequestAmountForm(
setSelection({start, end});
}}
onKeyPress={textInputKeyPress}
- isCurrencyPressable
+ isCurrencyPressable={isCurrencyPressable}
/>
{!!formError && (
;
+import type {CapturedLogs} from '@src/types/onyx';
type ConsolePageOnyxProps = {
/** Logs captured on the current device */
@@ -38,30 +36,6 @@ type ConsolePageOnyxProps = {
type ConsolePageProps = ConsolePageOnyxProps;
-/**
- * Loops through all the logs and parses the message if it's a stringified JSON
- * @param logs Logs captured on the current device
- * @returns CapturedLogs with parsed messages
- */
-const parseStringifyMessages = (logs: Log[]) => {
- if (isEmpty(logs)) {
- return;
- }
-
- return logs.map((log) => {
- try {
- const parsedMessage = JSON.parse(log.message);
- return {
- ...log,
- message: parsedMessage,
- };
- } catch {
- // If the message can't be parsed, just return the original log
- return log;
- }
- });
-};
-
function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) {
const [input, setInput] = useState('');
const [logs, setLogs] = useState(capturedLogs);
@@ -97,14 +71,14 @@ function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) {
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, executeArbitraryCode);
const saveLogs = () => {
- const logsWithParsedMessages = parseStringifyMessages(logsList);
+ const logsWithParsedMessages = parseStringifiedMessages(logsList);
localFileDownload('logs', JSON.stringify(logsWithParsedMessages, null, 2));
};
const shareLogs = () => {
setIsGeneratingLogsFile(true);
- const logsWithParsedMessages = parseStringifyMessages(logsList);
+ const logsWithParsedMessages = parseStringifiedMessages(logsList);
// Generate a file with the logs and pass its path to the list of reports to share it with
localFileCreate('logs', JSON.stringify(logsWithParsedMessages, null, 2)).then(({path, size}) => {
diff --git a/src/pages/settings/AboutPage/TroubleshootPage.tsx b/src/pages/settings/AboutPage/TroubleshootPage.tsx
index e04a834c6341..0e192540ebd2 100644
--- a/src/pages/settings/AboutPage/TroubleshootPage.tsx
+++ b/src/pages/settings/AboutPage/TroubleshootPage.tsx
@@ -3,14 +3,13 @@ import {View} from 'react-native';
import Onyx, {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import type {SvgProps} from 'react-native-svg';
+import ClientSideLoggingToolMenu from '@components/ClientSideLoggingToolMenu';
import ConfirmModal from '@components/ConfirmModal';
import * as Expensicons from '@components/Icon/Expensicons';
import IllustratedHeaderPageLayout from '@components/IllustratedHeaderPageLayout';
import LottieAnimations from '@components/LottieAnimations';
import MenuItemList from '@components/MenuItemList';
-import Switch from '@components/Switch';
import TestToolMenu from '@components/TestToolMenu';
-import TestToolRow from '@components/TestToolRow';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import useEnvironment from '@hooks/useEnvironment';
@@ -18,7 +17,6 @@ import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWaitForNavigation from '@hooks/useWaitForNavigation';
-import * as Console from '@libs/actions/Console';
import Navigation from '@libs/Navigation/Navigation';
import * as App from '@userActions/App';
import * as Report from '@userActions/Report';
@@ -113,13 +111,7 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) {
-
- (shouldStoreLogs ? Console.disableLoggingAndFlushLogs() : Console.setShouldStoreLogs(true))}
- />
-
+
privatePersonalDetails?.address, [privatePersonalDetails]);
- const countryFromUrl = route.params?.country;
+ const countryFromUrlTemp = route?.params?.country;
+
+ // Check if country is valid
+ const countryFromUrl = CONST.ALL_COUNTRIES[countryFromUrlTemp as keyof typeof CONST.ALL_COUNTRIES] ? countryFromUrlTemp : '';
+ const stateFromUrl = useGeographicalStateFromRoute();
const [currentCountry, setCurrentCountry] = useState(address?.country);
const isLoadingPersonalDetails = privatePersonalDetails?.isLoading ?? true;
const [street1, street2] = (address?.street ?? '').split('\n');
@@ -99,6 +105,13 @@ function AddressPage({privatePersonalDetails, route}: AddressPageProps) {
handleAddressChange(countryFromUrl, 'country');
}, [countryFromUrl, handleAddressChange]);
+ useEffect(() => {
+ if (!stateFromUrl) {
+ return;
+ }
+ handleAddressChange(stateFromUrl, 'state');
+ }, [handleAddressChange, stateFromUrl]);
+
return (
+ Object.keys(COMMON_CONST.STATES).map((state) => {
+ const stateName = translate(`allStates.${state as State}.stateName`);
+ const stateISO = translate(`allStates.${state as State}.stateISO`);
+ return {
+ value: stateISO,
+ keyForList: stateISO,
+ text: stateName,
+ isSelected: currentState === stateISO,
+ searchValue: StringUtils.sanitizeString(`${stateISO}${stateName}`),
+ };
+ }),
+
+ [translate, currentState],
+ );
+
+ const searchResults = searchCountryOptions(searchValue, countryStates);
+ const headerMessage = searchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : '';
+
+ const selectCountryState = useCallback(
+ (option: CountryData) => {
+ const backTo = params?.backTo ?? '';
+
+ // Determine navigation action based on "backTo" presence and route stack length.
+ if (navigation.getState()?.routes.length === 1) {
+ // If this is the only page in the navigation stack (examples include direct navigation to this page via URL or page reload).
+ if (_.isEmpty(backTo)) {
+ // No "backTo": default back navigation.
+ Navigation.goBack();
+ } else {
+ // "backTo" provided: navigate back to "backTo" with state parameter.
+ Navigation.goBack(appendParam(backTo, 'state', option.value) as Route);
+ }
+ } else if (!_.isEmpty(backTo)) {
+ // Most common case: Navigation stack has multiple routes and "backTo" is defined: navigate to "backTo" with state parameter.
+ Navigation.navigate(appendParam(backTo, 'state', option.value) as Route);
+ } else {
+ // This is a fallback block and should never execute if StateSelector is correctly appending the "backTo" route.
+ // Navigation stack has multiple routes but no "backTo" defined: default back navigation.
+ Navigation.goBack();
+ }
+ },
+ [navigation, params?.backTo],
+ );
+
+ return (
+
+ {
+ const backTo = params?.backTo ?? '';
+ let backToRoute = '';
+
+ if (backTo) {
+ backToRoute = appendParam(backTo, 'state', currentState ?? '');
+ }
+
+ // @ts-expect-error Navigation.goBack does take a param
+ Navigation.goBack(backToRoute);
+ }}
+ />
+ {/* This empty, non-harmful view fixes the issue with SelectionList scrolling and shouldUseDynamicMaxToRenderPerBatch. It can be removed without consequences if a solution for SelectionList is found. See comment https://github.com/Expensify/App/pull/36770#issuecomment-2017028096 */}
+
+
+
+
+ );
+}
+
+StateSelectionPage.displayName = 'StateSelectionPage';
+
+export default StateSelectionPage;
diff --git a/src/pages/settings/Wallet/AddDebitCardPage.tsx b/src/pages/settings/Wallet/AddDebitCardPage.tsx
index 17465e0c0715..0beb3c16018d 100644
--- a/src/pages/settings/Wallet/AddDebitCardPage.tsx
+++ b/src/pages/settings/Wallet/AddDebitCardPage.tsx
@@ -1,3 +1,4 @@
+import {useRoute} from '@react-navigation/native';
import React, {useEffect, useRef} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
@@ -10,7 +11,7 @@ import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import type {AnimatedTextInputRef} from '@components/RNTextInput';
import ScreenWrapper from '@components/ScreenWrapper';
-import StatePicker from '@components/StatePicker';
+import StateSelector from '@components/StateSelector';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
import TextLink from '@components/TextLink';
@@ -22,6 +23,8 @@ import * as ValidationUtils from '@libs/ValidationUtils';
import * as PaymentMethods from '@userActions/PaymentMethods';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import SCREENS from '@src/SCREENS';
import type {AddDebitCardForm} from '@src/types/form';
import INPUT_IDS from '@src/types/form/AddDebitCardForm';
@@ -58,6 +61,7 @@ function DebitCardPage({formData}: DebitCardPageProps) {
const {translate} = useLocalize();
const prevFormDataSetupComplete = usePrevious(!!formData?.setupComplete);
const nameOnCardRef = useRef(null);
+ const route = useRoute();
/**
* Reset the form values on the mount and unmount so that old errors don't show when this form is displayed again.
@@ -200,7 +204,8 @@ function DebitCardPage({formData}: DebitCardPageProps) {
/>
diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx
index a8354cfd3276..014097cd019c 100644
--- a/src/pages/workspace/WorkspaceInvitePage.tsx
+++ b/src/pages/workspace/WorkspaceInvitePage.tsx
@@ -9,8 +9,8 @@ import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
+import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem';
import type {Section} from '@components/SelectionList/types';
-import UserListItem from '@components/SelectionList/UserListItem';
import withNavigationTransitionEnd from '@components/withNavigationTransitionEnd';
import type {WithNavigationTransitionEndProps} from '@components/withNavigationTransitionEnd';
import useLocalize from '@hooks/useLocalize';
@@ -293,7 +293,7 @@ function WorkspaceInvitePage({
{
@@ -306,7 +306,6 @@ function WorkspaceInvitePage({
showScrollIndicator
showLoadingPlaceholder={!didScreenTransitionEnd || !OptionsListUtils.isPersonalDetailsReady(personalDetailsProp)}
shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
- checkmarkPosition={CONST.DIRECTION.RIGHT}
/>
{
setForeignCurrencyDefault(policyID, keyForList ?? '');
Navigation.goBack(ROUTES.WORKSPACE_TAXES_SETTINGS.getRoute(policyID));
@@ -55,8 +58,7 @@ function WorkspaceTaxesSettingsForeignCurrency({
diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx
index 2fe2985daa22..c6de23069837 100644
--- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx
+++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx
@@ -10,6 +10,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {setWorkspaceCurrencyDefault} from '@libs/actions/Policy';
import Navigation from '@libs/Navigation/Navigation';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
+import * as TransactionUtils from '@libs/TransactionUtils';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
@@ -31,6 +32,7 @@ function WorkspaceTaxesSettingsWorkspaceCurrency({
const {translate} = useLocalize();
const styles = useThemeStyles();
+ const selectedTaxRate = TransactionUtils.getTaxName(policy?.taxRates?.taxes ?? {}, policy?.taxRates?.foreignTaxDefault ?? '');
const submit = ({keyForList}: ListItem) => {
setWorkspaceCurrencyDefault(policyID, keyForList ?? '');
Navigation.goBack(ROUTES.WORKSPACE_TAXES_SETTINGS.getRoute(policyID));
@@ -55,8 +57,7 @@ function WorkspaceTaxesSettingsWorkspaceCurrency({
diff --git a/src/stories/Form.stories.tsx b/src/stories/Form.stories.tsx
index 8eeab971ea88..a2bcfe1db03f 100644
--- a/src/stories/Form.stories.tsx
+++ b/src/stories/Form.stories.tsx
@@ -8,7 +8,7 @@ import FormProvider from '@components/Form/FormProvider';
import type {FormProviderProps} from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
import Picker from '@components/Picker';
-import StatePicker from '@components/StatePicker';
+import StateSelector from '@components/StateSelector';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
import type {MaybePhraseKey} from '@libs/Localize';
@@ -50,7 +50,7 @@ const story: ComponentMeta = {
AddressSearch,
CheckboxWithLabel,
Picker,
- StatePicker,
+ StateSelector,
DatePicker,
},
};
@@ -148,7 +148,7 @@ function Template(props: FormProviderProps) {
/>
diff --git a/src/types/onyx/Console.ts b/src/types/onyx/Console.ts
index 883676fa2aee..592d23fecfa0 100644
--- a/src/types/onyx/Console.ts
+++ b/src/types/onyx/Console.ts
@@ -6,4 +6,6 @@ type Log = {
message: string;
};
-export default Log;
+type CapturedLogs = Record;
+
+export type {Log, CapturedLogs};
diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts
index 196267dc28cc..2e24fe00539a 100644
--- a/src/types/onyx/OriginalMessage.ts
+++ b/src/types/onyx/OriginalMessage.ts
@@ -268,6 +268,10 @@ type OriginalMessageModifiedExpense = {
category?: string;
oldTag?: string;
tag?: string;
+ oldTaxAmount?: number;
+ taxAmount?: number;
+ oldTaxRate?: string;
+ taxRate?: string;
oldBillable?: string;
billable?: string;
};
diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts
index ddb0c33c2f0c..247eb64f48e9 100644
--- a/src/types/onyx/Policy.ts
+++ b/src/types/onyx/Policy.ts
@@ -39,7 +39,7 @@ type TaxRate = OnyxCommon.OnyxValueWithOfflineFeedback<{
/** Name of the a tax rate. */
name: string;
- /** The value of the tax rate as percentage. */
+ /** The value of the tax rate. */
value: string;
/** The code associated with the tax rate. If a tax is created in old dot, code field is undefined */
diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts
index 4c1d154fc1ad..1750fa61e514 100644
--- a/src/types/onyx/Transaction.ts
+++ b/src/types/onyx/Transaction.ts
@@ -102,6 +102,12 @@ type Transaction = OnyxCommon.OnyxValueWithOfflineFeedback<
/** The original transaction amount */
amount: number;
+ /** The transaction tax amount */
+ taxAmount?: number;
+
+ /** The transaction tax code */
+ taxCode?: string;
+
/** Whether the request is billable */
billable?: boolean;
@@ -177,9 +183,6 @@ type Transaction = OnyxCommon.OnyxValueWithOfflineFeedback<
/** The transaction tax rate */
taxRate?: TaxRate;
- /** Tax amount */
- taxAmount?: number;
-
/** Card Transactions */
/** The parent transaction id */
diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts
index 915fba9308d2..e56066440d80 100644
--- a/src/types/onyx/index.ts
+++ b/src/types/onyx/index.ts
@@ -6,7 +6,7 @@ import type Beta from './Beta';
import type BlockedFromConcierge from './BlockedFromConcierge';
import type Card from './Card';
import type {CardList} from './Card';
-import type Log from './Console';
+import type {CapturedLogs, Log} from './Console';
import type Credentials from './Credentials';
import type Currency from './Currency';
import type {CurrencyList} from './Currency';
@@ -175,4 +175,5 @@ export type {
NewGroupChatDraft,
Log,
PolicyJoinMember,
+ CapturedLogs,
};
diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js
index d89c81f58262..49ad848fe466 100644
--- a/tests/unit/OptionsListUtilsTest.js
+++ b/tests/unit/OptionsListUtilsTest.js
@@ -2301,7 +2301,7 @@ describe('OptionsListUtils', () => {
// Adds 'Default' title to default tax.
// Adds value to tax name for more description.
text: 'Tax exempt 1 (0%) • Default',
- keyForList: 'CODE1',
+ keyForList: 'Tax exempt 1 (0%) • Default',
searchText: 'Tax exempt 1 (0%) • Default',
tooltipText: 'Tax exempt 1 (0%) • Default',
isDisabled: undefined,
@@ -2315,7 +2315,7 @@ describe('OptionsListUtils', () => {
},
{
text: 'Tax option 3 (5%)',
- keyForList: 'CODE3',
+ keyForList: 'Tax option 3 (5%)',
searchText: 'Tax option 3 (5%)',
tooltipText: 'Tax option 3 (5%)',
isDisabled: undefined,
@@ -2328,7 +2328,7 @@ describe('OptionsListUtils', () => {
},
{
text: 'Tax rate 2 (3%)',
- keyForList: 'CODE2',
+ keyForList: 'Tax rate 2 (3%)',
searchText: 'Tax rate 2 (3%)',
tooltipText: 'Tax rate 2 (3%)',
isDisabled: undefined,
@@ -2351,7 +2351,7 @@ describe('OptionsListUtils', () => {
data: [
{
text: 'Tax rate 2 (3%)',
- keyForList: 'CODE2',
+ keyForList: 'Tax rate 2 (3%)',
searchText: 'Tax rate 2 (3%)',
tooltipText: 'Tax rate 2 (3%)',
isDisabled: undefined,
diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts
index 8389086f8720..b967617918c1 100644
--- a/tests/unit/ViolationUtilsTest.ts
+++ b/tests/unit/ViolationUtilsTest.ts
@@ -7,22 +7,22 @@ import type {PolicyCategories, PolicyTagList, Transaction, TransactionViolation}
const categoryOutOfPolicyViolation = {
name: 'categoryOutOfPolicy',
- type: 'violation',
+ type: CONST.VIOLATION_TYPES.VIOLATION,
};
const missingCategoryViolation = {
name: 'missingCategory',
- type: 'violation',
+ type: CONST.VIOLATION_TYPES.VIOLATION,
};
const tagOutOfPolicyViolation = {
name: 'tagOutOfPolicy',
- type: 'violation',
+ type: CONST.VIOLATION_TYPES.VIOLATION,
};
const missingTagViolation = {
name: 'missingTag',
- type: 'violation',
+ type: CONST.VIOLATION_TYPES.VIOLATION,
};
describe('getViolationsOnyxData', () => {
@@ -54,8 +54,8 @@ describe('getViolationsOnyxData', () => {
it('should handle multiple violations', () => {
transactionViolations = [
- {name: 'duplicatedTransaction', type: 'violation'},
- {name: 'receiptRequired', type: 'violation'},
+ {name: 'duplicatedTransaction', type: CONST.VIOLATION_TYPES.VIOLATION},
+ {name: 'receiptRequired', type: CONST.VIOLATION_TYPES.VIOLATION},
];
const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
expect(result.value).toEqual(expect.arrayContaining(transactionViolations));
@@ -94,8 +94,8 @@ describe('getViolationsOnyxData', () => {
it('should add categoryOutOfPolicy violation to existing violations if they exist', () => {
transaction.category = 'Bananas';
transactionViolations = [
- {name: 'duplicatedTransaction', type: 'violation'},
- {name: 'receiptRequired', type: 'violation'},
+ {name: 'duplicatedTransaction', type: CONST.VIOLATION_TYPES.VIOLATION},
+ {name: 'receiptRequired', type: CONST.VIOLATION_TYPES.VIOLATION},
];
const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
@@ -106,8 +106,8 @@ describe('getViolationsOnyxData', () => {
it('should add missingCategory violation to existing violations if they exist', () => {
transaction.category = undefined;
transactionViolations = [
- {name: 'duplicatedTransaction', type: 'violation'},
- {name: 'receiptRequired', type: 'violation'},
+ {name: 'duplicatedTransaction', type: CONST.VIOLATION_TYPES.VIOLATION},
+ {name: 'receiptRequired', type: CONST.VIOLATION_TYPES.VIOLATION},
];
const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
@@ -177,8 +177,8 @@ describe('getViolationsOnyxData', () => {
it('should add tagOutOfPolicy violation to existing violations if transaction has tag that is not in the policy', () => {
transaction.tag = 'Bananas';
transactionViolations = [
- {name: 'duplicatedTransaction', type: 'violation'},
- {name: 'receiptRequired', type: 'violation'},
+ {name: 'duplicatedTransaction', type: CONST.VIOLATION_TYPES.VIOLATION},
+ {name: 'receiptRequired', type: CONST.VIOLATION_TYPES.VIOLATION},
];
const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
@@ -189,8 +189,8 @@ describe('getViolationsOnyxData', () => {
it('should add missingTag violation to existing violations if transaction does not have a tag', () => {
transaction.tag = undefined;
transactionViolations = [
- {name: 'duplicatedTransaction', type: 'violation'},
- {name: 'receiptRequired', type: 'violation'},
+ {name: 'duplicatedTransaction', type: CONST.VIOLATION_TYPES.VIOLATION},
+ {name: 'receiptRequired', type: CONST.VIOLATION_TYPES.VIOLATION},
];
const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
@@ -253,7 +253,7 @@ describe('getViolationsOnyxData', () => {
it('should return someTagLevelsRequired when a required tag is missing', () => {
const someTagLevelsRequiredViolation = {
name: 'someTagLevelsRequired',
- type: 'violation',
+ type: CONST.VIOLATION_TYPES.VIOLATION,
data: {
errorIndexes: [0, 1, 2],
},
diff --git a/workflow_tests/assertions/testBuildAssertions.ts b/workflow_tests/assertions/testBuildAssertions.ts
index 02e64fb83889..5fb0a8fd9342 100644
--- a/workflow_tests/assertions/testBuildAssertions.ts
+++ b/workflow_tests/assertions/testBuildAssertions.ts
@@ -397,7 +397,7 @@ function assertPostGithubCommentJobExecuted(
});
}
-export {
+export default {
assertValidateActorJobExecuted,
assertGetBranchRefJobExecuted,
assertAndroidJobExecuted,
diff --git a/workflow_tests/assertions/validateGithubActionsAssertions.ts b/workflow_tests/assertions/validateGithubActionsAssertions.ts
index 34d694111b1e..89eb444299ed 100644
--- a/workflow_tests/assertions/validateGithubActionsAssertions.ts
+++ b/workflow_tests/assertions/validateGithubActionsAssertions.ts
@@ -18,5 +18,4 @@ function assertVerifyJobExecuted(workflowResult: Step[], didExecute = true) {
});
}
-// eslint-disable-next-line import/prefer-default-export
-export {assertVerifyJobExecuted};
+export default {assertVerifyJobExecuted};
diff --git a/workflow_tests/assertions/verifyPodfileAssertions.ts b/workflow_tests/assertions/verifyPodfileAssertions.ts
index c2e4a1d8f30e..d837507ede1b 100644
--- a/workflow_tests/assertions/verifyPodfileAssertions.ts
+++ b/workflow_tests/assertions/verifyPodfileAssertions.ts
@@ -17,5 +17,4 @@ function assertVerifyJobExecuted(workflowResult: Step[], didExecute = true) {
});
}
-// eslint-disable-next-line import/prefer-default-export
-export {assertVerifyJobExecuted};
+export default {assertVerifyJobExecuted};
diff --git a/workflow_tests/assertions/verifySignedCommitsAssertions.ts b/workflow_tests/assertions/verifySignedCommitsAssertions.ts
index 99b2e28eb582..8aaabafa9d9d 100644
--- a/workflow_tests/assertions/verifySignedCommitsAssertions.ts
+++ b/workflow_tests/assertions/verifySignedCommitsAssertions.ts
@@ -14,4 +14,4 @@ function assertVerifySignedCommitsJobExecuted(workflowResult: Step[], didExecute
}
// eslint-disable-next-line import/prefer-default-export
-export {assertVerifySignedCommitsJobExecuted};
+export default {assertVerifySignedCommitsJobExecuted};
diff --git a/workflow_tests/jest.config.js b/workflow_tests/jest.config.js
deleted file mode 100644
index cecdf8589d7f..000000000000
--- a/workflow_tests/jest.config.js
+++ /dev/null
@@ -1,9 +0,0 @@
-module.exports = {
- verbose: true,
- transform: {
- '^.+\\.jsx?$': 'babel-jest',
- '^.+\\.tsx?$': 'ts-jest',
- },
- clearMocks: true,
- resetMocks: true,
-};
diff --git a/workflow_tests/jest.config.ts b/workflow_tests/jest.config.ts
new file mode 100644
index 000000000000..8156e449039f
--- /dev/null
+++ b/workflow_tests/jest.config.ts
@@ -0,0 +1,13 @@
+/* eslint-disable @typescript-eslint/naming-convention */
+import type {Config} from 'jest';
+
+const config: Config = {
+ verbose: true,
+ transform: {
+ '^.+\\.(js|jsx|ts|tsx)$': 'ts-jest',
+ },
+ clearMocks: true,
+ resetMocks: true,
+};
+
+export default config;
diff --git a/workflow_tests/mocks/testBuildMocks.ts b/workflow_tests/mocks/testBuildMocks.ts
index f502bfb248ba..cfd194d07185 100644
--- a/workflow_tests/mocks/testBuildMocks.ts
+++ b/workflow_tests/mocks/testBuildMocks.ts
@@ -247,7 +247,7 @@ const TESTBUILD__POSTGITHUBCOMMENT__STEP_MOCKS = [
TESTBUILD__POSTGITHUBCOMMENT__PUBLISH_LINKS_TO_APPS_FOR_DOWNLOAD__STEP_MOCK,
];
-export {
+export default {
TESTBUILD__VALIDATEACTOR__TEAM_MEMBER_HAS_FLAG__STEP_MOCKS,
TESTBUILD__VALIDATEACTOR__TEAM_MEMBER_NO_FLAG__STEP_MOCKS,
TESTBUILD__VALIDATEACTOR__NO_TEAM_MEMBER_HAS_FLAG__STEP_MOCKS,
diff --git a/workflow_tests/scripts/runWorkflowTests.sh b/workflow_tests/scripts/runWorkflowTests.sh
index 71ddcdceffb5..c8ee88e33e99 100755
--- a/workflow_tests/scripts/runWorkflowTests.sh
+++ b/workflow_tests/scripts/runWorkflowTests.sh
@@ -60,4 +60,4 @@ info 'ACT_BINARY environment variable set to an Act executable'
success 'Environment setup properly - running tests'
# Run tests
-npm test -- --config=workflow_tests/jest.config.js --runInBand "$@"
+npm test -- --config=workflow_tests/jest.config.ts --runInBand "$@"
diff --git a/workflow_tests/testBuild.test.js b/workflow_tests/testBuild.test.ts
similarity index 97%
rename from workflow_tests/testBuild.test.js
rename to workflow_tests/testBuild.test.ts
index 7e93b0a9ac9f..cd793fa9aaca 100644
--- a/workflow_tests/testBuild.test.js
+++ b/workflow_tests/testBuild.test.ts
@@ -1,12 +1,13 @@
-const path = require('path');
-const kieMockGithub = require('@kie/mock-github');
-const utils = require('./utils/utils');
-const assertions = require('./assertions/testBuildAssertions');
-const mocks = require('./mocks/testBuildMocks');
-const ExtendedAct = require('./utils/ExtendedAct').default;
+/* eslint-disable @typescript-eslint/naming-convention */
+import {MockGithub} from '@kie/mock-github';
+import path from 'path';
+import assertions from './assertions/testBuildAssertions';
+import mocks from './mocks/testBuildMocks';
+import ExtendedAct from './utils/ExtendedAct';
+import * as utils from './utils/utils';
jest.setTimeout(90 * 1000);
-let mockGithub;
+let mockGithub: MockGithub;
const FILES_TO_COPY_INTO_TEST_REPO = [
...utils.deepCopy(utils.FILES_TO_COPY_INTO_TEST_REPO),
{
@@ -32,7 +33,7 @@ describe('test workflow testBuild', () => {
MYAPP_UPLOAD_KEY_PASSWORD: 'dummy_myapp_upload_key_password',
};
- beforeAll(async () => {
+ beforeAll(() => {
// in case of the tests being interrupted without cleanup the mock repo directory may be left behind
// which breaks the next test run, this removes any possible leftovers
utils.removeMockRepoDir();
@@ -40,7 +41,7 @@ describe('test workflow testBuild', () => {
beforeEach(async () => {
// create a local repository and copy required files
- mockGithub = new kieMockGithub.MockGithub({
+ mockGithub = new MockGithub({
repo: {
testTestBuildWorkflowRepo: {
files: FILES_TO_COPY_INTO_TEST_REPO,
@@ -60,7 +61,7 @@ describe('test workflow testBuild', () => {
PULL_REQUEST_NUMBER: '1234',
};
it('executes workflow', async () => {
- const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, {}, secrets, githubToken, {}, inputs);
@@ -91,7 +92,7 @@ describe('test workflow testBuild', () => {
});
describe('actor is not a team member', () => {
it('stops the workflow after validation', async () => {
- const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, {}, secrets, githubToken, {}, inputs);
@@ -123,7 +124,7 @@ describe('test workflow testBuild', () => {
});
describe('PR does not have READY_TO_BUILD label', () => {
it('stops the workflow after validation', async () => {
- const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, {}, secrets, githubToken, {}, inputs);
@@ -155,7 +156,7 @@ describe('test workflow testBuild', () => {
});
describe('actor is not a team member and PR does not have READY_TO_BUILD label', () => {
it('stops the workflow after validation', async () => {
- const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, {}, secrets, githubToken, {}, inputs);
@@ -187,7 +188,7 @@ describe('test workflow testBuild', () => {
});
describe('android fails', () => {
it('executes workflow, failure reflected', async () => {
- const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, {}, secrets, githubToken, {}, inputs);
@@ -220,7 +221,7 @@ describe('test workflow testBuild', () => {
});
describe('iOS fails', () => {
it('executes workflow, failure reflected', async () => {
- const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, {}, secrets, githubToken, {}, inputs);
@@ -253,7 +254,7 @@ describe('test workflow testBuild', () => {
});
describe('desktop fails', () => {
it('executes workflow, failure reflected', async () => {
- const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, {}, secrets, githubToken, {}, inputs);
@@ -295,7 +296,7 @@ describe('test workflow testBuild', () => {
});
describe('web fails', () => {
it('executes workflow, failure reflected', async () => {
- const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, {}, secrets, githubToken, {}, inputs);
@@ -348,7 +349,7 @@ describe('test workflow testBuild', () => {
},
};
it('executes workflow, without getBranchRef', async () => {
- const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {});
@@ -379,7 +380,7 @@ describe('test workflow testBuild', () => {
});
describe('actor is not a team member', () => {
it('stops the workflow after validation', async () => {
- const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {});
@@ -411,7 +412,7 @@ describe('test workflow testBuild', () => {
});
describe('PR does not have READY_TO_BUILD label', () => {
it('stops the workflow after validation', async () => {
- const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {});
@@ -443,7 +444,7 @@ describe('test workflow testBuild', () => {
});
describe('actor is not a team member and PR does not have READY_TO_BUILD label', () => {
it('stops the workflow after validation', async () => {
- const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {});
@@ -486,7 +487,7 @@ describe('test workflow testBuild', () => {
},
};
it('executes workflow, without getBranchRef', async () => {
- const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {});
@@ -517,7 +518,7 @@ describe('test workflow testBuild', () => {
});
describe('actor is not a team member', () => {
it('stops the workflow after validation', async () => {
- const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {});
@@ -549,7 +550,7 @@ describe('test workflow testBuild', () => {
});
describe('PR does not have READY_TO_BUILD label', () => {
it('stops the workflow after validation', async () => {
- const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {});
@@ -581,7 +582,7 @@ describe('test workflow testBuild', () => {
});
describe('actor is not a team member and PR does not have READY_TO_BUILD label', () => {
it('stops the workflow after validation', async () => {
- const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {});
@@ -624,7 +625,7 @@ describe('test workflow testBuild', () => {
},
};
it('executes workflow, withuout getBranchRef', async () => {
- const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {});
@@ -655,7 +656,7 @@ describe('test workflow testBuild', () => {
});
describe('actor is not a team member', () => {
it('stops the workflow after validation', async () => {
- const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {});
@@ -687,7 +688,7 @@ describe('test workflow testBuild', () => {
});
describe('PR does not have READY_TO_BUILD label', () => {
it('stops the workflow after validation', async () => {
- const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {});
@@ -719,7 +720,7 @@ describe('test workflow testBuild', () => {
});
describe('actor is not a team member and PR does not have READY_TO_BUILD label', () => {
it('stops the workflow after validation', async () => {
- const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testTestBuildWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'testBuild.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, eventOptions, secrets, githubToken, {});
diff --git a/workflow_tests/validateGithubActions.test.js b/workflow_tests/validateGithubActions.test.ts
similarity index 86%
rename from workflow_tests/validateGithubActions.test.js
rename to workflow_tests/validateGithubActions.test.ts
index 404de4c66101..f4972a9ae653 100644
--- a/workflow_tests/validateGithubActions.test.js
+++ b/workflow_tests/validateGithubActions.test.ts
@@ -1,12 +1,12 @@
-const path = require('path');
-const kieMockGithub = require('@kie/mock-github');
-const utils = require('./utils/utils');
-const assertions = require('./assertions/validateGithubActionsAssertions');
-const mocks = require('./mocks/validateGithubActionsMocks').default;
-const ExtendedAct = require('./utils/ExtendedAct').default;
+import {MockGithub} from '@kie/mock-github';
+import path from 'path';
+import assertions from './assertions/validateGithubActionsAssertions';
+import mocks from './mocks/validateGithubActionsMocks';
+import ExtendedAct from './utils/ExtendedAct';
+import * as utils from './utils/utils';
jest.setTimeout(90 * 1000);
-let mockGithub;
+let mockGithub: MockGithub;
const FILES_TO_COPY_INTO_TEST_REPO = [
...utils.deepCopy(utils.FILES_TO_COPY_INTO_TEST_REPO),
{
@@ -19,7 +19,7 @@ describe('test workflow validateGithubActions', () => {
const githubToken = 'dummy_github_token';
const actor = 'Dummy Actor';
- beforeAll(async () => {
+ beforeAll(() => {
// in case of the tests being interrupted without cleanup the mock repo directory may be left behind
// which breaks the next test run, this removes any possible leftovers
utils.removeMockRepoDir();
@@ -27,7 +27,7 @@ describe('test workflow validateGithubActions', () => {
beforeEach(async () => {
// create a local repository and copy required files
- mockGithub = new kieMockGithub.MockGithub({
+ mockGithub = new MockGithub({
repo: {
testValidateGithubActionsWorkflowRepo: {
files: FILES_TO_COPY_INTO_TEST_REPO,
@@ -47,7 +47,7 @@ describe('test workflow validateGithubActions', () => {
action: 'opened',
};
it('executes verification', async () => {
- const repoPath = mockGithub.repo.getPath('testValidateGithubActionsWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testValidateGithubActionsWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'validateGithubActions.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, eventOptions, {}, githubToken);
@@ -70,7 +70,7 @@ describe('test workflow validateGithubActions', () => {
action: 'synchronize',
};
it('executes verification', async () => {
- const repoPath = mockGithub.repo.getPath('testValidateGithubActionsWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testValidateGithubActionsWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'validateGithubActions.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, eventOptions, {}, githubToken);
diff --git a/workflow_tests/verifyPodfile.test.js b/workflow_tests/verifyPodfile.test.ts
similarity index 91%
rename from workflow_tests/verifyPodfile.test.js
rename to workflow_tests/verifyPodfile.test.ts
index a58e10f974e4..6124cdc856c1 100644
--- a/workflow_tests/verifyPodfile.test.js
+++ b/workflow_tests/verifyPodfile.test.ts
@@ -1,12 +1,13 @@
-const path = require('path');
-const kieMockGithub = require('@kie/mock-github');
-const utils = require('./utils/utils');
-const assertions = require('./assertions/verifyPodfileAssertions');
-const mocks = require('./mocks/verifyPodfileMocks').default;
-const ExtendedAct = require('./utils/ExtendedAct').default;
+import {MockGithub} from '@kie/mock-github';
+import path from 'path';
+import assertions from './assertions/verifyPodfileAssertions';
+import mocks from './mocks/verifyPodfileMocks';
+import ExtendedAct from './utils/ExtendedAct';
+import * as utils from './utils/utils';
jest.setTimeout(90 * 1000);
-let mockGithub;
+let mockGithub: MockGithub;
+
const FILES_TO_COPY_INTO_TEST_REPO = [
...utils.deepCopy(utils.FILES_TO_COPY_INTO_TEST_REPO),
{
@@ -20,7 +21,7 @@ describe('test workflow verifyPodfile', () => {
const actor = 'Dummy Actor';
const osbotifyActor = 'OSBotify';
- beforeAll(async () => {
+ beforeAll(() => {
// in case of the tests being interrupted without cleanup the mock repo directory may be left behind
// which breaks the next test run, this removes any possible leftovers
utils.removeMockRepoDir();
@@ -28,7 +29,7 @@ describe('test workflow verifyPodfile', () => {
beforeEach(async () => {
// create a local repository and copy required files
- mockGithub = new kieMockGithub.MockGithub({
+ mockGithub = new MockGithub({
repo: {
testVerifyPodfileWorkflowRepo: {
files: FILES_TO_COPY_INTO_TEST_REPO,
@@ -48,7 +49,7 @@ describe('test workflow verifyPodfile', () => {
action: 'opened',
};
it('executes workflow', async () => {
- const repoPath = mockGithub.repo.getPath('testVerifyPodfileWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testVerifyPodfileWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'verifyPodfile.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, eventOptions, {}, githubToken);
@@ -67,7 +68,7 @@ describe('test workflow verifyPodfile', () => {
});
describe('actor is OSBotify', () => {
it('does not execute workflow', async () => {
- const repoPath = mockGithub.repo.getPath('testVerifyPodfileWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testVerifyPodfileWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'verifyPodfile.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, eventOptions, {}, githubToken);
@@ -92,7 +93,7 @@ describe('test workflow verifyPodfile', () => {
action: 'synchronize',
};
it('executes workflow', async () => {
- const repoPath = mockGithub.repo.getPath('testVerifyPodfileWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testVerifyPodfileWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'verifyPodfile.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, eventOptions, {}, githubToken);
@@ -111,7 +112,7 @@ describe('test workflow verifyPodfile', () => {
});
describe('actor is OSBotify', () => {
it('does not execute workflow', async () => {
- const repoPath = mockGithub.repo.getPath('testVerifyPodfileWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testVerifyPodfileWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'verifyPodfile.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, eventOptions, {}, githubToken);
diff --git a/workflow_tests/verifySignedCommits.test.js b/workflow_tests/verifySignedCommits.test.ts
similarity index 86%
rename from workflow_tests/verifySignedCommits.test.js
rename to workflow_tests/verifySignedCommits.test.ts
index 4c21cd24e360..3bf6c15ec1bb 100644
--- a/workflow_tests/verifySignedCommits.test.js
+++ b/workflow_tests/verifySignedCommits.test.ts
@@ -1,12 +1,13 @@
-const path = require('path');
-const kieMockGithub = require('@kie/mock-github');
-const utils = require('./utils/utils');
-const assertions = require('./assertions/verifySignedCommitsAssertions');
-const mocks = require('./mocks/verifySignedCommitsMocks').default;
-const ExtendedAct = require('./utils/ExtendedAct').default;
+import {MockGithub} from '@kie/mock-github';
+import path from 'path';
+import assertions from './assertions/verifySignedCommitsAssertions';
+import mocks from './mocks/verifySignedCommitsMocks';
+import ExtendedAct from './utils/ExtendedAct';
+import * as utils from './utils/utils';
jest.setTimeout(90 * 1000);
-let mockGithub;
+let mockGithub: MockGithub;
+
const FILES_TO_COPY_INTO_TEST_REPO = [
...utils.deepCopy(utils.FILES_TO_COPY_INTO_TEST_REPO),
{
@@ -19,7 +20,7 @@ describe('test workflow verifySignedCommits', () => {
const githubToken = 'dummy_github_token';
const actor = 'Dummy Actor';
- beforeAll(async () => {
+ beforeAll(() => {
// in case of the tests being interrupted without cleanup the mock repo directory may be left behind
// which breaks the next test run, this removes any possible leftovers
utils.removeMockRepoDir();
@@ -27,7 +28,7 @@ describe('test workflow verifySignedCommits', () => {
beforeEach(async () => {
// create a local repository and copy required files
- mockGithub = new kieMockGithub.MockGithub({
+ mockGithub = new MockGithub({
repo: {
testVerifySignedCommitsWorkflowRepo: {
files: FILES_TO_COPY_INTO_TEST_REPO,
@@ -47,7 +48,7 @@ describe('test workflow verifySignedCommits', () => {
action: 'opened',
};
it('test stub', async () => {
- const repoPath = mockGithub.repo.getPath('testVerifySignedCommitsWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testVerifySignedCommitsWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'verifySignedCommits.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, eventOptions, {}, githubToken);
@@ -70,7 +71,7 @@ describe('test workflow verifySignedCommits', () => {
action: 'synchronize',
};
it('test stub', async () => {
- const repoPath = mockGithub.repo.getPath('testVerifySignedCommitsWorkflowRepo') || '';
+ const repoPath = mockGithub.repo.getPath('testVerifySignedCommitsWorkflowRepo') ?? '';
const workflowPath = path.join(repoPath, '.github', 'workflows', 'verifySignedCommits.yml');
let act = new ExtendedAct(repoPath, workflowPath);
act = utils.setUpActParams(act, event, eventOptions, {}, githubToken);