diff --git a/.github/ISSUE_TEMPLATE/DesignDoc.md b/.github/ISSUE_TEMPLATE/DesignDoc.md index 424b549a0940..2fbdcf7a65d5 100644 --- a/.github/ISSUE_TEMPLATE/DesignDoc.md +++ b/.github/ISSUE_TEMPLATE/DesignDoc.md @@ -27,6 +27,7 @@ labels: Daily, NewFeature - [ ] Confirm that the doc has the minimum necessary number of reviews before proceeding - [ ] Email `strategy@expensify.com` one last time to let them know the Design Doc is moving into the implementation phase - [ ] Implement the changes +- [ ] Add regression tests so that QA can test your feature with every deploy ([instructions](https://stackoverflowteams.com/c/expensify/questions/363)) - [ ] Send out a follow up email to `strategy@expensify.com` once everything has been implemented and do a **Project Wrap-Up** retrospective that provides: - Summary of what we accomplished with this project - What went well? diff --git a/android/app/build.gradle b/android/app/build.gradle index 11ce415ad0ae..a9e7a0d48b73 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -90,8 +90,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001036300 - versionName "1.3.63-0" + versionCode 1001036400 + versionName "1.3.64-0" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index b69066e7546a..81d81db8616d 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.63 + 1.3.64 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.3.63.0 + 1.3.64.0 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index dcb0ddc8af49..377e23436ec7 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.3.63 + 1.3.64 CFBundleSignature ???? CFBundleVersion - 1.3.63.0 + 1.3.64.0 diff --git a/package-lock.json b/package-lock.json index d595fc527a77..ea138c99b8a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.3.63-0", + "version": "1.3.64-0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.3.63-0", + "version": "1.3.64-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index cf3fd570164b..1fc9d4022ee2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.3.63-0", + "version": "1.3.64-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/CONST.ts b/src/CONST.ts index 1c67535e54e4..56f61536b3cb 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -452,7 +452,7 @@ const CONST = { }, RECEIPT: { ICON_SIZE: 164, - PERMISSION_AUTHORIZED: 'authorized', + PERMISSION_GRANTED: 'granted', HAND_ICON_HEIGHT: 152, HAND_ICON_WIDTH: 200, SHUTTER_SIZE: 90, diff --git a/src/components/DownloadAppModal.js b/src/components/DownloadAppModal.js index ffa933708e4c..c96c6b3d28c0 100644 --- a/src/components/DownloadAppModal.js +++ b/src/components/DownloadAppModal.js @@ -26,13 +26,13 @@ const defaultProps = { }; function DownloadAppModal({isAuthenticated, showDownloadAppBanner}) { - const [shouldShowBanner, setshouldShowBanner] = useState(Browser.isMobile() && isAuthenticated && showDownloadAppBanner); + const [shouldShowBanner, setShouldShowBanner] = useState(Browser.isMobile() && isAuthenticated && showDownloadAppBanner); const {translate} = useLocalize(); const handleCloseBanner = () => { setShowDownloadAppModal(false); - setshouldShowBanner(false); + setShouldShowBanner(false); }; let link = ''; @@ -44,6 +44,8 @@ function DownloadAppModal({isAuthenticated, showDownloadAppBanner}) { } const handleOpenAppStore = () => { + setShowDownloadAppModal(false); + setShouldShowBanner(false); Link.openExternalLink(link, true); }; diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 6e2856a7e058..61f6981edbbe 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -65,7 +65,6 @@ class EmojiPickerMenu extends Component { this.filterEmojis = _.debounce(this.filterEmojis.bind(this), 300); this.highlightAdjacentEmoji = this.highlightAdjacentEmoji.bind(this); - this.scrollToHighlightedIndex = this.scrollToHighlightedIndex.bind(this); this.setupEventHandlers = this.setupEventHandlers.bind(this); this.cleanupEventHandlers = this.cleanupEventHandlers.bind(this); this.renderItem = this.renderItem.bind(this); @@ -76,7 +75,6 @@ class EmojiPickerMenu extends Component { this.getItemLayout = this.getItemLayout.bind(this); this.scrollToHeader = this.scrollToHeader.bind(this); - this.currentScrollOffset = 0; this.firstNonHeaderIndex = 0; const {filteredEmojis, headerEmojis, headerRowIndices} = this.getEmojisAndHeaderRowIndices(); @@ -299,9 +297,9 @@ class EmojiPickerMenu extends Component { return; } - // Blur the input and change the highlight type to keyboard + // Blur the input, change the highlight type to keyboard, and disable pointer events this.searchInput.blur(); - this.setState({isUsingKeyboardMovement: true}); + this.setState({isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); // We only want to hightlight the Emoji if none was highlighted already // If we already have a highlighted Emoji, lets just skip the first navigation @@ -311,10 +309,9 @@ class EmojiPickerMenu extends Component { } // If nothing is highlighted and an arrow key is pressed - // select the first emoji + // select the first emoji, apply keyboard movement styles, and disable pointer events if (this.state.highlightedIndex === -1) { - this.setState({highlightedIndex: this.firstNonHeaderIndex}); - this.scrollToHighlightedIndex(); + this.setState({highlightedIndex: this.firstNonHeaderIndex, isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); return; } @@ -368,10 +365,9 @@ class EmojiPickerMenu extends Component { break; } - // Actually highlight the new emoji, apply keyboard movement styles, and scroll to it if the index was changed + // Actually highlight the new emoji, apply keyboard movement styles, and disable pointer events if (newIndex !== this.state.highlightedIndex) { - this.setState({highlightedIndex: newIndex, isUsingKeyboardMovement: true}); - this.scrollToHighlightedIndex(); + this.setState({highlightedIndex: newIndex, isUsingKeyboardMovement: true, arePointerEventsDisabled: true}); } } @@ -381,36 +377,6 @@ class EmojiPickerMenu extends Component { this.emojiList.scrollToOffset({offset: calculatedOffset, animated: true}); } - /** - * Calculates the required scroll offset (aka distance from top) and scrolls the FlatList to the highlighted emoji - * if any portion of it falls outside of the window. - * Doing this because scrollToIndex doesn't work as expected. - */ - scrollToHighlightedIndex() { - // Calculate the number of rows above the current row, then add 1 to include the current row - const numRows = Math.floor(this.state.highlightedIndex / CONST.EMOJI_NUM_PER_ROW) + 1; - - // The scroll offsets at the top and bottom of the highlighted emoji - const offsetAtEmojiBottom = numRows * CONST.EMOJI_PICKER_HEADER_HEIGHT; - const offsetAtEmojiTop = offsetAtEmojiBottom - CONST.EMOJI_PICKER_ITEM_HEIGHT; - - // Scroll to fit the entire highlighted emoji into the window if we need to - let targetOffset = this.currentScrollOffset; - if (offsetAtEmojiBottom - this.currentScrollOffset >= CONST.NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT) { - targetOffset = offsetAtEmojiBottom - CONST.NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT; - } else if (offsetAtEmojiTop - CONST.EMOJI_PICKER_HEADER_HEIGHT <= this.currentScrollOffset) { - // There is always a sticky header on the top, subtract the EMOJI_PICKER_HEADER_HEIGHT from offsetAtEmojiTop to get the correct scroll position. - targetOffset = offsetAtEmojiTop - CONST.EMOJI_PICKER_HEADER_HEIGHT; - } - if (targetOffset !== this.currentScrollOffset) { - // Disable pointer events so that onHover doesn't get triggered when the items move while we're scrolling - if (!this.state.arePointerEventsDisabled) { - this.setState({arePointerEventsDisabled: true}); - } - this.emojiList.scrollToOffset({offset: targetOffset, animated: false}); - } - } - /** * Filter the entire list of emojis to only emojis that have the search term in their keywords * @@ -530,6 +496,7 @@ class EmojiPickerMenu extends Component { return ( @@ -566,10 +533,11 @@ class EmojiPickerMenu extends Component { {overscrollBehaviorY: 'contain'}, // Set overflow to hidden to prevent elastic scrolling when there are not enough contents to scroll in FlatList {overflowY: this.state.filteredEmojis.length > overflowLimit ? 'auto' : 'hidden'}, + // Set scrollPaddingTop to consider sticky headers while scrolling + {scrollPaddingTop: isFiltered ? 0 : CONST.EMOJI_PICKER_ITEM_HEIGHT}, ]} extraData={[this.state.filteredEmojis, this.state.highlightedIndex, this.props.preferredSkinTone]} stickyHeaderIndices={this.state.headerIndices} - onScroll={(e) => (this.currentScrollOffset = e.nativeEvent.contentOffset.y)} getItemLayout={this.getItemLayout} contentContainerStyle={styles.flexGrow1} ListEmptyComponent={{this.props.translate('common.noResultsFound')}} diff --git a/src/components/EmojiPicker/EmojiPickerMenuItem/index.js b/src/components/EmojiPicker/EmojiPickerMenuItem/index.js index 37e90f01c707..728e56792ddb 100644 --- a/src/components/EmojiPicker/EmojiPickerMenuItem/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenuItem/index.js @@ -42,13 +42,14 @@ class EmojiPickerMenuItem extends PureComponent { super(props); this.ref = null; + this.focusAndScroll = this.focusAndScroll.bind(this); } componentDidMount() { if (!this.props.isFocused) { return; } - this.ref.focus(); + this.focusAndScroll(); } componentDidUpdate(prevProps) { @@ -58,7 +59,12 @@ class EmojiPickerMenuItem extends PureComponent { if (!this.props.isFocused) { return; } - this.ref.focus(); + this.focusAndScroll(); + } + + focusAndScroll() { + this.ref.focus({preventScroll: true}); + this.ref.scrollIntoView({block: 'nearest'}); } render() { diff --git a/src/components/LHNOptionsList/OptionRowLHNData.js b/src/components/LHNOptionsList/OptionRowLHNData.js index 21fade6eb942..4c7bd54efa18 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.js +++ b/src/components/LHNOptionsList/OptionRowLHNData.js @@ -12,6 +12,9 @@ import withCurrentReportID, {withCurrentReportIDPropTypes, withCurrentReportIDDe import OptionRowLHN, {propTypes as basePropTypes, defaultProps as baseDefaultProps} from './OptionRowLHN'; import * as Report from '../../libs/actions/Report'; import * as UserUtils from '../../libs/UserUtils'; +import * as ReportActionsUtils from '../../libs/ReportActionsUtils'; +import * as TransactionUtils from '../../libs/TransactionUtils'; + import participantPropTypes from '../participantPropTypes'; import CONST from '../../CONST'; import reportActionPropTypes from '../../pages/home/report/reportActionPropTypes'; @@ -75,6 +78,7 @@ function OptionRowLHNData({ preferredLocale, comment, policies, + receiptTransactions, parentReportActions, ...propsToForward }) { @@ -88,6 +92,14 @@ function OptionRowLHNData({ const parentReportAction = parentReportActions[fullReport.parentReportActionID]; const optionItemRef = useRef(); + + const linkedTransaction = useMemo(() => { + const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(reportActions); + const lastReportAction = _.first(sortedReportActions); + return TransactionUtils.getLinkedTransaction(lastReportAction); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fullReport.reportID, receiptTransactions, reportActions]); + const optionItem = useMemo(() => { // Note: ideally we'd have this as a dependent selector in onyx! const item = SidebarUtils.getOptionData(fullReport, reportActions, personalDetails, preferredLocale, policy); @@ -98,7 +110,7 @@ function OptionRowLHNData({ return item; // Listen parentReportAction to update title of thread report when parentReportAction changed // eslint-disable-next-line react-hooks/exhaustive-deps - }, [fullReport, reportActions, personalDetails, preferredLocale, policy, parentReportAction]); + }, [fullReport, linkedTransaction, reportActions, personalDetails, preferredLocale, policy, parentReportAction]); useEffect(() => { if (!optionItem || optionItem.hasDraftComment || !comment || comment.length <= 0 || isFocused) { @@ -186,6 +198,11 @@ export default React.memo( key: ({fullReport}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${fullReport.parentReportID}`, canEvict: false, }, + // Ideally, we aim to access only the last transaction for the current report by listening to changes in reportActions. + // In some scenarios, a transaction might be created after reportActions have been modified. + // This can lead to situations where `lastTransaction` doesn't update and retains the previous value. + // However, performance overhead of this is minimized by using memos inside the component. + receiptTransactions: {key: ONYXKEYS.COLLECTION.TRANSACTION}, }), )(OptionRowLHNData), ); diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 637302f7d4f0..05c3463538c6 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -164,6 +164,8 @@ function MoneyRequestPreview(props) { !_.isEmpty(requestMerchant) && !props.isBillSplit && requestMerchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && requestMerchant !== CONST.TRANSACTION.DEFAULT_MERCHANT; const shouldShowDescription = !_.isEmpty(description) && !shouldShowMerchant; + const receiptImages = hasReceipt ? [ReceiptUtils.getThumbnailAndImageURIs(props.transaction.receipt.source, props.transaction.filename || props.transaction.receiptFilename || '')] : []; + const getSettledMessage = () => { switch (lodashGet(props.action, 'originalMessage.paymentType', '')) { case CONST.IOU.PAYMENT_TYPE.PAYPAL_ME: @@ -231,7 +233,7 @@ function MoneyRequestPreview(props) { {hasReceipt && ( )} diff --git a/src/components/ReportActionItem/ReportActionItemImage.js b/src/components/ReportActionItem/ReportActionItemImage.js index 5f8444af0b21..070f534f4924 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.js +++ b/src/components/ReportActionItem/ReportActionItemImage.js @@ -35,47 +35,44 @@ const defaultProps = { function ReportActionItemImage({thumbnail, image, enablePreviewModal}) { const {translate} = useLocalize(); + const imageSource = tryResolveUrlFromApiRoot(image || ''); + const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail || ''); - if (thumbnail) { - const imageSource = tryResolveUrlFromApiRoot(image); - const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail); - const thumbnailComponent = ( - - ); - - if (enablePreviewModal) { - return ( - - {({report}) => ( - { - const route = ROUTES.getReportAttachmentRoute(report.reportID, imageSource); - Navigation.navigate(route); - }} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} - accessibilityLabel={translate('accessibilityHints.viewAttachment')} - > - {thumbnailComponent} - - )} - - ); - } - return thumbnailComponent; - } - - return ( + const receiptImageComponent = thumbnail ? ( + + ) : ( ); + + if (enablePreviewModal) { + return ( + + {({report}) => ( + { + const route = ROUTES.getReportAttachmentRoute(report.reportID, imageSource); + Navigation.navigate(route); + }} + accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} + accessibilityLabel={translate('accessibilityHints.viewAttachment')} + > + {receiptImageComponent} + + )} + + ); + } + + return receiptImageComponent; } ReportActionItemImage.propTypes = propTypes; diff --git a/src/components/ReportActionItem/ReportActionItemImages.js b/src/components/ReportActionItem/ReportActionItemImages.js index e9fed1ec289c..82082b18ce1c 100644 --- a/src/components/ReportActionItem/ReportActionItemImages.js +++ b/src/components/ReportActionItem/ReportActionItemImages.js @@ -54,10 +54,14 @@ function ReportActionItemImages({images, size, total, isHovered}) { {_.map(shownImages, ({thumbnail, image}, index) => { const isLastImage = index === numberOfShownImages - 1; + + // Show a border to separate multiple images. Shown to the right for each except the last. + const shouldShowBorder = shownImages.length > 1 && index < shownImages.length - 1; + const borderStyle = shouldShowBorder ? styles.reportActionItemImageBorder : {}; return ( + ReceiptUtils.getThumbnailAndImageURIs(receipt.source, filename || receiptFilename || ''), + ); const hasOnlyOneReceiptRequest = numberOfRequests === 1 && hasReceipts; const previewSubtitle = hasOnlyOneReceiptRequest @@ -181,7 +184,7 @@ function ReportPreview(props) { {hasReceipts && ( ReceiptUtils.getThumbnailAndImageURIs(receipt.source, filename || ''))} + images={lastThreeReceipts} size={3} total={transactionsWithReceipts.length} isHovered={props.isHovered || isScanning} diff --git a/src/languages/en.js b/src/languages/en.ts similarity index 88% rename from src/languages/en.js rename to src/languages/en.ts index 364029a81ece..af7957e1a560 100755 --- a/src/languages/en.js +++ b/src/languages/en.ts @@ -1,5 +1,74 @@ import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; import CONST from '../CONST'; +import type { + AddressLineParams, + CharacterLimitParams, + MaxParticipantsReachedParams, + ZipCodeExampleFormatParams, + LoggedInAsParams, + NewFaceEnterMagicCodeParams, + WelcomeEnterMagicCodeParams, + AlreadySignedInParams, + GoBackMessageParams, + LocalTimeParams, + EditActionParams, + DeleteActionParams, + DeleteConfirmationParams, + BeginningOfChatHistoryDomainRoomPartOneParams, + BeginningOfChatHistoryAdminRoomPartOneParams, + BeginningOfChatHistoryAnnounceRoomPartOneParams, + BeginningOfChatHistoryAnnounceRoomPartTwo, + WelcomeToRoomParams, + ReportArchiveReasonsClosedParams, + ReportArchiveReasonsMergedParams, + ReportArchiveReasonsRemovedFromPolicyParams, + ReportArchiveReasonsPolicyDeletedParams, + RequestCountParams, + SettleExpensifyCardParams, + SettlePaypalMeParams, + RequestAmountParams, + SplitAmountParams, + AmountEachParams, + PayerOwesAmountParams, + PayerOwesParams, + PayerPaidAmountParams, + PayerPaidParams, + PayerSettledParams, + WaitingOnBankAccountParams, + SettledAfterAddedBankAccountParams, + PaidElsewhereWithAmountParams, + PaidUsingPaypalWithAmountParams, + PaidUsingExpensifyWithAmountParams, + ThreadRequestReportNameParams, + ThreadSentMoneyReportNameParams, + SizeExceededParams, + ResolutionConstraintsParams, + NotAllowedExtensionParams, + EnterMagicCodeParams, + TransferParams, + InstantSummaryParams, + NotYouParams, + DateShouldBeBeforeParams, + DateShouldBeAfterParams, + IncorrectZipFormatParams, + WeSentYouMagicSignInLinkParams, + ToValidateLoginParams, + NoLongerHaveAccessParams, + OurEmailProviderParams, + ConfirmThatParams, + UntilTimeParams, + StepCounterParams, + UserIsAlreadyMemberOfWorkspaceParams, + GoToRoomParams, + WelcomeNoteParams, + RoomNameReservedErrorParams, + RenamedRoomActionParams, + RoomRenamedToParams, + OOOEventSummaryFullDayParams, + OOOEventSummaryPartialDayParams, + ParentNavigationSummaryParams, + ManagerApprovedParams, +} from './types'; import * as ReportActionsUtils from '../libs/ReportActionsUtils'; /* eslint-disable max-len */ @@ -73,7 +142,7 @@ export default { currentMonth: 'Current month', ssnLast4: 'Last 4 digits of SSN', ssnFull9: 'Full 9 digits of SSN', - addressLine: ({lineNumber}) => `Address line ${lineNumber}`, + addressLine: ({lineNumber}: AddressLineParams) => `Address line ${lineNumber}`, personalAddress: 'Personal address', companyAddress: 'Company address', noPO: 'PO boxes and mail drop addresses are not allowed', @@ -104,7 +173,7 @@ export default { acceptTerms: 'You must accept the Terms of Service to continue', phoneNumber: `Please enter a valid phone number, with the country code (e.g. ${CONST.EXAMPLE_PHONE_NUMBER})`, fieldRequired: 'This field is required.', - characterLimit: ({limit}) => `Exceeds the maximum length of ${limit} characters`, + characterLimit: ({limit}: CharacterLimitParams) => `Exceeds the maximum length of ${limit} characters`, dateInvalid: 'Please select a valid date', invalidCharacter: 'Invalid character', enterMerchant: 'Enter a merchant name', @@ -137,14 +206,14 @@ export default { youAfterPreposition: 'you', your: 'your', conciergeHelp: 'Please reach out to Concierge for help.', - maxParticipantsReached: ({count}) => `You've selected the maximum number (${count}) of participants.`, + maxParticipantsReached: ({count}: MaxParticipantsReachedParams) => `You've selected the maximum number (${count}) of participants.`, youAppearToBeOffline: 'You appear to be offline.', thisFeatureRequiresInternet: 'This feature requires an active internet connection to be used.', areYouSure: 'Are you sure?', verify: 'Verify', yesContinue: 'Yes, continue', websiteExample: 'e.g. https://www.expensify.com', - zipCodeExampleFormat: ({zipSampleFormat}) => (zipSampleFormat ? `e.g. ${zipSampleFormat}` : ''), + zipCodeExampleFormat: ({zipSampleFormat}: ZipCodeExampleFormatParams) => (zipSampleFormat ? `e.g. ${zipSampleFormat}` : ''), description: 'Description', with: 'with', shareCode: 'Share code', @@ -210,7 +279,7 @@ export default { redirectedToDesktopApp: "We've redirected you to the desktop app.", youCanAlso: 'You can also', openLinkInBrowser: 'open this link in your browser', - loggedInAs: ({email}) => `You're logged in as ${email}. Click "Open link" in the prompt to log into the desktop app with this account.`, + loggedInAs: ({email}: LoggedInAsParams) => `You're logged in as ${email}. Click "Open link" in the prompt to log into the desktop app with this account.`, doNotSeePrompt: "Can't see the prompt?", tryAgain: 'Try again', or: ', or', @@ -256,8 +325,9 @@ export default { phrase2: "Money talks. And now that chat and payments are in one place, it's also easy.", phrase3: 'Your payments get to you as fast as you can get your point across.', enterPassword: 'Please enter your password', - newFaceEnterMagicCode: ({login}) => `It's always great to see a new face around here! Please enter the magic code sent to ${login}. It should arrive within a minute or two.`, - welcomeEnterMagicCode: ({login}) => `Please enter the magic code sent to ${login}. It should arrive within a minute or two.`, + newFaceEnterMagicCode: ({login}: NewFaceEnterMagicCodeParams) => + `It's always great to see a new face around here! Please enter the magic code sent to ${login}. It should arrive within a minute or two.`, + welcomeEnterMagicCode: ({login}: WelcomeEnterMagicCodeParams) => `Please enter the magic code sent to ${login}. It should arrive within a minute or two.`, }, DownloadAppModal: { downloadTheApp: 'Download the app', @@ -271,8 +341,8 @@ export default { }, }, thirdPartySignIn: { - alreadySignedIn: ({email}) => `You are already signed in as ${email}.`, - goBackMessage: ({provider}) => `Don't want to sign in with ${provider}?`, + alreadySignedIn: ({email}: AlreadySignedInParams) => `You are already signed in as ${email}.`, + goBackMessage: ({provider}: GoBackMessageParams) => `Don't want to sign in with ${provider}?`, continueWithMyCurrentSession: 'Continue with my current session', redirectToDesktopMessage: "We'll redirect you to the desktop app once you finish signing in.", signInAgreementMessage: 'By logging in, you agree to the', @@ -297,7 +367,7 @@ export default { ], blockedFromConcierge: 'Communication is barred', fileUploadFailed: 'Upload failed. File is not supported.', - localTime: ({user, time}) => `It's ${time} for ${user}`, + localTime: ({user, time}: LocalTimeParams) => `It's ${time} for ${user}`, edited: '(edited)', emoji: 'Emoji', collapse: 'Collapse', @@ -311,9 +381,9 @@ export default { copyEmailToClipboard: 'Copy email to clipboard', markAsUnread: 'Mark as unread', markAsRead: 'Mark as read', - editAction: ({action}) => `Edit ${ReportActionsUtils.isMoneyRequestAction(action) ? 'request' : 'comment'}`, - deleteAction: ({action}) => `Delete ${ReportActionsUtils.isMoneyRequestAction(action) ? 'request' : 'comment'}`, - deleteConfirmation: ({action}) => `Are you sure you want to delete this ${ReportActionsUtils.isMoneyRequestAction(action) ? 'request' : 'comment'}?`, + editAction: ({action}: EditActionParams) => `Edit ${ReportActionsUtils.isMoneyRequestAction(action) ? 'request' : 'comment'}`, + deleteAction: ({action}: DeleteActionParams) => `Delete ${ReportActionsUtils.isMoneyRequestAction(action) ? 'request' : 'comment'}`, + deleteConfirmation: ({action}: DeleteConfirmationParams) => `Are you sure you want to delete this ${ReportActionsUtils.isMoneyRequestAction(action) ? 'request' : 'comment'}?`, onlyVisible: 'Only visible to', replyInThread: 'Reply in thread', flagAsOffensive: 'Flag as offensive', @@ -325,13 +395,14 @@ export default { reportActionsView: { beginningOfArchivedRoomPartOne: 'You missed the party in ', beginningOfArchivedRoomPartTwo: ", there's nothing to see here.", - beginningOfChatHistoryDomainRoomPartOne: ({domainRoom}) => `Collaboration with everyone at ${domainRoom} starts here! 🎉\nUse `, + beginningOfChatHistoryDomainRoomPartOne: ({domainRoom}: BeginningOfChatHistoryDomainRoomPartOneParams) => `Collaboration with everyone at ${domainRoom} starts here! 🎉\nUse `, beginningOfChatHistoryDomainRoomPartTwo: ' to chat with colleagues, share tips, and ask questions.', - beginningOfChatHistoryAdminRoomPartOne: ({workspaceName}) => `Collaboration among ${workspaceName} admins starts here! 🎉\nUse `, + beginningOfChatHistoryAdminRoomPartOne: ({workspaceName}: BeginningOfChatHistoryAdminRoomPartOneParams) => `Collaboration among ${workspaceName} admins starts here! 🎉\nUse `, beginningOfChatHistoryAdminRoomPartTwo: ' to chat about topics such as workspace configurations and more.', beginningOfChatHistoryAdminOnlyPostingRoom: 'Only admins can send messages in this room.', - beginningOfChatHistoryAnnounceRoomPartOne: ({workspaceName}) => `Collaboration between all ${workspaceName} members starts here! 🎉\nUse `, - beginningOfChatHistoryAnnounceRoomPartTwo: ({workspaceName}) => ` to chat about anything ${workspaceName} related.`, + beginningOfChatHistoryAnnounceRoomPartOne: ({workspaceName}: BeginningOfChatHistoryAnnounceRoomPartOneParams) => + `Collaboration between all ${workspaceName} members starts here! 🎉\nUse `, + beginningOfChatHistoryAnnounceRoomPartTwo: ({workspaceName}: BeginningOfChatHistoryAnnounceRoomPartTwo) => ` to chat about anything ${workspaceName} related.`, beginningOfChatHistoryUserRoomPartOne: 'Collaboration starts here! 🎉\nUse this space to chat about anything ', beginningOfChatHistoryUserRoomPartTwo: ' related.', beginningOfChatHistory: 'This is the beginning of your chat with ', @@ -340,7 +411,7 @@ export default { beginningOfChatHistoryPolicyExpenseChatPartThree: ' starts here! 🎉 This is the place to chat, request money and settle up.', chatWithAccountManager: 'Chat with your account manager here', sayHello: 'Say hello!', - welcomeToRoom: ({roomName}) => `Welcome to ${roomName}!`, + welcomeToRoom: ({roomName}: WelcomeToRoomParams) => `Welcome to ${roomName}!`, usePlusButton: '\n\nYou can also use the + button below to request money or assign a task!', }, reportAction: { @@ -357,12 +428,14 @@ export default { }, reportArchiveReasons: { [CONST.REPORT.ARCHIVE_REASON.DEFAULT]: 'This chat room has been archived.', - [CONST.REPORT.ARCHIVE_REASON.ACCOUNT_CLOSED]: ({displayName}) => `This workspace chat is no longer active because ${displayName} closed their account.`, - [CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED]: ({displayName, oldDisplayName}) => + [CONST.REPORT.ARCHIVE_REASON.ACCOUNT_CLOSED]: ({displayName}: ReportArchiveReasonsClosedParams) => + `This workspace chat is no longer active because ${displayName} closed their account.`, + [CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED]: ({displayName, oldDisplayName}: ReportArchiveReasonsMergedParams) => `This workspace chat is no longer active because ${oldDisplayName} has merged their account with ${displayName}.`, - [CONST.REPORT.ARCHIVE_REASON.REMOVED_FROM_POLICY]: ({displayName, policyName}) => + [CONST.REPORT.ARCHIVE_REASON.REMOVED_FROM_POLICY]: ({displayName, policyName}: ReportArchiveReasonsRemovedFromPolicyParams) => `This workspace chat is no longer active because ${displayName} is no longer a member of the ${policyName} workspace.`, - [CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED]: ({policyName}) => `This workspace chat is no longer active because ${policyName} is no longer an active workspace.`, + [CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED]: ({policyName}: ReportArchiveReasonsPolicyDeletedParams) => + `This workspace chat is no longer active because ${policyName} is no longer an active workspace.`, }, writeCapabilityPage: { label: 'Who can post', @@ -424,33 +497,34 @@ export default { receiptScanning: 'Receipt scan in progress…', receiptStatusTitle: 'Scanning…', receiptStatusText: "Only you can see this receipt when it's scanning. Check back later or enter the details now.", - requestCount: ({count, scanningReceipts = 0}) => `${count} requests${scanningReceipts > 0 ? `, ${scanningReceipts} scanning` : ''}`, + requestCount: ({count, scanningReceipts = 0}: RequestCountParams) => `${count} requests${scanningReceipts > 0 ? `, ${scanningReceipts} scanning` : ''}`, deleteRequest: 'Delete request', deleteConfirmation: 'Are you sure that you want to delete this request?', settledExpensify: 'Paid', settledElsewhere: 'Paid elsewhere', settledPaypalMe: 'Paid using Paypal.me', - settleExpensify: ({formattedAmount}) => `Pay ${formattedAmount} with Expensify`, + settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => `Pay ${formattedAmount} with Expensify`, payElsewhere: 'Pay elsewhere', - settlePaypalMe: ({formattedAmount}) => `Pay ${formattedAmount} with PayPal.me`, - requestAmount: ({amount}) => `request ${amount}`, - splitAmount: ({amount}) => `split ${amount}`, - amountEach: ({amount}) => `${amount} each`, - payerOwesAmount: ({payer, amount}) => `${payer} owes ${amount}`, - payerOwes: ({payer}) => `${payer} owes: `, - payerPaidAmount: ({payer, amount}) => `${payer} paid ${amount}`, - payerPaid: ({payer}) => `${payer} paid: `, - managerApproved: ({manager}) => `${manager} approved:`, - payerSettled: ({amount}) => `paid ${amount}`, - waitingOnBankAccount: ({submitterDisplayName}) => `started settling up, payment is held until ${submitterDisplayName} adds a bank account`, - settledAfterAddedBankAccount: ({submitterDisplayName, amount}) => `${submitterDisplayName} added a bank account. The ${amount} payment has been made.`, - paidElsewhereWithAmount: ({amount}) => `paid ${amount} elsewhere`, - paidUsingPaypalWithAmount: ({amount}) => `paid ${amount} using Paypal.me`, - paidUsingExpensifyWithAmount: ({amount}) => `paid ${amount} using Expensify`, + settlePaypalMe: ({formattedAmount}: SettlePaypalMeParams) => `Pay ${formattedAmount} with PayPal.me`, + requestAmount: ({amount}: RequestAmountParams) => `request ${amount}`, + splitAmount: ({amount}: SplitAmountParams) => `split ${amount}`, + amountEach: ({amount}: AmountEachParams) => `${amount} each`, + payerOwesAmount: ({payer, amount}: PayerOwesAmountParams) => `${payer} owes ${amount}`, + payerOwes: ({payer}: PayerOwesParams) => `${payer} owes: `, + payerPaidAmount: ({payer, amount}: PayerPaidAmountParams): string => `${payer} paid ${amount}`, + payerPaid: ({payer}: PayerPaidParams) => `${payer} paid: `, + managerApproved: ({manager}: ManagerApprovedParams) => `${manager} approved:`, + payerSettled: ({amount}: PayerSettledParams) => `paid ${amount}`, + waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `started settling up, payment is held until ${submitterDisplayName} adds a bank account`, + settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) => + `${submitterDisplayName} added a bank account. The ${amount} payment has been made.`, + paidElsewhereWithAmount: ({amount}: PaidElsewhereWithAmountParams) => `paid ${amount} elsewhere`, + paidUsingPaypalWithAmount: ({amount}: PaidUsingPaypalWithAmountParams) => `paid ${amount} using Paypal.me`, + paidUsingExpensifyWithAmount: ({amount}: PaidUsingExpensifyWithAmountParams) => `paid ${amount} using Expensify`, noReimbursableExpenses: 'This report has an invalid amount', pendingConversionMessage: "Total will update when you're back online", - threadRequestReportName: ({formattedAmount, comment}) => `${formattedAmount} request${comment ? ` for ${comment}` : ''}`, - threadSentMoneyReportName: ({formattedAmount, comment}) => `${formattedAmount} sent${comment ? ` for ${comment}` : ''}`, + threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} request${comment ? ` for ${comment}` : ''}`, + threadSentMoneyReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} sent${comment ? ` for ${comment}` : ''}`, error: { invalidSplit: 'Split amounts do not equal total amount', other: 'Unexpected error, please try again later', @@ -478,10 +552,10 @@ export default { removePhoto: 'Remove photo', editImage: 'Edit photo', deleteWorkspaceError: 'Sorry, there was an unexpected problem deleting your workspace avatar.', - sizeExceeded: ({maxUploadSizeInMB}) => `The selected image exceeds the maximum upload size of ${maxUploadSizeInMB}MB.`, - resolutionConstraints: ({minHeightInPx, minWidthInPx, maxHeightInPx, maxWidthInPx}) => + sizeExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `The selected image exceeds the maximum upload size of ${maxUploadSizeInMB}MB.`, + resolutionConstraints: ({minHeightInPx, minWidthInPx, maxHeightInPx, maxWidthInPx}: ResolutionConstraintsParams) => `Please upload an image larger than ${minHeightInPx}x${minWidthInPx} pixels and smaller than ${maxHeightInPx}x${maxWidthInPx} pixels.`, - notAllowedExtension: ({allowedExtensions}) => `Profile picture must be one of the following types: ${allowedExtensions.join(', ')}.`, + notAllowedExtension: ({allowedExtensions}: NotAllowedExtensionParams) => `Profile picture must be one of the following types: ${allowedExtensions.join(', ')}.`, }, profilePage: { profile: 'Profile', @@ -517,7 +591,7 @@ export default { helpTextAfterEmail: ' from multiple email addresses.', pleaseVerify: 'Please verify this contact method', getInTouch: "Whenever we need to get in touch with you, we'll use this contact method.", - enterMagicCode: ({contactMethod}) => `Please enter the magic code sent to ${contactMethod}`, + enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Please enter the magic code sent to ${contactMethod}`, setAsDefault: 'Set as default', yourDefaultContactMethod: 'This is your current default contact method. You will not be able to delete this contact method until you set an alternative default by selecting another contact method and pressing “Set as default”.', @@ -536,6 +610,7 @@ export default { invalidContactMethod: 'Invalid contact method', }, newContactMethod: 'New contact method', + goBackContactMethods: 'Go back to contact methods', }, pronouns: { coCos: 'Co / Cos', @@ -708,9 +783,9 @@ export default { addBankAccountFailure: 'An unexpected error occurred while trying to add your bank account. Please try again.', }, transferAmountPage: { - transfer: ({amount}) => `Transfer${amount ? ` ${amount}` : ''}`, + transfer: ({amount}: TransferParams) => `Transfer${amount ? ` ${amount}` : ''}`, instant: 'Instant (Debit card)', - instantSummary: ({rate, minAmount}) => `${rate}% fee (${minAmount} minimum)`, + instantSummary: ({rate, minAmount}: InstantSummaryParams) => `${rate}% fee (${minAmount} minimum)`, ach: '1-3 Business days (Bank account)', achSummary: 'No fee', whichAccount: 'Which account?', @@ -841,7 +916,7 @@ export default { }, cannotGetAccountDetails: "Couldn't retrieve account details, please try to sign in again.", loginForm: 'Login form', - notYou: ({user}) => `Not ${user}?`, + notYou: ({user}: NotYouParams) => `Not ${user}?`, }, personalDetails: { error: { @@ -857,27 +932,29 @@ export default { legalLastName: 'Legal last name', homeAddress: 'Home address', error: { - dateShouldBeBefore: ({dateString}) => `Date should be before ${dateString}.`, - dateShouldBeAfter: ({dateString}) => `Date should be after ${dateString}.`, + dateShouldBeBefore: ({dateString}: DateShouldBeBeforeParams) => `Date should be before ${dateString}.`, + dateShouldBeAfter: ({dateString}: DateShouldBeAfterParams) => `Date should be after ${dateString}.`, hasInvalidCharacter: 'Name can only include letters.', - incorrectZipFormat: ({zipFormat}) => `Incorrect zip code format.${zipFormat ? ` Acceptable format: ${zipFormat}` : ''}`, + incorrectZipFormat: ({zipFormat}: IncorrectZipFormatParams) => `Incorrect zip code format.${zipFormat ? ` Acceptable format: ${zipFormat}` : ''}`, }, }, resendValidationForm: { linkHasBeenResent: 'Link has been re-sent', - weSentYouMagicSignInLink: ({login, loginType}) => `I've sent a magic sign-in link to ${login}. Please check your ${loginType} to sign in.`, + weSentYouMagicSignInLink: ({login, loginType}: WeSentYouMagicSignInLinkParams) => `I've sent a magic sign-in link to ${login}. Please check your ${loginType} to sign in.`, resendLink: 'Resend link', }, unlinkLoginForm: { - toValidateLogin: ({primaryLogin, secondaryLogin}) => `To validate ${secondaryLogin}, please resend the magic code from the Account Settings of ${primaryLogin}.`, - noLongerHaveAccess: ({primaryLogin}) => `If you no longer have access to ${primaryLogin}, please unlink your accounts.`, + toValidateLogin: ({primaryLogin, secondaryLogin}: ToValidateLoginParams) => + `To validate ${secondaryLogin}, please resend the magic code from the Account Settings of ${primaryLogin}.`, + noLongerHaveAccess: ({primaryLogin}: NoLongerHaveAccessParams) => `If you no longer have access to ${primaryLogin}, please unlink your accounts.`, unlink: 'Unlink', linkSent: 'Link sent!', succesfullyUnlinkedLogin: 'Secondary login successfully unlinked!', }, emailDeliveryFailurePage: { - ourEmailProvider: ({login}) => `Our email provider has temporarily suspended emails to ${login} due to delivery issues. To unblock your login, please follow these steps:`, - confirmThat: ({login}) => `Confirm that ${login} is spelled correctly and is a real, deliverable email address. `, + ourEmailProvider: (user: OurEmailProviderParams) => + `Our email provider has temporarily suspended emails to ${user.login} due to delivery issues. To unblock your login, please follow these steps:`, + confirmThat: ({login}: ConfirmThatParams) => `Confirm that ${login} is spelled correctly and is a real, deliverable email address. `, emailAliases: 'Email aliases such as "expenses@domain.com" must have access to their own email inbox for it to be a valid Expensify login.', ensureYourEmailClient: 'Ensure your email client allows expensify.com emails. ', youCanFindDirections: 'You can find directions on how to complete this step ', @@ -922,9 +999,9 @@ export default { save: 'Save', message: 'Message', untilTomorrow: 'Until tomorrow', - untilTime: ({time}) => `Until ${time}`, + untilTime: ({time}: UntilTimeParams) => `Until ${time}`, }, - stepCounter: ({step, total, text}) => { + stepCounter: ({step, total, text}: StepCounterParams) => { let result = `Step ${step}`; if (total) { @@ -1007,7 +1084,7 @@ export default { messages: { errorMessageInvalidPhone: `Please enter a valid phone number without brackets or dashes. If you're outside the US please include your country code (e.g. ${CONST.EXAMPLE_PHONE_NUMBER}).`, errorMessageInvalidEmail: 'Invalid email', - userIsAlreadyMemberOfWorkspace: ({login, workspace}) => `${login} is already a member of ${workspace}`, + userIsAlreadyMemberOfWorkspace: ({login, workspace}: UserIsAlreadyMemberOfWorkspaceParams) => `${login} is already a member of ${workspace}`, }, onfidoStep: { acceptTerms: 'By continuing with the request to activate your Expensify wallet, you confirm that you have read, understand and accept ', @@ -1212,7 +1289,7 @@ export default { unavailable: 'Unavailable workspace', memberNotFound: 'Member not found. To invite a new member to the workspace, please use the Invite button above.', notAuthorized: `You do not have access to this page. Are you trying to join the workspace? Please reach out to the owner of this workspace so they can add you as a member! Something else? Reach out to ${CONST.EMAIL.CONCIERGE}`, - goToRoom: ({roomName}) => `Go to ${roomName} room`, + goToRoom: ({roomName}: GoToRoomParams) => `Go to ${roomName} room`, }, emptyWorkspace: { title: 'Create a new workspace', @@ -1308,7 +1385,7 @@ export default { personalMessagePrompt: 'Message', genericFailureMessage: 'An error occurred inviting the user to the workspace, please try again.', inviteNoMembersError: 'Please select at least one member to invite', - welcomeNote: ({workspaceName}) => + welcomeNote: ({workspaceName}: WelcomeNoteParams) => `You have been invited to ${workspaceName || 'a workspace'}! Download the Expensify mobile app at use.expensify.com/download to start tracking your expenses.`, }, editor: { @@ -1381,15 +1458,16 @@ export default { restrictedDescription: 'People in your workspace can find this room', privateDescription: 'People invited to this room can find it', publicDescription: 'Anyone can find this room', + // eslint-disable-next-line @typescript-eslint/naming-convention public_announceDescription: 'Anyone can find this room', createRoom: 'Create room', roomAlreadyExistsError: 'A room with this name already exists', - roomNameReservedError: ({reservedName}) => `${reservedName} is a default room on all workspaces. Please choose another name.`, + roomNameReservedError: ({reservedName}: RoomNameReservedErrorParams) => `${reservedName} is a default room on all workspaces. Please choose another name.`, roomNameInvalidError: 'Room names can only include lowercase letters, numbers and hyphens', pleaseEnterRoomName: 'Please enter a room name', pleaseSelectWorkspace: 'Please select a workspace', - renamedRoomAction: ({oldName, newName}) => ` renamed this room from ${oldName} to ${newName}`, - roomRenamedTo: ({newName}) => `Room renamed to ${newName}`, + renamedRoomAction: ({oldName, newName}: RenamedRoomActionParams) => ` renamed this room from ${oldName} to ${newName}`, + roomRenamedTo: ({newName}: RoomRenamedToParams) => `Room renamed to ${newName}`, social: 'social', selectAWorkspace: 'Select a workspace', growlMessageOnRenameError: 'Unable to rename policy room, please check your connection and try again.', @@ -1397,6 +1475,7 @@ export default { restricted: 'Restricted', private: 'Private', public: 'Public', + // eslint-disable-next-line @typescript-eslint/naming-convention public_announce: 'Public Announce', }, }, @@ -1540,8 +1619,8 @@ export default { noActivityYet: 'No activity yet', }, chronos: { - oooEventSummaryFullDay: ({summary, dayCount, date}) => `${summary} for ${dayCount} ${dayCount === 1 ? 'day' : 'days'} until ${date}`, - oooEventSummaryPartialDay: ({summary, timePeriod, date}) => `${summary} from ${timePeriod} on ${date}`, + oooEventSummaryFullDay: ({summary, dayCount, date}: OOOEventSummaryFullDayParams) => `${summary} for ${dayCount} ${dayCount === 1 ? 'day' : 'days'} until ${date}`, + oooEventSummaryPartialDay: ({summary, timePeriod, date}: OOOEventSummaryPartialDayParams) => `${summary} from ${timePeriod} on ${date}`, }, footer: { features: 'Features', @@ -1597,7 +1676,7 @@ export default { reply: 'Reply', from: 'From', in: 'In', - parentNavigationSummary: ({rootReportName, workspaceName}) => `From ${rootReportName}${workspaceName ? ` in ${workspaceName}` : ''}`, + parentNavigationSummary: ({rootReportName, workspaceName}: ParentNavigationSummaryParams) => `From ${rootReportName}${workspaceName ? ` in ${workspaceName}` : ''}`, }, qrCodes: { copyUrlToClipboard: 'Copy URL to clipboard', @@ -1677,4 +1756,4 @@ export default { heroBody: 'Use New Expensify for event updates, networking, social chatter, and to get paid back for your ride to or from the show!', }, }, -}; +} as const; diff --git a/src/languages/es-ES.js b/src/languages/es-ES.ts similarity index 100% rename from src/languages/es-ES.js rename to src/languages/es-ES.ts diff --git a/src/languages/es.js b/src/languages/es.ts similarity index 90% rename from src/languages/es.js rename to src/languages/es.ts index 2e7ae7dd09eb..f950733b005c 100644 --- a/src/languages/es.js +++ b/src/languages/es.ts @@ -1,5 +1,74 @@ import CONST from '../CONST'; import * as ReportActionsUtils from '../libs/ReportActionsUtils'; +import type { + AddressLineParams, + CharacterLimitParams, + MaxParticipantsReachedParams, + ZipCodeExampleFormatParams, + LoggedInAsParams, + NewFaceEnterMagicCodeParams, + WelcomeEnterMagicCodeParams, + AlreadySignedInParams, + GoBackMessageParams, + LocalTimeParams, + EditActionParams, + DeleteActionParams, + DeleteConfirmationParams, + BeginningOfChatHistoryDomainRoomPartOneParams, + BeginningOfChatHistoryAdminRoomPartOneParams, + BeginningOfChatHistoryAnnounceRoomPartOneParams, + BeginningOfChatHistoryAnnounceRoomPartTwo, + WelcomeToRoomParams, + ReportArchiveReasonsClosedParams, + ReportArchiveReasonsMergedParams, + ReportArchiveReasonsRemovedFromPolicyParams, + ReportArchiveReasonsPolicyDeletedParams, + RequestCountParams, + SettleExpensifyCardParams, + SettlePaypalMeParams, + RequestAmountParams, + SplitAmountParams, + AmountEachParams, + PayerOwesAmountParams, + PayerOwesParams, + PayerPaidAmountParams, + PayerPaidParams, + PayerSettledParams, + WaitingOnBankAccountParams, + SettledAfterAddedBankAccountParams, + PaidElsewhereWithAmountParams, + PaidUsingPaypalWithAmountParams, + PaidUsingExpensifyWithAmountParams, + ThreadRequestReportNameParams, + ThreadSentMoneyReportNameParams, + SizeExceededParams, + ResolutionConstraintsParams, + NotAllowedExtensionParams, + EnterMagicCodeParams, + TransferParams, + InstantSummaryParams, + NotYouParams, + DateShouldBeBeforeParams, + DateShouldBeAfterParams, + IncorrectZipFormatParams, + WeSentYouMagicSignInLinkParams, + ToValidateLoginParams, + NoLongerHaveAccessParams, + OurEmailProviderParams, + ConfirmThatParams, + UntilTimeParams, + StepCounterParams, + UserIsAlreadyMemberOfWorkspaceParams, + GoToRoomParams, + WelcomeNoteParams, + RoomNameReservedErrorParams, + RenamedRoomActionParams, + RoomRenamedToParams, + OOOEventSummaryFullDayParams, + OOOEventSummaryPartialDayParams, + ParentNavigationSummaryParams, + ManagerApprovedParams, +} from './types'; /* eslint-disable max-len */ export default { @@ -72,7 +141,7 @@ export default { currentMonth: 'Mes actual', ssnLast4: 'Últimos 4 dígitos de su SSN', ssnFull9: 'Los 9 dígitos del SSN', - addressLine: ({lineNumber}) => `Dirección línea ${lineNumber}`, + addressLine: ({lineNumber}: AddressLineParams) => `Dirección línea ${lineNumber}`, personalAddress: 'Dirección física personal', companyAddress: 'Dirección física de la empresa', noPO: 'No se aceptan apartados ni direcciones postales', @@ -103,7 +172,7 @@ export default { acceptTerms: 'Debes aceptar los Términos de Servicio para continuar', phoneNumber: `Introduce un teléfono válido, incluyendo el código del país (p. ej. ${CONST.EXAMPLE_PHONE_NUMBER})`, fieldRequired: 'Este campo es obligatorio.', - characterLimit: ({limit}) => `Supera el límite de ${limit} caracteres`, + characterLimit: ({limit}: CharacterLimitParams) => `Supera el límite de ${limit} caracteres`, dateInvalid: 'Por favor, selecciona una fecha válida', invalidCharacter: 'Carácter invalido', enterMerchant: 'Introduce un comerciante', @@ -136,14 +205,14 @@ export default { youAfterPreposition: 'ti', your: 'tu', conciergeHelp: 'Por favor, contacta con Concierge para obtener ayuda.', - maxParticipantsReached: ({count}) => `Has seleccionado el número máximo (${count}) de participantes.`, + maxParticipantsReached: ({count}: MaxParticipantsReachedParams) => `Has seleccionado el número máximo (${count}) de participantes.`, youAppearToBeOffline: 'Parece que estás desconectado.', thisFeatureRequiresInternet: 'Esta función requiere una conexión a Internet activa para ser utilizada.', areYouSure: '¿Estás seguro?', verify: 'Verifique', yesContinue: 'Sí, continuar', websiteExample: 'p. ej. https://www.expensify.com', - zipCodeExampleFormat: ({zipSampleFormat}) => (zipSampleFormat ? `p. ej. ${zipSampleFormat}` : ''), + zipCodeExampleFormat: ({zipSampleFormat}: ZipCodeExampleFormatParams) => (zipSampleFormat ? `p. ej. ${zipSampleFormat}` : ''), description: 'Descripción', with: 'con', shareCode: 'Compartir código', @@ -209,7 +278,8 @@ export default { redirectedToDesktopApp: 'Te hemos redirigido a la aplicación de escritorio.', youCanAlso: 'También puedes', openLinkInBrowser: 'abrir este enlace en tu navegador', - loggedInAs: ({email}) => `Has iniciado sesión como ${email}. Haga clic en "Abrir enlace" en el aviso para iniciar sesión en la aplicación de escritorio con esta cuenta.`, + loggedInAs: ({email}: LoggedInAsParams) => + `Has iniciado sesión como ${email}. Haga clic en "Abrir enlace" en el aviso para iniciar sesión en la aplicación de escritorio con esta cuenta.`, doNotSeePrompt: '¿No ves el aviso?', tryAgain: 'Inténtalo de nuevo', or: ', o', @@ -255,8 +325,9 @@ export default { phrase2: 'El dinero habla. Y ahora que chat y pagos están en un mismo lugar, es también fácil.', phrase3: 'Tus pagos llegan tan rápido como tus mensajes.', enterPassword: 'Por favor, introduce tu contraseña', - newFaceEnterMagicCode: ({login}) => `¡Siempre es genial ver una cara nueva por aquí! Por favor ingresa el código mágico enviado a ${login}. Debería llegar en un par de minutos.`, - welcomeEnterMagicCode: ({login}) => `Por favor, introduce el código mágico enviado a ${login}. Debería llegar en un par de minutos.`, + newFaceEnterMagicCode: ({login}: NewFaceEnterMagicCodeParams) => + `¡Siempre es genial ver una cara nueva por aquí! Por favor ingresa el código mágico enviado a ${login}. Debería llegar en un par de minutos.`, + welcomeEnterMagicCode: ({login}: WelcomeEnterMagicCodeParams) => `Por favor, introduce el código mágico enviado a ${login}. Debería llegar en un par de minutos.`, }, DownloadAppModal: { downloadTheApp: 'Descarga la aplicación', @@ -270,8 +341,8 @@ export default { }, }, thirdPartySignIn: { - alreadySignedIn: ({email}) => `Ya has iniciado sesión con ${email}.`, - goBackMessage: ({provider}) => `No quieres iniciar sesión con ${provider}?`, + alreadySignedIn: ({email}: AlreadySignedInParams) => `Ya has iniciado sesión con ${email}.`, + goBackMessage: ({provider}: GoBackMessageParams) => `No quieres iniciar sesión con ${provider}?`, continueWithMyCurrentSession: 'Continuar con mi sesión actual', redirectToDesktopMessage: 'Lo redirigiremos a la aplicación de escritorio una vez que termine de iniciar sesión.', signInAgreementMessage: 'Al iniciar sesión, aceptas las', @@ -296,7 +367,7 @@ export default { ], blockedFromConcierge: 'Comunicación no permitida', fileUploadFailed: 'Subida fallida. El archivo no es compatible.', - localTime: ({user, time}) => `Son las ${time} para ${user}`, + localTime: ({user, time}: LocalTimeParams) => `Son las ${time} para ${user}`, edited: '(editado)', emoji: 'Emoji', collapse: 'Colapsar', @@ -310,9 +381,9 @@ export default { copyEmailToClipboard: 'Copiar email al portapapeles', markAsUnread: 'Marcar como no leído', markAsRead: 'Marcar como leído', - editAction: ({action}) => `Edit ${ReportActionsUtils.isMoneyRequestAction(action) ? 'pedido' : 'comentario'}`, - deleteAction: ({action}) => `Eliminar ${ReportActionsUtils.isMoneyRequestAction(action) ? 'pedido' : 'comentario'}`, - deleteConfirmation: ({action}) => `¿Estás seguro de que quieres eliminar este ${ReportActionsUtils.isMoneyRequestAction(action) ? 'pedido' : 'comentario'}`, + editAction: ({action}: EditActionParams) => `Edit ${ReportActionsUtils.isMoneyRequestAction(action) ? 'pedido' : 'comentario'}`, + deleteAction: ({action}: DeleteActionParams) => `Eliminar ${ReportActionsUtils.isMoneyRequestAction(action) ? 'pedido' : 'comentario'}`, + deleteConfirmation: ({action}: DeleteConfirmationParams) => `¿Estás seguro de que quieres eliminar este ${ReportActionsUtils.isMoneyRequestAction(action) ? 'pedido' : 'comentario'}`, onlyVisible: 'Visible sólo para', replyInThread: 'Responder en el hilo', flagAsOffensive: 'Marcar como ofensivo', @@ -324,13 +395,15 @@ export default { reportActionsView: { beginningOfArchivedRoomPartOne: 'Te perdiste la fiesta en ', beginningOfArchivedRoomPartTwo: ', no hay nada que ver aquí.', - beginningOfChatHistoryDomainRoomPartOne: ({domainRoom}) => `Colabora aquí con todos los participantes de ${domainRoom}! 🎉\nUtiliza `, + beginningOfChatHistoryDomainRoomPartOne: ({domainRoom}: BeginningOfChatHistoryDomainRoomPartOneParams) => `Colabora aquí con todos los participantes de ${domainRoom}! 🎉\nUtiliza `, beginningOfChatHistoryDomainRoomPartTwo: ' para chatear con compañeros, compartir consejos o hacer una pregunta.', - beginningOfChatHistoryAdminRoomPartOne: ({workspaceName}) => `Este es el lugar para que los administradores de ${workspaceName} colaboren! 🎉\nUsa `, + beginningOfChatHistoryAdminRoomPartOne: ({workspaceName}: BeginningOfChatHistoryAdminRoomPartOneParams) => + `Este es el lugar para que los administradores de ${workspaceName} colaboren! 🎉\nUsa `, beginningOfChatHistoryAdminRoomPartTwo: ' para chatear sobre temas como la configuración del espacio de trabajo y mas.', beginningOfChatHistoryAdminOnlyPostingRoom: 'Solo los administradores pueden enviar mensajes en esta sala.', - beginningOfChatHistoryAnnounceRoomPartOne: ({workspaceName}) => `Este es el lugar para que todos los miembros de ${workspaceName} colaboren! 🎉\nUsa `, - beginningOfChatHistoryAnnounceRoomPartTwo: ({workspaceName}) => ` para chatear sobre cualquier cosa relacionada con ${workspaceName}.`, + beginningOfChatHistoryAnnounceRoomPartOne: ({workspaceName}: BeginningOfChatHistoryAnnounceRoomPartOneParams) => + `Este es el lugar para que todos los miembros de ${workspaceName} colaboren! 🎉\nUsa `, + beginningOfChatHistoryAnnounceRoomPartTwo: ({workspaceName}: BeginningOfChatHistoryAnnounceRoomPartTwo) => ` para chatear sobre cualquier cosa relacionada con ${workspaceName}.`, beginningOfChatHistoryUserRoomPartOne: 'Este es el lugar para colaborar! 🎉\nUsa este espacio para chatear sobre cualquier cosa relacionada con ', beginningOfChatHistoryUserRoomPartTwo: '.', beginningOfChatHistory: 'Aquí comienzan tus conversaciones con ', @@ -339,7 +412,7 @@ export default { beginningOfChatHistoryPolicyExpenseChatPartThree: ' empieza aquí! 🎉 Este es el lugar donde chatear, pedir dinero y pagar.', chatWithAccountManager: 'Chatea con tu gestor de cuenta aquí', sayHello: '¡Saluda!', - welcomeToRoom: ({roomName}) => `¡Bienvenido a ${roomName}!`, + welcomeToRoom: ({roomName}: WelcomeToRoomParams) => `¡Bienvenido a ${roomName}!`, usePlusButton: '\n\n¡También puedes usar el botón + de abajo para pedir dinero o asignar una tarea!', }, reportAction: { @@ -356,12 +429,14 @@ export default { }, reportArchiveReasons: { [CONST.REPORT.ARCHIVE_REASON.DEFAULT]: 'Esta sala de chat ha sido eliminada.', - [CONST.REPORT.ARCHIVE_REASON.ACCOUNT_CLOSED]: ({displayName}) => `Este chat de espacio de trabajo esta desactivado porque ${displayName} ha cerrado su cuenta.`, - [CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED]: ({displayName, oldDisplayName}) => + [CONST.REPORT.ARCHIVE_REASON.ACCOUNT_CLOSED]: ({displayName}: ReportArchiveReasonsClosedParams) => + `Este chat de espacio de trabajo esta desactivado porque ${displayName} ha cerrado su cuenta.`, + [CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED]: ({displayName, oldDisplayName}: ReportArchiveReasonsMergedParams) => `Este chat de espacio de trabajo esta desactivado porque ${oldDisplayName} ha combinado su cuenta con ${displayName}.`, - [CONST.REPORT.ARCHIVE_REASON.REMOVED_FROM_POLICY]: ({displayName, policyName}) => + [CONST.REPORT.ARCHIVE_REASON.REMOVED_FROM_POLICY]: ({displayName, policyName}: ReportArchiveReasonsRemovedFromPolicyParams) => `Este chat de espacio de trabajo esta desactivado porque ${displayName} ha dejado de ser miembro del espacio de trabajo ${policyName}.`, - [CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED]: ({policyName}) => `Este chat de espacio de trabajo esta desactivado porque el espacio de trabajo ${policyName} se ha eliminado.`, + [CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED]: ({policyName}: ReportArchiveReasonsPolicyDeletedParams) => + `Este chat de espacio de trabajo esta desactivado porque el espacio de trabajo ${policyName} se ha eliminado.`, }, writeCapabilityPage: { label: 'Quién puede postear', @@ -423,33 +498,34 @@ export default { receiptScanning: 'Escaneo de recibo en curso…', receiptStatusTitle: 'Escaneando…', receiptStatusText: 'Solo tú puedes ver este recibo cuando se está escaneando. Vuelve más tarde o introduce los detalles ahora.', - requestCount: ({count, scanningReceipts = 0}) => `${count} solicitudes${scanningReceipts > 0 ? `, ${scanningReceipts} escaneando` : ''}`, + requestCount: ({count, scanningReceipts = 0}: RequestCountParams) => `${count} solicitudes${scanningReceipts > 0 ? `, ${scanningReceipts} escaneando` : ''}`, deleteRequest: 'Eliminar pedido', deleteConfirmation: '¿Estás seguro de que quieres eliminar este pedido?', settledExpensify: 'Pagado', settledElsewhere: 'Pagado de otra forma', settledPaypalMe: 'Pagado con PayPal.me', - settleExpensify: ({formattedAmount}) => `Pagar ${formattedAmount} con Expensify`, + settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => `Pagar ${formattedAmount} con Expensify`, payElsewhere: 'Pagar de otra forma', - settlePaypalMe: ({formattedAmount}) => `Pagar ${formattedAmount} con PayPal.me`, - requestAmount: ({amount}) => `solicitar ${amount}`, - splitAmount: ({amount}) => `dividir ${amount}`, - amountEach: ({amount}) => `${amount} cada uno`, - payerOwesAmount: ({payer, amount}) => `${payer} debe ${amount}`, - payerOwes: ({payer}) => `${payer} debe: `, - payerPaidAmount: ({payer, amount}) => `${payer} pagó ${amount}`, - payerPaid: ({payer}) => `${payer} pagó: `, - managerApproved: ({manager}) => `${manager} aprobó:`, - payerSettled: ({amount}) => `pagó ${amount}`, - waitingOnBankAccount: ({submitterDisplayName}) => `inicio el pago, pero no se procesará hasta que ${submitterDisplayName} añada una cuenta bancaria`, - settledAfterAddedBankAccount: ({submitterDisplayName, amount}) => `${submitterDisplayName} añadió una cuenta bancaria. El pago de ${amount} se ha realizado.`, - paidElsewhereWithAmount: ({amount}) => `pagó ${amount} de otra forma`, - paidUsingPaypalWithAmount: ({amount}) => `pagó ${amount} con PayPal.me`, - paidUsingExpensifyWithAmount: ({amount}) => `pagó ${amount} con Expensify`, + settlePaypalMe: ({formattedAmount}: SettlePaypalMeParams) => `Pagar ${formattedAmount} con PayPal.me`, + requestAmount: ({amount}: RequestAmountParams) => `solicitar ${amount}`, + splitAmount: ({amount}: SplitAmountParams) => `dividir ${amount}`, + amountEach: ({amount}: AmountEachParams) => `${amount} cada uno`, + payerOwesAmount: ({payer, amount}: PayerOwesAmountParams) => `${payer} debe ${amount}`, + payerOwes: ({payer}: PayerOwesParams) => `${payer} debe: `, + payerPaidAmount: ({payer, amount}: PayerPaidAmountParams) => `${payer} pagó ${amount}`, + payerPaid: ({payer}: PayerPaidParams) => `${payer} pagó: `, + managerApproved: ({manager}: ManagerApprovedParams) => `${manager} aprobó:`, + payerSettled: ({amount}: PayerSettledParams) => `pagó ${amount}`, + waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `inicio el pago, pero no se procesará hasta que ${submitterDisplayName} añada una cuenta bancaria`, + settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) => + `${submitterDisplayName} añadió una cuenta bancaria. El pago de ${amount} se ha realizado.`, + paidElsewhereWithAmount: ({amount}: PaidElsewhereWithAmountParams) => `pagó ${amount} de otra forma`, + paidUsingPaypalWithAmount: ({amount}: PaidUsingPaypalWithAmountParams) => `pagó ${amount} con PayPal.me`, + paidUsingExpensifyWithAmount: ({amount}: PaidUsingExpensifyWithAmountParams) => `pagó ${amount} con Expensify`, noReimbursableExpenses: 'El importe de este informe no es válido', pendingConversionMessage: 'El total se actualizará cuando estés online', - threadRequestReportName: ({formattedAmount, comment}) => `Solicitud de ${formattedAmount}${comment ? ` para ${comment}` : ''}`, - threadSentMoneyReportName: ({formattedAmount, comment}) => `${formattedAmount} enviado${comment ? ` para ${comment}` : ''}`, + threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Solicitud de ${formattedAmount}${comment ? ` para ${comment}` : ''}`, + threadSentMoneyReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} enviado${comment ? ` para ${comment}` : ''}`, error: { invalidSplit: 'La suma de las partes no equivale al monto total', other: 'Error inesperado, por favor inténtalo más tarde', @@ -477,10 +553,10 @@ export default { removePhoto: 'Eliminar foto', editImage: 'Editar foto', deleteWorkspaceError: 'Lo sentimos, hubo un problema eliminando el avatar de su espacio de trabajo.', - sizeExceeded: ({maxUploadSizeInMB}) => `La imagen supera el tamaño máximo de ${maxUploadSizeInMB}MB.`, - resolutionConstraints: ({minHeightInPx, minWidthInPx, maxHeightInPx, maxWidthInPx}) => + sizeExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `La imagen supera el tamaño máximo de ${maxUploadSizeInMB}MB.`, + resolutionConstraints: ({minHeightInPx, minWidthInPx, maxHeightInPx, maxWidthInPx}: ResolutionConstraintsParams) => `Por favor, elige una imagen más grande que ${minHeightInPx}x${minWidthInPx} píxeles y más pequeña que ${maxHeightInPx}x${maxWidthInPx} píxeles.`, - notAllowedExtension: ({allowedExtensions}) => `La foto de perfil debe ser de uno de los siguientes tipos: ${allowedExtensions.join(', ')}.`, + notAllowedExtension: ({allowedExtensions}: NotAllowedExtensionParams) => `La foto de perfil debe ser de uno de los siguientes tipos: ${allowedExtensions.join(', ')}.`, }, profilePage: { profile: 'Perfil', @@ -517,7 +593,7 @@ export default { helpTextAfterEmail: ' desde varias direcciones de correo electrónico.', pleaseVerify: 'Por favor, verifica este método de contacto', getInTouch: 'Utilizaremos este método de contacto cuando necesitemos contactarte.', - enterMagicCode: ({contactMethod}) => `Por favor, introduce el código mágico enviado a ${contactMethod}`, + enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Por favor, introduce el código mágico enviado a ${contactMethod}`, setAsDefault: 'Establecer como predeterminado', yourDefaultContactMethod: 'Este es tu método de contacto predeterminado. No podrás eliminarlo hasta que añadas otro método de contacto y lo marques como predeterminado pulsando "Establecer como predeterminado".', @@ -536,6 +612,7 @@ export default { invalidContactMethod: 'Método de contacto no válido', }, newContactMethod: 'Nuevo método de contacto', + goBackContactMethods: 'Volver a métodos de contacto', }, pronouns: { coCos: 'Co / Cos', @@ -709,9 +786,9 @@ export default { addBankAccountFailure: 'Ocurrió un error inesperado al intentar añadir la cuenta bancaria. Inténtalo de nuevo.', }, transferAmountPage: { - transfer: ({amount}) => `Transferir${amount ? ` ${amount}` : ''}`, + transfer: ({amount}: TransferParams) => `Transferir${amount ? ` ${amount}` : ''}`, instant: 'Instante', - instantSummary: ({rate, minAmount}) => `Tarifa del ${rate}% (${minAmount} mínimo)`, + instantSummary: ({rate, minAmount}: InstantSummaryParams) => `Tarifa del ${rate}% (${minAmount} mínimo)`, ach: '1-3 días laborales', achSummary: 'Sin cargo', whichAccount: '¿Qué cuenta?', @@ -843,7 +920,7 @@ export default { }, cannotGetAccountDetails: 'No se pudieron cargar los detalles de tu cuenta. Por favor, intenta iniciar sesión de nuevo.', loginForm: 'Formulario de inicio de sesión', - notYou: ({user}) => `¿No eres ${user}?`, + notYou: ({user}: NotYouParams) => `¿No eres ${user}?`, }, personalDetails: { error: { @@ -859,28 +936,30 @@ export default { legalLastName: 'Apellidos legales', homeAddress: 'Domicilio', error: { - dateShouldBeBefore: ({dateString}) => `La fecha debe ser anterior a ${dateString}.`, - dateShouldBeAfter: ({dateString}) => `La fecha debe ser posterior a ${dateString}.`, - incorrectZipFormat: ({zipFormat}) => `Formato de código postal incorrecto.${zipFormat ? ` Formato aceptable: ${zipFormat}` : ''}`, + dateShouldBeBefore: ({dateString}: DateShouldBeBeforeParams) => `La fecha debe ser anterior a ${dateString}.`, + dateShouldBeAfter: ({dateString}: DateShouldBeAfterParams) => `La fecha debe ser posterior a ${dateString}.`, + incorrectZipFormat: ({zipFormat}: IncorrectZipFormatParams) => `Formato de código postal incorrecto.${zipFormat ? ` Formato aceptable: ${zipFormat}` : ''}`, hasInvalidCharacter: 'El nombre sólo puede incluir letras.', }, }, resendValidationForm: { linkHasBeenResent: 'El enlace se ha reenviado', - weSentYouMagicSignInLink: ({login, loginType}) => `Te he enviado un hiperenlace mágico para iniciar sesión a ${login}. Por favor, revisa tu ${loginType}`, + weSentYouMagicSignInLink: ({login, loginType}: WeSentYouMagicSignInLinkParams) => + `Te he enviado un hiperenlace mágico para iniciar sesión a ${login}. Por favor, revisa tu ${loginType}`, resendLink: 'Reenviar enlace', }, unlinkLoginForm: { - toValidateLogin: ({primaryLogin, secondaryLogin}) => `Para validar ${secondaryLogin}, reenvía el código mágico desde la Configuración de la cuenta de ${primaryLogin}.`, - noLongerHaveAccess: ({primaryLogin}) => `Si ya no tienes acceso a ${primaryLogin} por favor, desvincula las cuentas.`, + toValidateLogin: ({primaryLogin, secondaryLogin}: ToValidateLoginParams) => + `Para validar ${secondaryLogin}, reenvía el código mágico desde la Configuración de la cuenta de ${primaryLogin}.`, + noLongerHaveAccess: ({primaryLogin}: NoLongerHaveAccessParams) => `Si ya no tienes acceso a ${primaryLogin} por favor, desvincula las cuentas.`, unlink: 'Desvincular', linkSent: '¡Enlace enviado!', succesfullyUnlinkedLogin: '¡Nombre de usuario secundario desvinculado correctamente!', }, emailDeliveryFailurePage: { - ourEmailProvider: ({login}) => + ourEmailProvider: ({login}: OurEmailProviderParams) => `Nuestro proveedor de correo electrónico ha suspendido temporalmente los correos electrónicos a ${login} debido a problemas de entrega. Para desbloquear el inicio de sesión, sigue estos pasos:`, - confirmThat: ({login}) => `Confirma que ${login} está escrito correctamente y que es una dirección de correo electrónico real que puede recibir correos. `, + confirmThat: ({login}: ConfirmThatParams) => `Confirma que ${login} está escrito correctamente y que es una dirección de correo electrónico real que puede recibir correos. `, emailAliases: 'Los alias de correo electrónico como "expenses@domain.com" deben tener acceso a su propia bandeja de entrada de correo electrónico para que sea un inicio de sesión válido de Expensify.', ensureYourEmailClient: 'Asegúrese de que su cliente de correo electrónico permita correos electrónicos de expensify.com. ', @@ -926,7 +1005,7 @@ export default { save: 'Guardar', message: 'Mensaje', untilTomorrow: 'Hasta mañana', - untilTime: ({time}) => { + untilTime: ({time}: UntilTimeParams) => { // Check for HH:MM AM/PM format and starts with '01:' if (CONST.REGEX.TIME_STARTS_01.test(time)) { return `Hasta la ${time}`; @@ -943,7 +1022,7 @@ export default { return `Hasta ${time}`; }, }, - stepCounter: ({step, total, text}) => { + stepCounter: ({step, total, text}: StepCounterParams) => { let result = `Paso ${step}`; if (total) { @@ -1029,7 +1108,7 @@ export default { messages: { errorMessageInvalidPhone: `Por favor, introduce un número de teléfono válido sin paréntesis o guiones. Si reside fuera de Estados Unidos, por favor incluye el prefijo internacional (p. ej. ${CONST.EXAMPLE_PHONE_NUMBER}).`, errorMessageInvalidEmail: 'Email inválido', - userIsAlreadyMemberOfWorkspace: ({login, workspace}) => `${login} ya es miembro de ${workspace}`, + userIsAlreadyMemberOfWorkspace: ({login, workspace}: UserIsAlreadyMemberOfWorkspaceParams) => `${login} ya es miembro de ${workspace}`, }, onfidoStep: { acceptTerms: 'Al continuar con la solicitud para activar su billetera Expensify, confirma que ha leído, comprende y acepta ', @@ -1237,7 +1316,7 @@ export default { unavailable: 'Espacio de trabajo no disponible', memberNotFound: 'Miembro no encontrado. Para invitar a un nuevo miembro al espacio de trabajo, por favor, utiliza el botón Invitar que está arriba.', notAuthorized: `No tienes acceso a esta página. ¿Estás tratando de unirte al espacio de trabajo? Comunícate con el propietario de este espacio de trabajo para que pueda añadirte como miembro. ¿Necesitas algo más? Comunícate con ${CONST.EMAIL.CONCIERGE}`, - goToRoom: ({roomName}) => `Ir a la sala ${roomName}`, + goToRoom: ({roomName}: GoToRoomParams) => `Ir a la sala ${roomName}`, }, emptyWorkspace: { title: 'Crear un nuevo espacio de trabajo', @@ -1334,7 +1413,7 @@ export default { personalMessagePrompt: 'Mensaje', inviteNoMembersError: 'Por favor, selecciona al menos un miembro a invitar', genericFailureMessage: 'Se produjo un error al invitar al usuario al espacio de trabajo. Vuelva a intentarlo..', - welcomeNote: ({workspaceName}) => + welcomeNote: ({workspaceName}: WelcomeNoteParams) => `¡Has sido invitado a ${workspaceName}! Descargue la aplicación móvil Expensify en use.expensify.com/download para comenzar a rastrear sus gastos.`, }, editor: { @@ -1408,15 +1487,17 @@ export default { restrictedDescription: 'Sólo las personas en tu espacio de trabajo pueden encontrar esta sala', privateDescription: 'Sólo las personas que están invitadas a esta sala pueden encontrarla', publicDescription: 'Cualquier persona puede unirse a esta sala', + // eslint-disable-next-line @typescript-eslint/naming-convention public_announceDescription: 'Cualquier persona puede unirse a esta sala', createRoom: 'Crea una sala de chat', roomAlreadyExistsError: 'Ya existe una sala con este nombre', - roomNameReservedError: ({reservedName}) => `${reservedName} es el nombre una sala por defecto de todos los espacios de trabajo. Por favor, elige otro nombre.`, + roomNameReservedError: ({reservedName}: RoomNameReservedErrorParams) => + `${reservedName} es el nombre una sala por defecto de todos los espacios de trabajo. Por favor, elige otro nombre.`, roomNameInvalidError: 'Los nombres de las salas solo pueden contener minúsculas, números y guiones', pleaseEnterRoomName: 'Por favor, escribe el nombre de una sala', pleaseSelectWorkspace: 'Por favor, selecciona un espacio de trabajo', - renamedRoomAction: ({oldName, newName}) => ` cambió el nombre de la sala de ${oldName} a ${newName}`, - roomRenamedTo: ({newName}) => `Sala renombrada a ${newName}`, + renamedRoomAction: ({oldName, newName}: RenamedRoomActionParams) => ` cambió el nombre de la sala de ${oldName} a ${newName}`, + roomRenamedTo: ({newName}: RoomRenamedToParams) => `Sala renombrada a ${newName}`, social: 'social', selectAWorkspace: 'Seleccionar un espacio de trabajo', growlMessageOnRenameError: 'No se ha podido cambiar el nombre del espacio de trabajo, por favor, comprueba tu conexión e inténtalo de nuevo.', @@ -1424,6 +1505,7 @@ export default { restricted: 'Restringida', private: 'Privada', public: 'Público', + // eslint-disable-next-line @typescript-eslint/naming-convention public_announce: 'Anuncio Público', }, }, @@ -1567,8 +1649,8 @@ export default { noActivityYet: 'Sin actividad todavía', }, chronos: { - oooEventSummaryFullDay: ({summary, dayCount, date}) => `${summary} por ${dayCount} ${dayCount === 1 ? 'día' : 'días'} hasta el ${date}`, - oooEventSummaryPartialDay: ({summary, timePeriod, date}) => `${summary} de ${timePeriod} del ${date}`, + oooEventSummaryFullDay: ({summary, dayCount, date}: OOOEventSummaryFullDayParams) => `${summary} por ${dayCount} ${dayCount === 1 ? 'día' : 'días'} hasta el ${date}`, + oooEventSummaryPartialDay: ({summary, timePeriod, date}: OOOEventSummaryPartialDayParams) => `${summary} de ${timePeriod} del ${date}`, }, footer: { features: 'Características', @@ -2084,7 +2166,7 @@ export default { reply: 'Respuesta', from: 'De', in: 'en', - parentNavigationSummary: ({rootReportName, workspaceName}) => `De ${rootReportName}${workspaceName ? ` en ${workspaceName}` : ''}`, + parentNavigationSummary: ({rootReportName, workspaceName}: ParentNavigationSummaryParams) => `De ${rootReportName}${workspaceName ? ` en ${workspaceName}` : ''}`, }, qrCodes: { copyUrlToClipboard: 'Copiar URL al portapapeles', diff --git a/src/languages/translations.js b/src/languages/translations.ts similarity index 65% rename from src/languages/translations.js rename to src/languages/translations.ts index c8dd8c8ab0e0..a2d27baa26c9 100644 --- a/src/languages/translations.js +++ b/src/languages/translations.ts @@ -5,5 +5,6 @@ import esES from './es-ES'; export default { en, es, + // eslint-disable-next-line @typescript-eslint/naming-convention 'es-ES': esES, }; diff --git a/src/languages/types.ts b/src/languages/types.ts new file mode 100644 index 000000000000..50290fb5776c --- /dev/null +++ b/src/languages/types.ts @@ -0,0 +1,255 @@ +type AddressLineParams = { + lineNumber: number; +}; + +type CharacterLimitParams = { + limit: number; +}; + +type MaxParticipantsReachedParams = { + count: number; +}; + +type ZipCodeExampleFormatParams = { + zipSampleFormat: string; +}; + +type LoggedInAsParams = { + email: string; +}; + +type NewFaceEnterMagicCodeParams = { + login: string; +}; + +type WelcomeEnterMagicCodeParams = { + login: string; +}; + +type AlreadySignedInParams = { + email: string; +}; + +type GoBackMessageParams = { + provider: string; +}; + +type LocalTimeParams = { + user: string; + time: string; +}; + +type EditActionParams = { + action: NonNullable; +}; + +type DeleteActionParams = { + action: NonNullable; +}; + +type DeleteConfirmationParams = { + action: NonNullable; +}; + +type BeginningOfChatHistoryDomainRoomPartOneParams = { + domainRoom: string; +}; + +type BeginningOfChatHistoryAdminRoomPartOneParams = { + workspaceName: string; +}; + +type BeginningOfChatHistoryAnnounceRoomPartOneParams = { + workspaceName: string; +}; + +type BeginningOfChatHistoryAnnounceRoomPartTwo = { + workspaceName: string; +}; + +type WelcomeToRoomParams = { + roomName: string; +}; + +type ReportArchiveReasonsClosedParams = { + displayName: string; +}; + +type ReportArchiveReasonsMergedParams = { + displayName: string; + oldDisplayName: string; +}; + +type ReportArchiveReasonsRemovedFromPolicyParams = { + displayName: string; + policyName: string; +}; + +type ReportArchiveReasonsPolicyDeletedParams = { + policyName: string; +}; + +type RequestCountParams = { + count: number; + scanningReceipts: number; +}; + +type SettleExpensifyCardParams = { + formattedAmount: string; +}; + +type SettlePaypalMeParams = {formattedAmount: string}; + +type RequestAmountParams = {amount: number}; + +type SplitAmountParams = {amount: number}; + +type AmountEachParams = {amount: number}; + +type PayerOwesAmountParams = {payer: string; amount: number}; + +type PayerOwesParams = {payer: string}; + +type PayerPaidAmountParams = {payer: string; amount: number}; + +type ManagerApprovedParams = {manager: string}; + +type PayerPaidParams = {payer: string}; + +type PayerSettledParams = {amount: number}; + +type WaitingOnBankAccountParams = {submitterDisplayName: string}; + +type SettledAfterAddedBankAccountParams = {submitterDisplayName: string; amount: string}; + +type PaidElsewhereWithAmountParams = {amount: string}; + +type PaidUsingPaypalWithAmountParams = {amount: string}; + +type PaidUsingExpensifyWithAmountParams = {amount: string}; + +type ThreadRequestReportNameParams = {formattedAmount: string; comment: string}; + +type ThreadSentMoneyReportNameParams = {formattedAmount: string; comment: string}; + +type SizeExceededParams = {maxUploadSizeInMB: number}; + +type ResolutionConstraintsParams = {minHeightInPx: number; minWidthInPx: number; maxHeightInPx: number; maxWidthInPx: number}; + +type NotAllowedExtensionParams = {allowedExtensions: string[]}; + +type EnterMagicCodeParams = {contactMethod: string}; + +type TransferParams = {amount: string}; + +type InstantSummaryParams = {rate: number; minAmount: number}; + +type NotYouParams = {user: string}; + +type DateShouldBeBeforeParams = {dateString: string}; + +type DateShouldBeAfterParams = {dateString: string}; + +type IncorrectZipFormatParams = {zipFormat?: string}; + +type WeSentYouMagicSignInLinkParams = {login: string; loginType: string}; + +type ToValidateLoginParams = {primaryLogin: string; secondaryLogin: string}; + +type NoLongerHaveAccessParams = {primaryLogin: string}; + +type OurEmailProviderParams = {login: string}; + +type ConfirmThatParams = {login: string}; + +type UntilTimeParams = {time: string}; + +type StepCounterParams = {step: number; total?: number; text?: string}; + +type UserIsAlreadyMemberOfWorkspaceParams = {login: string; workspace: string}; + +type GoToRoomParams = {roomName: string}; + +type WelcomeNoteParams = {workspaceName: string}; + +type RoomNameReservedErrorParams = {reservedName: string}; + +type RenamedRoomActionParams = {oldName: string; newName: string}; + +type RoomRenamedToParams = {newName: string}; + +type OOOEventSummaryFullDayParams = {summary: string; dayCount: number; date: string}; + +type OOOEventSummaryPartialDayParams = {summary: string; timePeriod: string; date: string}; + +type ParentNavigationSummaryParams = {rootReportName: string; workspaceName: string}; + +export type { + AddressLineParams, + CharacterLimitParams, + MaxParticipantsReachedParams, + ZipCodeExampleFormatParams, + LoggedInAsParams, + NewFaceEnterMagicCodeParams, + WelcomeEnterMagicCodeParams, + AlreadySignedInParams, + GoBackMessageParams, + LocalTimeParams, + EditActionParams, + DeleteActionParams, + DeleteConfirmationParams, + BeginningOfChatHistoryDomainRoomPartOneParams, + BeginningOfChatHistoryAdminRoomPartOneParams, + BeginningOfChatHistoryAnnounceRoomPartOneParams, + BeginningOfChatHistoryAnnounceRoomPartTwo, + WelcomeToRoomParams, + ReportArchiveReasonsClosedParams, + ReportArchiveReasonsMergedParams, + ReportArchiveReasonsRemovedFromPolicyParams, + ReportArchiveReasonsPolicyDeletedParams, + RequestCountParams, + SettleExpensifyCardParams, + SettlePaypalMeParams, + RequestAmountParams, + SplitAmountParams, + AmountEachParams, + PayerOwesAmountParams, + PayerOwesParams, + PayerPaidAmountParams, + PayerPaidParams, + ManagerApprovedParams, + PayerSettledParams, + WaitingOnBankAccountParams, + SettledAfterAddedBankAccountParams, + PaidElsewhereWithAmountParams, + PaidUsingPaypalWithAmountParams, + PaidUsingExpensifyWithAmountParams, + ThreadRequestReportNameParams, + ThreadSentMoneyReportNameParams, + SizeExceededParams, + ResolutionConstraintsParams, + NotAllowedExtensionParams, + EnterMagicCodeParams, + TransferParams, + InstantSummaryParams, + NotYouParams, + DateShouldBeBeforeParams, + DateShouldBeAfterParams, + IncorrectZipFormatParams, + WeSentYouMagicSignInLinkParams, + ToValidateLoginParams, + NoLongerHaveAccessParams, + OurEmailProviderParams, + ConfirmThatParams, + UntilTimeParams, + StepCounterParams, + UserIsAlreadyMemberOfWorkspaceParams, + GoToRoomParams, + WelcomeNoteParams, + RoomNameReservedErrorParams, + RenamedRoomActionParams, + RoomRenamedToParams, + OOOEventSummaryFullDayParams, + OOOEventSummaryPartialDayParams, + ParentNavigationSummaryParams, +}; diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index d50bd53a0a1c..e10e51b307e4 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -35,6 +35,7 @@ import styles from '../../../styles/styles'; import * as SessionUtils from '../../SessionUtils'; import NotFoundPage from '../../../pages/ErrorPage/NotFoundPage'; import getRootNavigatorScreenOptions from './getRootNavigatorScreenOptions'; +import DemoSetupPage from '../../../pages/DemoSetupPage'; let timezone; let currentAccountID; @@ -282,6 +283,16 @@ class AuthScreens extends React.Component { return ConciergePage; }} /> + + - - ); diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index 8390aa7d700b..ee3054e02f96 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -18,6 +18,10 @@ export default { [SCREENS.DESKTOP_SIGN_IN_REDIRECT]: ROUTES.DESKTOP_SIGN_IN_REDIRECT, [SCREENS.REPORT_ATTACHMENTS]: ROUTES.REPORT_ATTACHMENTS, + // Demo routes + [CONST.DEMO_PAGES.SAASTR]: ROUTES.SAASTR, + [CONST.DEMO_PAGES.SBE]: ROUTES.SBE, + // Sidebar [SCREENS.HOME]: { path: ROUTES.HOME, @@ -26,8 +30,6 @@ export default { [NAVIGATORS.CENTRAL_PANE_NAVIGATOR]: { screens: { [SCREENS.REPORT]: ROUTES.REPORT_WITH_ID, - [CONST.DEMO_PAGES.SAASTR]: ROUTES.SAASTR, - [CONST.DEMO_PAGES.SBE]: ROUTES.SBE, }, }, [SCREENS.NOT_FOUND]: '*', diff --git a/src/libs/NumberFormatUtils.js b/src/libs/NumberFormatUtils.js deleted file mode 100644 index 48e4d3dadbb6..000000000000 --- a/src/libs/NumberFormatUtils.js +++ /dev/null @@ -1,9 +0,0 @@ -function format(locale, number, options) { - return new Intl.NumberFormat(locale, options).format(number); -} - -function formatToParts(locale, number, options) { - return new Intl.NumberFormat(locale, options).formatToParts(number); -} - -export {format, formatToParts}; diff --git a/src/libs/NumberFormatUtils.ts b/src/libs/NumberFormatUtils.ts new file mode 100644 index 000000000000..7c81e71f4db8 --- /dev/null +++ b/src/libs/NumberFormatUtils.ts @@ -0,0 +1,9 @@ +function format(locale: string, number: number, options?: Intl.NumberFormatOptions): string { + return new Intl.NumberFormat(locale, options).format(number); +} + +function formatToParts(locale: string, number: number, options?: Intl.NumberFormatOptions): Intl.NumberFormatPart[] { + return new Intl.NumberFormat(locale, options).formatToParts(number); +} + +export {format, formatToParts}; diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index a9c319865bbb..d26ad48430b0 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -394,7 +394,7 @@ function getLastMessageTextForReport(report) { if (ReportUtils.isReportMessageAttachment({text: report.lastMessageText, html: report.lastMessageHtml, translationKey: report.lastMessageTranslationKey})) { lastMessageTextFromReport = `[${Localize.translateLocal(report.lastMessageTranslationKey || 'common.attachment')}]`; } else if (ReportActionUtils.isMoneyRequestAction(lastReportAction)) { - lastMessageTextFromReport = ReportUtils.getReportPreviewMessage(report, lastReportAction); + lastMessageTextFromReport = ReportUtils.getReportPreviewMessage(report, lastReportAction, true); } else if (ReportActionUtils.isReportPreviewAction(lastReportAction)) { const iouReport = ReportUtils.getReport(ReportActionUtils.getIOUReportIDFromReportActionPreview(lastReportAction)); lastMessageTextFromReport = ReportUtils.getReportPreviewMessage(iouReport, lastReportAction); diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 893145a8e5fa..7390bac47dd1 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1418,9 +1418,10 @@ function getTransactionReportName(reportAction) { * * @param {Object} report * @param {Object} [reportAction={}] + * @param {Boolean} [shouldConsiderReceiptBeingScanned=false] * @returns {String} */ -function getReportPreviewMessage(report, reportAction = {}) { +function getReportPreviewMessage(report, reportAction = {}, shouldConsiderReceiptBeingScanned = false) { const reportActionMessage = lodashGet(reportAction, 'message[0].html', ''); if (_.isEmpty(report) || !report.reportID) { @@ -1437,6 +1438,14 @@ function getReportPreviewMessage(report, reportAction = {}) { return `approved ${formattedAmount}`; } + if (shouldConsiderReceiptBeingScanned && ReportActionsUtils.isMoneyRequestAction(reportAction)) { + const linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); + + if (!_.isEmpty(linkedTransaction) && TransactionUtils.hasReceipt(linkedTransaction) && TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { + return Localize.translateLocal('iou.receiptScanning'); + } + } + if (isSettled(report.reportID)) { // A settled report preview message can come in three formats "paid ... using Paypal.me", "paid ... elsewhere" or "paid ... using Expensify" let translatePhraseKey = 'iou.paidElsewhereWithAmount'; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index b4f04174c1ac..8d1de1dc4d60 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -21,6 +21,7 @@ import * as ErrorUtils from '../ErrorUtils'; import * as UserUtils from '../UserUtils'; import * as Report from './Report'; import * as NumberUtils from '../NumberUtils'; +import ReceiptGeneric from '../../../assets/images/receipt-generic.png'; let allReports; Onyx.connect({ @@ -394,7 +395,7 @@ function getMoneyRequestInformation( let filename; if (receipt && receipt.source) { receiptObject.source = receipt.source; - receiptObject.state = CONST.IOU.RECEIPT_STATE.SCANREADY; + receiptObject.state = receipt.state || CONST.IOU.RECEIPT_STATE.SCANREADY; filename = receipt.name; } let optimisticTransaction = TransactionUtils.buildOptimisticTransaction( @@ -509,6 +510,10 @@ function getMoneyRequestInformation( * @param {String} merchant */ function createDistanceRequest(report, participant, comment, created, transactionID, amount, currency, merchant) { + const optimisticReceipt = { + source: ReceiptGeneric, + state: CONST.IOU.RECEIPT_STATE.OPEN, + }; const {iouReport, chatReport, transaction, iouAction, createdChatReportActionID, createdIOUReportActionID, reportPreviewAction, onyxData} = getMoneyRequestInformation( report, participant, @@ -519,7 +524,7 @@ function createDistanceRequest(report, participant, comment, created, transactio merchant, null, null, - null, + optimisticReceipt, transactionID, ); API.write( diff --git a/src/libs/setSelection/index.js b/src/libs/setSelection/index.js deleted file mode 100644 index c7f24ae4a199..000000000000 --- a/src/libs/setSelection/index.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function setSelection(textInput, start, end) { - if (!textInput) { - return; - } - - textInput.setSelectionRange(start, end); -} diff --git a/src/libs/setSelection/index.native.js b/src/libs/setSelection/index.native.js deleted file mode 100644 index 02d812d84cd4..000000000000 --- a/src/libs/setSelection/index.native.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function setSelection(textInput, start, end) { - if (!textInput) { - return; - } - - textInput.setSelection(start, end); -} diff --git a/src/libs/setSelection/index.native.ts b/src/libs/setSelection/index.native.ts new file mode 100644 index 000000000000..e27cd4e58bd7 --- /dev/null +++ b/src/libs/setSelection/index.native.ts @@ -0,0 +1,13 @@ +import SetSelection from './types'; + +const setSelection: SetSelection = (textInput, start, end) => { + if (!textInput) { + return; + } + + if ('setSelection' in textInput) { + textInput.setSelection(start, end); + } +}; + +export default setSelection; diff --git a/src/libs/setSelection/index.ts b/src/libs/setSelection/index.ts new file mode 100644 index 000000000000..5eee88881924 --- /dev/null +++ b/src/libs/setSelection/index.ts @@ -0,0 +1,13 @@ +import SetSelection from './types'; + +const setSelection: SetSelection = (textInput, start, end) => { + if (!textInput) { + return; + } + + if ('setSelectionRange' in textInput) { + textInput.setSelectionRange(start, end); + } +}; + +export default setSelection; diff --git a/src/libs/setSelection/types.ts b/src/libs/setSelection/types.ts new file mode 100644 index 000000000000..f2717079725f --- /dev/null +++ b/src/libs/setSelection/types.ts @@ -0,0 +1,5 @@ +import {TextInput} from 'react-native'; + +type SetSelection = (textInput: TextInput | HTMLInputElement, start: number, end: number) => void; + +export default SetSelection; diff --git a/src/pages/DemoSetupPage.js b/src/pages/DemoSetupPage.js index 53739820142b..0f7578760c16 100644 --- a/src/pages/DemoSetupPage.js +++ b/src/pages/DemoSetupPage.js @@ -22,14 +22,16 @@ const propTypes = { */ function DemoSetupPage(props) { useFocusEffect(() => { - // Depending on the route that the user hit to get here, run a specific demo flow - if (props.route.name === CONST.DEMO_PAGES.SAASTR) { - DemoActions.runSaastrDemo(); - } else if (props.route.name === CONST.DEMO_PAGES.SBE) { - DemoActions.runSbeDemo(); - } else { - Navigation.goBack(); - } + Navigation.isNavigationReady().then(() => { + // Depending on the route that the user hit to get here, run a specific demo flow + if (props.route.name === CONST.DEMO_PAGES.SAASTR) { + DemoActions.runSaastrDemo(); + } else if (props.route.name === CONST.DEMO_PAGES.SBE) { + DemoActions.runSbeDemo(); + } else { + Navigation.goBack(); + } + }); }); return ; diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js index 155f261693b5..bfbce8aed336 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.js @@ -140,18 +140,21 @@ function ReportActionItemSingle(props) { ] : props.action.person; + const reportID = props.report && props.report.reportID; + const iouReportID = props.iouReport && props.iouReport.reportID; + const showActorDetails = useCallback(() => { if (isWorkspaceActor) { - showWorkspaceDetails(props.report.reportID); + showWorkspaceDetails(reportID); } else { // Show participants page IOU report preview if (displayAllActors) { - Navigation.navigate(ROUTES.getReportParticipantsRoute(props.iouReport.reportID)); + Navigation.navigate(ROUTES.getReportParticipantsRoute(iouReportID)); return; } showUserDetails(props.action.delegateAccountID ? props.action.delegateAccountID : actorAccountID); } - }, [isWorkspaceActor, props.report.reportID, actorAccountID, props.action.delegateAccountID, props.iouReport, displayAllActors]); + }, [isWorkspaceActor, reportID, actorAccountID, props.action.delegateAccountID, iouReportID, displayAllActors]); const shouldDisableDetailPage = useMemo( () => !isWorkspaceActor && ReportUtils.isOptimisticPersonalDetail(props.action.delegateAccountID ? props.action.delegateAccountID : actorAccountID), diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index 0e8553b00dd0..1a3f63ede6e6 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -67,6 +67,16 @@ const propTypes = { /** Forwarded ref to FloatingActionButtonAndPopover */ innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + + /** Information about any currently running demos */ + demoInfo: PropTypes.shape({ + saastr: PropTypes.shape({ + isBeginningDemo: PropTypes.bool, + }), + sbe: PropTypes.shape({ + isBeginningDemo: PropTypes.bool, + }), + }), }; const defaultProps = { onHideCreateMenu: () => {}, @@ -76,6 +86,7 @@ const defaultProps = { isLoading: false, innerRef: null, shouldShowDownloadAppBanner: true, + demoInfo: {}, }; /** @@ -162,8 +173,11 @@ function FloatingActionButtonAndPopover(props) { if (props.shouldShowDownloadAppBanner && Browser.isMobile()) { return; } + if (lodashGet(props.demoInfo, 'saastr.isBeginningDemo', false) || lodashGet(props.demoInfo, 'sbe.isBeginningDemo', false)) { + return; + } Welcome.show({routes, showCreateMenu}); - }, [props.shouldShowDownloadAppBanner, props.navigation, showCreateMenu]); + }, [props.shouldShowDownloadAppBanner, props.navigation, showCreateMenu, props.demoInfo]); useEffect(() => { if (!didScreenBecomeInactive()) { @@ -299,6 +313,9 @@ export default compose( shouldShowDownloadAppBanner: { key: ONYXKEYS.SHOW_DOWNLOAD_APP_BANNER, }, + demoInfo: { + key: ONYXKEYS.DEMO_INFO, + }, }), )( forwardRef((props, ref) => ( diff --git a/src/pages/iou/ReceiptSelector/CameraPermission/index.android.js b/src/pages/iou/ReceiptSelector/CameraPermission/index.android.js new file mode 100644 index 000000000000..3eb9ef4eea5a --- /dev/null +++ b/src/pages/iou/ReceiptSelector/CameraPermission/index.android.js @@ -0,0 +1,12 @@ +import {check, PERMISSIONS, request} from 'react-native-permissions'; + +function requestCameraPermission() { + return request(PERMISSIONS.ANDROID.CAMERA); +} + +// Android will never return blocked after a check, you have to request the permission to get the info. +function getCameraPermissionStatus() { + return check(PERMISSIONS.ANDROID.CAMERA); +} + +export {requestCameraPermission, getCameraPermissionStatus}; diff --git a/src/pages/iou/ReceiptSelector/CameraPermission/index.ios.js b/src/pages/iou/ReceiptSelector/CameraPermission/index.ios.js new file mode 100644 index 000000000000..3c24bfa10d6f --- /dev/null +++ b/src/pages/iou/ReceiptSelector/CameraPermission/index.ios.js @@ -0,0 +1,11 @@ +import {check, PERMISSIONS, request} from 'react-native-permissions'; + +function requestCameraPermission() { + return request(PERMISSIONS.IOS.CAMERA); +} + +function getCameraPermissionStatus() { + return check(PERMISSIONS.IOS.CAMERA); +} + +export {requestCameraPermission, getCameraPermissionStatus}; diff --git a/src/pages/iou/ReceiptSelector/CameraPermission/index.js b/src/pages/iou/ReceiptSelector/CameraPermission/index.js new file mode 100644 index 000000000000..4357b592d7ef --- /dev/null +++ b/src/pages/iou/ReceiptSelector/CameraPermission/index.js @@ -0,0 +1,5 @@ +function requestCameraPermission() {} + +function getCameraPermissionStatus() {} + +export {requestCameraPermission, getCameraPermissionStatus}; diff --git a/src/pages/iou/ReceiptSelector/index.native.js b/src/pages/iou/ReceiptSelector/index.native.js index f012905667c7..4ff32d940c9f 100644 --- a/src/pages/iou/ReceiptSelector/index.native.js +++ b/src/pages/iou/ReceiptSelector/index.native.js @@ -1,10 +1,11 @@ import {ActivityIndicator, Alert, AppState, Linking, Text, View} from 'react-native'; import React, {useCallback, useEffect, useRef, useState} from 'react'; -import {Camera, useCameraDevices} from 'react-native-vision-camera'; +import {useCameraDevices} from 'react-native-vision-camera'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import {launchImageLibrary} from 'react-native-image-picker'; import {withOnyx} from 'react-native-onyx'; +import {RESULTS} from 'react-native-permissions'; import PressableWithFeedback from '../../../components/Pressable/PressableWithFeedback'; import Icon from '../../../components/Icon'; import * as Expensicons from '../../../components/Icon/Expensicons'; @@ -19,6 +20,7 @@ import Button from '../../../components/Button'; import useLocalize from '../../../hooks/useLocalize'; import ONYXKEYS from '../../../ONYXKEYS'; import Log from '../../../libs/Log'; +import * as CameraPermission from './CameraPermission'; import {iouPropTypes, iouDefaultProps} from '../propTypes'; import NavigationAwareCamera from './NavigationAwareCamera'; @@ -78,7 +80,8 @@ function ReceiptSelector(props) { const camera = useRef(null); const [flash, setFlash] = useState(false); - const [permissions, setPermissions] = useState('authorized'); + const [permissions, setPermissions] = useState('granted'); + const isAndroidBlockedPermissionRef = useRef(false); const appState = useRef(AppState.currentState); const iouType = lodashGet(props.route, 'params.iouType', ''); @@ -91,7 +94,7 @@ function ReceiptSelector(props) { useEffect(() => { const subscription = AppState.addEventListener('change', (nextAppState) => { if (appState.current.match(/inactive|background/) && nextAppState === 'active') { - Camera.getCameraPermissionStatus().then((permissionStatus) => { + CameraPermission.getCameraPermissionStatus().then((permissionStatus) => { setPermissions(permissionStatus); }); } @@ -134,12 +137,15 @@ function ReceiptSelector(props) { }; const askForPermissions = () => { - if (permissions === 'not-determined') { - Camera.requestCameraPermission().then((permissionStatus) => { + // There's no way we can check for the BLOCKED status without requesting the permission first + // https://github.com/zoontek/react-native-permissions/blob/a836e114ce3a180b2b23916292c79841a267d828/README.md?plain=1#L670 + if (permissions === RESULTS.BLOCKED || isAndroidBlockedPermissionRef.current) { + Linking.openSettings(); + } else if (permissions === RESULTS.DENIED) { + CameraPermission.requestCameraPermission().then((permissionStatus) => { setPermissions(permissionStatus); + isAndroidBlockedPermissionRef.current = permissionStatus === RESULTS.BLOCKED; }); - } else { - Linking.openSettings(); } }; @@ -198,13 +204,13 @@ function ReceiptSelector(props) { }); }, [flash, iouType, props.iou, props.report, reportID, translate]); - Camera.getCameraPermissionStatus().then((permissionStatus) => { + CameraPermission.getCameraPermissionStatus().then((permissionStatus) => { setPermissions(permissionStatus); }); return ( - {permissions !== CONST.RECEIPT.PERMISSION_AUTHORIZED && ( + {permissions !== RESULTS.GRANTED && ( )} - {permissions === CONST.RECEIPT.PERMISSION_AUTHORIZED && device == null && ( + {permissions === RESULTS.GRANTED && device == null && ( )} - {permissions === CONST.RECEIPT.PERMISSION_AUTHORIZED && device != null && ( + {permissions === RESULTS.GRANTED && device != null && ( ; + return ( + + Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS)} + onLinkPress={() => Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS)} + /> + + ); } const isDefaultContactMethod = this.props.session.email === loginData.partnerUserID; diff --git a/src/styles/styles.js b/src/styles/styles.js index 9d7e14a51fc1..7bb44acfb97a 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -3778,8 +3778,8 @@ const styles = { reportActionItemImages: { flexDirection: 'row', - borderWidth: 2, - borderColor: themeColors.cardBG, + borderWidth: 4, + borderColor: themeColors.transparent, borderTopLeftRadius: variables.componentBorderRadiusLarge, borderTopRightRadius: variables.componentBorderRadiusLarge, borderBottomLeftRadius: variables.componentBorderRadiusLarge, @@ -3789,8 +3789,6 @@ const styles = { }, reportActionItemImage: { - borderWidth: 1, - borderColor: themeColors.cardBG, flex: 1, width: '100%', height: '100%', @@ -3799,6 +3797,11 @@ const styles = { alignItems: 'center', }, + reportActionItemImageBorder: { + borderRightWidth: 2, + borderColor: themeColors.cardBG, + }, + reportActionItemImagesMore: { position: 'absolute', borderRadius: 18, diff --git a/src/types/modules/react-native.d.ts b/src/types/modules/react-native.d.ts new file mode 100644 index 000000000000..1b0b39e5f67d --- /dev/null +++ b/src/types/modules/react-native.d.ts @@ -0,0 +1,9 @@ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ +import 'react-native'; + +declare module 'react-native' { + interface TextInput { + // Typescript type declaration is missing in React Native for setting text selection. + setSelection: (start: number, end: number) => void; + } +}