From b36939e831432a0d2282da6cceaacfd09af40805 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 3 Jan 2024 13:16:48 +0800 Subject: [PATCH 001/345] prevent receipt image blink --- src/components/MultiGestureCanvas/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index c5fd2632c22d..9ca3525fefd0 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -544,6 +544,8 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr }, {scale: totalScale.value}, ], + // Hide the image if the size is not ready yet + opacity: props.contentSize?.width ? 1 : 0, }; }); From f9165ba786852c2771e81d0edc89e42eea1356c6 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 3 Jan 2024 13:34:37 +0800 Subject: [PATCH 002/345] simplify the condition --- src/components/MultiGestureCanvas/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 9ca3525fefd0..603ed4a8bb12 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -545,7 +545,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr {scale: totalScale.value}, ], // Hide the image if the size is not ready yet - opacity: props.contentSize?.width ? 1 : 0, + opacity: Boolean(props.contentSize) ? 1 : 0, }; }); From ea18bcf6c5df3d7addb25e729d0be27cdf25cfc8 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 3 Jan 2024 13:42:21 +0800 Subject: [PATCH 003/345] fix lint --- src/components/MultiGestureCanvas/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index 603ed4a8bb12..97557b7af071 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -545,7 +545,7 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr {scale: totalScale.value}, ], // Hide the image if the size is not ready yet - opacity: Boolean(props.contentSize) ? 1 : 0, + opacity: props.contentSize ? 1 : 0, }; }); From e4d33ce7740ea08b0c9c0dbe8f24fdcb3b1a495b Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Tue, 9 Jan 2024 16:01:54 +0100 Subject: [PATCH 004/345] [TS migration] Migrate 'AddressForm.js' component to TypeScript --- .../{AddressForm.js => AddressForm.tsx} | 142 +++++++++++------- src/libs/Navigation/Navigation.ts | 2 +- src/types/onyx/Form.ts | 33 +++- src/types/onyx/index.ts | 3 +- 4 files changed, 123 insertions(+), 57 deletions(-) rename src/components/{AddressForm.js => AddressForm.tsx} (62%) diff --git a/src/components/AddressForm.js b/src/components/AddressForm.tsx similarity index 62% rename from src/components/AddressForm.js rename to src/components/AddressForm.tsx index 68d451e5c7c8..24df70fa1397 100644 --- a/src/components/AddressForm.js +++ b/src/components/AddressForm.tsx @@ -1,14 +1,14 @@ -import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import type {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; import React, {useCallback} from 'react'; import {View} from 'react-native'; -import _ from 'underscore'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import type * as Localize from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import * as ValidationUtils from '@libs/ValidationUtils'; import CONST from '@src/CONST'; +import type {OnyxFormKey} from '@src/ONYXKEYS'; +import type {AddressForm as AddressFormValues} from '@src/types/onyx'; import AddressSearch from './AddressSearch'; import CountrySelector from './CountrySelector'; import FormProvider from './Form/FormProvider'; @@ -16,94 +16,122 @@ import InputWrapper from './Form/InputWrapper'; import StatePicker from './StatePicker'; import TextInput from './TextInput'; -const propTypes = { +type AddressFormProps = { /** Address city field */ - city: PropTypes.string, + city?: string; /** Address country field */ - country: PropTypes.string, + country?: keyof typeof CONST.COUNTRY_ZIP_REGEX_DATA | ''; /** Address state field */ - state: PropTypes.string, + state?: keyof typeof COMMON_CONST.STATES | ''; /** Address street line 1 field */ - street1: PropTypes.string, + street1?: string; /** Address street line 2 field */ - street2: PropTypes.string, + street2?: string; /** Address zip code field */ - zip: PropTypes.string, + zip?: string; /** Callback which is executed when the user changes address, city or state */ - onAddressChanged: PropTypes.func, + onAddressChanged?: (data: string, key: string) => void; /** Callback which is executed when the user submits his address changes */ - onSubmit: PropTypes.func.isRequired, + onSubmit: () => void; /** Whether or not should the form data should be saved as draft */ - shouldSaveDraft: PropTypes.bool, + shouldSaveDraft?: boolean; /** Text displayed on the bottom submit button */ - submitButtonText: PropTypes.string, + submitButtonText?: string; /** A unique Onyx key identifying the form */ - formID: PropTypes.string.isRequired, + formID: OnyxFormKey; }; -const defaultProps = { - city: '', - country: '', - onAddressChanged: () => {}, - shouldSaveDraft: false, - state: '', - street1: '', - street2: '', - submitButtonText: '', - zip: '', +type ValidatorErrors = { + addressLine1?: string; + city?: string; + country?: string; + state?: string; + zipPostCode?: Localize.MaybePhraseKey; }; -function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldSaveDraft, state, street1, street2, submitButtonText, zip}) { +function AddressForm({ + city = '', + country = '', + formID, + onAddressChanged = () => {}, + onSubmit, + shouldSaveDraft = false, + state = '', + street1 = '', + street2 = '', + submitButtonText = '', + zip = '', +}: AddressFormProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const zipSampleFormat = lodashGet(CONST.COUNTRY_ZIP_REGEX_DATA, [country, 'samples'], ''); + + let zipSampleFormat = ''; + + if (country) { + const countryData = CONST.COUNTRY_ZIP_REGEX_DATA[country]; + if (countryData && 'samples' in countryData) { + zipSampleFormat = countryData.samples; + } + } + const zipFormat = translate('common.zipCodeExampleFormat', {zipSampleFormat}); const isUSAForm = country === CONST.COUNTRY.US; /** - * @param {Function} translate - translate function - * @param {Boolean} isUSAForm - selected country ISO code is US - * @param {Object} values - form input values - * @returns {Object} - An object containing the errors for each inputID + * @param translate - translate function + * @param isUSAForm - selected country ISO code is US + * @param values - form input values + * @returns - An object containing the errors for each inputID */ - const validator = useCallback((values) => { - const errors = {}; - const requiredFields = ['addressLine1', 'city', 'country', 'state']; + + const validator = useCallback((values: AddressFormValues): ValidatorErrors => { + const errors: ValidatorErrors = {}; + const requiredFields = ['addressLine1', 'city', 'country', 'state'] as const; // Check "State" dropdown is a valid state if selected Country is USA - if (values.country === CONST.COUNTRY.US && !COMMON_CONST.STATES[values.state]) { + if (values.country === CONST.COUNTRY.US && !values.state) { errors.state = 'common.error.fieldRequired'; } // Add "Field required" errors if any required field is empty - _.each(requiredFields, (fieldKey) => { - if (ValidationUtils.isRequiredFulfilled(values[fieldKey])) { + requiredFields.forEach((fieldKey) => { + const fieldValue = values[fieldKey] ?? ''; + if (ValidationUtils.isRequiredFulfilled(fieldValue)) { return; } + errors[fieldKey] = 'common.error.fieldRequired'; }); // If no country is selected, default value is an empty string and there's no related regex data so we default to an empty object - const countryRegexDetails = lodashGet(CONST.COUNTRY_ZIP_REGEX_DATA, values.country, {}); + const countryRegexDetails = values.country ? CONST.COUNTRY_ZIP_REGEX_DATA[values.country] : {}; // The postal code system might not exist for a country, so no regex either for them. - const countrySpecificZipRegex = lodashGet(countryRegexDetails, 'regex'); - const countryZipFormat = lodashGet(countryRegexDetails, 'samples'); + let countrySpecificZipRegex; + let countryZipFormat; + + if ('regex' in countryRegexDetails) { + countrySpecificZipRegex = countryRegexDetails.regex as RegExp; + } + + if ('samples' in countryRegexDetails) { + countryZipFormat = countryRegexDetails.samples as string; + } if (countrySpecificZipRegex) { if (!countrySpecificZipRegex.test(values.zipPostCode.trim().toUpperCase())) { if (ValidationUtils.isRequiredFulfilled(values.zipPostCode.trim())) { - errors.zipPostCode = ['privatePersonalDetails.error.incorrectZipFormat', {zipFormat: countryZipFormat}]; + errors.zipPostCode = ['privatePersonalDetails.error.incorrectZipFormat', {zipFormat: countryZipFormat ?? ''}]; } else { errors.zipPostCode = 'common.error.fieldRequired'; } @@ -116,6 +144,7 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS }, []); return ( + // @ts-expect-error TODO: Remove this once FormProvider (https://github.com/Expensify/App/issues/25109) is migrated to TypeScript. { + onValueChange={(data: string, key: string) => { onAddressChanged(data, key); // This enforces the country selector to use the country from address instead of the country from URL Navigation.setParams({country: undefined}); }} - defaultValue={street1 || ''} + defaultValue={street1} renamedInputKeys={{ street: 'addressLine1', street2: 'addressLine2', @@ -149,12 +179,13 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS ) : ( , routeKey: string) { +function setParams(params: Record, routeKey = '') { navigationRef.current?.dispatch({ ...CommonActions.setParams(params), source: routeKey, diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index ca8d6574adf5..8384e61fcdf0 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -1,3 +1,5 @@ +import type {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; +import type CONST from '@src/CONST'; import type * as OnyxCommon from './OnyxCommon'; type Form = { @@ -21,6 +23,35 @@ type DateOfBirthForm = Form & { dob?: string; }; +type AddressForm = Form & { + /** Address line 1 for delivery */ + addressLine1: string; + + /** Address line 2 for delivery */ + addressLine2: string; + + /** City for delivery */ + city: string; + + /** Country for delivery */ + country: keyof typeof CONST.COUNTRY_ZIP_REGEX_DATA | ''; + + /** First name for delivery */ + legalFirstName: string; + + /** Last name for delivery */ + legalLastName: string; + + /** Phone number for delivery */ + phoneNumber: string; + + /** State for delivery */ + state: keyof typeof COMMON_CONST.STATES | ''; + + /** Zip code for delivery */ + zipPostCode: string; +}; + export default Form; -export type {AddDebitCardForm, DateOfBirthForm}; +export type {AddDebitCardForm, DateOfBirthForm, AddressForm}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 8cba351d0f45..5407f3abf3ec 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -9,7 +9,7 @@ import type Credentials from './Credentials'; import type Currency from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; import type Download from './Download'; -import type {AddDebitCardForm, DateOfBirthForm} from './Form'; +import type {AddDebitCardForm, AddressForm, DateOfBirthForm} from './Form'; import type Form from './Form'; import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; import type {FundList} from './Fund'; @@ -70,6 +70,7 @@ export type { Account, AccountData, AddDebitCardForm, + AddressForm, BankAccount, BankAccountList, Beta, From b930ed4024e23c9c767f42f6d655dfa55d166645 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 18 Jan 2024 16:34:24 +0700 Subject: [PATCH 005/345] implement tooltip for emoji --- src/components/EmojiWithTooltip/index.js | 52 +++++++++++++++++++ .../BaseHTMLEngineProvider.tsx | 1 + .../HTMLRenderers/EmojiRenderer.js | 22 ++++++++ .../HTMLEngineProvider/HTMLRenderers/index.js | 2 + .../ReportActionItem/ReportPreview.js | 4 +- src/pages/home/report/ReportActionItem.js | 2 +- src/styles/index.ts | 5 ++ 7 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 src/components/EmojiWithTooltip/index.js create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.js diff --git a/src/components/EmojiWithTooltip/index.js b/src/components/EmojiWithTooltip/index.js new file mode 100644 index 000000000000..e82d2f307761 --- /dev/null +++ b/src/components/EmojiWithTooltip/index.js @@ -0,0 +1,52 @@ +import React from 'react'; +import {View} from 'react-native'; +import _ from 'underscore'; +import Text from '@components/Text'; +import Tooltip from '@components/Tooltip'; +import * as EmojiUtils from '@libs/EmojiUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import PropTypes, { string } from 'prop-types'; + +const propTypes = { + emojiCode: PropTypes.string.isRequired, + style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]) +} + +const defaultProps = { + style: undefined +} + +function EmojiWithTooltip(props) { + const styles = useThemeStyles(); + const emojiCode = props.emojiCode; + const emoji = EmojiUtils.findEmojiByCode(emojiCode); + const emojiName = EmojiUtils.getEmojiName(emoji); + return ( + { + return ( + + + + {emojiCode} + + + {`:${emojiName}:`} + + ) + }}> + + {emojiCode} + + + + ); +} + +EmojiWithTooltip.propTypes = propTypes; +EmojiWithTooltip.defaultProps = defaultProps; +EmojiWithTooltip.displayName = 'EmojiWithTooltip'; + +export default EmojiWithTooltip; \ No newline at end of file diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx index 690f2fc6883a..9d8ebb0dd709 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx @@ -70,6 +70,7 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim mixedUAStyles: {whiteSpace: 'pre'}, contentModel: HTMLContentModel.block, }), + emoji: HTMLElementModel.fromCustomModel({tagName: 'emoji', contentModel: HTMLContentModel.textual}) }), [styles.colorMuted, styles.formError, styles.mb0, styles.textLabelSupporting, styles.lh16], ); diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.js new file mode 100644 index 000000000000..3431bda3df4b --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.js @@ -0,0 +1,22 @@ +import React from 'react'; +import {View} from 'react-native'; +import _ from 'underscore'; +import Text from '@components/Text'; +import htmlRendererPropTypes from './htmlRendererPropTypes'; +import Tooltip from '@components/Tooltip'; +import * as EmojiUtils from '@libs/EmojiUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import EmojiWithTooltip from '@components/EmojiWithTooltip'; + +function EmojiRenderer(props) { + return ( + + ) +} + +EmojiRenderer.propTypes = htmlRendererPropTypes; +EmojiRenderer.displayName = 'HereMentionRenderer'; + +export default EmojiRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/index.js b/src/components/HTMLEngineProvider/HTMLRenderers/index.js index 9d0dab731792..2a8ddde44bac 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/index.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/index.js @@ -1,6 +1,7 @@ import AnchorRenderer from './AnchorRenderer'; import CodeRenderer from './CodeRenderer'; import EditedRenderer from './EditedRenderer'; +import EmojiRenderer from './EmojiRenderer'; import ImageRenderer from './ImageRenderer'; import MentionHereRenderer from './MentionHereRenderer'; import MentionUserRenderer from './MentionUserRenderer'; @@ -22,5 +23,6 @@ export default { pre: PreRenderer, 'mention-user': MentionUserRenderer, 'mention-here': MentionHereRenderer, + 'emoji': EmojiRenderer, 'next-step-email': NextStepEmailRenderer, }; diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index 8483b7a481f2..806e8147b707 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -250,7 +250,7 @@ function ReportPreview(props) { }, [isPaidGroupPolicy, isCurrentUserManager, isDraftExpenseReport, isApproved, iouSettled]); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; return ( - + // { @@ -339,7 +339,7 @@ function ReportPreview(props) { - + // ); } diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 34129a87d9b5..445ec2c9422e 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -728,7 +728,7 @@ function ReportActionItem(props) { ReportActions.clearReportActionErrors(props.report.reportID, props.action)} pendingAction={ - !_.isUndefined(props.draftMessage) ? null : props.action.pendingAction || (props.action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : '') + !_.isUndefined(props.draftMessage) ? null : props.action.pendingAction || (props.action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : '') || lodashGet(props, 'iouReport.pendingFields.preview') } shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(props.action, props.report.reportID)} errors={props.action.errors} diff --git a/src/styles/index.ts b/src/styles/index.ts index aace13c34594..c0e753eb2638 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -278,6 +278,11 @@ const styles = (theme: ThemeColors) => ...wordBreak.breakWord, ...spacing.pr4, }, + emojiTooltipWrapper: { + backgroundColor: theme.appBG, + ...spacing.p2, + borderRadius: 8 + }, mentionSuggestionsAvatarContainer: { width: 24, From f6a3bf67de30ad4a5b7db387c95f21b7648b4773 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 23 Jan 2024 14:46:02 +0800 Subject: [PATCH 006/345] debounce set image load state and hide when not loaded yet --- src/components/Lightbox.js | 6 ++++-- src/components/MultiGestureCanvas/index.js | 2 -- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Lightbox.js b/src/components/Lightbox.js index 45326edb4610..73fb9ad13129 100644 --- a/src/components/Lightbox.js +++ b/src/components/Lightbox.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; +import useDebounce from '@hooks/useDebounce'; import useStyleUtils from '@hooks/useStyleUtils'; import * as AttachmentsPropTypes from './Attachments/propTypes'; import Image from './Image'; @@ -76,6 +77,7 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError const isItemActive = index === activeIndex; const [isActive, setActive] = useState(isItemActive); const [isImageLoaded, setImageLoaded] = useState(false); + const debouncedSetImageLoaded = useDebounce(useCallback(setImageLoaded, []), 500, {maxWait: 1000}); const isInactiveCarouselItem = hasSiblingCarouselItems && !isActive; const [isFallbackVisible, setFallbackVisible] = useState(isInactiveCarouselItem); @@ -178,10 +180,10 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError > setImageLoaded(true)} + onLoadEnd={() => debouncedSetImageLoaded(true)} onLoad={(e) => { const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); diff --git a/src/components/MultiGestureCanvas/index.js b/src/components/MultiGestureCanvas/index.js index ccb433592412..bbfb7768c461 100644 --- a/src/components/MultiGestureCanvas/index.js +++ b/src/components/MultiGestureCanvas/index.js @@ -559,8 +559,6 @@ function MultiGestureCanvas({canvasSize, isActive = true, onScaleChanged, childr }, {scale: totalScale.value}, ], - // Hide the image if the size is not ready yet - opacity: props.contentSize ? 1 : 0, }; }); From a6b448abd0181240cc7d4566bdd325540c8d0a55 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 23 Jan 2024 14:56:01 +0800 Subject: [PATCH 007/345] lint --- src/components/Lightbox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Lightbox.js b/src/components/Lightbox.js index 73fb9ad13129..de3a9d4b9733 100644 --- a/src/components/Lightbox.js +++ b/src/components/Lightbox.js @@ -77,7 +77,7 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError const isItemActive = index === activeIndex; const [isActive, setActive] = useState(isItemActive); const [isImageLoaded, setImageLoaded] = useState(false); - const debouncedSetImageLoaded = useDebounce(useCallback(setImageLoaded, []), 500, {maxWait: 1000}); + const debouncedSetImageLoaded = useDebounce(useCallback(setImageLoaded, [setImageLoaded]), 500, {maxWait: 1000}); const isInactiveCarouselItem = hasSiblingCarouselItems && !isActive; const [isFallbackVisible, setFallbackVisible] = useState(isInactiveCarouselItem); From 567f90ab6d1ed86218575d280b957a43c47548b5 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 23 Jan 2024 19:03:31 +0800 Subject: [PATCH 008/345] only debounce when there is no cache yet --- src/components/Lightbox.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/Lightbox.js b/src/components/Lightbox.js index de3a9d4b9733..2096587b312a 100644 --- a/src/components/Lightbox.js +++ b/src/components/Lightbox.js @@ -1,6 +1,6 @@ /* eslint-disable es/no-optional-chaining */ import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; import useDebounce from '@hooks/useDebounce'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -74,6 +74,7 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError cachedDimensions.set(source, newDimensions); }; + const hasCache = useRef(!!cachedDimensions.get(source)); const isItemActive = index === activeIndex; const [isActive, setActive] = useState(isItemActive); const [isImageLoaded, setImageLoaded] = useState(false); @@ -183,7 +184,13 @@ function Lightbox({isAuthTokenRequired, source, onScaleChanged, onPress, onError style={[imageDimensions?.lightboxSize || {width: DEFAULT_IMAGE_SIZE, height: DEFAULT_IMAGE_SIZE}, {opacity: isImageLoaded ? 1 : 0}]} isAuthTokenRequired={isAuthTokenRequired} onError={onError} - onLoadEnd={() => debouncedSetImageLoaded(true)} + onLoadEnd={() => { + if (!hasCache.current) { + debouncedSetImageLoaded(true); + return; + } + setImageLoaded(true); + }} onLoad={(e) => { const width = (e.nativeEvent?.width || 0) * PixelRatio.get(); const height = (e.nativeEvent?.height || 0) * PixelRatio.get(); From 925d6c353b1cb2b8d785ac6c0dc9b9b075a45dfb Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 24 Jan 2024 14:21:31 +0700 Subject: [PATCH 009/345] implement tooltip for emoji --- src/CONST.ts | 1 + src/components/EmojiWithTooltip/index.js | 28 ++++---- .../BaseHTMLEngineProvider.tsx | 2 +- .../HTMLRenderers/EmojiRenderer.js | 16 +---- .../HTMLEngineProvider/HTMLRenderers/index.js | 2 +- .../ReportActionItem/ReportPreview.js | 4 +- src/libs/EmojiUtils.ts | 3 + src/pages/home/report/ReportActionItem.js | 6 +- .../report/comment/TextCommentFragment.js | 65 ++++++++++++++----- src/styles/index.ts | 2 +- 10 files changed, 79 insertions(+), 50 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 5fee60e57617..53cfb4b51646 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1406,6 +1406,7 @@ const CONST = { CARD_EXPIRATION_DATE: /^(0[1-9]|1[0-2])([^0-9])?([0-9]{4}|([0-9]{2}))$/, ROOM_NAME: /^#[\p{Ll}0-9-]{1,80}$/u, + EMOJI_SPLIT: /([\uD800-\uDBFF][\uDC00-\uDFFF])/, // eslint-disable-next-line max-len, no-misleading-character-class EMOJI: /[\p{Extended_Pictographic}\u200d\u{1f1e6}-\u{1f1ff}\u{1f3fb}-\u{1f3ff}\u{e0020}-\u{e007f}\u20E3\uFE0F]|[#*0-9]\uFE0F?\u20E3/gu, // eslint-disable-next-line max-len, no-misleading-character-class diff --git a/src/components/EmojiWithTooltip/index.js b/src/components/EmojiWithTooltip/index.js index e82d2f307761..754044bb3188 100644 --- a/src/components/EmojiWithTooltip/index.js +++ b/src/components/EmojiWithTooltip/index.js @@ -1,20 +1,19 @@ +import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; -import _ from 'underscore'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; -import * as EmojiUtils from '@libs/EmojiUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import PropTypes, { string } from 'prop-types'; +import * as EmojiUtils from '@libs/EmojiUtils'; const propTypes = { emojiCode: PropTypes.string.isRequired, - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]) -} + style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), +}; const defaultProps = { - style: undefined -} + style: undefined, +}; function EmojiWithTooltip(props) { const styles = useThemeStyles(); @@ -22,8 +21,8 @@ function EmojiWithTooltip(props) { const emoji = EmojiUtils.findEmojiByCode(emojiCode); const emojiName = EmojiUtils.getEmojiName(emoji); return ( - { - return ( + ( {`:${emojiName}:`} - ) - }}> - - {emojiCode} - + )} + > + {emojiCode} - ); } @@ -49,4 +45,4 @@ EmojiWithTooltip.propTypes = propTypes; EmojiWithTooltip.defaultProps = defaultProps; EmojiWithTooltip.displayName = 'EmojiWithTooltip'; -export default EmojiWithTooltip; \ No newline at end of file +export default EmojiWithTooltip; diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx index 9d8ebb0dd709..885c3b55b24e 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx @@ -70,7 +70,7 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim mixedUAStyles: {whiteSpace: 'pre'}, contentModel: HTMLContentModel.block, }), - emoji: HTMLElementModel.fromCustomModel({tagName: 'emoji', contentModel: HTMLContentModel.textual}) + emoji: HTMLElementModel.fromCustomModel({tagName: 'emoji', contentModel: HTMLContentModel.textual}), }), [styles.colorMuted, styles.formError, styles.mb0, styles.textLabelSupporting, styles.lh16], ); diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.js index 3431bda3df4b..79472e2d88c5 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.js @@ -1,22 +1,12 @@ import React from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; -import Text from '@components/Text'; -import htmlRendererPropTypes from './htmlRendererPropTypes'; -import Tooltip from '@components/Tooltip'; -import * as EmojiUtils from '@libs/EmojiUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; import EmojiWithTooltip from '@components/EmojiWithTooltip'; +import htmlRendererPropTypes from './htmlRendererPropTypes'; function EmojiRenderer(props) { - return ( - - ) + return ; } EmojiRenderer.propTypes = htmlRendererPropTypes; -EmojiRenderer.displayName = 'HereMentionRenderer'; +EmojiRenderer.displayName = 'EmojiRenderer'; export default EmojiRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/index.js b/src/components/HTMLEngineProvider/HTMLRenderers/index.js index 2a8ddde44bac..824d9900f3e7 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/index.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/index.js @@ -23,6 +23,6 @@ export default { pre: PreRenderer, 'mention-user': MentionUserRenderer, 'mention-here': MentionHereRenderer, - 'emoji': EmojiRenderer, + emoji: EmojiRenderer, 'next-step-email': NextStepEmailRenderer, }; diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index 9b8d1bd5eefa..6b4cd440e7e8 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -260,7 +260,7 @@ function ReportPreview(props) { }, [isPaidGroupPolicy, isCurrentUserManager, isDraftExpenseReport, isApproved, iouSettled]); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; return ( - // + { @@ -349,7 +349,7 @@ function ReportPreview(props) { - // + ); } diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index e34fa0b90fc6..584d4964447f 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -39,6 +39,9 @@ const findEmojiByName = (name: string): Emoji => Emojis.emojiNameTable[name]; const findEmojiByCode = (code: string): Emoji => Emojis.emojiCodeTableWithSkinTones[code]; const getEmojiName = (emoji: Emoji, lang: 'en' | 'es' = CONST.LOCALES.DEFAULT): string => { + if (!emoji) { + return ''; + } if (lang === CONST.LOCALES.DEFAULT) { return emoji.name; } diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 91c5d611ecaf..cd0a6e9328d0 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -729,7 +729,11 @@ function ReportActionItem(props) { ReportActions.clearReportActionErrors(props.report.reportID, props.action)} pendingAction={ - !_.isUndefined(props.draftMessage) ? null : props.action.pendingAction || (props.action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : '') || lodashGet(props, 'iouReport.pendingFields.preview') + !_.isUndefined(props.draftMessage) + ? null + : props.action.pendingAction || + (props.action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : '') || + lodashGet(props, 'iouReport.pendingFields.preview') } shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(props.action, props.report.reportID)} errors={props.action.errors} diff --git a/src/pages/home/report/comment/TextCommentFragment.js b/src/pages/home/report/comment/TextCommentFragment.js index 3d6482344450..dca02e208627 100644 --- a/src/pages/home/report/comment/TextCommentFragment.js +++ b/src/pages/home/report/comment/TextCommentFragment.js @@ -1,6 +1,8 @@ import Str from 'expensify-common/lib/str'; import PropTypes from 'prop-types'; import React, {memo} from 'react'; +import _ from 'underscore'; +import EmojiWithTooltip from '@components/EmojiWithTooltip'; import Text from '@components/Text'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; @@ -46,19 +48,34 @@ const defaultProps = { iouMessage: undefined, }; +function removeLineBreakAndEmojiTag(html) { + const htmlWithoutLineBreak = Str.replaceAll(html, '
', '\n'); + const htmlWithoutEmojiOpenTag = Str.replaceAll(htmlWithoutLineBreak, '', ''); + return Str.replaceAll(htmlWithoutEmojiOpenTag, '', ''); +} + +/** + * Split the string containing emoji into an array + * @param {string} text + * @returns {Array} + */ +function getTextMatrix(text) { + return _.filter(text.split(CONST.REGEX.EMOJI_SPLIT), (value) => value !== ''); +} + function TextCommentFragment(props) { const theme = useTheme(); const styles = useThemeStyles(); const {fragment, styleAsDeleted} = props; const {html, text} = fragment; - // If the only difference between fragment.text and fragment.html is
tags + // If the only difference between fragment.text and fragment.html is
and the emoji tags // we render it as text, not as html. - // This is done to render emojis with line breaks between them as text. - const differByLineBreaksOnly = Str.replaceAll(html, '
', '\n') === text; + // This is done to render emojis with line breaks between them as text + const differByLineBreaksAndEmojiOnly = removeLineBreakAndEmojiTag(html) === text; // Only render HTML if we have html in the fragment - if (!differByLineBreaksOnly) { + if (!differByLineBreaksAndEmojiOnly) { const editedTag = fragment.isEdited ? `` : ''; const htmlContent = styleAsDeleted ? `${html}` : html; @@ -73,6 +90,7 @@ function TextCommentFragment(props) { } const containsOnlyEmojis = EmojiUtils.containsOnlyEmojis(text); + const textMatrix = getTextMatrix(convertToLTR(props.iouMessage || text)); return ( @@ -80,17 +98,34 @@ function TextCommentFragment(props) { text={text} displayAsGroup={props.displayAsGroup} /> - - {convertToLTR(props.iouMessage || text)} - + {_.map(textMatrix, (tx) => { + const isEmoji = CONST.REGEX.EMOJI.test(tx); + return isEmoji ? ( + + ) : ( + + {tx} + + ); + })} {Boolean(fragment.isEdited) && ( <> emojiTooltipWrapper: { backgroundColor: theme.appBG, ...spacing.p2, - borderRadius: 8 + borderRadius: 8, }, mentionSuggestionsAvatarContainer: { From ff81b72f7b3f8de625ca3eb29577249a95013e77 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 24 Jan 2024 15:26:31 +0700 Subject: [PATCH 010/345] write new file in typescript --- .../EmojiWithTooltip/{index.js => index.tsx} | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) rename src/components/EmojiWithTooltip/{index.js => index.tsx} (71%) diff --git a/src/components/EmojiWithTooltip/index.js b/src/components/EmojiWithTooltip/index.tsx similarity index 71% rename from src/components/EmojiWithTooltip/index.js rename to src/components/EmojiWithTooltip/index.tsx index 754044bb3188..6c745fa1b0d1 100644 --- a/src/components/EmojiWithTooltip/index.js +++ b/src/components/EmojiWithTooltip/index.tsx @@ -1,23 +1,18 @@ -import PropTypes from 'prop-types'; import React from 'react'; +import type {StyleProp, TextStyle} from 'react-native'; import {View} from 'react-native'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import useThemeStyles from '@hooks/useThemeStyles'; import * as EmojiUtils from '@libs/EmojiUtils'; -const propTypes = { - emojiCode: PropTypes.string.isRequired, - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), +type EmojiWithTooltipProps = { + emojiCode: string; + style?: StyleProp; }; -const defaultProps = { - style: undefined, -}; - -function EmojiWithTooltip(props) { +function EmojiWithTooltip({emojiCode, style = undefined}: EmojiWithTooltipProps) { const styles = useThemeStyles(); - const emojiCode = props.emojiCode; const emoji = EmojiUtils.findEmojiByCode(emojiCode); const emojiName = EmojiUtils.getEmojiName(emoji); return ( @@ -36,13 +31,11 @@ function EmojiWithTooltip(props) {
)} > - {emojiCode} + {emojiCode} ); } -EmojiWithTooltip.propTypes = propTypes; -EmojiWithTooltip.defaultProps = defaultProps; EmojiWithTooltip.displayName = 'EmojiWithTooltip'; export default EmojiWithTooltip; From dfcc774a90346e4e46100184b54499f09cda0825 Mon Sep 17 00:00:00 2001 From: RohanSasne Date: Tue, 30 Jan 2024 13:02:58 +0530 Subject: [PATCH 011/345] Add isExtraSmallScreenWidth Prop --- src/hooks/useWindowDimensions/index.native.ts | 2 ++ src/hooks/useWindowDimensions/index.ts | 2 ++ src/hooks/useWindowDimensions/types.ts | 1 + 3 files changed, 5 insertions(+) diff --git a/src/hooks/useWindowDimensions/index.native.ts b/src/hooks/useWindowDimensions/index.native.ts index 5d556234aeb9..5b389e8aa2e1 100644 --- a/src/hooks/useWindowDimensions/index.native.ts +++ b/src/hooks/useWindowDimensions/index.native.ts @@ -12,6 +12,7 @@ export default function (): WindowDimensions { const isSmallScreenWidth = true; const isMediumScreenWidth = false; const isLargeScreenWidth = false; + const isExtraSmallScreenWidth = windowWidth <= variables.extraSmallMobileResponsiveWidthBreakpoint; return { windowWidth, @@ -20,5 +21,6 @@ export default function (): WindowDimensions { isSmallScreenWidth, isMediumScreenWidth, isLargeScreenWidth, + isExtraSmallScreenWidth, }; } diff --git a/src/hooks/useWindowDimensions/index.ts b/src/hooks/useWindowDimensions/index.ts index b0a29e9f901b..625bbc3c7845 100644 --- a/src/hooks/useWindowDimensions/index.ts +++ b/src/hooks/useWindowDimensions/index.ts @@ -14,6 +14,7 @@ export default function (): WindowDimensions { const isSmallScreenWidth = windowWidth <= variables.mobileResponsiveWidthBreakpoint; const isMediumScreenWidth = windowWidth > variables.mobileResponsiveWidthBreakpoint && windowWidth <= variables.tabletResponsiveWidthBreakpoint; const isLargeScreenWidth = windowWidth > variables.tabletResponsiveWidthBreakpoint; + const isExtraSmallScreenWidth = windowWidth <= variables.extraSmallMobileResponsiveWidthBreakpoint; return { windowWidth, @@ -22,5 +23,6 @@ export default function (): WindowDimensions { isSmallScreenWidth, isMediumScreenWidth, isLargeScreenWidth, + isExtraSmallScreenWidth, }; } diff --git a/src/hooks/useWindowDimensions/types.ts b/src/hooks/useWindowDimensions/types.ts index 9b59d4968935..5793e8f2c806 100644 --- a/src/hooks/useWindowDimensions/types.ts +++ b/src/hooks/useWindowDimensions/types.ts @@ -5,6 +5,7 @@ type WindowDimensions = { isSmallScreenWidth: boolean; isMediumScreenWidth: boolean; isLargeScreenWidth: boolean; + isExtraSmallScreenWidth: boolean; }; export default WindowDimensions; From e9ca05234e0f137629b11228622a0b706fddacb4 Mon Sep 17 00:00:00 2001 From: RohanSasne Date: Tue, 30 Jan 2024 13:14:46 +0530 Subject: [PATCH 012/345] Convert steps to typescriopt --- .../Steps/{CodesStep.js => CodesStep.tsx} | 63 ++++++++++-------- .../{DisabledStep.js => DisabledStep.tsx} | 0 .../Steps/{EnabledStep.js => EnabledStep.tsx} | 0 .../Steps/{SuccessStep.js => SuccessStep.tsx} | 22 +++---- .../Steps/{VerifyStep.js => VerifyStep.tsx} | 64 ++++++++----------- 5 files changed, 74 insertions(+), 75 deletions(-) rename src/pages/settings/Security/TwoFactorAuth/Steps/{CodesStep.js => CodesStep.tsx} (74%) rename src/pages/settings/Security/TwoFactorAuth/Steps/{DisabledStep.js => DisabledStep.tsx} (100%) rename src/pages/settings/Security/TwoFactorAuth/Steps/{EnabledStep.js => EnabledStep.tsx} (100%) rename src/pages/settings/Security/TwoFactorAuth/Steps/{SuccessStep.js => SuccessStep.tsx} (83%) rename src/pages/settings/Security/TwoFactorAuth/Steps/{VerifyStep.js => VerifyStep.tsx} (77%) diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.js b/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx similarity index 74% rename from src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.js rename to src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx index 420d976dcd26..f124565db39b 100644 --- a/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.js +++ b/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.tsx @@ -1,7 +1,7 @@ -import React, {useEffect, useState} from 'react'; -import {ActivityIndicator, ScrollView, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import React, { useEffect, useState } from 'react'; +import { ActivityIndicator, ScrollView, View } from 'react-native'; +import { withOnyx } from 'react-native-onyx'; +import type { Route } from '@src/ROUTES'; import Button from '@components/Button'; import FixedFooter from '@components/FixedFooter'; import FormHelpMessage from '@components/FormHelpMessage'; @@ -18,23 +18,29 @@ import Clipboard from '@libs/Clipboard'; import localFileDownload from '@libs/localFileDownload'; import StepWrapper from '@pages/settings/Security/TwoFactorAuth/StepWrapper/StepWrapper'; import useTwoFactorAuthContext from '@pages/settings/Security/TwoFactorAuth/TwoFactorAuthContext/useTwoFactorAuth'; -import {defaultAccount, TwoFactorAuthPropTypes} from '@pages/settings/Security/TwoFactorAuth/TwoFactorAuthPropTypes'; +import type TwoFactorAuthOnyxProps from '@pages/settings/Security/TwoFactorAuth/TwoFactorAuthPropTypes'; import * as Session from '@userActions/Session'; import * as TwoFactorAuthActions from '@userActions/TwoFactorAuthActions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type { TranslationPaths } from '@src/languages/types'; -function CodesStep({account = defaultAccount, backTo}) { +type CodesStepProps = TwoFactorAuthOnyxProps & { + /** The route to go back to when the user cancels */ + backTo: Route | undefined; +} + +function CodesStep({ account, backTo }: CodesStepProps) { const theme = useTheme(); const styles = useThemeStyles(); - const {translate} = useLocalize(); - const {isExtraSmallScreenWidth, isSmallScreenWidth} = useWindowDimensions(); + const { translate } = useLocalize(); + const { isExtraSmallScreenWidth, isSmallScreenWidth } = useWindowDimensions(); const [error, setError] = useState(''); - const {setStep} = useTwoFactorAuthContext(); + const { setStep } = useTwoFactorAuthContext(); useEffect(() => { - if (account.requiresTwoFactorAuth || account.recoveryCodes) { + if (account?.requiresTwoFactorAuth ?? account?.recoveryCodes) { return; } Session.toggleTwoFactorAuth(true); @@ -62,23 +68,25 @@ function CodesStep({account = defaultAccount, backTo}) { {translate('twoFactorAuth.codesLoseAccess')} - - {account.isLoading ? ( + + {account?.isLoading ? ( ) : ( <> - {Boolean(account.recoveryCodes) && - _.map(account.recoveryCodes.split(', '), (code) => ( + {Boolean(account?.recoveryCodes) && + (account?.recoveryCodes?.split(', ') ?? []).map((code) => ( {code} - ))} + )) + } + { - Clipboard.setString(account.recoveryCodes); + Clipboard.setString(account?.recoveryCodes ?? ''); setError(''); TwoFactorAuthActions.setCodesAreCopied(); }} styles={[styles.button, styles.buttonMedium, styles.twoFactorAuthCodesButton]} textStyles={[styles.buttonMediumText]} + accessible={false} + tooltipText='' + tooltipTextChecked='' /> { - localFileDownload('two-factor-auth-codes', account.recoveryCodes); + localFileDownload('two-factor-auth-codes', account?.recoveryCodes ?? ''); setError(''); TwoFactorAuthActions.setCodesAreCopied(); }} inline={false} styles={[styles.button, styles.buttonMedium, styles.twoFactorAuthCodesButton]} textStyles={[styles.buttonMediumText]} + accessible={false} + tooltipText='' + tooltipTextChecked='' /> @@ -112,10 +126,10 @@ function CodesStep({account = defaultAccount, backTo}) { - {!_.isEmpty(error) && ( + {!error && ( )} @@ -123,7 +137,7 @@ function CodesStep({account = defaultAccount, backTo}) { success text={translate('common.next')} onPress={() => { - if (!account.codesAreCopied) { + if (!account?.codesAreCopied) { setError('twoFactorAuth.errorStepCodes'); return; } @@ -136,10 +150,9 @@ function CodesStep({account = defaultAccount, backTo}) { ); } -CodesStep.propTypes = TwoFactorAuthPropTypes; CodesStep.displayName = 'CodesStep'; -// eslint-disable-next-line rulesdir/onyx-props-must-have-default -export default withOnyx({ - account: {key: ONYXKEYS.ACCOUNT}, -})(CodesStep); +export default withOnyx({ + account: { key: ONYXKEYS.ACCOUNT }, + session: { key: ONYXKEYS.SESSION }, +})(CodesStep); \ No newline at end of file diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/DisabledStep.js b/src/pages/settings/Security/TwoFactorAuth/Steps/DisabledStep.tsx similarity index 100% rename from src/pages/settings/Security/TwoFactorAuth/Steps/DisabledStep.js rename to src/pages/settings/Security/TwoFactorAuth/Steps/DisabledStep.tsx diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.js b/src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.tsx similarity index 100% rename from src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.js rename to src/pages/settings/Security/TwoFactorAuth/Steps/EnabledStep.tsx diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/SuccessStep.js b/src/pages/settings/Security/TwoFactorAuth/Steps/SuccessStep.tsx similarity index 83% rename from src/pages/settings/Security/TwoFactorAuth/Steps/SuccessStep.js rename to src/pages/settings/Security/TwoFactorAuth/Steps/SuccessStep.tsx index de36888f30b8..9ffc25427a64 100644 --- a/src/pages/settings/Security/TwoFactorAuth/Steps/SuccessStep.js +++ b/src/pages/settings/Security/TwoFactorAuth/Steps/SuccessStep.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import ConfirmationPage from '@components/ConfirmationPage'; import LottieAnimations from '@components/LottieAnimations'; @@ -8,17 +7,16 @@ import StepWrapper from '@pages/settings/Security/TwoFactorAuth/StepWrapper/Step import useTwoFactorAuthContext from '@pages/settings/Security/TwoFactorAuth/TwoFactorAuthContext/useTwoFactorAuth'; import * as TwoFactorAuthActions from '@userActions/TwoFactorAuthActions'; import CONST from '@src/CONST'; +import type { Route } from '@src/ROUTES'; -const propTypes = { +type SuccessStepProps = { /** The route where user needs to be redirected after setting up 2FA */ - backTo: PropTypes.string, + backTo: string; }; -const defaultProps = { - backTo: '', -}; - -function SuccessStep({backTo}) { +function SuccessStep({ + backTo='' +}:SuccessStepProps) { const {setStep} = useTwoFactorAuthContext(); const {translate} = useLocalize(); @@ -29,6 +27,7 @@ function SuccessStep({backTo}) { stepCounter={{ step: 3, text: translate('twoFactorAuth.stepSuccess'), + total: 3, }} > @@ -49,7 +48,4 @@ function SuccessStep({backTo}) { ); } -SuccessStep.propTypes = propTypes; -SuccessStep.defaultProps = defaultProps; - -export default SuccessStep; +export default SuccessStep; \ No newline at end of file diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.js b/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.tsx similarity index 77% rename from src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.js rename to src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.tsx index e5f809204bd6..4e2152c74fb3 100644 --- a/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.js +++ b/src/pages/settings/Security/TwoFactorAuth/Steps/VerifyStep.tsx @@ -1,5 +1,4 @@ -import PropTypes from 'prop-types'; -import React, {useEffect} from 'react'; +import React, {useEffect, useRef} from 'react'; import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import expensifyLogo from '@assets/images/expensify-logo-round-transparent.png'; @@ -16,25 +15,29 @@ import Clipboard from '@libs/Clipboard'; import StepWrapper from '@pages/settings/Security/TwoFactorAuth/StepWrapper/StepWrapper'; import useTwoFactorAuthContext from '@pages/settings/Security/TwoFactorAuth/TwoFactorAuthContext/useTwoFactorAuth'; import TwoFactorAuthForm from '@pages/settings/Security/TwoFactorAuth/TwoFactorAuthForm'; -import {defaultAccount, TwoFactorAuthPropTypes} from '@pages/settings/Security/TwoFactorAuth/TwoFactorAuthPropTypes'; +import type TwoFactorAuthOnyxProps from '@pages/settings/Security/TwoFactorAuth/TwoFactorAuthPropTypes'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; const TROUBLESHOOTING_LINK = 'https://community.expensify.com/discussion/7736/faq-troubleshooting-two-factor-authentication-issues/p1?new=1'; -const defaultProps = { - account: defaultAccount, +type VerifyStepProps = TwoFactorAuthOnyxProps & { + /** Session of currently logged in user */ session: { - email: null, - }, + /** Email address */ + email: string; + }; }; -function VerifyStep({account, session}) { +function VerifyStep({ + account, + session, +}: VerifyStepProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const formRef = React.useRef(null); + const formRef = useRef(null);; const {setStep} = useTwoFactorAuthContext(); @@ -46,19 +49,16 @@ function VerifyStep({account, session}) { }, []); useEffect(() => { - if (!account.requiresTwoFactorAuth) { + if (!account?.requiresTwoFactorAuth) { return; } setStep(CONST.TWO_FACTOR_AUTH_STEPS.SUCCESS); - }, [account.requiresTwoFactorAuth, setStep]); + }, [account?.requiresTwoFactorAuth, setStep]); /** * Splits the two-factor auth secret key in 4 chunks - * - * @param {String} secret - * @returns {string} */ - function splitSecretInChunks(secret) { + function splitSecretInChunks(secret: string): string { if (secret.length !== 16) { return secret; } @@ -69,11 +69,9 @@ function VerifyStep({account, session}) { /** * Builds the URL string to generate the QRCode, using the otpauth:// protocol, * so it can be detected by authenticator apps - * - * @returns {string} */ - function buildAuthenticatorUrl() { - return `otpauth://totp/Expensify:${account.primaryLogin || session.email}?secret=${account.twoFactorAuthSecretKey}&issuer=Expensify`; + function buildAuthenticatorUrl(): string { + return `otpauth://totp/Expensify:${account?.primaryLogin ?? session.email}?secret=${account?.twoFactorAuthSecretKey}&issuer=Expensify`; } return ( @@ -85,7 +83,7 @@ function VerifyStep({account, session}) { total: 3, }} onBackButtonPress={() => setStep(CONST.TWO_FACTOR_AUTH_STEPS.CODES, CONST.ANIMATION_DIRECTION.OUT)} - onEntryTransitionEnd={() => formRef.current && formRef.current.focus()} + onEntryTransitionEnd={() => formRef.current?.focus()} > {translate('twoFactorAuth.addKey')} - {Boolean(account.twoFactorAuthSecretKey) && {splitSecretInChunks(account.twoFactorAuthSecretKey)}} + {Boolean(account?.twoFactorAuthSecretKey) && {splitSecretInChunks(account?.twoFactorAuthSecretKey ?? '')}} Clipboard.setString(account.twoFactorAuthSecretKey)} + onPress={() => Clipboard.setString(account?.twoFactorAuthSecretKey ?? '')} styles={[styles.button, styles.buttonMedium, styles.twoFactorAuthCopyCodeButton]} textStyles={[styles.buttonMediumText]} + accessible={false} + tooltipText='' + tooltipTextChecked='' /> {translate('twoFactorAuth.enterCode')} @@ -127,12 +128,12 @@ function VerifyStep({account, session}) {