From 1457b8b34ab9667d813da0da1a933e917bff57bd Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Thu, 1 Feb 2024 23:27:24 +0800 Subject: [PATCH 001/695] fix emoji picker keyboard issue --- src/CONST.ts | 9 + src/components/EmojiPicker/EmojiPicker.js | 1 + src/components/Modal/BaseModal.tsx | 35 ++- src/components/Modal/ModalContent.tsx | 19 ++ src/components/Modal/index.android.tsx | 10 - src/components/Modal/types.ts | 9 + src/libs/ComposerFocusManager.ts | 273 +++++++++++++++++- src/libs/focusComposerWithDelay.ts | 15 +- .../isWindowReadyToFocus/index.android.ts | 27 ++ src/libs/isWindowReadyToFocus/index.ts | 3 + 10 files changed, 363 insertions(+), 38 deletions(-) create mode 100644 src/components/Modal/ModalContent.tsx create mode 100644 src/libs/isWindowReadyToFocus/index.android.ts create mode 100644 src/libs/isWindowReadyToFocus/index.ts diff --git a/src/CONST.ts b/src/CONST.ts index 1ccdfd9a82a8..4c5523790a91 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -718,6 +718,15 @@ const CONST = { RIGHT: 'right', }, POPOVER_MENU_PADDING: 8, + BUSINESS_TYPE: { + DEFAULT: 'default', + ATTACHMENT: 'attachment', + }, + RESTORE_FOCUS_TYPE: { + DEFAULT: 'default', + DELETE: 'delete', + PRESERVE: 'preserve', + }, }, TIMING: { CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION: 'calc_most_recent_last_modified_action', diff --git a/src/components/EmojiPicker/EmojiPicker.js b/src/components/EmojiPicker/EmojiPicker.js index eaf89b7f64ea..10d040301e94 100644 --- a/src/components/EmojiPicker/EmojiPicker.js +++ b/src/components/EmojiPicker/EmojiPicker.js @@ -187,6 +187,7 @@ const EmojiPicker = forwardRef((props, ref) => { outerStyle={StyleUtils.getOuterModalStyle(windowHeight, props.viewportOffsetTop)} innerContainerStyle={styles.popoverInnerContainer} avoidKeyboard + shouldEnableNewFocusManagement > , ) { @@ -55,6 +58,14 @@ function BaseModal( const isVisibleRef = useRef(isVisible); const wasVisible = usePrevious(isVisible); + const modalId = useMemo(() => ComposerFocusManager.getId(), []); + const saveFocusState = () => { + if (shouldEnableNewFocusManagement) { + ComposerFocusManager.saveFocusState(modalId); + } + ComposerFocusManager.resetReadyToFocus(modalId); + }; + /** * Hides modal * @param callHideCallback - Should we call the onModalHide callback @@ -69,11 +80,9 @@ function BaseModal( onModalHide(); } Modal.onModalDidClose(); - if (!fullscreen) { - ComposerFocusManager.setReadyToFocus(); - } + ComposerFocusManager.tryRestoreFocusAfterClosedCompletely(modalId, restoreFocusType); }, - [shouldSetModalVisibility, onModalHide, fullscreen], + [shouldSetModalVisibility, onModalHide, restoreFocusType, modalId], ); useEffect(() => { @@ -121,7 +130,7 @@ function BaseModal( }; const handleDismissModal = () => { - ComposerFocusManager.setReadyToFocus(); + ComposerFocusManager.setReadyToFocus(modalId); }; const { @@ -183,7 +192,7 @@ function BaseModal( onModalShow={handleShowModal} propagateSwipe={propagateSwipe} onModalHide={hideModal} - onModalWillShow={() => ComposerFocusManager.resetReadyToFocus()} + onModalWillShow={saveFocusState} onDismiss={handleDismissModal} onSwipeComplete={() => onClose?.()} swipeDirection={swipeDirection} @@ -207,12 +216,14 @@ function BaseModal( avoidKeyboard={avoidKeyboard} customBackdrop={shouldUseCustomBackdrop ? : undefined} > - - {children} - + + + {children} + + ); } diff --git a/src/components/Modal/ModalContent.tsx b/src/components/Modal/ModalContent.tsx new file mode 100644 index 000000000000..5c8e0d2ece6b --- /dev/null +++ b/src/components/Modal/ModalContent.tsx @@ -0,0 +1,19 @@ +import type {ReactNode} from 'react'; +import React from 'react'; + +type ModalContentProps = { + /** Modal contents */ + children: ReactNode; + + /** called after modal content is dismissed */ + onDismiss: () => void; +}; + +function ModalContent({children, onDismiss = () => {}}: ModalContentProps) { + // eslint-disable-next-line react-hooks/exhaustive-deps + React.useEffect(() => () => onDismiss?.(), []); + return children; +} +ModalContent.displayName = 'ModalContent'; + +export default ModalContent; diff --git a/src/components/Modal/index.android.tsx b/src/components/Modal/index.android.tsx index 86a1fd272185..7cb2c6083752 100644 --- a/src/components/Modal/index.android.tsx +++ b/src/components/Modal/index.android.tsx @@ -1,17 +1,7 @@ import React from 'react'; -import {AppState} from 'react-native'; -import ComposerFocusManager from '@libs/ComposerFocusManager'; import BaseModal from './BaseModal'; import type BaseModalProps from './types'; -AppState.addEventListener('focus', () => { - ComposerFocusManager.setReadyToFocus(); -}); - -AppState.addEventListener('blur', () => { - ComposerFocusManager.resetReadyToFocus(); -}); - // Only want to use useNativeDriver on Android. It has strange flashes issue on IOS // https://github.com/react-native-modal/react-native-modal#the-modal-flashes-in-a-weird-way-when-animating function Modal({useNativeDriver = true, ...rest}: BaseModalProps) { diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts index a0cdb737d448..43a2c281415a 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -62,6 +62,15 @@ type BaseModalProps = Partial & { /** Should we use a custom backdrop for the modal? (This prevents focus issues on desktop) */ shouldUseCustomBackdrop?: boolean; + + /** + * Whether the modal should enable the new focus manager. + * We are attempting to migrate to a new refocus manager, adding this property for gradual migration. + * */ + shouldEnableNewFocusManagement?: boolean; + + /** How to re-focus after the modal is dismissed */ + restoreFocusType?: ValueOf; }; export default BaseModalProps; diff --git a/src/libs/ComposerFocusManager.ts b/src/libs/ComposerFocusManager.ts index b66bbe92599e..88e701a2e569 100644 --- a/src/libs/ComposerFocusManager.ts +++ b/src/libs/ComposerFocusManager.ts @@ -1,25 +1,278 @@ -let isReadyToFocusPromise = Promise.resolve(); -let resolveIsReadyToFocus: (value: void | PromiseLike) => void; +import type {View} from 'react-native'; +import {TextInput} from 'react-native'; +import type {ValueOf} from 'type-fest'; +import CONST from '@src/CONST'; +import isWindowReadyToFocus from './isWindowReadyToFocus'; -function resetReadyToFocus() { - isReadyToFocusPromise = new Promise((resolve) => { - resolveIsReadyToFocus = resolve; +type ModalId = number | undefined; + +type InputElement = (TextInput & HTMLElement) | null; + +/** + * So far, modern browsers only support the file cancel event in some newer versions + * (i.e., Chrome: 113+ / Firefox: 91+ / Safari 16.4+), and there is no standard feature detection method available. + * We will introduce this prop to isolate the impact of the file upload modal on the focus stack. + */ +type BusinessType = ValueOf | undefined; + +type RestoreFocusType = ValueOf | undefined; + +type ModalContainer = View | HTMLElement | undefined | null; + +type FocusMapValue = { + input: InputElement; + businessType?: BusinessType; +}; + +type PromiseMapValue = { + ready: Promise; + resolve: () => void; +}; + +let focusedInput: InputElement = null; +let uniqueModalId = 1; +const focusMap = new Map(); +const activeModals: ModalId[] = []; +const promiseMap = new Map(); + +/** + * react-native-web doesn't support `currentlyFocusedInput`, so we need to make it compatible. + */ +function getActiveInput() { + return (TextInput.State.currentlyFocusedInput ? TextInput.State.currentlyFocusedInput() : TextInput.State.currentlyFocusedField()) as InputElement; +} + +/** + * On web platform, if the modal is displayed by a click, the blur event is fired before the modal appears, + * so we need to cache the focused input in the pointerdown handler, which is fired before the blur event. + */ +function saveFocusedInput() { + focusedInput = getActiveInput(); +} + +/** + * If a click does not display the modal, we also should clear the cached value to avoid potential issues. + */ +function clearFocusedInput() { + if (!focusedInput) { + return; + } + + // we have to use timeout because of measureLayout + setTimeout(() => (focusedInput = null), CONST.ANIMATION_IN_TIMING); +} + +/** + * When a TextInput is unmounted, we also should release the reference here to avoid potential issues. + * + */ +function releaseInput(input: InputElement) { + if (!input) { + return; + } + if (input === focusedInput) { + focusedInput = null; + } + [...focusMap].forEach(([key, value]) => { + if (value.input !== input) { + return; + } + focusMap.delete(key); + }); +} + +function getId() { + return uniqueModalId++; +} + +/** + * Save the focus state when opening the modal. + */ +function saveFocusState(id: ModalId, businessType: BusinessType = CONST.MODAL.BUSINESS_TYPE.DEFAULT, shouldClearFocusWithType = false, container: ModalContainer = undefined) { + const activeInput = getActiveInput(); + + // For popoverWithoutOverlay, react calls autofocus before useEffect. + const input = focusedInput ?? activeInput; + focusedInput = null; + if (activeModals.indexOf(id) < 0) { + activeModals.push(id); + } + + if (shouldClearFocusWithType) { + [...focusMap].forEach(([key, value]) => { + if (value.businessType !== businessType) { + return; + } + focusMap.delete(key); + }); + } + + if (container && 'contains' in container && container.contains(input)) { + return; + } + focusMap.set(id, {input, businessType}); + if (!input) { + return; + } + input.blur(); +} + +/** + * On web platform, if we intentionally click on another input box, there is no need to restore focus. + * Additionally, if we are closing the RHP, we can ignore the focused input. + */ +function focus(input: InputElement, shouldIgnoreFocused = false) { + if (!input) { + return; + } + if (shouldIgnoreFocused) { + isWindowReadyToFocus().then(() => input.focus()); + return; + } + const activeInput = getActiveInput(); + if (activeInput) { + return; + } + isWindowReadyToFocus().then(() => input.focus()); +} + +/** + * Restore the focus state after the modal is dismissed. + */ +function restoreFocusState( + id: ModalId, + shouldIgnoreFocused = false, + restoreFocusType: RestoreFocusType = CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT, + businessType: BusinessType = CONST.MODAL.BUSINESS_TYPE.DEFAULT, +) { + if (!id) { + return; + } + + // The stack is empty + if (activeModals.length < 1) { + return; + } + const index = activeModals.indexOf(id); + + // This id has been removed from the stack. + if (index < 0) { + return; + } + activeModals.splice(index, 1); + if (restoreFocusType === CONST.MODAL.RESTORE_FOCUS_TYPE.PRESERVE) { + return; + } + + const {input} = focusMap.get(id) ?? {}; + focusMap.delete(id); + if (restoreFocusType === CONST.MODAL.RESTORE_FOCUS_TYPE.DELETE) { + return; + } + + // This modal is not the topmost one, do not restore it. + if (activeModals.length > index) { + if (input) { + const lastId = activeModals.slice(-1)[0]; + focusMap.set(lastId, {...focusMap.get(lastId), input}); + } + return; + } + if (input) { + focus(input, shouldIgnoreFocused); + return; + } + + // Try to find the topmost one and restore it + const stack = [...focusMap].filter(([, v]) => v.input && v.businessType === businessType); + if (stack.length < 1) { + return; + } + const [lastId, {input: lastInput}] = stack.slice(-1)[0]; + + // The previous modal is still active + if (activeModals.indexOf(lastId) >= 0) { + return; + } + focus(lastInput, shouldIgnoreFocused); + focusMap.delete(lastId); +} + +function resetReadyToFocus(id: ModalId) { + const promise: PromiseMapValue = { + ready: Promise.resolve(), + resolve: () => {}, + }; + promise.ready = new Promise((resolve) => { + promise.resolve = resolve; }); + promiseMap.set(id, promise); +} + +/** + * Backward compatibility, for cases without an ID, it's fine to just take the topmost one. + */ +function getKey(id: ModalId) { + if (id) { + return id; + } + if (promiseMap.size < 1) { + return 0; + } + return [...promiseMap.keys()].slice(-1)[0]; } -function setReadyToFocus() { - if (!resolveIsReadyToFocus) { +function setReadyToFocus(id?: ModalId) { + const key = getKey(id); + const promise = promiseMap.get(key); + if (!promise) { return; } - resolveIsReadyToFocus(); + promise.resolve?.(); + promiseMap.delete(key); } -function isReadyToFocus(): Promise { - return isReadyToFocusPromise; +function isReadyToFocus(id?: ModalId) { + const key = getKey(id); + const promise = promiseMap.get(key); + if (!promise) { + return Promise.resolve(); + } + return promise.ready; +} + +function tryRestoreFocusAfterClosedCompletely(id: ModalId, restoreType: RestoreFocusType, businessType?: BusinessType) { + isReadyToFocus(id)?.then(() => restoreFocusState(id, false, restoreType, businessType)); +} + +/** + * So far, this will only be called in file canceled event handler. + */ +function tryRestoreFocusByExternal(businessType: BusinessType) { + const stack = [...focusMap].filter(([, value]) => value.businessType === businessType && value.input); + if (stack.length < 1) { + return; + } + const [key, {input}] = stack.slice(-1)[0]; + focusMap.delete(key); + if (!input) { + return; + } + focus(input); } +export type {InputElement}; + export default { + getId, + saveFocusedInput, + clearFocusedInput, + releaseInput, + saveFocusState, + restoreFocusState, resetReadyToFocus, setReadyToFocus, isReadyToFocus, + tryRestoreFocusAfterClosedCompletely, + tryRestoreFocusByExternal, }; diff --git a/src/libs/focusComposerWithDelay.ts b/src/libs/focusComposerWithDelay.ts index 6a2f85f7d311..a61c45325b3b 100644 --- a/src/libs/focusComposerWithDelay.ts +++ b/src/libs/focusComposerWithDelay.ts @@ -1,6 +1,7 @@ import type {TextInput} from 'react-native'; import * as EmojiPickerAction from './actions/EmojiPickerAction'; import ComposerFocusManager from './ComposerFocusManager'; +import isWindowReadyToFocus from './isWindowReadyToFocus'; type FocusComposerWithDelay = (shouldDelay?: boolean) => void; /** @@ -22,12 +23,14 @@ function focusComposerWithDelay(textInput: TextInput | null): FocusComposerWithD textInput.focus(); return; } - ComposerFocusManager.isReadyToFocus().then(() => { - if (!textInput) { - return; - } - textInput.focus(); - }); + ComposerFocusManager.isReadyToFocus() + .then(isWindowReadyToFocus) + .then(() => { + if (!textInput) { + return; + } + textInput.focus(); + }); }; } diff --git a/src/libs/isWindowReadyToFocus/index.android.ts b/src/libs/isWindowReadyToFocus/index.android.ts new file mode 100644 index 000000000000..b9cca1b5a294 --- /dev/null +++ b/src/libs/isWindowReadyToFocus/index.android.ts @@ -0,0 +1,27 @@ +import {AppState} from 'react-native'; + +let isWindowReadyPromise = Promise.resolve(); +let resolveWindowReadyToFocus: () => void; + +AppState.addEventListener('focus', () => { + if (!resolveWindowReadyToFocus) { + return; + } + resolveWindowReadyToFocus(); +}); + +AppState.addEventListener('blur', () => { + isWindowReadyPromise = new Promise((resolve) => { + resolveWindowReadyToFocus = resolve; + }); +}); + +/** + * If we want to show the soft keyboard reliably, we need to ensure that the input's window gains focus first. + * Fortunately, we only need to manage the focus of the app window now, + * so we can achieve this by listening to the 'focus' event of the AppState. + * See {@link https://developer.android.com/develop/ui/views/touch-and-input/keyboard-input/visibility#ShowReliably} + */ +const isWindowReadyToFocus = () => isWindowReadyPromise; + +export default isWindowReadyToFocus; diff --git a/src/libs/isWindowReadyToFocus/index.ts b/src/libs/isWindowReadyToFocus/index.ts new file mode 100644 index 000000000000..7ae3930c0c1d --- /dev/null +++ b/src/libs/isWindowReadyToFocus/index.ts @@ -0,0 +1,3 @@ +const isWindowReadyToFocus = () => Promise.resolve(); + +export default isWindowReadyToFocus; From 70e763c4652f0c29454b2658e3e096bea23b2ccf Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Fri, 2 Feb 2024 12:30:39 +0800 Subject: [PATCH 002/695] fix lint error --- src/components/Modal/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts index 43a2c281415a..6692f2751e40 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -62,7 +62,7 @@ type BaseModalProps = Partial & { /** Should we use a custom backdrop for the modal? (This prevents focus issues on desktop) */ shouldUseCustomBackdrop?: boolean; - + /** * Whether the modal should enable the new focus manager. * We are attempting to migrate to a new refocus manager, adding this property for gradual migration. From 73d9a10a58b9fc5cda0dd6479f4acf635721f965 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Wed, 7 Feb 2024 20:48:26 +0500 Subject: [PATCH 003/695] feat: add delete option to deleteable report fields --- .../API/parameters/DeleteReportFieldParams.ts | 6 ++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + src/libs/actions/Report.ts | 58 +++++++++++++++++++ src/pages/EditReportFieldDatePage.tsx | 11 +++- src/pages/EditReportFieldDropdownPage.tsx | 11 +++- src/pages/EditReportFieldPage.tsx | 20 +++++++ src/pages/EditReportFieldTextPage.tsx | 11 +++- 8 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 src/libs/API/parameters/DeleteReportFieldParams.ts diff --git a/src/libs/API/parameters/DeleteReportFieldParams.ts b/src/libs/API/parameters/DeleteReportFieldParams.ts new file mode 100644 index 000000000000..393c21af0088 --- /dev/null +++ b/src/libs/API/parameters/DeleteReportFieldParams.ts @@ -0,0 +1,6 @@ +type DeleteReportFieldParams = { + reportID: string; + reportFields: string; +}; + +export default DeleteReportFieldParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index b7c3dff7c342..dba006979dec 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -125,3 +125,4 @@ export type {default as CompleteEngagementModalParams} from './CompleteEngagemen export type {default as SetNameValuePairParams} from './SetNameValuePairParams'; export type {default as SetReportFieldParams} from './SetReportFieldParams'; export type {default as SetReportNameParams} from './SetReportNameParams'; +export type {default as DeleteReportFieldParams} from './DeleteReportFieldParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index c011fa395f0f..5c3581a1a6ac 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -115,6 +115,7 @@ const WRITE_COMMANDS = { COMPLETE_ENGAGEMENT_MODAL: 'CompleteEngagementModal', SET_NAME_VALUE_PAIR: 'SetNameValuePair', SET_REPORT_FIELD: 'Report_SetFields', + DELETE_REPORT_FIELD: 'DELETE_ReportFields', SET_REPORT_NAME: 'RenameReport', } as const; @@ -229,6 +230,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_NAME_VALUE_PAIR]: Parameters.SetNameValuePairParams; [WRITE_COMMANDS.SET_REPORT_FIELD]: Parameters.SetReportFieldParams; [WRITE_COMMANDS.SET_REPORT_NAME]: Parameters.SetReportNameParams; + [WRITE_COMMANDS.DELETE_REPORT_FIELD]: Parameters.DeleteReportFieldParams; }; const READ_COMMANDS = { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 4bff826ceb3a..d8e59232688d 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1614,6 +1614,63 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre API.write(WRITE_COMMANDS.SET_REPORT_FIELD, parameters, {optimisticData, failureData, successData}); } +function deleteReportField(reportID: string, reportField: PolicyReportField) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + reportFields: { + [reportField.fieldID]: null, + }, + pendingFields: { + [reportField.fieldID]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + reportFields: { + [reportField.fieldID]: reportField, + }, + pendingFields: { + [reportField.fieldID]: null, + }, + errorFields: { + [reportField.fieldID]: ErrorUtils.getMicroSecondOnyxError('report.genericUpdateReportFieldFailureMessage'), + }, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + pendingFields: { + [reportField.fieldID]: null, + }, + errorFields: { + [reportField.fieldID]: null, + }, + }, + }, + ]; + + const parameters = { + reportID, + reportFields: JSON.stringify({[reportField.fieldID]: reportField}), + }; + + API.write(WRITE_COMMANDS.DELETE_REPORT_FIELD, parameters, {optimisticData, failureData, successData}); +} + function updateWelcomeMessage(reportID: string, previousValue: string, newValue: string) { // No change needed, navigate back if (previousValue === newValue) { @@ -2884,5 +2941,6 @@ export { clearNewRoomFormError, updateReportField, updateReportName, + deleteReportField, resolveActionableMentionWhisper, }; diff --git a/src/pages/EditReportFieldDatePage.tsx b/src/pages/EditReportFieldDatePage.tsx index 82659eca62c2..3379f6e5f4c1 100644 --- a/src/pages/EditReportFieldDatePage.tsx +++ b/src/pages/EditReportFieldDatePage.tsx @@ -5,6 +5,7 @@ import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {OnyxFormValuesFields} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {ThreeDotsMenuItem} from '@components/HeaderWithBackButton/types'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; @@ -26,11 +27,14 @@ type EditReportFieldDatePageProps = { /** Flag to indicate if the field can be left blank */ isRequired: boolean; + /** Three dot menu item options */ + menuItems?: ThreeDotsMenuItem[]; + /** Callback to fire when the Save button is pressed */ onSubmit: (form: OnyxFormValuesFields) => void; }; -function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, fieldID}: EditReportFieldDatePageProps) { +function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, menuItems, fieldID}: EditReportFieldDatePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const inputRef = useRef(null); @@ -55,7 +59,10 @@ function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, f }} testID={EditReportFieldDatePage.displayName} > - + ) => void; }; @@ -37,7 +41,7 @@ type EditReportFieldDropdownPageOnyxProps = { type EditReportFieldDropdownPageProps = EditReportFieldDropdownPageComponentProps & EditReportFieldDropdownPageOnyxProps; -function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, fieldOptions, recentlyUsedReportFields}: EditReportFieldDropdownPageProps) { +function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, fieldOptions, recentlyUsedReportFields, menuItems}: EditReportFieldDropdownPageProps) { const [searchValue, setSearchValue] = useState(''); const styles = useThemeStyles(); const {getSafeAreaMargins} = useStyleUtils(); @@ -80,7 +84,10 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, > {({insets}) => ( <> - + { + ReportActions.deleteReportField(report.reportID, reportField); + Navigation.dismissModal(report?.reportID); + }; + const fieldValue = isReportFieldTitle ? report.reportName ?? '' : reportField.value ?? reportField.defaultValue; + const menuItems: ThreeDotsMenuItem[] = []; + + const isReportFieldDeletable = report.reportFields?.deletable; + + if (isReportFieldDeletable) { + menuItems.push({icon: Expensicons.Trashcan, text: translate('common.delete'), onSelected: () => handleReportFieldDelete()}); + } + if (reportField.type === 'text' || isReportFieldTitle) { return ( ); @@ -96,6 +114,7 @@ function EditReportFieldPage({route, policy, report, policyReportFields}: EditRe fieldID={reportField.fieldID} fieldValue={fieldValue} isRequired={!reportField.deletable} + menuItems={menuItems} onSubmit={handleReportFieldChange} /> ); @@ -109,6 +128,7 @@ function EditReportFieldPage({route, policy, report, policyReportFields}: EditRe fieldName={Str.UCFirst(reportField.name)} fieldValue={fieldValue} fieldOptions={reportField.values} + menuItems={menuItems} onSubmit={handleReportFieldChange} /> ); diff --git a/src/pages/EditReportFieldTextPage.tsx b/src/pages/EditReportFieldTextPage.tsx index ea9d2d3bed6d..f06ad32e1598 100644 --- a/src/pages/EditReportFieldTextPage.tsx +++ b/src/pages/EditReportFieldTextPage.tsx @@ -4,6 +4,7 @@ import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {OnyxFormValuesFields} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {ThreeDotsMenuItem} from '@components/HeaderWithBackButton/types'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; @@ -26,11 +27,14 @@ type EditReportFieldTextPageProps = { /** Flag to indicate if the field can be left blank */ isRequired: boolean; + /** Three dot menu item options */ + menuItems?: ThreeDotsMenuItem[]; + /** Callback to fire when the Save button is pressed */ onSubmit: (form: OnyxFormValuesFields) => void; }; -function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, fieldID}: EditReportFieldTextPageProps) { +function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, fieldID, menuItems}: EditReportFieldTextPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const inputRef = useRef(null); @@ -55,7 +59,10 @@ function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, f }} testID={EditReportFieldTextPage.displayName} > - + Date: Fri, 9 Feb 2024 13:48:09 +0100 Subject: [PATCH 004/695] Implement modal for onboarding --- src/NAVIGATORS.ts | 1 + src/ROUTES.ts | 4 ++++ src/SCREENS.ts | 8 +++++++ src/components/Modal/BaseModal.tsx | 2 +- .../PurposeForUsingExpensifyModal.tsx | 5 +++- src/components/WorkspaceSwitcherButton.tsx | 2 +- .../Navigation/AppNavigator/AuthScreens.tsx | 13 ++++++++-- .../AppNavigator/ModalStackNavigators.tsx | 6 +++++ .../BottomTabBar.tsx | 1 - .../getRootNavigatorScreenOptions.ts | 9 +++++++ src/libs/Navigation/NavigationRoot.tsx | 2 ++ src/libs/Navigation/linkingConfig/config.ts | 24 +++++++++++++++++++ .../linkingConfig/getAdaptedStateFromPath.ts | 3 +++ src/libs/Navigation/types.ts | 8 +++++++ 14 files changed, 82 insertions(+), 6 deletions(-) diff --git a/src/NAVIGATORS.ts b/src/NAVIGATORS.ts index 3bc9c5e57b9b..10e268b07520 100644 --- a/src/NAVIGATORS.ts +++ b/src/NAVIGATORS.ts @@ -7,5 +7,6 @@ export default { BOTTOM_TAB_NAVIGATOR: 'BottomTabNavigator', LEFT_MODAL_NAVIGATOR: 'LeftModalNavigator', RIGHT_MODAL_NAVIGATOR: 'RightModalNavigator', + ONBOARDING_MODAL_NAVIGATOR: 'OnboardingModalNavigator', FULL_SCREEN_NAVIGATOR: 'FullScreenNavigator', } as const; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index e987c5b94d7d..c1ca60e35af0 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -491,6 +491,10 @@ const ROUTES = { getRoute: (contentType: string) => `referral/${contentType}` as const, }, PROCESS_MONEY_REQUEST_HOLD: 'hold-request-educational', + ONBOARDING_WELCOME: 'onboarding/welcome', + ONBOARDING_PURPOSE: 'onboarding/purpose', + WELCOME: 'welcome', + PURPOSE: 'purpose', } as const; /** diff --git a/src/SCREENS.ts b/src/SCREENS.ts index e2f0e9745561..02f2536cb602 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -119,6 +119,9 @@ const SCREENS = { REFERRAL: 'Referral', PROCESS_MONEY_REQUEST_HOLD: 'ProcessMoneyRequestHold', }, + ONBOARDING_MODAL: { + ONBOARDING: 'Onboarding', + }, SIGN_IN_WITH_APPLE_DESKTOP: 'AppleSignInDesktop', SIGN_IN_WITH_GOOGLE_DESKTOP: 'GoogleSignInDesktop', DESKTOP_SIGN_IN_REDIRECT: 'DesktopSignInRedirect', @@ -230,6 +233,11 @@ const SCREENS = { EDIT_CURRENCY: 'SplitDetails_Edit_Currency', }, + ONBOARDING: { + WELCOME: 'Onboarding_Welcome', + PURPOSE: 'Onboarding_Purpose', + }, + I_KNOW_A_TEACHER: 'I_Know_A_Teacher', INTRO_SCHOOL_PRINCIPAL: 'Intro_School_Principal', I_AM_A_TEACHER: 'I_Am_A_Teacher', diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index c1f4df8d4c99..c5fe44677476 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -191,7 +191,7 @@ function BaseModal( backdropColor={theme.overlay} backdropOpacity={!shouldUseCustomBackdrop && hideBackdrop ? 0 : variables.overlayOpacity} backdropTransitionOutTiming={0} - hasBackdrop={fullscreen} + hasBackdrop={false} coverScreen={fullscreen} style={modalStyle} deviceHeight={windowHeight} diff --git a/src/components/PurposeForUsingExpensifyModal.tsx b/src/components/PurposeForUsingExpensifyModal.tsx index e65646aeac84..175bb4acb698 100644 --- a/src/components/PurposeForUsingExpensifyModal.tsx +++ b/src/components/PurposeForUsingExpensifyModal.tsx @@ -23,6 +23,7 @@ import type {MenuItemProps} from './MenuItem'; import MenuItemList from './MenuItemList'; import Modal from './Modal'; import Text from './Text'; +import Navigation from '@libs/Navigation/Navigation'; // This is not translated because it is a message coming from concierge, which only supports english const messageCopy = { @@ -92,7 +93,7 @@ function PurposeForUsingExpensifyModal({isLoadingApp = false}: PurposeForUsingEx const styles = useThemeStyles(); const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); const navigation = useNavigation(); - const [isModalOpen, setIsModalOpen] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(true); const theme = useTheme(); useEffect(() => { @@ -110,11 +111,13 @@ function PurposeForUsingExpensifyModal({isLoadingApp = false}: PurposeForUsingEx const closeModal = useCallback(() => { Report.dismissEngagementModal(); setIsModalOpen(false); + Navigation.goBack(); }, []); const completeModalAndClose = useCallback((message: string, choice: ValueOf) => { Report.completeEngagementModal(message, choice); setIsModalOpen(false); + Navigation.goBack(); Report.navigateToConciergeChat(); }, []); diff --git a/src/components/WorkspaceSwitcherButton.tsx b/src/components/WorkspaceSwitcherButton.tsx index b7485fbab7a8..03e91b1f71ad 100644 --- a/src/components/WorkspaceSwitcherButton.tsx +++ b/src/components/WorkspaceSwitcherButton.tsx @@ -44,7 +44,7 @@ function WorkspaceSwitcherButton({activeWorkspaceID, policy}: WorkspaceSwitcherB accessible onPress={() => interceptAnonymousUser(() => { - Navigation.navigate(ROUTES.WORKSPACE_SWITCHER); + Navigation.navigate(ROUTES.WELCOME); }) } > diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 00c96d436496..4f3775abb1a3 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -41,6 +41,7 @@ import CentralPaneNavigator from './Navigators/CentralPaneNavigator'; import FullScreenNavigator from './Navigators/FullScreenNavigator'; import LeftModalNavigator from './Navigators/LeftModalNavigator'; import RightModalNavigator from './Navigators/RightModalNavigator'; +import OnboardingModalNavigator from './Navigators/OnboardingModalNavigator'; type AuthScreensProps = { /** Session of currently logged in user */ @@ -256,14 +257,22 @@ function AuthScreens({session, lastOpenedPublicRoomID, isUsingMemoryOnlyKeys = f unsubscribeChatShortcut(); Session.cleanupSession(); }; - + // Rule disabled because this effect is only for component did mount & will component unmount lifecycle event // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - + return ( + require('../../../pages/ProcessMoneyRequestHoldPage').default as React.ComponentType, }); +const OnboardingModalStackNavigator = createModalStackNavigator({ + [SCREENS.ONBOARDING.WELCOME]: () => require('../../../../src/components/PurposeForUsingExpensifyModal').default as React.ComponentType, + [SCREENS.ONBOARDING.PURPOSE]: () => require('../../../../src/components/PurposeForUsingExpensifyModal').default as React.ComponentType, +}) + export { AccountSettingsModalStackNavigator, AddPersonalBankAccountModalStackNavigator, @@ -317,4 +322,5 @@ export { TaskModalStackNavigator, WalletStatementStackNavigator, ProcessMoneyRequestHoldStackNavigator, + OnboardingModalStackNavigator, }; diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx index 7f7e86b3dafb..ef4df063c8a8 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx @@ -86,7 +86,6 @@ function BottomTabBar() { - ); } diff --git a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts index c3a69bbd7ccf..26e5829d82e5 100644 --- a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts +++ b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts @@ -39,6 +39,15 @@ const getRootNavigatorScreenOptions: GetRootNavigatorScreenOptions = (isSmallScr right: 0, }, }, + onboardingModalNavigator: { + headerShown: false, + detachPreviousScreen: false, + animationEnabled: false, + presentation: 'transparentModal', + cardStyle: { + backgroundColor: 'transparent', + }, + }, leftModalNavigator: { ...commonScreenOptions, cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, false, props, SLIDE_LEFT_OUTPUT_RANGE_MULTIPLIER), diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 20c426a74c71..d3c13eda4895 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -118,6 +118,8 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N if (!state) { return; } + Log.info("STATE"); + Log.info(JSON.stringify(state)); const activeWorkspaceID = getPolicyIDFromState(state as NavigationState); // Performance optimization to avoid context consumers to delay first render setTimeout(() => { diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 6f96642953af..2c640b05d9d7 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -80,6 +80,30 @@ const config: LinkingOptions['config'] = { }, }, }, + [NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR]: { + screens: { + // [SCREENS.ONBOARDING_MODAL.ONBOARDING]: { + // screens: { + // [SCREENS.ONBOARDING.WELCOME]: { + // path: ROUTES.ONBOARDING_WELCOME, + // exact: true, + // }, + // [SCREENS.ONBOARDING.PURPOSE]: { + // path: ROUTES.ONBOARDING_PURPOSE, + // exact: true, + // }, + // } + // }, + [SCREENS.ONBOARDING.WELCOME]: { + path: ROUTES.WELCOME, + exact: true, + }, + [SCREENS.ONBOARDING.PURPOSE]: { + path: ROUTES.PURPOSE, + exact: true, + } + } + }, [NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: { screens: { [SCREENS.RIGHT_MODAL.SETTINGS]: { diff --git a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts index 06e58282da70..2b702453955c 100644 --- a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts +++ b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts @@ -7,6 +7,7 @@ import type {BottomTabName, CentralPaneName, FullScreenName, NavigationPartialRo import {extractPolicyIDFromPath, getPathWithoutPolicyID} from '@libs/PolicyUtils'; import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; +import Log from '@libs/Log'; import CENTRAL_PANE_TO_RHP_MAPPING from './CENTRAL_PANE_TO_RHP_MAPPING'; import config from './config'; import FULL_SCREEN_TO_RHP_MAPPING from './FULL_SCREEN_TO_RHP_MAPPING'; @@ -288,6 +289,8 @@ const getAdaptedStateFromPath: GetAdaptedStateFromPath = (path, options) => { const policyID = isAnonymous ? undefined : extractPolicyIDFromPath(path); const state = getStateFromPath(pathWithoutPolicyID, options) as PartialState>; + Log.info("STATE FROM PATH"); + Log.info(JSON.stringify(state)); replacePathInNestedState(state, path); if (state === undefined) { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index d544c2ffa3b6..c5022bf1d278 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -402,6 +402,12 @@ type FullScreenNavigatorParamList = { [SCREENS.SETTINGS_CENTRAL_PANE]: NavigatorScreenParams; }; +type OnboardingModalNavigatorParamList = { + [SCREENS.ONBOARDING_MODAL.ONBOARDING]: undefined; + [SCREENS.ONBOARDING.WELCOME]: undefined; + [SCREENS.ONBOARDING.PURPOSE]: undefined; +}; + type BottomTabNavigatorParamList = { [SCREENS.HOME]: undefined; [SCREENS.ALL_SETTINGS]: undefined; @@ -461,6 +467,7 @@ type AuthScreensParamList = { [NAVIGATORS.LEFT_MODAL_NAVIGATOR]: NavigatorScreenParams; [NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: NavigatorScreenParams; [NAVIGATORS.FULL_SCREEN_NAVIGATOR]: NavigatorScreenParams; + [NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR]: NavigatorScreenParams; [SCREENS.DESKTOP_SIGN_IN_REDIRECT]: undefined; }; @@ -494,6 +501,7 @@ export type { BottomTabNavigatorParamList, LeftModalNavigatorParamList, RightModalNavigatorParamList, + OnboardingModalNavigatorParamList, PublicScreensParamList, MoneyRequestNavigatorParamList, SplitDetailsNavigatorParamList, From cf53a8ab1e05158afeac26c335fbb9e2a44ac32a Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Fri, 9 Feb 2024 13:48:33 +0100 Subject: [PATCH 005/695] Add onboarding navigator --- .../Navigators/OnboardingModalNavigator.tsx | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx diff --git a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx new file mode 100644 index 000000000000..8d6a7a931f85 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx @@ -0,0 +1,45 @@ +import React, { useMemo } from 'react'; +import {View} from 'react-native'; +import NoDropZone from "@components/DragAndDrop/NoDropZone"; +import type { OnboardingModalNavigatorParamList } from "@libs/Navigation/types"; +import { createStackNavigator } from "@react-navigation/stack"; +import SCREENS from "@src/SCREENS"; +import ModalNavigatorScreenOptions from '@libs/Navigation/AppNavigator/ModalNavigatorScreenOptions'; +import useThemeStyles from '@hooks/useThemeStyles'; +// import * as ModalStackNavigators from '@libs/Navigation/AppNavigator/ModalStackNavigators'; +import PurposeForUsingExpensifyModal from '@components/PurposeForUsingExpensifyModal'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import Overlay from './Overlay'; + +const Stack = createStackNavigator(); + +function OnboardingModalNavigator() { + + const styles = useThemeStyles(); + const screenOptions = useMemo(() => ModalNavigatorScreenOptions(styles), [styles]); + const {isSmallScreenWidth} = useWindowDimensions(); + + return + + {!isSmallScreenWidth && {}}/>} + + {/* */} + + + + + +} + +OnboardingModalNavigator.displayName = 'OnboardingModalNavigator'; + +export default OnboardingModalNavigator; \ No newline at end of file From 30b74113ebdf8935ad38e7d14652cc234a5b9449 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Fri, 9 Feb 2024 15:28:24 +0100 Subject: [PATCH 006/695] modify getAdaptedState to handle onboarding navigator --- .../linkingConfig/getAdaptedStateFromPath.ts | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts index 2b702453955c..ea6c94f1b698 100644 --- a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts +++ b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts @@ -2,12 +2,12 @@ import type {NavigationState, PartialState} from '@react-navigation/native'; import {getStateFromPath} from '@react-navigation/native'; import {isAnonymousUser} from '@libs/actions/Session'; import getIsSmallScreenWidth from '@libs/getIsSmallScreenWidth'; +import Log from '@libs/Log'; import getTopmostNestedRHPRoute from '@libs/Navigation/getTopmostNestedRHPRoute'; import type {BottomTabName, CentralPaneName, FullScreenName, NavigationPartialRoute, RootStackParamList} from '@libs/Navigation/types'; import {extractPolicyIDFromPath, getPathWithoutPolicyID} from '@libs/PolicyUtils'; import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; -import Log from '@libs/Log'; import CENTRAL_PANE_TO_RHP_MAPPING from './CENTRAL_PANE_TO_RHP_MAPPING'; import config from './config'; import FULL_SCREEN_TO_RHP_MAPPING from './FULL_SCREEN_TO_RHP_MAPPING'; @@ -145,6 +145,8 @@ function getAdaptedState(state: PartialState const fullScreenNavigator = state.routes.find((route) => route.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR); const rhpNavigator = state.routes.find((route) => route.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR); const lhpNavigator = state.routes.find((route) => route.name === NAVIGATORS.LEFT_MODAL_NAVIGATOR); + const onboardingModalNavigator = state.routes.find((route) => route.name === NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR); + if (rhpNavigator) { // Routes // - matching bottom tab @@ -177,13 +179,13 @@ function getAdaptedState(state: PartialState metainfo, }; } - if (lhpNavigator) { + if (lhpNavigator ?? onboardingModalNavigator) { // Routes // - default bottom tab // - default central pane on desktop layout - // - found lhp + // - found lhp / onboardingModalNavigator - // Currently there is only the search and workspace switcher in LHP both can have any central pane under the overlay. + // There is no screen in these navigators that would have mandatory central pane, bottom tab or fullscreen navigator. metainfo.isCentralPaneAndBottomTabMandatory = false; metainfo.isFullScreenNavigatorMandatory = false; const routes = []; @@ -202,7 +204,15 @@ function getAdaptedState(state: PartialState }), ); } - routes.push(lhpNavigator); + + // Separate ifs are necessary for typescript to see that we are not pushing unedinfed to the array. + if (lhpNavigator) { + routes.push(lhpNavigator); + } + + if (onboardingModalNavigator) { + routes.push(onboardingModalNavigator); + } return { adaptedState: getRoutesWithIndex(routes), @@ -266,6 +276,10 @@ function getAdaptedState(state: PartialState const matchingCentralPaneRoute = getMatchingCentralPaneRouteForState(state); if (matchingCentralPaneRoute) { routes.push(createCentralPaneNavigator(matchingCentralPaneRoute)); + } else { + // If there is no matching central pane, we want to add the default one. + metainfo.isCentralPaneAndBottomTabMandatory = false; + routes.push(createCentralPaneNavigator({name: SCREENS.REPORT})); } return { @@ -289,14 +303,16 @@ const getAdaptedStateFromPath: GetAdaptedStateFromPath = (path, options) => { const policyID = isAnonymous ? undefined : extractPolicyIDFromPath(path); const state = getStateFromPath(pathWithoutPolicyID, options) as PartialState>; - Log.info("STATE FROM PATH"); + Log.info('STATE FROM PATH'); Log.info(JSON.stringify(state)); replacePathInNestedState(state, path); if (state === undefined) { throw new Error('Unable to parse path'); } - return getAdaptedState(state, policyID); + + const tmp = getAdaptedState(state, policyID); + return tmp; }; export default getAdaptedStateFromPath; From 1895d1841f1da4c1ea2ff498493bf3642e80548f Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Mon, 12 Feb 2024 11:30:27 +0100 Subject: [PATCH 007/695] Adjust designs for medium sized screens --- src/CONST.ts | 1 + src/ROUTES.ts | 4 +- src/SCREENS.ts | 2 +- src/components/Modal/BaseModal.tsx | 10 +++- src/components/Modal/types.ts | 3 + src/hooks/useOnboardingLayout.ts | 16 ++++++ src/hooks/useWindowDimensions/index.ts | 3 + .../Navigation/AppNavigator/AuthScreens.tsx | 2 +- .../AppNavigator/ModalStackNavigators.tsx | 4 +- .../Navigators/OnboardingModalNavigator.tsx | 57 ++++++++----------- .../AppNavigator/Navigators/Overlay.tsx | 18 +++++- src/libs/Navigation/NavigationRoot.tsx | 6 +- src/libs/Navigation/linkingConfig/config.ts | 22 ++----- src/libs/Navigation/types.ts | 2 +- src/pages/OnboardingPersonalDetails.tsx | 46 +++++++++++++++ src/pages/OnboardingPurpose.tsx | 48 ++++++++++++++++ src/styles/index.ts | 12 ++++ .../utils/generators/ModalStyleUtils.ts | 35 +++++++++++- 18 files changed, 225 insertions(+), 66 deletions(-) create mode 100644 src/hooks/useOnboardingLayout.ts create mode 100644 src/pages/OnboardingPersonalDetails.tsx create mode 100644 src/pages/OnboardingPurpose.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 6c726cde12f7..f9f4ad1c562f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -715,6 +715,7 @@ const CONST = { BOTTOM_DOCKED: 'bottom_docked', POPOVER: 'popover', RIGHT_DOCKED: 'right_docked', + ONBOARDING: 'onboarding', }, ANCHOR_ORIGIN_VERTICAL: { TOP: 'top', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index c1ca60e35af0..108bd21a8251 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -491,10 +491,8 @@ const ROUTES = { getRoute: (contentType: string) => `referral/${contentType}` as const, }, PROCESS_MONEY_REQUEST_HOLD: 'hold-request-educational', - ONBOARDING_WELCOME: 'onboarding/welcome', + ONBOARDING_PERSONAL_DETAILS: 'onboarding/personal-details', ONBOARDING_PURPOSE: 'onboarding/purpose', - WELCOME: 'welcome', - PURPOSE: 'purpose', } as const; /** diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 02f2536cb602..6f1cf39207c8 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -234,7 +234,7 @@ const SCREENS = { }, ONBOARDING: { - WELCOME: 'Onboarding_Welcome', + PERSONAL_DETAILS: 'Onboarding_Personal_Details', PURPOSE: 'Onboarding_Purpose', }, diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index c5fe44677476..d950478cae6b 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -3,6 +3,7 @@ import {View} from 'react-native'; import ReactNativeModal from 'react-native-modal'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; import useKeyboardState from '@hooks/useKeyboardState'; +import useOnboardingLayout from '@hooks/useOnboardingLayout'; import usePrevious from '@hooks/usePrevious'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -22,6 +23,7 @@ function BaseModal( isVisible, onClose, shouldSetModalVisibility = true, + shouldForceHideBackdrop = false, onModalHide = () => {}, type, popoverAnchorPosition = {}, @@ -48,6 +50,7 @@ function BaseModal( const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {windowWidth, windowHeight, isSmallScreenWidth} = useWindowDimensions(); + const {shouldUseNarrowLayout} = useOnboardingLayout(); const keyboardStateContextValue = useKeyboardState(); const safeAreaInsets = useSafeAreaInsets(); @@ -147,8 +150,9 @@ function BaseModal( popoverAnchorPosition, innerContainerStyle, outerStyle, + shouldUseNarrowLayout, ), - [StyleUtils, type, windowWidth, windowHeight, isSmallScreenWidth, popoverAnchorPosition, innerContainerStyle, outerStyle], + [StyleUtils, type, windowWidth, windowHeight, isSmallScreenWidth, popoverAnchorPosition, innerContainerStyle, outerStyle, shouldUseNarrowLayout], ); const { @@ -189,9 +193,9 @@ function BaseModal( swipeDirection={swipeDirection} isVisible={isVisible} backdropColor={theme.overlay} - backdropOpacity={!shouldUseCustomBackdrop && hideBackdrop ? 0 : variables.overlayOpacity} + backdropOpacity={(!shouldUseCustomBackdrop && hideBackdrop) || shouldForceHideBackdrop ? 0 : variables.overlayOpacity} backdropTransitionOutTiming={0} - hasBackdrop={false} + hasBackdrop={fullscreen} coverScreen={fullscreen} style={modalStyle} deviceHeight={windowHeight} diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts index a0cdb737d448..f92532f6e27f 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -20,6 +20,9 @@ type BaseModalProps = Partial & { /** Should we announce the Modal visibility changes? */ shouldSetModalVisibility?: boolean; + /** Should we hide backdrop no matter what value is set in modal styles */ + shouldForceHideBackdrop?: boolean; + /** Callback method fired when the user requests to close the modal */ onClose: () => void; diff --git a/src/hooks/useOnboardingLayout.ts b/src/hooks/useOnboardingLayout.ts new file mode 100644 index 000000000000..99c47643d2b5 --- /dev/null +++ b/src/hooks/useOnboardingLayout.ts @@ -0,0 +1,16 @@ +// eslint-disable-next-line no-restricted-imports +import {useWindowDimensions} from 'react-native'; +import variables from '@styles/variables'; + +type OnboardingLayout = { + shouldUseNarrowLayout: boolean; +}; + +/** + * Onboarding layout for medium screen width is narrowed similarly as on web/desktop. + */ +export default function useOnboardingLayout(): OnboardingLayout { + const {width: windowWidth} = useWindowDimensions(); + + return {shouldUseNarrowLayout: windowWidth > variables.mobileResponsiveWidthBreakpoint}; +} diff --git a/src/hooks/useWindowDimensions/index.ts b/src/hooks/useWindowDimensions/index.ts index b0a29e9f901b..fdaa96d747c5 100644 --- a/src/hooks/useWindowDimensions/index.ts +++ b/src/hooks/useWindowDimensions/index.ts @@ -1,5 +1,6 @@ // eslint-disable-next-line no-restricted-imports import {Dimensions, useWindowDimensions} from 'react-native'; +import Log from '@libs/Log'; import variables from '@styles/variables'; import type WindowDimensions from './types'; @@ -15,6 +16,8 @@ export default function (): WindowDimensions { const isMediumScreenWidth = windowWidth > variables.mobileResponsiveWidthBreakpoint && windowWidth <= variables.tabletResponsiveWidthBreakpoint; const isLargeScreenWidth = windowWidth > variables.tabletResponsiveWidthBreakpoint; + Log.info(`WINDOW WIDTH ${windowWidth}`); + return { windowWidth, windowHeight, diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 4f3775abb1a3..cc17fcf07f4f 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -40,8 +40,8 @@ import BottomTabNavigator from './Navigators/BottomTabNavigator'; import CentralPaneNavigator from './Navigators/CentralPaneNavigator'; import FullScreenNavigator from './Navigators/FullScreenNavigator'; import LeftModalNavigator from './Navigators/LeftModalNavigator'; -import RightModalNavigator from './Navigators/RightModalNavigator'; import OnboardingModalNavigator from './Navigators/OnboardingModalNavigator'; +import RightModalNavigator from './Navigators/RightModalNavigator'; type AuthScreensProps = { /** Session of currently logged in user */ diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index b3ea3d28be04..4eb282595f1c 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -289,9 +289,9 @@ const ProcessMoneyRequestHoldStackNavigator = createModalStackNavigator({ }); const OnboardingModalStackNavigator = createModalStackNavigator({ - [SCREENS.ONBOARDING.WELCOME]: () => require('../../../../src/components/PurposeForUsingExpensifyModal').default as React.ComponentType, + [SCREENS.ONBOARDING.PERSONAL_DETAILS]: () => require('../../../../src/components/PurposeForUsingExpensifyModal').default as React.ComponentType, [SCREENS.ONBOARDING.PURPOSE]: () => require('../../../../src/components/PurposeForUsingExpensifyModal').default as React.ComponentType, -}) +}); export { AccountSettingsModalStackNavigator, diff --git a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx index 8d6a7a931f85..9b40f5748f53 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx @@ -1,45 +1,38 @@ -import React, { useMemo } from 'react'; +import {createStackNavigator} from '@react-navigation/stack'; +import React, {useMemo} from 'react'; import {View} from 'react-native'; -import NoDropZone from "@components/DragAndDrop/NoDropZone"; -import type { OnboardingModalNavigatorParamList } from "@libs/Navigation/types"; -import { createStackNavigator } from "@react-navigation/stack"; -import SCREENS from "@src/SCREENS"; -import ModalNavigatorScreenOptions from '@libs/Navigation/AppNavigator/ModalNavigatorScreenOptions'; +import NoDropZone from '@components/DragAndDrop/NoDropZone'; import useThemeStyles from '@hooks/useThemeStyles'; -// import * as ModalStackNavigators from '@libs/Navigation/AppNavigator/ModalStackNavigators'; -import PurposeForUsingExpensifyModal from '@components/PurposeForUsingExpensifyModal'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import Overlay from './Overlay'; +import ModalNavigatorScreenOptions from '@libs/Navigation/AppNavigator/ModalNavigatorScreenOptions'; +import type {OnboardingModalNavigatorParamList} from '@libs/Navigation/types'; +import OnboardingPersonalDetails from '@pages/OnboardingPersonalDetails'; +import OnboardingPurpose from '@pages/OnboardingPurpose'; +import SCREENS from '@src/SCREENS'; const Stack = createStackNavigator(); function OnboardingModalNavigator() { - const styles = useThemeStyles(); const screenOptions = useMemo(() => ModalNavigatorScreenOptions(styles), [styles]); - const {isSmallScreenWidth} = useWindowDimensions(); - return - - {!isSmallScreenWidth && {}}/>} - - {/* */} - - - - - + return ( + + + + + + + + + ); } OnboardingModalNavigator.displayName = 'OnboardingModalNavigator'; -export default OnboardingModalNavigator; \ No newline at end of file +export default OnboardingModalNavigator; diff --git a/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx b/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx index 5462b6c0ce4e..6917df30ce26 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx @@ -1,14 +1,16 @@ import {useCardAnimation} from '@react-navigation/stack'; -import React from 'react'; +import React, {useMemo} from 'react'; import {Animated, View} from 'react-native'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import useLocalize from '@hooks/useLocalize'; +import useOnboardingLayout from '@hooks/useOnboardingLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import getOperatingSystem from '@libs/getOperatingSystem'; import CONST from '@src/CONST'; type OverlayProps = { /* Callback to close the modal */ - onPress: () => void; + onPress?: () => void; /* Returns whether a modal is displayed on the left side of the screen. By default, the modal is displayed on the right */ isModalOnTheLeft?: boolean; @@ -18,9 +20,19 @@ function Overlay({onPress, isModalOnTheLeft = false}: OverlayProps) { const styles = useThemeStyles(); const {current} = useCardAnimation(); const {translate} = useLocalize(); + const {shouldUseNarrowLayout} = useOnboardingLayout(); + + // non-native styling uses fixed positioning not supported on native platforms + const shouldUseNativeStyles = useMemo(() => { + const os = getOperatingSystem(); + if ((os === CONST.OS.ANDROID || os === CONST.OS.IOS || os === CONST.OS.NATIVE) && shouldUseNarrowLayout) { + return true; + } + return false; + }, [shouldUseNarrowLayout]); return ( - + {/* In the latest Electron version buttons can't be both clickable and draggable. That's why we added this workaround. Because of two Pressable components on the desktop app diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index d3c13eda4895..76e9cfa9d650 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -41,6 +41,8 @@ function parseAndLogRoute(state: NavigationState) { return; } + Log.info(`Current state ${JSON.stringify(state)}`); + const currentPath = customGetPathFromState(state, linkingConfig.config); const focusedRoute = findFocusedRoute(state); @@ -74,6 +76,8 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N return undefined; } + return undefined; + const path = initialUrl ? getPathFromURL(initialUrl) : null; // For non-nullable paths we don't want to set initial state @@ -118,7 +122,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N if (!state) { return; } - Log.info("STATE"); + Log.info('STATE'); Log.info(JSON.stringify(state)); const activeWorkspaceID = getPolicyIDFromState(state as NavigationState); // Performance optimization to avoid context consumers to delay first render diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 2c640b05d9d7..094cd20db93f 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -82,27 +82,15 @@ const config: LinkingOptions['config'] = { }, [NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR]: { screens: { - // [SCREENS.ONBOARDING_MODAL.ONBOARDING]: { - // screens: { - // [SCREENS.ONBOARDING.WELCOME]: { - // path: ROUTES.ONBOARDING_WELCOME, - // exact: true, - // }, - // [SCREENS.ONBOARDING.PURPOSE]: { - // path: ROUTES.ONBOARDING_PURPOSE, - // exact: true, - // }, - // } - // }, - [SCREENS.ONBOARDING.WELCOME]: { - path: ROUTES.WELCOME, + [SCREENS.ONBOARDING.PERSONAL_DETAILS]: { + path: ROUTES.ONBOARDING_PERSONAL_DETAILS, exact: true, }, [SCREENS.ONBOARDING.PURPOSE]: { - path: ROUTES.PURPOSE, + path: ROUTES.ONBOARDING_PURPOSE, exact: true, - } - } + }, + }, }, [NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: { screens: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index c5022bf1d278..5f018ccb6561 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -404,7 +404,7 @@ type FullScreenNavigatorParamList = { type OnboardingModalNavigatorParamList = { [SCREENS.ONBOARDING_MODAL.ONBOARDING]: undefined; - [SCREENS.ONBOARDING.WELCOME]: undefined; + [SCREENS.ONBOARDING.PERSONAL_DETAILS]: undefined; [SCREENS.ONBOARDING.PURPOSE]: undefined; }; diff --git a/src/pages/OnboardingPersonalDetails.tsx b/src/pages/OnboardingPersonalDetails.tsx new file mode 100644 index 000000000000..370c8d10cbd1 --- /dev/null +++ b/src/pages/OnboardingPersonalDetails.tsx @@ -0,0 +1,46 @@ +import React, {useCallback, useState} from 'react'; +import {View} from 'react-native'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Modal from '@components/Modal'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import Navigation from '@libs/Navigation/Navigation'; +import * as Report from '@userActions/Report'; +import CONST from '@src/CONST'; + +function OnboardingPersonalDetails() { + const styles = useThemeStyles(); + const {windowHeight} = useWindowDimensions(); + const [isModalOpen, setIsModalOpen] = useState(true); + const theme = useTheme(); + + const closeModal = useCallback(() => { + Report.dismissEngagementModal(); + Navigation.goBack(); + setIsModalOpen(false); + }, []); + + return ( + + + + + + ); +} + +OnboardingPersonalDetails.displayName = 'OnboardingPersonalDetails'; +export default OnboardingPersonalDetails; diff --git a/src/pages/OnboardingPurpose.tsx b/src/pages/OnboardingPurpose.tsx new file mode 100644 index 000000000000..9ad9c0d74334 --- /dev/null +++ b/src/pages/OnboardingPurpose.tsx @@ -0,0 +1,48 @@ +import React, {useCallback, useState} from 'react'; +import {View} from 'react-native'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Modal from '@components/Modal'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import Navigation from '@libs/Navigation/Navigation'; +import * as Report from '@userActions/Report'; +import CONST from '@src/CONST'; + +function OnboardingPurpose() { + const styles = useThemeStyles(); + const {windowHeight} = useWindowDimensions(); + const [isModalOpen, setIsModalOpen] = useState(true); + const theme = useTheme(); + + const closeModal = useCallback(() => { + Report.dismissEngagementModal(); + Navigation.goBack(); + setIsModalOpen(false); + }, []); + + return ( + + + + + + ); +} + +OnboardingPurpose.displayName = 'OnboardingPurpose'; +export default OnboardingPurpose; diff --git a/src/styles/index.ts b/src/styles/index.ts index 2e16f1dbd7af..93c7f5d1e1b0 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -1711,6 +1711,18 @@ const styles = (theme: ThemeColors) => }), } satisfies ViewStyle), + nativeOverlayStyles: (current: OverlayStylesParams) => + ({ + backgroundColor: theme.overlay, + width: '100%', + height: '100%', + opacity: current.progress.interpolate({ + inputRange: [0, 1], + outputRange: [0, variables.overlayOpacity], + extrapolate: 'clamp', + }), + } satisfies ViewStyle), + appContent: { backgroundColor: theme.appBG, overflow: 'hidden', diff --git a/src/styles/utils/generators/ModalStyleUtils.ts b/src/styles/utils/generators/ModalStyleUtils.ts index 335ef8382941..da93b335709f 100644 --- a/src/styles/utils/generators/ModalStyleUtils.ts +++ b/src/styles/utils/generators/ModalStyleUtils.ts @@ -15,6 +15,10 @@ function getCenteredModalStyles(styles: ThemeStyles, windowWidth: number, isSmal }; } +function getOnboardingModalStyles(styles: ThemeStyles, windowWidth: number, shouldUseNarrowLayout: boolean, isFullScreenWhenSmall = false): ViewStyle { + return shouldUseNarrowLayout ? {width: 500, height: 712} : getCenteredModalStyles(styles, windowWidth, !shouldUseNarrowLayout, isFullScreenWhenSmall); +} + type WindowDimensions = { windowWidth: number; windowHeight: number; @@ -41,11 +45,12 @@ type GetModalStylesStyleUtil = { popoverAnchorPosition?: ViewStyle, innerContainerStyle?: ViewStyle, outerStyle?: ViewStyle, + shouldUseNarrowLayout?: boolean, ) => GetModalStyles; }; const createModalStyleUtils: StyleUtilGenerator = ({theme, styles}) => ({ - getModalStyles: (type, windowDimensions, popoverAnchorPosition = {}, innerContainerStyle = {}, outerStyle = {}): GetModalStyles => { + getModalStyles: (type, windowDimensions, popoverAnchorPosition = {}, innerContainerStyle = {}, outerStyle = {}, shouldUseNarrowLayout = false): GetModalStyles => { const {isSmallScreenWidth, windowWidth} = windowDimensions; let modalStyle: GetModalStyles['modalStyle'] = { @@ -162,7 +167,33 @@ const createModalStyleUtils: StyleUtilGenerator = ({the shouldAddTopSafeAreaMargin = false; shouldAddBottomSafeAreaMargin = false; shouldAddTopSafeAreaPadding = false; - shouldAddBottomSafeAreaPadding = false; + break; + case CONST.MODAL.MODAL_TYPE.ONBOARDING: + // Handles how onboarding modals should be rendered on different screens + modalStyle = { + ...modalStyle, + ...{ + alignItems: 'center', + }, + }; + modalContainerStyle = { + boxShadow: '0px 0px 5px 5px rgba(0, 0, 0, 0.1)', + borderWidth: 0, + flex: !shouldUseNarrowLayout ? 1 : undefined, + marginTop: 0, + marginBottom: 0, + borderRadius: !shouldUseNarrowLayout ? 0 : 16, + overflow: 'hidden', + ...getOnboardingModalStyles(styles, windowWidth, shouldUseNarrowLayout, true), + }; + + // Allow this modal to be dismissed with a swipe down or swipe right + // swipeDirection = ['down', 'right']; + animationIn = 'fadeIn'; + animationOut = 'fadeOut'; + shouldAddTopSafeAreaMargin = false; + shouldAddBottomSafeAreaMargin = false; + shouldAddTopSafeAreaPadding = false; break; case CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED: modalStyle = { From cc0d59f016317092a80d93748a0c9aa2932903ec Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Mon, 12 Feb 2024 11:39:21 +0100 Subject: [PATCH 008/695] Refactor code --- src/components/PurposeForUsingExpensifyModal.tsx | 5 +---- src/components/WorkspaceSwitcherButton.tsx | 2 +- src/hooks/useWindowDimensions/index.ts | 3 --- src/libs/Navigation/NavigationRoot.tsx | 6 ------ 4 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/components/PurposeForUsingExpensifyModal.tsx b/src/components/PurposeForUsingExpensifyModal.tsx index 175bb4acb698..e65646aeac84 100644 --- a/src/components/PurposeForUsingExpensifyModal.tsx +++ b/src/components/PurposeForUsingExpensifyModal.tsx @@ -23,7 +23,6 @@ import type {MenuItemProps} from './MenuItem'; import MenuItemList from './MenuItemList'; import Modal from './Modal'; import Text from './Text'; -import Navigation from '@libs/Navigation/Navigation'; // This is not translated because it is a message coming from concierge, which only supports english const messageCopy = { @@ -93,7 +92,7 @@ function PurposeForUsingExpensifyModal({isLoadingApp = false}: PurposeForUsingEx const styles = useThemeStyles(); const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); const navigation = useNavigation(); - const [isModalOpen, setIsModalOpen] = useState(true); + const [isModalOpen, setIsModalOpen] = useState(false); const theme = useTheme(); useEffect(() => { @@ -111,13 +110,11 @@ function PurposeForUsingExpensifyModal({isLoadingApp = false}: PurposeForUsingEx const closeModal = useCallback(() => { Report.dismissEngagementModal(); setIsModalOpen(false); - Navigation.goBack(); }, []); const completeModalAndClose = useCallback((message: string, choice: ValueOf) => { Report.completeEngagementModal(message, choice); setIsModalOpen(false); - Navigation.goBack(); Report.navigateToConciergeChat(); }, []); diff --git a/src/components/WorkspaceSwitcherButton.tsx b/src/components/WorkspaceSwitcherButton.tsx index 03e91b1f71ad..b7485fbab7a8 100644 --- a/src/components/WorkspaceSwitcherButton.tsx +++ b/src/components/WorkspaceSwitcherButton.tsx @@ -44,7 +44,7 @@ function WorkspaceSwitcherButton({activeWorkspaceID, policy}: WorkspaceSwitcherB accessible onPress={() => interceptAnonymousUser(() => { - Navigation.navigate(ROUTES.WELCOME); + Navigation.navigate(ROUTES.WORKSPACE_SWITCHER); }) } > diff --git a/src/hooks/useWindowDimensions/index.ts b/src/hooks/useWindowDimensions/index.ts index fdaa96d747c5..b0a29e9f901b 100644 --- a/src/hooks/useWindowDimensions/index.ts +++ b/src/hooks/useWindowDimensions/index.ts @@ -1,6 +1,5 @@ // eslint-disable-next-line no-restricted-imports import {Dimensions, useWindowDimensions} from 'react-native'; -import Log from '@libs/Log'; import variables from '@styles/variables'; import type WindowDimensions from './types'; @@ -16,8 +15,6 @@ export default function (): WindowDimensions { const isMediumScreenWidth = windowWidth > variables.mobileResponsiveWidthBreakpoint && windowWidth <= variables.tabletResponsiveWidthBreakpoint; const isLargeScreenWidth = windowWidth > variables.tabletResponsiveWidthBreakpoint; - Log.info(`WINDOW WIDTH ${windowWidth}`); - return { windowWidth, windowHeight, diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 76e9cfa9d650..20c426a74c71 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -41,8 +41,6 @@ function parseAndLogRoute(state: NavigationState) { return; } - Log.info(`Current state ${JSON.stringify(state)}`); - const currentPath = customGetPathFromState(state, linkingConfig.config); const focusedRoute = findFocusedRoute(state); @@ -76,8 +74,6 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N return undefined; } - return undefined; - const path = initialUrl ? getPathFromURL(initialUrl) : null; // For non-nullable paths we don't want to set initial state @@ -122,8 +118,6 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N if (!state) { return; } - Log.info('STATE'); - Log.info(JSON.stringify(state)); const activeWorkspaceID = getPolicyIDFromState(state as NavigationState); // Performance optimization to avoid context consumers to delay first render setTimeout(() => { From 279a00e27c86d42ecd7b39c03b415fe6c8a04047 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Mon, 12 Feb 2024 11:57:35 +0100 Subject: [PATCH 009/695] Add top margin on small devices --- .../createCustomBottomTabNavigator/BottomTabBar.tsx | 1 - src/styles/utils/generators/ModalStyleUtils.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx index ef4df063c8a8..91c4c65cc0af 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx @@ -4,7 +4,6 @@ import {View} from 'react-native'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import {PressableWithFeedback} from '@components/Pressable'; -import PurposeForUsingExpensifyModal from '@components/PurposeForUsingExpensifyModal'; import Tooltip from '@components/Tooltip'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/styles/utils/generators/ModalStyleUtils.ts b/src/styles/utils/generators/ModalStyleUtils.ts index da93b335709f..33dbbe6a4b6b 100644 --- a/src/styles/utils/generators/ModalStyleUtils.ts +++ b/src/styles/utils/generators/ModalStyleUtils.ts @@ -191,7 +191,7 @@ const createModalStyleUtils: StyleUtilGenerator = ({the // swipeDirection = ['down', 'right']; animationIn = 'fadeIn'; animationOut = 'fadeOut'; - shouldAddTopSafeAreaMargin = false; + shouldAddTopSafeAreaMargin = !shouldUseNarrowLayout; shouldAddBottomSafeAreaMargin = false; shouldAddTopSafeAreaPadding = false; break; From ddd6c4eb18d69aff6039441cfeda8cb8753e2157 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Mon, 12 Feb 2024 16:01:25 +0100 Subject: [PATCH 010/695] add base welcome video modal --- .../OnboardingWelcomeVideoModal.tsx | 61 +++++++++++++++++++ src/languages/en.ts | 7 +++ src/styles/index.ts | 5 ++ src/styles/theme/themes/dark.ts | 1 + src/styles/theme/themes/light.ts | 1 + src/styles/theme/types.ts | 1 + 6 files changed, 76 insertions(+) create mode 100644 src/components/OnboardingWelcomeVideoModal.tsx diff --git a/src/components/OnboardingWelcomeVideoModal.tsx b/src/components/OnboardingWelcomeVideoModal.tsx new file mode 100644 index 000000000000..2dc374b8f78a --- /dev/null +++ b/src/components/OnboardingWelcomeVideoModal.tsx @@ -0,0 +1,61 @@ +import React, {useCallback, useState} from 'react'; +import {View} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import CONST from '@src/CONST'; +import Button from './Button'; +import Lottie from './Lottie'; +import LottieAnimations from './LottieAnimations'; +import Modal from './Modal'; +import Text from './Text'; + +function OnboardingWelcomeVideoModal() { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); + const [isModalOpen, setIsModalOpen] = useState(true); + + const closeModal = useCallback(() => { + setIsModalOpen(false); + }, []); + + return ( + + + + + + ; + + + {translate('onboarding.welcomeVideo.title')} + {translate('onboarding.welcomeVideo.description')} +