diff --git a/android/app/build.gradle b/android/app/build.gradle
index 192537f08e3d..5b21487d92cd 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -107,8 +107,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1009000306
- versionName "9.0.3-6"
+ versionCode 1009000400
+ versionName "9.0.4-0"
// Supported language variants must be declared here to avoid from being removed during the compilation.
// This also helps us to not include unnecessary language variants in the APK.
resConfigs "en", "es"
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 17eaae3cc3fc..c1aae6e1265d 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 9.0.3
+ 9.0.4
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 9.0.3.6
+ 9.0.4.0
FullStory
OrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 618d394349ed..579c99455525 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 9.0.3
+ 9.0.4
CFBundleSignature
????
CFBundleVersion
- 9.0.3.6
+ 9.0.4.0
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index d5e50828e3c7..7981169f076b 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName
$(PRODUCT_NAME)
CFBundleShortVersionString
- 9.0.3
+ 9.0.4
CFBundleVersion
- 9.0.3.6
+ 9.0.4.0
NSExtension
NSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index 9f63be958d1a..c92ca2ec3813 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.0.3-6",
+ "version": "9.0.4-0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.3-6",
+ "version": "9.0.4-0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index c61316e22030..e4bd1d99db16 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.3-6",
+ "version": "9.0.4-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 d82ce9d63372..00f2245a55c0 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -369,6 +369,7 @@ const CONST = {
WORKSPACE_FEEDS: 'workspaceFeeds',
NETSUITE_USA_TAX: 'netsuiteUsaTax',
INTACCT_ON_NEW_EXPENSIFY: 'intacctOnNewExpensify',
+ COMMENT_LINKING: 'commentLinking',
},
BUTTON_STATES: {
DEFAULT: 'default',
@@ -1852,6 +1853,11 @@ const CONST = {
MAKE_MEMBER: 'makeMember',
MAKE_ADMIN: 'makeAdmin',
},
+ BULK_ACTION_TYPES: {
+ DELETE: 'delete',
+ DISABLE: 'disable',
+ ENABLE: 'enable',
+ },
MORE_FEATURES: {
ARE_CATEGORIES_ENABLED: 'areCategoriesEnabled',
ARE_TAGS_ENABLED: 'areTagsEnabled',
@@ -1862,21 +1868,6 @@ const CONST = {
ARE_EXPENSIFY_CARDS_ENABLED: 'areExpensifyCardsEnabled',
ARE_TAXES_ENABLED: 'tax',
},
- CATEGORIES_BULK_ACTION_TYPES: {
- DELETE: 'delete',
- DISABLE: 'disable',
- ENABLE: 'enable',
- },
- TAGS_BULK_ACTION_TYPES: {
- DELETE: 'delete',
- DISABLE: 'disable',
- ENABLE: 'enable',
- },
- DISTANCE_RATES_BULK_ACTION_TYPES: {
- DELETE: 'delete',
- DISABLE: 'disable',
- ENABLE: 'enable',
- },
DEFAULT_CATEGORIES: [
'Advertising',
'Benefits',
@@ -1907,11 +1898,6 @@ const CONST = {
DUPLICATE_SUBSCRIPTION: 'duplicateSubscription',
FAILED_TO_CLEAR_BALANCE: 'failedToClearBalance',
},
- TAX_RATES_BULK_ACTION_TYPES: {
- DELETE: 'delete',
- DISABLE: 'disable',
- ENABLE: 'enable',
- },
COLLECTION_KEYS: {
DESCRIPTION: 'description',
REIMBURSER: 'reimburser',
@@ -2259,6 +2245,7 @@ const CONST = {
LOGIN_CHARACTER_LIMIT: 254,
CATEGORY_NAME_LIMIT: 256,
TAG_NAME_LIMIT: 256,
+ WORKSPACE_REPORT_FIELD_POLICY_MAX_LENGTH: 256,
REPORT_NAME_LIMIT: 100,
TITLE_CHARACTER_LIMIT: 100,
DESCRIPTION_LIMIT: 500,
@@ -5082,6 +5069,12 @@ const CONST = {
},
EXCLUDE_FROM_LAST_VISITED_PATH: [SCREENS.NOT_FOUND, SCREENS.SAML_SIGN_IN, SCREENS.VALIDATE_LOGIN] as string[],
+
+ REPORT_FIELD_TYPES: {
+ TEXT: 'text',
+ DATE: 'date',
+ LIST: 'dropdown',
+ },
} as const;
type Country = keyof typeof CONST.ALL_COUNTRIES;
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 5088c1d3158f..709347fa71cd 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -359,12 +359,17 @@ const ONYXKEYS = {
/** Holds the checks used while transferring the ownership of the workspace */
POLICY_OWNERSHIP_CHANGE_CHECKS: 'policyOwnershipChangeChecks',
+ // These statuses below are in separate keys on purpose - it allows us to have different behaviours of the banner based on the status
+
/** Indicates whether ClearOutstandingBalance failed */
SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED: 'subscriptionRetryBillingStatusFailed',
/** Indicates whether ClearOutstandingBalance was successful */
SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL: 'subscriptionRetryBillingStatusSuccessful',
+ /** Indicates whether ClearOutstandingBalance is pending */
+ SUBSCRIPTION_RETRY_BILLING_STATUS_PENDING: 'subscriptionRetryBillingStatusPending',
+
/** Stores info during review duplicates flow */
REVIEW_DUPLICATES: 'reviewDuplicates',
@@ -454,6 +459,8 @@ const ONYXKEYS = {
WORKSPACE_RATE_AND_UNIT_FORM_DRAFT: 'workspaceRateAndUnitFormDraft',
WORKSPACE_TAX_CUSTOM_NAME: 'workspaceTaxCustomName',
WORKSPACE_TAX_CUSTOM_NAME_DRAFT: 'workspaceTaxCustomNameDraft',
+ WORKSPACE_REPORT_FIELDS_FORM: 'workspaceReportFieldsForm',
+ WORKSPACE_REPORT_FIELDS_FORM_DRAFT: 'workspaceReportFieldsFormDraft',
POLICY_CREATE_DISTANCE_RATE_FORM: 'policyCreateDistanceRateForm',
POLICY_CREATE_DISTANCE_RATE_FORM_DRAFT: 'policyCreateDistanceRateFormDraft',
POLICY_DISTANCE_RATE_EDIT_FORM: 'policyDistanceRateEditForm',
@@ -564,6 +571,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.WORKSPACE_TAG_FORM]: FormTypes.WorkspaceTagForm;
[ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: FormTypes.WorkspaceRateAndUnitForm;
[ONYXKEYS.FORMS.WORKSPACE_TAX_CUSTOM_NAME]: FormTypes.WorkspaceTaxCustomName;
+ [ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM]: FormTypes.WorkspaceReportFieldsForm;
[ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: FormTypes.CloseAccountForm;
[ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: FormTypes.ProfileSettingsForm;
[ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: FormTypes.DisplayNameForm;
@@ -781,6 +789,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE]: OnyxTypes.QuickAction;
[ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED]: boolean;
[ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL]: boolean;
+ [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_PENDING]: boolean;
[ONYXKEYS.NVP_TRAVEL_SETTINGS]: OnyxTypes.TravelSettings;
[ONYXKEYS.REVIEW_DUPLICATES]: OnyxTypes.ReviewDuplicates;
[ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD]: OnyxTypes.IssueNewCard;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 45c56abc71d5..a70d6e7502ae 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -783,6 +783,26 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/reportFields',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/reportFields` as const,
},
+ WORKSPACE_CREATE_REPORT_FIELD: {
+ route: 'settings/workspaces/:policyID/reportFields/new',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/reportFields/new` as const,
+ },
+ WORKSPACE_REPORT_FIELD_LIST_VALUES: {
+ route: 'settings/workspaces/:policyID/reportFields/new/listValues',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/reportFields/new/listValues` as const,
+ },
+ WORKSPACE_REPORT_FIELD_ADD_VALUE: {
+ route: 'settings/workspaces/:policyID/reportFields/new/addValue',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/reportFields/new/addValue` as const,
+ },
+ WORKSPACE_REPORT_FIELD_VALUE_SETTINGS: {
+ route: 'settings/workspaces/:policyID/reportFields/new/:valueIndex',
+ getRoute: (policyID: string, valueIndex: number) => `settings/workspaces/${policyID}/reportFields/new/${valueIndex}` as const,
+ },
+ WORKSPACE_REPORT_FIELD_EDIT_VALUE: {
+ route: 'settings/workspaces/:policyID/reportFields/new/:valueIndex/edit',
+ getRoute: (policyID: string, valueIndex: number) => `settings/workspaces/${policyID}/reportFields/new/${valueIndex}/edit` as const,
+ },
WORKSPACE_EXPENSIFY_CARD: {
route: 'settings/workspaces/:policyID/expensify-card',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card` as const,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 8214c04cef75..e12ccfdab072 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -313,6 +313,11 @@ const SCREENS = {
TAG_EDIT: 'Tag_Edit',
TAXES: 'Workspace_Taxes',
REPORT_FIELDS: 'Workspace_ReportFields',
+ REPORT_FIELDS_CREATE: 'Workspace_ReportFields_Create',
+ REPORT_FIELDS_LIST_VALUES: 'Workspace_ReportFields_ListValues',
+ REPORT_FIELDS_ADD_VALUE: 'Workspace_ReportFields_AddValue',
+ REPORT_FIELDS_VALUE_SETTINGS: 'Workspace_ReportFields_ValueSettings',
+ REPORT_FIELDS_EDIT_VALUE: 'Workspace_ReportFields_EditValue',
TAX_EDIT: 'Workspace_Tax_Edit',
TAX_NAME: 'Workspace_Tax_Name',
TAX_VALUE: 'Workspace_Tax_Value',
diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts
index 87a9108d5f2e..5893bcd9936e 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts
+++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts
@@ -1,5 +1,6 @@
import type {ForwardedRef} from 'react';
import {createContext} from 'react';
+import type {GestureType} from 'react-native-gesture-handler';
import type PagerView from 'react-native-pager-view';
import type {SharedValue} from 'react-native-reanimated';
import type {AttachmentSource} from '@components/Attachments/types';
@@ -17,16 +18,28 @@ type AttachmentCarouselPagerItems = {
};
type AttachmentCarouselPagerContextValue = {
- /** The list of items that are shown in the pager */
+ /** List of attachments displayed in the pager */
pagerItems: AttachmentCarouselPagerItems[];
- /** The index of the active page */
+ /** Index of the currently active page */
activePage: number;
- pagerRef?: ForwardedRef;
+
+ /** Ref to the active attachment */
+ pagerRef?: ForwardedRef;
+
+ /** Indicates if the pager is currently scrolling */
isPagerScrolling: SharedValue;
+
+ /** Indicates if scrolling is enabled for the attachment */
isScrollEnabled: SharedValue;
+
+ /** Function to call after a tap event */
onTap: () => void;
+
+ /** Function to call when the scale changes */
onScaleChanged: (scale: number) => void;
+
+ /** Function to call after a swipe down event */
onSwipeDown: () => void;
};
diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx
index b7ef9309eb10..f16ba2c53ae8 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx
+++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx
@@ -1,4 +1,4 @@
-import type {ForwardedRef} from 'react';
+import type {ForwardedRef, SetStateAction} from 'react';
import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import type {NativeSyntheticEvent} from 'react-native';
import {View} from 'react-native';
@@ -8,6 +8,7 @@ import type {PagerViewProps} from 'react-native-pager-view';
import PagerView from 'react-native-pager-view';
import Animated, {useAnimatedProps, useSharedValue} from 'react-native-reanimated';
import CarouselItem from '@components/Attachments/AttachmentCarousel/CarouselItem';
+import useCarouselContextEvents from '@components/Attachments/AttachmentCarousel/useCarouselContextEvents';
import type {Attachment, AttachmentSource} from '@components/Attachments/types';
import useThemeStyles from '@hooks/useThemeStyles';
import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext';
@@ -41,24 +42,21 @@ type AttachmentCarouselPagerProps = {
>,
) => void;
- /**
- * A callback that can be used to toggle the attachment carousel arrows, when the scale of the image changes.
- * @param showArrows If set, it will show/hide the arrows. If not set, it will toggle the arrows.
- */
- onRequestToggleArrows: (showArrows?: boolean) => void;
-
/** A callback that is called when swipe-down-to-close gesture happens */
onClose: () => void;
+
+ /** Sets the visibility of the arrows. */
+ setShouldShowArrows: (show?: SetStateAction) => void;
};
function AttachmentCarouselPager(
- {items, activeSource, initialPage, onPageSelected, onRequestToggleArrows, onClose}: AttachmentCarouselPagerProps,
+ {items, activeSource, initialPage, setShouldShowArrows, onPageSelected, onClose}: AttachmentCarouselPagerProps,
ref: ForwardedRef,
) {
+ const {handleTap, handleScaleChange} = useCarouselContextEvents(setShouldShowArrows);
const styles = useThemeStyles();
const pagerRef = useRef(null);
- const scale = useRef(1);
const isPagerScrolling = useSharedValue(false);
const isScrollEnabled = useSharedValue(true);
@@ -80,42 +78,6 @@ function AttachmentCarouselPager(
/** The `pagerItems` object that passed down to the context. Later used to detect current page, whether it's a single image gallery etc. */
const pagerItems = useMemo(() => items.map((item, index) => ({source: item.source, index, isActive: index === activePageIndex})), [activePageIndex, items]);
- /**
- * This callback is passed to the MultiGestureCanvas/Lightbox through the AttachmentCarouselPagerContext.
- * It is used to react to zooming/pinching and (mostly) enabling/disabling scrolling on the pager,
- * as well as enabling/disabling the carousel buttons.
- */
- const handleScaleChange = useCallback(
- (newScale: number) => {
- if (newScale === scale.current) {
- return;
- }
-
- scale.current = newScale;
-
- const newIsScrollEnabled = newScale === 1;
- if (isScrollEnabled.value === newIsScrollEnabled) {
- return;
- }
-
- isScrollEnabled.value = newIsScrollEnabled;
- onRequestToggleArrows(newIsScrollEnabled);
- },
- [isScrollEnabled, onRequestToggleArrows],
- );
-
- /**
- * This callback is passed to the MultiGestureCanvas/Lightbox through the AttachmentCarouselPagerContext.
- * It is used to trigger touch events on the pager when the user taps on the MultiGestureCanvas/Lightbox.
- */
- const handleTap = useCallback(() => {
- if (!isScrollEnabled.value) {
- return;
- }
-
- onRequestToggleArrows();
- }, [isScrollEnabled.value, onRequestToggleArrows]);
-
const extractItemKey = useCallback(
(item: Attachment, index: number) =>
typeof item.source === 'string' || typeof item.source === 'number' ? `source-${item.source}` : `reportActionID-${item.reportActionID}` ?? `index-${index}`,
diff --git a/src/components/Attachments/AttachmentCarousel/index.native.tsx b/src/components/Attachments/AttachmentCarousel/index.native.tsx
index 15740725c42e..243fc52f1f5d 100644
--- a/src/components/Attachments/AttachmentCarousel/index.native.tsx
+++ b/src/components/Attachments/AttachmentCarousel/index.native.tsx
@@ -96,22 +96,6 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
[autoHideArrows, page, updatePage],
);
- /**
- * Toggles the arrows visibility
- * @param {Boolean} showArrows if showArrows is passed, it will set the visibility to the passed value
- */
- const toggleArrows = useCallback(
- (showArrows?: boolean) => {
- if (showArrows === undefined) {
- setShouldShowArrows((prevShouldShowArrows) => !prevShouldShowArrows);
- return;
- }
-
- setShouldShowArrows(showArrows);
- },
- [setShouldShowArrows],
- );
-
const containerStyles = [styles.flex1, styles.attachmentCarouselContainer];
if (page == null) {
@@ -147,7 +131,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
items={attachments}
initialPage={page}
activeSource={activeSource}
- onRequestToggleArrows={toggleArrows}
+ setShouldShowArrows={setShouldShowArrows}
onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
onClose={onClose}
ref={pagerRef}
diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx
index 076ba87e9341..36abe1e2e5ed 100644
--- a/src/components/Attachments/AttachmentCarousel/index.tsx
+++ b/src/components/Attachments/AttachmentCarousel/index.tsx
@@ -1,10 +1,12 @@
import isEqual from 'lodash/isEqual';
-import React, {useCallback, useEffect, useMemo, useState} from 'react';
+import type {MutableRefObject} from 'react';
+import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import type {ListRenderItemInfo} from 'react-native';
import {Keyboard, PixelRatio, View} from 'react-native';
+import type {GestureType} from 'react-native-gesture-handler';
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
import {withOnyx} from 'react-native-onyx';
-import Animated, {scrollTo, useAnimatedRef} from 'react-native-reanimated';
+import Animated, {scrollTo, useAnimatedRef, useSharedValue} from 'react-native-reanimated';
import type {Attachment, AttachmentSource} from '@components/Attachments/types';
import BlockingView from '@components/BlockingViews/BlockingView';
import * as Illustrations from '@components/Icon/Illustrations';
@@ -22,8 +24,10 @@ import CarouselActions from './CarouselActions';
import CarouselButtons from './CarouselButtons';
import CarouselItem from './CarouselItem';
import extractAttachments from './extractAttachments';
+import AttachmentCarouselPagerContext from './Pager/AttachmentCarouselPagerContext';
import type {AttachmentCaraouselOnyxProps, AttachmentCarouselProps, UpdatePageProps} from './types';
import useCarouselArrows from './useCarouselArrows';
+import useCarouselContextEvents from './useCarouselContextEvents';
const viewabilityConfig = {
// To facilitate paging through the attachments, we want to consider an item "viewable" when it is
@@ -33,13 +37,15 @@ const viewabilityConfig = {
const MIN_FLING_VELOCITY = 500;
-function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, type, accountID}: AttachmentCarouselProps) {
+function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, type, accountID, onClose}: AttachmentCarouselProps) {
const theme = useTheme();
const {translate} = useLocalize();
const {isSmallScreenWidth, windowWidth} = useWindowDimensions();
const styles = useThemeStyles();
const {isFullScreenRef} = useFullScreenContext();
const scrollRef = useAnimatedRef>>();
+ const nope = useSharedValue(false);
+ const pagerRef = useRef(null);
const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
@@ -52,6 +58,14 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
const [attachments, setAttachments] = useState([]);
const [activeSource, setActiveSource] = useState(source);
const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows();
+ const {handleTap, handleScaleChange, scale} = useCarouselContextEvents(setShouldShowArrows);
+
+ useEffect(() => {
+ if (!canUseTouchScreen) {
+ return;
+ }
+ setShouldShowArrows(true);
+ }, [canUseTouchScreen, page, setShouldShowArrows]);
const compareImage = useCallback((attachment: Attachment) => attachment.source === source, [source]);
@@ -169,6 +183,20 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
[cellWidth],
);
+ const context = useMemo(
+ () => ({
+ pagerItems: [{source, index: 0, isActive: true}],
+ activePage: 0,
+ pagerRef,
+ isPagerScrolling: nope,
+ isScrollEnabled: nope,
+ onTap: handleTap,
+ onScaleChanged: handleScaleChange,
+ onSwipeDown: onClose,
+ }),
+ [source, nope, handleTap, handleScaleChange, onClose],
+ );
+
/** Defines how a single attachment should be rendered */
const renderItem = useCallback(
({item}: ListRenderItemInfo) => (
@@ -176,20 +204,30 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
setShouldShowArrows((oldState) => !oldState) : undefined}
+ onPress={canUseTouchScreen ? handleTap : undefined}
isModalHovered={shouldShowArrows}
/>
),
- [activeSource, canUseTouchScreen, cellWidth, setShouldShowArrows, shouldShowArrows, styles.h100],
+ [activeSource, canUseTouchScreen, cellWidth, handleTap, shouldShowArrows, styles.h100],
);
/** Pan gesture handing swiping through attachments on touch screen devices */
const pan = useMemo(
() =>
Gesture.Pan()
.enabled(canUseTouchScreen)
- .onUpdate(({translationX}) => scrollTo(scrollRef, page * cellWidth - translationX, 0, false))
+ .onUpdate(({translationX}) => {
+ if (scale.current !== 1) {
+ return;
+ }
+
+ scrollTo(scrollRef, page * cellWidth - translationX, 0, false);
+ })
.onEnd(({translationX, velocityX}) => {
+ if (scale.current !== 1) {
+ return;
+ }
+
let newIndex;
if (velocityX > MIN_FLING_VELOCITY) {
// User flung to the right
@@ -204,8 +242,9 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
}
scrollTo(scrollRef, newIndex * cellWidth, 0, true);
- }),
- [attachments.length, canUseTouchScreen, cellWidth, page, scrollRef],
+ })
+ .withRef(pagerRef as MutableRefObject),
+ [attachments.length, canUseTouchScreen, cellWidth, page, scale, scrollRef],
);
return (
@@ -233,27 +272,28 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
autoHideArrow={autoHideArrows}
cancelAutoHideArrow={cancelAutoHideArrows}
/>
-
-
-
-
+
+
+
+
+
>
diff --git a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts
index 12ca3db4e2ff..a7ce0f93114b 100644
--- a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts
+++ b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts
@@ -32,6 +32,9 @@ function useCarouselArrows() {
}, CONST.ARROW_HIDE_DELAY);
}, [canUseTouchScreen, cancelAutoHideArrows]);
+ /**
+ * Sets the visibility of the arrows.
+ */
const setShouldShowArrows = useCallback(
(show: SetStateAction = true) => {
setShouldShowArrowsInternal(show);
diff --git a/src/components/Attachments/AttachmentCarousel/useCarouselContextEvents.ts b/src/components/Attachments/AttachmentCarousel/useCarouselContextEvents.ts
new file mode 100644
index 000000000000..d516879322ea
--- /dev/null
+++ b/src/components/Attachments/AttachmentCarousel/useCarouselContextEvents.ts
@@ -0,0 +1,63 @@
+import {useCallback, useRef} from 'react';
+import type {SetStateAction} from 'react';
+import {useSharedValue} from 'react-native-reanimated';
+
+function useCarouselContextEvents(setShouldShowArrows: (show?: SetStateAction) => void) {
+ const scale = useRef(1);
+ const isScrollEnabled = useSharedValue(true);
+
+ /**
+ * Toggles the arrows visibility
+ */
+ const onRequestToggleArrows = useCallback(
+ (showArrows?: boolean) => {
+ if (showArrows === undefined) {
+ setShouldShowArrows((prevShouldShowArrows) => !prevShouldShowArrows);
+ return;
+ }
+
+ setShouldShowArrows(showArrows);
+ },
+ [setShouldShowArrows],
+ );
+
+ /**
+ * This callback is passed to the MultiGestureCanvas/Lightbox through the AttachmentCarouselPagerContext.
+ * It is used to react to zooming/pinching and (mostly) enabling/disabling scrolling on the pager,
+ * as well as enabling/disabling the carousel buttons.
+ */
+ const handleScaleChange = useCallback(
+ (newScale: number) => {
+ if (newScale === scale.current) {
+ return;
+ }
+
+ scale.current = newScale;
+
+ const newIsScrollEnabled = newScale === 1;
+ if (isScrollEnabled.value === newIsScrollEnabled) {
+ return;
+ }
+
+ isScrollEnabled.value = newIsScrollEnabled;
+ onRequestToggleArrows(newIsScrollEnabled);
+ },
+ [isScrollEnabled, onRequestToggleArrows],
+ );
+
+ /**
+ * This callback is passed to the MultiGestureCanvas/Lightbox through the AttachmentCarouselPagerContext.
+ * It is used to trigger touch events on the pager when the user taps on the MultiGestureCanvas/Lightbox.
+ */
+ const handleTap = useCallback(() => {
+ if (!isScrollEnabled.value) {
+ return;
+ }
+
+ onRequestToggleArrows();
+ }, [isScrollEnabled.value, onRequestToggleArrows]);
+
+ return {handleTap, handleScaleChange, scale};
+}
+
+export default useCarouselContextEvents;
diff --git a/src/components/ButtonWithDropdownMenu/types.ts b/src/components/ButtonWithDropdownMenu/types.ts
index 702f0380ceef..d1eedd560694 100644
--- a/src/components/ButtonWithDropdownMenu/types.ts
+++ b/src/components/ButtonWithDropdownMenu/types.ts
@@ -10,9 +10,9 @@ type PaymentType = DeepValueOf;
-type WorkspaceDistanceRatesBulkActionType = DeepValueOf;
+type WorkspaceDistanceRatesBulkActionType = DeepValueOf;
-type WorkspaceTaxRatesBulkActionType = DeepValueOf;
+type WorkspaceTaxRatesBulkActionType = DeepValueOf;
type DropdownOption = {
value: TValueType;
diff --git a/src/components/ConnectToQuickbooksOnlineButton/index.tsx b/src/components/ConnectToQuickbooksOnlineButton/index.tsx
index 71f1fba91187..50ee9165b8a3 100644
--- a/src/components/ConnectToQuickbooksOnlineButton/index.tsx
+++ b/src/components/ConnectToQuickbooksOnlineButton/index.tsx
@@ -40,6 +40,8 @@ function ConnectToQuickbooksOnlineButton({policyID, shouldDisconnectIntegrationB
{shouldDisconnectIntegrationBeforeConnecting && integrationToDisconnect && isDisconnectModalOpen && (
{
+ // Since QBO doesn't support Taxes, we should disable them from the LHN when connecting to QBO
+ PolicyAction.enablePolicyTaxes(policyID, false);
removePolicyConnection(policyID, integrationToDisconnect);
Link.openLink(getQuickBooksOnlineSetupLink(policyID), environmentURL);
setIsDisconnectModalOpen(false);
diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx
index f4216dcc9f8a..a8d636db460b 100644
--- a/src/components/EReceiptThumbnail.tsx
+++ b/src/components/EReceiptThumbnail.tsx
@@ -5,6 +5,7 @@ import {withOnyx} from 'react-native-onyx';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ReportUtils from '@libs/ReportUtils';
+import * as TripReservationUtils from '@libs/TripReservationUtils';
import colors from '@styles/theme/colors';
import variables from '@styles/variables';
import CONST from '@src/CONST';
@@ -56,7 +57,8 @@ const backgroundImages = {
function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptThumbnail = false, centerIconV = true, iconSize = 'large'}: EReceiptThumbnailProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
- const colorCode = isReceiptThumbnail ? StyleUtils.getFileExtensionColorCode(fileExtension) : StyleUtils.getEReceiptColorCode(transaction);
+ const {tripIcon, tripBGColor} = TripReservationUtils.getTripEReceiptData(transaction);
+ const colorCode = tripBGColor ?? (isReceiptThumbnail ? StyleUtils.getFileExtensionColorCode(fileExtension) : StyleUtils.getEReceiptColorCode(transaction));
const backgroundImage = useMemo(() => backgroundImages[colorCode], [colorCode]);
@@ -141,6 +143,14 @@ function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptT
fill={primaryColor}
/>
) : null}
+ {tripIcon ? (
+
+ ) : null}
diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts
index 6245fdcf7b49..afbe2bb124b5 100644
--- a/src/components/Form/types.ts
+++ b/src/components/Form/types.ts
@@ -49,12 +49,14 @@ type ValidInputs =
| typeof AddPlaidBankAccount
| typeof EmojiPickerButtonDropdown;
-type ValueTypeKey = 'string' | 'boolean' | 'date' | 'country';
+type ValueTypeKey = 'string' | 'boolean' | 'date' | 'country' | 'reportFields' | 'disabledListValues';
type ValueTypeMap = {
string: string;
boolean: boolean;
date: Date;
country: Country | '';
+ reportFields: string[];
+ disabledListValues: boolean[];
};
type FormValue = ValueOf;
diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx
index c74d9bd5aa52..e12be53d01ae 100644
--- a/src/components/ImageView/index.tsx
+++ b/src/components/ImageView/index.tsx
@@ -7,6 +7,7 @@ import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import Image from '@components/Image';
import RESIZE_MODES from '@components/Image/resizeModes';
import type {ImageOnLoadEvent} from '@components/Image/types';
+import Lightbox from '@components/Lightbox';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import useNetwork from '@hooks/useNetwork';
import useStyleUtils from '@hooks/useStyleUtils';
@@ -200,25 +201,11 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV
if (canUseTouchScreen) {
return (
-
- 1 ? RESIZE_MODES.center : RESIZE_MODES.contain}
- onLoadStart={imageLoadingStart}
- onLoad={imageLoad}
- onError={onError}
- />
- {((isLoading && (!isOffline || isLocalFile)) || (!isLoading && zoomScale === 0)) && }
- {isLoading && !isLocalFile && }
-
+
);
}
return (
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 2628e938e215..780c8c7d2ea4 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -437,6 +437,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
paymentType={paymentType}
chatReport={chatReport}
moneyRequestReport={moneyRequestReport}
+ transactionCount={transactionIDs.length}
/>
)}
;
/** If there is a pager wrapping the canvas, we need to disable the pan gesture in case the pager is swiping */
- pagerRef?: ForwardedRef; // TODO: For TS migration: Exclude
+ pagerRef?: ForwardedRef; // TODO: For TS migration: Exclude
/** Handles scale changed event */
onScaleChanged?: OnScaleChangedCallback;
@@ -48,6 +49,7 @@ type MultiGestureCanvasProps = ChildrenProps & {
/** Handles scale changed event */
onTap?: OnTapCallback;
+ /** Handles swipe down event */
onSwipeDown?: OnSwipeDownCallback;
};
@@ -242,11 +244,12 @@ function MultiGestureCanvas({
e.preventDefault()}
style={StyleUtils.getFullscreenCenteredContentStyles()}
>
{children}
diff --git a/src/components/MultiGestureCanvas/usePanGesture.ts b/src/components/MultiGestureCanvas/usePanGesture.ts
index 903f384dd525..636913fdf05d 100644
--- a/src/components/MultiGestureCanvas/usePanGesture.ts
+++ b/src/components/MultiGestureCanvas/usePanGesture.ts
@@ -3,6 +3,7 @@ import {Dimensions} from 'react-native';
import type {PanGesture} from 'react-native-gesture-handler';
import {Gesture} from 'react-native-gesture-handler';
import {runOnJS, useDerivedValue, useSharedValue, useWorkletCallback, withDecay, withSpring} from 'react-native-reanimated';
+import * as Browser from '@libs/Browser';
import {SPRING_CONFIG} from './constants';
import type {MultiGestureCanvasVariables} from './types';
import * as MultiGestureCanvasUtils from './utils';
@@ -57,6 +58,8 @@ const usePanGesture = ({
const panVelocityX = useSharedValue(0);
const panVelocityY = useSharedValue(0);
+ const isMobileBrowser = Browser.isMobile();
+
// Disable "swipe down to close" gesture when content is bigger than the canvas
const enableSwipeDownToClose = useDerivedValue(() => canvasSize.height < zoomedContentHeight.value, [canvasSize.height]);
@@ -207,7 +210,9 @@ const usePanGesture = ({
panVelocityY.value = evt.velocityY;
if (!isSwipingDownToClose.value) {
- panTranslateX.value += evt.changeX;
+ if (!isMobileBrowser || (isMobileBrowser && zoomScale.value !== 1)) {
+ panTranslateX.value += evt.changeX;
+ }
}
if (enableSwipeDownToClose.value || isSwipingDownToClose.value) {
diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx
index 46ec51994d90..872464d8a5b0 100644
--- a/src/components/ProcessMoneyReportHoldMenu.tsx
+++ b/src/components/ProcessMoneyReportHoldMenu.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, {useMemo} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import Navigation from '@libs/Navigation/Navigation';
@@ -40,6 +40,9 @@ type ProcessMoneyReportHoldMenuProps = {
/** Type of action handled */
requestType?: ActionHandledType;
+
+ /** Number of transaction of a money request */
+ transactionCount: number;
};
function ProcessMoneyReportHoldMenu({
@@ -52,6 +55,7 @@ function ProcessMoneyReportHoldMenu({
paymentType,
chatReport,
moneyRequestReport,
+ transactionCount,
}: ProcessMoneyReportHoldMenuProps) {
const {translate} = useLocalize();
const isApprove = requestType === CONST.IOU.REPORT_ACTION_TYPE.APPROVE;
@@ -68,12 +72,19 @@ function ProcessMoneyReportHoldMenu({
onClose();
};
+ const promptText = useMemo(() => {
+ if (nonHeldAmount) {
+ return translate(isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount');
+ }
+ return translate(isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount', {transactionCount});
+ }, [nonHeldAmount, transactionCount, translate, isApprove]);
+
return (
onSubmit(false)}
diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index 3c27ec64d130..9693b982ec4a 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -464,6 +464,7 @@ function ReportPreview({
paymentType={paymentType}
chatReport={chatReport}
moneyRequestReport={iouReport}
+ transactionCount={numberOfRequests}
/>
)}
diff --git a/src/components/SelectionList/Search/ReportListItem.tsx b/src/components/SelectionList/Search/ReportListItem.tsx
index 553839ae8457..7119cee06cd9 100644
--- a/src/components/SelectionList/Search/ReportListItem.tsx
+++ b/src/components/SelectionList/Search/ReportListItem.tsx
@@ -88,8 +88,8 @@ function ReportListItem({
return null;
}
- const participantFrom = reportItem.transactions[0].from;
- const participantTo = reportItem.transactions[0].to;
+ const participantFrom = reportItem.from;
+ const participantTo = reportItem.to;
// These values should come as part of the item via SearchUtils.getSections() but ReportListItem is not yet 100% handled
// This will be simplified in future once sorting of ReportListItem is done
diff --git a/src/components/SelectionList/TableListItem.tsx b/src/components/SelectionList/TableListItem.tsx
index 9fc138254f8b..83bc8df36571 100644
--- a/src/components/SelectionList/TableListItem.tsx
+++ b/src/components/SelectionList/TableListItem.tsx
@@ -43,7 +43,7 @@ function TableListItem({
return (
({
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
disabled={isDisabled || item.isDisabledCheckbox}
onPress={handleCheckboxPress}
- style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), item.isDisabledCheckbox && styles.cursorDisabled, styles.mr3]}
+ style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), item.isDisabledCheckbox && styles.cursorDisabled, styles.mr3, item.cursorStyle]}
>
-
+
{item.isSelected && (
(null);
const focusTimeoutRef = useRef(null);
+ const hide = useCallback(() => {
+ onClose();
+ if (shouldClearOnClose) {
+ setValue('');
+ }
+ }, [onClose, shouldClearOnClose]);
+
useFocusEffect(
useCallback(() => {
focusTimeoutRef.current = setTimeout(() => {
@@ -44,8 +52,8 @@ function TextSelectorModal({value, description = '', onValueSelected, isVisible,
+ {!!subtitle && {subtitle}}
&
+
+ /** Whether to clear the input value when the modal closes */
+ shouldClearOnClose?: boolean;
+} & Pick &
TextProps;
type TextPickerProps = {
@@ -39,7 +42,7 @@ type TextPickerProps = {
/** Whether to show the tooltip text */
shouldShowTooltips?: boolean;
-} & Pick &
+} & Pick &
TextProps;
export type {TextSelectorModalProps, TextPickerProps};
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 197b9dc48063..37b5c650e1b8 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -16,6 +16,7 @@ import type {
ChangePolicyParams,
ChangeTypeParams,
CharacterLimitParams,
+ ConfirmHoldExpenseParams,
ConfirmThatParams,
DateShouldBeAfterParams,
DateShouldBeBeforeParams,
@@ -355,6 +356,9 @@ export default {
companyID: 'Company ID',
userID: 'User ID',
disable: 'Disable',
+ initialValue: 'Initial value',
+ currentDate: 'Current date',
+ value: 'Value',
},
location: {
useCurrent: 'Use current location',
@@ -789,8 +793,12 @@ export default {
keepAll: 'Keep all',
confirmApprove: 'Confirm approval amount',
confirmApprovalAmount: "Approve what's not on hold, or approve the entire report.",
+ confirmApprovalAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) =>
+ `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to approve anyway?`,
confirmPay: 'Confirm payment amount',
- confirmPayAmount: "Pay what's not on hold, or pay all out-of-pocket spend.",
+ confirmPayAmount: "Pay what's not on hold, or pay the entire report.",
+ confirmPayAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) =>
+ `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to pay anyway?`,
payOnly: 'Pay only',
approveOnly: 'Approve only',
holdEducationalTitle: 'This expense is on',
@@ -1990,7 +1998,7 @@ export default {
reimburse: 'Reimbursements',
categories: 'Categories',
tags: 'Tags',
- reportFields: 'Report Fields',
+ reportFields: 'Report fields',
taxes: 'Taxes',
bills: 'Bills',
invoices: 'Invoices',
@@ -2433,9 +2441,42 @@ export default {
title: "You haven't created any report fields",
subtitle: 'Add a custom field (text, date, or dropdown) that appears on reports.',
},
- subtitle: "Report fields apply to all spend and can be helpful when you'd like to prompt for extra information",
+ subtitle: "Report fields apply to all spend and can be helpful when you'd like to prompt for extra information.",
disableReportFields: 'Disable report fields',
disableReportFieldsConfirmation: 'Are you sure? Text and date fields will be deleted, and lists will be disabled.',
+ textType: 'Text',
+ dateType: 'Date',
+ dropdownType: 'List',
+ textAlternateText: 'Add a field for free text input.',
+ dateAlternateText: 'Add a calendar for date selection.',
+ dropdownAlternateText: 'Add a list of options to choose from.',
+ nameInputSubtitle: 'Choose a name for the report field.',
+ typeInputSubtitle: 'Choose what type of report field to use.',
+ initialValueInputSubtitle: 'Enter a starting value to show in the report field.',
+ listValuesInputSubtitle: 'These values will appear in your report field dropdown. Enabled values can be selected by members.',
+ listInputSubtitle: 'These values will appear in your report field list. Enabled values can be selected by members.',
+ deleteValue: 'Delete value',
+ deleteValues: 'Delete values',
+ disableValue: 'Disable value',
+ disableValues: 'Disable values',
+ enableValue: 'Enable value',
+ enableValues: 'Enable values',
+ emptyReportFieldsValues: {
+ title: "You haven't created any list values",
+ subtitle: 'Add custom values to appear on reports.',
+ },
+ deleteValuePrompt: 'Are you sure you want to delete this list value?',
+ deleteValuesPrompt: 'Are you sure you want to delete these list values?',
+ listValueRequiredError: 'Please enter a list value name',
+ existingListValueError: 'A list value with this name already exists',
+ editValue: 'Edit value',
+ listValues: 'List values',
+ addValue: 'Add value',
+ existingReportFieldNameError: 'A report field with this name already exists',
+ reportFieldNameRequiredError: 'Please enter a report field name',
+ reportFieldTypeRequiredError: 'Please choose a report field type',
+ reportFieldInitialValueRequiredError: 'Please choose a report field initial value',
+ genericFailureMessage: 'An error occurred while updating the report field. Please try again.',
},
tags: {
tagName: 'Tag name',
@@ -2847,6 +2888,8 @@ export default {
editor: {
descriptionInputLabel: 'Description',
nameInputLabel: 'Name',
+ typeInputLabel: 'Type',
+ initialValueInputLabel: 'Initial value',
nameInputHelpText: "This is the name you'll see on your workspace.",
nameIsRequiredError: "You'll need to give your workspace a name.",
currencyInputLabel: 'Default currency',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index ced35f3f927d..754ca9037d3c 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -15,6 +15,7 @@ import type {
ChangePolicyParams,
ChangeTypeParams,
CharacterLimitParams,
+ ConfirmHoldExpenseParams,
ConfirmThatParams,
DateShouldBeAfterParams,
DateShouldBeBeforeParams,
@@ -346,6 +347,9 @@ export default {
companyID: 'Empresa ID',
userID: 'Usuario ID',
disable: 'Deshabilitar',
+ initialValue: 'Valor inicial',
+ currentDate: 'Fecha actual',
+ value: 'Valor',
},
connectionComplete: {
title: 'Conexión completa',
@@ -783,8 +787,20 @@ export default {
keepAll: 'Mantener todos',
confirmApprove: 'Confirmar importe a aprobar',
confirmApprovalAmount: 'Aprueba lo que no está bloqueado, o aprueba todo el informe.',
+ confirmApprovalAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) =>
+ `${Str.pluralize('Este gasto está bloqueado', 'Estos gastos están bloqueados', transactionCount)}. ¿Quieres ${Str.pluralize(
+ 'aprobar',
+ 'aprobarlos',
+ transactionCount,
+ )} de todos modos?`,
confirmPay: 'Confirmar importe de pago',
- confirmPayAmount: 'Paga lo que no está bloqueado, o paga todos los gastos por cuenta propia.',
+ confirmPayAmount: 'Paga lo que no está bloqueado, o paga el informe completo.',
+ confirmPayAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) =>
+ `${Str.pluralize('Este gasto está bloqueado', 'Estos gastos están bloqueados', transactionCount)}. ¿Quieres ${Str.pluralize(
+ 'pagar',
+ 'pagarlo',
+ transactionCount,
+ )} de todos modos?`,
payOnly: 'Solo pagar',
approveOnly: 'Solo aprobar',
hold: 'Bloquear',
@@ -2467,9 +2483,42 @@ export default {
title: 'No has creado ningún campo de informe',
subtitle: 'Añade un campo personalizado (texto, fecha o desplegable) que aparezca en los informes.',
},
- subtitle: 'Los campos de informe se aplican a todos los gastos y pueden ser útiles cuando desees solicitar información adicional',
+ subtitle: 'Los campos de informe se aplican a todos los gastos y pueden ser útiles cuando quieras solicitar información adicional.',
disableReportFields: 'Desactivar campos de informe',
disableReportFieldsConfirmation: 'Estás seguro? Se eliminarán los campos de texto y fecha y se desactivarán las listas.',
+ textType: 'Texto',
+ dateType: 'Fecha',
+ dropdownType: 'Lista',
+ textAlternateText: 'Añade un campo para introducir texto libre.',
+ dateAlternateText: 'Añade un calendario para la selección de fechas.',
+ dropdownAlternateText: 'Añade una lista de opciones para elegir.',
+ nameInputSubtitle: 'Elige un nombre para el campo del informe.',
+ typeInputSubtitle: 'Elige qué tipo de campo de informe utilizar.',
+ initialValueInputSubtitle: 'Ingresa un valor inicial para mostrar en el campo del informe.',
+ listValuesInputSubtitle: 'Estos valores aparecerán en el desplegable del campo de tu informe. Los miembros pueden seleccionar los valores habilitados.',
+ listInputSubtitle: 'Estos valores aparecerán en la lista de campos de tu informe. Los miembros pueden seleccionar los valores habilitados.',
+ deleteValue: 'Eliminar valor',
+ deleteValues: 'Eliminar valores',
+ disableValue: 'Desactivar valor',
+ disableValues: 'Desactivar valores',
+ enableValue: 'Habilitar valor',
+ enableValues: 'Habilitar valores',
+ emptyReportFieldsValues: {
+ title: 'No has creado ningún valor en la lista',
+ subtitle: 'Añade valores personalizados para que aparezcan en los informes.',
+ },
+ deleteValuePrompt: '¿Estás seguro de que quieres eliminar este valor de la lista?',
+ deleteValuesPrompt: '¿Estás seguro de que quieres eliminar estos valores de la lista?',
+ listValueRequiredError: 'Ingresa un nombre para el valor de la lista',
+ existingListValueError: 'Ya existe un valor en la lista con este nombre',
+ editValue: 'Editar valor',
+ listValues: 'Valores de la lista',
+ addValue: 'Añade valor',
+ existingReportFieldNameError: 'Ya existe un campo de informe con este nombre',
+ reportFieldNameRequiredError: 'Ingresa un nombre de campo de informe',
+ reportFieldTypeRequiredError: 'Elige un tipo de campo de informe',
+ reportFieldInitialValueRequiredError: 'Elige un valor inicial de campo de informe',
+ genericFailureMessage: 'Se ha producido un error al actualizar el campo del informe. Por favor, inténtalo de nuevo.',
},
tags: {
tagName: 'Nombre de etiqueta',
@@ -2881,6 +2930,8 @@ export default {
editor: {
nameInputLabel: 'Nombre',
descriptionInputLabel: 'Descripción',
+ typeInputLabel: 'Tipo',
+ initialValueInputLabel: 'Valor inicial',
nameInputHelpText: 'Este es el nombre que verás en tu espacio de trabajo.',
nameIsRequiredError: 'Debes definir un nombre para tu espacio de trabajo.',
currencyInputLabel: 'Moneda por defecto',
diff --git a/src/languages/types.ts b/src/languages/types.ts
index 7ec56760c2f1..78a711fe8282 100644
--- a/src/languages/types.ts
+++ b/src/languages/types.ts
@@ -298,6 +298,8 @@ type DistanceRateOperationsParams = {count: number};
type ReimbursementRateParams = {unit: Unit};
+type ConfirmHoldExpenseParams = {transactionCount: number};
+
type ChangeFieldParams = {oldValue?: string; newValue: string; fieldName: string};
type ChangePolicyParams = {fromPolicy: string; toPolicy: string};
@@ -350,6 +352,7 @@ export type {
BeginningOfChatHistoryDomainRoomPartOneParams,
CanceledRequestParams,
CharacterLimitParams,
+ ConfirmHoldExpenseParams,
ConfirmThatParams,
DateShouldBeAfterParams,
DateShouldBeBeforeParams,
diff --git a/src/libs/API/parameters/CreateWorkspaceReportFieldParams.ts b/src/libs/API/parameters/CreateWorkspaceReportFieldParams.ts
new file mode 100644
index 000000000000..13844a279905
--- /dev/null
+++ b/src/libs/API/parameters/CreateWorkspaceReportFieldParams.ts
@@ -0,0 +1,6 @@
+type CreateWorkspaceReportFieldParams = {
+ policyID: string;
+ reportFields: string;
+};
+
+export default CreateWorkspaceReportFieldParams;
diff --git a/src/libs/API/parameters/SyncPolicyToNetSuiteParams.ts b/src/libs/API/parameters/SyncPolicyToNetSuiteParams.ts
new file mode 100644
index 000000000000..9227f40997ff
--- /dev/null
+++ b/src/libs/API/parameters/SyncPolicyToNetSuiteParams.ts
@@ -0,0 +1,6 @@
+type SyncPolicyToNetSuiteParams = {
+ policyID: string;
+ idempotencyKey: string;
+};
+
+export default SyncPolicyToNetSuiteParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 2f203a4cfd9a..0b63ec3ed465 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -14,6 +14,7 @@ export type {default as ConnectBankAccountParams} from './ConnectBankAccountPara
export type {default as ConnectPolicyToAccountingIntegrationParams} from './ConnectPolicyToAccountingIntegrationParams';
export type {default as SyncPolicyToQuickbooksOnlineParams} from './SyncPolicyToQuickbooksOnlineParams';
export type {default as SyncPolicyToXeroParams} from './SyncPolicyToXeroParams';
+export type {default as SyncPolicyToNetSuiteParams} from './SyncPolicyToNetSuiteParams';
export type {default as DeleteContactMethodParams} from './DeleteContactMethodParams';
export type {default as DeletePaymentBankAccountParams} from './DeletePaymentBankAccountParams';
export type {default as DeletePaymentCardParams} from './DeletePaymentCardParams';
@@ -237,6 +238,7 @@ export type {default as DeleteMoneyRequestOnSearchParams} from './DeleteMoneyReq
export type {default as HoldMoneyRequestOnSearchParams} from './HoldMoneyRequestOnSearchParams';
export type {default as UnholdMoneyRequestOnSearchParams} from './UnholdMoneyRequestOnSearchParams';
export type {default as UpdateNetSuiteSubsidiaryParams} from './UpdateNetSuiteSubsidiaryParams';
+export type {default as CreateWorkspaceReportFieldParams} from './CreateWorkspaceReportFieldParams';
export type {default as OpenPolicyExpensifyCardsPageParams} from './OpenPolicyExpensifyCardsPageParams';
export type {default as RequestExpensifyCardLimitIncreaseParams} from './RequestExpensifyCardLimitIncreaseParams';
export type {default as UpdateNetSuiteGenericTypeParams} from './UpdateNetSuiteGenericTypeParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index c5d5f1ad1e6e..bd10be8948bc 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -234,6 +234,7 @@ const WRITE_COMMANDS = {
UNHOLD_MONEY_REQUEST_ON_SEARCH: 'UnholdMoneyRequestOnSearch',
REQUEST_REFUND: 'User_RefundPurchase',
UPDATE_NETSUITE_SUBSIDIARY: 'UpdateNetSuiteSubsidiary',
+ CREATE_WORKSPACE_REPORT_FIELD: 'CreatePolicyReportField',
UPDATE_NETSUITE_SYNC_TAX_CONFIGURATION: 'UpdateNetSuiteSyncTaxConfiguration',
UPDATE_NETSUITE_EXPORTER: 'UpdateNetSuiteExporter',
UPDATE_NETSUITE_EXPORT_DATE: 'UpdateNetSuiteExportDate',
@@ -252,6 +253,7 @@ const WRITE_COMMANDS = {
UPDATE_NETSUITE_EXPORT_TO_NEXT_OPEN_PERIOD: 'UpdateNetSuiteExportToNextOpenPeriod',
REQUEST_EXPENSIFY_CARD_LIMIT_INCREASE: 'RequestExpensifyCardLimitIncrease',
CONNECT_POLICY_TO_SAGE_INTACCT: 'ConnectPolicyToSageIntacct',
+ CLEAR_OUTSTANDING_BALANCE: 'ClearOutstandingBalance',
} as const;
type WriteCommand = ValueOf;
@@ -456,6 +458,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY]: Parameters.SetPolicyDistanceRatesDefaultCategoryParams;
[WRITE_COMMANDS.ENABLE_DISTANCE_REQUEST_TAX]: Parameters.SetPolicyDistanceRatesDefaultCategoryParams;
[WRITE_COMMANDS.REQUEST_EXPENSIFY_CARD_LIMIT_INCREASE]: Parameters.RequestExpensifyCardLimitIncreaseParams;
+ [WRITE_COMMANDS.CLEAR_OUTSTANDING_BALANCE]: null;
[WRITE_COMMANDS.UPDATE_POLICY_CONNECTION_CONFIG]: Parameters.UpdatePolicyConnectionConfigParams;
[WRITE_COMMANDS.UPDATE_MANY_POLICY_CONNECTION_CONFIGS]: Parameters.UpdateManyPolicyConnectionConfigurationsParams;
@@ -492,6 +495,10 @@ type WriteCommandParameters = {
// Netsuite parameters
[WRITE_COMMANDS.UPDATE_NETSUITE_SUBSIDIARY]: Parameters.UpdateNetSuiteSubsidiaryParams;
+
+ // Workspace report field parameters
+ [WRITE_COMMANDS.CREATE_WORKSPACE_REPORT_FIELD]: Parameters.CreateWorkspaceReportFieldParams;
+
[WRITE_COMMANDS.UPDATE_NETSUITE_SYNC_TAX_CONFIGURATION]: Parameters.UpdateNetSuiteGenericTypeParams<'enabled', boolean>;
[WRITE_COMMANDS.UPDATE_NETSUITE_EXPORTER]: Parameters.UpdateNetSuiteGenericTypeParams<'email', string>;
[WRITE_COMMANDS.UPDATE_NETSUITE_EXPORT_DATE]: Parameters.UpdateNetSuiteGenericTypeParams<'value', ValueOf>;
@@ -515,6 +522,7 @@ const READ_COMMANDS = {
CONNECT_POLICY_TO_XERO: 'ConnectPolicyToXero',
SYNC_POLICY_TO_QUICKBOOKS_ONLINE: 'SyncPolicyToQuickbooksOnline',
SYNC_POLICY_TO_XERO: 'SyncPolicyToXero',
+ SYNC_POLICY_TO_NETSUITE: 'SyncPolicyToNetSuite',
OPEN_REIMBURSEMENT_ACCOUNT_PAGE: 'OpenReimbursementAccountPage',
OPEN_WORKSPACE_VIEW: 'OpenWorkspaceView',
GET_MAPBOX_ACCESS_TOKEN: 'GetMapboxAccessToken',
@@ -564,6 +572,7 @@ type ReadCommandParameters = {
[READ_COMMANDS.CONNECT_POLICY_TO_XERO]: Parameters.ConnectPolicyToAccountingIntegrationParams;
[READ_COMMANDS.SYNC_POLICY_TO_QUICKBOOKS_ONLINE]: Parameters.SyncPolicyToQuickbooksOnlineParams;
[READ_COMMANDS.SYNC_POLICY_TO_XERO]: Parameters.SyncPolicyToXeroParams;
+ [READ_COMMANDS.SYNC_POLICY_TO_NETSUITE]: Parameters.SyncPolicyToNetSuiteParams;
[READ_COMMANDS.OPEN_REIMBURSEMENT_ACCOUNT_PAGE]: Parameters.OpenReimbursementAccountPageParams;
[READ_COMMANDS.OPEN_WORKSPACE_VIEW]: Parameters.OpenWorkspaceViewParams;
[READ_COMMANDS.GET_MAPBOX_ACCESS_TOKEN]: null;
diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts
index f538e5e719e2..8a8888902e92 100644
--- a/src/libs/DateUtils.ts
+++ b/src/libs/DateUtils.ts
@@ -806,6 +806,19 @@ function doesDateBelongToAPastYear(date: string): boolean {
return transactionYear !== new Date().getFullYear();
}
+/**
+ * Returns a boolean value indicating whether the card has expired.
+ * @param expiryMonth month when card expires (starts from 1 so can be any number between 1 and 12)
+ * @param expiryYear year when card expires
+ */
+
+function isCardExpired(expiryMonth: number, expiryYear: number): boolean {
+ const currentYear = new Date().getFullYear();
+ const currentMonth = new Date().getMonth() + 1;
+
+ return expiryYear < currentYear || (expiryYear === currentYear && expiryMonth < currentMonth);
+}
+
const DateUtils = {
isDate,
formatToDayOfWeek,
@@ -850,6 +863,7 @@ const DateUtils = {
getFormattedReservationRangeDate,
getFormattedTransportDate,
doesDateBelongToAPastYear,
+ isCardExpired,
};
export default DateUtils;
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index e0fb17f882d3..4fd6251ec644 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -361,6 +361,11 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Subscription/PaymentCard/ChangeBillingCurrency').default,
[SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD]: () => require('../../../../pages/settings/Subscription/PaymentCard').default,
[SCREENS.SETTINGS.ADD_PAYMENT_CARD_CHANGE_CURRENCY]: () => require('../../../../pages/settings/PaymentCard/ChangeCurrency').default,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE]: () => require('../../../../pages/workspace/reportFields/WorkspaceCreateReportFieldPage').default,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: () => require('../../../../pages/workspace/reportFields/ReportFieldListValuesPage').default,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE]: () => require('../../../../pages/workspace/reportFields/ReportFieldAddListValuePage').default,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS]: () => require('../../../../pages/workspace/reportFields/ValueSettingsPage').default,
+ [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE]: () => require('../../../../pages/workspace/reportFields/ReportFieldEditValuePage').default,
});
const EnablePaymentsStackNavigator = createModalStackNavigator({
diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
index 1ebbdb5aa0df..3baa0f9f6889 100755
--- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
@@ -102,7 +102,13 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.DISTANCE_RATE_TAX_RATE_EDIT,
SCREENS.WORKSPACE.DISTANCE_RATE_DETAILS,
],
- [SCREENS.WORKSPACE.REPORT_FIELDS]: [],
+ [SCREENS.WORKSPACE.REPORT_FIELDS]: [
+ SCREENS.WORKSPACE.REPORT_FIELDS_CREATE,
+ SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES,
+ SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE,
+ SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS,
+ SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE,
+ ],
[SCREENS.WORKSPACE.EXPENSIFY_CARD]: [],
};
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 01b467fb53de..3f4896e0c5d2 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -525,6 +525,21 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.TAXES_SETTINGS_WORKSPACE_CURRENCY_DEFAULT]: {
path: ROUTES.WORKSPACE_TAXES_SETTINGS_WORKSPACE_CURRENCY_DEFAULT.route,
},
+ [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE]: {
+ path: ROUTES.WORKSPACE_CREATE_REPORT_FIELD.route,
+ },
+ [SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: {
+ path: ROUTES.WORKSPACE_REPORT_FIELD_LIST_VALUES.route,
+ },
+ [SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE]: {
+ path: ROUTES.WORKSPACE_REPORT_FIELD_ADD_VALUE.route,
+ },
+ [SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS]: {
+ path: ROUTES.WORKSPACE_REPORT_FIELD_VALUE_SETTINGS.route,
+ },
+ [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE]: {
+ path: ROUTES.WORKSPACE_REPORT_FIELD_EDIT_VALUE.route,
+ },
[SCREENS.REIMBURSEMENT_ACCOUNT]: {
path: ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.route,
exact: true,
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 0a2809d97208..ac9710b65d19 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -268,6 +268,23 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.TAXES_SETTINGS_WORKSPACE_CURRENCY_DEFAULT]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS]: {
+ policyID: string;
+ valueIndex: number;
+ };
+ [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE]: {
+ policyID: string;
+ valueIndex: number;
+ };
[SCREENS.WORKSPACE.MEMBER_DETAILS]: {
policyID: string;
accountID: string;
diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts
index faea5965fee4..aafc38a9040b 100644
--- a/src/libs/Permissions.ts
+++ b/src/libs/Permissions.ts
@@ -60,6 +60,10 @@ function canUseNetSuiteUSATax(betas: OnyxEntry): boolean {
return !!betas?.includes(CONST.BETAS.NETSUITE_USA_TAX) || canUseAllBetas(betas);
}
+function canUseCommentLinking(betas: OnyxEntry): boolean {
+ return !!betas?.includes(CONST.BETAS.COMMENT_LINKING) || canUseAllBetas(betas);
+}
+
/**
* Link previews are temporarily disabled.
*/
@@ -82,4 +86,5 @@ export default {
canUseReportFieldsFeature,
canUseWorkspaceFeeds,
canUseNetSuiteUSATax,
+ canUseCommentLinking,
};
diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts
index 8ba468e87ed0..8bdf0cb1d5fe 100644
--- a/src/libs/PersonalDetailsUtils.ts
+++ b/src/libs/PersonalDetailsUtils.ts
@@ -157,6 +157,7 @@ function getPersonalDetailsOnyxDataForOptimisticUsers(newLogins: string[], newAc
login,
accountID,
displayName: LocalePhoneNumber.formatPhoneNumber(login),
+ isOptimisticPersonalDetail: true,
};
/**
diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts
index 119c20179a0c..cd641cdc8c90 100644
--- a/src/libs/SearchUtils.ts
+++ b/src/libs/SearchUtils.ts
@@ -166,6 +166,8 @@ function getReportSections(data: OnyxTypes.SearchResults['data'], metadata: Onyx
reportIDToTransactions[reportKey] = {
...value,
+ from: data.personalDetailsList?.[value.accountID],
+ to: data.personalDetailsList?.[value.managerID],
transactions,
};
} else if (key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)) {
@@ -199,7 +201,7 @@ function getReportSections(data: OnyxTypes.SearchResults['data'], metadata: Onyx
if (reportIDToTransactions[reportKey]?.transactions) {
reportIDToTransactions[reportKey].transactions.push(transaction);
} else {
- reportIDToTransactions[reportKey] = {transactions: [transaction]};
+ reportIDToTransactions[reportKey].transactions = [transaction];
}
}
}
diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts
index 8569a3f03128..c807d0ca4a7e 100644
--- a/src/libs/SubscriptionUtils.ts
+++ b/src/libs/SubscriptionUtils.ts
@@ -81,6 +81,7 @@ Onyx.connect({
let retryBillingSuccessful: OnyxEntry;
Onyx.connect({
key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL,
+ initWithStoredValues: false,
callback: (value) => {
if (value === undefined) {
return;
diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts
index ead786b8eafd..e937979ae7b9 100644
--- a/src/libs/TripReservationUtils.ts
+++ b/src/libs/TripReservationUtils.ts
@@ -1,3 +1,4 @@
+import type {EReceiptColorName} from '@styles/utils/types';
import * as Expensicons from '@src/components/Icon/Expensicons';
import CONST from '@src/CONST';
import type {Reservation, ReservationType} from '@src/types/onyx/Transaction';
@@ -24,4 +25,26 @@ function getReservationsFromTripTransactions(transactions: Transaction[]): Reser
.flat();
}
-export {getTripReservationIcon, getReservationsFromTripTransactions};
+type TripEReceiptData = {
+ /** Icon asset associated with the type of trip reservation */
+ tripIcon?: IconAsset;
+
+ /** EReceipt background color associated with the type of trip reservation */
+ tripBGColor?: EReceiptColorName;
+};
+
+function getTripEReceiptData(transaction?: Transaction): TripEReceiptData {
+ const reservationType = transaction ? transaction.receipt?.reservationList?.[0]?.type : '';
+
+ switch (reservationType) {
+ case CONST.RESERVATION_TYPE.FLIGHT:
+ case CONST.RESERVATION_TYPE.CAR:
+ return {tripIcon: Expensicons.Plane, tripBGColor: CONST.ERECEIPT_COLORS.PINK};
+ case CONST.RESERVATION_TYPE.HOTEL:
+ return {tripIcon: Expensicons.Bed, tripBGColor: CONST.ERECEIPT_COLORS.YELLOW};
+ default:
+ return {};
+ }
+}
+
+export {getTripReservationIcon, getReservationsFromTripTransactions, getTripEReceiptData};
diff --git a/src/libs/WorkspaceReportFieldsUtils.ts b/src/libs/WorkspaceReportFieldsUtils.ts
new file mode 100644
index 000000000000..0cc3cae24a23
--- /dev/null
+++ b/src/libs/WorkspaceReportFieldsUtils.ts
@@ -0,0 +1,70 @@
+import type {FormInputErrors} from '@components/Form/types';
+import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import type ONYXKEYS from '@src/ONYXKEYS';
+import type {InputID} from '@src/types/form/WorkspaceReportFieldsForm';
+import type {PolicyReportFieldType} from '@src/types/onyx/Policy';
+import * as ErrorUtils from './ErrorUtils';
+import * as Localize from './Localize';
+import * as ValidationUtils from './ValidationUtils';
+
+/**
+ * Gets the translation key for the report field type.
+ */
+function getReportFieldTypeTranslationKey(reportFieldType: PolicyReportFieldType): TranslationPaths {
+ const typeTranslationKeysStrategy: Record = {
+ [CONST.REPORT_FIELD_TYPES.TEXT]: 'workspace.reportFields.textType',
+ [CONST.REPORT_FIELD_TYPES.DATE]: 'workspace.reportFields.dateType',
+ [CONST.REPORT_FIELD_TYPES.LIST]: 'workspace.reportFields.dropdownType',
+ };
+
+ return typeTranslationKeysStrategy[reportFieldType];
+}
+
+/**
+ * Gets the translation key for the alternative text for the report field.
+ */
+function getReportFieldAlternativeTextTranslationKey(reportFieldType: PolicyReportFieldType): TranslationPaths {
+ const typeTranslationKeysStrategy: Record = {
+ [CONST.REPORT_FIELD_TYPES.TEXT]: 'workspace.reportFields.textAlternateText',
+ [CONST.REPORT_FIELD_TYPES.DATE]: 'workspace.reportFields.dateAlternateText',
+ [CONST.REPORT_FIELD_TYPES.LIST]: 'workspace.reportFields.dropdownAlternateText',
+ };
+
+ return typeTranslationKeysStrategy[reportFieldType];
+}
+
+/**
+ * Validates the list value name.
+ */
+function validateReportFieldListValueName(
+ valueName: string,
+ priorValueName: string,
+ listValues: string[],
+ inputID: InputID,
+): FormInputErrors {
+ const errors: FormInputErrors = {};
+
+ if (!ValidationUtils.isRequiredFulfilled(valueName)) {
+ errors[inputID] = Localize.translateLocal('workspace.reportFields.listValueRequiredError');
+ } else if (priorValueName !== valueName && listValues.some((currentValueName) => currentValueName === valueName)) {
+ errors[inputID] = Localize.translateLocal('workspace.reportFields.existingListValueError');
+ } else if ([...valueName].length > CONST.WORKSPACE_REPORT_FIELD_POLICY_MAX_LENGTH) {
+ // Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 code units.
+ ErrorUtils.addErrorMessage(
+ errors,
+ inputID,
+ Localize.translateLocal('common.error.characterLimitExceedCounter', {length: [...valueName].length, limit: CONST.WORKSPACE_REPORT_FIELD_POLICY_MAX_LENGTH}),
+ );
+ }
+
+ return errors;
+}
+/**
+ * Generates a field ID based on the field name.
+ */
+function generateFieldID(name: string) {
+ return `field_id_${name.replace(CONST.REGEX.ANY_SPACE, '_').toUpperCase()}`;
+}
+
+export {getReportFieldTypeTranslationKey, getReportFieldAlternativeTextTranslationKey, validateReportFieldListValueName, generateFieldID};
diff --git a/src/libs/actions/Policy/ReportFields.ts b/src/libs/actions/Policy/ReportFields.ts
new file mode 100644
index 000000000000..220432cbc3c6
--- /dev/null
+++ b/src/libs/actions/Policy/ReportFields.ts
@@ -0,0 +1,209 @@
+import type {NullishDeep, OnyxCollection} from 'react-native-onyx';
+import Onyx from 'react-native-onyx';
+import * as API from '@libs/API';
+import type {CreateWorkspaceReportFieldParams} from '@libs/API/parameters';
+import {WRITE_COMMANDS} from '@libs/API/types';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import * as ReportUtils from '@libs/ReportUtils';
+import {generateFieldID} from '@libs/WorkspaceReportFieldsUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {WorkspaceReportFieldsForm} from '@src/types/form/WorkspaceReportFieldsForm';
+import INPUT_IDS from '@src/types/form/WorkspaceReportFieldsForm';
+import type {Policy, PolicyReportField} from '@src/types/onyx';
+import type {OnyxData} from '@src/types/onyx/Request';
+
+let listValues: string[];
+let disabledListValues: boolean[];
+Onyx.connect({
+ key: ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT,
+ callback: (value) => {
+ if (!value) {
+ return;
+ }
+
+ listValues = value[INPUT_IDS.LIST_VALUES] ?? [];
+ disabledListValues = value[INPUT_IDS.DISABLED_LIST_VALUES] ?? [];
+ },
+});
+
+const allPolicies: OnyxCollection = {};
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.POLICY,
+ callback: (value, key) => {
+ if (!key) {
+ return;
+ }
+
+ if (value === null || value === undefined) {
+ // If we are deleting a policy, we have to check every report linked to that policy
+ // and unset the draft indicator (pencil icon) alongside removing any draft comments. Clearing these values will keep the newly archived chats from being displayed in the LHN.
+ // More info: https://github.com/Expensify/App/issues/14260
+ const policyID = key.replace(ONYXKEYS.COLLECTION.POLICY, '');
+ const policyReports = ReportUtils.getAllPolicyReports(policyID);
+ const cleanUpMergeQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT}${string}`, NullishDeep> = {};
+ const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${string}` | `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {};
+ policyReports.forEach((policyReport) => {
+ if (!policyReport) {
+ return;
+ }
+ const {reportID} = policyReport;
+ cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] = null;
+ cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`] = null;
+ });
+ Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, cleanUpMergeQueries);
+ Onyx.multiSet(cleanUpSetQueries);
+ delete allPolicies[key];
+ return;
+ }
+
+ allPolicies[key] = value;
+ },
+});
+
+/**
+ * Sets the initial form values for the workspace report fields form.
+ */
+function setInitialCreateReportFieldsForm() {
+ Onyx.set(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT, {
+ [INPUT_IDS.INITIAL_VALUE]: '',
+ });
+}
+
+/**
+ * Creates a new list value in the workspace report fields form.
+ */
+function createReportFieldsListValue(valueName: string) {
+ Onyx.merge(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT, {
+ [INPUT_IDS.LIST_VALUES]: [...listValues, valueName],
+ [INPUT_IDS.DISABLED_LIST_VALUES]: [...disabledListValues, false],
+ });
+}
+
+/**
+ * Renames a list value in the workspace report fields form.
+ */
+function renameReportFieldsListValue(valueIndex: number, newValueName: string) {
+ const listValuesCopy = [...listValues];
+ listValuesCopy[valueIndex] = newValueName;
+
+ Onyx.merge(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT, {
+ [INPUT_IDS.LIST_VALUES]: listValuesCopy,
+ });
+}
+
+/**
+ * Sets the enabled state of a list value in the workspace report fields form.
+ */
+function setReportFieldsListValueEnabled(valueIndexes: number[], enabled: boolean) {
+ const disabledListValuesCopy = [...disabledListValues];
+
+ valueIndexes.forEach((valueIndex) => {
+ disabledListValuesCopy[valueIndex] = !enabled;
+ });
+
+ Onyx.merge(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT, {
+ [INPUT_IDS.DISABLED_LIST_VALUES]: disabledListValuesCopy,
+ });
+}
+
+/**
+ * Deletes a list value from the workspace report fields form.
+ */
+function deleteReportFieldsListValue(valueIndexes: number[]) {
+ const listValuesCopy = [...listValues];
+ const disabledListValuesCopy = [...disabledListValues];
+
+ valueIndexes
+ .sort((a, b) => b - a)
+ .forEach((valueIndex) => {
+ listValuesCopy.splice(valueIndex, 1);
+ disabledListValuesCopy.splice(valueIndex, 1);
+ });
+
+ Onyx.merge(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT, {
+ [INPUT_IDS.LIST_VALUES]: listValuesCopy,
+ [INPUT_IDS.DISABLED_LIST_VALUES]: disabledListValuesCopy,
+ });
+}
+
+type CreateReportFieldArguments = Pick;
+
+/**
+ * Creates a new report field.
+ */
+function createReportField(policyID: string, {name, type, initialValue}: CreateReportFieldArguments) {
+ const previousFieldList = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]?.fieldList ?? {};
+ const fieldID = generateFieldID(name);
+ const fieldKey = ReportUtils.getReportFieldKey(fieldID);
+ const newReportField: PolicyReportField = {
+ name,
+ type,
+ defaultValue: initialValue,
+ values: listValues,
+ disabledOptions: disabledListValues,
+ fieldID,
+ orderWeight: Object.keys(previousFieldList).length + 1,
+ deletable: false,
+ value: type === CONST.REPORT_FIELD_TYPES.LIST ? CONST.REPORT_FIELD_TYPES.LIST : null,
+ keys: [],
+ externalIDs: [],
+ isTax: false,
+ };
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ onyxMethod: Onyx.METHOD.MERGE,
+ value: {
+ fieldList: {
+ [fieldKey]: newReportField,
+ },
+ pendingFields: {
+ [fieldKey]: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ },
+ errorFields: null,
+ },
+ },
+ ],
+ successData: [
+ {
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ onyxMethod: Onyx.METHOD.MERGE,
+ value: {
+ pendingFields: {
+ [fieldKey]: null,
+ },
+ errorFields: null,
+ },
+ },
+ ],
+ failureData: [
+ {
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ onyxMethod: Onyx.METHOD.MERGE,
+ value: {
+ fieldList: {
+ [fieldKey]: null,
+ },
+ pendingFields: {
+ [fieldKey]: null,
+ },
+ errorFields: {
+ [fieldKey]: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.reportFields.genericFailureMessage'),
+ },
+ },
+ },
+ ],
+ };
+ const parameters: CreateWorkspaceReportFieldParams = {
+ policyID,
+ reportFields: JSON.stringify([newReportField]),
+ };
+
+ API.write(WRITE_COMMANDS.CREATE_WORKSPACE_REPORT_FIELD, parameters, onyxData);
+}
+
+export type {CreateReportFieldArguments};
+
+export {setInitialCreateReportFieldsForm, createReportFieldsListValue, renameReportFieldsListValue, setReportFieldsListValueEnabled, deleteReportFieldsListValue, createReportField};
diff --git a/src/libs/actions/Subscription.ts b/src/libs/actions/Subscription.ts
index 19a3bf0c547e..beed2b1b2962 100644
--- a/src/libs/actions/Subscription.ts
+++ b/src/libs/actions/Subscription.ts
@@ -231,4 +231,60 @@ function clearUpdateSubscriptionSizeError() {
});
}
-export {openSubscriptionPage, updateSubscriptionAutoRenew, updateSubscriptionAddNewUsersAutomatically, updateSubscriptionSize, clearUpdateSubscriptionSizeError, updateSubscriptionType};
+function clearOutstandingBalance() {
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_PENDING,
+ value: true,
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_PENDING,
+ value: false,
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL,
+ value: true,
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED,
+ value: false,
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_PENDING,
+ value: false,
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL,
+ value: false,
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED,
+ value: true,
+ },
+ ],
+ };
+
+ API.write(WRITE_COMMANDS.CLEAR_OUTSTANDING_BALANCE, null, onyxData);
+}
+
+export {
+ openSubscriptionPage,
+ updateSubscriptionAutoRenew,
+ updateSubscriptionAddNewUsersAutomatically,
+ updateSubscriptionSize,
+ clearUpdateSubscriptionSizeError,
+ updateSubscriptionType,
+ clearOutstandingBalance,
+};
diff --git a/src/libs/actions/connections/index.ts b/src/libs/actions/connections/index.ts
index 872e82951834..fd6440c3a92c 100644
--- a/src/libs/actions/connections/index.ts
+++ b/src/libs/actions/connections/index.ts
@@ -132,6 +132,9 @@ function getSyncConnectionParameters(connectionName: PolicyConnectionName) {
case CONST.POLICY.CONNECTIONS.NAME.XERO: {
return {readCommand: READ_COMMANDS.SYNC_POLICY_TO_XERO, stageInProgress: CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.STARTING_IMPORT_XERO};
}
+ case CONST.POLICY.CONNECTIONS.NAME.NETSUITE: {
+ return {readCommand: READ_COMMANDS.SYNC_POLICY_TO_NETSUITE, stageInProgress: CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.NETSUITE_SYNC_CONNECTION};
+ }
default:
return undefined;
}
diff --git a/src/pages/InviteReportParticipantsPage.tsx b/src/pages/InviteReportParticipantsPage.tsx
index 78cb5dfcd991..91fdd903ec3a 100644
--- a/src/pages/InviteReportParticipantsPage.tsx
+++ b/src/pages/InviteReportParticipantsPage.tsx
@@ -197,7 +197,6 @@ function InviteReportParticipantsPage({betas, personalDetails, report, didScreen
onSubmit={inviteUsers}
containerStyles={[styles.flexReset, styles.flexGrow0, styles.flexShrink0, styles.flexBasisAuto]}
enabledWhenOffline
- disablePressOnEnter
/>
),
[selectedOptions.length, inviteUsers, translate, styles],
diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx
index afa57755ad70..8a807655ae57 100644
--- a/src/pages/RoomInvitePage.tsx
+++ b/src/pages/RoomInvitePage.tsx
@@ -266,7 +266,6 @@ function RoomInvitePage({
onSubmit={inviteUsers}
containerStyles={[styles.flexReset, styles.flexGrow0, styles.flexShrink0, styles.flexBasisAuto, styles.mb5, styles.ph5]}
enabledWhenOffline
- disablePressOnEnter
isAlertVisible={false}
/>
diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
index 136d8e5b59eb..5ab38cbf2e7e 100755
--- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
+++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
@@ -166,7 +166,6 @@ function BaseReportActionContextMenu({
disabledIndexes,
maxIndex: filteredContextMenuActions.length - 1,
isActive: shouldEnableArrowNavigation,
- disableCyclicTraversal: true,
});
/**
diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
index bf634b4ac8ae..0e29e7496def 100644
--- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
@@ -19,6 +19,7 @@ import * as Localize from '@libs/Localize';
import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage';
import Navigation from '@libs/Navigation/Navigation';
import {parseHtmlToMarkdown, parseHtmlToText} from '@libs/OnyxAwareParser';
+import Permissions from '@libs/Permissions';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
@@ -425,6 +426,10 @@ const ContextMenuActions: ContextMenuAction[] = [
successIcon: Expensicons.Checkmark,
successTextTranslateKey: 'reportActionContextMenu.copied',
shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget) => {
+ if (!Permissions.canUseCommentLinking(betas)) {
+ return false;
+ }
+
const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction);
// Only hide the copylink menu item when context menu is opened over img element.
diff --git a/src/pages/settings/Subscription/CardSection/BillingBanner/BillingBanner.tsx b/src/pages/settings/Subscription/CardSection/BillingBanner/BillingBanner.tsx
index 4587dfee2fe6..bbb06dac4549 100644
--- a/src/pages/settings/Subscription/CardSection/BillingBanner/BillingBanner.tsx
+++ b/src/pages/settings/Subscription/CardSection/BillingBanner/BillingBanner.tsx
@@ -1,9 +1,10 @@
-import React from 'react';
+import React, {useMemo} from 'react';
import type {StyleProp, TextStyle, ViewStyle} from 'react-native';
import {View} from 'react-native';
import type {ValueOf} from 'type-fest';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
+import {PressableWithoutFeedback} from '@components/Pressable';
import Text from '@components/Text';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -35,12 +36,50 @@ type BillingBannerProps = {
/** An icon to be rendered instead of the RBR / GBR indicator. */
rightIcon?: IconAsset;
+
+ /** Callback to be called when the right icon is pressed. */
+ onRightIconPress?: () => void;
+
+ /** Accessibility label for the right icon. */
+ rightIconAccessibilityLabel?: string;
};
-function BillingBanner({title, subtitle, icon, brickRoadIndicator, style, titleStyle, subtitleStyle, rightIcon}: BillingBannerProps) {
+function BillingBanner({title, subtitle, icon, brickRoadIndicator, style, titleStyle, subtitleStyle, rightIcon, onRightIconPress, rightIconAccessibilityLabel}: BillingBannerProps) {
const styles = useThemeStyles();
const theme = useTheme();
+ const rightIconComponent = useMemo(() => {
+ if (rightIcon) {
+ return onRightIconPress && rightIconAccessibilityLabel ? (
+
+
+
+ ) : (
+
+ );
+ }
+
+ return (
+ !!brickRoadIndicator && (
+
+ )
+ );
+ }, [brickRoadIndicator, onRightIconPress, rightIcon, rightIconAccessibilityLabel, styles.touchableButtonImage, theme.danger, theme.icon, theme.success]);
+
return (
{title} : title}
{typeof subtitle === 'string' ? {subtitle} : subtitle}
- {rightIcon ? (
-
- ) : (
- !!brickRoadIndicator && (
-
- )
- )}
+ {rightIconComponent}
);
}
diff --git a/src/pages/settings/Subscription/CardSection/BillingBanner/SubscriptionBillingBanner.tsx b/src/pages/settings/Subscription/CardSection/BillingBanner/SubscriptionBillingBanner.tsx
index dce215e7dbbc..d949e2699e44 100644
--- a/src/pages/settings/Subscription/CardSection/BillingBanner/SubscriptionBillingBanner.tsx
+++ b/src/pages/settings/Subscription/CardSection/BillingBanner/SubscriptionBillingBanner.tsx
@@ -14,7 +14,7 @@ type SubscriptionBillingBannerProps = Omit
);
}
diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx
index e873569e4583..f3b78b3f2b95 100644
--- a/src/pages/settings/Subscription/CardSection/CardSection.tsx
+++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx
@@ -1,6 +1,7 @@
-import React, {useCallback, useMemo, useState} from 'react';
+import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
+import Button from '@components/Button';
import ConfirmModal from '@components/ConfirmModal';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
@@ -8,6 +9,7 @@ import MenuItem from '@components/MenuItem';
import Section from '@components/Section';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
import useSubscriptionPlan from '@hooks/useSubscriptionPlan';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -15,6 +17,7 @@ import * as User from '@libs/actions/User';
import DateUtils from '@libs/DateUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as SubscriptionUtils from '@libs/SubscriptionUtils';
+import * as Subscription from '@userActions/Subscription';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -24,6 +27,7 @@ import SubscriptionBillingBanner from './BillingBanner/SubscriptionBillingBanner
import TrialStartedBillingBanner from './BillingBanner/TrialStartedBillingBanner';
import CardSectionActions from './CardSectionActions';
import CardSectionDataEmpty from './CardSectionDataEmpty';
+import type {BillingStatusResult} from './utils';
import CardSectionUtils from './utils';
function CardSection() {
@@ -35,8 +39,10 @@ function CardSection() {
const [privateSubscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION);
const [fundList] = useOnyx(ONYXKEYS.FUND_LIST);
const subscriptionPlan = useSubscriptionPlan();
- const [network] = useOnyx(ONYXKEYS.NETWORK);
-
+ const [subscriptionRetryBillingStatusPending] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_PENDING);
+ const [subscriptionRetryBillingStatusSuccessful] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL);
+ const [subscriptionRetryBillingStatusFailed] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED);
+ const {isOffline} = useNetwork();
const defaultCard = useMemo(() => Object.values(fundList ?? {}).find((card) => card.accountData?.additionalData?.isBillingCard), [fundList]);
const cardMonth = useMemo(() => DateUtils.getMonthNames(preferredLocale)[(defaultCard?.accountData?.cardMonth ?? 1) - 1], [defaultCard?.accountData?.cardMonth, preferredLocale]);
@@ -47,12 +53,24 @@ function CardSection() {
Navigation.resetToHome();
}, []);
- const billingStatus = CardSectionUtils.getBillingStatus(translate, defaultCard?.accountData?.cardNumber ?? '');
+ const [billingStatus, setBillingStatus] = useState(CardSectionUtils.getBillingStatus(translate, defaultCard?.accountData ?? {}));
const nextPaymentDate = !isEmptyObject(privateSubscription) ? CardSectionUtils.getNextBillingDate() : undefined;
const sectionSubtitle = defaultCard && !!nextPaymentDate ? translate('subscription.cardSection.cardNextPayment', {nextPaymentDate}) : translate('subscription.cardSection.subtitle');
+ useEffect(() => {
+ setBillingStatus(CardSectionUtils.getBillingStatus(translate, defaultCard?.accountData ?? {}));
+ }, [subscriptionRetryBillingStatusPending, subscriptionRetryBillingStatusSuccessful, subscriptionRetryBillingStatusFailed, translate, defaultCard?.accountData]);
+
+ const handleRetryPayment = () => {
+ Subscription.clearOutstandingBalance();
+ };
+
+ const handleBillingBannerClose = () => {
+ setBillingStatus(undefined);
+ };
+
let BillingBanner: React.ReactNode | undefined;
if (CardSectionUtils.shouldShowPreTrialBillingBanner()) {
BillingBanner = ;
@@ -66,6 +84,8 @@ function CardSection() {
isError={billingStatus.isError}
icon={billingStatus.icon}
rightIcon={billingStatus.rightIcon}
+ onRightIconPress={handleBillingBannerClose}
+ rightIconAccessibilityLabel={translate('common.close')}
/>
);
}
@@ -105,6 +125,18 @@ function CardSection() {
{isEmptyObject(defaultCard?.accountData) && }
+
+ {billingStatus?.isRetryAvailable !== undefined && (
+
+ )}
+
{!!account?.hasPurchases && (
)}
+
{!!(subscriptionPlan && account?.isEligibleForRefund) && (