diff --git a/android/app/build.gradle b/android/app/build.gradle
index b792f7830ea4..e6b32dcd4d4b 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 1001045200
+ versionName "1.4.52-0"
}
flavorDimensions "default"
diff --git a/assets/images/document-slash.svg b/assets/images/document-slash.svg
index 25a4c96038b4..ebb183142e40 100644
--- a/assets/images/document-slash.svg
+++ b/assets/images/document-slash.svg
@@ -1,6 +1 @@
-
+
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/assets/images/tax.svg b/assets/images/tax.svg
new file mode 100644
index 000000000000..aa3c68e72ea8
--- /dev/null
+++ b/assets/images/tax.svg
@@ -0,0 +1,8 @@
+
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/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/NewApp_AdHoc.mobileprovision.gpg b/ios/NewApp_AdHoc.mobileprovision.gpg
index c4412cf650ee..2c5350cec2aa 100644
Binary files a/ios/NewApp_AdHoc.mobileprovision.gpg and b/ios/NewApp_AdHoc.mobileprovision.gpg differ
diff --git a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg
index 999442b550da..bae3cd9f3e21 100644
Binary files a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg and b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg differ
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index ab5d359a5460..3991493df583 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.0ITSAppUsesNonExemptEncryptionLSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index ca9200c78376..c04d2b7d3802 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.0
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index f20b520b1480..7d5db6a4159b 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.0NSExtensionNSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index 3083c15c0196..5529a99e861d 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-0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.51-3",
+ "version": "1.4.52-0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index dc5b261c3b40..1f6e6ce85450 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.51-3",
+ "version": "1.4.52-0",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
diff --git a/src/CONST.ts b/src/CONST.ts
index d4fbd0ff6ef3..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',
@@ -1414,6 +1412,11 @@ const CONST = {
MAKE_MEMBER: 'makeMember',
MAKE_ADMIN: 'makeAdmin',
},
+ CATEGORIES_BULK_ACTION_TYPES: {
+ DELETE: 'delete',
+ DISABLE: 'disable',
+ ENABLE: 'enable',
+ },
DISTANCE_RATES_BULK_ACTION_TYPES: {
DELETE: 'delete',
DISABLE: 'disable',
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 8c48cbad561f..820937b1eb89 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 defb945ba8c2..5739aad8baa6 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -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,
@@ -585,6 +589,10 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/tags/edit',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags/edit` as const,
},
+ WORKSPACE_TAXES: {
+ route: 'settings/workspaces/:policyID/taxes',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/taxes` as const,
+ },
WORKSPACE_MEMBER_DETAILS: {
route: 'settings/workspaces/:policyID/members/:accountID',
getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/members/${accountID}`, backTo),
@@ -597,7 +605,6 @@ const ROUTES = {
route: 'workspace/:policyID/distance-rates',
getRoute: (policyID: string) => `workspace/${policyID}/distance-rates` as const,
},
-
// Referral program promotion
REFERRAL_DETAILS_MODAL: {
route: 'referral/:contentType',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 4db5fd9115a5..7ccb24aa19e5 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -219,6 +219,7 @@ const SCREENS = {
TAGS: 'Workspace_Tags',
TAGS_SETTINGS: 'Tags_Settings',
TAGS_EDIT: 'Tags_Edit',
+ TAXES: 'Workspace_Taxes',
TAG_CREATE: 'Tag_Create',
CURRENCY: 'Workspace_Profile_Currency',
WORKFLOWS: 'Workspace_Workflows',
@@ -230,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 b676722f3c6c..e1f686cea6a7 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,26 +196,26 @@ function AddPlaidBankAccount({
}
const onError = useCallback((error) => {
- Log.hmmm('[PlaidLink] Error: ', error.message);
+ Log.hmmm('[PlaidLink] Error: ', error?.message);
}, []);
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={onError}
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);
}
}
@@ -259,7 +236,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({
@@ -421,6 +420,7 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose
style={[styles.m5]}
onPress={cropAndSaveImage}
pressOnEnter
+ large
text={translate('common.save')}
/>
diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx
index 5755c69641c8..8bcda759d26c 100644
--- a/src/components/AvatarWithImagePicker.tsx
+++ b/src/components/AvatarWithImagePicker.tsx
@@ -54,6 +54,9 @@ type AvatarWithImagePickerProps = {
/** Additional style props for disabled picker */
disabledStyle?: StyleProp;
+ /** Additional style props for the edit icon */
+ editIconStyle?: StyleProp;
+
/** Executed once an image has been selected */
onImageSelected?: (file: File | CustomRNImageManipulatorResult) => void;
@@ -120,6 +123,7 @@ function AvatarWithImagePicker({
DefaultAvatar = () => null,
style,
disabledStyle,
+ editIconStyle,
pendingAction,
errors,
errorRowStyles,
@@ -323,7 +327,7 @@ function AvatarWithImagePicker({
)}
{!disabled && (
-
+ & {
/** Should the press event bubble across multiple instances when Enter key triggers it. */
allowBubble?: boolean;
@@ -116,6 +106,15 @@ type ButtonProps = (ButtonWithText | ChildrenProps) & {
/** Accessibility label for the component */
accessibilityLabel?: string;
+
+ /** The icon asset to display to the left of the text */
+ icon?: IconAsset | null;
+
+ /** The text for the button label */
+ text?: string;
+
+ /** Boolean whether to display the right icon */
+ shouldShowRightIcon?: boolean;
};
type KeyboardShortcutComponentProps = Pick;
@@ -162,8 +161,10 @@ function Button(
iconRight = Expensicons.ArrowRight,
iconFill,
+ icon = null,
iconStyles = [],
iconRightStyles = [],
+ text = '',
small = false,
large = false,
@@ -193,6 +194,7 @@ function Button(
shouldRemoveLeftBorderRadius = false,
shouldEnableHapticFeedback = false,
isLongPressDisabled = false,
+ shouldShowRightIcon = false,
id = '',
accessibilityLabel = '',
@@ -202,14 +204,13 @@ function Button(
) {
const theme = useTheme();
const styles = useThemeStyles();
+ const StyleUtils = useStyleUtils();
const renderContent = () => {
if ('children' in rest) {
return rest.children;
}
- const {text = '', icon = null, shouldShowRightIcon = false} = rest;
-
const textComponent = (
{icon && (
-
+
)}
{textComponent}
{shouldShowRightIcon && (
-
+
)}
@@ -310,9 +315,7 @@ function Button(
]}
style={[
styles.button,
- small ? styles.buttonSmall : undefined,
- medium ? styles.buttonMedium : undefined,
- large ? styles.buttonLarge : undefined,
+ StyleUtils.getButtonStyleWithIcon(styles, small, medium, large, Boolean(icon), Boolean(text?.length > 0), shouldShowRightIcon),
success ? styles.buttonSuccess : undefined,
danger ? styles.buttonDanger : undefined,
isDisabled ? styles.buttonOpacityDisabled : undefined,
@@ -320,7 +323,7 @@ function Button(
shouldRemoveRightBorderRadius ? styles.noRightBorderRadius : undefined,
shouldRemoveLeftBorderRadius ? styles.noLeftBorderRadius : undefined,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- 'text' in rest && rest?.shouldShowRightIcon ? styles.alignItemsStretch : undefined,
+ text && shouldShowRightIcon ? styles.alignItemsStretch : undefined,
innerStyles,
]}
hoverStyle={[
diff --git a/src/components/ConfirmContent.tsx b/src/components/ConfirmContent.tsx
index c0c146bbae08..26331f92401c 100644
--- a/src/components/ConfirmContent.tsx
+++ b/src/components/ConfirmContent.tsx
@@ -145,6 +145,7 @@ function ConfirmContent({
style={[styles.mt4]}
onPress={onConfirm}
pressOnEnter
+ large
text={confirmText || translate('common.yes')}
isDisabled={isOffline && shouldDisableConfirmButtonWhenOffline}
/>
@@ -152,6 +153,7 @@ function ConfirmContent({
)}
diff --git a/src/components/ConfirmationPage.tsx b/src/components/ConfirmationPage.tsx
index fe4036a4435f..4c2fb46dccf8 100644
--- a/src/components/ConfirmationPage.tsx
+++ b/src/components/ConfirmationPage.tsx
@@ -47,6 +47,7 @@ function ConfirmationPage({animation = LottieAnimations.Fireworks, heading, desc
diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx
index ee3b3607401e..e9c5b80b37ac 100644
--- a/src/components/Form/FormProvider.tsx
+++ b/src/components/Form/FormProvider.tsx
@@ -1,5 +1,5 @@
import lodashIsEqual from 'lodash/isEqual';
-import type {ForwardedRef, MutableRefObject, ReactNode} from 'react';
+import type {ForwardedRef, MutableRefObject, ReactNode, RefAttributes} from 'react';
import React, {createRef, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import type {NativeSyntheticEvent, StyleProp, TextInputSubmitEditingEventData, ViewStyle} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
@@ -18,7 +18,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {RegisterInput} from './FormContext';
import FormContext from './FormContext';
import FormWrapper from './FormWrapper';
-import type {FormInputErrors, FormOnyxValues, FormProps, InputComponentBaseProps, InputRefs, ValueTypeKey} from './types';
+import type {FormInputErrors, FormOnyxValues, FormProps, FormRef, InputComponentBaseProps, InputRefs, ValueTypeKey} from './types';
// In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web.
// 200ms delay was chosen as a result of empirical testing.
@@ -73,10 +73,6 @@ type FormProviderProps = FormProvider
submitFlexEnabled?: boolean;
};
-type FormRef = {
- resetForm: (optionalValue: FormOnyxValues) => void;
-};
-
function FormProvider(
{
formID,
@@ -393,6 +389,6 @@ export default withOnyx({
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any
key: (props) => `${props.formID}Draft` as any,
},
-})(forwardRef(FormProvider)) as (props: Omit, keyof FormProviderOnyxProps>) => ReactNode;
+})(forwardRef(FormProvider)) as (props: Omit & RefAttributes, keyof FormProviderOnyxProps>) => ReactNode;
export type {FormProviderProps};
diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts
index 33d127308449..8df5c0287d3b 100644
--- a/src/components/Form/types.ts
+++ b/src/components/Form/types.ts
@@ -1,6 +1,7 @@
import type {ComponentType, FocusEvent, Key, MutableRefObject, ReactNode, Ref} from 'react';
import type {GestureResponderEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, TextInputSubmitEditingEventData, ViewStyle} from 'react-native';
import type {ValueOf} from 'type-fest';
+import type AddPlaidBankAccount from '@components/AddPlaidBankAccount';
import type AddressSearch from '@components/AddressSearch';
import type AmountForm from '@components/AmountForm';
import type AmountTextInput from '@components/AmountTextInput';
@@ -41,7 +42,8 @@ type ValidInputs =
| typeof RoomNameInput
| typeof ValuePicker
| typeof DatePicker
- | typeof RadioButtons;
+ | typeof RadioButtons
+ | typeof AddPlaidBankAccount;
type ValueTypeKey = 'string' | 'boolean' | 'date' | 'country';
type ValueTypeMap = {
@@ -128,8 +130,25 @@ type FormProps = {
disablePressOnEnter?: boolean;
};
+type FormRef = {
+ resetForm: (optionalValue: FormOnyxValues) => void;
+};
+
type InputRefs = Record>;
type FormInputErrors = Partial, MaybePhraseKey>>;
-export type {FormProps, ValidInputs, InputComponentValueProps, FormValue, ValueTypeKey, FormOnyxValues, FormOnyxKeys, FormInputErrors, InputRefs, InputComponentBaseProps, ValueTypeMap};
+export type {
+ FormProps,
+ ValidInputs,
+ InputComponentValueProps,
+ FormValue,
+ ValueTypeKey,
+ FormOnyxValues,
+ FormOnyxKeys,
+ FormInputErrors,
+ InputRefs,
+ InputComponentBaseProps,
+ ValueTypeMap,
+ FormRef,
+};
diff --git a/src/components/FormAlertWithSubmitButton.tsx b/src/components/FormAlertWithSubmitButton.tsx
index 270d476f4f79..27db5687a925 100644
--- a/src/components/FormAlertWithSubmitButton.tsx
+++ b/src/components/FormAlertWithSubmitButton.tsx
@@ -96,6 +96,7 @@ function FormAlertWithSubmitButton({
style={style}
danger={isSubmitActionDangerous}
medium={useSmallerSubmitButtonSize}
+ large={!useSmallerSubmitButtonSize}
/>
) : (
)}
{footerContent}
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index 3f1cde92a583..73a091815460 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -138,6 +138,7 @@ import Stopwatch from '@assets/images/stopwatch.svg';
import Sync from '@assets/images/sync.svg';
import Tag from '@assets/images/tag.svg';
import Task from '@assets/images/task.svg';
+import Tax from '@assets/images/tax.svg';
import ThreeDots from '@assets/images/three-dots.svg';
import ThumbsUp from '@assets/images/thumbs-up.svg';
import Transfer from '@assets/images/transfer.svg';
@@ -226,6 +227,7 @@ export {
Fullscreen,
Folder,
Tag,
+ Tax,
Gallery,
Gear,
Globe,
diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx
index d3214565ba46..e4484980fff1 100644
--- a/src/components/Icon/index.tsx
+++ b/src/components/Icon/index.tsx
@@ -25,6 +25,12 @@ type IconProps = {
/** Is small icon */
small?: boolean;
+ /** Is large icon */
+ large?: boolean;
+
+ /** Is medium icon */
+ medium?: boolean;
+
/** Is inline icon */
inline?: boolean;
@@ -50,6 +56,8 @@ function Icon({
height = variables.iconSizeNormal,
fill = undefined,
small = false,
+ large = false,
+ medium = false,
inline = false,
additionalStyles = [],
hovered = false,
@@ -59,8 +67,7 @@ function Icon({
}: IconProps) {
const StyleUtils = useStyleUtils();
const styles = useThemeStyles();
- const iconWidth = small ? variables.iconSizeSmall : width;
- const iconHeight = small ? variables.iconSizeSmall : height;
+ const {width: iconWidth, height: iconHeight} = StyleUtils.getIconWidthAndHeightStyle(small, medium, large, width, height);
const iconStyles = [StyleUtils.getWidthAndHeightStyle(width ?? 0, height), IconWrapperStyles, styles.pAbsolute, additionalStyles];
if (inline) {
diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx
index 2bad8322c8d9..6b562525258d 100755
--- a/src/components/MoneyRequestConfirmationList.tsx
+++ b/src/components/MoneyRequestConfirmationList.tsx
@@ -785,7 +785,7 @@ function MoneyRequestConfirmationList({
{
diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
index 968e1dfbfdca..5a9f96453987 100755
--- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
+++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
@@ -717,7 +717,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
) : (
diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx
index 83da817da858..4ee070e19893 100644
--- a/src/components/PopoverMenu.tsx
+++ b/src/components/PopoverMenu.tsx
@@ -1,4 +1,3 @@
-import type {ImageContentFit} from 'expo-image';
import type {RefObject} from 'react';
import React, {useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
@@ -10,48 +9,21 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
import CONST from '@src/CONST';
import type {AnchorPosition} from '@src/styles';
import type AnchorAlignment from '@src/types/utils/AnchorAlignment';
-import type IconAsset from '@src/types/utils/IconAsset';
import * as Expensicons from './Icon/Expensicons';
+import type {MenuItemProps} from './MenuItem';
import MenuItem from './MenuItem';
import PopoverWithMeasuredContent from './PopoverWithMeasuredContent';
import Text from './Text';
-type PopoverMenuItem = {
- /** An icon element displayed on the left side */
- icon?: IconAsset;
-
+type PopoverMenuItem = MenuItemProps & {
/** Text label */
text: string;
/** A callback triggered when this item is selected */
- onSelected: () => void;
-
- /** A description text to show under the title */
- description?: string;
-
- /** The fill color to pass into the icon. */
- iconFill?: string;
-
- /** Icon Width */
- iconWidth?: number;
-
- /** Icon Height */
- iconHeight?: number;
-
- /** Icon should be displayed in its own color */
- displayInDefaultIconColor?: boolean;
-
- /** Determines how the icon should be resized to fit its container */
- contentFit?: ImageContentFit;
+ onSelected?: () => void;
/** Sub menu items to be rendered after a menu item is selected */
subMenuItems?: PopoverMenuItem[];
-
- /** Determines whether an icon should be displayed on the right side of the menu item. */
- shouldShowRightIcon?: boolean;
-
- /** Adds padding to the left of the text when there is no icon. */
- shouldPutLeftPaddingWhenNoIcon?: boolean;
};
type PopoverModalProps = Pick;
@@ -181,7 +153,7 @@ function PopoverMenu({
const onModalHide = () => {
setFocusedIndex(-1);
if (selectedItemIndex.current !== null) {
- currentMenuItems[selectedItemIndex.current].onSelected();
+ currentMenuItems[selectedItemIndex.current].onSelected?.();
selectedItemIndex.current = null;
}
};
diff --git a/src/components/ProcessMoneyRequestHoldMenu.tsx b/src/components/ProcessMoneyRequestHoldMenu.tsx
index f1dc47b8556b..eefab719a103 100644
--- a/src/components/ProcessMoneyRequestHoldMenu.tsx
+++ b/src/components/ProcessMoneyRequestHoldMenu.tsx
@@ -53,6 +53,7 @@ function ProcessMoneyRequestHoldMenu({isVisible, onClose, onConfirm, anchorPosit
style={[styles.mt5]}
text={translate('common.buttonConfirm')}
onPress={onConfirm}
+ large
/>
diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx
index 6eedc322f393..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,13 +78,14 @@ function BaseListItem({
style={pressableStyle}
>
- {canSelectMultiple && (
+ {canSelectMultiple && checkmarkPosition === CONST.DIRECTION.LEFT && (
{item.isSelected && (
@@ -99,6 +102,21 @@ function BaseListItem({
{typeof children === 'function' ? children(hovered) : children}
+ {canSelectMultiple && checkmarkPosition === CONST.DIRECTION.RIGHT && (
+
+
+
+ )}
+
{!canSelectMultiple && item.isSelected && !rightHandSideComponent && (
(
{
@@ -61,18 +61,20 @@ function BaseSelectionList(
shouldShowTooltips = true,
shouldUseDynamicMaxToRenderPerBatch = false,
rightHandSideComponent,
+ checkmarkPosition,
isLoadingNewOptions = false,
onLayout,
customListHeader,
listHeaderWrapperStyle,
isRowMultilineSupported = false,
+ textInputRef,
}: BaseSelectionListProps,
- inputRef: ForwardedRef,
+ ref: ForwardedRef,
) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const listRef = useRef>>(null);
- const textInputRef = useRef(null);
+ const innerTextInputRef = useRef(null);
const focusTimeoutRef = useRef(null);
const shouldShowTextInput = !!textInputLabel;
const shouldShowSelectAll = !!onSelectAll;
@@ -80,6 +82,8 @@ function BaseSelectionList(
const isFocused = useIsFocused();
const [maxToRenderPerBatch, setMaxToRenderPerBatch] = useState(shouldUseDynamicMaxToRenderPerBatch ? 0 : CONST.MAX_TO_RENDER_PER_BATCH.DEFAULT);
const [isInitialSectionListRender, setIsInitialSectionListRender] = useState(true);
+ const [itemsToHighlight, setItemsToHighlight] = useState | null>(null);
+ const itemFocusTimeoutRef = useRef(null);
const [currentPage, setCurrentPage] = useState(1);
const incrementPage = () => setCurrentPage((prev) => prev + 1);
@@ -116,7 +120,8 @@ function BaseSelectionList(
});
// If disabled, add to the disabled indexes array
- if (!!section.isDisabled || item.isDisabled) {
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ if (!!section.isDisabled || item.isDisabled || item.isDisabledCheckbox) {
disabledOptionsIndexes.push(disabledIndex);
}
disabledIndex += 1;
@@ -235,16 +240,16 @@ function BaseSelectionList(
onSelectRow(item);
- if (shouldShowTextInput && shouldPreventDefaultFocusOnSelectRow && textInputRef.current) {
- textInputRef.current.focus();
+ if (shouldShowTextInput && shouldPreventDefaultFocusOnSelectRow && innerTextInputRef.current) {
+ innerTextInputRef.current.focus();
}
};
const selectAllRow = () => {
onSelectAll?.();
- if (shouldShowTextInput && shouldPreventDefaultFocusOnSelectRow && textInputRef.current) {
- textInputRef.current.focus();
+ if (shouldShowTextInput && shouldPreventDefaultFocusOnSelectRow && innerTextInputRef.current) {
+ innerTextInputRef.current.focus();
}
};
@@ -310,7 +315,7 @@ function BaseSelectionList(
const indexOffset = section.indexOffset ? section.indexOffset : 0;
const normalizedIndex = index + indexOffset;
const isDisabled = !!section.isDisabled || item.isDisabled;
- const isItemFocused = !isDisabled && focusedIndex === normalizedIndex;
+ const isItemFocused = !isDisabled && (focusedIndex === normalizedIndex || itemsToHighlight?.has(item.keyForList ?? ''));
// We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade.
const showTooltip = shouldShowTooltips && normalizedIndex < 10;
@@ -326,6 +331,7 @@ function BaseSelectionList(
onDismissError={() => onDismissError?.(item)}
shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow}
rightHandSideComponent={rightHandSideComponent}
+ checkmarkPosition={checkmarkPosition}
keyForList={item.keyForList ?? ''}
isMultilineSupported={isRowMultilineSupported}
/>
@@ -370,10 +376,10 @@ function BaseSelectionList(
useCallback(() => {
if (shouldShowTextInput) {
focusTimeoutRef.current = setTimeout(() => {
- if (!textInputRef.current) {
+ if (!innerTextInputRef.current) {
return;
}
- textInputRef.current.focus();
+ innerTextInputRef.current.focus();
}, CONST.ANIMATED_TRANSITION);
}
return () => {
@@ -399,6 +405,46 @@ function BaseSelectionList(
updateAndScrollToFocusedIndex(newSelectedIndex);
}, [canSelectMultiple, flattenedSections.allOptions.length, prevTextInputValue, textInputValue, updateAndScrollToFocusedIndex]);
+ useEffect(
+ () => () => {
+ if (!itemFocusTimeoutRef.current) {
+ return;
+ }
+ clearTimeout(itemFocusTimeoutRef.current);
+ },
+ [],
+ );
+
+ /**
+ * Highlights the items and scrolls to the first item present in the items list.
+ *
+ * @param items - The list of items to highlight.
+ * @param timeout - The timeout in milliseconds before removing the highlight.
+ */
+ const scrollAndHighlightItem = useCallback(
+ (items: string[], timeout: number) => {
+ const newItemsToHighlight = new Set();
+ items.forEach((item) => {
+ newItemsToHighlight.add(item);
+ });
+ const index = flattenedSections.allOptions.findIndex((option) => newItemsToHighlight.has(option.keyForList ?? ''));
+ updateAndScrollToFocusedIndex(index);
+ setItemsToHighlight(newItemsToHighlight);
+
+ if (itemFocusTimeoutRef.current) {
+ clearTimeout(itemFocusTimeoutRef.current);
+ }
+
+ itemFocusTimeoutRef.current = setTimeout(() => {
+ setFocusedIndex(-1);
+ setItemsToHighlight(null);
+ }, timeout);
+ },
+ [flattenedSections.allOptions, updateAndScrollToFocusedIndex],
+ );
+
+ useImperativeHandle(ref, () => ({scrollAndHighlightItem}), [scrollAndHighlightItem]);
+
/** Selects row when pressing Enter */
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedOption, {
captureOnInputs: true,
@@ -428,15 +474,14 @@ function BaseSelectionList(
{
- textInputRef.current = element as RNTextInput;
+ innerTextInputRef.current = element as RNTextInput;
- if (!inputRef) {
+ if (!textInputRef) {
return;
}
- if (typeof inputRef === 'function') {
- inputRef(element as RNTextInput);
- }
+ // eslint-disable-next-line no-param-reassign
+ textInputRef.current = element as RNTextInput;
}}
label={textInputLabel}
accessibilityLabel={textInputLabel}
@@ -512,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/index.android.tsx b/src/components/SelectionList/index.android.tsx
index 46f2af8356f6..f8e54b219f5b 100644
--- a/src/components/SelectionList/index.android.tsx
+++ b/src/components/SelectionList/index.android.tsx
@@ -1,11 +1,10 @@
import React, {forwardRef} from 'react';
import type {ForwardedRef} from 'react';
import {Keyboard} from 'react-native';
-import type {TextInput} from 'react-native';
import BaseSelectionList from './BaseSelectionList';
-import type {BaseSelectionListProps, ListItem} from './types';
+import type {BaseSelectionListProps, ListItem, SelectionListHandle} from './types';
-function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) {
+function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) {
return (
(props: BaseSelectionListProps, ref: ForwardedRef) {
+function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) {
return (
// eslint-disable-next-line react/jsx-props-no-spreading
diff --git a/src/components/SelectionList/index.tsx b/src/components/SelectionList/index.tsx
index 2446e1b4f5c1..a6fd636cc215 100644
--- a/src/components/SelectionList/index.tsx
+++ b/src/components/SelectionList/index.tsx
@@ -1,12 +1,11 @@
import React, {forwardRef, useEffect, useState} from 'react';
import type {ForwardedRef} from 'react';
import {Keyboard} from 'react-native';
-import type {TextInput} from 'react-native';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import BaseSelectionList from './BaseSelectionList';
-import type {BaseSelectionListProps, ListItem} from './types';
+import type {BaseSelectionListProps, ListItem, SelectionListHandle} from './types';
-function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) {
+function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) {
const [isScreenTouched, setIsScreenTouched] = useState(false);
const touchStart = () => setIsScreenTouched(true);
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index a9cd3dacc1a7..9e9ba7e5fc27 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -1,5 +1,6 @@
-import type {ReactElement, ReactNode} from 'react';
-import type {GestureResponderEvent, InputModeOptions, LayoutChangeEvent, SectionListData, StyleProp, TextStyle, ViewStyle} from 'react-native';
+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;
@@ -60,6 +64,9 @@ type ListItem = {
/** Whether this option is selected */
isSelected?: boolean;
+ /** Whether the checkbox should be disabled */
+ isDisabledCheckbox?: boolean;
+
/** Whether this option is disabled for selection */
isDisabled?: boolean | null;
@@ -267,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;
@@ -284,6 +294,13 @@ type BaseSelectionListProps = Partial & {
/** Whether to wrap long text up to 2 lines */
isRowMultilineSupported?: boolean;
+
+ /** Ref for textInput */
+ textInputRef?: MutableRefObject;
+};
+
+type SelectionListHandle = {
+ scrollAndHighlightItem?: (items: string[], timeout: number) => void;
};
type ItemLayout = {
@@ -317,4 +334,5 @@ export type {
ItemLayout,
ButtonOrCheckBoxRoles,
SectionListDataType,
+ SelectionListHandle,
};
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 {
ExitSurvey.switchToOldDot();
diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx
index a57f308b5623..7059efab309f 100755
--- a/src/pages/settings/InitialSettingsPage.tsx
+++ b/src/pages/settings/InitialSettingsPage.tsx
@@ -415,6 +415,7 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa
originalFileName={currentUserDetails.originalFileName}
headerTitle={translate('profilePage.profileAvatar')}
fallbackIcon={currentUserDetails?.fallbackIcon}
+ editIconStyle={styles.smallEditIconAccount}
/>
-
+
{headerContent}
{accountMenuItems}
{workspaceMenuItems}
diff --git a/src/pages/settings/Preferences/LanguagePage.js b/src/pages/settings/Preferences/LanguagePage.tsx
similarity index 56%
rename from src/pages/settings/Preferences/LanguagePage.js
rename to src/pages/settings/Preferences/LanguagePage.tsx
index 1df1565214c9..b577e006acfb 100644
--- a/src/pages/settings/Preferences/LanguagePage.js
+++ b/src/pages/settings/Preferences/LanguagePage.tsx
@@ -1,28 +1,21 @@
-import PropTypes from 'prop-types';
import React from 'react';
-import _ from 'underscore';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
import Navigation from '@libs/Navigation/Navigation';
import * as App from '@userActions/App';
import CONST from '@src/CONST';
-const propTypes = {
- ...withLocalizePropTypes,
+function LanguagePage() {
+ const {translate, preferredLocale} = useLocalize();
- /** The preferred language of the App */
- preferredLocale: PropTypes.string.isRequired,
-};
-
-function LanguagePage(props) {
- const localesToLanguages = _.map(CONST.LANGUAGES, (language) => ({
+ const localesToLanguages = CONST.LANGUAGES.map((language) => ({
value: language,
- text: props.translate(`languagePage.languages.${language}.label`),
+ text: translate(`languagePage.languages.${language}.label`),
keyForList: language,
- isSelected: props.preferredLocale === language,
+ isSelected: preferredLocale === language,
}));
return (
@@ -31,20 +24,19 @@ function LanguagePage(props) {
testID={LanguagePage.displayName}
>
Navigation.goBack()}
/>
App.setLocaleAndNavigate(language.value)}
- initiallyFocusedOptionKey={_.find(localesToLanguages, (locale) => locale.isSelected).keyForList}
+ initiallyFocusedOptionKey={localesToLanguages.find((locale) => locale.isSelected)?.keyForList}
/>
);
}
LanguagePage.displayName = 'LanguagePage';
-LanguagePage.propTypes = propTypes;
-export default withLocalize(LanguagePage);
+export default LanguagePage;
diff --git a/src/pages/settings/Preferences/PreferencesPage.js b/src/pages/settings/Preferences/PreferencesPage.tsx
similarity index 86%
rename from src/pages/settings/Preferences/PreferencesPage.js
rename to src/pages/settings/Preferences/PreferencesPage.tsx
index 36a26ccffaa2..5849f323dc36 100755
--- a/src/pages/settings/Preferences/PreferencesPage.js
+++ b/src/pages/settings/Preferences/PreferencesPage.tsx
@@ -1,7 +1,6 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Illustrations from '@components/Icon/Illustrations';
@@ -15,33 +14,28 @@ import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
+import LocaleUtils from '@libs/LocaleUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as User from '@userActions/User';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type {PreferredTheme, PriorityMode, User as UserType} from '@src/types/onyx';
-const propTypes = {
+type PreferencesPageOnyxProps = {
/** The chat priority mode */
- priorityMode: PropTypes.string,
+ priorityMode: PriorityMode;
/** The app's color theme */
- preferredTheme: PropTypes.string,
+ preferredTheme: PreferredTheme;
/** The details about the user that is signed in */
- user: PropTypes.shape({
- /** Whether or not the user is subscribed to news updates */
- isSubscribedToNewsletter: PropTypes.bool,
- }),
+ user: OnyxEntry;
};
-const defaultProps = {
- priorityMode: CONST.PRIORITY_MODE.DEFAULT,
- preferredTheme: CONST.DEFAULT_THEME,
- user: {},
-};
+type PreferencesPageProps = PreferencesPageOnyxProps;
-function PreferencesPage(props) {
+function PreferencesPage({priorityMode, preferredTheme, user}: PreferencesPageProps) {
const styles = useThemeStyles();
const {translate, preferredLocale} = useLocalize();
const {isSmallScreenWidth} = useWindowDimensions();
@@ -83,7 +77,7 @@ function PreferencesPage(props) {
@@ -95,28 +89,28 @@ function PreferencesPage(props) {
Navigation.navigate(ROUTES.SETTINGS_PRIORITY_MODE)}
wrapperStyle={styles.sectionMenuItemTopDescription}
/>
Navigation.navigate(ROUTES.SETTINGS_LANGUAGE)}
wrapperStyle={styles.sectionMenuItemTopDescription}
/>
Navigation.navigate(ROUTES.SETTINGS_THEME)}
wrapperStyle={styles.sectionMenuItemTopDescription}
@@ -129,11 +123,9 @@ function PreferencesPage(props) {
);
}
-PreferencesPage.propTypes = propTypes;
-PreferencesPage.defaultProps = defaultProps;
PreferencesPage.displayName = 'PreferencesPage';
-export default withOnyx({
+export default withOnyx({
priorityMode: {
key: ONYXKEYS.NVP_PRIORITY_MODE,
},
diff --git a/src/pages/settings/Preferences/PriorityModePage.js b/src/pages/settings/Preferences/PriorityModePage.tsx
similarity index 50%
rename from src/pages/settings/Preferences/PriorityModePage.js
rename to src/pages/settings/Preferences/PriorityModePage.tsx
index 05c0546c2e41..677d3813acd7 100644
--- a/src/pages/settings/Preferences/PriorityModePage.js
+++ b/src/pages/settings/Preferences/PriorityModePage.tsx
@@ -1,49 +1,54 @@
-import PropTypes from 'prop-types';
import React, {useCallback} from 'react';
import {withOnyx} from 'react-native-onyx';
-import _, {compose} 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 Text from '@components/Text';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import * as User from '@userActions/User';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import type {PriorityMode} from '@src/types/onyx';
-const propTypes = {
- /** The chat priority mode */
- priorityMode: PropTypes.string,
-
- ...withLocalizePropTypes,
+type PriorityModeItem = {
+ value: ValueOf;
+ text: string;
+ alternateText: string;
+ keyForList: ValueOf;
+ isSelected: boolean;
};
-const defaultProps = {
- priorityMode: CONST.PRIORITY_MODE.DEFAULT,
+type PriorityModePageOnyxProps = {
+ /** The chat priority mode */
+ priorityMode: PriorityMode;
};
-function PriorityModePage(props) {
+type PriorityModePageProps = PriorityModePageOnyxProps;
+
+function PriorityModePage({priorityMode}: PriorityModePageProps) {
+ const {translate} = useLocalize();
const styles = useThemeStyles();
- const priorityModes = _.map(_.values(CONST.PRIORITY_MODE), (mode) => ({
+ const priorityModes = Object.values(CONST.PRIORITY_MODE).map((mode) => ({
value: mode,
- text: props.translate(`priorityModePage.priorityModes.${mode}.label`),
- alternateText: props.translate(`priorityModePage.priorityModes.${mode}.description`),
+ text: translate(`priorityModePage.priorityModes.${mode}.label`),
+ alternateText: translate(`priorityModePage.priorityModes.${mode}.description`),
keyForList: mode,
- isSelected: props.priorityMode === mode,
+ isSelected: priorityMode === mode,
}));
const updateMode = useCallback(
- (mode) => {
- if (mode.value === props.priorityMode) {
+ (mode: PriorityModeItem) => {
+ if (mode.value === priorityMode) {
Navigation.goBack();
return;
}
User.updateChatPriorityMode(mode.value);
},
- [props.priorityMode],
+ [priorityMode],
);
return (
@@ -52,29 +57,24 @@ function PriorityModePage(props) {
testID={PriorityModePage.displayName}
>
Navigation.goBack()}
/>
- {props.translate('priorityModePage.explainerText')}
+ {translate('priorityModePage.explainerText')} mode.isSelected).keyForList}
+ initiallyFocusedOptionKey={priorityModes.find((mode) => mode.isSelected)?.keyForList}
/>
);
}
PriorityModePage.displayName = 'PriorityModePage';
-PriorityModePage.propTypes = propTypes;
-PriorityModePage.defaultProps = defaultProps;
-export default compose(
- withLocalize,
- withOnyx({
- priorityMode: {
- key: ONYXKEYS.NVP_PRIORITY_MODE,
- },
- }),
-)(PriorityModePage);
+export default withOnyx({
+ priorityMode: {
+ key: ONYXKEYS.NVP_PRIORITY_MODE,
+ },
+})(PriorityModePage);
diff --git a/src/pages/settings/Preferences/ThemePage.js b/src/pages/settings/Preferences/ThemePage.tsx
similarity index 74%
rename from src/pages/settings/Preferences/ThemePage.js
rename to src/pages/settings/Preferences/ThemePage.tsx
index 0724eb286620..f879a7a259ea 100644
--- a/src/pages/settings/Preferences/ThemePage.js
+++ b/src/pages/settings/Preferences/ThemePage.tsx
@@ -1,7 +1,5 @@
-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 SelectionList from '@components/SelectionList';
@@ -13,24 +11,24 @@ import Navigation from '@libs/Navigation/Navigation';
import * as User from '@userActions/User';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import type {PreferredTheme} from '@src/types/onyx';
-const propTypes = {
+type ThemePageOnyxProps = {
/** The theme of the app */
- preferredTheme: PropTypes.string,
+ preferredTheme: PreferredTheme;
};
-const defaultProps = {
- preferredTheme: CONST.THEME.DEFAULT,
-};
+type ThemePageProps = ThemePageOnyxProps;
-function ThemePage(props) {
+function ThemePage({preferredTheme}: ThemePageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
- const localesToThemes = _.map(_.values(_.omit(CONST.THEME, 'DEFAULT', 'FALLBACK')), (theme) => ({
+ const {DEFAULT, FALLBACK, ...themes} = CONST.THEME;
+ const localesToThemes = Object.values(themes).map((theme) => ({
value: theme,
text: translate(`themePage.themes.${theme}.label`),
keyForList: theme,
- isSelected: (props.preferredTheme || CONST.THEME.DEFAULT) === theme,
+ isSelected: (preferredTheme ?? CONST.THEME.DEFAULT) === theme,
}));
return (
@@ -51,17 +49,15 @@ function ThemePage(props) {
sections={[{data: localesToThemes}]}
ListItem={RadioListItem}
onSelectRow={(theme) => User.updateTheme(theme.value)}
- initiallyFocusedOptionKey={_.find(localesToThemes, (theme) => theme.isSelected).keyForList}
+ initiallyFocusedOptionKey={localesToThemes.find((theme) => theme.isSelected)?.keyForList}
/>
);
}
ThemePage.displayName = 'ThemePage';
-ThemePage.propTypes = propTypes;
-ThemePage.defaultProps = defaultProps;
-export default withOnyx({
+export default withOnyx({
preferredTheme: {
key: ONYXKEYS.PREFERRED_THEME,
},
diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx
index 3851ef7153fb..80b76b9a6acb 100644
--- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx
+++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx
@@ -121,6 +121,7 @@ function ContactMethodsPage({loginList, session, route}: ContactMethodsPageProps
{loginMenuItems}
diff --git a/src/pages/settings/Profile/CustomStatus/StatusPage.js b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx
similarity index 76%
rename from src/pages/settings/Profile/CustomStatus/StatusPage.js
rename to src/pages/settings/Profile/CustomStatus/StatusPage.tsx
index f6c5f5543fa0..bb7be4a6866c 100644
--- a/src/pages/settings/Profile/CustomStatus/StatusPage.js
+++ b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx
@@ -1,10 +1,12 @@
-import lodashGet from 'lodash/get';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {InteractionManager, View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
import EmojiPickerButtonDropdown from '@components/EmojiPicker/EmojiPickerButtonDropdown';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues, FormRef} from '@components/Form/types';
import HeaderPageLayout from '@components/HeaderPageLayout';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
@@ -13,13 +15,13 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
-import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails';
+import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
+import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import DateUtils from '@libs/DateUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as User from '@userActions/User';
@@ -27,41 +29,43 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
+import INPUT_IDS from '@src/types/form/SettingsStatusSetForm';
+import type {CustomStatusDraft} from '@src/types/onyx';
-const INPUT_IDS = {
- EMOJI_CODE: 'emojiCode',
- STATUS_TEXT: 'statusText',
+type StatusPageOnyxProps = {
+ draftStatus: OnyxEntry;
};
-const propTypes = {
- ...withCurrentUserPersonalDetailsPropTypes,
-};
+type StatusPageProps = StatusPageOnyxProps & WithCurrentUserPersonalDetailsProps;
const initialEmoji = '💬';
-function StatusPage({draftStatus, currentUserPersonalDetails}) {
+function StatusPage({draftStatus, currentUserPersonalDetails}: StatusPageProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
- const formRef = useRef(null);
- const [brickRoadIndicator, setBrickRoadIndicator] = useState('');
- const currentUserEmojiCode = lodashGet(currentUserPersonalDetails, 'status.emojiCode', '');
- const currentUserStatusText = lodashGet(currentUserPersonalDetails, 'status.text', '');
- const currentUserClearAfter = lodashGet(currentUserPersonalDetails, 'status.clearAfter', '');
- const draftEmojiCode = lodashGet(draftStatus, 'emojiCode');
- const draftText = lodashGet(draftStatus, 'text');
- const draftClearAfter = lodashGet(draftStatus, 'clearAfter');
-
+ const formRef = useRef(null);
+ const [brickRoadIndicator, setBrickRoadIndicator] = useState>();
+ const currentUserEmojiCode = currentUserPersonalDetails?.status?.emojiCode ?? '';
+ const currentUserStatusText = currentUserPersonalDetails?.status?.text ?? '';
+ const currentUserClearAfter = currentUserPersonalDetails?.status?.clearAfter ?? '';
+ const draftEmojiCode = draftStatus?.emojiCode;
+ const draftText = draftStatus?.text;
+ const draftClearAfter = draftStatus?.clearAfter;
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const defaultEmoji = draftEmojiCode || currentUserEmojiCode;
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const defaultText = draftText || currentUserStatusText;
const customClearAfter = useMemo(() => {
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const dataToShow = draftClearAfter || currentUserClearAfter;
return DateUtils.getLocalizedTimePeriodDescription(dataToShow);
}, [draftClearAfter, currentUserClearAfter]);
const isValidClearAfterDate = useCallback(() => {
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const clearAfterTime = draftClearAfter || currentUserClearAfter;
if (clearAfterTime === CONST.CUSTOM_STATUS_TYPES.NEVER || clearAfterTime === '') {
return true;
@@ -72,11 +76,12 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) {
const navigateBackToPreviousScreen = useCallback(() => Navigation.goBack(), []);
const updateStatus = useCallback(
- ({emojiCode, statusText}) => {
+ ({emojiCode, statusText}: FormOnyxValues) => {
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const clearAfterTime = draftClearAfter || currentUserClearAfter || CONST.CUSTOM_STATUS_TYPES.NEVER;
const isValid = DateUtils.isTimeAtLeastOneMinuteInFuture({dateTimeString: clearAfterTime});
if (!isValid && clearAfterTime !== CONST.CUSTOM_STATUS_TYPES.NEVER) {
- setBrickRoadIndicator(isValidClearAfterDate() ? null : CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR);
+ setBrickRoadIndicator(isValidClearAfterDate() ? undefined : CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR);
return;
}
User.updateCustomStatus({
@@ -84,7 +89,6 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) {
emojiCode: !emojiCode && statusText ? initialEmoji : emojiCode,
clearAfter: clearAfterTime !== CONST.CUSTOM_STATUS_TYPES.NEVER ? clearAfterTime : '',
});
-
User.clearDraftCustomStatus();
InteractionManager.runAfterInteractions(() => {
navigateBackToPreviousScreen();
@@ -100,13 +104,14 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) {
emojiCode: '',
clearAfter: DateUtils.getEndOfToday(),
});
- formRef.current.resetForm({[INPUT_IDS.EMOJI_CODE]: ''});
+ formRef.current?.resetForm({[INPUT_IDS.EMOJI_CODE]: ''});
+
InteractionManager.runAfterInteractions(() => {
navigateBackToPreviousScreen();
});
};
- useEffect(() => setBrickRoadIndicator(isValidClearAfterDate() ? null : CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR), [isValidClearAfterDate]);
+ useEffect(() => setBrickRoadIndicator(isValidClearAfterDate() ? undefined : CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR), [isValidClearAfterDate]);
useEffect(() => {
if (!currentUserEmojiCode && !currentUserClearAfter && !draftClearAfter) {
@@ -119,7 +124,7 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- const validateForm = useCallback(() => {
+ const validateForm = useCallback((): FormInputErrors => {
if (brickRoadIndicator) {
return {clearAfter: ''};
}
@@ -157,8 +162,9 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) {
@@ -166,7 +172,7 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) {
InputComponent={TextInput}
ref={inputCallbackRef}
inputID={INPUT_IDS.STATUS_TEXT}
- role={CONST.ACCESSIBILITY_ROLE.TEXT}
+ role={CONST.ROLE.PRESENTATION}
label={translate('statusPage.message')}
accessibilityLabel={INPUT_IDS.STATUS_TEXT}
defaultValue={defaultText}
@@ -198,13 +204,11 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) {
}
StatusPage.displayName = 'StatusPage';
-StatusPage.propTypes = propTypes;
-export default compose(
- withCurrentUserPersonalDetails,
- withOnyx({
+export default withCurrentUserPersonalDetails(
+ withOnyx({
draftStatus: {
key: () => ONYXKEYS.CUSTOM_STATUS_DRAFT,
},
- }),
-)(StatusPage);
+ })(StatusPage),
+);
diff --git a/src/pages/settings/Profile/DisplayNamePage.js b/src/pages/settings/Profile/DisplayNamePage.tsx
similarity index 67%
rename from src/pages/settings/Profile/DisplayNamePage.js
rename to src/pages/settings/Profile/DisplayNamePage.tsx
index c443a91a7313..2e7a67509139 100644
--- a/src/pages/settings/Profile/DisplayNamePage.js
+++ b/src/pages/settings/Profile/DisplayNamePage.tsx
@@ -1,19 +1,19 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
-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 * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as ValidationUtils from '@libs/ValidationUtils';
@@ -22,39 +22,27 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import INPUT_IDS from '@src/types/form/DisplayNameForm';
-const propTypes = {
- ...withLocalizePropTypes,
- ...withCurrentUserPersonalDetailsPropTypes,
- isLoadingApp: PropTypes.bool,
+type DisplayNamePageOnyxProps = {
+ isLoadingApp: OnyxEntry;
};
-const defaultProps = {
- ...withCurrentUserPersonalDetailsDefaultProps,
- isLoadingApp: true,
-};
+type DisplayNamePageProps = DisplayNamePageOnyxProps & WithCurrentUserPersonalDetailsProps;
/**
* Submit form to update user's first and last name (and display name)
- * @param {Object} values
- * @param {String} values.firstName
- * @param {String} values.lastName
*/
-const updateDisplayName = (values) => {
+const updateDisplayName = (values: FormOnyxValues) => {
PersonalDetails.updateDisplayName(values.firstName.trim(), values.lastName.trim());
};
-function DisplayNamePage(props) {
+function DisplayNamePage({isLoadingApp = true, currentUserPersonalDetails}: DisplayNamePageProps) {
const styles = useThemeStyles();
- const currentUserDetails = props.currentUserPersonalDetails || {};
+ const {translate} = useLocalize();
+
+ const currentUserDetails = currentUserPersonalDetails ?? {};
- /**
- * @param {Object} values
- * @param {String} values.firstName
- * @param {String} values.lastName
- * @returns {Object} - An object containing the errors for each inputID
- */
- const validate = (values) => {
- const errors = {};
+ const validate = (values: FormOnyxValues) => {
+ const errors: FormInputErrors = {};
// First we validate the first name field
if (!ValidationUtils.isValidDisplayName(values.firstName)) {
@@ -77,7 +65,6 @@ function DisplayNamePage(props) {
}
return errors;
};
-
return (
Navigation.goBack()}
/>
- {props.isLoadingApp ? (
+ {isLoadingApp ? (
) : (
- {props.translate('displayNamePage.isShownOnProfile')}
+ {translate('displayNamePage.isShownOnProfile')}
@@ -119,10 +106,10 @@ function DisplayNamePage(props) {
InputComponent={TextInput}
inputID={INPUT_IDS.LAST_NAME}
name="lname"
- label={props.translate('common.lastName')}
- aria-label={props.translate('common.lastName')}
+ label={translate('common.lastName')}
+ aria-label={translate('common.lastName')}
role={CONST.ROLE.PRESENTATION}
- defaultValue={lodashGet(currentUserDetails, 'lastName', '')}
+ defaultValue={currentUserDetails.lastName ?? ''}
spellCheck={false}
/>
@@ -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
;
+
+ /** Collection of categories attached to a policy */
+ policyCategories: OnyxEntry;
};
type WorkspaceInitialPageProps = WithPolicyAndFullscreenLoadingProps & WorkspaceInitialPageOnyxProps & StackScreenProps;
@@ -57,7 +60,7 @@ function dismissError(policyID: string) {
Policy.removeWorkspace(policyID);
}
-function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, reimbursementAccount}: WorkspaceInitialPageProps) {
+function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, reimbursementAccount, policyCategories}: WorkspaceInitialPageProps) {
const styles = useThemeStyles();
const policy = policyDraft?.id ? policyDraft : policyProp;
const [isCurrencyModalOpen, setIsCurrencyModalOpen] = useState(false);
@@ -97,6 +100,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
}, [policyID, policyName]);
const hasMembersError = PolicyUtils.hasPolicyMemberError(policyMembers);
+ const hasPolicyCategoryError = PolicyUtils.hasPolicyCategoriesError(policyCategories);
const hasGeneralSettingsError = !isEmptyObject(policy?.errorFields?.generalSettings ?? {}) || !isEmptyObject(policy?.errorFields?.avatar ?? {});
const shouldShowProtectedItems = PolicyUtils.isPolicyAdmin(policy);
const isPaidGroupPolicy = PolicyUtils.isPaidGroupPolicy(policy);
@@ -170,6 +174,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
translationKey: 'workspace.common.categories',
icon: Expensicons.Folder,
action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES.getRoute(policyID)))),
+ brickRoadIndicator: hasPolicyCategoryError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
routeName: SCREENS.WORKSPACE.CATEGORIES,
});
}
@@ -183,6 +188,15 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
});
}
+ if (policy?.tax?.trackingEnabled) {
+ protectedCollectPolicyMenuItems.push({
+ translationKey: 'workspace.common.taxes',
+ icon: Expensicons.Tax,
+ action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TAXES.getRoute(policyID)))),
+ routeName: SCREENS.WORKSPACE.TAXES,
+ });
+ }
+
protectedCollectPolicyMenuItems.push({
translationKey: 'workspace.common.moreFeatures',
icon: Expensicons.Gear,
@@ -302,5 +316,8 @@ export default withPolicyAndFullscreenLoading(
reimbursementAccount: {
key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
},
+ policyCategories: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route.params?.policyID ?? '0'}`,
+ },
})(WorkspaceInitialPage),
);
diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx
index 72f08095b58a..df1d3cd63011 100644
--- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx
+++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx
@@ -85,7 +85,6 @@ function WorkspaceInviteMessagePage({workspaceInviteMessageDraft, invitedEmailsT
Keyboard.dismiss();
// Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details
Policy.addMembersToWorkspace(invitedEmailsToAccountIDsDraft ?? {}, welcomeNote ?? '', route.params.policyID);
- Policy.setWorkspaceInviteMembersDraft(route.params.policyID, {});
SearchInputManager.searchInput = '';
// Pop the invite message page before navigating to the members page.
Navigation.goBack();
diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx
index 67bf6f8064da..00d7eaa33f6b 100644
--- a/src/pages/workspace/WorkspaceInvitePage.tsx
+++ b/src/pages/workspace/WorkspaceInvitePage.tsx
@@ -321,6 +321,7 @@ function WorkspaceInvitePage({
showScrollIndicator
showLoadingPlaceholder={!didScreenTransitionEnd || !OptionsListUtils.isPersonalDetailsReady(personalDetailsProp)}
shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
+ checkmarkPosition={CONST.DIRECTION.RIGHT}
/>
;
+
/** Session info for the currently logged in user. */
session: OnyxEntry;
+
+ /** An object containing the accountID for every invited user email */
+ invitedEmailsToAccountIDsDraft: OnyxEntry;
};
type WorkspaceMembersPageProps = WithPolicyAndFullscreenLoadingProps &
@@ -70,7 +75,16 @@ function invertObject(object: Record): Record {
type MemberOption = Omit & {accountID: number};
-function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, session, currentUserPersonalDetails, isLoadingReportData = true}: WorkspaceMembersPageProps) {
+function WorkspaceMembersPage({
+ policyMembers,
+ personalDetails,
+ invitedEmailsToAccountIDsDraft,
+ route,
+ policy,
+ session,
+ currentUserPersonalDetails,
+ isLoadingReportData = true,
+}: WorkspaceMembersPageProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const [selectedEmployees, setSelectedEmployees] = useState([]);
@@ -91,6 +105,8 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se
() => !isOfflineAndNoMemberDataAvailable && (!OptionsListUtils.isPersonalDetailsReady(personalDetails) || isEmptyObject(policyMembers)),
[isOfflineAndNoMemberDataAvailable, personalDetails, policyMembers],
);
+ const selectionListRef = useRef(null);
+ const isFocused = useIsFocused();
/**
* Get filtered personalDetails list with current policyMembers
@@ -288,15 +304,17 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se
/**
* Check if the policy member is deleted from the workspace
*/
- const isDeletedPolicyMember = (policyMember: PolicyMember): boolean =>
- !isOffline && policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isEmptyObject(policyMember.errors);
+ const isDeletedPolicyMember = useCallback(
+ (policyMember: PolicyMember): boolean => !isOffline && policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isEmptyObject(policyMember.errors),
+ [isOffline],
+ );
const policyOwner = policy?.owner;
const currentUserLogin = currentUserPersonalDetails.login;
const policyID = route.params.policyID;
const invitedPrimaryToSecondaryLogins = invertObject(policy?.primaryLoginsInvited ?? {});
- const getUsers = (): MemberOption[] => {
+ const getUsers = useCallback((): MemberOption[] => {
let result: MemberOption[] = [];
Object.entries(policyMembers ?? {}).forEach(([accountIDKey, policyMember]) => {
@@ -369,8 +387,40 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se
result = result.sort((a, b) => (a.text ?? '').toLowerCase().localeCompare((b.text ?? '').toLowerCase()));
return result;
- };
- const data = getUsers();
+ }, [
+ StyleUtils,
+ currentUserLogin,
+ formatPhoneNumber,
+ invitedPrimaryToSecondaryLogins,
+ isDeletedPolicyMember,
+ isPolicyAdmin,
+ personalDetails,
+ policy?.owner,
+ policy?.ownerAccountID,
+ policyMembers,
+ policyOwner,
+ selectedEmployees,
+ session?.accountID,
+ styles.activeItemBadge,
+ styles.badgeBordered,
+ styles.justifyContentCenter,
+ styles.textStrong,
+ translate,
+ ]);
+
+ const data = useMemo(() => getUsers(), [getUsers]);
+
+ useEffect(() => {
+ if (!isFocused) {
+ return;
+ }
+ if (isEmptyObject(invitedEmailsToAccountIDsDraft) || accountIDs === prevAccountIDs) {
+ return;
+ }
+ const invitedEmails = Object.values(invitedEmailsToAccountIDsDraft).map(String);
+ selectionListRef.current?.scrollAndHighlightItem?.(invitedEmails, 1500);
+ Policy.setWorkspaceInviteMembersDraft(route.params.policyID, {});
+ }, [invitedEmailsToAccountIDsDraft, route.params.policyID, isFocused, accountIDs, prevAccountIDs]);
const getHeaderMessage = () => {
if (isOfflineAndNoMemberDataAvailable) {
@@ -483,7 +533,6 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se
onPress={inviteUser}
text={translate('workspace.invite.member')}
icon={Expensicons.Plus}
- iconStyles={StyleUtils.getTransformScaleStyle(0.6)}
innerStyles={[isSmallScreenWidth && styles.alignItemsCenter]}
style={[isSmallScreenWidth && styles.flexGrow1]}
/>
@@ -537,6 +586,7 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se
/>
@@ -568,6 +618,9 @@ export default withCurrentUserPersonalDetails(
personalDetails: {
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
},
+ invitedEmailsToAccountIDsDraft: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`,
+ },
session: {
key: ONYXKEYS.SESSION,
},
diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
index 000ba0db7bc7..2c8123670e0b 100644
--- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
+++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
@@ -1,5 +1,5 @@
import type {StackScreenProps} from '@react-navigation/stack';
-import React, {useCallback} from 'react';
+import React, {useCallback, useEffect} from 'react';
import {View} from 'react-native';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Illustrations from '@components/Icon/Illustrations';
@@ -7,6 +7,7 @@ import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import type {WorkspacesCentralPaneNavigatorParamList} from '@libs/Navigation/types';
@@ -87,6 +88,16 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
Policy.enablePolicyTags(policy?.id ?? '', isEnabled);
},
},
+ {
+ icon: Illustrations.Coins,
+ titleTranslationKey: 'workspace.moreFeatures.taxes.title',
+ subtitleTranslationKey: 'workspace.moreFeatures.taxes.subtitle',
+ isActive: policy?.tax?.trackingEnabled ?? false,
+ pendingAction: policy?.pendingFields?.tax,
+ action: (isEnabled: boolean) => {
+ Policy.enablePolicyTaxes(policy?.id ?? '', isEnabled);
+ },
+ },
];
const sections: SectionObject[] = [
@@ -141,6 +152,17 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
[isSmallScreenWidth, styles, renderItem, translate],
);
+ function fetchFeatures() {
+ Policy.openPolicyMoreFeaturesPage(route.params.policyID);
+ }
+
+ useNetwork({onReconnect: fetchFeatures});
+
+ useEffect(() => {
+ fetchFeatures();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
return (
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 ddebc9d4b368..2df6dd0489f9 100644
--- a/src/pages/workspace/WorkspaceProfilePage.tsx
+++ b/src/pages/workspace/WorkspaceProfilePage.tsx
@@ -127,6 +127,7 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi
styles.alignItemsStart,
styles.sectionMenuItemTopDescription,
]}
+ editIconStyle={styles.smallEditIconWorkspace}
isUsingDefaultAvatar={!policy?.avatar ?? null}
onImageSelected={(file) => Policy.updateWorkspaceAvatar(policy?.id ?? '', file as File)}
onImageRemoved={() => Policy.deleteWorkspaceAvatar(policy?.id ?? '')}
@@ -193,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 3f2ef8ce6aa6..d22b822359f9 100644
--- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
+++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
@@ -1,9 +1,11 @@
import type {StackScreenProps} from '@react-navigation/stack';
-import React, {useEffect, useMemo, useState} from 'react';
+import React, {useEffect, useMemo, useRef, useState} from 'react';
import {ActivityIndicator, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import Button from '@components/Button';
+import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
+import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
@@ -11,6 +13,7 @@ import * as Illustrations from '@components/Icon/Illustrations';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import TableListItem from '@components/SelectionList/TableListItem';
+import type {ListItem} from '@components/SelectionList/types';
import Text from '@components/Text';
import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection';
import useLocalize from '@hooks/useLocalize';
@@ -18,6 +21,7 @@ import useNetwork from '@hooks/useNetwork';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
+import {deleteWorkspaceCategories, setWorkspaceCategoryEnabled} from '@libs/actions/Policy';
import localeCompare from '@libs/LocaleCompare';
import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
@@ -30,13 +34,11 @@ 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 DeepValueOf from '@src/types/utils/DeepValueOf';
-type PolicyForList = {
- value: string;
- text: string;
+type PolicyOption = ListItem & {
+ /** Category name is used as a key for the selectedCategories state */
keyForList: string;
- isSelected: boolean;
- rightElement: React.ReactNode;
};
type WorkspaceCategoriesOnyxProps = {
@@ -55,6 +57,7 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat
const theme = useTheme();
const {translate} = useLocalize();
const [selectedCategories, setSelectedCategories] = useState>({});
+ const dropdownButtonRef = useRef(null);
function fetchCategories() {
Policy.openPolicyCategoriesPage(route.params.policyID);
@@ -67,43 +70,63 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- const categoryList = useMemo(
+ const categoryList = useMemo(
() =>
Object.values(policyCategories ?? {})
.sort((a, b) => localeCompare(a.name, b.name))
- .map((value) => ({
- value: value.name,
- text: value.name,
- keyForList: value.name,
- isSelected: !!selectedCategories[value.name],
- pendingAction: value.pendingAction,
- rightElement: (
-
-
- {value.enabled ? translate('workspace.common.enabled') : translate('workspace.common.disabled')}
-
-
-
+ .map((value) => {
+ const isDisabled = value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.values(value.pendingFields ?? {}).length > 0;
+ return {
+ text: value.name,
+ keyForList: value.name,
+ isSelected: !!selectedCategories[value.name],
+ isDisabled,
+ pendingAction: value.pendingAction,
+ errors: value.errors ?? undefined,
+ rightElement: (
+
+
+ {value.enabled ? translate('workspace.common.enabled') : translate('workspace.common.disabled')}
+
+
+
+
-
- ),
- })),
- [policyCategories, selectedCategories, styles.alignSelfCenter, styles.flexRow, styles.label, styles.p1, styles.pl2, styles.textSupporting, theme.icon, translate],
+ ),
+ };
+ }),
+ [
+ policyCategories,
+ selectedCategories,
+ styles.alignSelfCenter,
+ styles.buttonOpacityDisabled,
+ styles.flexRow,
+ styles.label,
+ styles.p1,
+ styles.pl2,
+ styles.textSupporting,
+ theme.icon,
+ translate,
+ ],
);
- const toggleCategory = (category: PolicyForList) => {
- setSelectedCategories((prev) => ({
- ...prev,
- [category.value]: !prev[category.value],
- }));
+ const toggleCategory = (category: PolicyOption) => {
+ setSelectedCategories((prev) => {
+ if (prev[category.keyForList]) {
+ const {[category.keyForList]: omittedCategory, ...newCategories} = prev;
+ return newCategories;
+ }
+ return {...prev, [category.keyForList]: true};
+ });
};
const toggleAllCategories = () => {
- const isAllSelected = categoryList.every((category) => !!selectedCategories[category.value]);
- setSelectedCategories(isAllSelected ? {} : Object.fromEntries(categoryList.map((item) => [item.value, true])));
+ const availableCategories = categoryList.filter((category) => category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE);
+ const isAllSelected = availableCategories.length === Object.keys(selectedCategories).length;
+ setSelectedCategories(isAllSelected ? {} : Object.fromEntries(availableCategories.map((item) => [item.keyForList, true])));
};
const getCustomListHeader = () => (
@@ -113,42 +136,122 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat
);
- const navigateToCategoriesSettings = () => {
- Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES_SETTINGS.getRoute(route.params.policyID));
+ const navigateToCategorySettings = (category: PolicyOption) => {
+ Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(route.params.policyID, category.keyForList));
};
- const navigateToCategorySettings = (category: PolicyForList) => {
- Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(route.params.policyID, category.text));
+ const navigateToCategoriesSettings = () => {
+ Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES_SETTINGS.getRoute(route.params.policyID));
};
const navigateToCreateCategoryPage = () => {
Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_CREATE.getRoute(route.params.policyID));
};
- const isLoading = !isOffline && policyCategories === undefined;
+ const dismissError = (item: PolicyOption) => {
+ Policy.clearCategoryErrors(route.params.policyID, item.keyForList);
+ };
+
+ const selectedCategoriesArray = Object.keys(selectedCategories).filter((key) => selectedCategories[key]);
+
+ const getHeaderButtons = () => {
+ const options: Array>> = [];
+
+ if (selectedCategoriesArray.length > 0) {
+ options.push({
+ icon: Expensicons.Trashcan,
+ text: translate('workspace.categories.deleteCategories'),
+ value: CONST.POLICY.CATEGORIES_BULK_ACTION_TYPES.DELETE,
+ onSelected: () => {
+ setSelectedCategories({});
+ deleteWorkspaceCategories(route.params.policyID, selectedCategoriesArray);
+ },
+ });
+
+ const enabledCategories = selectedCategoriesArray.filter((categoryName) => policyCategories?.[categoryName].enabled);
+ if (enabledCategories.length > 0) {
+ const categoriesToDisable = selectedCategoriesArray
+ .filter((categoryName) => policyCategories?.[categoryName].enabled)
+ .reduce>((acc, categoryName) => {
+ acc[categoryName] = {
+ name: categoryName,
+ enabled: false,
+ };
+ return acc;
+ }, {});
- const headerButtons = (
-
- {!PolicyUtils.hasAccountingConnections(policy) && (
+ options.push({
+ icon: Expensicons.DocumentSlash,
+ text: translate('workspace.categories.disableCategories'),
+ value: CONST.POLICY.CATEGORIES_BULK_ACTION_TYPES.DISABLE,
+ onSelected: () => {
+ setSelectedCategories({});
+ setWorkspaceCategoryEnabled(route.params.policyID, categoriesToDisable);
+ },
+ });
+ }
+
+ const disabledCategories = selectedCategoriesArray.filter((categoryName) => !policyCategories?.[categoryName].enabled);
+ if (disabledCategories.length > 0) {
+ const categoriesToEnable = selectedCategoriesArray
+ .filter((categoryName) => !policyCategories?.[categoryName].enabled)
+ .reduce>((acc, categoryName) => {
+ acc[categoryName] = {
+ name: categoryName,
+ enabled: true,
+ };
+ return acc;
+ }, {});
+ options.push({
+ icon: Expensicons.Document,
+ text: translate('workspace.categories.enableCategories'),
+ value: CONST.POLICY.CATEGORIES_BULK_ACTION_TYPES.ENABLE,
+ onSelected: () => {
+ setSelectedCategories({});
+ setWorkspaceCategoryEnabled(route.params.policyID, categoriesToEnable);
+ },
+ });
+ }
+
+ return (
+ null}
+ shouldAlwaysShowDropdownMenu
+ pressOnEnter
+ buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
+ customText={translate('workspace.common.selected', {selectedNumber: selectedCategoriesArray.length})}
+ options={options}
+ style={[isSmallScreenWidth && styles.w50, isSmallScreenWidth && styles.mb3]}
+ />
+ );
+ }
+
+ return (
+
+ {!PolicyUtils.hasAccountingConnections(policy) && (
+
+ )}
- )}
-
-
- );
+
+ );
+ };
+
+ const isLoading = !isOffline && policyCategories === undefined;
return (
@@ -164,9 +267,9 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat
title={translate('workspace.common.categories')}
shouldShowBackButton={isSmallScreenWidth}
>
- {!isSmallScreenWidth && headerButtons}
+ {!isSmallScreenWidth && getHeaderButtons()}
- {isSmallScreenWidth && {headerButtons}}
+ {isSmallScreenWidth && {getHeaderButtons()}}
{translate('workspace.categories.subtitle')}
@@ -193,6 +296,7 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat
onSelectAll={toggleAllCategories}
showScrollIndicator
ListItem={TableListItem}
+ onDismissError={dismissError}
customListHeader={getCustomListHeader()}
listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]}
/>
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}
/>
;
+
+function WorkspaceTaxesPage({policy, route}: WorkspaceTaxesPageProps) {
+ const {isSmallScreenWidth} = useWindowDimensions();
+ const styles = useThemeStyles();
+ const theme = useTheme();
+ const {translate} = useLocalize();
+ const [selectedTaxesIDs, setSelectedTaxesIDs] = useState([]);
+ const defaultExternalID = policy?.taxRates?.defaultExternalID;
+ const foreignTaxDefault = policy?.taxRates?.foreignTaxDefault;
+
+ const fetchTaxes = () => {
+ openPolicyTaxesPage(route.params.policyID);
+ };
+
+ const {isOffline} = useNetwork({onReconnect: fetchTaxes});
+
+ useEffect(() => {
+ fetchTaxes();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const textForDefault = useCallback(
+ (taxID: string): string => {
+ if (taxID === defaultExternalID && taxID === foreignTaxDefault) {
+ return translate('common.default');
+ }
+ if (taxID === defaultExternalID) {
+ return translate('workspace.taxes.workspaceDefault');
+ }
+ if (taxID === foreignTaxDefault) {
+ return translate('workspace.taxes.foreignDefault');
+ }
+ return '';
+ },
+ [defaultExternalID, foreignTaxDefault, translate],
+ );
+
+ const taxesList = useMemo(
+ () =>
+ Object.entries(policy?.taxRates?.taxes ?? {})
+ .map(([key, value]) => ({
+ text: value.name,
+ alternateText: textForDefault(key),
+ keyForList: key,
+ isSelected: !!selectedTaxesIDs.includes(key),
+ isDisabledCheckbox: key === defaultExternalID,
+ rightElement: (
+
+
+ {value.isDisabled ? translate('workspace.common.disabled') : translate('workspace.common.enabled')}
+
+
+
+
+
+ ),
+ }))
+ .sort((a, b) => a.text.localeCompare(b.text)),
+ [policy?.taxRates?.taxes, textForDefault, defaultExternalID, selectedTaxesIDs, styles, theme.icon, translate],
+ );
+
+ const isLoading = !isOffline && taxesList === undefined;
+
+ const toggleTax = (tax: ListItem) => {
+ const key = tax.keyForList;
+ if (typeof key !== 'string') {
+ return;
+ }
+
+ setSelectedTaxesIDs((prev) => {
+ if (prev?.includes(key)) {
+ return prev.filter((item) => item !== key);
+ }
+ return [...prev, key];
+ });
+ };
+
+ const toggleAllTaxes = () => {
+ const taxesToSelect = taxesList.filter((tax) => tax.keyForList !== defaultExternalID);
+ setSelectedTaxesIDs((prev) => {
+ if (prev.length === taxesToSelect.length) {
+ return [];
+ }
+
+ return taxesToSelect.map((item) => (item.keyForList ? item.keyForList : ''));
+ });
+ };
+
+ const getCustomListHeader = () => (
+
+ {translate('common.name')}
+ {translate('statusPage.status')}
+
+ );
+
+ const headerButtons = (
+
+ {}}
+ icon={Expensicons.Plus}
+ text={translate('workspace.taxes.addRate')}
+ style={[styles.mr3, isSmallScreenWidth && styles.w50]}
+ />
+ {}}
+ icon={Expensicons.Gear}
+ text={translate('common.settings')}
+ style={[isSmallScreenWidth && styles.w50]}
+ />
+
+ );
+
+ return (
+
+
+
+
+ {!isSmallScreenWidth && headerButtons}
+
+
+ {isSmallScreenWidth && {headerButtons}}
+
+
+ {translate('workspace.taxes.subtitle')}
+
+ {isLoading && (
+
+ )}
+ {}}
+ onSelectAll={toggleAllTaxes}
+ showScrollIndicator
+ ListItem={TableListItem}
+ customListHeader={getCustomListHeader()}
+ listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]}
+ />
+
+
+
+ );
+}
+
+WorkspaceTaxesPage.displayName = 'WorkspaceTaxesPage';
+
+export default withPolicyAndFullscreenLoading(WorkspaceTaxesPage);
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 3cfb38ef4bab..3a0ddf446597 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,21 @@ const styles = (theme: ThemeColors) =>
buttonSmall: {
borderRadius: variables.buttonBorderRadius,
minHeight: variables.componentSizeSmall,
- paddingHorizontal: 14,
+ paddingHorizontal: 12,
backgroundColor: theme.buttonDefaultBG,
},
buttonMedium: {
borderRadius: variables.buttonBorderRadius,
minHeight: variables.componentSizeNormal,
- paddingRight: 16,
- paddingLeft: 16,
+ paddingHorizontal: 16,
backgroundColor: theme.buttonDefaultBG,
},
buttonLarge: {
borderRadius: variables.buttonBorderRadius,
minHeight: variables.componentSizeLarge,
- paddingRight: 10,
- paddingLeft: 10,
+ paddingHorizontal: 20,
backgroundColor: theme.buttonDefaultBG,
},
@@ -1443,8 +1441,8 @@ const styles = (theme: ThemeColors) =>
sidebarAvatar: {
borderRadius: 28,
- height: variables.componentSizeSmall,
- width: variables.componentSizeSmall,
+ height: 28,
+ width: 28,
},
selectedAvatarBorder: {
@@ -3098,7 +3096,6 @@ const styles = (theme: ThemeColors) =>
smallEditIcon: {
alignItems: 'center',
backgroundColor: theme.buttonDefaultBG,
- borderColor: theme.appBG,
borderRadius: 20,
borderWidth: 3,
color: theme.textReversed,
@@ -3107,6 +3104,14 @@ const styles = (theme: ThemeColors) =>
justifyContent: 'center',
},
+ smallEditIconWorkspace: {
+ borderColor: theme.cardBG,
+ },
+
+ smallEditIconAccount: {
+ borderColor: theme.appBG,
+ },
+
smallAvatarEditIcon: {
position: 'absolute',
right: -8,
diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts
index 31a19904b81c..f1141dd11e94 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,49 @@ function getWidthAndHeightStyle(width: number, height?: number): Pick {
+ switch (true) {
+ case small:
+ return {width: variables.iconSizeExtraSmall, height: variables.iconSizeExtraSmall};
+ case medium:
+ return {width: variables.iconSizeSmall, height: variables.iconSizeSmall};
+ case large:
+ return {width: variables.iconSizeNormal, height: variables.iconSizeNormal};
+ 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: {
+ return undefined;
+ }
+ }
+}
+
/**
* Combine margin/padding with safe area inset
*
@@ -1096,6 +1140,8 @@ const staticStyleUtils = {
getOpacityStyle,
getMultiGestureCanvasContainerStyle,
getSignInBgStyles,
+ getIconWidthAndHeightStyle,
+ getButtonStyleWithIcon,
};
const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({
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/Policy.ts b/src/types/onyx/Policy.ts
index ed30f4b7d201..688300933a5e 100644
--- a/src/types/onyx/Policy.ts
+++ b/src/types/onyx/Policy.ts
@@ -210,6 +210,7 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback<
/** The URL for the policy avatar */
avatar?: string;
+ avatarURL?: string;
/** Error objects keyed by field name containing errors keyed by microtime */
errorFields?: OnyxCommon.ErrorFields;
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/e2e/merge.js b/tests/e2e/merge.ts
similarity index 100%
rename from tests/e2e/merge.js
rename to tests/e2e/merge.ts
diff --git a/tests/e2e/nativeCommands/adbBackspace.js b/tests/e2e/nativeCommands/adbBackspace.ts
similarity index 87%
rename from tests/e2e/nativeCommands/adbBackspace.js
rename to tests/e2e/nativeCommands/adbBackspace.ts
index 7b01ed58080d..2891d1daf0e9 100644
--- a/tests/e2e/nativeCommands/adbBackspace.js
+++ b/tests/e2e/nativeCommands/adbBackspace.ts
@@ -1,7 +1,7 @@
import execAsync from '../utils/execAsync';
import * as Logger from '../utils/logger';
-const adbBackspace = async () => {
+const adbBackspace = () => {
Logger.log(`🔙 Pressing backspace`);
execAsync(`adb shell input keyevent KEYCODE_DEL`);
return true;
diff --git a/tests/e2e/nativeCommands/adbTypeText.js b/tests/e2e/nativeCommands/adbTypeText.ts
similarity index 85%
rename from tests/e2e/nativeCommands/adbTypeText.js
rename to tests/e2e/nativeCommands/adbTypeText.ts
index b8c6cfd09c29..72fefbd25d26 100644
--- a/tests/e2e/nativeCommands/adbTypeText.js
+++ b/tests/e2e/nativeCommands/adbTypeText.ts
@@ -1,7 +1,7 @@
import execAsync from '../utils/execAsync';
import * as Logger from '../utils/logger';
-const adbTypeText = async (text) => {
+const adbTypeText = (text: string) => {
Logger.log(`📝 Typing text: ${text}`);
execAsync(`adb shell input text "${text}"`);
return true;
diff --git a/tests/e2e/server/index.ts b/tests/e2e/server/index.ts
index 16f23fd325cb..34a19b52d97e 100644
--- a/tests/e2e/server/index.ts
+++ b/tests/e2e/server/index.ts
@@ -139,16 +139,15 @@ const createServerInstance = (): ServerInstance => {
case Routes.testNativeCommand: {
getPostJSONRequestData(req, res)
- ?.then((data) =>
- nativeCommands.executeFromPayload(data?.actionName, data?.payload).then((status) => {
- if (status) {
- res.end('ok');
- return;
- }
- res.statusCode = 500;
- res.end('Error executing command');
- }),
- )
+ ?.then((data) => {
+ const status = nativeCommands.executeFromPayload(data?.actionName, data?.payload);
+ if (status) {
+ res.end('ok');
+ return;
+ }
+ res.statusCode = 500;
+ res.end('Error executing command');
+ })
.catch((error) => {
Logger.error('Error executing command', error);
res.statusCode = 500;
diff --git a/tests/perf-test/GooglePlacesUtils.perf-test.js b/tests/perf-test/GooglePlacesUtils.perf-test.ts
similarity index 95%
rename from tests/perf-test/GooglePlacesUtils.perf-test.js
rename to tests/perf-test/GooglePlacesUtils.perf-test.ts
index 674fb4329205..1dd64feb23e0 100644
--- a/tests/perf-test/GooglePlacesUtils.perf-test.js
+++ b/tests/perf-test/GooglePlacesUtils.perf-test.ts
@@ -1,7 +1,8 @@
+/* eslint-disable @typescript-eslint/naming-convention */
import {measureFunction} from 'reassure';
-import * as GooglePlacesUtils from '../../src/libs/GooglePlacesUtils';
+import * as GooglePlacesUtils from '@src/libs/GooglePlacesUtils';
-const addressComponents = [
+const addressComponents: GooglePlacesUtils.AddressComponent[] = [
{
long_name: 'Bushwick',
short_name: 'Bushwick',
@@ -34,7 +35,7 @@ const addressComponents = [
},
];
-const bigObjectToFind = {
+const bigObjectToFind: GooglePlacesUtils.FieldsToExtract = {
sublocality: 'long_name',
administrative_area_level_1: 'short_name',
postal_code: 'long_name',
diff --git a/tests/unit/removeInvisibleCharacters.js b/tests/unit/removeInvisibleCharacters.ts
similarity index 98%
rename from tests/unit/removeInvisibleCharacters.js
rename to tests/unit/removeInvisibleCharacters.ts
index 98d1c7c71baf..8294b8cefc5f 100644
--- a/tests/unit/removeInvisibleCharacters.js
+++ b/tests/unit/removeInvisibleCharacters.ts
@@ -1,6 +1,5 @@
-import _ from 'underscore';
-import enEmojis from '../../assets/emojis/en';
-import StringUtils from '../../src/libs/StringUtils';
+import enEmojis from '@assets/emojis/en';
+import StringUtils from '@src/libs/StringUtils';
describe('libs/StringUtils.removeInvisibleCharacters', () => {
it('basic tests', () => {
@@ -80,7 +79,7 @@ describe('libs/StringUtils.removeInvisibleCharacters', () => {
expect(StringUtils.removeInvisibleCharacters('test😀😀😀')).toBe('test😀😀😀');
});
it('all emojis not removed', () => {
- _.keys(enEmojis).forEach((key) => {
+ Object.keys(enEmojis).forEach((key) => {
expect(StringUtils.removeInvisibleCharacters(key)).toBe(key);
});
});