From a790c0e15a138457f8a182781121484c248d7389 Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Wed, 6 Dec 2023 03:59:23 +0530 Subject: [PATCH 001/242] Add confirmation modal when user cancels a task --- src/pages/home/HeaderView.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index 5b57419c8530..8cb504a0c4b0 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -19,6 +19,7 @@ import TaskHeaderActionButton from '@components/TaskHeaderActionButton'; import Text from '@components/Text'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; import Tooltip from '@components/Tooltip'; +import ConfirmModal from '@components/ConfirmModal'; import useLocalize from '@hooks/useLocalize'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {getGroupChatName} from '@libs/GroupChatUtils'; @@ -80,6 +81,7 @@ const defaultProps = { }; function HeaderView(props) { + const [isCancelTaskConfirmModalVisible, setIsCancelTaskConfirmModalVisible] = React.useState(false); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); const {translate} = useLocalize(); const theme = useTheme(); @@ -128,7 +130,7 @@ function HeaderView(props) { threeDotMenuItems.push({ icon: Expensicons.Trashcan, text: translate('common.cancel'), - onSelected: Session.checkIfActionIsAllowed(() => Task.cancelTask(props.report.reportID, props.report.reportName, props.report.stateNum, props.report.statusNum)), + onSelected: () => setIsCancelTaskConfirmModalVisible(true), }); } } @@ -283,6 +285,19 @@ function HeaderView(props) { )} + { + setIsCancelTaskConfirmModalVisible(false); + Session.checkIfActionIsAllowed(Task.cancelTask(props.report.reportID, props.report.reportName, props.report.stateNum, props.report.statusNum)); + }} + onCancel={() => setIsCancelTaskConfirmModalVisible(false)} + title={translate('task.cancelTask')} + prompt={translate('task.cancelConfirmation')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> )} From 5a4037cdc37ef1982040258faf53537b4ee96538 Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Wed, 6 Dec 2023 03:59:54 +0530 Subject: [PATCH 002/242] Add text strings to language files --- src/languages/en.ts | 2 ++ src/languages/es.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index 817f06f6b344..30f87c625044 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1679,6 +1679,8 @@ export default { markAsIncomplete: 'Mark as incomplete', assigneeError: 'There was an error assigning this task, please try another assignee.', genericCreateTaskFailureMessage: 'Unexpected error create task, please try again later.', + cancelTask: 'Cancel task', + cancelConfirmation: 'Are you sure that you want to cancel this task?', }, statementPage: { title: (year, monthName) => `${monthName} ${year} statement`, diff --git a/src/languages/es.ts b/src/languages/es.ts index b219021daa0f..3f7e24602124 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1703,6 +1703,8 @@ export default { markAsIncomplete: 'Marcar como incompleta', assigneeError: 'Hubo un error al asignar esta tarea, inténtalo con otro usuario.', genericCreateTaskFailureMessage: 'Error inesperado al crear el tarea, por favor, inténtalo más tarde.', + cancelTask: 'Cancelar tarea', + cancelConfirmation: '¿Estás seguro de que quieres cancelar esta tarea?', }, statementPage: { title: (year, monthName) => `Estado de cuenta de ${monthName} ${year}`, From 8e85d34d0b7525509132d4a730deca89bc46561d Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Wed, 6 Dec 2023 04:04:16 +0530 Subject: [PATCH 003/242] Prettier changes --- src/pages/home/HeaderView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index 8cb504a0c4b0..f0c6a44fca8e 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -6,6 +6,7 @@ import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import GoogleMeetIcon from '@assets/images/google-meet.svg'; import ZoomIcon from '@assets/images/zoom-icon.svg'; +import ConfirmModal from '@components/ConfirmModal'; import DisplayNames from '@components/DisplayNames'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -19,7 +20,6 @@ import TaskHeaderActionButton from '@components/TaskHeaderActionButton'; import Text from '@components/Text'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; import Tooltip from '@components/Tooltip'; -import ConfirmModal from '@components/ConfirmModal'; import useLocalize from '@hooks/useLocalize'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {getGroupChatName} from '@libs/GroupChatUtils'; From f623dbbdb4bccfd48b4df1d46d3ddb130909a4cd Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 13 Dec 2023 14:59:09 +0700 Subject: [PATCH 004/242] use set method when creating policy for payment --- src/libs/actions/Policy.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index f33e6637e2de..430caaf6d6b4 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -1568,12 +1568,12 @@ function createWorkspaceFromIOUPayment(iouReport) { const optimisticData = [ { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: newWorkspace, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, value: { [sessionAccountID]: { @@ -1587,7 +1587,7 @@ function createWorkspaceFromIOUPayment(iouReport) { }, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${announceChatReportID}`, value: { pendingFields: { @@ -1597,12 +1597,12 @@ function createWorkspaceFromIOUPayment(iouReport) { }, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${announceChatReportID}`, value: announceReportActionData, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`, value: { pendingFields: { @@ -1612,12 +1612,12 @@ function createWorkspaceFromIOUPayment(iouReport) { }, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${adminsChatReportID}`, value: adminsReportActionData, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${workspaceChatReportID}`, value: { pendingFields: { @@ -1627,17 +1627,17 @@ function createWorkspaceFromIOUPayment(iouReport) { }, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${workspaceChatReportID}`, value: workspaceChatReportActionData, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`, value: null, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS}${policyID}`, value: null, }, @@ -1712,37 +1712,37 @@ function createWorkspaceFromIOUPayment(iouReport) { const failureData = [ { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, value: null, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${announceChatReportID}`, value: null, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${announceChatReportID}`, value: null, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`, value: null, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${adminsChatReportID}`, value: null, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${workspaceChatReportID}`, value: null, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${workspaceChatReportID}`, value: null, }, From 9d22a45679e58d5e1638beb5a0e367dce05f6909 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 20 Dec 2023 14:27:29 +0700 Subject: [PATCH 005/242] fix: 33318 --- src/components/ThemeProvider.tsx | 7 +++- src/libs/DomUtils/index.native.ts | 6 ++++ src/libs/DomUtils/index.ts | 57 +++++++++++++++++++++++++++++++ web/index.html | 26 -------------- 4 files changed, 69 insertions(+), 27 deletions(-) diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx index 34bc32be9c99..41423991f051 100644 --- a/src/components/ThemeProvider.tsx +++ b/src/components/ThemeProvider.tsx @@ -1,10 +1,11 @@ /* eslint-disable react/jsx-props-no-spreading */ import PropTypes from 'prop-types'; -import React, {useMemo} from 'react'; +import React, {useEffect, useMemo} from 'react'; import useThemePreferenceWithStaticOverride from '@hooks/useThemePreferenceWithStaticOverride'; import themes from '@styles/theme'; import ThemeContext from '@styles/theme/context/ThemeContext'; import {ThemePreferenceWithoutSystem} from '@styles/theme/types'; +import DomUtils from '@libs/DomUtils'; const propTypes = { /** Rendered child component */ @@ -20,6 +21,10 @@ function ThemeProvider({children, theme: staticThemePreference}: ThemeProviderPr const theme = useMemo(() => themes[themePreference], [themePreference]); + useEffect(() => { + DomUtils.addCSS(DomUtils.getAutofilledInputStyle(theme.text), 'autofill-input') + }, [theme.text]); + return {children}; } diff --git a/src/libs/DomUtils/index.native.ts b/src/libs/DomUtils/index.native.ts index 0864f1a16ac0..f161e0eeeeb2 100644 --- a/src/libs/DomUtils/index.native.ts +++ b/src/libs/DomUtils/index.native.ts @@ -2,6 +2,10 @@ import GetActiveElement from './types'; const getActiveElement: GetActiveElement = () => null; +const addCSS = () => null; + +const getAutofilledInputStyle = () => null; + const requestAnimationFrame = (callback: () => void) => { if (!callback) { return; @@ -11,6 +15,8 @@ const requestAnimationFrame = (callback: () => void) => { }; export default { + addCSS, + getAutofilledInputStyle, getActiveElement, requestAnimationFrame, }; diff --git a/src/libs/DomUtils/index.ts b/src/libs/DomUtils/index.ts index 6a2eed57fbe6..001d57745f53 100644 --- a/src/libs/DomUtils/index.ts +++ b/src/libs/DomUtils/index.ts @@ -2,7 +2,64 @@ import GetActiveElement from './types'; const getActiveElement: GetActiveElement = () => document.activeElement; +const addCSS = (css: string, styleId: string) => { + var head = document.getElementsByTagName('head')[0]; + var existingStyle = document.getElementById(styleId); + + if (existingStyle) { + // If style tag with the specified ID exists, update its content + if (existingStyle.styleSheet) { // IE + existingStyle.styleSheet.cssText = css; + } else { // the world + existingStyle.innerHTML = css; + } + } else { + // If style tag doesn't exist, create a new one + var s = document.createElement('style'); + s.setAttribute("id", styleId); + s.setAttribute('type', 'text/css'); + + if (s.styleSheet) { // IE + s.styleSheet.cssText = css; + } else { // the world + s.appendChild(document.createTextNode(css)); + } + + head.appendChild(s); + } +} + +/* Customizes the background and text colors for autofill inputs in Chrome */ +/* Chrome on iOS does not support the autofill pseudo class because it is a non-standard webkit feature. +We should rely on the chrome-autofilled property being added to the input when users use auto-fill */ +const getAutofilledInputStyle = (inputTextColor: string) => ` + input[chrome-autofilled], + input[chrome-autofilled]:hover, + input[chrome-autofilled]:focus, + textarea[chrome-autofilled], + textarea[chrome-autofilled]:hover, + textarea[chrome-autofilled]:focus, + select[chrome-autofilled], + select[chrome-autofilled]:hover, + select[chrome-autofilled]:focus, + input:-webkit-autofill, + input:-webkit-autofill:hover, + input:-webkit-autofill:focus, + textarea:-webkit-autofill, + textarea:-webkit-autofill:hover, + textarea:-webkit-autofill:focus, + select:-webkit-autofill, + select:-webkit-autofill:hover, + select:-webkit-autofill:focus { + -webkit-background-clip: text; + -webkit-text-fill-color: ${inputTextColor}; + caret-color: ${inputTextColor}; + } +`; + export default { + addCSS, + getAutofilledInputStyle, getActiveElement, requestAnimationFrame: window.requestAnimationFrame.bind(window), }; diff --git a/web/index.html b/web/index.html index 967873fe586c..e83f4527a1a3 100644 --- a/web/index.html +++ b/web/index.html @@ -70,32 +70,6 @@ display: none; } - /* Customizes the background and text colors for autofill inputs in Chrome */ - /* Chrome on iOS does not support the autofill pseudo class because it is a non-standard webkit feature. - We should rely on the chrome-autofilled property being added to the input when users use auto-fill */ - input[chrome-autofilled], - input[chrome-autofilled]:hover, - input[chrome-autofilled]:focus, - textarea[chrome-autofilled], - textarea[chrome-autofilled]:hover, - textarea[chrome-autofilled]:focus, - select[chrome-autofilled], - select[chrome-autofilled]:hover, - select[chrome-autofilled]:focus, - input:-webkit-autofill, - input:-webkit-autofill:hover, - input:-webkit-autofill:focus, - textarea:-webkit-autofill, - textarea:-webkit-autofill:hover, - textarea:-webkit-autofill:focus, - select:-webkit-autofill, - select:-webkit-autofill:hover, - select:-webkit-autofill:focus { - -webkit-background-clip: text; - -webkit-text-fill-color: #ffffff; - caret-color: #ffffff; - } - /* Prevent autofill from overlapping with the input label in Chrome */ div:has(input:-webkit-autofill, input[chrome-autofilled]) > label { transform: translateY(var(--active-label-translate-y)) scale(var(--active-label-scale)) !important; From 81142e7cf9191e19c461e8704c27f2fcbe54117a Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 22 Dec 2023 15:54:36 +0700 Subject: [PATCH 006/242] fix ts error --- src/components/ThemeProvider.tsx | 4 ++-- src/libs/DomUtils/index.ts | 40 +++++++++++++++++--------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx index 41423991f051..5fe9bfec1e4a 100644 --- a/src/components/ThemeProvider.tsx +++ b/src/components/ThemeProvider.tsx @@ -2,10 +2,10 @@ import PropTypes from 'prop-types'; import React, {useEffect, useMemo} from 'react'; import useThemePreferenceWithStaticOverride from '@hooks/useThemePreferenceWithStaticOverride'; +import DomUtils from '@libs/DomUtils'; import themes from '@styles/theme'; import ThemeContext from '@styles/theme/context/ThemeContext'; import {ThemePreferenceWithoutSystem} from '@styles/theme/types'; -import DomUtils from '@libs/DomUtils'; const propTypes = { /** Rendered child component */ @@ -22,7 +22,7 @@ function ThemeProvider({children, theme: staticThemePreference}: ThemeProviderPr const theme = useMemo(() => themes[themePreference], [themePreference]); useEffect(() => { - DomUtils.addCSS(DomUtils.getAutofilledInputStyle(theme.text), 'autofill-input') + DomUtils.addCSS(DomUtils.getAutofilledInputStyle(theme.text), 'autofill-input'); }, [theme.text]); return {children}; diff --git a/src/libs/DomUtils/index.ts b/src/libs/DomUtils/index.ts index 001d57745f53..17dce79cf503 100644 --- a/src/libs/DomUtils/index.ts +++ b/src/libs/DomUtils/index.ts @@ -3,35 +3,37 @@ import GetActiveElement from './types'; const getActiveElement: GetActiveElement = () => document.activeElement; const addCSS = (css: string, styleId: string) => { - var head = document.getElementsByTagName('head')[0]; - var existingStyle = document.getElementById(styleId); + const existingStyle = document.getElementById(styleId); if (existingStyle) { - // If style tag with the specified ID exists, update its content - if (existingStyle.styleSheet) { // IE - existingStyle.styleSheet.cssText = css; - } else { // the world + if ('styleSheet' in existingStyle) { + // Supports IE8 and below + (existingStyle.styleSheet as any).cssText = css; + } else { existingStyle.innerHTML = css; } } else { - // If style tag doesn't exist, create a new one - var s = document.createElement('style'); - s.setAttribute("id", styleId); - s.setAttribute('type', 'text/css'); + const styleElement = document.createElement('style'); + styleElement.setAttribute('id', styleId); + styleElement.setAttribute('type', 'text/css'); - if (s.styleSheet) { // IE - s.styleSheet.cssText = css; - } else { // the world - s.appendChild(document.createTextNode(css)); + if ('styleSheet' in styleElement) { + // Supports IE8 and below + (styleElement.styleSheet as any).cssText = css; + } else { + styleElement.appendChild(document.createTextNode(css)); } - head.appendChild(s); + const head = document.getElementsByTagName('head')[0]; + head.appendChild(styleElement); } -} +}; -/* Customizes the background and text colors for autofill inputs in Chrome */ -/* Chrome on iOS does not support the autofill pseudo class because it is a non-standard webkit feature. -We should rely on the chrome-autofilled property being added to the input when users use auto-fill */ +/** + * Customizes the background and text colors for autofill inputs in Chrome + * Chrome on iOS does not support the autofill pseudo class because it is a non-standard webkit feature. + * We should rely on the chrome-autofilled property being added to the input when users use auto-fill + */ const getAutofilledInputStyle = (inputTextColor: string) => ` input[chrome-autofilled], input[chrome-autofilled]:hover, From d5ba5b5f9d82cb2fa7027466de5ac7864dad41c9 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 22 Dec 2023 16:17:26 +0700 Subject: [PATCH 007/242] fix lint --- src/libs/DomUtils/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/DomUtils/index.ts b/src/libs/DomUtils/index.ts index 17dce79cf503..25700ca015bb 100644 --- a/src/libs/DomUtils/index.ts +++ b/src/libs/DomUtils/index.ts @@ -8,6 +8,7 @@ const addCSS = (css: string, styleId: string) => { if (existingStyle) { if ('styleSheet' in existingStyle) { // Supports IE8 and below + // eslint-disable-next-line @typescript-eslint/no-explicit-any (existingStyle.styleSheet as any).cssText = css; } else { existingStyle.innerHTML = css; @@ -19,6 +20,7 @@ const addCSS = (css: string, styleId: string) => { if ('styleSheet' in styleElement) { // Supports IE8 and below + // eslint-disable-next-line @typescript-eslint/no-explicit-any (styleElement.styleSheet as any).cssText = css; } else { styleElement.appendChild(document.createTextNode(css)); From 06acbcd4c0aabb6056df4c6c39bb446ef8c5fd04 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 27 Dec 2023 12:19:43 +0700 Subject: [PATCH 008/242] fix: timer in login page restarts --- src/pages/signin/SignInPage.js | 13 +++++++------ .../signin/ValidateCodeForm/BaseValidateCodeForm.js | 2 ++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.js index 8cb0ef9907af..fc221d841036 100644 --- a/src/pages/signin/SignInPage.js +++ b/src/pages/signin/SignInPage.js @@ -262,14 +262,15 @@ function SignInPageInner({credentials, account, isInModal, activeClients, prefer blurOnSubmit={account.validated === false} scrollPageToTop={signInPageLayoutRef.current && signInPageLayoutRef.current.scrollPageToTop} /> + {shouldShowValidateCodeForm && ( + + )} {isClientTheLeader && ( <> - {shouldShowValidateCodeForm && ( - - )} {shouldShowUnlinkLoginForm && } {shouldShowChooseSSOOrMagicCode && } {shouldShowEmailDeliveryFailurePage && } diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js index 03db6d3436cf..3130965ecc99 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js @@ -13,6 +13,7 @@ import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import withToggleVisibilityView from '@components/withToggleVisibilityView'; import usePrevious from '@hooks/usePrevious'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; @@ -417,4 +418,5 @@ export default compose( session: {key: ONYXKEYS.SESSION}, }), withNetwork(), + withToggleVisibilityView, )(BaseValidateCodeForm); From e9cb53c9171ac913e641928995ade1ff9ee5e061 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 3 Jan 2024 08:50:02 +0100 Subject: [PATCH 009/242] start migrating MagicCodeInput to TypeScript --- src/CONST.ts | 5 + src/components/MagicCodeInput-draft.tsx | 418 ++++++++++++++++++++++++ 2 files changed, 423 insertions(+) create mode 100644 src/components/MagicCodeInput-draft.tsx diff --git a/src/CONST.ts b/src/CONST.ts index abba27b0c33b..30757a269166 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -903,6 +903,7 @@ const CONST = { KEYBOARD_TYPE: { VISIBLE_PASSWORD: 'visible-password', ASCII_CAPABLE: 'ascii-capable', + NUMBER_PAD: 'number-pad', }, INPUT_MODE: { @@ -2831,12 +2832,16 @@ const CONST = { CHECKBOX: 'checkbox', /** Use for elements that allow a choice from multiple options. */ COMBOBOX: 'combobox', + /** Use for form elements. */ + FORM: 'form', /** Use with scrollable lists to represent a grid layout. */ GRID: 'grid', /** Use for section headers or titles. */ HEADING: 'heading', /** Use for image elements. */ IMG: 'img', + /** Use for input elements. */ + INPUT: 'input', /** Use for elements that navigate to other pages or content. */ LINK: 'link', /** Use to identify a list of items. */ diff --git a/src/components/MagicCodeInput-draft.tsx b/src/components/MagicCodeInput-draft.tsx new file mode 100644 index 000000000000..6c1cb1851e18 --- /dev/null +++ b/src/components/MagicCodeInput-draft.tsx @@ -0,0 +1,418 @@ +import React, {ForwardedRef, forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import {StyleSheet, View, TextInput as RNTextInput, NativeSyntheticEvent, TextInputFocusEventData} from 'react-native'; +import {HandlerStateChangeEvent, TapGestureHandler} from 'react-native-gesture-handler'; +import useNetwork from '@hooks/useNetwork'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Browser from '@libs/Browser'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import CONST from '@src/CONST'; +import FormHelpMessage from './FormHelpMessage'; +import Text from './Text'; +import TextInput from './TextInput'; + +const TEXT_INPUT_EMPTY_STATE = ''; + +type MagicCodeInputProps = { + /** Name attribute for the input */ + name?: string, + + /** Input value */ + value?: string, + + /** Should the input auto focus */ + autoFocus?: boolean, + + /** Whether we should wait before focusing the TextInput, useful when using transitions */ + shouldDelayFocus?: boolean, + + /** Error text to display */ + errorText?: string, + + /** Specifies autocomplete hints for the system, so it can provide autofill */ + autoComplete: 'sms-otp' | 'one-time-code' | 'off', + + /* Should submit when the input is complete */ + shouldSubmitOnComplete?: boolean, + + /** Function to call when the input is changed */ + onChangeText?: (value: string) => void, + + /** Function to call when the input is submitted or fully complete */ + onFulfill?: (value: string) => void, + + /** Specifies if the input has a validation error */ + hasError?: boolean, + + /** Specifies the max length of the input */ + maxLength?: number, + + /** Specifies if the keyboard should be disabled */ + isDisableKeyboard?: boolean, + + /** Last pressed digit on BigDigitPad */ + lastPressedDigit?: string, +} + +/** + * Converts a given string into an array of numbers that must have the same + * number of elements as the number of inputs. + */ +const decomposeString = (value: string, length: number): string[] => { + let arr = value.split('').slice(0, length).map((v) => (ValidationUtils.isNumeric(v) ? v : CONST.MAGIC_CODE_EMPTY_CHAR)) + if (arr.length < length) { + arr = arr.concat(Array(length - arr.length).fill(CONST.MAGIC_CODE_EMPTY_CHAR)); + } + return arr; +}; + +/** + * Converts an array of strings into a single string. If there are undefined or + * empty values, it will replace them with a space. + */ +const composeToString = (value: string[]): string => value.map((v) => (v === undefined || v === '' ? CONST.MAGIC_CODE_EMPTY_CHAR : v)).join(''); + +const getInputPlaceholderSlots = (length: number): number[] => Array.from(Array(length).keys()); + +function MagicCodeInput({ + value = '', + name = '', + autoFocus = true, + shouldDelayFocus = false, + errorText = '', + shouldSubmitOnComplete = true, + onChangeText: onChangeTextProp = () => {}, + onFulfill = () => {}, + hasError = false, + maxLength = CONST.MAGIC_CODE_LENGTH, + isDisableKeyboard = false, + lastPressedDigit = '', + autoComplete, +}: MagicCodeInputProps, ref: ForwardedRef) { + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const inputRefs = useRef(); + const [input, setInput] = useState(TEXT_INPUT_EMPTY_STATE); + const [focusedIndex, setFocusedIndex] = useState(0); + const [editIndex, setEditIndex] = useState(0); + const [wasSubmitted, setWasSubmitted] = useState(false); + const shouldFocusLast = useRef(false); + const inputWidth = useRef(0); + const lastFocusedIndex = useRef(0); + const lastValue = useRef(TEXT_INPUT_EMPTY_STATE); + + console.log("** I RENDER **") + + useEffect(() => { + lastValue.current = input.length; + }, [input]); + + const blurMagicCodeInput = () => { + inputRefs.current?.blur(); + setFocusedIndex(undefined); + }; + + const focusMagicCodeInput = () => { + setFocusedIndex(0); + lastFocusedIndex.current = 0; + setEditIndex(0); + inputRefs.current?.focus(); + }; + + const setInputAndIndex = (index: number | undefined) => { + setInput(TEXT_INPUT_EMPTY_STATE); + setFocusedIndex(index); + setEditIndex(index); + }; + + useImperativeHandle(ref, () => ({ + focus() { + focusMagicCodeInput(); + }, + focusLastSelected() { + inputRefs.current?.focus(); + }, + resetFocus() { + setInput(TEXT_INPUT_EMPTY_STATE); + focusMagicCodeInput(); + }, + clear() { + lastFocusedIndex.current = 0; + setInputAndIndex(0); + inputRefs.current?.focus(); + onChangeTextProp(''); + }, + blur() { + blurMagicCodeInput(); + }, + })); + + const validateAndSubmit = () => { + const numbers = decomposeString(value, maxLength); + if (wasSubmitted || !shouldSubmitOnComplete || numbers.filter((n) => ValidationUtils.isNumeric(n)).length !== maxLength || isOffline) { + return; + } + if (!wasSubmitted) { + setWasSubmitted(true); + } + // Blurs the input and removes focus from the last input and, if it should submit + // on complete, it will call the onFulfill callback. + blurMagicCodeInput(); + onFulfill(value); + lastValue.current = ''; + }; + + const {isOffline} = useNetwork({onReconnect: validateAndSubmit}); + + useEffect(() => { + validateAndSubmit(); + + // We have not added: + // + the editIndex as the dependency because we don't want to run this logic after focusing on an input to edit it after the user has completed the code. + // + the onFulfill as the dependency because onFulfill is changed when the preferred locale changed => avoid auto submit form when preferred locale changed. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value, shouldSubmitOnComplete]); + + /** + * Focuses on the input when it is pressed. + * + * @param event + * @param index + */ + const onFocus = (event: NativeSyntheticEvent) => { + if (shouldFocusLast.current) { + lastValue.current = TEXT_INPUT_EMPTY_STATE; + setInputAndIndex(lastFocusedIndex.current); + } + event.preventDefault(); + }; + + /** + * Callback for the onPress event, updates the indexes + * of the currently focused input. + * + * @param index + */ + const onPress = (index: number) => { + shouldFocusLast.current = false; + // TapGestureHandler works differently on mobile web and native app + // On web gesture handler doesn't block interactions with textInput below so there is no need to run `focus()` manually + if (!Browser.isMobileChrome() && !Browser.isMobileSafari()) { + inputRefs.current?.focus(); + } + setInputAndIndex(index); + lastFocusedIndex.current = index; + }; + + /** + * Updates the magic inputs with the contents written in the + * input. It spreads each number into each input and updates + * the focused input on the next empty one, if exists. + * It handles both fast typing and only one digit at a time + * in a specific position. + * + * @param value + */ + const onChangeText = (val: string) => { + console.log('ON CHANGE', val) + if (!val || !ValidationUtils.isNumeric(val)) { + return; + } + + // Checks if one new character was added, or if the content was replaced + const hasToSlice = val.length - 1 === lastValue.current.length && val.slice(0, val.length - 1) === lastValue.current; + + // Gets the new value added by the user + const addedValue = hasToSlice ? val.slice(lastValue.current.length, val.length) : val; + + lastValue.current = val; + // Updates the focused input taking into consideration the last input + // edited and the number of digits added by the user. + const numbersArr = addedValue + .trim() + .split('') + .slice(0, maxLength - editIndex); + const updatedFocusedIndex = Math.min(editIndex + (numbersArr.length - 1) + 1, maxLength - 1); + + let numbers = decomposeString(val, maxLength); + numbers = [...numbers.slice(0, editIndex), ...numbersArr, ...numbers.slice(numbersArr.length + editIndex, maxLength)]; + + setInputAndIndex(updatedFocusedIndex); + + const finalInput = composeToString(numbers); + onChangeTextProp(finalInput); + }; + + /** + * Handles logic related to certain key presses. + * + * NOTE: when using Android Emulator, this can only be tested using + * hardware keyboard inputs. + * + * @param event + */ + const onKeyPress = ({nativeEvent: {key: keyValue}}) => { + if (keyValue === 'Backspace' || keyValue === '<') { + let numbers = decomposeString(value, maxLength); + + // If keyboard is disabled and no input is focused we need to remove + // the last entered digit and focus on the correct input + if (isDisableKeyboard && focusedIndex === undefined) { + const indexBeforeLastEditIndex = editIndex === 0 ? editIndex : editIndex - 1; + + const indexToFocus = numbers[editIndex] === CONST.MAGIC_CODE_EMPTY_CHAR ? indexBeforeLastEditIndex : editIndex; + inputRefs.current[indexToFocus].focus(); + onChangeTextProp(value.substring(0, indexToFocus)); + + return; + } + + // If the currently focused index already has a value, it will delete + // that value but maintain the focus on the same input. + if (numbers[focusedIndex] !== CONST.MAGIC_CODE_EMPTY_CHAR) { + setInput(TEXT_INPUT_EMPTY_STATE); + numbers = [...numbers.slice(0, focusedIndex), CONST.MAGIC_CODE_EMPTY_CHAR, ...numbers.slice(focusedIndex + 1, maxLength)]; + setEditIndex(focusedIndex); + onChangeTextProp(composeToString(numbers)); + return; + } + + const hasInputs = numbers.filter((n) => ValidationUtils.isNumeric(n)).length !== 0 + + // Fill the array with empty characters if there are no inputs. + if (focusedIndex === 0 && !hasInputs) { + numbers = Array(maxLength).fill(CONST.MAGIC_CODE_EMPTY_CHAR); + + // Deletes the value of the previous input and focuses on it. + } else if (focusedIndex !== 0) { + numbers = [...numbers.slice(0, Math.max(0, focusedIndex - 1)), CONST.MAGIC_CODE_EMPTY_CHAR, ...numbers.slice(focusedIndex, maxLength)]; + } + + const newFocusedIndex = Math.max(0, focusedIndex - 1); + + // Saves the input string so that it can compare to the change text + // event that will be triggered, this is a workaround for mobile that + // triggers the change text on the event after the key press. + setInputAndIndex(newFocusedIndex); + onChangeTextProp(composeToString(numbers)); + + if (newFocusedIndex !== undefined) { + inputRefs.current?.focus(); + } + } + if (keyValue === 'ArrowLeft' && focusedIndex !== undefined) { + const newFocusedIndex = Math.max(0, focusedIndex - 1); + setInputAndIndex(newFocusedIndex); + inputRefs.current?.focus(); + } else if (keyValue === 'ArrowRight' && focusedIndex !== undefined) { + const newFocusedIndex = Math.min(focusedIndex + 1, maxLength - 1); + setInputAndIndex(newFocusedIndex); + inputRefs.current?.focus(); + } else if (keyValue === 'Enter') { + // We should prevent users from submitting when it's offline. + if (isOffline) { + return; + } + setInput(TEXT_INPUT_EMPTY_STATE); + onFulfill(value); + } + }; + + /** + * If isDisableKeyboard is true we will have to call onKeyPress and onChangeText manually + * as the press on digit pad will not trigger native events. We take lastPressedDigit from props + * as it stores the last pressed digit pressed on digit pad. We take only the first character + * as anything after that is added to differentiate between two same digits passed in a row. + */ + + useEffect(() => { + if (!isDisableKeyboard) { + return; + } + + const val = lastPressedDigit.charAt(0); + onKeyPress({nativeEvent: {key: val}}); + onChangeText(val); + + // We have not added: + // + the onChangeText and onKeyPress as the dependencies because we only want to run this when lastPressedDigit changes. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [lastPressedDigit, isDisableKeyboard]); + + return ( + <> + + { + onPress(Math.floor(e.nativeEvent.x / (inputWidth.current / maxLength))); + }} + > + {/* Android does not handle touch on invisible Views so I created a wrapper around invisible TextInput just to handle taps */} + + { + inputWidth.current = e.nativeEvent.layout.width; + }} + ref={(inputRef) => (inputRefs.current = inputRef)} + autoFocus={autoFocus} + inputMode="numeric" + textContentType="oneTimeCode" + name={name} + maxLength={maxLength} + value={input} + hideFocusedState + autoComplete={input.length === 0 && autoComplete} + shouldDelayFocus={input.length === 0 && shouldDelayFocus} + keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} + onChangeText={(text: string) => { + onChangeText(text); + }} + onKeyPress={onKeyPress} + onFocus={onFocus} + onBlur={() => { + shouldFocusLast.current = true; + lastFocusedIndex.current = focusedIndex; + setFocusedIndex(undefined); + }} + selectionColor="transparent" + inputStyle={[styles.inputTransparent]} + role={CONST.ROLE.FORM} + style={[styles.inputTransparent]} + textInputContainerStyles={[styles.borderNone]} + /> + + + {getInputPlaceholderSlots(maxLength).map((index) => ( + + + {decomposeString(value, maxLength)[index] || ''} + + + ))} + + {errorText && ( + + )} + + ); +} + +MagicCodeInput.displayName = 'MagicCodeInput'; + +export default forwardRef(MagicCodeInput); From f81a7fa611474f9651b915e2f9452eaca197c164 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 3 Jan 2024 12:39:00 +0100 Subject: [PATCH 010/242] fix focus issue --- .../{MagicCodeInput.js => MagicCodeInput.tsx} | 176 ++++++++++-------- 1 file changed, 103 insertions(+), 73 deletions(-) rename src/components/{MagicCodeInput.js => MagicCodeInput.tsx} (72%) diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.tsx similarity index 72% rename from src/components/MagicCodeInput.js rename to src/components/MagicCodeInput.tsx index 55a65237a691..b238c774405c 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.tsx @@ -1,8 +1,7 @@ import PropTypes from 'prop-types'; import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import {StyleSheet, View} from 'react-native'; +import {NativeSyntheticEvent, StyleSheet, TextInputFocusEventData, View} from 'react-native'; import {TapGestureHandler} from 'react-native-gesture-handler'; -import _ from 'underscore'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -10,17 +9,12 @@ import * as Browser from '@libs/Browser'; import * as ValidationUtils from '@libs/ValidationUtils'; import CONST from '@src/CONST'; import FormHelpMessage from './FormHelpMessage'; -import networkPropTypes from './networkPropTypes'; -import {withNetwork} from './OnyxProvider'; import Text from './Text'; import TextInput from './TextInput'; const TEXT_INPUT_EMPTY_STATE = ''; const propTypes = { - /** Information about the network */ - network: networkPropTypes.isRequired, - /** Name attribute for the input */ name: PropTypes.string, @@ -63,6 +57,49 @@ const propTypes = { lastPressedDigit: PropTypes.string, }; +type MagicCodeInputProps = { + /** Name attribute for the input */ + name?: string, + + /** Input value */ + value?: string, + + /** Should the input auto focus */ + autoFocus?: boolean, + + /** Whether we should wait before focusing the TextInput, useful when using transitions */ + shouldDelayFocus?: boolean, + + /** Error text to display */ + errorText?: string, + + /** Specifies autocomplete hints for the system, so it can provide autofill */ + autoComplete: 'sms-otp' | 'one-time-code' | 'off', + + /* Should submit when the input is complete */ + shouldSubmitOnComplete?: boolean, + + /** Function to call when the input is changed */ + onChangeText?: (value: string) => void, + + /** Function to call when the input is submitted or fully complete */ + onFulfill?: (value: string) => void, + + /** Specifies if the input has a validation error */ + hasError?: boolean, + + /** Specifies the max length of the input */ + maxLength?: number, + + /** Specifies if the keyboard should be disabled */ + isDisableKeyboard?: boolean, + + /** Last pressed digit on BigDigitPad */ + lastPressedDigit?: string, + + innerRef: unknown; +} + const defaultProps = { value: '', name: '', @@ -82,13 +119,9 @@ const defaultProps = { /** * Converts a given string into an array of numbers that must have the same * number of elements as the number of inputs. - * - * @param {String} value - * @param {Number} length - * @returns {Array} */ -const decomposeString = (value, length) => { - let arr = _.map(value.split('').slice(0, length), (v) => (ValidationUtils.isNumeric(v) ? v : CONST.MAGIC_CODE_EMPTY_CHAR)); +const decomposeString = (value: string, length: number): string[] => { + let arr = value.split('').slice(0, length).map((v) => (ValidationUtils.isNumeric(v) ? v : CONST.MAGIC_CODE_EMPTY_CHAR)) if (arr.length < length) { arr = arr.concat(Array(length - arr.length).fill(CONST.MAGIC_CODE_EMPTY_CHAR)); } @@ -98,26 +131,24 @@ const decomposeString = (value, length) => { /** * Converts an array of strings into a single string. If there are undefined or * empty values, it will replace them with a space. - * - * @param {Array} value - * @returns {String} */ -const composeToString = (value) => _.map(value, (v) => (v === undefined || v === '' ? CONST.MAGIC_CODE_EMPTY_CHAR : v)).join(''); +const composeToString = (value: string[]): string => value.map((v) => (v === undefined || v === '' ? CONST.MAGIC_CODE_EMPTY_CHAR : v)).join(''); -const getInputPlaceholderSlots = (length) => Array.from(Array(length).keys()); +const getInputPlaceholderSlots = (length: number): number[] => Array.from(Array(length).keys()); -function MagicCodeInput(props) { +function MagicCodeInput(props: MagicCodeInputProps) { + const {value = '', name = '', autoFocus = true, shouldDelayFocus = false, errorText = '', shouldSubmitOnComplete = true, onChangeText: onChangeTextProp = () => {}} = props const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const inputRefs = useRef(); const [input, setInput] = useState(TEXT_INPUT_EMPTY_STATE); - const [focusedIndex, setFocusedIndex] = useState(0); + const [focusedIndex, setFocusedIndex] = useState(0); const [editIndex, setEditIndex] = useState(0); const [wasSubmitted, setWasSubmitted] = useState(false); const shouldFocusLast = useRef(false); const inputWidth = useRef(0); const lastFocusedIndex = useRef(0); - const lastValue = useRef(TEXT_INPUT_EMPTY_STATE); + const lastValue = useRef(TEXT_INPUT_EMPTY_STATE); useEffect(() => { lastValue.current = input.length; @@ -135,7 +166,7 @@ function MagicCodeInput(props) { inputRefs.current.focus(); }; - const setInputAndIndex = (index) => { + const setInputAndIndex = (index: number) => { setInput(TEXT_INPUT_EMPTY_STATE); setFocusedIndex(index); setEditIndex(index); @@ -156,7 +187,7 @@ function MagicCodeInput(props) { lastFocusedIndex.current = 0; setInputAndIndex(0); inputRefs.current.focus(); - props.onChangeText(''); + onChangeTextProp(''); }, blur() { blurMagicCodeInput(); @@ -164,8 +195,8 @@ function MagicCodeInput(props) { })); const validateAndSubmit = () => { - const numbers = decomposeString(props.value, props.maxLength); - if (wasSubmitted || !props.shouldSubmitOnComplete || _.filter(numbers, (n) => ValidationUtils.isNumeric(n)).length !== props.maxLength || props.network.isOffline) { + const numbers = decomposeString(value, props.maxLength); + if (wasSubmitted || !shouldSubmitOnComplete || numbers.filter((n) => ValidationUtils.isNumeric(n)).length !== props.maxLength || isOffline) { return; } if (!wasSubmitted) { @@ -174,11 +205,11 @@ function MagicCodeInput(props) { // Blurs the input and removes focus from the last input and, if it should submit // on complete, it will call the onFulfill callback. blurMagicCodeInput(); - props.onFulfill(props.value); + props.onFulfill(value); lastValue.current = ''; }; - useNetwork({onReconnect: validateAndSubmit}); + const {isOffline} = useNetwork({onReconnect: validateAndSubmit}); useEffect(() => { validateAndSubmit(); @@ -187,15 +218,15 @@ function MagicCodeInput(props) { // + the editIndex as the dependency because we don't want to run this logic after focusing on an input to edit it after the user has completed the code. // + the props.onFulfill as the dependency because props.onFulfill is changed when the preferred locale changed => avoid auto submit form when preferred locale changed. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.value, props.shouldSubmitOnComplete]); + }, [value, shouldSubmitOnComplete]); /** * Focuses on the input when it is pressed. * - * @param {Object} event - * @param {Number} index + * @param event + * @param index */ - const onFocus = (event) => { + const onFocus = (event: NativeSyntheticEvent) => { if (shouldFocusLast.current) { lastValue.current = TEXT_INPUT_EMPTY_STATE; setInputAndIndex(lastFocusedIndex.current); @@ -207,9 +238,9 @@ function MagicCodeInput(props) { * Callback for the onPress event, updates the indexes * of the currently focused input. * - * @param {Number} index + * @param index */ - const onPress = (index) => { + const onPress = (index: number) => { shouldFocusLast.current = false; // TapGestureHandler works differently on mobile web and native app // On web gesture handler doesn't block interactions with textInput below so there is no need to run `focus()` manually @@ -227,20 +258,20 @@ function MagicCodeInput(props) { * It handles both fast typing and only one digit at a time * in a specific position. * - * @param {String} value + * @param textValue */ - const onChangeText = (value) => { - if (_.isUndefined(value) || _.isEmpty(value) || !ValidationUtils.isNumeric(value)) { + const onChangeText = (textValue?: string) => { + if (!textValue?.length || !ValidationUtils.isNumeric(textValue)) { return; } // Checks if one new character was added, or if the content was replaced - const hasToSlice = value.length - 1 === lastValue.current.length && value.slice(0, value.length - 1) === lastValue.current; + const hasToSlice = typeof lastValue.current === 'string' && textValue.length - 1 === lastValue.current.length && textValue.slice(0, textValue.length - 1) === lastValue.current; - // Gets the new value added by the user - const addedValue = hasToSlice ? value.slice(lastValue.current.length, value.length) : value; + // Gets the new textValue added by the user + const addedValue = (hasToSlice && typeof lastValue.current === 'string') ? textValue.slice(lastValue.current.length, textValue.length) : textValue; - lastValue.current = value; + lastValue.current = textValue; // Updates the focused input taking into consideration the last input // edited and the number of digits added by the user. const numbersArr = addedValue @@ -249,13 +280,13 @@ function MagicCodeInput(props) { .slice(0, props.maxLength - editIndex); const updatedFocusedIndex = Math.min(editIndex + (numbersArr.length - 1) + 1, props.maxLength - 1); - let numbers = decomposeString(props.value, props.maxLength); + let numbers = decomposeString(value, props.maxLength); numbers = [...numbers.slice(0, editIndex), ...numbersArr, ...numbers.slice(numbersArr.length + editIndex, props.maxLength)]; setInputAndIndex(updatedFocusedIndex); const finalInput = composeToString(numbers); - props.onChangeText(finalInput); + onChangeTextProp(finalInput); }; /** @@ -264,11 +295,11 @@ function MagicCodeInput(props) { * NOTE: when using Android Emulator, this can only be tested using * hardware keyboard inputs. * - * @param {Object} event + * @param event */ const onKeyPress = ({nativeEvent: {key: keyValue}}) => { if (keyValue === 'Backspace' || keyValue === '<') { - let numbers = decomposeString(props.value, props.maxLength); + let numbers = decomposeString(value, props.maxLength); // If keyboard is disabled and no input is focused we need to remove // the last entered digit and focus on the correct input @@ -277,59 +308,59 @@ function MagicCodeInput(props) { const indexToFocus = numbers[editIndex] === CONST.MAGIC_CODE_EMPTY_CHAR ? indexBeforeLastEditIndex : editIndex; inputRefs.current[indexToFocus].focus(); - props.onChangeText(props.value.substring(0, indexToFocus)); + onChangeTextProp(value.substring(0, indexToFocus)); return; } // If the currently focused index already has a value, it will delete // that value but maintain the focus on the same input. - if (numbers[focusedIndex] !== CONST.MAGIC_CODE_EMPTY_CHAR) { + if (focusedIndex && numbers[focusedIndex] !== CONST.MAGIC_CODE_EMPTY_CHAR) { setInput(TEXT_INPUT_EMPTY_STATE); numbers = [...numbers.slice(0, focusedIndex), CONST.MAGIC_CODE_EMPTY_CHAR, ...numbers.slice(focusedIndex + 1, props.maxLength)]; setEditIndex(focusedIndex); - props.onChangeText(composeToString(numbers)); + onChangeTextProp(composeToString(numbers)); return; } - const hasInputs = _.filter(numbers, (n) => ValidationUtils.isNumeric(n)).length !== 0; + const hasInputs = numbers.filter((n) => ValidationUtils.isNumeric(n)).length !== 0; // Fill the array with empty characters if there are no inputs. if (focusedIndex === 0 && !hasInputs) { numbers = Array(props.maxLength).fill(CONST.MAGIC_CODE_EMPTY_CHAR); // Deletes the value of the previous input and focuses on it. - } else if (focusedIndex !== 0) { + } else if (focusedIndex && focusedIndex !== 0) { numbers = [...numbers.slice(0, Math.max(0, focusedIndex - 1)), CONST.MAGIC_CODE_EMPTY_CHAR, ...numbers.slice(focusedIndex, props.maxLength)]; } - const newFocusedIndex = Math.max(0, focusedIndex - 1); + const newFocusedIndex = Math.max(0, (focusedIndex ?? 0) - 1); // Saves the input string so that it can compare to the change text // event that will be triggered, this is a workaround for mobile that // triggers the change text on the event after the key press. setInputAndIndex(newFocusedIndex); - props.onChangeText(composeToString(numbers)); + onChangeTextProp(composeToString(numbers)); - if (!_.isUndefined(newFocusedIndex)) { + if (newFocusedIndex !== undefined) { inputRefs.current.focus(); } } - if (keyValue === 'ArrowLeft' && !_.isUndefined(focusedIndex)) { + if (keyValue === 'ArrowLeft' && focusedIndex !== undefined) { const newFocusedIndex = Math.max(0, focusedIndex - 1); setInputAndIndex(newFocusedIndex); inputRefs.current.focus(); - } else if (keyValue === 'ArrowRight' && !_.isUndefined(focusedIndex)) { + } else if (keyValue === 'ArrowRight' && focusedIndex !== undefined) { const newFocusedIndex = Math.min(focusedIndex + 1, props.maxLength - 1); setInputAndIndex(newFocusedIndex); inputRefs.current.focus(); } else if (keyValue === 'Enter') { // We should prevent users from submitting when it's offline. - if (props.network.isOffline) { + if (isOffline) { return; } setInput(TEXT_INPUT_EMPTY_STATE); - props.onFulfill(props.value); + props.onFulfill(value); } }; @@ -345,9 +376,9 @@ function MagicCodeInput(props) { return; } - const value = props.lastPressedDigit.charAt(0); - onKeyPress({nativeEvent: {key: value}}); - onChangeText(value); + const textValue = props.lastPressedDigit.charAt(0); + onKeyPress({nativeEvent: {key: textValue}}); + onChangeText(textValue); // We have not added: // + the onChangeText and onKeyPress as the dependencies because we only want to run this when lastPressedDigit changes. @@ -372,18 +403,18 @@ function MagicCodeInput(props) { inputWidth.current = e.nativeEvent.layout.width; }} ref={(ref) => (inputRefs.current = ref)} - autoFocus={props.autoFocus} + autoFocus={autoFocus} inputMode="numeric" textContentType="oneTimeCode" - name={props.name} + name={name} maxLength={props.maxLength} value={input} hideFocusedState autoComplete={input.length === 0 && props.autoComplete} - shouldDelayFocus={input.length === 0 && props.shouldDelayFocus} + shouldDelayFocus={input.length === 0 && shouldDelayFocus} keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} - onChangeText={(value) => { - onChangeText(value); + onChangeText={(textValue) => { + onChangeText(textValue); }} onKeyPress={onKeyPress} onFocus={onFocus} @@ -394,13 +425,14 @@ function MagicCodeInput(props) { }} selectionColor="transparent" inputStyle={[styles.inputTransparent]} - role={CONST.ACCESSIBILITY_ROLE.TEXT} + // role={CONST.ACCESSIBILITY_ROLE.TEXT} + role='none' style={[styles.inputTransparent]} textInputContainerStyles={[styles.borderNone]} /> - {_.map(getInputPlaceholderSlots(props.maxLength), (index) => ( + {getInputPlaceholderSlots(props.maxLength).map((index) => ( - {decomposeString(props.value, props.maxLength)[index] || ''} + {decomposeString(value, props.maxLength)[index] || ''} ))} - {!_.isEmpty(props.errorText) && ( + {errorText && ( )} @@ -440,6 +472,4 @@ const MagicCodeInputWithRef = forwardRef((props, ref) => ( /> )); -MagicCodeInputWithRef.displayName = 'MagicCodeInputWithRef'; - -export default withNetwork()(MagicCodeInputWithRef); +export default MagicCodeInputWithRef; From a9aaa2857eb7cce120f30564237a75d190d585db Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 4 Jan 2024 09:49:42 +0100 Subject: [PATCH 011/242] ref: started AttachemntModal migration --- ...AttachmentModal.js => AttachmentModal.tsx} | 327 ++++++++---------- src/libs/TransactionUtils.ts | 4 +- 2 files changed, 154 insertions(+), 177 deletions(-) rename src/components/{AttachmentModal.js => AttachmentModal.tsx} (64%) diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.tsx similarity index 64% rename from src/components/AttachmentModal.js rename to src/components/AttachmentModal.tsx index 51912c04eb31..80c2428b534a 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.tsx @@ -1,12 +1,11 @@ import Str from 'expensify-common/lib/str'; import lodashExtend from 'lodash/extend'; -import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {Animated, Keyboard, View} from 'react-native'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import {ValueOf} from 'type-fest'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -14,7 +13,6 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; -import compose from '@libs/compose'; import fileDownload from '@libs/fileDownload'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -22,11 +20,15 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import useNativeDriver from '@libs/useNativeDriver'; +import {AvatarSource} from '@libs/UserUtils'; import reportPropTypes from '@pages/reportPropTypes'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; +import {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import * as OnyxTypes from '@src/types/onyx'; +import {isNotEmptyObject} from '@src/types/utils/EmptyObject'; import AttachmentCarousel from './Attachments/AttachmentCarousel'; import AttachmentView from './Attachments/AttachmentView'; import Button from './Button'; @@ -37,108 +39,92 @@ import * as Expensicons from './Icon/Expensicons'; import sourcePropTypes from './Image/sourcePropTypes'; import Modal from './Modal'; import SafeAreaConsumer from './SafeAreaConsumer'; -import transactionPropTypes from './transactionPropTypes'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; /** * Modal render prop component that exposes modal launching triggers that can be used * to display a full size image or PDF modally with optional confirmation button. */ -const propTypes = { - /** Optional source (URL, SVG function) for the image shown. If not passed in via props must be specified when modal is opened. */ - source: PropTypes.oneOfType([PropTypes.string, sourcePropTypes]), - - /** Optional callback to fire when we want to preview an image and approve it for use. */ - onConfirm: PropTypes.func, - - /** Whether the modal should be open by default */ - defaultOpen: PropTypes.bool, - - /** Optional callback to fire when we want to do something after modal show. */ - onModalShow: PropTypes.func, - - /** Optional callback to fire when we want to do something after modal hide. */ - onModalHide: PropTypes.func, - - /** Optional callback to fire when we want to do something after attachment carousel changes. */ - onCarouselAttachmentChange: PropTypes.func, - - /** Optional original filename when uploading */ - originalFileName: PropTypes.string, - - /** A function as a child to pass modal launching methods to */ - children: PropTypes.func, - - /** Whether source url requires authentication */ - isAuthTokenRequired: PropTypes.bool, - - /** Determines if download Button should be shown or not */ - allowDownload: PropTypes.bool, - - /** Title shown in the header of the modal */ - headerTitle: PropTypes.string, - - /** The report that has this attachment */ - report: reportPropTypes, - - /** The transaction associated with the receipt attachment, if any */ - transaction: transactionPropTypes, - - ...withLocalizePropTypes, - - ...windowDimensionsPropTypes, +type AttachmentModalOnyxProps = { + transaction: OnyxEntry; + parentReport: OnyxEntry; + policy: OnyxEntry; + parentReportActions: OnyxEntry; + session: OnyxEntry; +}; - /** Denotes whether it is a workspace avatar or not */ - isWorkspaceAvatar: PropTypes.bool, +type File = { + name: string; +}; - /** Whether it is a receipt attachment or not */ - isReceiptAttachment: PropTypes.bool, +type ChildrenProps = { + displayFileInModal: (data: any) => void; + show: () => void; }; -const defaultProps = { - source: '', - onConfirm: null, - defaultOpen: false, - originalFileName: '', - children: null, - isAuthTokenRequired: false, - allowDownload: false, - headerTitle: null, - report: {}, - transaction: {}, - onModalShow: () => {}, - onModalHide: () => {}, - onCarouselAttachmentChange: () => {}, - isWorkspaceAvatar: false, - isReceiptAttachment: false, +type AttachmentModalProps = AttachmentModalOnyxProps & { + source?: string | AvatarSource | number; + onConfirm?: ((file: File) => void) | null; + defaultOpen?: boolean; + originalFileName?: string; + isAuthTokenRequired?: boolean; + allowDownload?: boolean; + headerTitle?: string; + report?: OnyxTypes.Report; + onModalShow?: () => void; + onModalHide?: (e: any) => void; + onCarouselAttachmentChange?: (attachment: {source: string; isAuthTokenRequired: boolean; file: {name: string}; isReceipt: boolean}) => void; + isWorkspaceAvatar?: boolean; + isReceiptAttachment?: boolean; + children?: React.FC; + fallbackSource?: string | AvatarSource | number; }; -function AttachmentModal(props) { +function AttachmentModal({ + source = '', + onConfirm = null, + defaultOpen = false, + originalFileName = '', + isAuthTokenRequired = false, + allowDownload = false, + report, + onModalShow = () => {}, + onModalHide = () => {}, + onCarouselAttachmentChange = () => {}, + isReceiptAttachment = false, + isWorkspaceAvatar = false, + transaction, + parentReport, + parentReportActions, + headerTitle, + policy, + children, + fallbackSource, +}: AttachmentModalProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const [isModalOpen, setIsModalOpen] = useState(props.defaultOpen); + const [isModalOpen, setIsModalOpen] = useState(defaultOpen); const [shouldLoadAttachment, setShouldLoadAttachment] = useState(false); const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false); + const [isDeleteReceiptConfirmModalVisible, setIsDeleteReceiptConfirmModalVisible] = useState(false); - const [isAuthTokenRequired, setIsAuthTokenRequired] = useState(props.isAuthTokenRequired); + const [isAuthTokenRequiredState, setIsAuthTokenRequiredState] = useState(isAuthTokenRequired); const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState(''); - const [attachmentInvalidReason, setAttachmentInvalidReason] = useState(null); - const [source, setSource] = useState(props.source); - const [modalType, setModalType] = useState(CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE); + const [attachmentInvalidReason, setAttachmentInvalidReason] = useState(null); + const [sourceState, setSourceState] = useState(source); + const [modalType, setModalType] = useState>(CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE); const [isConfirmButtonDisabled, setIsConfirmButtonDisabled] = useState(false); const [confirmButtonFadeAnimation] = useState(() => new Animated.Value(1)); const [isDownloadButtonReadyToBeShown, setIsDownloadButtonReadyToBeShown] = React.useState(true); - const {windowWidth} = useWindowDimensions(); + const {windowWidth, isSmallScreenWidth} = useWindowDimensions(); - const isOverlayModalVisible = (props.isReceiptAttachment && isDeleteReceiptConfirmModalVisible) || (!props.isReceiptAttachment && isAttachmentInvalid); + const isOverlayModalVisible = (isReceiptAttachment && isDeleteReceiptConfirmModalVisible) || (!isReceiptAttachment && isAttachmentInvalid); const [file, setFile] = useState( - props.originalFileName + originalFileName ? { - name: props.originalFileName, + name: originalFileName, } : undefined, ); @@ -146,10 +132,8 @@ function AttachmentModal(props) { const {isOffline} = useNetwork(); useEffect(() => { - setFile(props.originalFileName ? {name: props.originalFileName} : undefined); - }, [props.originalFileName]); - - const onCarouselAttachmentChange = props.onCarouselAttachmentChange; + setFile(originalFileName ? {name: originalFileName} : undefined); + }, [originalFileName]); /** * Keeps the attachment source in sync with the attachment displayed currently in the carousel. @@ -157,9 +141,9 @@ function AttachmentModal(props) { */ const onNavigate = useCallback( (attachment) => { - setSource(attachment.source); + setSourceState(attachment.source); setFile(attachment.file); - setIsAuthTokenRequired(attachment.isAuthTokenRequired); + setIsAuthTokenRequiredState(attachment.isAuthTokenRequired); onCarouselAttachmentChange(attachment); }, [onCarouselAttachmentChange], @@ -180,7 +164,7 @@ function AttachmentModal(props) { ); const setDownloadButtonVisibility = useCallback( - (isButtonVisible) => { + (isButtonVisible: boolean) => { if (isDownloadButtonReadyToBeShown === isButtonVisible) { return; } @@ -193,17 +177,17 @@ function AttachmentModal(props) { * Download the currently viewed attachment. */ const downloadAttachment = useCallback(() => { - let sourceURL = source; - if (isAuthTokenRequired) { - sourceURL = addEncryptedAuthTokenToURL(sourceURL); + let sourceURL = sourceState; + if (isAuthTokenRequiredState) { + sourceURL = addEncryptedAuthTokenToURL(sourceURL ?? ''); } - fileDownload(sourceURL, lodashGet(file, 'name', '')); + fileDownload(sourceURL, file?.name ?? ''); // At ios, if the keyboard is open while opening the attachment, then after downloading // the attachment keyboard will show up. So, to fix it we need to dismiss the keyboard. Keyboard.dismiss(); - }, [isAuthTokenRequired, source, file]); + }, [isAuthTokenRequiredState, sourceState, file]); /** * Execute the onConfirm callback and close the modal. @@ -215,13 +199,13 @@ function AttachmentModal(props) { return; } - if (props.onConfirm) { - props.onConfirm(lodashExtend(file, {source})); + if (onConfirm) { + onConfirm(lodashExtend(file, {sourceState})); } setIsModalOpen(false); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isModalOpen, isConfirmButtonDisabled, props.onConfirm, file, source]); + }, [isModalOpen, isConfirmButtonDisabled, onConfirm, file, sourceState]); /** * Close the confirm modals. @@ -235,24 +219,24 @@ function AttachmentModal(props) { * Detach the receipt and close the modal. */ const deleteAndCloseModal = useCallback(() => { - IOU.detachReceipt(props.transaction.transactionID, props.report.reportID); + IOU.detachReceipt(transaction?.transactionID); setIsDeleteReceiptConfirmModalVisible(false); - Navigation.dismissModal(props.report.reportID); - }, [props.transaction, props.report]); + Navigation.dismissModal(report?.reportID); + }, [transaction, report]); /** * @param {Object} _file * @returns {Boolean} */ const isValidFile = useCallback((_file) => { - if (lodashGet(_file, 'size', 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) { + if ((_file.size ?? 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) { setIsAttachmentInvalid(true); setAttachmentInvalidReasonTitle('attachmentPicker.attachmentTooLarge'); setAttachmentInvalidReason('attachmentPicker.sizeExceeded'); return false; } - if (lodashGet(_file, 'size', 0) < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) { + if ((_file.size ?? 0) < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) { setIsAttachmentInvalid(true); setAttachmentInvalidReasonTitle('attachmentPicker.attachmentTooSmall'); setAttachmentInvalidReason('attachmentPicker.sizeNotMet'); @@ -308,13 +292,13 @@ function AttachmentModal(props) { const inputSource = URL.createObjectURL(updatedFile); const inputModalType = getModalType(inputSource, updatedFile); setIsModalOpen(true); - setSource(inputSource); + setSourceState(inputSource); setFile(updatedFile); setModalType(inputModalType); } else { const inputModalType = getModalType(fileObject.uri, fileObject); setIsModalOpen(true); - setSource(fileObject.uri); + setSourceState(fileObject.uri); setFile(fileObject); setModalType(inputModalType); } @@ -331,7 +315,7 @@ function AttachmentModal(props) { * @param {Boolean} shouldFadeOut If true, fade out confirm button. Otherwise fade in. */ const updateConfirmButtonVisibility = useCallback( - (shouldFadeOut) => { + (shouldFadeOut: boolean) => { setIsConfirmButtonDisabled(shouldFadeOut); const toValue = shouldFadeOut ? 0 : 1; @@ -359,44 +343,44 @@ function AttachmentModal(props) { }, []); useEffect(() => { - setSource(props.source); - }, [props.source]); + setSourceState(source); + }, [source]); useEffect(() => { - setIsAuthTokenRequired(props.isAuthTokenRequired); - }, [props.isAuthTokenRequired]); + setIsAuthTokenRequiredState(isAuthTokenRequired); + }, [isAuthTokenRequired]); - const sourceForAttachmentView = props.source || source; + const sourceForAttachmentView = source || source; const threeDotsMenuItems = useMemo(() => { - if (!props.isReceiptAttachment || !props.parentReport || !props.parentReportActions) { + if (!isReceiptAttachment || !parentReport || !parentReportActions) { return []; } const menuItems = []; - const parentReportAction = props.parentReportActions[props.report.parentReportActionID]; + const parentReportAction = parentReportActions[report?.parentReportActionID ?? '']; const canEdit = - ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, props.parentReport.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT, props.transaction) && - !TransactionUtils.isDistanceRequest(props.transaction); + ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, parentReport.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT, transaction) && + !TransactionUtils.isDistanceRequest(transaction); if (canEdit) { menuItems.push({ icon: Expensicons.Camera, - text: props.translate('common.replace'), + text: translate('common.replace'), onSelected: () => { closeModal(); - Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(props.report.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT)); + Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report?.reportID ?? '', CONST.EDIT_REQUEST_FIELD.RECEIPT)); }, }); } menuItems.push({ icon: Expensicons.Download, - text: props.translate('common.download'), - onSelected: () => downloadAttachment(source), + text: translate('common.download'), + onSelected: () => downloadAttachment(sourceState), }); - if (TransactionUtils.hasReceipt(props.transaction) && !TransactionUtils.isReceiptBeingScanned(props.transaction) && canEdit) { + if (TransactionUtils.hasReceipt(transaction) && !TransactionUtils.isReceiptBeingScanned(transaction) && canEdit) { menuItems.push({ icon: Expensicons.Trashcan, - text: props.translate('receipt.deleteReceipt'), + text: translate('receipt.deleteReceipt'), onSelected: () => { setIsDeleteReceiptConfirmModalVisible(true); }, @@ -404,17 +388,17 @@ function AttachmentModal(props) { } return menuItems; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.isReceiptAttachment, props.parentReport, props.parentReportActions, props.policy, props.transaction, file, source]); + }, [isReceiptAttachment, parentReport, parentReportActions, policy, transaction, file, sourceState]); // There are a few things that shouldn't be set until we absolutely know if the file is a receipt or an attachment. // props.isReceiptAttachment will be null until its certain what the file is, in which case it will then be true|false. - let headerTitle = props.headerTitle; + let headerTitleValue = headerTitle; let shouldShowDownloadButton = false; let shouldShowThreeDotsButton = false; - if (!_.isEmpty(props.report)) { - headerTitle = translate(props.isReceiptAttachment ? 'common.receipt' : 'common.attachment'); - shouldShowDownloadButton = props.allowDownload && isDownloadButtonReadyToBeShown && !props.isReceiptAttachment && !isOffline; - shouldShowThreeDotsButton = props.isReceiptAttachment && isModalOpen; + if (isNotEmptyObject(report)) { + headerTitleValue = translate(isReceiptAttachment ? 'common.receipt' : 'common.attachment'); + shouldShowDownloadButton = allowDownload && isDownloadButtonReadyToBeShown && !isReceiptAttachment && !isOffline; + shouldShowThreeDotsButton = isReceiptAttachment && isModalOpen; } return ( @@ -426,24 +410,24 @@ function AttachmentModal(props) { isVisible={isModalOpen} backgroundColor={theme.componentBG} onModalShow={() => { - props.onModalShow(); + onModalShow(); setShouldLoadAttachment(true); }} onModalHide={(e) => { - props.onModalHide(e); + onModalHide(e); setShouldLoadAttachment(false); }} propagateSwipe > - {props.isSmallScreenWidth && } + {isSmallScreenWidth && } downloadAttachment(source)} - shouldShowCloseButton={!props.isSmallScreenWidth} - shouldShowBackButton={props.isSmallScreenWidth} + onDownloadButtonPress={() => downloadAttachment(sourceState)} + shouldShowCloseButton={!isSmallScreenWidth} + shouldShowBackButton={isSmallScreenWidth} onBackButtonPress={closeModal} onCloseButtonPress={closeModal} shouldShowThreeDotsButton={shouldShowThreeDotsButton} @@ -452,11 +436,11 @@ function AttachmentModal(props) { shouldOverlay /> - {!_.isEmpty(props.report) && !props.isReceiptAttachment ? ( + {report && !isReceiptAttachment ? ( ) )} {/* If we have an onConfirm method show a confirmation button */} - {Boolean(props.onConfirm) && ( + {Boolean(onConfirm) && ( {({safeAreaPaddingBottomStyle}) => (