diff --git a/android/app/build.gradle b/android/app/build.gradle
index cbfdb4b0b0a5..ed7b2f568b93 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 1001043902
- versionName "1.4.39-2"
+ versionCode 1001043907
+ versionName "1.4.39-7"
}
flavorDimensions "default"
diff --git a/assets/images/chatbubble-add.svg b/assets/images/chatbubble-add.svg
index 047a43073b3c..48eebf863cc3 100644
--- a/assets/images/chatbubble-add.svg
+++ b/assets/images/chatbubble-add.svg
@@ -1,13 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/chatbubble-unread.svg b/assets/images/chatbubble-unread.svg
index 9da789510276..492616cf2ab5 100644
--- a/assets/images/chatbubble-unread.svg
+++ b/assets/images/chatbubble-unread.svg
@@ -1,12 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/home.svg b/assets/images/home.svg
index 6b2411407be7..d4e02b723fee 100644
--- a/assets/images/home.svg
+++ b/assets/images/home.svg
@@ -1,3 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/images/olddot-wireframe.svg b/assets/images/olddot-wireframe.svg
index ee9aa93be255..055059edfd70 100644
--- a/assets/images/olddot-wireframe.svg
+++ b/assets/images/olddot-wireframe.svg
@@ -1,3422 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__gears.svg b/assets/images/simple-illustrations/simple-illustration__gears.svg
index 3b4cbc001e3b..2798feb4e04d 100644
--- a/assets/images/simple-illustrations/simple-illustration__gears.svg
+++ b/assets/images/simple-illustrations/simple-illustration__gears.svg
@@ -1,101 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__lockclosed.svg b/assets/images/simple-illustrations/simple-illustration__lockclosed.svg
index 3779b92b0b0f..791500c28032 100644
--- a/assets/images/simple-illustrations/simple-illustration__lockclosed.svg
+++ b/assets/images/simple-illustrations/simple-illustration__lockclosed.svg
@@ -1,17 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__palmtree.svg b/assets/images/simple-illustrations/simple-illustration__palmtree.svg
index 2aef4956cde9..c67e871dc434 100644
--- a/assets/images/simple-illustrations/simple-illustration__palmtree.svg
+++ b/assets/images/simple-illustrations/simple-illustration__palmtree.svg
@@ -1,15 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__profile.svg b/assets/images/simple-illustrations/simple-illustration__profile.svg
index 85312f26e186..085f02822bc0 100644
--- a/assets/images/simple-illustrations/simple-illustration__profile.svg
+++ b/assets/images/simple-illustrations/simple-illustration__profile.svg
@@ -1,6 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__qr-code.svg b/assets/images/simple-illustrations/simple-illustration__qr-code.svg
index 10268d747588..7bd460d5f4e9 100644
--- a/assets/images/simple-illustrations/simple-illustration__qr-code.svg
+++ b/assets/images/simple-illustrations/simple-illustration__qr-code.svg
@@ -1,4 +1 @@
-
+
\ No newline at end of file
diff --git a/desktop/main.js b/desktop/main.js
index e53f03530b57..4b38c5d36ab3 100644
--- a/desktop/main.js
+++ b/desktop/main.js
@@ -410,6 +410,14 @@ const mainWindow = () => {
browserWindow.webContents.goBack();
},
},
+ {
+ role: 'back',
+ visible: false,
+ accelerator: process.platform === 'darwin' ? 'Cmd+Left' : 'Shift+Left',
+ click: () => {
+ browserWindow.webContents.goBack();
+ },
+ },
{
id: 'forward',
role: 'forward',
@@ -418,6 +426,14 @@ const mainWindow = () => {
browserWindow.webContents.goForward();
},
},
+ {
+ role: 'forward',
+ visible: false,
+ accelerator: process.platform === 'darwin' ? 'Cmd+Right' : 'Shift+Right',
+ click: () => {
+ browserWindow.webContents.goForward();
+ },
+ },
],
},
{
diff --git a/docs/assets/images/info.svg b/docs/assets/images/info.svg
index 96924fbb6cf7..fbe9b3612667 100644
--- a/docs/assets/images/info.svg
+++ b/docs/assets/images/info.svg
@@ -1,9 +1 @@
-
+
\ No newline at end of file
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 0c53a62c88a6..4f571739ecc7 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.39.2
+ 1.4.39.7
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index ed74e6865d2f..bc29fdff60c2 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.4.39.2
+ 1.4.39.7
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 94c770eaf29f..b54ddc36ddf0 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
CFBundleShortVersionString
1.4.39
CFBundleVersion
- 1.4.39.2
+ 1.4.39.7
NSExtension
NSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index 694646334480..1cc630ce5dac 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.39-2",
+ "version": "1.4.39-7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.39-2",
+ "version": "1.4.39-7",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 0f856afb44e0..5870241897b9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.39-2",
+ "version": "1.4.39-7",
"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/patches/react-native-vision-camera+2.16.5.patch b/patches/react-native-vision-camera+2.16.5.patch
deleted file mode 100644
index d08f7c11f5f3..000000000000
--- a/patches/react-native-vision-camera+2.16.5.patch
+++ /dev/null
@@ -1,13 +0,0 @@
-diff --git a/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessorRuntimeManager.kt b/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessorRuntimeManager.kt
-index c0a8b23..653b51e 100644
---- a/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessorRuntimeManager.kt
-+++ b/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessorRuntimeManager.kt
-@@ -40,7 +40,7 @@ class FrameProcessorRuntimeManager(context: ReactApplicationContext, frameProces
- val holder = context.catalystInstance.jsCallInvokerHolder as CallInvokerHolderImpl
- mScheduler = VisionCameraScheduler(frameProcessorThread)
- mContext = WeakReference(context)
-- mHybridData = initHybrid(context.javaScriptContextHolder.get(), holder, mScheduler!!)
-+ mHybridData = initHybrid(context.javaScriptContextHolder!!.get(), holder, mScheduler!!)
- initializeRuntime()
-
- Log.i(TAG, "Installing JSI Bindings on JS Thread...")
diff --git a/patches/react-native-vision-camera+2.16.5+001+fix-boost-dependency.patch b/patches/react-native-vision-camera+2.16.8.patch
similarity index 100%
rename from patches/react-native-vision-camera+2.16.5+001+fix-boost-dependency.patch
rename to patches/react-native-vision-camera+2.16.8.patch
diff --git a/src/CONST.ts b/src/CONST.ts
index 79895d20aa57..eae4b8ec7a2b 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1506,7 +1506,7 @@ const CONST = {
GUIDES_CALL_TASK_IDS: {
CONCIERGE_DM: 'NewExpensifyConciergeDM',
WORKSPACE_INITIAL: 'WorkspaceHome',
- WORKSPACE_OVERVIEW: 'WorkspaceOverview',
+ WORKSPACE_PROFILE: 'WorkspaceProfile',
WORKSPACE_CARD: 'WorkspaceCorporateCards',
WORKSPACE_REIMBURSE: 'WorkspaceReimburseReceipts',
WORKSPACE_BILLS: 'WorkspacePayBills',
@@ -1567,6 +1567,10 @@ const CONST = {
FORM_CHARACTER_LIMIT: 50,
LEGAL_NAMES_CHARACTER_LIMIT: 150,
LOGIN_CHARACTER_LIMIT: 254,
+
+ TITLE_CHARACTER_LIMIT: 100,
+ DESCRIPTION_LIMIT: 500,
+
WORKSPACE_NAME_CHARACTER_LIMIT: 80,
AVATAR_CROP_MODAL: {
// The next two constants control what is min and max value of the image crop scale.
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index cbf4d7714967..b46d3db8b60d 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -465,7 +465,8 @@ type OnyxValues = {
[ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories;
[ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS]: OnyxTypes.PolicyReportFields;
[ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMembers;
- [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: Record;
+ [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: OnyxTypes.InvitedEmailsToAccountIDs | undefined;
+ [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT]: string | undefined;
[ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report;
[ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata;
[ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportActions;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 678dbe433417..4424c07765c6 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -436,17 +436,17 @@ const ROUTES = {
route: 'workspace/:policyID/invite-message',
getRoute: (policyID: string) => `workspace/${policyID}/invite-message` as const,
},
- WORKSPACE_OVERVIEW: {
- route: 'workspace/:policyID/overview',
- getRoute: (policyID: string) => `workspace/${policyID}/overview` as const,
+ WORKSPACE_PROFILE: {
+ route: 'workspace/:policyID/profile',
+ getRoute: (policyID: string) => `workspace/${policyID}/profile` as const,
},
- WORKSPACE_OVERVIEW_CURRENCY: {
- route: 'workspace/:policyID/overview/currency',
- getRoute: (policyID: string) => `workspace/${policyID}/overview/currency` as const,
+ WORKSPACE_PROFILE_CURRENCY: {
+ route: 'workspace/:policyID/profile/currency',
+ getRoute: (policyID: string) => `workspace/${policyID}/profile/currency` as const,
},
- WORKSPACE_OVERVIEW_NAME: {
- route: 'workspace/:policyID/overview/name',
- getRoute: (policyID: string) => `workspace/${policyID}/overview/name` as const,
+ WORKSPACE_PROFILE_NAME: {
+ route: 'workspace/:policyID/profile/name',
+ getRoute: (policyID: string) => `workspace/${policyID}/profile/name` as const,
},
WORKSPACE_AVATAR: {
route: 'workspace/:policyID/avatar',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index cd80937a3864..1d7c77bf129c 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -194,7 +194,7 @@ const SCREENS = {
WORKSPACE: {
INITIAL: 'Workspace_Initial',
- OVERVIEW: 'Workspace_Overview',
+ PROFILE: 'Workspace_Profile',
CARD: 'Workspace_Card',
REIMBURSE: 'Workspace_Reimburse',
RATE_AND_UNIT: 'Workspace_RateAndUnit',
@@ -204,8 +204,8 @@ const SCREENS = {
MEMBERS: 'Workspace_Members',
INVITE: 'Workspace_Invite',
INVITE_MESSAGE: 'Workspace_Invite_Message',
- CURRENCY: 'Workspace_Overview_Currency',
- NAME: 'Workspace_Overview_Name',
+ CURRENCY: 'Workspace_Profile_Currency',
+ NAME: 'Workspace_Profile_Name',
},
EDIT_REQUEST: {
diff --git a/src/components/AmountTextInput.tsx b/src/components/AmountTextInput.tsx
index 245aa2126d08..05080fcdd21c 100644
--- a/src/components/AmountTextInput.tsx
+++ b/src/components/AmountTextInput.tsx
@@ -43,7 +43,6 @@ function AmountTextInput(
disableKeyboard
autoGrow
hideFocusedState
- shouldInterceptSwipe
inputStyle={[styles.iouAmountTextInput, styles.p0, styles.noLeftBorderRadius, styles.noRightBorderRadius, style]}
textInputContainerStyles={[styles.borderNone, styles.noLeftBorderRadius, styles.noRightBorderRadius]}
onChangeText={onChangeAmount}
diff --git a/src/components/ContextMenuItem.tsx b/src/components/ContextMenuItem.tsx
index d292f80d135e..d6c8fd973983 100644
--- a/src/components/ContextMenuItem.tsx
+++ b/src/components/ContextMenuItem.tsx
@@ -1,6 +1,6 @@
import type {ForwardedRef} from 'react';
import React, {forwardRef, useImperativeHandle} from 'react';
-import type {GestureResponderEvent} from 'react-native';
+import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import useThrottledButtonState from '@hooks/useThrottledButtonState';
@@ -41,6 +41,9 @@ type ContextMenuItemProps = {
/** Whether the width should be limited */
shouldLimitWidth?: boolean;
+
+ /** Styles to apply to ManuItem wrapper */
+ wrapperStyle?: StyleProp;
};
type ContextMenuItemHandle = {
@@ -48,7 +51,19 @@ type ContextMenuItemHandle = {
};
function ContextMenuItem(
- {onPress, successIcon, successText = '', icon, text, isMini = false, description = '', isAnonymousAction = false, isFocused = false, shouldLimitWidth = true}: ContextMenuItemProps,
+ {
+ onPress,
+ successIcon,
+ successText = '',
+ icon,
+ text,
+ isMini = false,
+ description = '',
+ isAnonymousAction = false,
+ isFocused = false,
+ shouldLimitWidth = true,
+ wrapperStyle,
+ }: ContextMenuItemProps,
ref: ForwardedRef,
) {
const styles = useThemeStyles();
@@ -93,7 +108,7 @@ function ContextMenuItem(
title={itemText}
icon={itemIcon}
onPress={triggerPressAndUpdateSuccess}
- wrapperStyle={styles.pr9}
+ wrapperStyle={[styles.pr9, wrapperStyle]}
success={!isThrottledButtonActive}
description={description}
descriptionTextStyle={styles.breakWord}
diff --git a/src/components/IFrame.tsx b/src/components/IFrame.tsx
index ab27597aeebd..05da3a1edb9c 100644
--- a/src/components/IFrame.tsx
+++ b/src/components/IFrame.tsx
@@ -37,7 +37,7 @@ function getNewDotURL(url: string): string {
if (pathname === 'policy') {
const workspaceID = params.policyID || '';
- const section = urlObj.hash.slice(1) || 'overview';
+ const section = urlObj.hash.slice(1) || 'profile';
return `workspace/${workspaceID}/${section}`;
}
diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx
index 133996fde41c..f0cd8dc1b4b5 100644
--- a/src/components/ReportActionItem/MoneyReportView.tsx
+++ b/src/components/ReportActionItem/MoneyReportView.tsx
@@ -9,7 +9,6 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback';
import SpacerView from '@components/SpacerView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
-import usePermissions from '@hooks/usePermissions';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -42,7 +41,6 @@ function MoneyReportView({report, policy, policyReportFields, shouldShowHorizont
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const {isSmallScreenWidth} = useWindowDimensions();
- const {canUseReportFields} = usePermissions();
const isSettled = ReportUtils.isSettled(report.reportID);
const isTotalUpdated = ReportUtils.hasUpdatedTotal(report);
@@ -69,7 +67,7 @@ function MoneyReportView({report, policy, policyReportFields, shouldShowHorizont
- {canUseReportFields &&
+ {ReportUtils.reportFieldsEnabled(report) &&
sortedPolicyReportFields.map((reportField) => {
const isTitleField = ReportUtils.isReportFieldOfTypeTitle(reportField);
const fieldValue = isTitleField ? report.reportName : reportField.value ?? reportField.defaultValue;
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index 3fa6f1872ca3..f0956da948c9 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -110,6 +110,7 @@ function MoneyRequestView({
const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && CurrencyUtils.convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency);
const isCardTransaction = TransactionUtils.isCardTransaction(transaction);
const cardProgramName = isCardTransaction && transactionCardID !== undefined ? CardUtils.getCardDescription(transactionCardID) : '';
+ const isApproved = ReportUtils.isReportApproved(moneyRequestReport);
// Flags for allowing or disallowing editing a money request
const isSettled = ReportUtils.isSettled(moneyRequestReport?.reportID);
@@ -173,7 +174,7 @@ function MoneyRequestView({
if (!isDistanceRequest) {
amountDescription += ` • ${translate('iou.cash')}`;
}
- if (ReportUtils.isReportApproved(report)) {
+ if (isApproved) {
amountDescription += ` • ${translate('iou.approved')}`;
} else if (isCancelled) {
amountDescription += ` • ${translate('iou.canceled')}`;
@@ -267,13 +268,7 @@ function MoneyRequestView({
titleStyle={styles.flex1}
onPress={() =>
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(
- CONST.IOU.ACTION.EDIT,
- CONST.IOU.TYPE.REQUEST,
- transaction?.transactionID ?? '',
- report.reportID,
- Navigation.getActiveRouteWithoutParams(),
- ),
+ ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(CONST.IOU.ACTION.EDIT, CONST.IOU.TYPE.REQUEST, transaction?.transactionID ?? '', report.reportID),
)
}
wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index b8a92820e6c0..be871971ee92 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -55,7 +55,7 @@ type User = {
login?: string;
/** Element to show on the right side of the item */
- rightElement?: ReactElement;
+ rightElement?: ReactNode;
/** Icons for the user (can be multiple if it's a Workspace) */
icons?: Icon[];
@@ -129,6 +129,9 @@ type Section = {
/** Whether this section items disabled for selection */
isDisabled?: boolean;
+
+ /** Whether this section should be shown or not */
+ shouldShow?: boolean;
};
type BaseSelectionListProps = Partial & {
diff --git a/src/languages/en.ts b/src/languages/en.ts
index cd1797d78220..f24b0e3e2438 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -719,6 +719,10 @@ export default {
subtitle: 'These details are used for travel and payments. They are never shown on your public profile.',
},
},
+ shareCodePage: {
+ title: 'Your code',
+ subtitle: 'Invite members to Expensify by sharing your personal QR code or referral link.',
+ },
loungeAccessPage: {
loungeAccess: 'Lounge access',
headline: 'The Expensify Lounge is closed.',
@@ -1539,7 +1543,7 @@ export default {
travel: 'Travel',
members: 'Members',
plan: 'Plan',
- overview: 'Overview',
+ profile: 'Profile',
bankAccount: 'Bank account',
connectBankAccount: 'Connect bank account',
testTransactions: 'Test transactions',
@@ -1679,7 +1683,6 @@ export default {
nameInputLabel: 'Name',
nameInputHelpText: 'This is the name you will see on your workspace.',
nameIsRequiredError: 'You need to define a name for your workspace.',
- nameIsTooLongError: `Your workspace name can be at most ${CONST.WORKSPACE_NAME_CHARACTER_LIMIT} characters long.`,
currencyInputLabel: 'Default currency',
currencyInputHelpText: 'All expenses on this workspace will be converted to this currency.',
currencyInputDisabledText: "The default currency can't be changed because this workspace is linked to a USD bank account.",
@@ -1984,7 +1987,7 @@ export default {
parentNavigationSummary: ({rootReportName, workspaceName}: ParentNavigationSummaryParams) => `From ${rootReportName}${workspaceName ? ` in ${workspaceName}` : ''}`,
},
qrCodes: {
- copy: 'Copy',
+ copy: 'Copy URL',
copied: 'Copied!',
},
moderation: {
diff --git a/src/languages/es.ts b/src/languages/es.ts
index f61abda6cd4d..3238e7fa94b0 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -713,6 +713,10 @@ export default {
subtitle: 'Estos detalles se utilizan para viajes y pagos. Nunca se mostrarán en tu perfil público.',
},
},
+ shareCodePage: {
+ title: 'Tu código',
+ subtitle: 'Invita a miembros a Expensify compartiendo tu código QR personal o enlace de invitación.',
+ },
loungeAccessPage: {
loungeAccess: 'Acceso a la sala vip',
headline: 'La sala vip de Expensify está cerrada.',
@@ -1561,7 +1565,7 @@ export default {
travel: 'Viajes',
members: 'Miembros',
plan: 'Plan',
- overview: 'Descripción',
+ profile: 'Perfil',
bankAccount: 'Cuenta bancaria',
connectBankAccount: 'Conectar cuenta bancaria',
testTransactions: 'Transacciones de prueba',
@@ -1702,7 +1706,6 @@ export default {
nameInputLabel: 'Nombre',
nameInputHelpText: 'Este es el nombre que verás en tu espacio de trabajo.',
nameIsRequiredError: 'Debes definir un nombre para tu espacio de trabajo.',
- nameIsTooLongError: `El nombre de su espacio de trabajo no puede tener más de ${CONST.WORKSPACE_NAME_CHARACTER_LIMIT} caracteres.`,
currencyInputLabel: 'Moneda por defecto',
currencyInputHelpText: 'Todas los gastos en este espacio de trabajo serán convertidos a esta moneda.',
currencyInputDisabledText: 'La moneda predeterminada no se puede cambiar porque este espacio de trabajo está vinculado a una cuenta bancaria en USD.',
@@ -2470,7 +2473,7 @@ export default {
parentNavigationSummary: ({rootReportName, workspaceName}: ParentNavigationSummaryParams) => `De ${rootReportName}${workspaceName ? ` en ${workspaceName}` : ''}`,
},
qrCodes: {
- copy: 'Copiar',
+ copy: 'Copiar URL',
copied: '¡Copiado!',
},
actionableMentionWhisperOptions: {
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
index c7be135e8b57..b0ec6d1f3a94 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
@@ -242,7 +242,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/WorkspaceInvitePage').default as React.ComponentType,
[SCREENS.WORKSPACE.INVITE_MESSAGE]: () => require('../../../pages/workspace/WorkspaceInviteMessagePage').default as React.ComponentType,
[SCREENS.WORKSPACE.NAME]: () => require('../../../pages/workspace/WorkspaceNamePage').default as React.ComponentType,
- [SCREENS.WORKSPACE.CURRENCY]: () => require('../../../pages/workspace/WorkspaceOverviewCurrencyPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.CURRENCY]: () => require('../../../pages/workspace/WorkspaceProfileCurrencyPage').default as React.ComponentType,
[SCREENS.REIMBURSEMENT_ACCOUNT]: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default as React.ComponentType,
[SCREENS.GET_ASSISTANCE]: () => require('../../../pages/GetAssistancePage').default as React.ComponentType,
[SCREENS.SETTINGS.TWO_FACTOR_AUTH]: () => require('../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').default as React.ComponentType,
diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx
index 53928b71be4e..087e963b3892 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx
@@ -15,7 +15,7 @@ type Screens = Partial React.C
const workspaceSettingsScreens = {
[SCREENS.SETTINGS.WORKSPACES]: () => require('../../../../../pages/workspace/WorkspacesListPage').default as React.ComponentType,
- [SCREENS.WORKSPACE.OVERVIEW]: () => require('../../../../../pages/workspace/WorkspaceOverviewPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.PROFILE]: () => require('../../../../../pages/workspace/WorkspaceProfilePage').default as React.ComponentType,
[SCREENS.WORKSPACE.CARD]: () => require('../../../../../pages/workspace/card/WorkspaceCardPage').default as React.ComponentType,
[SCREENS.WORKSPACE.REIMBURSE]: () => require('../../../../../pages/workspace/reimburse/WorkspaceReimbursePage').default as React.ComponentType,
[SCREENS.WORKSPACE.BILLS]: () => require('../../../../../pages/workspace/bills/WorkspaceBillsPage').default as React.ComponentType,
diff --git a/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts b/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts
index a22185422a11..b0825b4b2991 100644
--- a/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts
+++ b/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts
@@ -26,9 +26,9 @@ type GetPartialStateDiffReturnType = {
* This function returns partial additive diff between the two states.
*
* Example: Let's start with state A on route /r/123. If the screen is wide we will have a HOME opened on bottom tab and REPORT on central pane.
- * Now let's say we want to navigate to /workspace/345/overview. We will generate state B from this path.
- * State B will have WORKSPACE_INITIAL on the bottom tab and WORKSPACE_OVERVIEW on the central pane.
- * Now we will generate partial diff between state A and state B. The diff will tell us that we need to push WORKSPACE_INITIAL on the bottom tab and WORKSPACE_OVERVIEW on the central pane.
+ * Now let's say we want to navigate to /workspace/345/profile. We will generate state B from this path.
+ * State B will have WORKSPACE_INITIAL on the bottom tab and WORKSPACE_PROFILE on the central pane.
+ * Now we will generate partial diff between state A and state B. The diff will tell us that we need to push WORKSPACE_INITIAL on the bottom tab and WORKSPACE_PROFILE on the central pane.
*
* Then we can generate actions from this diff and dispatch them to the linkTo function.
*
diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
index d61b36871434..d96ad416832d 100755
--- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
@@ -2,7 +2,7 @@ import type {CentralPaneName} from '@libs/Navigation/types';
import SCREENS from '@src/SCREENS';
const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = {
- [SCREENS.WORKSPACE.OVERVIEW]: [SCREENS.WORKSPACE.NAME, SCREENS.WORKSPACE.CURRENCY],
+ [SCREENS.WORKSPACE.PROFILE]: [SCREENS.WORKSPACE.NAME, SCREENS.WORKSPACE.CURRENCY],
[SCREENS.WORKSPACE.REIMBURSE]: [SCREENS.WORKSPACE.RATE_AND_UNIT],
[SCREENS.WORKSPACE.MEMBERS]: [SCREENS.WORKSPACE.INVITE, SCREENS.WORKSPACE.INVITE_MESSAGE],
};
diff --git a/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts b/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts
index 3344cffe94ae..446fb479ea09 100755
--- a/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts
@@ -5,7 +5,7 @@ const TAB_TO_CENTRAL_PANE_MAPPING: Record = {
[SCREENS.HOME]: [SCREENS.REPORT],
[SCREENS.ALL_SETTINGS]: [SCREENS.SETTINGS.WORKSPACES],
[SCREENS.WORKSPACE.INITIAL]: [
- SCREENS.WORKSPACE.OVERVIEW,
+ SCREENS.WORKSPACE.PROFILE,
SCREENS.WORKSPACE.CARD,
SCREENS.WORKSPACE.REIMBURSE,
SCREENS.WORKSPACE.BILLS,
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 12577e360784..ab218adc3879 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -42,7 +42,7 @@ const config: LinkingOptions['config'] = {
[SCREENS.REPORT]: ROUTES.REPORT_WITH_ID.route,
[SCREENS.SETTINGS.WORKSPACES]: ROUTES.SETTINGS_WORKSPACES,
- [SCREENS.WORKSPACE.OVERVIEW]: ROUTES.WORKSPACE_OVERVIEW.route,
+ [SCREENS.WORKSPACE.PROFILE]: ROUTES.WORKSPACE_PROFILE.route,
[SCREENS.WORKSPACE.CARD]: {
path: ROUTES.WORKSPACE_CARD.route,
},
@@ -224,7 +224,7 @@ const config: LinkingOptions['config'] = {
path: ROUTES.SETTINGS_STATUS_CLEAR_AFTER_TIME,
},
[SCREENS.WORKSPACE.CURRENCY]: {
- path: ROUTES.WORKSPACE_OVERVIEW_CURRENCY.route,
+ path: ROUTES.WORKSPACE_PROFILE_CURRENCY.route,
},
[SCREENS.WORKSPACE.RATE_AND_UNIT]: {
path: ROUTES.WORKSPACE_RATE_AND_UNIT.route,
@@ -245,7 +245,7 @@ const config: LinkingOptions['config'] = {
[SCREENS.KEYBOARD_SHORTCUTS]: {
path: ROUTES.KEYBOARD_SHORTCUTS,
},
- [SCREENS.WORKSPACE.NAME]: ROUTES.WORKSPACE_OVERVIEW_NAME.route,
+ [SCREENS.WORKSPACE.NAME]: ROUTES.WORKSPACE_PROFILE_NAME.route,
},
},
[SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: {
diff --git a/src/libs/Navigation/switchPolicyID.ts b/src/libs/Navigation/switchPolicyID.ts
index 9afb325eee99..72a7c3e32fb4 100644
--- a/src/libs/Navigation/switchPolicyID.ts
+++ b/src/libs/Navigation/switchPolicyID.ts
@@ -122,9 +122,9 @@ export default function switchPolicyID(navigation: NavigationContainerRef): MemberForList;
+function formatMemberForList(member: null | undefined, config?: Partial): undefined;
+function formatMemberForList(member: ReportUtils.OptionData | null | undefined, config: Partial = {}): MemberForList | undefined {
if (!member) {
return undefined;
}
- const accountID = member.accountID;
+ const accountID = member.accountID ?? undefined;
return {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
@@ -2022,3 +2024,5 @@ export {
formatSectionsFromSearchTerm,
transformedTaxRates,
};
+
+export type {MemberForList};
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 747c44a60e0c..26280f95447d 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -1889,6 +1889,13 @@ function isReportFieldOfTypeTitle(reportField: OnyxEntry): bo
return reportField?.type === 'formula' && reportField?.fieldID === CONST.REPORT_FIELD_TITLE_FIELD_ID;
}
+/**
+ * Check if report fields are available to use in a report
+ */
+function reportFieldsEnabled(report: Report) {
+ return Permissions.canUseReportFields(allBetas ?? []) && isPaidGroupPolicyExpenseReport(report);
+}
+
/**
* Given a report field, check if the field can be edited or not.
* For title fields, its considered disabled if `deletable` prop is `true` (https://github.com/Expensify/App/issues/35043#issuecomment-1911275433)
@@ -1946,7 +1953,7 @@ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry<
const reportFields = isReportSettled ? report?.reportFields : getReportFieldsByPolicyID(report?.policyID ?? '');
const titleReportField = getFormulaTypeReportField(reportFields ?? {});
- if (titleReportField && report?.reportName && Permissions.canUseReportFields(allBetas ?? [])) {
+ if (titleReportField && report?.reportName && reportFieldsEnabled(report)) {
return report.reportName;
}
@@ -5034,6 +5041,7 @@ export {
hasUpdatedTotal,
isReportFieldDisabled,
getAvailableReportFields,
+ reportFieldsEnabled,
getAllAncestorReportActionIDs,
};
diff --git a/src/libs/SessionUtils.ts b/src/libs/SessionUtils.ts
index c73513c747af..52521d5146cc 100644
--- a/src/libs/SessionUtils.ts
+++ b/src/libs/SessionUtils.ts
@@ -4,7 +4,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
/**
* Determine if the transitioning user is logging in as a new user.
*/
-function isLoggingInAsNewUser(transitionURL: string, sessionEmail: string): boolean {
+function isLoggingInAsNewUser(transitionURL?: string, sessionEmail?: string): boolean {
// The OldDot mobile app does not URL encode the parameters, but OldDot web
// does. We don't want to deploy OldDot mobile again, so as a work around we
// compare the session email to both the decoded and raw email from the transition link.
@@ -20,7 +20,7 @@ function isLoggingInAsNewUser(transitionURL: string, sessionEmail: string): bool
// If they do not match it might be due to encoding, so check the raw value
// Capture the un-encoded text in the email param
const emailParamRegex = /[?&]email=([^&]*)/g;
- const matches = emailParamRegex.exec(transitionURL);
+ const matches = emailParamRegex.exec(transitionURL ?? '');
const linkedEmail = matches?.[1] ?? null;
return linkedEmail !== sessionEmail;
}
diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts
index 866206895d5e..498ce6918509 100644
--- a/src/libs/actions/Policy.ts
+++ b/src/libs/actions/Policy.ts
@@ -35,7 +35,19 @@ import * as ReportUtils from '@libs/ReportUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {PersonalDetailsList, Policy, PolicyMember, PolicyTags, RecentlyUsedCategories, RecentlyUsedTags, ReimbursementAccount, Report, ReportAction, Transaction} from '@src/types/onyx';
+import type {
+ InvitedEmailsToAccountIDs,
+ PersonalDetailsList,
+ Policy,
+ PolicyMember,
+ PolicyTags,
+ RecentlyUsedCategories,
+ RecentlyUsedTags,
+ ReimbursementAccount,
+ Report,
+ ReportAction,
+ Transaction,
+} from '@src/types/onyx';
import type {Errors} from '@src/types/onyx/OnyxCommon';
import type {CustomUnit} from '@src/types/onyx/Policy';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
@@ -518,7 +530,7 @@ function removeMembers(accountIDs: number[], policyID: string) {
*
* @returns - object with onyxSuccessData, onyxOptimisticData, and optimisticReportIDs (map login to reportID)
*/
-function createPolicyExpenseChats(policyID: string, invitedEmailsToAccountIDs: Record, hasOutstandingChildRequest = false): WorkspaceMembersChats {
+function createPolicyExpenseChats(policyID: string, invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs, hasOutstandingChildRequest = false): WorkspaceMembersChats {
const workspaceMembersChats: WorkspaceMembersChats = {
onyxSuccessData: [],
onyxOptimisticData: [],
@@ -607,7 +619,7 @@ function createPolicyExpenseChats(policyID: string, invitedEmailsToAccountIDs: R
/**
* Adds members to the specified workspace/policyID
*/
-function addMembersToWorkspace(invitedEmailsToAccountIDs: Record, welcomeNote: string, policyID: string) {
+function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs, welcomeNote: string, policyID: string) {
const membersListKey = `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}` as const;
const logins = Object.keys(invitedEmailsToAccountIDs).map((memberLogin) => OptionsListUtils.addSMSDomainIfPhoneNumber(memberLogin));
const accountIDs = Object.values(invitedEmailsToAccountIDs);
@@ -1499,7 +1511,7 @@ function openDraftWorkspaceRequest(policyID: string) {
API.read(READ_COMMANDS.OPEN_DRAFT_WORKSPACE_REQUEST, params);
}
-function setWorkspaceInviteMembersDraft(policyID: string, invitedEmailsToAccountIDs: Record) {
+function setWorkspaceInviteMembersDraft(policyID: string, invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs) {
Onyx.set(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${policyID}`, invitedEmailsToAccountIDs);
}
diff --git a/src/libs/actions/Welcome.ts b/src/libs/actions/Welcome.ts
index 3f6b2dc99a8f..952d19117679 100644
--- a/src/libs/actions/Welcome.ts
+++ b/src/libs/actions/Welcome.ts
@@ -136,11 +136,13 @@ function show(routes: NavigationState['routes'], showEngagem
const transitionRoute = routes.find(
(route): route is NavigationState>['routes'][number] => route.name === SCREENS.TRANSITION_BETWEEN_APPS,
);
+ const activeRoute = Navigation.getActiveRouteWithoutParams();
+ const isOnWorkspaceOverviewPage = activeRoute?.startsWith('/workspace') && activeRoute?.endsWith('/overview');
const isExitingToWorkspaceRoute = transitionRoute?.params?.exitTo === 'workspace/new';
// If we already opened the workspace settings or want the admin room to stay open, do not
// navigate away to the workspace chat report
- const shouldNavigateToWorkspaceChat = !isExitingToWorkspaceRoute;
+ const shouldNavigateToWorkspaceChat = !isExitingToWorkspaceRoute && !isOnWorkspaceOverviewPage;
const workspaceChatReport = Object.values(allReports ?? {}).find((report) => {
if (report) {
diff --git a/src/pages/LogOutPreviousUserPage.js b/src/pages/LogOutPreviousUserPage.js
deleted file mode 100644
index 0d1bf4f47025..000000000000
--- a/src/pages/LogOutPreviousUserPage.js
+++ /dev/null
@@ -1,96 +0,0 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
-import React, {useContext, useEffect} from 'react';
-import {Linking, NativeModules} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
-import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
-import InitialUrlContext from '@libs/InitialUrlContext';
-import Log from '@libs/Log';
-import Navigation from '@libs/Navigation/Navigation';
-import * as SessionUtils from '@libs/SessionUtils';
-import * as Session from '@userActions/Session';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
-
-const propTypes = {
- /** The details about the account that the user is signing in with */
- account: PropTypes.shape({
- /** Whether the account data is loading */
- isLoading: PropTypes.bool,
- }),
-
- /** The data about the current session which will be set once the user is authenticated and we return to this component as an AuthScreen */
- session: PropTypes.shape({
- /** The user's email for the current session */
- email: PropTypes.string,
- }),
-};
-
-const defaultProps = {
- account: {
- isLoading: false,
- },
- session: {
- email: null,
- },
-};
-
-// This page is responsible for handling transitions from OldDot. Specifically, it logs the current user
-// out if the transition is for another user.
-//
-// This component should not do any other navigation as that handled in App.setUpPoliciesAndNavigate
-function LogOutPreviousUserPage(props) {
- const initUrl = useContext(InitialUrlContext);
- useEffect(() => {
- Linking.getInitialURL().then((url) => {
- const sessionEmail = props.session.email;
- const transitionUrl = NativeModules.HybridAppModule ? CONST.DEEPLINK_BASE_URL + initUrl : url;
- const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(transitionUrl, sessionEmail);
-
- if (isLoggingInAsNewUser) {
- Session.signOutAndRedirectToSignIn();
- }
-
- // We need to signin and fetch a new authToken, if a user was already authenticated in NewDot, and was redirected to OldDot
- // and their authToken stored in Onyx becomes invalid.
- // This workflow is triggered while setting up VBBA. User is redirected from NewDot to OldDot to set up 2FA, and then redirected back to NewDot
- // On Enabling 2FA, authToken stored in Onyx becomes expired and hence we need to fetch new authToken
- const shouldForceLogin = lodashGet(props, 'route.params.shouldForceLogin', '') === 'true';
- if (shouldForceLogin) {
- Log.info('LogOutPreviousUserPage - forcing login with shortLivedAuthToken');
- const email = lodashGet(props, 'route.params.email', '');
- const shortLivedAuthToken = lodashGet(props, 'route.params.shortLivedAuthToken', '');
- Session.signInWithShortLivedAuthToken(email, shortLivedAuthToken);
- }
-
- const exitTo = lodashGet(props, 'route.params.exitTo', '');
- // We don't want to navigate to the exitTo route when creating a new workspace from a deep link,
- // because we already handle creating the optimistic policy and navigating to it in App.setUpPoliciesAndNavigate,
- // which is already called when AuthScreens mounts.
- if (exitTo && exitTo !== ROUTES.WORKSPACE_NEW && !props.account.isLoading && !isLoggingInAsNewUser) {
- Navigation.isNavigationReady().then(() => {
- // remove this screen and navigate to exit route
- const exitUrl = NativeModules.HybridAppModule ? Navigation.parseHybridAppUrl(exitTo) : exitTo;
- Navigation.goBack();
- Navigation.navigate(exitUrl);
- });
- }
- });
- }, [initUrl, props]);
-
- return ;
-}
-
-LogOutPreviousUserPage.propTypes = propTypes;
-LogOutPreviousUserPage.defaultProps = defaultProps;
-LogOutPreviousUserPage.displayName = 'LogOutPreviousUserPage';
-
-export default withOnyx({
- account: {
- key: ONYXKEYS.ACCOUNT,
- },
- session: {
- key: ONYXKEYS.SESSION,
- },
-})(LogOutPreviousUserPage);
diff --git a/src/pages/LogOutPreviousUserPage.tsx b/src/pages/LogOutPreviousUserPage.tsx
new file mode 100644
index 000000000000..f68344604dfa
--- /dev/null
+++ b/src/pages/LogOutPreviousUserPage.tsx
@@ -0,0 +1,60 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useEffect} from 'react';
+import {Linking} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
+import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
+import * as SessionUtils from '@libs/SessionUtils';
+import type {AuthScreensParamList} from '@navigation/types';
+import * as SessionActions from '@userActions/Session';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type SCREENS from '@src/SCREENS';
+import type {Session} from '@src/types/onyx';
+
+type LogOutPreviousUserPageOnyxProps = {
+ /** The data about the current session which will be set once the user is authenticated and we return to this component as an AuthScreen */
+ session: OnyxEntry;
+};
+
+type LogOutPreviousUserPageProps = LogOutPreviousUserPageOnyxProps & StackScreenProps;
+
+// This page is responsible for handling transitions from OldDot. Specifically, it logs the current user
+// out if the transition is for another user.
+//
+// This component should not do any other navigation as that handled in App.setUpPoliciesAndNavigate
+function LogOutPreviousUserPage({session, route}: LogOutPreviousUserPageProps) {
+ useEffect(() => {
+ Linking.getInitialURL().then((transitionURL) => {
+ const sessionEmail = session?.email;
+ const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(transitionURL ?? undefined, sessionEmail);
+
+ if (isLoggingInAsNewUser) {
+ SessionActions.signOutAndRedirectToSignIn();
+ }
+
+ // We need to signin and fetch a new authToken, if a user was already authenticated in NewDot, and was redirected to OldDot
+ // and their authToken stored in Onyx becomes invalid.
+ // This workflow is triggered while setting up VBBA. User is redirected from NewDot to OldDot to set up 2FA, and then redirected back to NewDot
+ // On Enabling 2FA, authToken stored in Onyx becomes expired and hence we need to fetch new authToken
+ const shouldForceLogin = route.params.shouldForceLogin === 'true';
+ if (shouldForceLogin) {
+ const email = route.params.email ?? '';
+ const shortLivedAuthToken = route.params.shortLivedAuthToken ?? '';
+ SessionActions.signInWithShortLivedAuthToken(email, shortLivedAuthToken);
+ }
+ });
+
+ // We only want to run this effect once on mount (when the page first loads after transitioning from OldDot)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return ;
+}
+
+LogOutPreviousUserPage.displayName = 'LogOutPreviousUserPage';
+
+export default withOnyx({
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+})(LogOutPreviousUserPage);
diff --git a/src/pages/OnboardEngagement/PurposeForUsingExpensifyPage.tsx b/src/pages/OnboardEngagement/PurposeForUsingExpensifyPage.tsx
index 66d25da28f9d..025dcafd9740 100644
--- a/src/pages/OnboardEngagement/PurposeForUsingExpensifyPage.tsx
+++ b/src/pages/OnboardEngagement/PurposeForUsingExpensifyPage.tsx
@@ -90,7 +90,7 @@ function PurposeForUsingExpensifyModal() {
}
Report.completeEngagementModal(message, choice);
- Report.navigateToConciergeChat();
+ Report.navigateToConciergeChat(false, true);
}, []);
const menuItems: MenuItemProps[] = useMemo(
diff --git a/src/pages/RoomInvitePage.js b/src/pages/RoomInvitePage.js
index 3f55ad6c0ff7..9c43a1820aa9 100644
--- a/src/pages/RoomInvitePage.js
+++ b/src/pages/RoomInvitePage.js
@@ -86,7 +86,7 @@ function RoomInvitePage(props) {
// Update selectedOptions with the latest personalDetails information
const detailsMap = {};
- _.forEach(inviteOptions.personalDetails, (detail) => (detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail, false)));
+ _.forEach(inviteOptions.personalDetails, (detail) => (detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail)));
const newSelectedOptions = [];
_.forEach(selectedOptions, (option) => {
newSelectedOptions.push(_.has(detailsMap, option.login) ? {...detailsMap[option.login], isSelected: true} : option);
@@ -142,7 +142,7 @@ function RoomInvitePage(props) {
// Filtering out selected users from the search results
const selectedLogins = _.map(selectedOptions, ({login}) => login);
const personalDetailsWithoutSelected = _.filter(personalDetails, ({login}) => !_.contains(selectedLogins, login));
- const personalDetailsFormatted = _.map(personalDetailsWithoutSelected, (personalDetail) => OptionsListUtils.formatMemberForList(personalDetail, false));
+ const personalDetailsFormatted = _.map(personalDetailsWithoutSelected, (personalDetail) => OptionsListUtils.formatMemberForList(personalDetail));
const hasUnselectedUserToInvite = userToInvite && !_.contains(selectedLogins, userToInvite.login);
sectionsArr.push({
@@ -156,7 +156,7 @@ function RoomInvitePage(props) {
if (hasUnselectedUserToInvite) {
sectionsArr.push({
title: undefined,
- data: [OptionsListUtils.formatMemberForList(userToInvite, false)],
+ data: [OptionsListUtils.formatMemberForList(userToInvite)],
shouldShow: true,
indexOffset,
});
diff --git a/src/pages/ShareCodePage.tsx b/src/pages/ShareCodePage.tsx
index 1752c8d4a1a3..7df92875b0ac 100644
--- a/src/pages/ShareCodePage.tsx
+++ b/src/pages/ShareCodePage.tsx
@@ -11,6 +11,7 @@ import MenuItem from '@components/MenuItem';
import QRShareWithDownload from '@components/QRShare/QRShareWithDownload';
import type QRShareWithDownloadHandle from '@components/QRShare/QRShareWithDownload/types';
import ScreenWrapper from '@components/ScreenWrapper';
+import Section from '@components/Section';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
import useEnvironment from '@hooks/useEnvironment';
@@ -82,45 +83,60 @@ function ShareCodePage({report, session, currentUserPersonalDetails}: ShareCodeP
shouldShowBackButton={isReport || isSmallScreenWidth}
icon={Illustrations.QrCode}
/>
-
-
-
-
+
+
+
+
+
+
-
- Clipboard.setString(url)}
- shouldLimitWidth={false}
- />
+
+ Clipboard.setString(url)}
+ shouldLimitWidth={false}
+ wrapperStyle={themeStyles.sectionMenuItemTopDescription}
+ />
- {isNative && (
-
+
diff --git a/src/pages/iou/request/step/IOURequestStepDescription.js b/src/pages/iou/request/step/IOURequestStepDescription.js
index 3a50eff13918..f3324a311732 100644
--- a/src/pages/iou/request/step/IOURequestStepDescription.js
+++ b/src/pages/iou/request/step/IOURequestStepDescription.js
@@ -14,6 +14,7 @@ import transactionPropTypes from '@components/transactionPropTypes';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
+import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import updateMultilineInputRange from '@libs/updateMultilineInputRange';
import * as IOU from '@userActions/IOU';
@@ -88,6 +89,24 @@ function IOURequestStepDescription({
}, []),
);
+ /**
+ * @param {Object} values
+ * @param {String} values.title
+ * @returns {Object} - An object containing the errors for each inputID
+ */
+ const validate = useCallback((values) => {
+ const errors = {};
+
+ if (values.moneyRequestComment.length > CONST.DESCRIPTION_LIMIT) {
+ ErrorUtils.addErrorMessage(errors, 'moneyRequestComment', [
+ 'common.error.characterLimitExceedCounter',
+ {length: values.moneyRequestComment.length, limit: CONST.DESCRIPTION_LIMIT},
+ ]);
+ }
+
+ return errors;
+ }, []);
+
const navigateBack = () => {
Navigation.goBack(backTo);
};
@@ -132,6 +151,7 @@ function IOURequestStepDescription({
style={[styles.flexGrow1, styles.ph5]}
formID={ONYXKEYS.FORMS.MONEY_REQUEST_DESCRIPTION_FORM}
onSubmit={updateComment}
+ validate={validate}
submitButtonText={translate('common.save')}
enabledWhenOffline
>
diff --git a/src/pages/settings/Profile/PersonalDetails/LegalNamePage.js b/src/pages/settings/Profile/PersonalDetails/LegalNamePage.js
index edb54534b76c..e45253462b60 100644
--- a/src/pages/settings/Profile/PersonalDetails/LegalNamePage.js
+++ b/src/pages/settings/Profile/PersonalDetails/LegalNamePage.js
@@ -58,25 +58,23 @@ function LegalNamePage(props) {
ErrorUtils.addErrorMessage(errors, 'legalFirstName', 'privatePersonalDetails.error.hasInvalidCharacter');
} else if (_.isEmpty(values.legalFirstName)) {
errors.legalFirstName = 'common.error.fieldRequired';
+ } else if (values.legalFirstName.length > CONST.TITLE_CHARACTER_LIMIT) {
+ ErrorUtils.addErrorMessage(errors, 'legalFirstName', ['common.error.characterLimitExceedCounter', {length: values.legalFirstName.length, limit: CONST.TITLE_CHARACTER_LIMIT}]);
}
if (ValidationUtils.doesContainReservedWord(values.legalFirstName, CONST.DISPLAY_NAME.RESERVED_NAMES)) {
ErrorUtils.addErrorMessage(errors, 'legalFirstName', 'personalDetails.error.containsReservedWord');
}
- if (values.legalFirstName.length > CONST.LEGAL_NAME.MAX_LENGTH) {
- ErrorUtils.addErrorMessage(errors, 'legalFirstName', ['common.error.characterLimitExceedCounter', {length: values.legalFirstName.length, limit: CONST.LEGAL_NAME.MAX_LENGTH}]);
- }
if (!ValidationUtils.isValidLegalName(values.legalLastName)) {
ErrorUtils.addErrorMessage(errors, 'legalLastName', 'privatePersonalDetails.error.hasInvalidCharacter');
} else if (_.isEmpty(values.legalLastName)) {
errors.legalLastName = 'common.error.fieldRequired';
+ } else if (values.legalLastName.length > CONST.TITLE_CHARACTER_LIMIT) {
+ ErrorUtils.addErrorMessage(errors, 'legalLastName', ['common.error.characterLimitExceedCounter', {length: values.legalLastName.length, limit: CONST.TITLE_CHARACTER_LIMIT}]);
}
if (ValidationUtils.doesContainReservedWord(values.legalLastName, CONST.DISPLAY_NAME.RESERVED_NAMES)) {
ErrorUtils.addErrorMessage(errors, 'legalLastName', 'personalDetails.error.containsReservedWord');
}
- if (values.legalLastName.length > CONST.LEGAL_NAME.MAX_LENGTH) {
- ErrorUtils.addErrorMessage(errors, 'legalLastName', ['common.error.characterLimitExceedCounter', {length: values.legalLastName.length, limit: CONST.LEGAL_NAME.MAX_LENGTH}]);
- }
return errors;
}, []);
@@ -111,7 +109,6 @@ function LegalNamePage(props) {
aria-label={props.translate('privatePersonalDetails.legalFirstName')}
role={CONST.ROLE.PRESENTATION}
defaultValue={legalFirstName}
- maxLength={CONST.LEGAL_NAME.MAX_LENGTH + CONST.SEARCH_MAX_LENGTH}
spellCheck={false}
/>
@@ -124,7 +121,6 @@ function LegalNamePage(props) {
aria-label={props.translate('privatePersonalDetails.legalLastName')}
role={CONST.ROLE.PRESENTATION}
defaultValue={legalLastName}
- maxLength={CONST.LEGAL_NAME.MAX_LENGTH + CONST.SEARCH_MAX_LENGTH}
spellCheck={false}
/>
diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js
index b5e77ab438f5..2872fae5b511 100755
--- a/src/pages/settings/Profile/ProfilePage.js
+++ b/src/pages/settings/Profile/ProfilePage.js
@@ -168,7 +168,7 @@ function ProfilePage(props) {
shouldShowBackButton={props.isSmallScreenWidth}
icon={Illustrations.Profile}
/>
-
+
CONST.TITLE_CHARACTER_LIMIT) {
+ ErrorUtils.addErrorMessage(errors, 'roomName', ['common.error.characterLimitExceedCounter', {length: values.roomName.length, limit: CONST.TITLE_CHARACTER_LIMIT}]);
}
return errors;
diff --git a/src/pages/tasks/NewTaskDescriptionPage.js b/src/pages/tasks/NewTaskDescriptionPage.js
index 4d84cac90537..54b0094dfcf7 100644
--- a/src/pages/tasks/NewTaskDescriptionPage.js
+++ b/src/pages/tasks/NewTaskDescriptionPage.js
@@ -12,6 +12,7 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
+import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import updateMultilineInputRange from '@libs/updateMultilineInputRange';
import * as Task from '@userActions/Task';
@@ -46,6 +47,20 @@ function NewTaskDescriptionPage(props) {
Navigation.goBack(ROUTES.NEW_TASK);
};
+ /**
+ * @param {Object} values - form input values passed by the Form component
+ * @returns {Boolean}
+ */
+ function validate(values) {
+ const errors = {};
+
+ if (values.taskDescription.length > CONST.DESCRIPTION_LIMIT) {
+ ErrorUtils.addErrorMessage(errors, 'taskDescription', ['common.error.characterLimitExceedCounter', {length: values.taskDescription.length, limit: CONST.DESCRIPTION_LIMIT}]);
+ }
+
+ return errors;
+ }
+
return (
validate(values)}
onSubmit={(values) => onSubmit(values)}
enabledWhenOffline
>
diff --git a/src/pages/tasks/NewTaskDetailsPage.js b/src/pages/tasks/NewTaskDetailsPage.js
index 4f4f2560a0d9..9595e1adbe76 100644
--- a/src/pages/tasks/NewTaskDetailsPage.js
+++ b/src/pages/tasks/NewTaskDetailsPage.js
@@ -57,6 +57,11 @@ function NewTaskDetailsPage(props) {
if (!values.taskTitle) {
// We error if the user doesn't enter a task name
ErrorUtils.addErrorMessage(errors, 'taskTitle', 'newTaskPage.pleaseEnterTaskName');
+ } else if (values.taskTitle.length > CONST.TITLE_CHARACTER_LIMIT) {
+ ErrorUtils.addErrorMessage(errors, 'taskTitle', ['common.error.characterLimitExceedCounter', {length: values.taskTitle.length, limit: CONST.TITLE_CHARACTER_LIMIT}]);
+ }
+ if (values.taskDescription.length > CONST.DESCRIPTION_LIMIT) {
+ ErrorUtils.addErrorMessage(errors, 'taskDescription', ['common.error.characterLimitExceedCounter', {length: values.taskDescription.length, limit: CONST.DESCRIPTION_LIMIT}]);
}
return errors;
diff --git a/src/pages/tasks/NewTaskTitlePage.js b/src/pages/tasks/NewTaskTitlePage.js
index 7bf6065625c0..e6da2a06435d 100644
--- a/src/pages/tasks/NewTaskTitlePage.js
+++ b/src/pages/tasks/NewTaskTitlePage.js
@@ -48,6 +48,8 @@ function NewTaskTitlePage(props) {
if (!values.taskTitle) {
// We error if the user doesn't enter a task name
ErrorUtils.addErrorMessage(errors, 'taskTitle', 'newTaskPage.pleaseEnterTaskName');
+ } else if (values.taskTitle.length > CONST.TITLE_CHARACTER_LIMIT) {
+ ErrorUtils.addErrorMessage(errors, 'taskTitle', ['common.error.characterLimitExceedCounter', {length: values.taskTitle.length, limit: CONST.TITLE_CHARACTER_LIMIT}]);
}
return errors;
diff --git a/src/pages/tasks/TaskDescriptionPage.js b/src/pages/tasks/TaskDescriptionPage.js
index 48be7022b187..add2cf3da057 100644
--- a/src/pages/tasks/TaskDescriptionPage.js
+++ b/src/pages/tasks/TaskDescriptionPage.js
@@ -13,6 +13,7 @@ import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalD
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
+import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as ReportUtils from '@libs/ReportUtils';
import StringUtils from '@libs/StringUtils';
@@ -38,7 +39,20 @@ const defaultProps = {
const parser = new ExpensiMark();
function TaskDescriptionPage(props) {
const styles = useThemeStyles();
- const validate = useCallback(() => ({}), []);
+
+ /**
+ * @param {Object} values - form input values passed by the Form component
+ * @returns {Boolean}
+ */
+ const validate = useCallback((values) => {
+ const errors = {};
+
+ if (values.description.length > CONST.DESCRIPTION_LIMIT) {
+ ErrorUtils.addErrorMessage(errors, 'description', ['common.error.characterLimitExceedCounter', {length: values.description.length, limit: CONST.DESCRIPTION_LIMIT}]);
+ }
+
+ return errors;
+ }, []);
const submit = useCallback(
(values) => {
diff --git a/src/pages/tasks/TaskTitlePage.js b/src/pages/tasks/TaskTitlePage.js
index 9b3d28a0d032..9fd1f29d3a0d 100644
--- a/src/pages/tasks/TaskTitlePage.js
+++ b/src/pages/tasks/TaskTitlePage.js
@@ -12,6 +12,7 @@ import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalD
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
+import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as ReportUtils from '@libs/ReportUtils';
import withReportOrNotFound from '@pages/home/report/withReportOrNotFound';
@@ -44,6 +45,8 @@ function TaskTitlePage(props) {
if (_.isEmpty(values.title)) {
errors.title = 'newTaskPage.pleaseEnterTaskName';
+ } else if (values.title.length > CONST.TITLE_CHARACTER_LIMIT) {
+ ErrorUtils.addErrorMessage(errors, 'title', ['common.error.characterLimitExceedCounter', {length: values.title.length, limit: CONST.TITLE_CHARACTER_LIMIT}]);
}
return errors;
diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx
index ea243c56ac76..3f7bfc92d48d 100644
--- a/src/pages/workspace/WorkspaceInitialPage.tsx
+++ b/src/pages/workspace/WorkspaceInitialPage.tsx
@@ -150,11 +150,11 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
const menuItems: WorkspaceMenuItem[] = [
{
- translationKey: 'workspace.common.overview',
+ translationKey: 'workspace.common.profile',
icon: Expensicons.Home,
- action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW.getRoute(policyID)))),
+ action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE.getRoute(policyID)))),
brickRoadIndicator: hasGeneralSettingsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
- routeName: SCREENS.WORKSPACE.OVERVIEW,
+ routeName: SCREENS.WORKSPACE.PROFILE,
},
...(shouldShowProtectedItems ? protectedMenuItems : []),
];
diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.js b/src/pages/workspace/WorkspaceInviteMessagePage.tsx
similarity index 64%
rename from src/pages/workspace/WorkspaceInviteMessagePage.js
rename to src/pages/workspace/WorkspaceInviteMessagePage.tsx
index bd5de51e0503..2e43708ed3ce 100644
--- a/src/pages/workspace/WorkspaceInviteMessagePage.js
+++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx
@@ -1,130 +1,110 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
+import type {StackScreenProps} from '@react-navigation/stack';
+import lodashDebounce from 'lodash/debounce';
import React, {useEffect, useState} from 'react';
import {Keyboard, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
+import type {OnyxEntry} from 'react-native-onyx';
+import type {GestureResponderEvent} from 'react-native/Libraries/Types/CoreEventTypes';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import MultipleAvatars from '@components/MultipleAvatars';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
+import type {AnimatedTextInputRef} from '@components/RNTextInput';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
-import withNavigationFocus from '@components/withNavigationFocus';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import Navigation from '@libs/Navigation/Navigation';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
import updateMultilineInputRange from '@libs/updateMultilineInputRange';
+import type {SettingsNavigatorParamList} from '@navigation/types';
import * as Link from '@userActions/Link';
import * as Policy from '@userActions/Policy';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import type {InvitedEmailsToAccountIDs, PersonalDetailsList} from '@src/types/onyx';
+import type {Errors} from '@src/types/onyx/OnyxCommon';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
import SearchInputManager from './SearchInputManager';
-import {policyDefaultProps, policyPropTypes} from './withPolicy';
import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading';
+import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading';
-const personalDetailsPropTypes = PropTypes.shape({
- /** The accountID of the person */
- accountID: PropTypes.number.isRequired,
-
- /** The login of the person (either email or phone number) */
- login: PropTypes.string,
-
- /** The URL of the person's avatar (there should already be a default avatar if
- the person doesn't have their own avatar uploaded yet, except for anon users) */
- avatar: PropTypes.string,
-
- /** This is either the user's full name, or their login if full name is an empty string */
- displayName: PropTypes.string,
-});
-
-const propTypes = {
+type WorkspaceInviteMessagePageOnyxProps = {
/** All of the personal details for everyone */
- allPersonalDetails: PropTypes.objectOf(personalDetailsPropTypes),
-
- invitedEmailsToAccountIDsDraft: PropTypes.objectOf(PropTypes.number),
+ allPersonalDetails: OnyxEntry;
- /** URL Route params */
- route: PropTypes.shape({
- /** Params from the URL path */
- params: PropTypes.shape({
- /** policyID passed via route: /workspace/:policyID/invite-message */
- policyID: PropTypes.string,
- }),
- }).isRequired,
+ /** An object containing the accountID for every invited user email */
+ invitedEmailsToAccountIDsDraft: OnyxEntry;
- ...policyPropTypes,
+ /** Updated workspace invite message */
+ workspaceInviteMessageDraft: OnyxEntry;
};
-const defaultProps = {
- ...policyDefaultProps,
- allPersonalDetails: {},
- invitedEmailsToAccountIDsDraft: {},
-};
+type WorkspaceInviteMessagePageProps = WithPolicyAndFullscreenLoadingProps &
+ WorkspaceInviteMessagePageOnyxProps &
+ StackScreenProps;
-function WorkspaceInviteMessagePage(props) {
+function WorkspaceInviteMessagePage({workspaceInviteMessageDraft, invitedEmailsToAccountIDsDraft, policy, route, allPersonalDetails}: WorkspaceInviteMessagePageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
- const [welcomeNote, setWelcomeNote] = useState();
+ const [welcomeNote, setWelcomeNote] = useState();
const {inputCallbackRef} = useAutoFocusInput();
const getDefaultWelcomeNote = () =>
- props.workspaceInviteMessageDraft ||
+ // workspaceInviteMessageDraft can be an empty string
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ workspaceInviteMessageDraft ||
translate('workspace.inviteMessage.welcomeNote', {
- workspaceName: props.policy.name,
+ workspaceName: policy?.name ?? '',
});
useEffect(() => {
- if (!_.isEmpty(props.invitedEmailsToAccountIDsDraft)) {
+ if (!isEmptyObject(invitedEmailsToAccountIDsDraft)) {
setWelcomeNote(getDefaultWelcomeNote());
return;
}
- Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(props.route.params.policyID), true);
+ Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID), true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- const debouncedSaveDraft = _.debounce((newDraft) => {
- Policy.setWorkspaceInviteMessageDraft(props.route.params.policyID, newDraft);
+ const debouncedSaveDraft = lodashDebounce((newDraft: string) => {
+ Policy.setWorkspaceInviteMessageDraft(route.params.policyID, newDraft);
});
const sendInvitation = () => {
Keyboard.dismiss();
- Policy.addMembersToWorkspace(props.invitedEmailsToAccountIDsDraft, welcomeNote, props.route.params.policyID);
- Policy.setWorkspaceInviteMembersDraft(props.route.params.policyID, {});
+ 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();
- Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(props.route.params.policyID));
+ Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(route.params.policyID));
};
- /**
- * Opens privacy url as an external link
- * @param {Object} event
- */
- const openPrivacyURL = (event) => {
- event.preventDefault();
+ /** Opens privacy url as an external link */
+ const openPrivacyURL = (event: GestureResponderEvent | KeyboardEvent | undefined) => {
+ event?.preventDefault();
Link.openExternalLink(CONST.PRIVACY_URL);
};
- const validate = () => {
- const errorFields = {};
- if (_.isEmpty(props.invitedEmailsToAccountIDsDraft)) {
+ const validate = (): Errors => {
+ const errorFields: Errors = {};
+ if (isEmptyObject(invitedEmailsToAccountIDsDraft)) {
errorFields.welcomeMessage = 'workspace.inviteMessage.inviteNoMembersError';
}
return errorFields;
};
- const policyName = lodashGet(props.policy, 'name');
+ const policyName = policy?.name;
return (
Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)}
>
Navigation.dismissModal()}
- onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(props.route.params.policyID))}
+ onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID))}
/>
{
+ onChangeText={(text: string) => {
setWelcomeNote(text);
debouncedSaveDraft(text);
}}
- ref={(el) => {
- if (!el) {
+ ref={(element: AnimatedTextInputRef) => {
+ if (!element) {
return;
}
- inputCallbackRef(el);
- updateMultilineInputRange(el);
+ inputCallbackRef(element);
+ updateMultilineInputRange(element);
}}
/>
@@ -210,13 +194,10 @@ function WorkspaceInviteMessagePage(props) {
);
}
-WorkspaceInviteMessagePage.propTypes = propTypes;
-WorkspaceInviteMessagePage.defaultProps = defaultProps;
WorkspaceInviteMessagePage.displayName = 'WorkspaceInviteMessagePage';
-export default compose(
- withPolicyAndFullscreenLoading,
- withOnyx({
+export default withPolicyAndFullscreenLoading(
+ withOnyx({
allPersonalDetails: {
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
},
@@ -226,6 +207,5 @@ export default compose(
workspaceInviteMessageDraft: {
key: ({route}) => `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT}${route.params.policyID.toString()}`,
},
- }),
- withNavigationFocus,
-)(WorkspaceInviteMessagePage);
+ })(WorkspaceInviteMessagePage),
+);
diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.tsx
similarity index 57%
rename from src/pages/workspace/WorkspaceInvitePage.js
rename to src/pages/workspace/WorkspaceInvitePage.tsx
index 8fcf425d8f34..ae139752a052 100644
--- a/src/pages/workspace/WorkspaceInvitePage.js
+++ b/src/pages/workspace/WorkspaceInvitePage.tsx
@@ -1,98 +1,87 @@
import {useNavigation} from '@react-navigation/native';
+import type {StackNavigationProp, StackScreenProps} from '@react-navigation/stack';
import Str from 'expensify-common/lib/str';
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
import React, {useEffect, useMemo, useState} from 'react';
+import type {SectionListData} from 'react-native';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
+import type {OnyxEntry} from 'react-native-onyx';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
+import type {Section} from '@components/SelectionList/types';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import * as LoginUtils from '@libs/LoginUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as OptionsListUtils from '@libs/OptionsListUtils';
+import type {MemberForList} from '@libs/OptionsListUtils';
import {parsePhoneNumber} from '@libs/PhoneNumber';
import * as PolicyUtils from '@libs/PolicyUtils';
+import type {OptionData} from '@libs/ReportUtils';
+import type {SettingsNavigatorParamList} from '@navigation/types';
import * as Policy from '@userActions/Policy';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import type {Beta, InvitedEmailsToAccountIDs, PersonalDetailsList} from '@src/types/onyx';
+import type {Errors} from '@src/types/onyx/OnyxCommon';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
import SearchInputManager from './SearchInputManager';
-import {policyDefaultProps, policyPropTypes} from './withPolicy';
import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading';
+import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading';
-const personalDetailsPropTypes = PropTypes.shape({
- /** The login of the person (either email or phone number) */
- login: PropTypes.string,
+type MembersSection = SectionListData>;
- /** The URL of the person's avatar (there should already be a default avatar if
- the person doesn't have their own avatar uploaded yet, except for anon users) */
- avatar: PropTypes.string,
-
- /** This is either the user's full name, or their login if full name is an empty string */
- displayName: PropTypes.string,
-});
+type WorkspaceInvitePageOnyxProps = {
+ /** All of the personal details for everyone */
+ personalDetails: OnyxEntry;
-const propTypes = {
/** Beta features list */
- betas: PropTypes.arrayOf(PropTypes.string),
-
- /** All of the personal details for everyone */
- personalDetails: PropTypes.objectOf(personalDetailsPropTypes),
-
- /** URL Route params */
- route: PropTypes.shape({
- /** Params from the URL path */
- params: PropTypes.shape({
- /** policyID passed via route: /workspace/:policyID/invite */
- policyID: PropTypes.string,
- }),
- }).isRequired,
-
- isLoadingReportData: PropTypes.bool,
- invitedEmailsToAccountIDsDraft: PropTypes.objectOf(PropTypes.number),
- ...policyPropTypes,
-};
+ betas: OnyxEntry;
-const defaultProps = {
- personalDetails: {},
- betas: [],
- isLoadingReportData: true,
- invitedEmailsToAccountIDsDraft: {},
- ...policyDefaultProps,
+ /** An object containing the accountID for every invited user email */
+ invitedEmailsToAccountIDsDraft: OnyxEntry;
};
-function WorkspaceInvitePage(props) {
+type WorkspaceInvitePageProps = WithPolicyAndFullscreenLoadingProps & WorkspaceInvitePageOnyxProps & StackScreenProps;
+
+function WorkspaceInvitePage({
+ route,
+ policyMembers,
+ personalDetails: personalDetailsProp,
+ betas,
+ invitedEmailsToAccountIDsDraft,
+ policy,
+ isLoadingReportData = true,
+}: WorkspaceInvitePageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const [searchTerm, setSearchTerm] = useState('');
- const [selectedOptions, setSelectedOptions] = useState([]);
- const [personalDetails, setPersonalDetails] = useState([]);
- const [usersToInvite, setUsersToInvite] = useState([]);
+ const [selectedOptions, setSelectedOptions] = useState([]);
+ const [personalDetails, setPersonalDetails] = useState([]);
+ const [usersToInvite, setUsersToInvite] = useState([]);
const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false);
- const navigation = useNavigation();
+ const navigation = useNavigation>();
const openWorkspaceInvitePage = () => {
- const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(props.policyMembers, props.personalDetails);
- Policy.openWorkspaceInvitePage(props.route.params.policyID, _.keys(policyMemberEmailsToAccountIDs));
+ const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policyMembers, personalDetailsProp);
+ Policy.openWorkspaceInvitePage(route.params.policyID, Object.keys(policyMemberEmailsToAccountIDs));
};
useEffect(() => {
setSearchTerm(SearchInputManager.searchInput);
return () => {
- Policy.setWorkspaceInviteMembersDraft(props.route.params.policyID, {});
+ Policy.setWorkspaceInviteMembersDraft(route.params.policyID, {});
};
- }, [props.route.params.policyID]);
+ }, [route.params.policyID]);
useEffect(() => {
- Policy.clearErrors(props.route.params.policyID);
+ Policy.clearErrors(route.params.policyID);
openWorkspaceInvitePage();
// eslint-disable-next-line react-hooks/exhaustive-deps -- policyID changes remount the component
}, []);
@@ -111,57 +100,69 @@ function WorkspaceInvitePage(props) {
useNetwork({onReconnect: openWorkspaceInvitePage});
- const excludedUsers = useMemo(() => PolicyUtils.getIneligibleInvitees(props.policyMembers, props.personalDetails), [props.policyMembers, props.personalDetails]);
+ const excludedUsers = useMemo(() => PolicyUtils.getIneligibleInvitees(policyMembers, personalDetailsProp), [policyMembers, personalDetailsProp]);
useEffect(() => {
- const newUsersToInviteDict = {};
- const newPersonalDetailsDict = {};
- const newSelectedOptionsDict = {};
+ const newUsersToInviteDict: Record = {};
+ const newPersonalDetailsDict: Record = {};
+ const newSelectedOptionsDict: Record = {};
- const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, searchTerm, excludedUsers, true);
+ const inviteOptions = OptionsListUtils.getMemberInviteOptions(personalDetailsProp, betas ?? [], searchTerm, excludedUsers, true);
// Update selectedOptions with the latest personalDetails and policyMembers information
- const detailsMap = {};
- _.each(inviteOptions.personalDetails, (detail) => (detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail)));
+ const detailsMap: Record = {};
+ inviteOptions.personalDetails.forEach((detail) => {
+ if (!detail.login) {
+ return;
+ }
- const newSelectedOptions = [];
- _.each(_.keys(props.invitedEmailsToAccountIDsDraft), (login) => {
- if (!_.has(detailsMap, login)) {
+ detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail);
+ });
+
+ const newSelectedOptions: MemberForList[] = [];
+ Object.keys(invitedEmailsToAccountIDsDraft ?? {}).forEach((login) => {
+ if (!(login in detailsMap)) {
return;
}
newSelectedOptions.push({...detailsMap[login], isSelected: true});
});
- _.each(selectedOptions, (option) => {
- newSelectedOptions.push(_.has(detailsMap, option.login) ? {...detailsMap[option.login], isSelected: true} : option);
+ selectedOptions.forEach((option) => {
+ newSelectedOptions.push(option.login && option.login in detailsMap ? {...detailsMap[option.login], isSelected: true} : option);
});
const userToInvite = inviteOptions.userToInvite;
// Only add the user to the invites list if it is valid
- if (userToInvite) {
+ if (typeof userToInvite?.accountID === 'number') {
newUsersToInviteDict[userToInvite.accountID] = userToInvite;
}
// Add all personal details to the new dict
- _.each(inviteOptions.personalDetails, (details) => {
+ inviteOptions.personalDetails.forEach((details) => {
+ if (typeof details.accountID !== 'number') {
+ return;
+ }
newPersonalDetailsDict[details.accountID] = details;
});
// Add all selected options to the new dict
- _.each(newSelectedOptions, (option) => {
+ newSelectedOptions.forEach((option) => {
+ if (typeof option.accountID !== 'number') {
+ return;
+ }
newSelectedOptionsDict[option.accountID] = option;
});
// Strip out dictionary keys and update arrays
- setUsersToInvite(_.values(newUsersToInviteDict));
- setPersonalDetails(_.values(newPersonalDetailsDict));
- setSelectedOptions(_.values(newSelectedOptionsDict));
+ setUsersToInvite(Object.values(newUsersToInviteDict));
+ setPersonalDetails(Object.values(newPersonalDetailsDict));
+ setSelectedOptions(Object.values(newSelectedOptionsDict));
// eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change
- }, [props.personalDetails, props.policyMembers, props.betas, searchTerm, excludedUsers]);
+ }, [personalDetailsProp, policyMembers, betas, searchTerm, excludedUsers]);
- const sections = useMemo(() => {
- const sectionsArr = [];
+ const sections: MembersSection[] = useMemo(() => {
+ const sectionsArr: MembersSection[] = [];
let indexOffset = 0;
if (!didScreenTransitionEnd) {
@@ -171,13 +172,13 @@ function WorkspaceInvitePage(props) {
// Filter all options that is a part of the search term or in the personal details
let filterSelectedOptions = selectedOptions;
if (searchTerm !== '') {
- filterSelectedOptions = _.filter(selectedOptions, (option) => {
- const accountID = lodashGet(option, 'accountID', null);
- const isOptionInPersonalDetails = _.some(personalDetails, (personalDetail) => personalDetail.accountID === accountID);
+ filterSelectedOptions = selectedOptions.filter((option) => {
+ const accountID = option.accountID;
+ const isOptionInPersonalDetails = Object.values(personalDetails).some((personalDetail) => personalDetail.accountID === accountID);
const parsedPhoneNumber = parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchTerm)));
- const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number.e164 : searchTerm.toLowerCase();
+ const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 ?? '' : searchTerm.toLowerCase();
- const isPartOfSearchTerm = option.text.toLowerCase().includes(searchValue) || option.login.toLowerCase().includes(searchValue);
+ const isPartOfSearchTerm = !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue);
return isPartOfSearchTerm || isOptionInPersonalDetails;
});
}
@@ -191,20 +192,20 @@ function WorkspaceInvitePage(props) {
indexOffset += filterSelectedOptions.length;
// Filtering out selected users from the search results
- const selectedLogins = _.map(selectedOptions, ({login}) => login);
- const personalDetailsWithoutSelected = _.filter(personalDetails, ({login}) => !_.contains(selectedLogins, login));
- const personalDetailsFormatted = _.map(personalDetailsWithoutSelected, OptionsListUtils.formatMemberForList);
+ const selectedLogins = selectedOptions.map(({login}) => login);
+ const personalDetailsWithoutSelected = Object.values(personalDetails).filter(({login}) => !selectedLogins.some((selectedLogin) => selectedLogin === login));
+ const personalDetailsFormatted = personalDetailsWithoutSelected.map((item) => OptionsListUtils.formatMemberForList(item));
sectionsArr.push({
title: translate('common.contacts'),
data: personalDetailsFormatted,
- shouldShow: !_.isEmpty(personalDetailsFormatted),
+ shouldShow: !isEmptyObject(personalDetailsFormatted),
indexOffset,
});
indexOffset += personalDetailsFormatted.length;
- _.each(usersToInvite, (userToInvite) => {
- const hasUnselectedUserToInvite = !_.contains(selectedLogins, userToInvite.login);
+ Object.values(usersToInvite).forEach((userToInvite) => {
+ const hasUnselectedUserToInvite = !selectedLogins.some((selectedLogin) => selectedLogin === userToInvite.login);
if (hasUnselectedUserToInvite) {
sectionsArr.push({
@@ -219,14 +220,14 @@ function WorkspaceInvitePage(props) {
return sectionsArr;
}, [personalDetails, searchTerm, selectedOptions, usersToInvite, translate, didScreenTransitionEnd]);
- const toggleOption = (option) => {
- Policy.clearErrors(props.route.params.policyID);
+ const toggleOption = (option: MemberForList) => {
+ Policy.clearErrors(route.params.policyID);
- const isOptionInList = _.some(selectedOptions, (selectedOption) => selectedOption.login === option.login);
+ const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option.login);
- let newSelectedOptions;
+ let newSelectedOptions: MemberForList[];
if (isOptionInList) {
- newSelectedOptions = _.reject(selectedOptions, (selectedOption) => selectedOption.login === option.login);
+ newSelectedOptions = selectedOptions.filter((selectedOption) => selectedOption.login !== option.login);
} else {
newSelectedOptions = [...selectedOptions, {...option, isSelected: true}];
}
@@ -234,14 +235,14 @@ function WorkspaceInvitePage(props) {
setSelectedOptions(newSelectedOptions);
};
- const validate = () => {
- const errors = {};
+ const validate = (): boolean => {
+ const errors: Errors = {};
if (selectedOptions.length <= 0) {
- errors.noUserSelected = true;
+ errors.noUserSelected = 'true';
}
- Policy.setWorkspaceErrors(props.route.params.policyID, errors);
- return _.size(errors) <= 0;
+ Policy.setWorkspaceErrors(route.params.policyID, errors);
+ return isEmptyObject(errors);
};
const inviteUser = () => {
@@ -249,27 +250,24 @@ function WorkspaceInvitePage(props) {
return;
}
- const invitedEmailsToAccountIDs = {};
- _.each(selectedOptions, (option) => {
- const login = option.login || '';
- const accountID = lodashGet(option, 'accountID', '');
+ const invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs = {};
+ selectedOptions.forEach((option) => {
+ const login = option.login ?? '';
+ const accountID = option.accountID ?? '';
if (!login.toLowerCase().trim() || !accountID) {
return;
}
invitedEmailsToAccountIDs[login] = Number(accountID);
});
- Policy.setWorkspaceInviteMembersDraft(props.route.params.policyID, invitedEmailsToAccountIDs);
- Navigation.navigate(ROUTES.WORKSPACE_INVITE_MESSAGE.getRoute(props.route.params.policyID));
+ Policy.setWorkspaceInviteMembersDraft(route.params.policyID, invitedEmailsToAccountIDs);
+ Navigation.navigate(ROUTES.WORKSPACE_INVITE_MESSAGE.getRoute(route.params.policyID));
};
- const [policyName, shouldShowAlertPrompt] = useMemo(
- () => [lodashGet(props.policy, 'name'), _.size(lodashGet(props.policy, 'errors', {})) > 0 || lodashGet(props.policy, 'alertMessage', '').length > 0],
- [props.policy],
- );
+ const [policyName, shouldShowAlertPrompt] = useMemo(() => [policy?.name ?? '', !isEmptyObject(policy?.errors) || !!policy?.alertMessage], [policy]);
const headerMessage = useMemo(() => {
const searchValue = searchTerm.trim().toLowerCase();
- if (usersToInvite.length === 0 && CONST.EXPENSIFY_EMAILS.includes(searchValue)) {
+ if (usersToInvite.length === 0 && CONST.EXPENSIFY_EMAILS.some((email) => email === searchValue)) {
return translate('messages.errorMessageInvalidEmail');
}
if (
@@ -289,8 +287,8 @@ function WorkspaceInvitePage(props) {
testID={WorkspaceInvitePage.displayName}
>
Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)}
>
{
- Policy.clearErrors(props.route.params.policyID);
+ Policy.clearErrors(route.params.policyID);
Navigation.goBack();
}}
/>
@@ -316,7 +314,7 @@ function WorkspaceInvitePage(props) {
onSelectRow={toggleOption}
onConfirm={inviteUser}
showScrollIndicator
- showLoadingPlaceholder={!didScreenTransitionEnd || !OptionsListUtils.isPersonalDetailsReady(props.personalDetails)}
+ showLoadingPlaceholder={!didScreenTransitionEnd || !OptionsListUtils.isPersonalDetailsReady(personalDetailsProp)}
shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
/>
@@ -325,7 +323,7 @@ function WorkspaceInvitePage(props) {
isAlertVisible={shouldShowAlertPrompt}
buttonText={translate('common.next')}
onSubmit={inviteUser}
- message={[props.policy.alertMessage, {isTranslated: true}]}
+ message={[policy?.alertMessage ?? '', {isTranslated: true}]}
containerStyles={[styles.flexReset, styles.flexGrow0, styles.flexShrink0, styles.flexBasisAuto, styles.mb5]}
enabledWhenOffline
disablePressOnEnter
@@ -336,24 +334,18 @@ function WorkspaceInvitePage(props) {
);
}
-WorkspaceInvitePage.propTypes = propTypes;
-WorkspaceInvitePage.defaultProps = defaultProps;
WorkspaceInvitePage.displayName = 'WorkspaceInvitePage';
-export default compose(
- withPolicyAndFullscreenLoading,
- withOnyx({
+export default withPolicyAndFullscreenLoading(
+ withOnyx({
personalDetails: {
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
},
betas: {
key: ONYXKEYS.BETAS,
},
- isLoadingReportData: {
- key: ONYXKEYS.IS_LOADING_REPORT_DATA,
- },
invitedEmailsToAccountIDsDraft: {
key: ({route}) => `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`,
},
- }),
-)(WorkspaceInvitePage);
+ })(WorkspaceInvitePage),
+);
diff --git a/src/pages/workspace/WorkspaceNamePage.tsx b/src/pages/workspace/WorkspaceNamePage.tsx
index e9d1ddd021d0..59679456be56 100644
--- a/src/pages/workspace/WorkspaceNamePage.tsx
+++ b/src/pages/workspace/WorkspaceNamePage.tsx
@@ -8,6 +8,7 @@ import ScreenWrapper from '@components/ScreenWrapper';
import TextInput from '@components/TextInput';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
+import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as ValidationUtils from '@libs/ValidationUtils';
import * as Policy from '@userActions/Policy';
@@ -41,10 +42,10 @@ function WorkspaceNamePage({policy}: Props) {
if (!ValidationUtils.isRequiredFulfilled(name)) {
errors.name = 'workspace.editor.nameIsRequiredError';
- } else if ([...name].length > CONST.WORKSPACE_NAME_CHARACTER_LIMIT) {
+ } else if ([...name].length > CONST.TITLE_CHARACTER_LIMIT) {
// Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16
// code units.
- errors.name = 'workspace.editor.nameIsTooLongError';
+ ErrorUtils.addErrorMessage(errors, 'name', ['common.error.characterLimitExceedCounter', {length: [...name].length, limit: CONST.TITLE_CHARACTER_LIMIT}]);
}
return errors;
diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js
index b8676faf0510..5c77f3a03191 100644
--- a/src/pages/workspace/WorkspaceNewRoomPage.js
+++ b/src/pages/workspace/WorkspaceNewRoomPage.js
@@ -211,6 +211,8 @@ function WorkspaceNewRoomPage(props) {
} else if (ValidationUtils.isExistingRoomName(values.roomName, props.reports, values.policyID)) {
// Certain names are reserved for default rooms and should not be used for policy rooms.
ErrorUtils.addErrorMessage(errors, 'roomName', 'newRoomPage.roomAlreadyExistsError');
+ } else if (values.roomName.length > CONST.TITLE_CHARACTER_LIMIT) {
+ ErrorUtils.addErrorMessage(errors, 'roomName', ['common.error.characterLimitExceedCounter', {length: values.roomName.length, limit: CONST.TITLE_CHARACTER_LIMIT}]);
}
if (!values.policyID) {
diff --git a/src/pages/workspace/WorkspaceOverviewCurrencyPage.js b/src/pages/workspace/WorkspaceProfileCurrencyPage.js
similarity index 100%
rename from src/pages/workspace/WorkspaceOverviewCurrencyPage.js
rename to src/pages/workspace/WorkspaceProfileCurrencyPage.js
diff --git a/src/pages/workspace/WorkspaceOverviewPage.js b/src/pages/workspace/WorkspaceProfilePage.js
similarity index 93%
rename from src/pages/workspace/WorkspaceOverviewPage.js
rename to src/pages/workspace/WorkspaceProfilePage.js
index 4e18d09c9137..c030e7e3841a 100644
--- a/src/pages/workspace/WorkspaceOverviewPage.js
+++ b/src/pages/workspace/WorkspaceProfilePage.js
@@ -50,23 +50,23 @@ const defaultProps = {
...policyDefaultProps,
};
-function WorkspaceOverviewPage({policy, currencyList, route}) {
+function WorkspaceProfilePage({policy, currencyList, route}) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const formattedCurrency = !_.isEmpty(policy) && !_.isEmpty(currencyList) ? `${policy.outputCurrency} - ${currencyList[policy.outputCurrency].symbol}` : '';
- const onPressCurrency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW_CURRENCY.getRoute(policy.id)), [policy.id]);
- const onPressName = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW_NAME.getRoute(policy.id)), [policy.id]);
+ const onPressCurrency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_CURRENCY.getRoute(policy.id)), [policy.id]);
+ const onPressName = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_NAME.getRoute(policy.id)), [policy.id]);
const policyName = lodashGet(policy, 'name', '');
const readOnly = !PolicyUtils.isPolicyAdmin(policy);
return (
alignItems: 'center',
},
+ qrShareSection: {
+ width: 264,
+ },
+
sectionMenuItemTopDescription: {
...spacing.ph8,
...spacing.mhn8,
diff --git a/src/types/onyx/InvitedEmailsToAccountIDs.ts b/src/types/onyx/InvitedEmailsToAccountIDs.ts
new file mode 100644
index 000000000000..929d21746682
--- /dev/null
+++ b/src/types/onyx/InvitedEmailsToAccountIDs.ts
@@ -0,0 +1,3 @@
+type InvitedEmailsToAccountIDs = Record;
+
+export default InvitedEmailsToAccountIDs;
diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts
index 989bab441dda..d5ed5dd36aba 100644
--- a/src/types/onyx/Policy.ts
+++ b/src/types/onyx/Policy.ts
@@ -31,6 +31,18 @@ type DisabledFields = {
reimbursable?: boolean;
};
+// These types are for the Integration connections for a policy (eg. Quickbooks, Xero, etc).
+// This data is not yet used in the codebase which is why it is given a very generic type, but the data is being put into Onyx for future use.
+// Once the data is being used, these types should be defined appropriately.
+type ConnectionLastSync = Record;
+type ConnectionData = Record;
+type ConnectionConfig = Record;
+type Connection = {
+ lastSync?: ConnectionLastSync;
+ data: ConnectionData;
+ config: ConnectionConfig;
+};
+
type AutoReportingOffset = number | ValueOf;
type Policy = {
@@ -158,6 +170,9 @@ type Policy = {
/** ReportID of the announce room for this workspace */
chatReportIDAnnounce?: number;
+
+ /** All the integration connections attached to the policy */
+ connections?: Record;
};
export default Policy;
diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts
index cdc87f5df252..aae3b6f2532a 100644
--- a/src/types/onyx/index.ts
+++ b/src/types/onyx/index.ts
@@ -29,6 +29,7 @@ import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji';
import type {FundList} from './Fund';
import type Fund from './Fund';
import type IntroSelected from './IntroSelected';
+import type InvitedEmailsToAccountIDs from './InvitedEmailsToAccountIDs';
import type IOU from './IOU';
import type LastPaymentMethod from './LastPaymentMethod';
import type Locale from './Locale';
@@ -171,6 +172,7 @@ export type {
NewRoomForm,
IKnowATeacherForm,
IntroSchoolPrincipalForm,
+ InvitedEmailsToAccountIDs,
PrivateNotesForm,
ReportFieldEditForm,
RoomNameForm,
diff --git a/tests/unit/CardUtilsTest.js b/tests/unit/CardUtilsTest.ts
similarity index 97%
rename from tests/unit/CardUtilsTest.js
rename to tests/unit/CardUtilsTest.ts
index f754b0895c6d..405b588fa93f 100644
--- a/tests/unit/CardUtilsTest.js
+++ b/tests/unit/CardUtilsTest.ts
@@ -1,4 +1,4 @@
-const cardUtils = require('../../src/libs/CardUtils');
+import * as cardUtils from '@src/libs/CardUtils';
const shortDate = '0924';
const shortDateSlashed = '09/24';