diff --git a/android/app/build.gradle b/android/app/build.gradle
index b792f7830ea4..eb66dbf2f89d 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 1001045103
- versionName "1.4.51-3"
+ versionCode 1001045203
+ versionName "1.4.52-3"
}
flavorDimensions "default"
diff --git a/assets/images/qrcode.svg b/assets/images/qrcode.svg
index f506a944d54e..42c49c958246 100644
--- a/assets/images/qrcode.svg
+++ b/assets/images/qrcode.svg
@@ -1 +1,14 @@
-
\ No newline at end of file
+
+
+
diff --git a/contributingGuides/APPLE_GOOGLE_SIGNIN.md b/contributingGuides/APPLE_GOOGLE_SIGNIN.md
index 4bb86e31b486..5c95dfb60950 100644
--- a/contributingGuides/APPLE_GOOGLE_SIGNIN.md
+++ b/contributingGuides/APPLE_GOOGLE_SIGNIN.md
@@ -98,27 +98,6 @@ Due to some technical constraints, Apple and Google sign-in may require addition
## Apple
-### iOS/Android
-
-The iOS and Android implementations do not require extra steps to test, aside from signing into an Apple account on the iOS device before being able to use Sign in with Apple.
-
-### Web and desktop
-
-#### Render the web Sign In with Apple button in development
-
-The Google Sign In button renders differently in development mode. To prevent confusion
-for developers about a possible regression, we decided to not render third party buttons in
-development mode.
-
-To show the Apple Sign In button in development mode, you can comment out the following code in the
-LoginForm.js file:
-
-```js
-if (CONFIG.ENVIRONMENT === CONST.ENVIRONMENT.DEV) {
- return;
-}
-```
-
#### Port requirements
The Sign in with Apple process will break after the user signs in if the pop-up process is not started from a page at an HTTPS domain registered with Apple. To fix this, you could make a new configuration with your own HTTPS domain, but then the Apple configuration won't match that of Expensify's backend.
@@ -240,53 +219,49 @@ open desktop-build
2. Even with this build, the deep link may not be handled by the correct app, as the development Electron config seems to intercept it sometimes. To manage this, install [SwiftDefaultApps](https://github.com/Lord-Kamina/SwiftDefaultApps), which adds a preference pane that can be used to configure which app should handle deep links.
-## Google
-
-### Web
-
-#### Render the web Sign In with Google button in Development
+### Test the Apple / Google SSO buttons in development environment
-The Google Sign In button renders differently in development mode. To prevent confusion
+The Apple/Google Sign In button renders differently in development mode. To prevent confusion
for developers about a possible regression, we decided to not render third party buttons in
development mode.
-To show the Google Sign In button in development mode, you can comment out the following code in the
-LoginForm.js file:
-
-```js
-if (CONFIG.ENVIRONMENT === CONST.ENVIRONMENT.DEV) {
- return;
-}
-```
-
-#### Host/Port requirements
-
-Google allows the web app to be hosted at localhost, but according to the
-current Google console configuration for the Expensify client ID, it must be
-hosted on port 8082.
-
-Also note that you'll need to update the webpack.dev.js config to change `host` from `dev.new.expensify.com` to `localhost` and server type from `https` to `http`. The reason for this is that Google Sign In allows localhost, but `dev.new.expensify.com` is not a registered Google Sign In domain.
-
-```diff
-diff --git a/config/webpack/webpack.dev.js b/config/webpack/webpack.dev.js
-index e28383eff5..b14f6f34aa 100644
---- a/config/webpack/webpack.dev.js
-+++ b/config/webpack/webpack.dev.js
-@@ -44,9 +44,9 @@ module.exports = (env = {}) =>
- ...proxySettings,
- historyApiFallback: true,
- port,
-- host: 'dev.new.expensify.com',
-+ host: 'localhost',
- server: {
-- type: 'https',
-+ type: 'http',
- options: {
- key: path.join(__dirname, 'key.pem'),
- cert: path.join(__dirname, 'certificate.pem'),
-```
-
-### Desktop
+Here's how you can re-enable the SSO buttons in development mode:
+
+- Remove this [condition](https://github.com/Expensify/App/blob/c2a718c9100e704c89ad9564301348bc53a49777/src/pages/signin/LoginForm/BaseLoginForm.tsx#L300) so that we always render the SSO button components
+ ```diff
+ diff --git a/src/pages/signin/LoginForm/BaseLoginForm.tsx b/src/pages/signin/LoginForm/BaseLoginForm.tsx
+ index 4286a26033..850f8944ca 100644
+ --- a/src/pages/signin/LoginForm/BaseLoginForm.tsx
+ +++ b/src/pages/signin/LoginForm/BaseLoginForm.tsx
+ @@ -288,7 +288,7 @@ function BaseLoginForm({account, credentials, closeAccount, blurOnSubmit = false
+ // for developers about possible regressions, we won't render buttons in development mode.
+ // For more information about these differences and how to test in development mode,
+ // see`Expensify/App/contributingGuides/APPLE_GOOGLE_SIGNIN.md`
+ - CONFIG.ENVIRONMENT !== CONST.ENVIRONMENT.DEV && (
+ + (
+
+
+ ...proxySettings,
+ historyApiFallback: true,
+ port,
+ - host: 'dev.new.expensify.com',
+ + host: 'localhost',
+ server: {
+ - type: 'https',
+ + type: 'http',
+ options: {
+ key: path.join(__dirname, 'key.pem'),
+ cert: path.join(__dirname, 'certificate.pem'),
+ ```
#### Set Environment to something other than "Development"
diff --git a/docs/articles/expensify-classic/copilots-and-delegates/Act-as-a-copilot.md b/docs/articles/expensify-classic/copilots-and-delegates/Act-as-a-copilot.md
new file mode 100644
index 000000000000..04bc82a90774
--- /dev/null
+++ b/docs/articles/expensify-classic/copilots-and-delegates/Act-as-a-copilot.md
@@ -0,0 +1,42 @@
+---
+title: Act as a Copilot
+description: How to access another account after being granted Copilot permissions
+---
+
+
+After being assigned as a Copilot, you can access the account you’ve been given Copilot permissions to via the Expensify website or the mobile app.
+
+{% include selector.html values="desktop, mobile" %}
+
+{% include option.html value="desktop" %}
+To switch to Copilot mode,
+1. Click your profile icon in the upper left side of the page.
+2. In the “Copilot Access” section of the dropdown, choose the account you wish to access.
+
+The Expensify header will change to blue, and an airplane icon will appear to show that you are in copilot mode. You can return to your own account any time by clicking your profile icon and selecting **Return to your account**.
+
+{% include end-option.html %}
+
+{% include option.html value="mobile" %}
+To switch to Copilot mode,
+1. Tap the menu icon in the top left.
+2. Tap your profile icon.
+3. Tap **Switch to Copilot Mode** and choose the account.
+
+An airplane icon will appear to show that you are in copilot mode. To return to your own account, follow the same steps above and select **Return to your account**.
+
+{% include end-option.html %}
+
+{% include end-selector.html %}
+
+# FAQs
+
+**Can a Copilot’s secondary login be used to forward receipts?**
+
+Yes, a Copilot can use any of the email addresses tied to their account to forward receipts into the account they are copiloting. To ensure a receipt is routed to the Expensify account you are copiloting instead of your own account, email the receipt to receipts@expensify.com with the email address of the account you are copiloting as the subject line of the email.
+
+**Can I add another copilot to an account that I’m copiloting?**
+
+No, only the original account holder can add another Copilot to their account.
+
+
diff --git a/docs/articles/expensify-classic/domains/Claim-And-Verify-A-Domain.md b/docs/articles/expensify-classic/domains/Claim-And-Verify-A-Domain.md
index bb4b21547892..509961b026e5 100644
--- a/docs/articles/expensify-classic/domains/Claim-And-Verify-A-Domain.md
+++ b/docs/articles/expensify-classic/domains/Claim-And-Verify-A-Domain.md
@@ -31,7 +31,8 @@ You can claim and verify private domains only. Public domains (like gmail.com) c
# Step 2: Verify domain ownership
{% include info.html %}
-To complete this step, you must have a Control workspace, and you’ll need access to your domain provider account (GoDaddy, Wix, GSuite, etc.). If you don’t verify the domain, you will still have access to the domain to add and manage credit card expenses and domain admins, but you will not be able to invite members, add groups, use domain reporting tools, set delegates for employees on vacation, or enable SAML SSO. For more guidance on how to complete this process for a specific provider, check the provider’s website.{% include end-info.html %}
+To complete this step, you must have a Control workspace, and you’ll need access to your domain provider account (GoDaddy, Wix, GSuite, etc.). If you don’t verify the domain, you will still have access to the domain to add and manage credit card expenses and domain admins, but you will not be able to invite members, add groups, use domain reporting tools, set delegates for employees on vacation, or enable SAML SSO. For more guidance on how to complete this process for a specific provider, check the provider’s website.
+{% include end-info.html %}
Log in to your DNS service provider (which may be the website you purchased the domain from or that currently hosts the domain, like NameCheap, GoDaddy, DNSMadeEasy, or Amazon Route53. You may need to contact your company’s IT department if your domain is managed internally).
diff --git a/docs/expensify-classic/hubs/billing-and-subscriptions/index.html b/docs/expensify-classic/hubs/expensify-billing/index.html
similarity index 100%
rename from docs/expensify-classic/hubs/billing-and-subscriptions/index.html
rename to docs/expensify-classic/hubs/expensify-billing/index.html
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index ab5d359a5460..43e6d566c6d0 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageTypeAPPLCFBundleShortVersionString
- 1.4.51
+ 1.4.52CFBundleSignature????CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.51.3
+ 1.4.52.3ITSAppUsesNonExemptEncryptionLSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index ca9200c78376..9cf5dc5d864d 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageTypeBNDLCFBundleShortVersionString
- 1.4.51
+ 1.4.52CFBundleSignature????CFBundleVersion
- 1.4.51.3
+ 1.4.52.3
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index f20b520b1480..a394b26719fa 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName$(PRODUCT_NAME)CFBundleShortVersionString
- 1.4.51
+ 1.4.52CFBundleVersion
- 1.4.51.3
+ 1.4.52.3NSExtensionNSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index 3083c15c0196..ee7a091dc0a9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.51-3",
+ "version": "1.4.52-3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.51-3",
+ "version": "1.4.52-3",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index dc5b261c3b40..90704f0df54f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.51-3",
+ "version": "1.4.52-3",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
diff --git a/src/CONST.ts b/src/CONST.ts
index 6b7ede6f0e10..fa44cda20720 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -993,8 +993,6 @@ const CONST = {
MAGIC_CODE_LENGTH: 6,
MAGIC_CODE_EMPTY_CHAR: ' ',
- RECOVERY_CODE_LENGTH: 8,
-
KEYBOARD_TYPE: {
VISIBLE_PASSWORD: 'visible-password',
ASCII_CAPABLE: 'ascii-capable',
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 97ad4deb477c..3980789223f2 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -330,8 +330,8 @@ const ONYXKEYS = {
ADD_DEBIT_CARD_FORM: 'addDebitCardForm',
ADD_DEBIT_CARD_FORM_DRAFT: 'addDebitCardFormDraft',
WORKSPACE_SETTINGS_FORM: 'workspaceSettingsForm',
- WORKSPACE_CATEGORY_CREATE_FORM: 'workspaceCategoryCreate',
- WORKSPACE_CATEGORY_CREATE_FORM_DRAFT: 'workspaceCategoryCreateDraft',
+ WORKSPACE_CATEGORY_FORM: 'workspaceCategoryForm',
+ WORKSPACE_CATEGORY_FORM_DRAFT: 'workspaceCategoryFormDraft',
WORKSPACE_TAG_CREATE_FORM: 'workspaceTagCreate',
WORKSPACE_TAG_CREATE_FORM_DRAFT: 'workspaceTagCreateDraft',
WORKSPACE_SETTINGS_FORM_DRAFT: 'workspaceSettingsFormDraft',
@@ -417,7 +417,7 @@ type AllOnyxKeys = DeepValueOf;
type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: FormTypes.AddDebitCardForm;
[ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: FormTypes.WorkspaceSettingsForm;
- [ONYXKEYS.FORMS.WORKSPACE_CATEGORY_CREATE_FORM]: FormTypes.WorkspaceCategoryCreateForm;
+ [ONYXKEYS.FORMS.WORKSPACE_CATEGORY_FORM]: FormTypes.WorkspaceCategoryForm;
[ONYXKEYS.FORMS.WORKSPACE_TAG_CREATE_FORM]: FormTypes.WorkspaceTagCreateForm;
[ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: FormTypes.WorkspaceRateAndUnitForm;
[ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: FormTypes.CloseAccountForm;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index ad2d9c10700b..680c5bced9a9 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -555,7 +555,7 @@ const ROUTES = {
},
WORKSPACE_CATEGORY_SETTINGS: {
route: 'settings/workspaces/:policyID/categories/:categoryName',
- getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURI(categoryName)}` as const,
+ getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}` as const,
},
WORKSPACE_CATEGORIES_SETTINGS: {
route: 'settings/workspaces/:policyID/categories/settings',
@@ -569,6 +569,10 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/categories/new',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/new` as const,
},
+ WORKSPACE_CATEGORY_EDIT: {
+ route: 'workspace/:policyID/categories/:categoryName/edit',
+ getRoute: (policyID: string, categoryName: string) => `workspace/${policyID}/categories/${encodeURI(categoryName)}/edit` as const,
+ },
WORKSPACE_TAGS: {
route: 'settings/workspaces/:policyID/tags',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags` as const,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 6c742f08bfb7..7ccb24aa19e5 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -231,6 +231,7 @@ const SCREENS = {
SHARE: 'Workspace_Profile_Share',
NAME: 'Workspace_Profile_Name',
CATEGORY_CREATE: 'Category_Create',
+ CATEGORY_EDIT: 'Category_Edit',
CATEGORY_SETTINGS: 'Category_Settings',
CATEGORIES_SETTINGS: 'Categories_Settings',
MORE_FEATURES: 'Workspace_More_Features',
diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.tsx
similarity index 71%
rename from src/components/AddPlaidBankAccount.js
rename to src/components/AddPlaidBankAccount.tsx
index b6fc639546a8..e64c95325fae 100644
--- a/src/components/AddPlaidBankAccount.js
+++ b/src/components/AddPlaidBankAccount.tsx
@@ -1,20 +1,19 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {ActivityIndicator, View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import KeyboardShortcut from '@libs/KeyboardShortcut';
import Log from '@libs/Log';
-import {plaidDataPropTypes} from '@pages/ReimbursementAccount/plaidDataPropTypes';
import * as App from '@userActions/App';
import * as BankAccounts from '@userActions/BankAccounts';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import type {PlaidData} from '@src/types/onyx';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
import FullPageOfflineBlockingView from './BlockingViews/FullPageOfflineBlockingView';
import FormHelpMessage from './FormHelpMessage';
import Icon from './Icon';
@@ -24,103 +23,82 @@ import PlaidLink from './PlaidLink';
import RadioButtons from './RadioButtons';
import Text from './Text';
-const propTypes = {
+type AddPlaidBankAccountOnyxProps = {
/** If the user has been throttled from Plaid */
- isPlaidDisabled: PropTypes.bool,
+ isPlaidDisabled: OnyxEntry;
+ /** Plaid SDK token to use to initialize the widget */
+ plaidLinkToken: OnyxEntry;
+};
+
+type AddPlaidBankAccountProps = AddPlaidBankAccountOnyxProps & {
/** Contains plaid data */
- plaidData: plaidDataPropTypes.isRequired,
+ plaidData: OnyxEntry;
/** Selected account ID from the Picker associated with the end of the Plaid flow */
- selectedPlaidAccountID: PropTypes.string,
-
- /** Plaid SDK token to use to initialize the widget */
- plaidLinkToken: PropTypes.string,
+ selectedPlaidAccountID?: string;
/** Fired when the user exits the Plaid flow */
- onExitPlaid: PropTypes.func,
+ onExitPlaid?: () => void;
/** Fired when the user selects an account */
- onSelect: PropTypes.func,
+ onSelect?: (plaidAccountID: string) => void;
/** Additional text to display */
- text: PropTypes.string,
+ text?: string;
/** The OAuth URI + stateID needed to re-initialize the PlaidLink after the user logs into their bank */
- receivedRedirectURI: PropTypes.string,
+ receivedRedirectURI?: string;
/** During the OAuth flow we need to use the plaidLink token that we initially connected with */
- plaidLinkOAuthToken: PropTypes.string,
+ plaidLinkOAuthToken?: string;
/** If we're updating an existing bank account, what's its bank account ID? */
- bankAccountID: PropTypes.number,
+ bankAccountID?: number;
/** Are we adding a withdrawal account? */
- allowDebit: PropTypes.bool,
+ allowDebit?: boolean;
/** Is displayed in new VBBA */
- isDisplayedInNewVBBA: PropTypes.bool,
+ isDisplayedInNewVBBA?: boolean;
/** Text to display on error message */
- errorText: PropTypes.string,
+ errorText?: string;
/** Function called whenever radio button value changes */
- onInputChange: PropTypes.func,
-};
-
-const defaultProps = {
- selectedPlaidAccountID: '',
- plaidLinkToken: '',
- onExitPlaid: () => {},
- onSelect: () => {},
- text: '',
- receivedRedirectURI: null,
- plaidLinkOAuthToken: '',
- allowDebit: false,
- bankAccountID: 0,
- isPlaidDisabled: false,
- isDisplayedInNewVBBA: false,
- errorText: '',
- onInputChange: () => {},
+ onInputChange?: (plaidAccountID: string) => void;
};
function AddPlaidBankAccount({
plaidData,
- selectedPlaidAccountID,
+ selectedPlaidAccountID = '',
plaidLinkToken,
- onExitPlaid,
- onSelect,
- text,
+ onExitPlaid = () => {},
+ onSelect = () => {},
+ text = '',
receivedRedirectURI,
- plaidLinkOAuthToken,
- bankAccountID,
- allowDebit,
+ plaidLinkOAuthToken = '',
+ bankAccountID = 0,
+ allowDebit = false,
isPlaidDisabled,
- isDisplayedInNewVBBA,
- errorText,
- onInputChange,
-}) {
+ isDisplayedInNewVBBA = false,
+ errorText = '',
+ onInputChange = () => {},
+}: AddPlaidBankAccountProps) {
const theme = useTheme();
const styles = useThemeStyles();
- const plaidBankAccounts = lodashGet(plaidData, 'bankAccounts', []);
- const defaultSelectedPlaidAccount = _.find(plaidBankAccounts, (account) => account.plaidAccountID === selectedPlaidAccountID);
- const defaultSelectedPlaidAccountID = lodashGet(defaultSelectedPlaidAccount, 'plaidAccountID', '');
- const defaultSelectedPlaidAccountMask = lodashGet(
- _.find(plaidBankAccounts, (account) => account.plaidAccountID === selectedPlaidAccountID),
- 'mask',
- '',
- );
- const subscribedKeyboardShortcuts = useRef([]);
- const previousNetworkState = useRef();
+ const plaidBankAccounts = plaidData?.bankAccounts ?? [];
+ const defaultSelectedPlaidAccount = plaidBankAccounts.find((account) => account.plaidAccountID === selectedPlaidAccountID);
+ const defaultSelectedPlaidAccountID = defaultSelectedPlaidAccount?.plaidAccountID ?? '';
+ const defaultSelectedPlaidAccountMask = plaidBankAccounts.find((account) => account.plaidAccountID === selectedPlaidAccountID)?.mask ?? '';
+ const subscribedKeyboardShortcuts = useRef void>>([]);
+ const previousNetworkState = useRef();
const [selectedPlaidAccountMask, setSelectedPlaidAccountMask] = useState(defaultSelectedPlaidAccountMask);
const {translate} = useLocalize();
const {isOffline} = useNetwork();
- /**
- * @returns {String}
- */
- const getPlaidLinkToken = () => {
+ const getPlaidLinkToken = (): string | undefined => {
if (plaidLinkToken) {
return plaidLinkToken;
}
@@ -135,7 +113,7 @@ function AddPlaidBankAccount({
* I'm using useCallback so the useEffect which uses this function doesn't run on every render.
*/
const isAuthenticatedWithPlaid = useCallback(
- () => (receivedRedirectURI && plaidLinkOAuthToken) || !_.isEmpty(lodashGet(plaidData, 'bankAccounts')) || !_.isEmpty(lodashGet(plaidData, 'errors')),
+ () => (!!receivedRedirectURI && !!plaidLinkOAuthToken) || !!plaidData?.bankAccounts?.length || !isEmptyObject(plaidData?.errors),
[plaidData, plaidLinkOAuthToken, receivedRedirectURI],
);
@@ -144,15 +122,15 @@ function AddPlaidBankAccount({
*/
const subscribeToNavigationShortcuts = () => {
// find and block the shortcuts
- const shortcutsToBlock = _.filter(CONST.KEYBOARD_SHORTCUTS, (x) => x.type === CONST.KEYBOARD_SHORTCUTS_TYPES.NAVIGATION_SHORTCUT);
- subscribedKeyboardShortcuts.current = _.map(shortcutsToBlock, (shortcut) =>
+ const shortcutsToBlock = Object.values(CONST.KEYBOARD_SHORTCUTS).filter((shortcut) => 'type' in shortcut && shortcut.type === CONST.KEYBOARD_SHORTCUTS_TYPES.NAVIGATION_SHORTCUT);
+ subscribedKeyboardShortcuts.current = shortcutsToBlock.map((shortcut) =>
KeyboardShortcut.subscribe(
shortcut.shortcutKey,
() => {}, // do nothing
shortcut.descriptionKey,
shortcut.modifiers,
false,
- () => lodashGet(plaidData, 'bankAccounts', []).length > 0, // start bubbling when there are bank accounts
+ () => (plaidData?.bankAccounts ?? []).length > 0, // start bubbling when there are bank accounts
),
);
};
@@ -161,7 +139,7 @@ function AddPlaidBankAccount({
* Unblocks the keyboard shortcuts that can navigate
*/
const unsubscribeToNavigationShortcuts = () => {
- _.each(subscribedKeyboardShortcuts.current, (unsubscribe) => unsubscribe());
+ subscribedKeyboardShortcuts.current.forEach((unsubscribe) => unsubscribe());
subscribedKeyboardShortcuts.current = [];
};
@@ -189,22 +167,21 @@ function AddPlaidBankAccount({
}, [allowDebit, bankAccountID, isAuthenticatedWithPlaid, isOffline]);
const token = getPlaidLinkToken();
- const options = _.map(plaidBankAccounts, (account) => ({
+ const options = plaidBankAccounts.map((account) => ({
value: account.plaidAccountID,
- label: account.addressName,
+ label: account.addressName ?? '',
}));
const {icon, iconSize, iconStyles} = getBankIcon({styles});
- const plaidErrors = lodashGet(plaidData, 'errors');
- const plaidDataErrorMessage = !_.isEmpty(plaidErrors) ? _.chain(plaidErrors).values().first().value() : '';
- const bankName = lodashGet(plaidData, 'bankName');
+ const plaidErrors = plaidData?.errors;
+ const plaidDataErrorMessage = !isEmptyObject(plaidErrors) ? (Object.values(plaidErrors)[0] as string) : '';
+ const bankName = plaidData?.bankName;
/**
- * @param {String} plaidAccountID
*
* When user selects one of plaid accounts we need to set the mask in order to display it on UI
*/
- const handleSelectingPlaidAccount = (plaidAccountID) => {
- const mask = _.find(plaidBankAccounts, (account) => account.plaidAccountID === plaidAccountID).mask;
+ const handleSelectingPlaidAccount = (plaidAccountID: string) => {
+ const mask = plaidBankAccounts.find((account) => account.plaidAccountID === plaidAccountID)?.mask ?? '';
setSelectedPlaidAccountMask(mask);
onSelect(plaidAccountID);
onInputChange(plaidAccountID);
@@ -219,24 +196,24 @@ function AddPlaidBankAccount({
}
const renderPlaidLink = () => {
- if (Boolean(token) && !bankName) {
+ if (!!token && !bankName) {
return (
{
Log.info('[PlaidLink] Success!');
- BankAccounts.openPlaidBankAccountSelector(publicToken, metadata.institution.name, allowDebit, bankAccountID);
+ BankAccounts.openPlaidBankAccountSelector(publicToken, metadata?.institution?.name ?? '', allowDebit, bankAccountID);
}}
onError={(error) => {
- Log.hmmm('[PlaidLink] Error: ', error.message);
+ Log.hmmm('[PlaidLink] Error: ', error?.message);
}}
onEvent={(event, metadata) => {
BankAccounts.setPlaidEvent(event);
// Handle Plaid login errors (will potentially reset plaid token and item depending on the error)
if (event === 'ERROR') {
- Log.hmmm('[PlaidLink] Error: ', metadata);
- if (bankAccountID && metadata && metadata.error_code) {
- BankAccounts.handlePlaidError(bankAccountID, metadata.error_code, metadata.error_message, metadata.request_id);
+ Log.hmmm('[PlaidLink] Error: ', {...metadata});
+ if (bankAccountID && metadata && 'error_code' in metadata) {
+ BankAccounts.handlePlaidError(bankAccountID, metadata.error_code ?? '', metadata.error_message ?? '', metadata.request_id);
}
}
@@ -257,7 +234,7 @@ function AddPlaidBankAccount({
return {plaidDataErrorMessage};
}
- if (lodashGet(plaidData, 'isLoading')) {
+ if (plaidData?.isLoading) {
return (
{translate('bankAccount.chooseAnAccount')}
- {!_.isEmpty(text) && {text}}
+ {!!text && {text}}
- {!_.isEmpty(text) && {text}}
+ {!!text && {text}}
({
plaidLinkToken: {
key: ONYXKEYS.PLAID_LINK_TOKEN,
initWithStoredValues: false,
diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx
index 1ed7b6d188a0..0c047ce52dc8 100755
--- a/src/components/AttachmentModal.tsx
+++ b/src/components/AttachmentModal.tsx
@@ -569,6 +569,7 @@ function AttachmentModal({
diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx
index 2af07701c5d3..303b90a682e6 100644
--- a/src/components/SelectionList/BaseListItem.tsx
+++ b/src/components/SelectionList/BaseListItem.tsx
@@ -4,6 +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';
@@ -23,6 +24,7 @@ function BaseListItem({
onCheckboxPress,
onDismissError = () => {},
rightHandSideComponent,
+ checkmarkPosition = CONST.DIRECTION.LEFT,
keyForList,
errors,
pendingAction,
@@ -76,7 +78,7 @@ function BaseListItem({
style={pressableStyle}
>
- {canSelectMultiple && (
+ {canSelectMultiple && checkmarkPosition === CONST.DIRECTION.LEFT && (
({
{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,
@@ -330,6 +331,7 @@ function BaseSelectionList(
onDismissError={() => onDismissError?.(item)}
shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow}
rightHandSideComponent={rightHandSideComponent}
+ checkmarkPosition={checkmarkPosition}
keyForList={item.keyForList ?? ''}
isMultilineSupported={isRowMultilineSupported}
/>
@@ -555,6 +557,7 @@ function BaseSelectionList(
<>
diff --git a/src/components/SelectionList/TableListItem.tsx b/src/components/SelectionList/TableListItem.tsx
index bc798c64dbc9..6a5630bc9df6 100644
--- a/src/components/SelectionList/TableListItem.tsx
+++ b/src/components/SelectionList/TableListItem.tsx
@@ -19,6 +19,7 @@ function TableListItem({
onDismissError,
shouldPreventDefaultFocusOnSelectRow,
rightHandSideComponent,
+ checkmarkPosition,
}: TableListItemProps) {
const styles = useThemeStyles();
const theme = useTheme();
@@ -42,6 +43,7 @@ function TableListItem({
onDismissError={onDismissError}
shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow}
rightHandSideComponent={rightHandSideComponent}
+ checkmarkPosition={checkmarkPosition}
errors={item.errors}
pendingAction={item.pendingAction}
keyForList={item.keyForList}
diff --git a/src/components/SelectionList/UserListItem.tsx b/src/components/SelectionList/UserListItem.tsx
index 2a3a8dd04a79..b52289ffdb59 100644
--- a/src/components/SelectionList/UserListItem.tsx
+++ b/src/components/SelectionList/UserListItem.tsx
@@ -23,6 +23,7 @@ function UserListItem({
onDismissError,
shouldPreventDefaultFocusOnSelectRow,
rightHandSideComponent,
+ checkmarkPosition,
}: UserListItemProps) {
const styles = useThemeStyles();
const theme = useTheme();
@@ -47,6 +48,7 @@ function UserListItem({
onDismissError={onDismissError}
shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow}
rightHandSideComponent={rightHandSideComponent}
+ checkmarkPosition={checkmarkPosition}
errors={item.errors}
pendingAction={item.pendingAction}
FooterComponent={
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index 08b10369e31f..9e9ba7e5fc27 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -1,5 +1,6 @@
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';
@@ -34,6 +35,9 @@ 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;
@@ -270,6 +274,9 @@ type BaseSelectionListProps = Partial & {
/** 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;
diff --git a/src/components/TimePicker/TimePicker.js b/src/components/TimePicker/TimePicker.js
index f57f2540dfb3..95af2bc0c1b5 100644
--- a/src/components/TimePicker/TimePicker.js
+++ b/src/components/TimePicker/TimePicker.js
@@ -511,6 +511,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
shouldEnableHapticFeedback
innerStyles={styleForAM}
medium={isExtraSmallScreenHeight}
+ large={!isExtraSmallScreenHeight}
text={translate('common.am')}
onLongPress={() => {}}
onPress={() => {
@@ -523,6 +524,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
shouldEnableHapticFeedback
innerStyles={[...styleForPM, styles.ml1]}
medium={isExtraSmallScreenHeight}
+ large={!isExtraSmallScreenHeight}
text={translate('common.pm')}
onLongPress={() => {}}
onPress={() => {
@@ -551,6 +553,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) {
(null);
-function PlaybackContextProvider({children}) {
- const [currentlyPlayingURL, setCurrentlyPlayingURL] = useState(null);
- const [sharedElement, setSharedElement] = useState(null);
- const [originalParent, setOriginalParent] = useState(null);
- const currentVideoPlayerRef = useRef(null);
- const {currentReportID} = useCurrentReportID();
+function PlaybackContextProvider({children}: ChildrenProps) {
+ const [currentlyPlayingURL, setCurrentlyPlayingURL] = useState(null);
+ const [sharedElement, setSharedElement] = useState(null);
+ const [originalParent, setOriginalParent] = useState(null);
+ const currentVideoPlayerRef = useRef
@@ -132,16 +119,12 @@ function DisplayNamePage(props) {
);
}
-DisplayNamePage.propTypes = propTypes;
-DisplayNamePage.defaultProps = defaultProps;
DisplayNamePage.displayName = 'DisplayNamePage';
-export default compose(
- withLocalize,
- withCurrentUserPersonalDetails,
- withOnyx({
+export default withCurrentUserPersonalDetails(
+ withOnyx({
isLoadingApp: {
key: ONYXKEYS.IS_LOADING_APP,
},
- }),
-)(DisplayNamePage);
+ })(DisplayNamePage),
+);
diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.tsx
similarity index 53%
rename from src/pages/settings/Profile/ProfilePage.js
rename to src/pages/settings/Profile/ProfilePage.tsx
index 2fa133f41616..58a323bf0a10 100755
--- a/src/pages/settings/Profile/ProfilePage.js
+++ b/src/pages/settings/Profile/ProfilePage.tsx
@@ -1,10 +1,8 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
import React, {useEffect} from 'react';
import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
-import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
+import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Illustrations from '@components/Icon/Illustrations';
import MenuItemGroup from '@components/MenuItemGroup';
@@ -12,63 +10,36 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
-import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
-import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions';
+import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
+import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
+import useLocalize from '@hooks/useLocalize';
import usePrivatePersonalDetails from '@hooks/usePrivatePersonalDetails';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
-import compose from '@libs/compose';
-import {translatableTextPropTypes} from '@libs/Localize';
+import * as LocalePhoneNumber from '@libs/LocalePhoneNumber';
import Navigation from '@libs/Navigation/Navigation';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as UserUtils from '@libs/UserUtils';
import * as App from '@userActions/App';
import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type {LoginList, PersonalDetails, PrivatePersonalDetails} from '@src/types/onyx';
-const propTypes = {
- /* Onyx Props */
-
- /** Login list for the user that is signed in */
- loginList: PropTypes.objectOf(
- PropTypes.shape({
- /** Date login was validated, used to show brickroad info status */
- validatedDate: PropTypes.string,
-
- /** Field-specific server side errors keyed by microtime */
- errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)),
- }),
- ),
-
+type ProfilePageOnyxProps = {
+ loginList: OnyxEntry;
/** User's private personal details */
- privatePersonalDetails: PropTypes.shape({
- legalFirstName: PropTypes.string,
- legalLastName: PropTypes.string,
- dob: PropTypes.string,
-
- /** User's home address */
- address: PropTypes.shape({
- street: PropTypes.string,
- city: PropTypes.string,
- state: PropTypes.string,
- zip: PropTypes.string,
- country: PropTypes.string,
- }),
- }),
-
- ...withLocalizePropTypes,
- ...windowDimensionsPropTypes,
- ...withCurrentUserPersonalDetailsPropTypes,
+ privatePersonalDetails: OnyxEntry;
};
-const defaultProps = {
- loginList: {},
- ...withCurrentUserPersonalDetailsDefaultProps,
- privatePersonalDetails: {
+type ProfilePageProps = ProfilePageOnyxProps & WithCurrentUserPersonalDetailsProps;
+
+function ProfilePage({
+ loginList,
+ privatePersonalDetails = {
legalFirstName: '',
legalLastName: '',
dob: '',
@@ -81,79 +52,73 @@ const defaultProps = {
country: '',
},
},
-};
-
-function ProfilePage(props) {
+ currentUserPersonalDetails,
+}: ProfilePageProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
- const getPronouns = () => {
- let pronounsKey = lodashGet(props.currentUserPersonalDetails, 'pronouns', '');
- if (pronounsKey.startsWith(CONST.PRONOUNS.PREFIX)) {
- pronounsKey = pronounsKey.slice(CONST.PRONOUNS.PREFIX.length);
- }
+ const {translate} = useLocalize();
+ const {isSmallScreenWidth} = useWindowDimensions();
- if (!pronounsKey) {
- return props.translate('profilePage.selectYourPronouns');
- }
- return props.translate(`pronouns.${pronounsKey}`);
+ const getPronouns = (): string => {
+ const pronounsKey = currentUserPersonalDetails?.pronouns?.replace(CONST.PRONOUNS.PREFIX, '') ?? '';
+ return pronounsKey ? translate(`pronouns.${pronounsKey}` as TranslationPaths) : translate('profilePage.selectYourPronouns');
};
- const currentUserDetails = props.currentUserPersonalDetails || {};
- const contactMethodBrickRoadIndicator = UserUtils.getLoginListBrickRoadIndicator(props.loginList);
- const emojiCode = lodashGet(props, 'currentUserPersonalDetails.status.emojiCode', '');
- const {isSmallScreenWidth} = useWindowDimensions();
+
+ const contactMethodBrickRoadIndicator = UserUtils.getLoginListBrickRoadIndicator(loginList);
+ const emojiCode = currentUserPersonalDetails?.status?.emojiCode ?? '';
usePrivatePersonalDetails();
- const privateDetails = props.privatePersonalDetails || {};
- const legalName = `${privateDetails.legalFirstName || ''} ${privateDetails.legalLastName || ''}`.trim();
- const isLoadingPersonalDetails = lodashGet(props.privatePersonalDetails, 'isLoading', true);
+ const privateDetails = privatePersonalDetails ?? {};
+ const legalName = `${privateDetails.legalFirstName ?? ''} ${privateDetails.legalLastName ?? ''}`.trim();
+ const isLoadingPersonalDetails = privatePersonalDetails?.isLoading ?? true;
const publicOptions = [
{
- description: props.translate('displayNamePage.headerTitle'),
- title: lodashGet(currentUserDetails, 'displayName', ''),
+ description: translate('displayNamePage.headerTitle'),
+ title: currentUserPersonalDetails?.displayName ?? '',
pageRoute: ROUTES.SETTINGS_DISPLAY_NAME,
},
{
- description: props.translate('contacts.contactMethod'),
- title: props.formatPhoneNumber(lodashGet(currentUserDetails, 'login', '')),
+ description: translate('contacts.contactMethod'),
+ title: LocalePhoneNumber.formatPhoneNumber(currentUserPersonalDetails?.login ?? ''),
pageRoute: ROUTES.SETTINGS_CONTACT_METHODS.route,
brickRoadIndicator: contactMethodBrickRoadIndicator,
},
{
- description: props.translate('statusPage.status'),
- title: emojiCode ? `${emojiCode} ${lodashGet(props, 'currentUserPersonalDetails.status.text', '')}` : '',
+ description: translate('statusPage.status'),
+ title: emojiCode ? `${emojiCode} ${currentUserPersonalDetails?.status?.text ?? ''}` : '',
pageRoute: ROUTES.SETTINGS_STATUS,
},
{
- description: props.translate('pronounsPage.pronouns'),
+ description: translate('pronounsPage.pronouns'),
title: getPronouns(),
pageRoute: ROUTES.SETTINGS_PRONOUNS,
},
{
- description: props.translate('timezonePage.timezone'),
- title: `${lodashGet(currentUserDetails, 'timezone.selected', '')}`,
+ description: translate('timezonePage.timezone'),
+ title: currentUserPersonalDetails?.timezone?.selected ?? '',
pageRoute: ROUTES.SETTINGS_TIMEZONE,
},
];
useEffect(() => {
- App.openProfile(props.currentUserPersonalDetails);
- }, [props.currentUserPersonalDetails]);
+ App.openProfile(currentUserPersonalDetails as PersonalDetails);
+ }, [currentUserPersonalDetails]);
const privateOptions = [
{
- description: props.translate('privatePersonalDetails.legalName'),
+ description: translate('privatePersonalDetails.legalName'),
title: legalName,
pageRoute: ROUTES.SETTINGS_LEGAL_NAME,
},
{
- description: props.translate('common.dob'),
- title: privateDetails.dob || '',
+ description: translate('common.dob'),
+ title: privateDetails.dob ?? '',
pageRoute: ROUTES.SETTINGS_DATE_OF_BIRTH,
},
{
- description: props.translate('privatePersonalDetails.address'),
- title: PersonalDetailsUtils.getFormattedAddress(props.privatePersonalDetails),
+ description: translate('privatePersonalDetails.address'),
+ title: PersonalDetailsUtils.getFormattedAddress(privateDetails),
pageRoute: ROUTES.SETTINGS_ADDRESS,
},
];
@@ -165,24 +130,25 @@ function ProfilePage(props) {
shouldShowOfflineIndicatorInWideScreen
>
Navigation.goBack()}
- shouldShowBackButton={props.isSmallScreenWidth}
+ shouldShowBackButton={isSmallScreenWidth}
icon={Illustrations.Profile}
/>
- {_.map(publicOptions, (detail, index) => (
+ {publicOptions.map((detail, index) => (
{isLoadingPersonalDetails ? (
-
+
) : (
<>
- {_.map(privateOptions, (detail, index) => (
+ {privateOptions.map((detail, index) => (
({
loginList: {
key: ONYXKEYS.LOGIN_LIST,
},
privatePersonalDetails: {
key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS,
},
- user: {
- key: ONYXKEYS.USER,
- },
- }),
-)(ProfilePage);
+ })(ProfilePage),
+);
diff --git a/src/pages/settings/Profile/PronounsPage.js b/src/pages/settings/Profile/PronounsPage.tsx
similarity index 58%
rename from src/pages/settings/Profile/PronounsPage.js
rename to src/pages/settings/Profile/PronounsPage.tsx
index 1d4675a42b8a..5bd2737a98a4 100644
--- a/src/pages/settings/Profile/PronounsPage.js
+++ b/src/pages/settings/Profile/PronounsPage.tsx
@@ -1,49 +1,43 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
import React, {useEffect, useMemo, useState} from 'react';
+import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
+import type {ListItem} from '@components/SelectionList/types';
import Text from '@components/Text';
-import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails';
+import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
+import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import Navigation from '@libs/Navigation/Navigation';
import * as PersonalDetails from '@userActions/PersonalDetails';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-const propTypes = {
- ...withCurrentUserPersonalDetailsPropTypes,
-
- /** Indicates whether the app is loading initial data */
- isLoadingApp: PropTypes.bool,
+type PronounEntry = ListItem & {
+ value: string;
};
-const defaultProps = {
- ...withCurrentUserPersonalDetailsDefaultProps,
- isLoadingApp: true,
+type PronounsPageOnyxProps = {
+ isLoadingApp: OnyxEntry;
};
+type PronounsPageProps = PronounsPageOnyxProps & WithCurrentUserPersonalDetailsProps;
-function PronounsPage({currentUserPersonalDetails, isLoadingApp}) {
+function PronounsPage({currentUserPersonalDetails, isLoadingApp = true}: PronounsPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
- const currentPronouns = lodashGet(currentUserPersonalDetails, 'pronouns', '');
+ const currentPronouns = currentUserPersonalDetails?.pronouns ?? '';
const currentPronounsKey = currentPronouns.substring(CONST.PRONOUNS.PREFIX.length);
const [searchValue, setSearchValue] = useState('');
useEffect(() => {
- if (isLoadingApp && _.isUndefined(currentUserPersonalDetails.pronouns)) {
+ if (isLoadingApp && !currentUserPersonalDetails.pronouns) {
return;
}
- const currentPronounsText = _.chain(CONST.PRONOUNS_LIST)
- .find((_value) => _value === currentPronounsKey)
- .value();
+ const currentPronounsText = CONST.PRONOUNS_LIST.find((value) => value === currentPronounsKey);
setSearchValue(currentPronounsText ? translate(`pronouns.${currentPronounsText}`) : '');
@@ -51,34 +45,31 @@ function PronounsPage({currentUserPersonalDetails, isLoadingApp}) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoadingApp]);
- const filteredPronounsList = useMemo(() => {
- const pronouns = _.chain(CONST.PRONOUNS_LIST)
- .map((value) => {
- const fullPronounKey = `${CONST.PRONOUNS.PREFIX}${value}`;
- const isCurrentPronouns = fullPronounKey === currentPronouns;
+ const filteredPronounsList = useMemo((): PronounEntry[] => {
+ const pronouns = CONST.PRONOUNS_LIST.map((value) => {
+ const fullPronounKey = `${CONST.PRONOUNS.PREFIX}${value}`;
+ const isCurrentPronouns = fullPronounKey === currentPronouns;
- return {
- text: translate(`pronouns.${value}`),
- value: fullPronounKey,
- keyForList: value,
- isSelected: isCurrentPronouns,
- };
- })
- .sortBy((pronoun) => pronoun.text.toLowerCase())
- .value();
+ return {
+ text: translate(`pronouns.${value}`),
+ value: fullPronounKey,
+ keyForList: value,
+ isSelected: isCurrentPronouns,
+ };
+ }).sort((a, b) => a.text.toLowerCase().localeCompare(b.text.toLowerCase()));
const trimmedSearch = searchValue.trim();
if (trimmedSearch.length === 0) {
return [];
}
- return _.filter(pronouns, (pronoun) => pronoun.text.toLowerCase().indexOf(trimmedSearch.toLowerCase()) >= 0);
+ return pronouns.filter((pronoun) => pronoun.text.toLowerCase().indexOf(trimmedSearch.toLowerCase()) >= 0);
}, [searchValue, currentPronouns, translate]);
- const headerMessage = searchValue.trim() && filteredPronounsList.length === 0 ? translate('common.noResultsFound') : '';
+ const headerMessage = searchValue.trim() && filteredPronounsList?.length === 0 ? translate('common.noResultsFound') : '';
- const updatePronouns = (selectedPronouns) => {
- PersonalDetails.updatePronouns(selectedPronouns.keyForList === currentPronounsKey ? '' : lodashGet(selectedPronouns, 'value', ''));
+ const updatePronouns = (selectedPronouns: PronounEntry) => {
+ PersonalDetails.updatePronouns(selectedPronouns.keyForList === currentPronounsKey ? '' : selectedPronouns?.value ?? '');
};
return (
@@ -86,7 +77,7 @@ function PronounsPage({currentUserPersonalDetails, isLoadingApp}) {
includeSafeAreaPaddingBottom={false}
testID={PronounsPage.displayName}
>
- {isLoadingApp && _.isUndefined(currentUserPersonalDetails.pronouns) ? (
+ {isLoadingApp && !currentUserPersonalDetails.pronouns ? (
) : (
<>
@@ -112,15 +103,12 @@ function PronounsPage({currentUserPersonalDetails, isLoadingApp}) {
);
}
-PronounsPage.propTypes = propTypes;
-PronounsPage.defaultProps = defaultProps;
PronounsPage.displayName = 'PronounsPage';
-export default compose(
- withCurrentUserPersonalDetails,
- withOnyx({
+export default withCurrentUserPersonalDetails(
+ withOnyx({
isLoadingApp: {
key: ONYXKEYS.IS_LOADING_APP,
},
- }),
-)(PronounsPage);
+ })(PronounsPage),
+);
diff --git a/src/pages/settings/Profile/TimezoneInitialPage.js b/src/pages/settings/Profile/TimezoneInitialPage.tsx
similarity index 55%
rename from src/pages/settings/Profile/TimezoneInitialPage.js
rename to src/pages/settings/Profile/TimezoneInitialPage.tsx
index 1b2596b9d369..1df0063c57b1 100644
--- a/src/pages/settings/Profile/TimezoneInitialPage.js
+++ b/src/pages/settings/Profile/TimezoneInitialPage.tsx
@@ -1,4 +1,3 @@
-import lodashGet from 'lodash/get';
import React from 'react';
import {View} from 'react-native';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
@@ -6,62 +5,56 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import ScreenWrapper from '@components/ScreenWrapper';
import Switch from '@components/Switch';
import Text from '@components/Text';
-import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
+import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import Navigation from '@libs/Navigation/Navigation';
import * as PersonalDetails from '@userActions/PersonalDetails';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
+import type {SelectedTimezone, Timezone} from '@src/types/onyx/PersonalDetails';
-const propTypes = {
- ...withLocalizePropTypes,
- ...withCurrentUserPersonalDetailsPropTypes,
-};
+type TimezoneInitialPageProps = WithCurrentUserPersonalDetailsProps;
-const defaultProps = {
- ...withCurrentUserPersonalDetailsDefaultProps,
-};
-
-function TimezoneInitialPage(props) {
+function TimezoneInitialPage({currentUserPersonalDetails}: TimezoneInitialPageProps) {
const styles = useThemeStyles();
- const timezone = lodashGet(props.currentUserPersonalDetails, 'timezone', CONST.DEFAULT_TIME_ZONE);
+ const timezone: Timezone = currentUserPersonalDetails?.timezone ?? CONST.DEFAULT_TIME_ZONE;
+
+ const {translate} = useLocalize();
/**
* Updates setting for automatic timezone selection.
* Note: If we are updating automatically, we'll immediately calculate the user's timezone.
- *
- * @param {Boolean} isAutomatic
*/
- const updateAutomaticTimezone = (isAutomatic) => {
+ const updateAutomaticTimezone = (isAutomatic: boolean) => {
PersonalDetails.updateAutomaticTimezone({
automatic: isAutomatic,
- selected: isAutomatic ? Intl.DateTimeFormat().resolvedOptions().timeZone : timezone.selected,
+ selected: isAutomatic ? (Intl.DateTimeFormat().resolvedOptions().timeZone as SelectedTimezone) : timezone.selected,
});
};
return (
Navigation.goBack()}
/>
- {props.translate('timezonePage.isShownOnProfile')}
+ {translate('timezonePage.isShownOnProfile')}
- {props.translate('timezonePage.getLocationAutomatically')}
+ {translate('timezonePage.getLocationAutomatically')} Navigation.navigate(ROUTES.SETTINGS_TIMEZONE_SELECT)}
@@ -71,8 +64,6 @@ function TimezoneInitialPage(props) {
);
}
-TimezoneInitialPage.propTypes = propTypes;
-TimezoneInitialPage.defaultProps = defaultProps;
TimezoneInitialPage.displayName = 'TimezoneInitialPage';
-export default compose(withLocalize, withCurrentUserPersonalDetails)(TimezoneInitialPage);
+export default withCurrentUserPersonalDetails(TimezoneInitialPage);
diff --git a/src/pages/settings/Profile/TimezoneSelectPage.js b/src/pages/settings/Profile/TimezoneSelectPage.tsx
similarity index 53%
rename from src/pages/settings/Profile/TimezoneSelectPage.js
rename to src/pages/settings/Profile/TimezoneSelectPage.tsx
index b6c8a5967abc..3aff5f820cf8 100644
--- a/src/pages/settings/Profile/TimezoneSelectPage.js
+++ b/src/pages/settings/Profile/TimezoneSelectPage.tsx
@@ -1,11 +1,11 @@
-import lodashGet from 'lodash/get';
import React, {useState} from 'react';
-import _ from 'underscore';
+import type {ValueOf} from 'type-fest';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
-import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails';
+import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
+import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
import useInitialValue from '@hooks/useInitialValue';
import useLocalize from '@hooks/useLocalize';
import Navigation from '@libs/Navigation/Navigation';
@@ -13,67 +13,45 @@ import * as PersonalDetails from '@userActions/PersonalDetails';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import TIMEZONES from '@src/TIMEZONES';
+import type {SelectedTimezone} from '@src/types/onyx/PersonalDetails';
-const propTypes = {
- ...withCurrentUserPersonalDetailsPropTypes,
-};
-
-const defaultProps = {
- ...withCurrentUserPersonalDetailsDefaultProps,
-};
+type TimezoneSelectPageProps = Pick;
/**
* We add the current time to the key to fix a bug where the list options don't update unless the key is updated.
- * @param {String} text
- * @return {string} key for list item
*/
-const getKey = (text) => `${text}-${new Date().getTime()}`;
+const getKey = (text: string): string => `${text}-${new Date().getTime()}`;
-/**
- * @param {Object} currentUserPersonalDetails
- * @return {Object} user's timezone data
- */
-const getUserTimezone = (currentUserPersonalDetails) => lodashGet(currentUserPersonalDetails, 'timezone', CONST.DEFAULT_TIME_ZONE);
+const getUserTimezone = (currentUserPersonalDetails: ValueOf) =>
+ currentUserPersonalDetails?.timezone ?? CONST.DEFAULT_TIME_ZONE;
-function TimezoneSelectPage(props) {
+function TimezoneSelectPage({currentUserPersonalDetails}: TimezoneSelectPageProps) {
const {translate} = useLocalize();
- const timezone = getUserTimezone(props.currentUserPersonalDetails);
+ const timezone = getUserTimezone(currentUserPersonalDetails);
const allTimezones = useInitialValue(() =>
- _.chain(TIMEZONES)
- .filter((tz) => !tz.startsWith('Etc/GMT'))
- .map((text) => ({
- text,
- keyForList: getKey(text),
- isSelected: text === timezone.selected,
- }))
- .value(),
+ TIMEZONES.filter((tz: string) => !tz.startsWith('Etc/GMT')).map((text: string) => ({
+ text,
+ keyForList: getKey(text),
+ isSelected: text === timezone.selected,
+ })),
);
const [timezoneInputText, setTimezoneInputText] = useState('');
const [timezoneOptions, setTimezoneOptions] = useState(allTimezones);
- /**
- * @param {Object} timezone
- * @param {String} timezone.text
- */
- const saveSelectedTimezone = ({text}) => {
- PersonalDetails.updateSelectedTimezone(text);
+ const saveSelectedTimezone = ({text}: {text: string}) => {
+ PersonalDetails.updateSelectedTimezone(text as SelectedTimezone);
};
- /**
- * @param {String} searchText
- */
- const filterShownTimezones = (searchText) => {
+ const filterShownTimezones = (searchText: string) => {
setTimezoneInputText(searchText);
- const searchWords = searchText.toLowerCase().match(/[a-z0-9]+/g) || [];
+ const searchWords = searchText.toLowerCase().match(/[a-z0-9]+/g) ?? [];
setTimezoneOptions(
- _.filter(allTimezones, (tz) =>
- _.every(
- searchWords,
- (word) =>
- tz.text
- .toLowerCase()
- .replace(/[^a-z0-9]/g, ' ')
- .indexOf(word) > -1,
+ allTimezones.filter((tz) =>
+ searchWords.every((word) =>
+ tz.text
+ .toLowerCase()
+ .replace(/[^a-z0-9]/g, ' ')
+ .includes(word),
),
),
);
@@ -95,7 +73,7 @@ function TimezoneSelectPage(props) {
onChangeText={filterShownTimezones}
onSelectRow={saveSelectedTimezone}
sections={[{data: timezoneOptions, indexOffset: 0, isDisabled: timezone.automatic}]}
- initiallyFocusedOptionKey={_.get(_.filter(timezoneOptions, (tz) => tz.text === timezone.selected)[0], 'keyForList')}
+ initiallyFocusedOptionKey={timezoneOptions.find((tz) => tz.text === timezone.selected)?.keyForList}
showScrollIndicator
shouldShowTooltips={false}
ListItem={RadioListItem}
@@ -104,8 +82,6 @@ function TimezoneSelectPage(props) {
);
}
-TimezoneSelectPage.propTypes = propTypes;
-TimezoneSelectPage.defaultProps = defaultProps;
TimezoneSelectPage.displayName = 'TimezoneSelectPage';
export default withCurrentUserPersonalDetails(TimezoneSelectPage);
diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx b/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx
index b4c1bc249c81..dfa769077374 100644
--- a/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx
+++ b/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx
@@ -131,6 +131,7 @@ function CodesStep({account, backTo}: CodesStepProps) {
)}
{
if (!account?.codesAreCopied) {
diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/DisabledStep.tsx b/src/pages/settings/Security/TwoFactorAuth/Steps/DisabledStep.tsx
index db64a805d088..588c21defee3 100644
--- a/src/pages/settings/Security/TwoFactorAuth/Steps/DisabledStep.tsx
+++ b/src/pages/settings/Security/TwoFactorAuth/Steps/DisabledStep.tsx
@@ -25,6 +25,7 @@ function DisabledStep() {
TwoFactorAuthActions.quitAndNavigateBack()}
/>
diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.tsx b/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.tsx
index 58e7d98d69de..cfd233d61cf1 100644
--- a/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.tsx
+++ b/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.tsx
@@ -121,6 +121,7 @@ function VerifyStep({account}: VerifyStepProps) {
{
diff --git a/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx b/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx
index 4edf637618ea..d5b1230fd216 100644
--- a/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx
+++ b/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx
@@ -150,6 +150,7 @@ function ActivatePhysicalCardPage({
isDisabled={isOffline}
isLoading={physicalCard?.isLoading}
medium={isExtraSmallScreenHeight}
+ large={!isExtraSmallScreenHeight}
style={[styles.w100, styles.p5, styles.mtAuto]}
onPress={submitAndNavigateToNextPage}
pressOnEnter
diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage.tsx
index 097b2cf28ed0..732a21bc993c 100644
--- a/src/pages/settings/Wallet/ExpensifyCardPage.tsx
+++ b/src/pages/settings/Wallet/ExpensifyCardPage.tsx
@@ -230,6 +230,7 @@ function ExpensifyCardPage({
{physicalCard?.state === CONST.EXPENSIFY_CARD.STATE.NOT_ACTIVATED && (
Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_ACTIVATE.getRoute(domain))}
text={translate('activateCardPage.activatePhysicalCard')}
@@ -238,6 +239,7 @@ function ExpensifyCardPage({
{physicalCard?.state === CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED && (
{
diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx
index 8111e8d39afa..d6a3e7b2d56b 100755
--- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx
+++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx
@@ -143,7 +143,7 @@ function BaseValidateCodeForm({account, credentials, session, autoComplete, isUs
setTwoFactorAuthCode(text);
}
if (key === 'recoveryCode') {
- setRecoveryCode(text);
+ setRecoveryCode(text.trim());
}
setFormError((prevError) => ({...prevError, [key]: undefined}));
@@ -290,7 +290,7 @@ function BaseValidateCodeForm({account, credentials, session, autoComplete, isUs
accessibilityLabel={translate('recoveryCodeForm.recoveryCode')}
value={recoveryCode}
onChangeText={(text) => onTextInput(text, 'recoveryCode')}
- maxLength={CONST.RECOVERY_CODE_LENGTH}
+ maxLength={CONST.FORM_CHARACTER_LIMIT}
label={translate('recoveryCodeForm.recoveryCode')}
errorText={formError?.recoveryCode ?? ''}
hasError={hasError}
@@ -380,6 +380,7 @@ function BaseValidateCodeForm({account, credentials, session, autoComplete, isUs
diff --git a/src/pages/workspace/WorkspaceNewRoomPage.tsx b/src/pages/workspace/WorkspaceNewRoomPage.tsx
index b9236b0e7252..69f2d74b6be7 100644
--- a/src/pages/workspace/WorkspaceNewRoomPage.tsx
+++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx
@@ -223,6 +223,7 @@ function WorkspaceNewRoomPage({policies, reports, formState, session, activePoli
/>
Navigation.navigate(ROUTES.SETTINGS_WORKSPACES)}
style={[styles.mh5, styles.mb5]}
diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx
index 9fa5e0700091..2df6dd0489f9 100644
--- a/src/pages/workspace/WorkspaceProfilePage.tsx
+++ b/src/pages/workspace/WorkspaceProfilePage.tsx
@@ -194,6 +194,7 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi
text={translate('common.share')}
onPress={onPressShare}
medium
+ icon={Expensicons.QrCode}
/>
setIsDeleteModalOpen(true)}
medium
+ icon={Expensicons.Trashcan}
/>
)}
diff --git a/src/pages/workspace/card/WorkspaceCardVBANoECardView.tsx b/src/pages/workspace/card/WorkspaceCardVBANoECardView.tsx
index 02874a96ba76..db7a3f4b6368 100644
--- a/src/pages/workspace/card/WorkspaceCardVBANoECardView.tsx
+++ b/src/pages/workspace/card/WorkspaceCardVBANoECardView.tsx
@@ -44,7 +44,6 @@ function WorkspaceCardVBANoECardView({user}: WorkspaceCardVBANoECardViewProps) {
}}
icon={Expensicons.Mail}
style={[styles.mt4]}
- iconStyles={[styles.buttonCTAIcon]}
shouldShowRightIcon
large
success
diff --git a/src/pages/workspace/categories/CategoryForm.tsx b/src/pages/workspace/categories/CategoryForm.tsx
new file mode 100644
index 000000000000..aaf954f64468
--- /dev/null
+++ b/src/pages/workspace/categories/CategoryForm.tsx
@@ -0,0 +1,91 @@
+import React, {useCallback} from 'react';
+import {Keyboard} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
+import TextInput from '@components/TextInput';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import * as ValidationUtils from '@libs/ValidationUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import INPUT_IDS from '@src/types/form/WorkspaceCategoryForm';
+import type {PolicyCategories} from '@src/types/onyx';
+
+type CategoryFormProps = {
+ /** All policy categories */
+ policyCategories: OnyxEntry;
+
+ /** The name of the category */
+ categoryName?: string;
+
+ /** Function to call when the form is submitted */
+ onSubmit: (values: FormOnyxValues) => void;
+};
+
+function CategoryForm({onSubmit, policyCategories, categoryName}: CategoryFormProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const {inputCallbackRef} = useAutoFocusInput();
+
+ const validate = useCallback(
+ (values: FormOnyxValues) => {
+ const errors: FormInputErrors = {};
+ const newCategoryName = values.categoryName.trim();
+
+ if (!ValidationUtils.isRequiredFulfilled(newCategoryName)) {
+ errors.categoryName = 'workspace.categories.categoryRequiredError';
+ } else if (policyCategories?.[newCategoryName]) {
+ errors.categoryName = 'workspace.categories.existingCategoryError';
+ } else if (newCategoryName === CONST.INVALID_CATEGORY_NAME) {
+ errors.categoryName = 'workspace.categories.invalidCategoryName';
+ } else if ([...newCategoryName].length > CONST.CATEGORY_NAME_LIMIT) {
+ // Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 code units.
+ ErrorUtils.addErrorMessage(errors, 'categoryName', ['common.error.characterLimitExceedCounter', {length: [...newCategoryName].length, limit: CONST.CATEGORY_NAME_LIMIT}]);
+ }
+
+ return errors;
+ },
+ [policyCategories],
+ );
+
+ const submit = useCallback(
+ (values: FormOnyxValues) => {
+ onSubmit(values);
+ Keyboard.dismiss();
+ Navigation.dismissModal();
+ },
+ [onSubmit],
+ );
+
+ return (
+
+
+
+ );
+}
+
+CategoryForm.displayName = 'CategoryForm';
+
+export default CategoryForm;
diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx
index 16f128e5ea1f..8aad48fd94a7 100644
--- a/src/pages/workspace/categories/CategorySettingsPage.tsx
+++ b/src/pages/workspace/categories/CategorySettingsPage.tsx
@@ -13,12 +13,14 @@ import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import {setWorkspaceCategoryEnabled} from '@libs/actions/Policy';
import * as ErrorUtils from '@libs/ErrorUtils';
+import Navigation from '@libs/Navigation/Navigation';
import type {SettingsNavigatorParamList} from '@navigation/types';
import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import * as Policy from '@userActions/Policy';
import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';
@@ -43,6 +45,10 @@ function CategorySettingsPage({route, policyCategories}: CategorySettingsPagePro
setWorkspaceCategoryEnabled(route.params.policyID, {[policyCategory.name]: {name: policyCategory.name, enabled: value}});
};
+ const navigateToEditCategory = () => {
+ Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_EDIT.getRoute(route.params.policyID, policyCategory.name));
+ };
+
return (
@@ -73,6 +79,7 @@ function CategorySettingsPage({route, policyCategories}: CategorySettingsPagePro
diff --git a/src/pages/workspace/categories/CreateCategoryPage.tsx b/src/pages/workspace/categories/CreateCategoryPage.tsx
index 8de0e3a07980..80370d2197fa 100644
--- a/src/pages/workspace/categories/CreateCategoryPage.tsx
+++ b/src/pages/workspace/categories/CreateCategoryPage.tsx
@@ -1,29 +1,21 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useCallback} from 'react';
-import {Keyboard} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
-import FormProvider from '@components/Form/FormProvider';
-import InputWrapper from '@components/Form/InputWrapper';
-import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
+import type {FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
-import TextInput from '@components/TextInput';
-import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
-import * as ValidationUtils from '@libs/ValidationUtils';
import type {SettingsNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import * as Policy from '@userActions/Policy';
-import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
-import INPUT_IDS from '@src/types/form/WorkspaceCategoryCreateForm';
import type {PolicyCategories} from '@src/types/onyx';
+import CategoryForm from './CategoryForm';
type WorkspaceCreateCategoryPageOnyxProps = {
/** All policy categories */
@@ -35,34 +27,10 @@ type CreateCategoryPageProps = WorkspaceCreateCategoryPageOnyxProps & StackScree
function CreateCategoryPage({route, policyCategories}: CreateCategoryPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
- const {inputCallbackRef} = useAutoFocusInput();
-
- const validate = useCallback(
- (values: FormOnyxValues) => {
- const errors: FormInputErrors = {};
- const categoryName = values.categoryName.trim();
-
- if (!ValidationUtils.isRequiredFulfilled(categoryName)) {
- errors.categoryName = 'workspace.categories.categoryRequiredError';
- } else if (policyCategories?.[categoryName]) {
- errors.categoryName = 'workspace.categories.existingCategoryError';
- } else if (categoryName === CONST.INVALID_CATEGORY_NAME) {
- errors.categoryName = 'workspace.categories.invalidCategoryName';
- } else if ([...categoryName].length > CONST.CATEGORY_NAME_LIMIT) {
- // Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 code units.
- ErrorUtils.addErrorMessage(errors, 'categoryName', ['common.error.characterLimitExceedCounter', {length: [...categoryName].length, limit: CONST.CATEGORY_NAME_LIMIT}]);
- }
-
- return errors;
- },
- [policyCategories],
- );
const createCategory = useCallback(
- (values: FormOnyxValues) => {
+ (values: FormOnyxValues) => {
Policy.createPolicyCategory(route.params.policyID, values.categoryName.trim());
- Keyboard.dismiss();
- Navigation.goBack();
},
[route.params.policyID],
);
@@ -80,24 +48,10 @@ function CreateCategoryPage({route, policyCategories}: CreateCategoryPageProps)
title={translate('workspace.categories.addCategory')}
onBackButtonPress={Navigation.goBack}
/>
-
-
-
+ policyCategories={policyCategories}
+ />
diff --git a/src/pages/workspace/categories/EditCategoryPage.tsx b/src/pages/workspace/categories/EditCategoryPage.tsx
new file mode 100644
index 000000000000..35a8648f1a18
--- /dev/null
+++ b/src/pages/workspace/categories/EditCategoryPage.tsx
@@ -0,0 +1,68 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useCallback} from 'react';
+import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
+import type {FormOnyxValues} from '@components/Form/types';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import type {SettingsNavigatorParamList} from '@navigation/types';
+import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
+import * as Policy from '@userActions/Policy';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type SCREENS from '@src/SCREENS';
+import type {PolicyCategories} from '@src/types/onyx';
+import CategoryForm from './CategoryForm';
+
+type WorkspaceEditCategoryPageOnyxProps = {
+ /** All policy categories */
+ policyCategories: OnyxEntry;
+};
+
+type EditCategoryPageProps = WorkspaceEditCategoryPageOnyxProps & StackScreenProps;
+
+function EditCategoryPage({route, policyCategories}: EditCategoryPageProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const editCategory = useCallback(
+ (values: FormOnyxValues) => {
+ Policy.renamePolicyCategory(route.params.policyID, {oldName: route.params.categoryName, newName: values.categoryName});
+ },
+ [route.params.categoryName, route.params.policyID],
+ );
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+EditCategoryPage.displayName = 'EditCategoryPage';
+
+export default withOnyx({
+ policyCategories: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route?.params?.policyID}`,
+ },
+})(EditCategoryPage);
diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
index d22b822359f9..30a3ac257e4f 100644
--- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
+++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
@@ -236,14 +236,13 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat
onPress={navigateToCreateCategoryPage}
icon={Expensicons.Plus}
text={translate('workspace.categories.addCategory')}
- style={[styles.pr2, isSmallScreenWidth && styles.w50]}
+ style={[styles.mr3, isSmallScreenWidth && styles.w50]}
/>
)}
diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx
index ca508791c028..ea1d9ac6afd1 100644
--- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx
+++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx
@@ -200,7 +200,6 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps)
onPress={addRate}
style={[styles.mr3, isSmallScreenWidth && styles.flexGrow1]}
icon={Expensicons.Plus}
- iconStyles={[styles.mr2]}
success
/>
@@ -210,7 +209,6 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps)
onPress={openSettings}
style={[isSmallScreenWidth && styles.flexGrow1]}
icon={Expensicons.Gear}
- iconStyles={[styles.mr2]}
/>
>
) : (
diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx
index d44ff8baa08b..bb7f4d6a03aa 100644
--- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx
+++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx
@@ -14,7 +14,6 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
-import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import * as UserUtils from '@libs/UserUtils';
import Navigation from '@navigation/Navigation';
@@ -41,7 +40,6 @@ type WorkspaceMemberDetailsPageProps = WithPolicyAndFullscreenLoadingProps & Wor
function WorkspaceMemberDetailsPage({personalDetails, policyMembers, policy, route}: WorkspaceMemberDetailsPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
- const StyleUtils = useStyleUtils();
const [isRemoveMemberConfirmModalVisible, setIsRemoveMemberConfirmModalVisible] = React.useState(false);
@@ -106,7 +104,6 @@ function WorkspaceMemberDetailsPage({personalDetails, policyMembers, policy, rou
onPress={askForConfirmationToRemove}
medium
icon={Expensicons.RemoveMembers}
- iconStyles={StyleUtils.getTransformScaleStyle(0.8)}
style={styles.mv5}
/>
{policyTags && (
diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx
index 9934de95878b..6da120f95766 100644
--- a/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx
+++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx
@@ -149,14 +149,14 @@ function WorkspaceWorkflowsPayerPage({route, policy, policyMembers, personalDeta
sectionsArray.push({
data: formattedAuthorizedPayer,
shouldShow: true,
- indexOffset: formattedPolicyAdmins.length,
+ indexOffset: 0,
});
sectionsArray.push({
title: translate('workflowsPayerPage.admins'),
data: formattedPolicyAdmins,
shouldShow: true,
- indexOffset: 0,
+ indexOffset: formattedAuthorizedPayer.length,
});
return sectionsArray;
}, [formattedPolicyAdmins, formattedAuthorizedPayer, translate, searchTerm]);
@@ -170,13 +170,12 @@ function WorkspaceWorkflowsPayerPage({route, policy, policyMembers, personalDeta
const setPolicyAuthorizedPayer = (member: MemberOption) => {
const authorizedPayerEmail = personalDetails?.[member.accountID]?.login ?? '';
- if (policy?.reimburserEmail === authorizedPayerEmail) {
- return;
- }
- if (policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES) {
+ if (policy?.reimburserEmail === authorizedPayerEmail || policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES) {
+ Navigation.goBack();
return;
}
+
const authorizedPayerAccountID = member.accountID;
Policy.setWorkspacePayer(policy?.id ?? '', authorizedPayerEmail, authorizedPayerAccountID);
Navigation.goBack();
diff --git a/src/styles/index.ts b/src/styles/index.ts
index c7860f0e20a6..df89cd823fa4 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -534,7 +534,7 @@ const styles = (theme: ThemeColors) =>
button: {
backgroundColor: theme.buttonDefaultBG,
borderRadius: variables.buttonBorderRadius,
- minHeight: variables.componentSizeLarge,
+ minHeight: variables.componentSizeNormal,
justifyContent: 'center',
alignItems: 'center',
...spacing.ph3,
@@ -573,23 +573,24 @@ const styles = (theme: ThemeColors) =>
buttonSmall: {
borderRadius: variables.buttonBorderRadius,
minHeight: variables.componentSizeSmall,
- paddingHorizontal: 14,
+ minWidth: variables.componentSizeSmall,
+ paddingHorizontal: 12,
backgroundColor: theme.buttonDefaultBG,
},
buttonMedium: {
borderRadius: variables.buttonBorderRadius,
minHeight: variables.componentSizeNormal,
- paddingRight: 16,
- paddingLeft: 16,
+ minWidth: variables.componentSizeNormal,
+ paddingHorizontal: 16,
backgroundColor: theme.buttonDefaultBG,
},
buttonLarge: {
borderRadius: variables.buttonBorderRadius,
minHeight: variables.componentSizeLarge,
- paddingRight: 10,
- paddingLeft: 10,
+ minWidth: variables.componentSizeLarge,
+ paddingHorizontal: 20,
backgroundColor: theme.buttonDefaultBG,
},
@@ -1443,8 +1444,8 @@ const styles = (theme: ThemeColors) =>
sidebarAvatar: {
borderRadius: 28,
- height: variables.componentSizeSmall,
- width: variables.componentSizeSmall,
+ height: 28,
+ width: 28,
},
selectedAvatarBorder: {
@@ -2093,6 +2094,10 @@ const styles = (theme: ThemeColors) =>
alignSelf: 'flex-end',
},
+ customMarginButtonWithMenuItem: {
+ marginRight: variables.bankButtonMargin,
+ },
+
composerSizeButton: {
alignSelf: 'center',
height: 32,
diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts
index 31a19904b81c..9a25313837fe 100644
--- a/src/styles/utils/index.ts
+++ b/src/styles/utils/index.ts
@@ -3,6 +3,7 @@ import type {AnimatableNumericValue, Animated, ColorValue, DimensionValue, Image
import type {OnyxEntry} from 'react-native-onyx';
import type {EdgeInsets} from 'react-native-safe-area-context';
import type {ValueOf} from 'type-fest';
+import type ImageSVGProps from '@components/ImageSVG/types';
import * as Browser from '@libs/Browser';
import * as UserUtils from '@libs/UserUtils';
// eslint-disable-next-line no-restricted-imports
@@ -460,6 +461,53 @@ function getWidthAndHeightStyle(width: number, height?: number): Pick {
+ switch (true) {
+ case small:
+ return {width: hasText ? variables.iconSizeExtraSmall : variables.iconSizeSmall, height: hasText ? variables.iconSizeExtraSmall : variables?.iconSizeSmall};
+ case medium:
+ return {width: hasText ? variables.iconSizeSmall : variables.iconSizeNormal, height: hasText ? variables.iconSizeSmall : variables.iconSizeNormal};
+ case large:
+ return {width: hasText ? variables.iconSizeNormal : variables.iconSizeLarge, height: hasText ? variables.iconSizeNormal : variables.iconSizeLarge};
+ default: {
+ return {width, height};
+ }
+ }
+}
+
+function getButtonStyleWithIcon(
+ styles: ThemeStyles,
+ small: boolean,
+ medium: boolean,
+ large: boolean,
+ hasIcon?: boolean,
+ hasText?: boolean,
+ shouldShowRightIcon?: boolean,
+): ViewStyle | undefined {
+ const useDefaultButtonStyles = Boolean(hasIcon && shouldShowRightIcon) || Boolean(!hasIcon && !shouldShowRightIcon);
+ switch (true) {
+ case small: {
+ const verticalStyle = hasIcon ? styles.pl2 : styles.pr2;
+ return useDefaultButtonStyles ? styles.buttonSmall : {...styles.buttonSmall, ...(hasText ? verticalStyle : styles.ph0)};
+ }
+ case medium: {
+ const verticalStyle = hasIcon ? styles.pl3 : styles.pr3;
+ return useDefaultButtonStyles ? styles.buttonMedium : {...styles.buttonMedium, ...(hasText ? verticalStyle : styles.ph0)};
+ }
+ case large: {
+ const verticalStyle = hasIcon ? styles.pl4 : styles.pr4;
+ return useDefaultButtonStyles ? styles.buttonLarge : {...styles.buttonLarge, ...(hasText ? verticalStyle : styles.ph0)};
+ }
+ default: {
+ if (hasIcon && !hasText) {
+ return {...styles.buttonMedium, ...styles.ph0};
+ }
+
+ return undefined;
+ }
+ }
+}
+
/**
* Combine margin/padding with safe area inset
*
@@ -1096,6 +1144,8 @@ const staticStyleUtils = {
getOpacityStyle,
getMultiGestureCanvasContainerStyle,
getSignInBgStyles,
+ getIconWidthAndHeightStyle,
+ getButtonStyleWithIcon,
};
const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({
diff --git a/src/styles/utils/sizing.ts b/src/styles/utils/sizing.ts
index b461fc0f5d09..1e9a6662d102 100644
--- a/src/styles/utils/sizing.ts
+++ b/src/styles/utils/sizing.ts
@@ -14,6 +14,10 @@ export default {
height: 272,
},
+ h13: {
+ height: 52,
+ },
+
w15: {
width: '15%',
},
diff --git a/src/styles/utils/spacing.ts b/src/styles/utils/spacing.ts
index 56000a851e7b..740bf053c2d0 100644
--- a/src/styles/utils/spacing.ts
+++ b/src/styles/utils/spacing.ts
@@ -485,6 +485,10 @@ export default {
paddingRight: 32,
},
+ pr10: {
+ paddingRight: 40,
+ },
+
pr15: {
paddingRight: 60,
},
diff --git a/src/styles/variables.ts b/src/styles/variables.ts
index 61c16a8c2fd7..0ebc4e60991f 100644
--- a/src/styles/variables.ts
+++ b/src/styles/variables.ts
@@ -222,4 +222,5 @@ export default {
mushroomTopHatWidth: 138,
mushroomTopHatHeight: 128,
+ bankButtonMargin: 23,
} as const;
diff --git a/src/types/form/SettingsStatusSetForm.ts b/src/types/form/SettingsStatusSetForm.ts
index 9aeec26c4887..53e42ba01cfa 100644
--- a/src/types/form/SettingsStatusSetForm.ts
+++ b/src/types/form/SettingsStatusSetForm.ts
@@ -1,6 +1,23 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
-type SettingsStatusSetForm = Form;
+const INPUT_IDS = {
+ EMOJI_CODE: 'emojiCode',
+ STATUS_TEXT: 'statusText',
+ clearAfter: 'clearAfter',
+} as const;
+
+type InputID = ValueOf;
+
+type SettingsStatusSetForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.EMOJI_CODE]: string;
+ [INPUT_IDS.STATUS_TEXT]: string;
+ [INPUT_IDS.clearAfter]: string;
+ }
+>;
// eslint-disable-next-line import/prefer-default-export
export type {SettingsStatusSetForm};
+export default INPUT_IDS;
diff --git a/src/types/form/WorkspaceCategoryCreateForm.ts b/src/types/form/WorkspaceCategoryForm.ts
similarity index 77%
rename from src/types/form/WorkspaceCategoryCreateForm.ts
rename to src/types/form/WorkspaceCategoryForm.ts
index 051bf705fbf8..4f5f9282373c 100644
--- a/src/types/form/WorkspaceCategoryCreateForm.ts
+++ b/src/types/form/WorkspaceCategoryForm.ts
@@ -7,12 +7,12 @@ const INPUT_IDS = {
type InputID = ValueOf;
-type WorkspaceCategoryCreateForm = Form<
+type WorkspaceCategoryForm = Form<
InputID,
{
[INPUT_IDS.CATEGORY_NAME]: string;
}
>;
-export type {WorkspaceCategoryCreateForm};
+export type {WorkspaceCategoryForm};
export default INPUT_IDS;
diff --git a/src/types/form/index.ts b/src/types/form/index.ts
index 5a574de3db54..c4c0460b4f44 100644
--- a/src/types/form/index.ts
+++ b/src/types/form/index.ts
@@ -34,7 +34,7 @@ export type {SettingsStatusSetForm} from './SettingsStatusSetForm';
export type {WaypointForm} from './WaypointForm';
export type {WorkspaceInviteMessageForm} from './WorkspaceInviteMessageForm';
export type {WorkspaceRateAndUnitForm} from './WorkspaceRateAndUnitForm';
-export type {WorkspaceCategoryCreateForm} from './WorkspaceCategoryCreateForm';
+export type {WorkspaceCategoryForm} from './WorkspaceCategoryForm';
export type {WorkspaceSettingsForm} from './WorkspaceSettingsForm';
export type {ReportPhysicalCardForm} from './ReportPhysicalCardForm';
export type {WorkspaceDescriptionForm} from './WorkspaceDescriptionForm';
diff --git a/src/types/onyx/CustomStatusDraft.ts b/src/types/onyx/CustomStatusDraft.ts
index b2801a1d89e0..73c8fa4baa1a 100644
--- a/src/types/onyx/CustomStatusDraft.ts
+++ b/src/types/onyx/CustomStatusDraft.ts
@@ -1,9 +1,9 @@
type CustomStatusDraft = {
/** The emoji code of the draft status */
- emojiCode: string;
+ emojiCode?: string;
/** The text of the draft status */
- text: string;
+ text?: string;
/** ISO 8601 format string, which represents the time when the status should be cleared */
clearAfter: string;
diff --git a/src/types/onyx/PolicyCategory.ts b/src/types/onyx/PolicyCategory.ts
index 5cef49afc9d4..0fd498632dbc 100644
--- a/src/types/onyx/PolicyCategory.ts
+++ b/src/types/onyx/PolicyCategory.ts
@@ -4,6 +4,9 @@ type PolicyCategory = OnyxCommon.OnyxValueWithOfflineFeedback<{
/** Name of a category */
name: string;
+ /** Unencoded name of a category */
+ unencodedName: string;
+
/** Flag that determines if a category is active and able to be selected */
enabled: boolean;
diff --git a/src/types/onyx/PreferredTheme.ts b/src/types/onyx/PreferredTheme.ts
new file mode 100644
index 000000000000..408748ad06ea
--- /dev/null
+++ b/src/types/onyx/PreferredTheme.ts
@@ -0,0 +1,7 @@
+import type {OnyxEntry} from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
+import type CONST from '@src/CONST';
+
+type PreferredTheme = OnyxEntry>;
+
+export default PreferredTheme;
diff --git a/src/types/onyx/PriorityMode.ts b/src/types/onyx/PriorityMode.ts
new file mode 100644
index 000000000000..224c86867f35
--- /dev/null
+++ b/src/types/onyx/PriorityMode.ts
@@ -0,0 +1,7 @@
+import type {OnyxEntry} from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
+import type CONST from '@src/CONST';
+
+type PriorityMode = OnyxEntry>;
+
+export default PriorityMode;
diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts
index d12b0a22bba2..e3118538258e 100644
--- a/src/types/onyx/index.ts
+++ b/src/types/onyx/index.ts
@@ -40,6 +40,8 @@ import type {PolicyMembers} from './PolicyMember';
import type PolicyMember from './PolicyMember';
import type {PolicyReportField, PolicyReportFields} from './PolicyReportField';
import type {PolicyTag, PolicyTagList, PolicyTags} from './PolicyTag';
+import type PreferredTheme from './PreferredTheme';
+import type PriorityMode from './PriorityMode';
import type PrivatePersonalDetails from './PrivatePersonalDetails';
import type RecentlyUsedCategories from './RecentlyUsedCategories';
import type RecentlyUsedReportFields from './RecentlyUsedReportFields';
@@ -114,6 +116,8 @@ export type {
PolicyTag,
PolicyTags,
PolicyTagList,
+ PreferredTheme,
+ PriorityMode,
PrivatePersonalDetails,
RecentWaypoint,
RecentlyUsedCategories,
diff --git a/tests/unit/postTestBuildComment.js b/tests/unit/postTestBuildComment.ts
similarity index 83%
rename from tests/unit/postTestBuildComment.js
rename to tests/unit/postTestBuildComment.ts
index ff77ca190c08..5d4148134e19 100644
--- a/tests/unit/postTestBuildComment.js
+++ b/tests/unit/postTestBuildComment.ts
@@ -1,10 +1,11 @@
import * as core from '@actions/core';
import {when} from 'jest-when';
+import asMutable from '@src/types/utils/asMutable';
import ghAction from '../../.github/actions/javascript/postTestBuildComment/postTestBuildComment';
import GithubUtils from '../../.github/libs/GithubUtils';
const mockGetInput = jest.fn();
-const mockCreateComment = jest.fn();
+const createCommentMock = jest.spyOn(GithubUtils, 'createComment');
const mockListComments = jest.fn();
const mockGraphql = jest.fn();
jest.spyOn(GithubUtils, 'octokit', 'get').mockReturnValue({
@@ -12,7 +13,11 @@ jest.spyOn(GithubUtils, 'octokit', 'get').mockReturnValue({
listComments: mockListComments,
},
});
-jest.spyOn(GithubUtils, 'paginate', 'get').mockReturnValue((endpoint, params) => endpoint(params).then(({data}) => data));
+
+jest.spyOn(GithubUtils, 'paginate', 'get').mockReturnValue((endpoint: (params: Record) => Promise<{data: TData}>, params: Record) =>
+ endpoint(params).then((response) => response.data),
+);
+
jest.spyOn(GithubUtils, 'graphql', 'get').mockReturnValue(mockGraphql);
jest.mock('@actions/github', () => ({
@@ -52,12 +57,11 @@ const message = `:test_tube::test_tube: Use the links below to test this adhoc b
describe('Post test build comments action tests', () => {
beforeAll(() => {
// Mock core module
- core.getInput = mockGetInput;
- GithubUtils.createComment = mockCreateComment;
+ asMutable(core).getInput = mockGetInput;
});
test('Test GH action', async () => {
- when(core.getInput).calledWith('PR_NUMBER', {required: true}).mockReturnValue(12);
+ when(core.getInput).calledWith('PR_NUMBER', {required: true}).mockReturnValue('12');
when(core.getInput).calledWith('ANDROID', {required: true}).mockReturnValue('success');
when(core.getInput).calledWith('IOS', {required: true}).mockReturnValue('success');
when(core.getInput).calledWith('WEB', {required: true}).mockReturnValue('success');
@@ -66,11 +70,12 @@ describe('Post test build comments action tests', () => {
when(core.getInput).calledWith('IOS_LINK').mockReturnValue('https://expensify.app/IOS_LINK');
when(core.getInput).calledWith('WEB_LINK').mockReturnValue('https://expensify.app/WEB_LINK');
when(core.getInput).calledWith('DESKTOP_LINK').mockReturnValue('https://expensify.app/DESKTOP_LINK');
- GithubUtils.createComment.mockResolvedValue(true);
+ createCommentMock.mockResolvedValue(true);
mockListComments.mockResolvedValue({
data: [
{
body: ':test_tube::test_tube: Use the links below to test this adhoc build on Android, iOS, Desktop, and Web. Happy testing!',
+ // eslint-disable-next-line @typescript-eslint/naming-convention
node_id: 'IC_abcd',
},
],
@@ -86,7 +91,7 @@ describe('Post test build comments action tests', () => {
}
}
`);
- expect(GithubUtils.createComment).toBeCalledTimes(1);
- expect(GithubUtils.createComment).toBeCalledWith('App', 12, message);
+ expect(createCommentMock).toBeCalledTimes(1);
+ expect(createCommentMock).toBeCalledWith('App', '12', message);
});
});
diff --git a/tests/utils/LHNTestUtils.tsx b/tests/utils/LHNTestUtils.tsx
index 85c2d67f80bc..3aa428cc3eef 100644
--- a/tests/utils/LHNTestUtils.tsx
+++ b/tests/utils/LHNTestUtils.tsx
@@ -282,7 +282,6 @@ function MockedSidebarLinks({currentReportID = ''}: MockedSidebarLinksProps) {
return (
{}}
insets={{
top: 0,
@@ -290,7 +289,7 @@ function MockedSidebarLinks({currentReportID = ''}: MockedSidebarLinksProps) {
right: 0,
bottom: 0,
}}
- isSmallScreenWidth={false}
+ // @ts-expect-error - normally this comes from withCurrentReportID hoc, but here we are just mocking this
currentReportID={currentReportID}
/>