From d8eb4262395888cb5147c63fd70fa6a0148d9163 Mon Sep 17 00:00:00 2001 From: christianwen Date: Fri, 8 Dec 2023 17:13:46 +0700 Subject: [PATCH 001/219] fix: 10731 --- src/components/Composer/index.js | 5 +++++ .../ComposerWithSuggestions.js | 18 +++++++++++------- .../ReportActionCompose/ReportActionCompose.js | 6 +----- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 4bb3df5c1b85..bea38fc38414 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -86,6 +86,8 @@ const propTypes = { /** Whether the sull composer is open */ isComposerFullSize: PropTypes.bool, + showSoftInputOnFocus: PropTypes.bool, + ...withLocalizePropTypes, }; @@ -113,6 +115,7 @@ const defaultProps = { checkComposerVisibility: () => false, isReportActionCompose: false, isComposerFullSize: false, + showSoftInputOnFocus: true, }; /** @@ -164,6 +167,7 @@ function Composer({ selection: selectionProp, isReportActionCompose, isComposerFullSize, + showSoftInputOnFocus, ...props }) { const theme = useTheme(); @@ -445,6 +449,7 @@ function Composer({ forwardedRef={forwardedRef} defaultValue={defaultValue} autoFocus={autoFocus} + inputMode={!showSoftInputOnFocus && 'none'} /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} onSelectionChange={addCursorPositionToSelectionChange} diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index f99bd7ab7d9d..5cd9fe8be1d5 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -49,10 +49,6 @@ const debouncedBroadcastUserIsTyping = _.debounce((reportID) => { const willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutsideFunc(); -// We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will -// prevent auto focus on existing chat for mobile device -const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); - /** * This component holds the value and selection state. * If a component really needs access to these state values it should be put here. @@ -120,9 +116,9 @@ function ComposerWithSuggestions({ const {isSmallScreenWidth} = useWindowDimensions(); const maxComposerLines = isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES; - const isEmptyChat = useMemo(() => _.size(reportActions) === 1, [reportActions]); - const parentAction = ReportActionsUtils.getParentReportAction(report); - const shouldAutoFocus = !modal.isVisible && (shouldFocusInputOnScreenFocus || (isEmptyChat && !ReportActionsUtils.isTransactionThread(parentAction))) && shouldShowComposeInput; + const shouldAutoFocus = !modal.isVisible && shouldShowComposeInput; + + const [showSoftInputOnFocus, setShowSoftInputOnFocus] = useState(false); const valueRef = useRef(value); valueRef.current = value; @@ -556,6 +552,14 @@ function ComposerWithSuggestions({ setComposerHeight(composerLayoutHeight); }} onScroll={hideSuggestionMenu} + showSoftInputOnFocus={showSoftInputOnFocus} + onTouchStart={() => { + if (showSoftInputOnFocus) { + return; + } + + setShowSoftInputOnFocus(true); + }} /> diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index 2632324a963f..885d566cf450 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -89,10 +89,6 @@ const defaultProps = { ...withCurrentUserPersonalDetailsDefaultProps, }; -// We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will -// prevent auto focus on existing chat for mobile device -const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); - const willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutsideFunc(); function ReportActionCompose({ @@ -121,7 +117,7 @@ function ReportActionCompose({ */ const [isFocused, setIsFocused] = useState(() => { const initialModalState = getModalState(); - return shouldFocusInputOnScreenFocus && shouldShowComposeInput && !initialModalState.isVisible && !initialModalState.willAlertModalBecomeVisible; + return shouldShowComposeInput && !initialModalState.isVisible && !initialModalState.willAlertModalBecomeVisible; }); const [isFullComposerAvailable, setIsFullComposerAvailable] = useState(isComposerFullSize); From a5f99b738732b9d3609bb7c5ef4a826fabf8169b Mon Sep 17 00:00:00 2001 From: christianwen Date: Mon, 11 Dec 2023 11:42:09 +0700 Subject: [PATCH 002/219] lint fix --- .../ComposerWithSuggestions/ComposerWithSuggestions.js | 1 - src/pages/home/report/ReportActionCompose/ReportActionCompose.js | 1 - 2 files changed, 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index d39ad7e8ff0c..ad8e5daa6256 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -10,7 +10,6 @@ import useDebounce from '@hooks/useDebounce'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; import compose from '@libs/compose'; import * as ComposerUtils from '@libs/ComposerUtils'; import getDraftComment from '@libs/ComposerUtils/getDraftComment'; diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index 885d566cf450..cd49f1b0585d 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -15,7 +15,6 @@ import {usePersonalDetails, withNetwork} from '@components/OnyxProvider'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; import compose from '@libs/compose'; import getDraftComment from '@libs/ComposerUtils/getDraftComment'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; From f6e2e28c0d57cacfeed4c2c615936b42122a1c59 Mon Sep 17 00:00:00 2001 From: christianwen Date: Tue, 2 Jan 2024 16:00:51 +0700 Subject: [PATCH 003/219] lint fix --- src/components/Composer/index.tsx | 2 +- src/components/Composer/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 849bf14594df..d6249a1e3dcc 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -363,7 +363,7 @@ function Composer( value={value} defaultValue={defaultValue} autoFocus={autoFocus} - inputMode={!showSoftInputOnFocus ? 'none': 'text'} + inputMode={!showSoftInputOnFocus ? 'none' : 'text'} /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} onSelectionChange={addCursorPositionToSelectionChange} diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 208def7fabab..92ecc5038be4 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -72,7 +72,7 @@ type ComposerProps = { /** Should make the input only scroll inside the element avoid scroll out to parent */ shouldContainScroll?: boolean; - showSoftInputOnFocus?: boolean, + showSoftInputOnFocus?: boolean; }; export type {TextSelection, ComposerProps}; From 7e930b8e104ba234ea360d9db1750299f5816cf3 Mon Sep 17 00:00:00 2001 From: christianwen Date: Thu, 25 Jan 2024 17:49:48 +0700 Subject: [PATCH 004/219] lint fix --- .../ComposerWithSuggestions/ComposerWithSuggestions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 535344e38786..c80016519ee6 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -120,7 +120,7 @@ function ComposerWithSuggestions({ const {isSmallScreenWidth} = useWindowDimensions(); const maxComposerLines = isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES; - + const parentReportAction = lodashGet(parentReportActions, [report.parentReportActionID]); const shouldAutoFocus = !modal.isVisible && shouldShowComposeInput; From 5dc692229b23addada41e7486d63a181d53f91dd Mon Sep 17 00:00:00 2001 From: christianwen Date: Tue, 5 Mar 2024 16:14:36 +0700 Subject: [PATCH 005/219] fix focus input --- src/components/Composer/index.tsx | 12 ++++++ .../ComposerWithSuggestions.tsx | 38 +++++++------------ .../ReportActionCompose.tsx | 7 +--- 3 files changed, 26 insertions(+), 31 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 51bebd710e62..ffdb6825a211 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -327,6 +327,18 @@ function Composer( [numberOfLines, scrollStyleMemo, styles.rtlTextRenderForSafari, style, StyleUtils, isComposerFullSize], ); + useEffect(() => { + if (!showSoftInputOnFocus) { + return; + } + textInput.current?.blur(); + // On Safari when changing inputMode from none to text, the keyboard will cover the view + // We need the logic to re-focus to trigger the keyboard to open below the view + setTimeout(() => { + textInput.current?.focus(); + }, 2000); + }, [showSoftInputOnFocus]); + return ( <> ; - /** The parent report actions for the report */ - parentReportActions: OnyxEntry; - /** The modal state */ modal: OnyxEntry; @@ -155,21 +150,11 @@ type ComposerWithSuggestionsProps = ComposerWithSuggestionsOnyxProps & /** Whether the edit is focused */ editFocused: boolean; - /** Wheater chat is empty */ - isEmptyChat?: boolean; - /** The last report action */ lastReportAction?: OnyxTypes.ReportAction; /** Whether to include chronos */ includeChronos?: boolean; - - /** The parent report action ID */ - parentReportActionID?: string; - - /** The parent report ID */ - // eslint-disable-next-line react/no-unused-prop-types -- its used in the withOnyx HOC - parentReportID: string | undefined; }; const {RNTextInputReset} = NativeModules; @@ -196,15 +181,12 @@ function ComposerWithSuggestions( // Onyx modal, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, - parentReportActions, numberOfLines, // Props: Report reportID, includeChronos, - isEmptyChat, lastReportAction, - parentReportActionID, // Focus onFocus, @@ -262,7 +244,6 @@ function ComposerWithSuggestions( const {isSmallScreenWidth} = useWindowDimensions(); const maxComposerLines = isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES; - const parentReportAction = parentReportActions?.[parentReportActionID ?? ''] ?? null; const shouldAutoFocus = !modal?.isVisible && shouldShowComposeInput; const valueRef = useRef(value); @@ -276,6 +257,7 @@ function ComposerWithSuggestions( const textInputRef = useRef(null); const insertedEmojisRef = useRef([]); + const shouldInitFocus = useRef(true); const syncSelectionWithOnChangeTextRef = useRef(null); @@ -659,7 +641,15 @@ function ComposerWithSuggestions( // We want to focus or refocus the input when a modal has been closed or the underlying screen is refocused. // We avoid doing this on native platforms since the software keyboard popping // open creates a jarring and broken UX. - if (!((willBlurTextInputOnTapOutside || shouldAutoFocus) && !isNextModalWillOpenRef.current && !modal?.isVisible && isFocused && (!!prevIsModalVisible || !prevIsFocused))) { + if ( + !( + (willBlurTextInputOnTapOutside || shouldAutoFocus) && + !isNextModalWillOpenRef.current && + !modal?.isVisible && + isFocused && + (!!prevIsModalVisible || !prevIsFocused || shouldInitFocus.current) + ) + ) { return; } @@ -668,6 +658,9 @@ function ComposerWithSuggestions( return; } focus(true); + if (shouldInitFocus.current) { + shouldInitFocus.current = false; + } }, [focus, prevIsFocused, editFocused, prevIsModalVisible, isFocused, modal?.isVisible, isNextModalWillOpenRef, shouldAutoFocus]); useEffect(() => { @@ -817,11 +810,6 @@ export default withOnyx `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, - canEvict: false, - initWithStoredValues: false, - }, })(memo(ComposerWithSuggestionsWithRef)); export type {ComposerWithSuggestionsProps}; diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index bc3bb4fd19d6..345f4a24cfea 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -23,7 +23,6 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import compose from '@libs/compose'; import getDraftComment from '@libs/ComposerUtils/getDraftComment'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import getModalState from '@libs/getModalState'; @@ -73,7 +72,7 @@ type ReportActionComposeOnyxProps = { type ReportActionComposeProps = ReportActionComposeOnyxProps & WithCurrentUserPersonalDetailsProps & - Pick & { + Pick & { /** A method to call when the form is submitted */ onSubmit: (newComment: string | undefined) => void; @@ -101,7 +100,6 @@ function ReportActionCompose({ listHeight = 0, shouldShowComposeInput = true, isReportReadyForDisplay = true, - isEmptyChat, lastReportAction, }: ReportActionComposeProps) { const styles = useThemeStyles(); @@ -414,10 +412,7 @@ function ReportActionCompose({ isScrollLikelyLayoutTriggered={isScrollLikelyLayoutTriggered} raiseIsScrollLikelyLayoutTriggered={raiseIsScrollLikelyLayoutTriggered} reportID={reportID} - parentReportID={report?.parentReportID} - parentReportActionID={report?.parentReportActionID} includeChronos={ReportUtils.chatIncludesChronos(report)} - isEmptyChat={isEmptyChat} lastReportAction={lastReportAction} isMenuVisible={isMenuVisible} inputPlaceholder={inputPlaceholder} From b6647b48816e5772850cb754c00d9aff5a7ae796 Mon Sep 17 00:00:00 2001 From: christianwen Date: Tue, 5 Mar 2024 17:20:09 +0700 Subject: [PATCH 006/219] focus input on safari --- src/components/Composer/index.tsx | 12 ---------- .../ComposerWithSuggestions.tsx | 22 ++++++++++++++++++- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index ffdb6825a211..51bebd710e62 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -327,18 +327,6 @@ function Composer( [numberOfLines, scrollStyleMemo, styles.rtlTextRenderForSafari, style, StyleUtils, isComposerFullSize], ); - useEffect(() => { - if (!showSoftInputOnFocus) { - return; - } - textInput.current?.blur(); - // On Safari when changing inputMode from none to text, the keyboard will cover the view - // We need the logic to re-focus to trigger the keyboard to open below the view - setTimeout(() => { - textInput.current?.focus(); - }, 2000); - }, [showSoftInputOnFocus]); - return ( <> (null); const insertedEmojisRef = useRef([]); const shouldInitFocus = useRef(true); + const isFocusedWhileChangingInputMode = useRef(false); const syncSelectionWithOnChangeTextRef = useRef(null); @@ -711,6 +712,16 @@ function ComposerWithSuggestions( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useEffect(() => { + if (!showSoftInputOnFocus || !isFocusedWhileChangingInputMode.current) { + return; + } + // On Safari when changing inputMode from none to text, the keyboard will cover the view + // We need to re-focus to trigger the keyboard to open below the view + isFocusedWhileChangingInputMode.current = false; + textInputRef.current?.focus(); + }, [showSoftInputOnFocus]); + return ( <> @@ -727,7 +738,12 @@ function ComposerWithSuggestions( style={[styles.textInputCompose, isComposerFullSize ? styles.textInputFullCompose : styles.textInputCollapseCompose]} maxLines={maxComposerLines} onFocus={onFocus} - onBlur={onBlur} + onBlur={(e) => { + if (isFocusedWhileChangingInputMode.current) { + return; + } + onBlur(e); + }} onClick={setShouldBlockSuggestionCalcToFalse} onPasteFile={displayFileInModal} shouldClear={textInputShouldClear} @@ -751,6 +767,10 @@ function ComposerWithSuggestions( if (showSoftInputOnFocus) { return; } + if (Browser.isMobileSafari()) { + isFocusedWhileChangingInputMode.current = true; + textInputRef.current?.blur(); + } setShowSoftInputOnFocus(true); }} From eb79042fcbecc617a2e4651fb3353efb2fa9e8c1 Mon Sep 17 00:00:00 2001 From: christianwen Date: Wed, 10 Apr 2024 11:09:59 +0700 Subject: [PATCH 007/219] lint fix --- .../home/report/ReportActionCompose/ReportActionCompose.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 6214b30e8278..a10e3bd0a36c 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -23,7 +23,6 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import {getDraftComment} from '@libs/DraftCommentUtils'; import getModalState from '@libs/getModalState'; From 67e37b9f54dc4752f45a6ee65af4b1600d50af34 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 12 Aug 2024 11:44:24 +0200 Subject: [PATCH 008/219] create raw WorkspaceInvoiceVBASection --- .../invoices/WorkspaceInvoiceVBASection.tsx | 46 +++++++++++++++++++ .../invoices/WorkspaceInvoicesPage.tsx | 2 + 2 files changed, 48 insertions(+) create mode 100644 src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx diff --git a/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx b/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx new file mode 100644 index 000000000000..1ff2c7e85856 --- /dev/null +++ b/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import {useOnyx} from 'react-native-onyx'; +import Section from '@components/Section'; +import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; +import PaymentMethodList from '@pages/settings/Wallet/PaymentMethodList'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type WorkspaceInvoiceVBASectionProps = { + /** The policy ID currently being configured */ + policyID: string; +}; + +function WorkspaceInvoiceVBASection({policyID}: WorkspaceInvoiceVBASectionProps) { + const styles = useThemeStyles(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {translate} = useLocalize(); + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + + return ( +
+ console.debug('onPress')} + // actionPaymentMethodType={shouldShowDefaultDeleteMenu ? paymentMethod.selectedPaymentMethodType : ''} + activePaymentMethodID={policy?.invoice?.bankAccount?.transferBankAccountID ?? ''} + // buttonRef={addPaymentMethodAnchorRef} + // onListContentSizeChange={shouldShowAddPaymentMenu || shouldShowDefaultDeleteMenu ? setMenuPosition : () => {}} + shouldEnableScroll={false} + style={[styles.mt5, [shouldUseNarrowLayout ? styles.mhn5 : styles.mhn8]]} + listItemStyle={shouldUseNarrowLayout ? styles.ph5 : styles.ph8} + /> +
+ ); +} + +WorkspaceInvoiceVBASection.displayName = 'WorkspaceInvoiceVBASection'; + +export default WorkspaceInvoiceVBASection; diff --git a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx index 40a8239b9ab1..2946f918c043 100644 --- a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx +++ b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx @@ -10,6 +10,7 @@ import CONST from '@src/CONST'; import type SCREENS from '@src/SCREENS'; import WorkspaceInvoicesNoVBAView from './WorkspaceInvoicesNoVBAView'; import WorkspaceInvoicesVBAView from './WorkspaceInvoicesVBAView'; +import WorkspaceInvoiceVBASection from './WorkspaceInvoiceVBASection'; type WorkspaceInvoicesPageProps = StackScreenProps; @@ -28,6 +29,7 @@ function WorkspaceInvoicesPage({route}: WorkspaceInvoicesPageProps) { > {(hasVBA?: boolean, policyID?: string) => ( + {policyID && } {!hasVBA && policyID && } {hasVBA && policyID && } From c2b9af42c3c90bab1969a1e747ba3960aee666b0 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 12 Aug 2024 14:02:20 +0200 Subject: [PATCH 009/219] add invoice types --- src/types/onyx/Policy.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 9724229361ec..5512b7a5b56f 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1299,6 +1299,15 @@ type PolicyInvoicingDetails = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Stripe Connect company website */ companyWebsite?: string; + + /** Back account */ + bankAccount?: { + /** Account balance */ + stripeConnectAccountBalance?: number; + + /** bankAccountID of selected BBA for payouts */ + transferBankAccountID?: number; + }; }>; /** Names of policy features */ From be904818353197e5f303ac1d9ebdef7da3bf9c9a Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 12 Aug 2024 16:21:45 +0200 Subject: [PATCH 010/219] integrate bank accounts logic --- .../invoices/WorkspaceInvoiceVBASection.tsx | 294 +++++++++++++++++- 1 file changed, 289 insertions(+), 5 deletions(-) diff --git a/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx b/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx index 1ff2c7e85856..828c562ac71d 100644 --- a/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx +++ b/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx @@ -1,11 +1,46 @@ -import React from 'react'; +import type {RefObject} from 'react'; +import React, {useCallback, useRef, useState} from 'react'; +import {View} from 'react-native'; +import type {GestureResponderEvent} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu'; +import ConfirmModal from '@components/ConfirmModal'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItem from '@components/MenuItem'; +import Popover from '@components/Popover'; import Section from '@components/Section'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import getClickedTargetLocation from '@libs/getClickedTargetLocation'; +import Navigation from '@libs/Navigation/Navigation'; +import * as PaymentUtils from '@libs/PaymentUtils'; import PaymentMethodList from '@pages/settings/Wallet/PaymentMethodList'; +import type {FormattedSelectedPaymentMethodIcon} from '@pages/settings/Wallet/WalletPage/types'; +import variables from '@styles/variables'; +import * as BankAccounts from '@userActions/BankAccounts'; +import * as PaymentMethods from '@userActions/PaymentMethods'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {AccountData} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +type FormattedSelectedPaymentMethod = { + title: string; + icon?: FormattedSelectedPaymentMethodIcon; + description?: string; + type?: string; +}; + +type PaymentMethodState = { + isSelectedPaymentMethodDefault: boolean; + selectedPaymentMethod: AccountData; + formattedSelectedPaymentMethod: FormattedSelectedPaymentMethod; + methodID: string | number; + selectedPaymentMethodType: string; +}; type WorkspaceInvoiceVBASectionProps = { /** The policy ID currently being configured */ @@ -15,8 +50,186 @@ type WorkspaceInvoiceVBASectionProps = { function WorkspaceInvoiceVBASection({policyID}: WorkspaceInvoiceVBASectionProps) { const styles = useThemeStyles(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {windowWidth} = useWindowDimensions(); const {translate} = useLocalize(); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); + const [cardList] = useOnyx(ONYXKEYS.CARD_LIST); + const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET); + const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); + const addPaymentMethodAnchorRef = useRef(null); + const paymentMethodButtonRef = useRef(null); + const [shouldShowAddPaymentMenu, setShouldShowAddPaymentMenu] = useState(false); + const [showConfirmDeleteModal, setShowConfirmDeleteModal] = useState(false); + const [shouldShowDefaultDeleteMenu, setShouldShowDefaultDeleteMenu] = useState(false); + const [paymentMethod, setPaymentMethod] = useState({ + isSelectedPaymentMethodDefault: false, + selectedPaymentMethod: {}, + formattedSelectedPaymentMethod: { + title: '', + }, + methodID: '', + selectedPaymentMethodType: '', + }); + const [anchorPosition, setAnchorPosition] = useState({ + anchorPositionHorizontal: 0, + anchorPositionVertical: 0, + anchorPositionTop: 0, + anchorPositionRight: 0, + }); + const hasBankAccount = !isEmptyObject(bankAccountList) || !isEmptyObject(fundList); + const hasWallet = !isEmptyObject(userWallet); + const hasAssignedCard = !isEmptyObject(cardList); + const shouldShowEmptyState = !hasBankAccount && !hasWallet && !hasAssignedCard; + // Determines whether or not the modal popup is mounted from the bottom of the screen instead of the side mount on Web or Desktop screens + const isPopoverBottomMount = anchorPosition.anchorPositionTop === 0 || shouldUseNarrowLayout; + const shouldShowMakeDefaultButton = + !paymentMethod.isSelectedPaymentMethodDefault && + !(paymentMethod.formattedSelectedPaymentMethod.type === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT && paymentMethod.selectedPaymentMethod.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS); + + /** + * Set position of the payment menu + */ + const setMenuPosition = useCallback(() => { + if (!paymentMethodButtonRef.current) { + return; + } + + const position = getClickedTargetLocation(paymentMethodButtonRef.current); + + setAnchorPosition({ + anchorPositionTop: position.top + position.height - variables.bankAccountActionPopoverTopSpacing, + // We want the position to be 23px to the right of the left border + anchorPositionRight: windowWidth - position.right + variables.bankAccountActionPopoverRightSpacing, + anchorPositionHorizontal: position.x + (shouldShowEmptyState ? -variables.addPaymentMethodLeftSpacing : variables.addBankAccountLeftSpacing), + anchorPositionVertical: position.y, + }); + }, [shouldShowEmptyState, windowWidth]); + + /** + * Display the delete/default menu, or the add payment method menu + */ + const paymentMethodPressed = ( + nativeEvent?: GestureResponderEvent | KeyboardEvent, + accountType?: string, + account?: AccountData, + icon?: FormattedSelectedPaymentMethodIcon, + isDefault?: boolean, + methodID?: string | number, + ) => { + if (shouldShowAddPaymentMenu) { + setShouldShowAddPaymentMenu(false); + return; + } + + if (shouldShowDefaultDeleteMenu) { + setShouldShowDefaultDeleteMenu(false); + return; + } + paymentMethodButtonRef.current = nativeEvent?.currentTarget as HTMLDivElement; + + // The delete/default menu + if (accountType) { + let formattedSelectedPaymentMethod: FormattedSelectedPaymentMethod = { + title: '', + }; + if (accountType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) { + formattedSelectedPaymentMethod = { + title: account?.addressName ?? '', + icon, + description: PaymentUtils.getPaymentMethodDescription(accountType, account), + type: CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, + }; + } + setPaymentMethod({ + isSelectedPaymentMethodDefault: !!isDefault, + selectedPaymentMethod: account ?? {}, + selectedPaymentMethodType: accountType, + formattedSelectedPaymentMethod, + methodID: methodID ?? '-1', + }); + setShouldShowDefaultDeleteMenu(true); + setMenuPosition(); + return; + } + setShouldShowAddPaymentMenu(true); + setMenuPosition(); + }; + + /** + * Hide the add payment modal + */ + const hideAddPaymentMenu = () => { + setShouldShowAddPaymentMenu(false); + }; + + /** + * Hide the default / delete modal + */ + const hideDefaultDeleteMenu = useCallback(() => { + setShouldShowDefaultDeleteMenu(false); + setShowConfirmDeleteModal(false); + }, [setShouldShowDefaultDeleteMenu, setShowConfirmDeleteModal]); + + const deletePaymentMethod = useCallback(() => { + const bankAccountID = paymentMethod.selectedPaymentMethod.bankAccountID; + if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT && bankAccountID) { + BankAccounts.deletePaymentBankAccount(bankAccountID); + } + }, [paymentMethod.selectedPaymentMethod.bankAccountID, paymentMethod.selectedPaymentMethodType]); + + const makeDefaultPaymentMethod = useCallback(() => { + const paymentCardList = fundList ?? {}; + // Find the previous default payment method so we can revert if the MakeDefaultPaymentMethod command errors + const paymentMethods = PaymentUtils.formatPaymentMethods(bankAccountList ?? {}, paymentCardList, styles); + + const previousPaymentMethod = paymentMethods.find((method) => !!method.isDefault); + const currentPaymentMethod = paymentMethods.find((method) => method.methodID === paymentMethod.methodID); + if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) { + PaymentMethods.makeDefaultPaymentMethod(paymentMethod.selectedPaymentMethod.bankAccountID ?? -1, 0, previousPaymentMethod, currentPaymentMethod); + } else if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.DEBIT_CARD) { + PaymentMethods.makeDefaultPaymentMethod(0, paymentMethod.selectedPaymentMethod.fundID ?? -1, previousPaymentMethod, currentPaymentMethod); + } + }, [ + paymentMethod.methodID, + paymentMethod.selectedPaymentMethod.bankAccountID, + paymentMethod.selectedPaymentMethod.fundID, + paymentMethod.selectedPaymentMethodType, + bankAccountList, + fundList, + styles, + ]); + + const resetSelectedPaymentMethodData = useCallback(() => { + // Reset to same values as in the constructor + setPaymentMethod({ + isSelectedPaymentMethodDefault: false, + selectedPaymentMethod: {}, + formattedSelectedPaymentMethod: { + title: '', + }, + methodID: '', + selectedPaymentMethodType: '', + }); + }, [setPaymentMethod]); + + /** + * Navigate to the appropriate payment type addition screen + */ + const addPaymentMethodTypePressed = (paymentType: string) => { + hideAddPaymentMenu(); + + if (paymentType === CONST.PAYMENT_METHODS.DEBIT_CARD) { + Navigation.navigate(ROUTES.SETTINGS_ADD_DEBIT_CARD); + return; + } + if (paymentType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT || paymentType === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT) { + BankAccounts.openPersonalBankAccountSetupView(); + return; + } + + throw new Error('Invalid payment method type selected'); + }; return (
console.debug('onPress')} - // actionPaymentMethodType={shouldShowDefaultDeleteMenu ? paymentMethod.selectedPaymentMethodType : ''} + onPress={paymentMethodPressed} activePaymentMethodID={policy?.invoice?.bankAccount?.transferBankAccountID ?? ''} - // buttonRef={addPaymentMethodAnchorRef} - // onListContentSizeChange={shouldShowAddPaymentMenu || shouldShowDefaultDeleteMenu ? setMenuPosition : () => {}} + actionPaymentMethodType={shouldShowDefaultDeleteMenu ? paymentMethod.selectedPaymentMethodType : ''} + buttonRef={addPaymentMethodAnchorRef} shouldEnableScroll={false} style={[styles.mt5, [shouldUseNarrowLayout ? styles.mhn5 : styles.mhn8]]} listItemStyle={shouldUseNarrowLayout ? styles.ph5 : styles.ph8} /> + + } + > + {!showConfirmDeleteModal && ( + + {isPopoverBottomMount && ( + + )} + {shouldShowMakeDefaultButton && ( + { + makeDefaultPaymentMethod(); + setShouldShowDefaultDeleteMenu(false); + }} + wrapperStyle={[styles.pv3, styles.ph5, !shouldUseNarrowLayout ? styles.sidebarPopover : {}]} + /> + )} + setShowConfirmDeleteModal(true)} + wrapperStyle={[styles.pv3, styles.ph5, !shouldUseNarrowLayout ? styles.sidebarPopover : {}]} + /> + + )} + { + deletePaymentMethod(); + hideDefaultDeleteMenu(); + }} + onCancel={hideDefaultDeleteMenu} + title={translate('walletPage.deleteAccount')} + prompt={translate('walletPage.deleteConfirmation')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + shouldShowCancelButton + danger + onModalHide={resetSelectedPaymentMethodData} + /> + + + addPaymentMethodTypePressed(method)} + anchorRef={addPaymentMethodAnchorRef} + shouldShowPersonalBankAccountOption + />
); } From 8e9d73eee7496c1aa7ded7559e84492f9f34982c Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 12 Aug 2024 16:44:47 +0200 Subject: [PATCH 011/219] add translations --- src/languages/en.ts | 3 ++- src/languages/es.ts | 3 ++- src/pages/settings/Wallet/WalletPage/WalletPage.tsx | 2 +- src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx | 6 ++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index bfe0eef70178..f59e24a26dbe 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -373,6 +373,7 @@ export default { filterLogs: 'Filter Logs', network: 'Network', reportID: 'Report ID', + bankAccounts: 'Bank accounts', }, location: { useCurrent: 'Use current location', @@ -1210,7 +1211,6 @@ export default { enableWalletToSendAndReceiveMoney: 'Enable your wallet to send and receive money with friends.', walletEnabledToSendAndReceiveMoney: 'Your wallet has been enabled to send and receive money with friends.', enableWallet: 'Enable wallet', - bankAccounts: 'Bank accounts', addBankAccountToSendAndReceive: 'Adding a bank account allows you to get paid back for expenses you submit to a workspace.', addBankAccount: 'Add bank account', assignedCards: 'Assigned cards', @@ -3321,6 +3321,7 @@ export default { payingAsIndividual: 'Paying as an individual', payingAsBusiness: 'Paying as a business', }, + bankAccountsSubtitle: 'Add a bank account to receive invoice payments.', }, travel: { unlockConciergeBookingTravel: 'Unlock Concierge travel booking', diff --git a/src/languages/es.ts b/src/languages/es.ts index 76d55a096808..3360fa18b00d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -363,6 +363,7 @@ export default { filterLogs: 'Registros de filtrado', network: 'La red', reportID: 'ID del informe', + bankAccounts: 'Cuentas bancarias', }, connectionComplete: { title: 'Conexión completa', @@ -1219,7 +1220,6 @@ export default { enableWalletToSendAndReceiveMoney: 'Habilita tu Billetera Expensify para comenzar a enviar y recibir dinero con amigos.', walletEnabledToSendAndReceiveMoney: 'Tu billetera ha sido habilitada para enviar y recibir dinero con amigos.', enableWallet: 'Habilitar billetera', - bankAccounts: 'Cuentas bancarias', addBankAccountToSendAndReceive: 'Agregar una cuenta bancaria te permite recibir reembolsos por los gastos que envíes a un espacio de trabajo.', addBankAccount: 'Añadir cuenta bancaria', assignedCards: 'Tarjetas asignadas', @@ -3372,6 +3372,7 @@ export default { payingAsIndividual: 'Pago individual', payingAsBusiness: 'Pagar como una empresa', }, + bankAccountsSubtitle: 'Agrega una cuenta bancaria para recibir pagos de facturas.', }, travel: { unlockConciergeBookingTravel: 'Desbloquea la reserva de viajes con Concierge', diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx index 7e242a5c8782..5674d6e0608c 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx @@ -369,7 +369,7 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi >
@@ -249,7 +249,6 @@ function WorkspaceInvoiceVBASection({policyID}: WorkspaceInvoiceVBASectionProps) style={[styles.mt5, [shouldUseNarrowLayout ? styles.mhn5 : styles.mhn8]]} listItemStyle={shouldUseNarrowLayout ? styles.ph5 : styles.ph8} /> - - Date: Mon, 12 Aug 2024 17:12:57 +0200 Subject: [PATCH 012/219] integrate add bank account button --- .../settings/Wallet/PaymentMethodList.tsx | 39 +++++++++++++------ .../invoices/WorkspaceInvoiceVBASection.tsx | 3 +- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index 0d127e3346ae..a2e073714e76 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -91,6 +91,9 @@ type PaymentMethodListProps = PaymentMethodListOnyxProps & { /** Whether the add Payment button be shown on the list */ shouldShowAddPaymentMethodButton?: boolean; + /** Whether the add Bank account button be shown on the list */ + shouldShowAddBankAccountButton?: boolean; + /** Whether the assigned cards should be shown on the list */ shouldShowAssignedCards?: boolean; @@ -183,6 +186,7 @@ function PaymentMethodList({ onPress, shouldShowSelectedState = false, shouldShowAddPaymentMethodButton = true, + shouldShowAddBankAccountButton = false, shouldShowAddBankAccount = true, shouldShowEmptyListMessage = true, shouldShowAssignedCards = false, @@ -313,19 +317,30 @@ function PaymentMethodList({ const renderListEmptyComponent = () => {translate('paymentMethodList.addFirstPaymentMethod')}; const renderListFooterComponent = useCallback( - () => ( - - ), + () => + shouldShowAddBankAccountButton ? ( +