From 6ffc1055ca26d026c7ec3a67ff91756a7a220293 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Tue, 3 Oct 2023 11:10:34 +0200 Subject: [PATCH 001/193] Migrated User.js lib to TypeScript. --- src/libs/Navigation/Navigation.js | 2 +- src/libs/actions/{User.js => User.ts} | 148 +++++++++++--------------- 2 files changed, 62 insertions(+), 88 deletions(-) rename src/libs/actions/{User.js => User.ts} (85%) diff --git a/src/libs/Navigation/Navigation.js b/src/libs/Navigation/Navigation.js index de6162685079..5cbd8e9b0af6 100644 --- a/src/libs/Navigation/Navigation.js +++ b/src/libs/Navigation/Navigation.js @@ -77,7 +77,7 @@ const getActiveRouteIndex = function (route, index) { /** * Main navigation method for redirecting to a route. * @param {String} route - * @param {String} type - Type of action to perform. Currently UP is supported. + * @param {String} [type] - Type of action to perform. Currently UP is supported. */ function navigate(route = ROUTES.HOME, type) { if (!canNavigate('navigate', {route})) { diff --git a/src/libs/actions/User.js b/src/libs/actions/User.ts similarity index 85% rename from src/libs/actions/User.js rename to src/libs/actions/User.ts index 1830d1e51f6f..7f03ff7a2231 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.ts @@ -1,6 +1,4 @@ -import _ from 'underscore'; -import lodashGet from 'lodash/get'; -import Onyx from 'react-native-onyx'; +import Onyx, {OnyxUpdate} from 'react-native-onyx'; import moment from 'moment'; import ONYXKEYS from '../../ONYXKEYS'; import * as API from '../API'; @@ -18,18 +16,21 @@ import * as Session from './Session'; import * as PersonalDetails from './PersonalDetails'; import * as OnyxUpdates from './OnyxUpdates'; import redirectToSignIn from './SignInRedirect'; +import type Login from '../../types/onyx/Login'; +import type OnyxPersonalDetails from '../../types/onyx/PersonalDetails'; +import type {OnyxUpdatesFromServer} from '../../types/onyx'; -let currentUserAccountID = ''; +let currentUserAccountID = -1; let currentEmail = ''; Onyx.connect({ key: ONYXKEYS.SESSION, callback: (val) => { - currentUserAccountID = lodashGet(val, 'accountID', -1); - currentEmail = lodashGet(val, 'email', ''); + currentUserAccountID = val?.accountID ?? -1; + currentEmail = val?.email ?? ''; }, }); -let myPersonalDetails = {}; +let myPersonalDetails: Partial = {}; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (val) => { @@ -44,9 +45,9 @@ Onyx.connect({ /** * Attempt to close the user's account * - * @param {String} message optional reason for closing account + * @param message optional reason for closing account */ -function closeAccount(message) { +function closeAccount(message: string) { // Note: successData does not need to set isLoading to false because if the CloseAccount // command succeeds, a Pusher response will clear all Onyx data. API.write( @@ -75,20 +76,17 @@ function closeAccount(message) { /** * Resends a validation link to a given login - * - * @param {String} login - * @param {Boolean} isPasswordless - temporary param to trigger passwordless flow in backend */ -function resendValidateCode(login) { +function resendValidateCode(login: string) { Session.resendValidateCode(login); } /** * Requests a new validate code be sent for the passed contact method * - * @param {String} contactMethod - the new contact method that the user is trying to verify + * @param contactMethod - the new contact method that the user is trying to verify */ -function requestContactMethodValidateCode(contactMethod) { +function requestContactMethodValidateCode(contactMethod: string) { const optimisticData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -149,11 +147,9 @@ function requestContactMethodValidateCode(contactMethod) { } /** - * Sets whether or not the user is subscribed to Expensify news - * - * @param {Boolean} isSubscribed + * Sets whether the user is subscribed to Expensify news */ -function updateNewsletterSubscription(isSubscribed) { +function updateNewsletterSubscription(isSubscribed: boolean) { API.write( 'UpdateNewsletterSubscription', { @@ -181,10 +177,9 @@ function updateNewsletterSubscription(isSubscribed) { /** * Delete a specific contact method * - * @param {String} contactMethod - the contact method being deleted - * @param {Array} loginList + * @param contactMethod - the contact method being deleted */ -function deleteContactMethod(contactMethod, loginList) { +function deleteContactMethod(contactMethod: string, loginList: Record) { const oldLoginData = loginList[contactMethod]; const optimisticData = [ @@ -243,11 +238,8 @@ function deleteContactMethod(contactMethod, loginList) { /** * Clears any possible stored errors for a specific field on a contact method - * - * @param {String} contactMethod - * @param {String} fieldName */ -function clearContactMethodErrors(contactMethod, fieldName) { +function clearContactMethodErrors(contactMethod: string, fieldName: string) { Onyx.merge(ONYXKEYS.LOGIN_LIST, { [contactMethod]: { errorFields: { @@ -263,9 +255,9 @@ function clearContactMethodErrors(contactMethod, fieldName) { /** * Resets the state indicating whether a validation code has been sent to a specific contact method. * - * @param {String} contactMethod - The identifier of the contact method to reset. + * @param contactMethod - The identifier of the contact method to reset. */ -function resetContactMethodValidateCodeSentState(contactMethod) { +function resetContactMethodValidateCodeSentState(contactMethod: string) { Onyx.merge(ONYXKEYS.LOGIN_LIST, { [contactMethod]: { validateCodeSent: false, @@ -275,10 +267,8 @@ function resetContactMethodValidateCodeSentState(contactMethod) { /** * Adds a secondary login to a user's account - * - * @param {String} contactMethod */ -function addNewContactMethodAndNavigate(contactMethod) { +function addNewContactMethodAndNavigate(contactMethod: string) { const optimisticData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -333,11 +323,8 @@ function addNewContactMethodAndNavigate(contactMethod) { /** * Validates a login given an accountID and validation code - * - * @param {Number} accountID - * @param {String} validateCode */ -function validateLogin(accountID, validateCode) { +function validateLogin(accountID: number, validateCode: string) { Onyx.merge(ONYXKEYS.ACCOUNT, {...CONST.DEFAULT_ACCOUNT_DATA, isLoading: true}); const optimisticData = [ @@ -363,10 +350,9 @@ function validateLogin(accountID, validateCode) { /** * Validates a secondary login / contact method * - * @param {String} contactMethod - The contact method the user is trying to verify - * @param {String} validateCode + * @param contactMethod - The contact method the user is trying to verify */ -function validateSecondaryLogin(contactMethod, validateCode) { +function validateSecondaryLogin(contactMethod: string, validateCode: string) { const optimisticData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -448,11 +434,9 @@ function validateSecondaryLogin(contactMethod, validateCode) { * Checks the blockedFromConcierge object to see if it has an expiresAt key, * and if so whether the expiresAt date of a user's ban is before right now * - * @param {Object} blockedFromConciergeNVP - * @returns {Boolean} */ -function isBlockedFromConcierge(blockedFromConciergeNVP) { - if (_.isEmpty(blockedFromConciergeNVP)) { +function isBlockedFromConcierge(blockedFromConciergeNVP: {expiresAt: number}) { + if (!blockedFromConciergeNVP || Object.keys(blockedFromConciergeNVP).length === 0) { return false; } @@ -463,18 +447,17 @@ function isBlockedFromConcierge(blockedFromConciergeNVP) { return moment().isBefore(moment(blockedFromConciergeNVP.expiresAt), 'day'); } -function triggerNotifications(onyxUpdates) { - _.each(onyxUpdates, (update) => { +function triggerNotifications(onyxUpdates: any) { + onyxUpdates.forEach((update) => { if (!update.shouldNotify) { return; } const reportID = update.key.replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, ''); - const reportActions = _.values(update.value); + const reportActions = Object.values(update.value); - // eslint-disable-next-line rulesdir/no-negated-variables - const notifiableActions = _.filter(reportActions, (action) => ReportActionsUtils.isNotifiableReportAction(action)); - _.each(notifiableActions, (action) => Report.showReportActionNotification(reportID, action)); + const notifiableActions = reportActions.filter((action) => ReportActionsUtils.isNotifiableReportAction(action)); + notifiableActions.forEach((action) => Report.showReportActionNotification(reportID, action)); }); } @@ -490,7 +473,7 @@ function subscribeToUserEvents() { // Handles the mega multipleEvents from Pusher which contains an array of single events. // Each single event is passed to PusherUtils in order to trigger the callbacks for that event - PusherUtils.subscribeToPrivateUserChannelEvent(Pusher.TYPE.MULTIPLE_EVENTS, currentUserAccountID, (pushJSON) => { + PusherUtils.subscribeToPrivateUserChannelEvent(Pusher.TYPE.MULTIPLE_EVENTS, currentUserAccountID.toString(), (pushJSON: OnyxUpdatesFromServer) => { // The data for this push event comes in two different formats: // 1. Original format - this is what was sent before the RELIABLE_UPDATES project and will go away once RELIABLE_UPDATES is fully complete // - The data is an array of objects, where each object is an onyx update @@ -498,8 +481,8 @@ function subscribeToUserEvents() { // 1. Reliable updates format - this is what was sent with the RELIABLE_UPDATES project and will be the format from now on // - The data is an object, containing updateIDs from the server and an array of onyx updates (this array is the same format as the original format above) // Example: {lastUpdateID: 1, previousUpdateID: 0, updates: [{onyxMethod: 'whatever', key: 'foo', value: 'bar'}]} - if (_.isArray(pushJSON)) { - _.each(pushJSON, (multipleEvent) => { + if (Array.isArray(pushJSON)) { + pushJSON.forEach((multipleEvent) => { PusherUtils.triggerMultiEventHandler(multipleEvent.eventType, multipleEvent.data); }); return; @@ -512,7 +495,7 @@ function subscribeToUserEvents() { previousUpdateID: Number(pushJSON.previousUpdateID || 0), }; if (!OnyxUpdates.doesClientNeedToBeUpdated(Number(pushJSON.previousUpdateID || 0))) { - OnyxUpdates.apply(updates); + OnyxUpdates.apply(updates as any); return; } @@ -522,7 +505,7 @@ function subscribeToUserEvents() { }); // Handles Onyx updates coming from Pusher through the mega multipleEvents. - PusherUtils.subscribeToMultiEvent(Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE, (pushJSON) => + PusherUtils.subscribeToMultiEvent(Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE, (pushJSON: OnyxUpdate[]) => SequentialQueue.getCurrentRequest().then(() => { // If we don't have the currentUserAccountID (user is logged out) we don't want to update Onyx with data from Pusher if (!currentUserAccountID) { @@ -541,9 +524,8 @@ function subscribeToUserEvents() { /** * Sync preferredSkinTone with Onyx and Server - * @param {String} skinTone */ -function updatePreferredSkinTone(skinTone) { +function updatePreferredSkinTone(skinTone: string) { const optimisticData = [ { onyxMethod: Onyx.METHOD.SET, @@ -562,9 +544,8 @@ function updatePreferredSkinTone(skinTone) { /** * Sync frequentlyUsedEmojis with Onyx and Server - * @param {Object[]} frequentlyUsedEmojis */ -function updateFrequentlyUsedEmojis(frequentlyUsedEmojis) { +function updateFrequentlyUsedEmojis(frequentlyUsedEmojis: string[]) { const optimisticData = [ { onyxMethod: Onyx.METHOD.SET, @@ -583,9 +564,8 @@ function updateFrequentlyUsedEmojis(frequentlyUsedEmojis) { /** * Sync user chat priority mode with Onyx and Server - * @param {String} mode */ -function updateChatPriorityMode(mode) { +function updateChatPriorityMode(mode: string) { const optimisticData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -603,10 +583,7 @@ function updateChatPriorityMode(mode) { Navigation.goBack(ROUTES.SETTINGS_PREFERENCES); } -/** - * @param {Boolean} shouldUseStagingServer - */ -function setShouldUseStagingServer(shouldUseStagingServer) { +function setShouldUseStagingServer(shouldUseStagingServer: boolean) { Onyx.merge(ONYXKEYS.USER, {shouldUseStagingServer}); } @@ -623,19 +600,19 @@ function clearScreenShareRequest() { /** * Open an OldDot tab linking to a screen share request. - * @param {String} accessToken Access token required to join a screen share room, generated by the backend - * @param {String} roomName Name of the screen share room to join + * @param accessToken Access token required to join a screen share room, generated by the backend + * @param roomName Name of the screen share room to join */ -function joinScreenShare(accessToken, roomName) { +function joinScreenShare(accessToken: string, roomName: string) { Link.openOldDotLink(`inbox?action=screenShare&accessToken=${accessToken}&name=${roomName}`); clearScreenShareRequest(); } /** * Downloads the statement PDF for the provided period - * @param {String} period YYYYMM format + * @param period YYYYMM format */ -function generateStatementPDF(period) { +function generateStatementPDF(period: string) { API.read( 'GetStatementPDF', {period}, @@ -673,10 +650,8 @@ function generateStatementPDF(period) { /** * Sets a contact method / secondary login as the user's "Default" contact method. - * - * @param {String} newDefaultContactMethod */ -function setContactMethodAsDefault(newDefaultContactMethod) { +function setContactMethodAsDefault(newDefaultContactMethod: string) { const oldDefaultContactMethod = currentEmail; const optimisticData = [ { @@ -754,14 +729,19 @@ function setContactMethodAsDefault(newDefaultContactMethod) { }, }, ]; - API.write('SetContactMethodAsDefault', {partnerUserID: newDefaultContactMethod}, {optimisticData, successData, failureData}); + API.write( + 'SetContactMethodAsDefault', + {partnerUserID: newDefaultContactMethod}, + { + optimisticData, + successData, + failureData, + }, + ); Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS); } -/** - * @param {String} theme - */ -function updateTheme(theme) { +function updateTheme(theme: string) { const optimisticData = [ { onyxMethod: Onyx.METHOD.SET, @@ -783,12 +763,10 @@ function updateTheme(theme) { /** * Sets a custom status - * - * @param {Object} status - * @param {String} status.text - * @param {String} status.emojiCode */ -function updateCustomStatus(status) { +type CustomStatus = {text: string; emojiCode: string; clearAfter?: string}; + +function updateCustomStatus(status: CustomStatus) { API.write('UpdateStatus', status, { optimisticData: [ { @@ -826,18 +804,14 @@ function clearCustomStatus() { /** * Sets a custom status * - * @param {Object} status - * @param {String} status.text - * @param {String} status.emojiCode - * @param {String} status.clearAfter - ISO 8601 format string, which represents the time when the status should be cleared + * @param status.clearAfter - ISO 8601 format string, which represents the time when the status should be cleared */ -function updateDraftCustomStatus(status) { +function updateDraftCustomStatus(status: CustomStatus) { Onyx.merge(ONYXKEYS.CUSTOM_STATUS_DRAFT, status); } /** * Clear the custom draft status - * */ function clearDraftCustomStatus() { Onyx.merge(ONYXKEYS.CUSTOM_STATUS_DRAFT, {text: '', emojiCode: '', clearAfter: ''}); From 7dbca33e8f5f69a5094e474a8e59365397efab2d Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Tue, 3 Oct 2023 11:13:34 +0200 Subject: [PATCH 002/193] Uninlined types. --- src/libs/actions/User.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 7f03ff7a2231..2ab91f6d59cd 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -20,6 +20,9 @@ import type Login from '../../types/onyx/Login'; import type OnyxPersonalDetails from '../../types/onyx/PersonalDetails'; import type {OnyxUpdatesFromServer} from '../../types/onyx'; +type CustomStatus = {text: string; emojiCode: string; clearAfter?: string}; +type BlockedFromConciergeNVP = {expiresAt: number}; + let currentUserAccountID = -1; let currentEmail = ''; Onyx.connect({ @@ -435,7 +438,7 @@ function validateSecondaryLogin(contactMethod: string, validateCode: string) { * and if so whether the expiresAt date of a user's ban is before right now * */ -function isBlockedFromConcierge(blockedFromConciergeNVP: {expiresAt: number}) { +function isBlockedFromConcierge(blockedFromConciergeNVP: BlockedFromConciergeNVP) { if (!blockedFromConciergeNVP || Object.keys(blockedFromConciergeNVP).length === 0) { return false; } @@ -764,8 +767,6 @@ function updateTheme(theme: string) { /** * Sets a custom status */ -type CustomStatus = {text: string; emojiCode: string; clearAfter?: string}; - function updateCustomStatus(status: CustomStatus) { API.write('UpdateStatus', status, { optimisticData: [ From 45b40db6bd5e28135feb34f1722501bffb41da00 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Thu, 5 Oct 2023 10:59:43 +0200 Subject: [PATCH 003/193] WIP: Work sync --- src/libs/actions/User.ts | 398 +++++++++++++----------- src/types/onyx/OnyxUpdatesFromServer.ts | 6 +- src/types/onyx/PersonalDetails.ts | 3 + 3 files changed, 218 insertions(+), 189 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 2ab91f6d59cd..993559ffc2ec 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -1,4 +1,4 @@ -import Onyx, {OnyxUpdate} from 'react-native-onyx'; +import Onyx, {OnyxCollection, OnyxUpdate} from 'react-native-onyx'; import moment from 'moment'; import ONYXKEYS from '../../ONYXKEYS'; import * as API from '../API'; @@ -18,7 +18,9 @@ import * as OnyxUpdates from './OnyxUpdates'; import redirectToSignIn from './SignInRedirect'; import type Login from '../../types/onyx/Login'; import type OnyxPersonalDetails from '../../types/onyx/PersonalDetails'; -import type {OnyxUpdatesFromServer} from '../../types/onyx'; +import type {FrequentlyUsedEmoji, OnyxUpdatesFromServer} from '../../types/onyx'; +import {OnyxServerUpdate} from '../../types/onyx/OnyxUpdatesFromServer'; +import ReportAction from '../../types/onyx/ReportAction'; type CustomStatus = {text: string; emojiCode: string; clearAfter?: string}; type BlockedFromConciergeNVP = {expiresAt: number}; @@ -27,21 +29,21 @@ let currentUserAccountID = -1; let currentEmail = ''; Onyx.connect({ key: ONYXKEYS.SESSION, - callback: (val) => { - currentUserAccountID = val?.accountID ?? -1; - currentEmail = val?.email ?? ''; + callback: (value) => { + currentUserAccountID = value?.accountID ?? -1; + currentEmail = value?.email ?? ''; }, }); let myPersonalDetails: Partial = {}; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, - callback: (val) => { - if (!val || !currentUserAccountID) { + callback: (value) => { + if (!value || !currentUserAccountID) { return; } - myPersonalDetails = val[currentUserAccountID]; + myPersonalDetails = value[currentUserAccountID]; }, }); @@ -53,32 +55,38 @@ Onyx.connect({ function closeAccount(message: string) { // Note: successData does not need to set isLoading to false because if the CloseAccount // command succeeds, a Pusher response will clear all Onyx data. - API.write( - 'CloseAccount', - {message}, - { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM, - value: {isLoading: true}, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM, - value: {isLoading: false}, - }, - ], + + type CloseAccountParam = {message: string}; + + const parameters: CloseAccountParam = {message}; + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM, + value: {isLoading: true}, }, - ); + ]; + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM, + value: {isLoading: false}, + }, + ]; + + API.write('CloseAccount', parameters, { + optimisticData, + failureData, + }); // Run cleanup actions to prevent reconnection callbacks from blocking logging in again redirectToSignIn(); } /** * Resends a validation link to a given login + * @param login + * @param isPasswordless - temporary param to trigger passwordless flow in backend */ function resendValidateCode(login: string) { Session.resendValidateCode(login); @@ -90,7 +98,7 @@ function resendValidateCode(login: string) { * @param contactMethod - the new contact method that the user is trying to verify */ function requestContactMethodValidateCode(contactMethod: string) { - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.LOGIN_LIST, @@ -108,7 +116,7 @@ function requestContactMethodValidateCode(contactMethod: string) { }, }, ]; - const successData = [ + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.LOGIN_LIST, @@ -122,7 +130,7 @@ function requestContactMethodValidateCode(contactMethod: string) { }, }, ]; - const failureData = [ + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.LOGIN_LIST, @@ -140,52 +148,51 @@ function requestContactMethodValidateCode(contactMethod: string) { }, ]; - API.write( - 'RequestContactMethodValidateCode', - { - email: contactMethod, - }, - {optimisticData, successData, failureData}, - ); + type RequestContactMethodValidateCodeParam = {email: string}; + + const parameters: RequestContactMethodValidateCodeParam = {email: contactMethod}; + + API.write('RequestContactMethodValidateCode', parameters, {optimisticData, successData, failureData}); } /** * Sets whether the user is subscribed to Expensify news */ function updateNewsletterSubscription(isSubscribed: boolean) { - API.write( - 'UpdateNewsletterSubscription', + type UpdateNewsletterSubscriptionParam = {isSubscribed: boolean}; + + const parameters: UpdateNewsletterSubscriptionParam = {isSubscribed}; + + const optimisticData: OnyxUpdate[] = [ { - isSubscribed, + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.USER, + value: {isSubscribedToNewsletter: isSubscribed}, }, + ]; + const failureData: OnyxUpdate[] = [ { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.USER, - value: {isSubscribedToNewsletter: isSubscribed}, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.USER, - value: {isSubscribedToNewsletter: !isSubscribed}, - }, - ], + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.USER, + value: {isSubscribedToNewsletter: !isSubscribed}, }, - ); + ]; + + API.write('UpdateNewsletterSubscription', parameters, { + optimisticData, + failureData, + }); } /** * Delete a specific contact method - * * @param contactMethod - the contact method being deleted + * @param loginList */ function deleteContactMethod(contactMethod: string, loginList: Record) { const oldLoginData = loginList[contactMethod]; - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.LOGIN_LIST, @@ -202,7 +209,7 @@ function deleteContactMethod(contactMethod: string, loginList: Record { if (!update.shouldNotify) { return; } const reportID = update.key.replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, ''); - const reportActions = Object.values(update.value); + const reportActions = Object.values((update.value as OnyxCollection) ?? {}); - const notifiableActions = reportActions.filter((action) => ReportActionsUtils.isNotifiableReportAction(action)); - notifiableActions.forEach((action) => Report.showReportActionNotification(reportID, action)); + const actions = reportActions.filter((action) => ReportActionsUtils.isNotifiableReportAction(action)) as ReportAction[]; + actions.forEach((action) => Report.showReportActionNotification(reportID, action)); }); } @@ -498,7 +506,7 @@ function subscribeToUserEvents() { previousUpdateID: Number(pushJSON.previousUpdateID || 0), }; if (!OnyxUpdates.doesClientNeedToBeUpdated(Number(pushJSON.previousUpdateID || 0))) { - OnyxUpdates.apply(updates as any); + OnyxUpdates.apply(updates); return; } @@ -529,60 +537,62 @@ function subscribeToUserEvents() { * Sync preferredSkinTone with Onyx and Server */ function updatePreferredSkinTone(skinTone: string) { - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.SET, key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, value: skinTone, }, ]; - API.write( - 'UpdatePreferredEmojiSkinTone', - { - value: skinTone, - }, - {optimisticData}, - ); + + type UpdatePreferredEmojiSkinTone = { + value: string; + }; + + const parameters: UpdatePreferredEmojiSkinTone = {value: skinTone}; + + API.write('UpdatePreferredEmojiSkinTone', parameters, {optimisticData}); } /** * Sync frequentlyUsedEmojis with Onyx and Server */ -function updateFrequentlyUsedEmojis(frequentlyUsedEmojis: string[]) { - const optimisticData = [ +function updateFrequentlyUsedEmojis(frequentlyUsedEmojis: FrequentlyUsedEmoji[]) { + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.SET, key: ONYXKEYS.FREQUENTLY_USED_EMOJIS, value: frequentlyUsedEmojis, }, ]; - API.write( - 'UpdateFrequentlyUsedEmojis', - { - value: JSON.stringify(frequentlyUsedEmojis), - }, - {optimisticData}, - ); + type UpdateFrequentlyUsedEmojisParam = {value: string}; + + const parameters: UpdateFrequentlyUsedEmojisParam = {value: JSON.stringify(frequentlyUsedEmojis)}; + + API.write('UpdateFrequentlyUsedEmojis', parameters, {optimisticData}); } /** * Sync user chat priority mode with Onyx and Server */ function updateChatPriorityMode(mode: string) { - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_PRIORITY_MODE, value: mode, }, ]; - API.write( - 'UpdateChatPriorityMode', - { - value: mode, - }, - {optimisticData}, - ); + + type UpdateChatPriorityModeParam = { + value: string; + }; + + const parameters: UpdateChatPriorityModeParam = { + value: mode, + }; + + API.write('UpdateChatPriorityMode', parameters, {optimisticData}); Navigation.goBack(ROUTES.SETTINGS_PREFERENCES); } @@ -616,37 +626,40 @@ function joinScreenShare(accessToken: string, roomName: string) { * @param period YYYYMM format */ function generateStatementPDF(period: string) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.WALLET_STATEMENT, + value: { + isGenerating: true, + }, + }, + ]; + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.WALLET_STATEMENT, + value: { + isGenerating: false, + }, + }, + ]; + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.WALLET_STATEMENT, + value: { + isGenerating: false, + }, + }, + ]; API.read( 'GetStatementPDF', {period}, { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.WALLET_STATEMENT, - value: { - isGenerating: true, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.WALLET_STATEMENT, - value: { - isGenerating: false, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.WALLET_STATEMENT, - value: { - isGenerating: false, - }, - }, - ], + optimisticData, + successData, + failureData, }, ); } @@ -656,7 +669,7 @@ function generateStatementPDF(period: string) { */ function setContactMethodAsDefault(newDefaultContactMethod: string) { const oldDefaultContactMethod = currentEmail; - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.SESSION, @@ -689,7 +702,7 @@ function setContactMethodAsDefault(newDefaultContactMethod: string) { }, }, ]; - const successData = [ + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.LOGIN_LIST, @@ -702,7 +715,7 @@ function setContactMethodAsDefault(newDefaultContactMethod: string) { }, }, ]; - const failureData = [ + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.SESSION, @@ -732,20 +745,25 @@ function setContactMethodAsDefault(newDefaultContactMethod: string) { }, }, ]; - API.write( - 'SetContactMethodAsDefault', - {partnerUserID: newDefaultContactMethod}, - { - optimisticData, - successData, - failureData, - }, - ); + + type SetContactMethodAsDefaultParam = { + partnerUserID: string; + }; + + const parameters: SetContactMethodAsDefaultParam = { + partnerUserID: newDefaultContactMethod, + }; + + API.write('SetContactMethodAsDefault', parameters, { + optimisticData, + successData, + failureData, + }); Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS); } function updateTheme(theme: string) { - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.SET, key: ONYXKEYS.PREFERRED_THEME, @@ -753,13 +771,15 @@ function updateTheme(theme: string) { }, ]; - API.write( - 'UpdateTheme', - { - value: theme, - }, - {optimisticData}, - ); + type UpdateThemeParam = { + value: string; + }; + + const parameters: UpdateThemeParam = { + value: theme, + }; + + API.write('UpdateTheme', parameters, {optimisticData}); Navigation.navigate(ROUTES.SETTINGS_PREFERENCES); } @@ -768,18 +788,19 @@ function updateTheme(theme: string) { * Sets a custom status */ function updateCustomStatus(status: CustomStatus) { - API.write('UpdateStatus', status, { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: { - [currentUserAccountID]: { - status, - }, + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: { + [currentUserAccountID]: { + status, }, }, - ], + }, + ]; + API.write('UpdateStatus', status, { + optimisticData, }); } @@ -787,24 +808,27 @@ function updateCustomStatus(status: CustomStatus) { * Clears the custom status */ function clearCustomStatus() { - API.write('ClearStatus', undefined, { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: { - [currentUserAccountID]: { - status: null, // Clearing the field - }, + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: { + [currentUserAccountID]: { + status: null, // Clearing the field }, }, - ], + }, + ]; + API.write('ClearStatus', undefined, { + optimisticData, }); } /** * Sets a custom status * + * @param status.text + * @param status.emojiCode * @param status.clearAfter - ISO 8601 format string, which represents the time when the status should be cleared */ function updateDraftCustomStatus(status: CustomStatus) { diff --git a/src/types/onyx/OnyxUpdatesFromServer.ts b/src/types/onyx/OnyxUpdatesFromServer.ts index 50b1503b90bd..843d3ae86e46 100644 --- a/src/types/onyx/OnyxUpdatesFromServer.ts +++ b/src/types/onyx/OnyxUpdatesFromServer.ts @@ -2,9 +2,11 @@ import {OnyxUpdate} from 'react-native-onyx'; import Request from './Request'; import Response from './Response'; +type OnyxServerUpdate = OnyxUpdate & {shouldNotify?: boolean}; + type OnyxUpdateEvent = { eventType: string; - data: OnyxUpdate[]; + data: OnyxServerUpdate[]; }; type OnyxUpdatesFromServer = { @@ -16,4 +18,4 @@ type OnyxUpdatesFromServer = { updates?: OnyxUpdateEvent[]; }; -export type {OnyxUpdatesFromServer, OnyxUpdateEvent}; +export type {OnyxUpdatesFromServer, OnyxUpdateEvent, OnyxServerUpdate}; diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index 201273beac63..6bb41849b0b6 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -37,6 +37,9 @@ type PersonalDetails = { /** Pronouns of the current user from their personal details */ pronouns?: string; + /** User status */ + status: {text: string; emojiCode: string; clearAfter?: string} | null; + /** Local currency for the user */ localCurrencyCode?: string; From 671cd2dc47a8a08a1ee08f850a6fcf2bdf5e935b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Wed, 8 Nov 2023 18:18:34 -0600 Subject: [PATCH 004/193] Update TEACHERS_UNITE constants --- src/CONST.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index ce9329d909ae..f619dcb9fc7e 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2717,8 +2717,10 @@ const CONST = { ATTACHMENT: 'common.attachment', }, TEACHERS_UNITE: { - PUBLIC_ROOM_ID: '7470147100835202', - POLICY_ID: 'B795B6319125BDF2', + PROD_PUBLIC_ROOM_ID: '7470147100835202', + PROD_POLICY_ID: 'B795B6319125BDF2', + TEST_PUBLIC_ROOM_ID: '207591744844000', + TEST_POLICY_ID: 'ABD1345ED7293535', POLICY_NAME: 'Expensify.org / Teachers Unite!', PUBLIC_ROOM_NAME: '#teachers-unite', }, From 3f45a72b7a09c0f0f5361c5ed27d5567ae53d268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Wed, 8 Nov 2023 18:18:48 -0600 Subject: [PATCH 005/193] Add policyID and publicRoomReportID parameters to referTeachersUniteVolunteer and addSchoolPrincipal functions --- src/libs/actions/TeachersUnite.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/TeachersUnite.js b/src/libs/actions/TeachersUnite.js index 98b1f82629a4..8bcd0ea1e42f 100644 --- a/src/libs/actions/TeachersUnite.js +++ b/src/libs/actions/TeachersUnite.js @@ -28,9 +28,11 @@ Onyx.connect({ * @param {String} partnerUserID * @param {String} firstName * @param {String} lastName + * @param {String} policyID + * @param {String} publicRoomReportID */ -function referTeachersUniteVolunteer(partnerUserID, firstName, lastName) { - const optimisticPublicRoom = ReportUtils.buildOptimisticChatReport([], CONST.TEACHERS_UNITE.PUBLIC_ROOM_NAME, CONST.REPORT.CHAT_TYPE.POLICY_ROOM, CONST.TEACHERS_UNITE.POLICY_ID); +function referTeachersUniteVolunteer(partnerUserID, firstName, lastName, policyID, publicRoomReportID) { + const optimisticPublicRoom = ReportUtils.buildOptimisticChatReport([], CONST.TEACHERS_UNITE.PUBLIC_ROOM_NAME, CONST.REPORT.CHAT_TYPE.POLICY_ROOM, policyID); const optimisticData = [ { onyxMethod: Onyx.METHOD.SET, @@ -52,7 +54,7 @@ function referTeachersUniteVolunteer(partnerUserID, firstName, lastName) { }, {optimisticData}, ); - Navigation.dismissModal(CONST.TEACHERS_UNITE.PUBLIC_ROOM_ID); + Navigation.dismissModal(publicRoomReportID); } /** @@ -60,10 +62,10 @@ function referTeachersUniteVolunteer(partnerUserID, firstName, lastName) { * @param {String} firstName * @param {String} partnerUserID * @param {String} lastName + * @param {String} policyID */ -function addSchoolPrincipal(firstName, partnerUserID, lastName) { +function addSchoolPrincipal(firstName, partnerUserID, lastName, policyID) { const policyName = CONST.TEACHERS_UNITE.POLICY_NAME; - const policyID = CONST.TEACHERS_UNITE.POLICY_ID; const loggedInEmail = OptionsListUtils.addSMSDomainIfPhoneNumber(sessionEmail); const reportCreationData = {}; From e2665723040d62dd0cecbb090ebaefa450efa821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Wed, 8 Nov 2023 18:19:02 -0600 Subject: [PATCH 006/193] Add useEnvironment hook to IntroSchoolPrincipalPage --- src/pages/TeachersUnite/IntroSchoolPrincipalPage.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js b/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js index 16389d69053d..a2b658238523 100644 --- a/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js +++ b/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js @@ -5,6 +5,7 @@ import React, {useCallback} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; +import useEnvironment from '@hooks/useEnvironment'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -35,6 +36,7 @@ const defaultProps = { function IntroSchoolPrincipalPage(props) { const {translate} = useLocalize(); + const {environment} = useEnvironment(); /** * @param {Object} values @@ -43,7 +45,8 @@ function IntroSchoolPrincipalPage(props) { * @param {String} values.lastName */ const onSubmit = (values) => { - TeachersUnite.addSchoolPrincipal(values.firstName.trim(), values.partnerUserID.trim(), values.lastName.trim()); + const policyID = environment === CONST.ENVIRONMENT.PRODUCTION ? CONST.TEACHERS_UNITE.PROD_POLICY_ID : CONST.TEACHERS_UNITE.TEST_POLICY_ID; + TeachersUnite.addSchoolPrincipal(values.firstName.trim(), values.partnerUserID.trim(), values.lastName.trim(), policyID); }; /** From 0903d8f096a2efb74ae668c8e1d2f12028ea7fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Wed, 8 Nov 2023 18:19:12 -0600 Subject: [PATCH 007/193] Add useEnvironment hook to KnowATeacherPage --- src/pages/TeachersUnite/KnowATeacherPage.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/TeachersUnite/KnowATeacherPage.js b/src/pages/TeachersUnite/KnowATeacherPage.js index 696a9ef8b704..015ed095b7ac 100644 --- a/src/pages/TeachersUnite/KnowATeacherPage.js +++ b/src/pages/TeachersUnite/KnowATeacherPage.js @@ -11,6 +11,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; +import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as LoginUtils from '@libs/LoginUtils'; @@ -36,6 +37,7 @@ const defaultProps = { function KnowATeacherPage(props) { const {translate} = useLocalize(); + const {environment} = useEnvironment(); /** * Submit form to pass firstName, partnerUserID and lastName @@ -51,7 +53,10 @@ function KnowATeacherPage(props) { const firstName = values.firstName.trim(); const lastName = values.lastName.trim(); - TeachersUnite.referTeachersUniteVolunteer(contactMethod, firstName, lastName); + + const policyID = environment === CONST.ENVIRONMENT.PRODUCTION ? CONST.TEACHERS_UNITE.PROD_POLICY_ID : CONST.TEACHERS_UNITE.TEST_POLICY_ID; + const publicRoomReportID = environment === CONST.ENVIRONMENT.PRODUCTION ? CONST.TEACHERS_UNITE.PROD_PUBLIC_ROOM_ID : CONST.TEACHERS_UNITE.TEST_PUBLIC_ROOM_ID; + TeachersUnite.referTeachersUniteVolunteer(contactMethod, firstName, lastName, policyID, publicRoomReportID); }; /** From 30f0fccb98f0836ea524aa34d33fc99a21730de4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Wed, 8 Nov 2023 18:26:45 -0600 Subject: [PATCH 008/193] Show "I'm a teacher" button always --- src/pages/TeachersUnite/SaveTheWorldPage.js | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/pages/TeachersUnite/SaveTheWorldPage.js b/src/pages/TeachersUnite/SaveTheWorldPage.js index 76e4c42294c1..940ac0432db3 100644 --- a/src/pages/TeachersUnite/SaveTheWorldPage.js +++ b/src/pages/TeachersUnite/SaveTheWorldPage.js @@ -28,9 +28,8 @@ const defaultProps = { policy: {}, }; -function SaveTheWorldPage(props) { +function SaveTheWorldPage() { const {translate} = useLocalize(); - const isTeacherAlreadyInvited = !_.isUndefined(props.policy) && props.policy.role === CONST.POLICY.ROLE.USER; return ( Navigation.navigate(ROUTES.I_KNOW_A_TEACHER)} /> - {!isTeacherAlreadyInvited && ( - Navigation.navigate(ROUTES.I_AM_A_TEACHER)} - /> - )} + Navigation.navigate(ROUTES.I_AM_A_TEACHER)} + /> ); } @@ -66,8 +63,4 @@ SaveTheWorldPage.propTypes = propTypes; SaveTheWorldPage.defaultProps = defaultProps; SaveTheWorldPage.displayName = 'SaveTheWorldPage'; -export default withOnyx({ - policy: { - key: () => `${ONYXKEYS.COLLECTION.POLICY}${CONST.TEACHERS_UNITE.POLICY_ID}`, - }, -})(SaveTheWorldPage); +export default SaveTheWorldPage; From 880a00a3cbea3a157b4b1522e805871c496e1f78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Wed, 8 Nov 2023 19:13:31 -0600 Subject: [PATCH 009/193] Remove unused imports from SaveTheWorldPage.js --- src/pages/TeachersUnite/SaveTheWorldPage.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/pages/TeachersUnite/SaveTheWorldPage.js b/src/pages/TeachersUnite/SaveTheWorldPage.js index 940ac0432db3..f825b0760408 100644 --- a/src/pages/TeachersUnite/SaveTheWorldPage.js +++ b/src/pages/TeachersUnite/SaveTheWorldPage.js @@ -1,8 +1,6 @@ -import _ from 'lodash'; import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; import IllustratedHeaderPageLayout from '@components/IllustratedHeaderPageLayout'; import * as LottieAnimations from '@components/LottieAnimations'; import MenuItem from '@components/MenuItem'; @@ -11,8 +9,6 @@ import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import styles from '@styles/styles'; import themeColors from '@styles/themes/default'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; From 8c9a58795cde0369d5e7eb8e84169cc7b6f0657e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Wed, 8 Nov 2023 19:13:56 -0600 Subject: [PATCH 010/193] Refactor environment variable to isProduction in IntroSchoolPrincipalPage --- src/pages/TeachersUnite/IntroSchoolPrincipalPage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js b/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js index a2b658238523..1e02281cff0e 100644 --- a/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js +++ b/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js @@ -36,7 +36,7 @@ const defaultProps = { function IntroSchoolPrincipalPage(props) { const {translate} = useLocalize(); - const {environment} = useEnvironment(); + const {isProduction} = useEnvironment(); /** * @param {Object} values @@ -45,7 +45,7 @@ function IntroSchoolPrincipalPage(props) { * @param {String} values.lastName */ const onSubmit = (values) => { - const policyID = environment === CONST.ENVIRONMENT.PRODUCTION ? CONST.TEACHERS_UNITE.PROD_POLICY_ID : CONST.TEACHERS_UNITE.TEST_POLICY_ID; + const policyID = isProduction ? CONST.TEACHERS_UNITE.PROD_POLICY_ID : CONST.TEACHERS_UNITE.TEST_POLICY_ID; TeachersUnite.addSchoolPrincipal(values.firstName.trim(), values.partnerUserID.trim(), values.lastName.trim(), policyID); }; From 1fb005d3c0243a6fcbffd671bfaf3937e1b9b1bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Wed, 8 Nov 2023 19:14:12 -0600 Subject: [PATCH 011/193] Refactor useEnvironment hook to use isProduction instead of environment --- src/pages/TeachersUnite/KnowATeacherPage.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/TeachersUnite/KnowATeacherPage.js b/src/pages/TeachersUnite/KnowATeacherPage.js index 015ed095b7ac..9bf68a5dbb28 100644 --- a/src/pages/TeachersUnite/KnowATeacherPage.js +++ b/src/pages/TeachersUnite/KnowATeacherPage.js @@ -37,7 +37,7 @@ const defaultProps = { function KnowATeacherPage(props) { const {translate} = useLocalize(); - const {environment} = useEnvironment(); + const {isProduction} = useEnvironment(); /** * Submit form to pass firstName, partnerUserID and lastName @@ -54,8 +54,8 @@ function KnowATeacherPage(props) { const lastName = values.lastName.trim(); - const policyID = environment === CONST.ENVIRONMENT.PRODUCTION ? CONST.TEACHERS_UNITE.PROD_POLICY_ID : CONST.TEACHERS_UNITE.TEST_POLICY_ID; - const publicRoomReportID = environment === CONST.ENVIRONMENT.PRODUCTION ? CONST.TEACHERS_UNITE.PROD_PUBLIC_ROOM_ID : CONST.TEACHERS_UNITE.TEST_PUBLIC_ROOM_ID; + const policyID = isProduction ? CONST.TEACHERS_UNITE.PROD_POLICY_ID : CONST.TEACHERS_UNITE.TEST_POLICY_ID; + const publicRoomReportID = isProduction ? CONST.TEACHERS_UNITE.PROD_PUBLIC_ROOM_ID : CONST.TEACHERS_UNITE.TEST_PUBLIC_ROOM_ID; TeachersUnite.referTeachersUniteVolunteer(contactMethod, firstName, lastName, policyID, publicRoomReportID); }; From 5ff73dcc41f30fcd08bd4939228cd85dec6da1c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Wed, 8 Nov 2023 19:57:40 -0600 Subject: [PATCH 012/193] Fix import statement and remove unnecessary whitespace --- src/pages/TeachersUnite/IntroSchoolPrincipalPage.js | 2 +- src/pages/TeachersUnite/KnowATeacherPage.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js b/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js index 1e02281cff0e..a677d6802391 100644 --- a/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js +++ b/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js @@ -5,13 +5,13 @@ import React, {useCallback} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; -import useEnvironment from '@hooks/useEnvironment'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; +import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; diff --git a/src/pages/TeachersUnite/KnowATeacherPage.js b/src/pages/TeachersUnite/KnowATeacherPage.js index 9bf68a5dbb28..d4e0f6ea7957 100644 --- a/src/pages/TeachersUnite/KnowATeacherPage.js +++ b/src/pages/TeachersUnite/KnowATeacherPage.js @@ -53,7 +53,6 @@ function KnowATeacherPage(props) { const firstName = values.firstName.trim(); const lastName = values.lastName.trim(); - const policyID = isProduction ? CONST.TEACHERS_UNITE.PROD_POLICY_ID : CONST.TEACHERS_UNITE.TEST_POLICY_ID; const publicRoomReportID = isProduction ? CONST.TEACHERS_UNITE.PROD_PUBLIC_ROOM_ID : CONST.TEACHERS_UNITE.TEST_PUBLIC_ROOM_ID; TeachersUnite.referTeachersUniteVolunteer(contactMethod, firstName, lastName, policyID, publicRoomReportID); From 9ce523714c7ecf6f4b15aa40656b848e6d730e66 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Tue, 14 Nov 2023 17:39:00 +0100 Subject: [PATCH 013/193] User.ts remigrated. --- src/libs/Network/SequentialQueue.ts | 2 +- src/libs/actions/User.ts | 52 +++++++++-------------------- 2 files changed, 16 insertions(+), 38 deletions(-) diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index d4aee4a221e5..4ce97f349194 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -176,7 +176,7 @@ function push(request: OnyxRequest) { flush(); } -function getCurrentRequest(): OnyxRequest | Promise { +function getCurrentRequest(): Promise { if (currentRequest === null) { return Promise.resolve(); } diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index e58612210485..7c959b390838 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -1,7 +1,6 @@ import {isBefore} from 'date-fns'; -import lodashGet from 'lodash/get'; -import Onyx from 'react-native-onyx'; -import _ from 'underscore'; +import Onyx, {OnyxCollection, OnyxUpdate} from 'react-native-onyx'; +import {ValueOf} from 'type-fest'; import * as API from '@libs/API'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -12,25 +11,17 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import Onyx, {OnyxCollection, OnyxUpdate} from 'react-native-onyx'; -import moment from 'moment'; -import ONYXKEYS from '../../ONYXKEYS'; -import * as API from '../API'; -import CONST from '../../CONST'; -import Navigation from '../Navigation/Navigation'; -import ROUTES from '../../ROUTES'; -import * as Pusher from '../Pusher/pusher'; +import type {FrequentlyUsedEmoji} from '@src/types/onyx'; +import type Login from '@src/types/onyx/Login'; +import {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer'; +import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails'; +import ReportAction from '@src/types/onyx/ReportAction'; import * as Link from './Link'; import * as OnyxUpdates from './OnyxUpdates'; import * as PersonalDetails from './PersonalDetails'; import * as Report from './Report'; import * as Session from './Session'; import redirectToSignIn from './SignInRedirect'; -import type Login from '../../types/onyx/Login'; -import type OnyxPersonalDetails from '../../types/onyx/PersonalDetails'; -import type {FrequentlyUsedEmoji, OnyxUpdatesFromServer} from '../../types/onyx'; -import {OnyxServerUpdate} from '../../types/onyx/OnyxUpdatesFromServer'; -import ReportAction from '../../types/onyx/ReportAction'; type CustomStatus = {text: string; emojiCode: string; clearAfter?: string}; type BlockedFromConciergeNVP = {expiresAt: number}; @@ -96,7 +87,6 @@ function closeAccount(message: string) { /** * Resends a validation link to a given login * @param login - * @param isPasswordless - temporary param to trigger passwordless flow in backend */ function resendValidateCode(login: string) { Session.resendValidateCode(login); @@ -114,7 +104,6 @@ function requestContactMethodValidateCode(contactMethod: string) { key: ONYXKEYS.LOGIN_LIST, value: { [contactMethod]: { - validateCodeSent: false, errorFields: { validateCodeSent: null, validateLogin: null, @@ -132,7 +121,6 @@ function requestContactMethodValidateCode(contactMethod: string) { key: ONYXKEYS.LOGIN_LIST, value: { [contactMethod]: { - validateCodeSent: true, pendingFields: { validateCodeSent: null, }, @@ -146,7 +134,6 @@ function requestContactMethodValidateCode(contactMethod: string) { key: ONYXKEYS.LOGIN_LIST, value: { [contactMethod]: { - validateCodeSent: false, errorFields: { validateCodeSent: ErrorUtils.getMicroSecondOnyxError('contacts.genericFailureMessages.requestContactMethodValidateCode'), }, @@ -250,15 +237,8 @@ function deleteContactMethod(contactMethod: string, loginList: Record { + PusherUtils.subscribeToPrivateUserChannelEvent(Pusher.TYPE.MULTIPLE_EVENTS, currentUserAccountID.toString(), (pushJSON) => { // The data for this push event comes in two different formats: // 1. Original format - this is what was sent before the RELIABLE_UPDATES project and will go away once RELIABLE_UPDATES is fully complete // - The data is an array of objects, where each object is an onyx update @@ -520,7 +498,7 @@ function subscribeToUserEvents() { const updates = { type: CONST.ONYX_UPDATE_TYPES.PUSHER, lastUpdateID: Number(pushJSON.lastUpdateID || 0), - updates: pushJSON.updates, + updates: pushJSON.updates ?? [], previousUpdateID: Number(pushJSON.previousUpdateID || 0), }; if (!OnyxUpdates.doesClientNeedToBeUpdated(Number(pushJSON.previousUpdateID || 0))) { @@ -593,7 +571,7 @@ function updateFrequentlyUsedEmojis(frequentlyUsedEmojis: FrequentlyUsedEmoji[]) /** * Sync user chat priority mode with Onyx and Server */ -function updateChatPriorityMode(mode: string) { +function updateChatPriorityMode(mode: ValueOf) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -777,10 +755,10 @@ function setContactMethodAsDefault(newDefaultContactMethod: string) { successData, failureData, }); - Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS); + Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute()); } -function updateTheme(theme: string) { +function updateTheme(theme: ValueOf) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.SET, From c2f91cefea0d9ea4c1ac43d0f5b231a3ec88e4e7 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Tue, 14 Nov 2023 17:47:49 +0100 Subject: [PATCH 014/193] User.ts remigrated. --- src/libs/actions/User.ts | 11 +++++++++-- src/types/onyx/PersonalDetails.ts | 3 --- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 7c959b390838..21b5ce811e48 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -783,7 +783,7 @@ function updateTheme(theme: ValueOf) { /** * Sets a custom status */ -function updateCustomStatus(status: CustomStatus) { +function updateCustomStatus(status: string) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -795,7 +795,14 @@ function updateCustomStatus(status: CustomStatus) { }, }, ]; - API.write('UpdateStatus', status, { + + type UpdateStatusParam = { + status: string; + }; + + const params: UpdateStatusParam = {status}; + + API.write('UpdateStatus', params, { optimisticData, }); } diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index 92204bf7bd28..8fc627158495 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -47,9 +47,6 @@ type PersonalDetails = { /** Pronouns of the current user from their personal details */ pronouns?: string; - /** User status */ - status: {text: string; emojiCode: string; clearAfter?: string} | null; - /** Local currency for the user */ localCurrencyCode?: string; From cba3f8cd93bdfbdc0776a1ac18aeecabd83f7a9e Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Tue, 14 Nov 2023 18:43:05 +0100 Subject: [PATCH 015/193] Small alignments. --- src/libs/actions/User.ts | 24 ++++++++++++++++-------- src/types/onyx/Login.ts | 3 +++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 21b5ce811e48..a76474541364 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -86,7 +86,6 @@ function closeAccount(message: string) { /** * Resends a validation link to a given login - * @param login */ function resendValidateCode(login: string) { Session.resendValidateCode(login); @@ -104,6 +103,7 @@ function requestContactMethodValidateCode(contactMethod: string) { key: ONYXKEYS.LOGIN_LIST, value: { [contactMethod]: { + validateCodeSent: false, errorFields: { validateCodeSent: null, validateLogin: null, @@ -121,6 +121,7 @@ function requestContactMethodValidateCode(contactMethod: string) { key: ONYXKEYS.LOGIN_LIST, value: { [contactMethod]: { + validateCodeSent: true, pendingFields: { validateCodeSent: null, }, @@ -128,12 +129,14 @@ function requestContactMethodValidateCode(contactMethod: string) { }, }, ]; + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.LOGIN_LIST, value: { [contactMethod]: { + validateCodeSent: false, errorFields: { validateCodeSent: ErrorUtils.getMicroSecondOnyxError('contacts.genericFailureMessages.requestContactMethodValidateCode'), }, @@ -265,7 +268,9 @@ function clearContactMethodErrors(contactMethod: string, fieldName: string) { */ function resetContactMethodValidateCodeSentState(contactMethod: string) { Onyx.merge(ONYXKEYS.LOGIN_LIST, { - [contactMethod]: {}, + [contactMethod]: { + validateCodeSent: false, + }, }); } @@ -408,6 +413,7 @@ function validateSecondaryLogin(contactMethod: string, validateCode: string) { value: {isLoading: false}, }, ]; + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -512,7 +518,7 @@ function subscribeToUserEvents() { }); // Handles Onyx updates coming from Pusher through the mega multipleEvents. - PusherUtils.subscribeToMultiEvent(Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE, (pushJSON: OnyxUpdate[]) => + PusherUtils.subscribeToMultiEvent(Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE, (pushJSON: OnyxServerUpdate[]) => SequentialQueue.getCurrentRequest().then(() => { // If we don't have the currentUserAccountID (user is logged out) we don't want to update Onyx with data from Pusher if (!currentUserAccountID) { @@ -783,26 +789,28 @@ function updateTheme(theme: ValueOf) { /** * Sets a custom status */ -function updateCustomStatus(status: string) { +function updateCustomStatus(status: CustomStatus) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PERSONAL_DETAILS_LIST, value: { [currentUserAccountID]: { - status, + status: status.text, }, }, }, ]; type UpdateStatusParam = { - status: string; + text: string; + emojiCode: string; + clearAfter?: string; }; - const params: UpdateStatusParam = {status}; + const parameters: UpdateStatusParam = {text: status.text, emojiCode: status.emojiCode, clearAfter: status.clearAfter}; - API.write('UpdateStatus', params, { + API.write('UpdateStatus', parameters, { optimisticData, }); } diff --git a/src/types/onyx/Login.ts b/src/types/onyx/Login.ts index c770e2f81f90..deedb1b71af9 100644 --- a/src/types/onyx/Login.ts +++ b/src/types/onyx/Login.ts @@ -10,6 +10,9 @@ type Login = { /** Date login was validated, used to show info indicator status */ validatedDate?: string; + /** Whether the user validation code was sent */ + validateCodeSent?: boolean; + /** Field-specific server side errors keyed by microtime */ errorFields?: OnyxCommon.ErrorFields; From 85a2c939757f931e6888987f2bceb0d94c1b8b00 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Wed, 15 Nov 2023 10:00:08 +0100 Subject: [PATCH 016/193] Changes after review. --- src/libs/actions/User.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index a76474541364..b208c3bb0989 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -40,7 +40,7 @@ let myPersonalDetails: Partial = {}; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (value) => { - if (!value || !currentUserAccountID) { + if (!value || currentUserAccountID === -1) { return; } @@ -240,7 +240,6 @@ function deleteContactMethod(contactMethod: string, loginList: Record SequentialQueue.getCurrentRequest().then(() => { // If we don't have the currentUserAccountID (user is logged out) we don't want to update Onyx with data from Pusher - if (!currentUserAccountID) { + if (currentUserAccountID === -1) { return; } @@ -761,7 +760,7 @@ function setContactMethodAsDefault(newDefaultContactMethod: string) { successData, failureData, }); - Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute()); + Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route); } function updateTheme(theme: ValueOf) { From 60be0181c8bf9b372e2a4551ea43ca4432a8a7d7 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Wed, 15 Nov 2023 10:46:35 +0100 Subject: [PATCH 017/193] Changes after review. --- src/libs/actions/User.ts | 52 ++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index b208c3bb0989..238e73a4bf83 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -57,9 +57,9 @@ function closeAccount(message: string) { // Note: successData does not need to set isLoading to false because if the CloseAccount // command succeeds, a Pusher response will clear all Onyx data. - type CloseAccountParam = {message: string}; + type CloseAccountParams = {message: string}; - const parameters: CloseAccountParam = {message}; + const parameters: CloseAccountParams = {message}; const optimisticData: OnyxUpdate[] = [ { @@ -148,9 +148,9 @@ function requestContactMethodValidateCode(contactMethod: string) { }, ]; - type RequestContactMethodValidateCodeParam = {email: string}; + type RequestContactMethodValidateCodeParams = {email: string}; - const parameters: RequestContactMethodValidateCodeParam = {email: contactMethod}; + const parameters: RequestContactMethodValidateCodeParams = {email: contactMethod}; API.write('RequestContactMethodValidateCode', parameters, {optimisticData, successData, failureData}); } @@ -159,9 +159,9 @@ function requestContactMethodValidateCode(contactMethod: string) { * Sets whether the user is subscribed to Expensify news */ function updateNewsletterSubscription(isSubscribed: boolean) { - type UpdateNewsletterSubscriptionParam = {isSubscribed: boolean}; + type UpdateNewsletterSubscriptionParams = {isSubscribed: boolean}; - const parameters: UpdateNewsletterSubscriptionParam = {isSubscribed}; + const parameters: UpdateNewsletterSubscriptionParams = {isSubscribed}; const optimisticData: OnyxUpdate[] = [ { @@ -236,9 +236,9 @@ function deleteContactMethod(contactMethod: string, loginList: Record) { }, ]; - type UpdateChatPriorityModeParam = { + type UpdateChatPriorityModeParams = { value: string; }; - const parameters: UpdateChatPriorityModeParam = { + const parameters: UpdateChatPriorityModeParams = { value: mode, }; @@ -747,11 +747,11 @@ function setContactMethodAsDefault(newDefaultContactMethod: string) { }, ]; - type SetContactMethodAsDefaultParam = { + type SetContactMethodAsDefaultParams = { partnerUserID: string; }; - const parameters: SetContactMethodAsDefaultParam = { + const parameters: SetContactMethodAsDefaultParams = { partnerUserID: newDefaultContactMethod, }; @@ -772,11 +772,11 @@ function updateTheme(theme: ValueOf) { }, ]; - type UpdateThemeParam = { + type UpdateThemeParams = { value: string; }; - const parameters: UpdateThemeParam = { + const parameters: UpdateThemeParams = { value: theme, }; @@ -801,13 +801,13 @@ function updateCustomStatus(status: CustomStatus) { }, ]; - type UpdateStatusParam = { + type UpdateStatusParams = { text: string; emojiCode: string; clearAfter?: string; }; - const parameters: UpdateStatusParam = {text: status.text, emojiCode: status.emojiCode, clearAfter: status.clearAfter}; + const parameters: UpdateStatusParams = {text: status.text, emojiCode: status.emojiCode, clearAfter: status.clearAfter}; API.write('UpdateStatus', parameters, { optimisticData, From ee33de2fa620b55ac06ba52ba69ae2689b0cf0c3 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Wed, 15 Nov 2023 10:50:37 +0100 Subject: [PATCH 018/193] Changes after review. --- src/libs/actions/User.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 238e73a4bf83..6290275ed3bb 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -36,7 +36,7 @@ Onyx.connect({ }, }); -let myPersonalDetails: Partial = {}; +let myPersonalDetails: OnyxPersonalDetails | Record = {}; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (value) => { From 6ce19e4a4aca1d0a1f08d6b5d23c6242aa985cf6 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Wed, 15 Nov 2023 11:33:04 +0100 Subject: [PATCH 019/193] Changes after review. --- src/libs/actions/User.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 6290275ed3bb..4e417192e751 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -16,6 +16,7 @@ import type Login from '@src/types/onyx/Login'; import {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer'; import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails'; import ReportAction from '@src/types/onyx/ReportAction'; +import {OnyxEntry} from "react-native-onyx/lib/types"; import * as Link from './Link'; import * as OnyxUpdates from './OnyxUpdates'; import * as PersonalDetails from './PersonalDetails'; @@ -447,7 +448,7 @@ function validateSecondaryLogin(contactMethod: string, validateCode: string) { * and if so whether the expiresAt date of a user's ban is before right now * */ -function isBlockedFromConcierge(blockedFromConciergeNVP: BlockedFromConciergeNVP): boolean { +function isBlockedFromConcierge(blockedFromConciergeNVP: OnyxEntry): boolean { if (!blockedFromConciergeNVP || Object.keys(blockedFromConciergeNVP).length === 0) { return false; } From 4edce16177063d54aab7ce219bc31931ce73cd18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=27fvlvte=27=20Fa=C5=82at?= Date: Mon, 27 Nov 2023 10:32:51 +0100 Subject: [PATCH 020/193] Prettier fix. --- src/libs/actions/User.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 4e417192e751..a75975e07ebe 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -1,5 +1,6 @@ import {isBefore} from 'date-fns'; import Onyx, {OnyxCollection, OnyxUpdate} from 'react-native-onyx'; +import {OnyxEntry} from 'react-native-onyx/lib/types'; import {ValueOf} from 'type-fest'; import * as API from '@libs/API'; import * as ErrorUtils from '@libs/ErrorUtils'; @@ -16,7 +17,6 @@ import type Login from '@src/types/onyx/Login'; import {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer'; import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails'; import ReportAction from '@src/types/onyx/ReportAction'; -import {OnyxEntry} from "react-native-onyx/lib/types"; import * as Link from './Link'; import * as OnyxUpdates from './OnyxUpdates'; import * as PersonalDetails from './PersonalDetails'; From d624dd3421c17f54c8ce0706ac27c56122ac2eeb Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 28 Nov 2023 11:34:06 +0100 Subject: [PATCH 021/193] create MVCPFlatList --- src/components/FlatList/MVCPFlatList.js | 206 ++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 src/components/FlatList/MVCPFlatList.js diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js new file mode 100644 index 000000000000..733ec575ac08 --- /dev/null +++ b/src/components/FlatList/MVCPFlatList.js @@ -0,0 +1,206 @@ +/* eslint-disable es/no-optional-chaining, es/no-nullish-coalescing-operators, react/prop-types */ +import PropTypes from 'prop-types'; +import React from 'react'; +import {FlatList} from 'react-native'; + +function mergeRefs(...args) { + return function forwardRef(node) { + args.forEach((ref) => { + if (ref == null) { + return; + } + if (typeof ref === 'function') { + ref(node); + return; + } + if (typeof ref === 'object') { + // eslint-disable-next-line no-param-reassign + ref.current = node; + return; + } + console.error(`mergeRefs cannot handle Refs of type boolean, number or string, received ref ${String(ref)}`); + }); + }; +} + +function useMergeRefs(...args) { + return React.useMemo( + () => mergeRefs(...args), + // eslint-disable-next-line + [...args], + ); +} + +const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizontal, inverted, onScroll, ...props}, forwardedRef) => { + const {minIndexForVisible: mvcpMinIndexForVisible, autoscrollToTopThreshold: mvcpAutoscrollToTopThreshold} = maintainVisibleContentPosition ?? {}; + const scrollRef = React.useRef(null); + const prevFirstVisibleOffsetRef = React.useRef(null); + const firstVisibleViewRef = React.useRef(null); + const mutationObserverRef = React.useRef(null); + const lastScrollOffsetRef = React.useRef(0); + + const getScrollOffset = React.useCallback(() => { + if (scrollRef.current == null) { + return 0; + } + return horizontal ? scrollRef.current.getScrollableNode().scrollLeft : scrollRef.current.getScrollableNode().scrollTop; + }, [horizontal]); + + const getContentView = React.useCallback(() => scrollRef.current?.getScrollableNode().childNodes[0], []); + + const scrollToOffset = React.useCallback( + (offset, animated) => { + const behavior = animated ? 'smooth' : 'instant'; + scrollRef.current?.getScrollableNode().scroll(horizontal ? {left: offset, behavior} : {top: offset, behavior}); + }, + [horizontal], + ); + + const prepareForMaintainVisibleContentPosition = React.useCallback(() => { + if (mvcpMinIndexForVisible == null) { + return; + } + + const contentView = getContentView(); + if (contentView == null) { + return; + } + + const scrollOffset = getScrollOffset(); + + const contentViewLength = contentView.childNodes.length; + for (let i = mvcpMinIndexForVisible; i < contentViewLength; i++) { + const subview = contentView.childNodes[inverted ? contentViewLength - i - 1 : i]; + const subviewOffset = horizontal ? subview.offsetLeft : subview.offsetTop; + if (subviewOffset > scrollOffset || i === contentViewLength - 1) { + prevFirstVisibleOffsetRef.current = subviewOffset; + firstVisibleViewRef.current = subview; + break; + } + } + }, [getContentView, getScrollOffset, mvcpMinIndexForVisible, horizontal, inverted]); + + const adjustForMaintainVisibleContentPosition = React.useCallback(() => { + if (mvcpMinIndexForVisible == null) { + return; + } + + const firstVisibleView = firstVisibleViewRef.current; + const prevFirstVisibleOffset = prevFirstVisibleOffsetRef.current; + if (firstVisibleView == null || prevFirstVisibleOffset == null) { + return; + } + + const firstVisibleViewOffset = horizontal ? firstVisibleView.offsetLeft : firstVisibleView.offsetTop; + const delta = firstVisibleViewOffset - prevFirstVisibleOffset; + if (Math.abs(delta) > 0.5) { + const scrollOffset = getScrollOffset(); + prevFirstVisibleOffsetRef.current = firstVisibleViewOffset; + scrollToOffset(scrollOffset + delta, false); + if (mvcpAutoscrollToTopThreshold != null && scrollOffset <= mvcpAutoscrollToTopThreshold) { + scrollToOffset(0, true); + } + } + }, [getScrollOffset, scrollToOffset, mvcpMinIndexForVisible, mvcpAutoscrollToTopThreshold, horizontal]); + + const setupMutationObserver = React.useCallback(() => { + const contentView = getContentView(); + if (contentView == null) { + return; + } + + mutationObserverRef.current?.disconnect(); + + const mutationObserver = new MutationObserver(() => { + // Chrome adjusts scroll position when elements are added at the top of the + // view. We want to have the same behavior as react-native / Safari so we + // reset the scroll position to the last value we got from an event. + const lastScrollOffset = lastScrollOffsetRef.current; + const scrollOffset = getScrollOffset(); + if (lastScrollOffset !== scrollOffset) { + scrollToOffset(lastScrollOffset, false); + } + + // This needs to execute after scroll events are dispatched, but + // in the same tick to avoid flickering. rAF provides the right timing. + requestAnimationFrame(() => { + adjustForMaintainVisibleContentPosition(); + }); + }); + mutationObserver.observe(contentView, { + attributes: true, + childList: true, + subtree: true, + }); + + mutationObserverRef.current = mutationObserver; + }, [adjustForMaintainVisibleContentPosition, getContentView, getScrollOffset, scrollToOffset]); + + React.useEffect(() => { + prepareForMaintainVisibleContentPosition(); + setupMutationObserver(); + }, [prepareForMaintainVisibleContentPosition, setupMutationObserver]); + + const setMergedRef = useMergeRefs(scrollRef, forwardedRef); + + const onRef = React.useCallback( + (newRef) => { + // Make sure to only call refs and re-attach listeners if the node changed. + if (newRef == null || newRef === scrollRef.current) { + return; + } + + setMergedRef(newRef); + prepareForMaintainVisibleContentPosition(); + setupMutationObserver(); + }, + [prepareForMaintainVisibleContentPosition, setMergedRef, setupMutationObserver], + ); + + React.useEffect(() => { + const mutationObserver = mutationObserverRef.current; + return () => { + mutationObserver?.disconnect(); + }; + }, []); + + const onScrollInternal = React.useCallback( + (ev) => { + lastScrollOffsetRef.current = getScrollOffset(); + + prepareForMaintainVisibleContentPosition(); + + onScroll?.(ev); + }, + [getScrollOffset, prepareForMaintainVisibleContentPosition, onScroll], + ); + + return ( + + ); +}); + +MVCPFlatList.displayName = 'MVCPFlatList'; +MVCPFlatList.propTypes = { + maintainVisibleContentPosition: PropTypes.shape({ + minIndexForVisible: PropTypes.number.isRequired, + autoscrollToTopThreshold: PropTypes.number, + }), + horizontal: PropTypes.bool, +}; + +MVCPFlatList.defaultProps = { + maintainVisibleContentPosition: null, + horizontal: false, +}; + +export default MVCPFlatList; From a34fed2c6191c49fa96be0b5f5577ccda245a1a9 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 28 Nov 2023 11:34:18 +0100 Subject: [PATCH 022/193] use MVCPFlatList --- src/components/FlatList/index.web.js | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/components/FlatList/index.web.js diff --git a/src/components/FlatList/index.web.js b/src/components/FlatList/index.web.js new file mode 100644 index 000000000000..7299776db9bc --- /dev/null +++ b/src/components/FlatList/index.web.js @@ -0,0 +1,3 @@ +import MVCPFlatList from './MVCPFlatList'; + +export default MVCPFlatList; From 5d67d029365477c02a7e3ac70bf91c1ed79f1ab1 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 28 Nov 2023 17:42:15 +0100 Subject: [PATCH 023/193] move scrollToOffset into requestAnimationFrame --- src/components/FlatList/MVCPFlatList.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js index 733ec575ac08..c9ec3c6a95c1 100644 --- a/src/components/FlatList/MVCPFlatList.js +++ b/src/components/FlatList/MVCPFlatList.js @@ -112,18 +112,18 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont mutationObserverRef.current?.disconnect(); const mutationObserver = new MutationObserver(() => { - // Chrome adjusts scroll position when elements are added at the top of the - // view. We want to have the same behavior as react-native / Safari so we - // reset the scroll position to the last value we got from an event. - const lastScrollOffset = lastScrollOffsetRef.current; - const scrollOffset = getScrollOffset(); - if (lastScrollOffset !== scrollOffset) { - scrollToOffset(lastScrollOffset, false); - } - // This needs to execute after scroll events are dispatched, but // in the same tick to avoid flickering. rAF provides the right timing. requestAnimationFrame(() => { + // Chrome adjusts scroll position when elements are added at the top of the + // view. We want to have the same behavior as react-native / Safari so we + // reset the scroll position to the last value we got from an event. + const lastScrollOffset = lastScrollOffsetRef.current; + const scrollOffset = getScrollOffset(); + if (lastScrollOffset !== scrollOffset) { + scrollToOffset(lastScrollOffset, false); + } + adjustForMaintainVisibleContentPosition(); }); }); From e9fa5bfa14ae35d86a3c3461f04b26caded8b556 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 11 Dec 2023 12:55:37 +0100 Subject: [PATCH 024/193] fix scrolling --- src/pages/home/ReportScreen.js | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 8db899a8f73f..8c9ac633000d 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -1,7 +1,7 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; +import {InteractionManager, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import Banner from '@components/Banner'; @@ -158,6 +158,7 @@ function ReportScreen({ const firstRenderRef = useRef(true); const flatListRef = useRef(); const reactionListRef = useRef(); + const shouldScrollForAttachment = useRef(false); const prevReport = usePrevious(report); const prevUserLeavingStatus = usePrevious(userLeavingStatus); const [isBannerVisible, setIsBannerVisible] = useState(true); @@ -268,17 +269,28 @@ function ReportScreen({ const onSubmitComment = useCallback( (text) => { Report.addComment(getReportID(route), text); - - // We need to scroll to the bottom of the list after the comment is added - const refID = setTimeout(() => { - flatListRef.current.scrollToOffset({animated: false, offset: 0}); - }, 10); - - return () => clearTimeout(refID); }, [route], ); + const firstReportActions = lodashGet(reportActions, ['0']); + + const scrollToBottomAfterInteraction = useCallback(() => InteractionManager.runAfterInteractions(() => flatListRef.current.scrollToOffset({animated: false, offset: 0})), []); + useEffect(() => { + if (!firstReportActions) { + return; + } + // Scroll to bottom if it's a new message or a new attachment following a message. + if (firstReportActions.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { + scrollToBottomAfterInteraction(); + shouldScrollForAttachment.current = firstReportActions.isAttachment; + } else if (shouldScrollForAttachment.current) { + // This handles scrolling for image attachments. Initially, a placeholder is added and we scroll to it, followed by the actual image, which also triggers a scroll. + scrollToBottomAfterInteraction(); + shouldScrollForAttachment.current = false; + } + }, [firstReportActions, scrollToBottomAfterInteraction]); + useEffect(() => { fetchReportIfNeeded(); ComposerActions.setShouldShowComposeInput(true); From 5e6d6f69b5228d699719e2990ecdf5c55c5f54d8 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 11 Dec 2023 13:13:52 +0100 Subject: [PATCH 025/193] remove scrollToOffset --- src/pages/home/ReportScreen.js | 21 +-------------------- src/pages/home/report/ReportActionsList.js | 4 ++-- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 8c9ac633000d..16490a34bf37 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -1,7 +1,7 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {InteractionManager, View} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import Banner from '@components/Banner'; @@ -158,7 +158,6 @@ function ReportScreen({ const firstRenderRef = useRef(true); const flatListRef = useRef(); const reactionListRef = useRef(); - const shouldScrollForAttachment = useRef(false); const prevReport = usePrevious(report); const prevUserLeavingStatus = usePrevious(userLeavingStatus); const [isBannerVisible, setIsBannerVisible] = useState(true); @@ -273,24 +272,6 @@ function ReportScreen({ [route], ); - const firstReportActions = lodashGet(reportActions, ['0']); - - const scrollToBottomAfterInteraction = useCallback(() => InteractionManager.runAfterInteractions(() => flatListRef.current.scrollToOffset({animated: false, offset: 0})), []); - useEffect(() => { - if (!firstReportActions) { - return; - } - // Scroll to bottom if it's a new message or a new attachment following a message. - if (firstReportActions.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { - scrollToBottomAfterInteraction(); - shouldScrollForAttachment.current = firstReportActions.isAttachment; - } else if (shouldScrollForAttachment.current) { - // This handles scrolling for image attachments. Initially, a placeholder is added and we scroll to it, followed by the actual image, which also triggers a scroll. - scrollToBottomAfterInteraction(); - shouldScrollForAttachment.current = false; - } - }, [firstReportActions, scrollToBottomAfterInteraction]); - useEffect(() => { fetchReportIfNeeded(); ComposerActions.setShouldShowComposeInput(true); diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 183665891929..69977d72feda 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -2,7 +2,7 @@ import {useRoute} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {DeviceEventEmitter} from 'react-native'; +import {DeviceEventEmitter, InteractionManager} from 'react-native'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import _ from 'underscore'; import InvertedFlatList from '@components/InvertedFlatList'; @@ -259,7 +259,7 @@ function ReportActionsList({ if (!isFromCurrentUser) { return; } - reportScrollManager.scrollToBottom(); + InteractionManager.runAfterInteractions(() => reportScrollManager.scrollToBottom()); }); const cleanup = () => { From 5781a6a152c3fb2eb62d5def9fed80ab7f001898 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Tue, 12 Dec 2023 01:22:53 +0100 Subject: [PATCH 026/193] Requested changes + custom status handling fix. --- src/libs/ReportActionsUtils.ts | 2 +- src/libs/SidebarUtils.ts | 2 +- src/libs/actions/User.ts | 78 +++++++++++++++++++------------ src/types/onyx/PersonalDetails.ts | 5 +- 4 files changed, 52 insertions(+), 35 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 6ddc2ac99e06..69e9ead46ab5 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -643,7 +643,7 @@ function isReportActionAttachment(reportAction: OnyxEntry): boolea } // eslint-disable-next-line rulesdir/no-negated-variables -function isNotifiableReportAction(reportAction: OnyxEntry): boolean { +function isNotifiableReportAction(reportAction: OnyxEntry): reportAction is ReportAction { if (!reportAction) { return false; } diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 9a1db07ca683..17461395e30e 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -438,7 +438,7 @@ function getOptionData( result.displayNamesWithTooltips = displayNamesWithTooltips; if (status) { - result.status = status; + result.status = status.text; } result.type = report.type; diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index a75975e07ebe..5ae26f532499 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -15,8 +15,9 @@ import ROUTES from '@src/ROUTES'; import type {FrequentlyUsedEmoji} from '@src/types/onyx'; import type Login from '@src/types/onyx/Login'; import {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer'; -import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails'; +import OnyxPersonalDetails, {CustomStatus} from '@src/types/onyx/PersonalDetails'; import ReportAction from '@src/types/onyx/ReportAction'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import * as Link from './Link'; import * as OnyxUpdates from './OnyxUpdates'; import * as PersonalDetails from './PersonalDetails'; @@ -24,7 +25,6 @@ import * as Report from './Report'; import * as Session from './Session'; import redirectToSignIn from './SignInRedirect'; -type CustomStatus = {text: string; emojiCode: string; clearAfter?: string}; type BlockedFromConciergeNVP = {expiresAt: number}; let currentUserAccountID = -1; @@ -58,10 +58,6 @@ function closeAccount(message: string) { // Note: successData does not need to set isLoading to false because if the CloseAccount // command succeeds, a Pusher response will clear all Onyx data. - type CloseAccountParams = {message: string}; - - const parameters: CloseAccountParams = {message}; - const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -77,7 +73,9 @@ function closeAccount(message: string) { }, ]; - API.write('CloseAccount', parameters, { + type CloseAccountParams = {message: string}; + + API.write('CloseAccount', {message} as CloseAccountParams, { optimisticData, failureData, }); @@ -160,10 +158,6 @@ function requestContactMethodValidateCode(contactMethod: string) { * Sets whether the user is subscribed to Expensify news */ function updateNewsletterSubscription(isSubscribed: boolean) { - type UpdateNewsletterSubscriptionParams = {isSubscribed: boolean}; - - const parameters: UpdateNewsletterSubscriptionParams = {isSubscribed}; - const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -179,7 +173,9 @@ function updateNewsletterSubscription(isSubscribed: boolean) { }, ]; - API.write('UpdateNewsletterSubscription', parameters, { + type UpdateNewsletterSubscriptionParams = {isSubscribed: boolean}; + + API.write('UpdateNewsletterSubscription', {isSubscribed} as UpdateNewsletterSubscriptionParams, { optimisticData, failureData, }); @@ -449,11 +445,11 @@ function validateSecondaryLogin(contactMethod: string, validateCode: string) { * */ function isBlockedFromConcierge(blockedFromConciergeNVP: OnyxEntry): boolean { - if (!blockedFromConciergeNVP || Object.keys(blockedFromConciergeNVP).length === 0) { + if (isEmptyObject(blockedFromConciergeNVP)) { return false; } - if (!blockedFromConciergeNVP.expiresAt) { + if (!blockedFromConciergeNVP?.expiresAt) { return false; } @@ -469,8 +465,7 @@ function triggerNotifications(onyxUpdates: OnyxServerUpdate[]) { const reportID = update.key.replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, ''); const reportActions = Object.values((update.value as OnyxCollection) ?? {}); - const actions = reportActions.filter((action) => ReportActionsUtils.isNotifiableReportAction(action)) as ReportAction[]; - actions.forEach((action) => Report.showReportActionNotification(reportID, action)); + reportActions.forEach((action) => ReportActionsUtils.isNotifiableReportAction(action) && Report.showReportActionNotification(reportID, action)); }); } @@ -525,8 +520,9 @@ function subscribeToUserEvents() { return; } - const onyxUpdatePromise = Onyx.update(pushJSON); - triggerNotifications(pushJSON); + const onyxUpdatePromise = Onyx.update(pushJSON).then(() => { + triggerNotifications(pushJSON); + }); // Return a promise when Onyx is done updating so that the OnyxUpdatesManager can properly apply all // the onyx updates in order @@ -576,8 +572,11 @@ function updateFrequentlyUsedEmojis(frequentlyUsedEmojis: FrequentlyUsedEmoji[]) /** * Sync user chat priority mode with Onyx and Server + * @param mode + * @param [automatic] if we changed the mode automatically */ -function updateChatPriorityMode(mode: ValueOf) { +function updateChatPriorityMode(mode: ValueOf, automatic = false) { + const autoSwitchedToFocusMode = mode === CONST.PRIORITY_MODE.GSD && automatic; const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -586,16 +585,33 @@ function updateChatPriorityMode(mode: ValueOf) { }, ]; + if (autoSwitchedToFocusMode) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_TRY_FOCUS_MODE, + value: true, + }); + } + type UpdateChatPriorityModeParams = { - value: string; + value: ValueOf; + automatic: boolean; }; const parameters: UpdateChatPriorityModeParams = { value: mode, + automatic, }; API.write('UpdateChatPriorityMode', parameters, {optimisticData}); - Navigation.goBack(ROUTES.SETTINGS_PREFERENCES); + + if (!autoSwitchedToFocusMode) { + Navigation.goBack(ROUTES.SETTINGS_PREFERENCES); + } +} + +function clearFocusModeNotification() { + Onyx.set(ONYXKEYS.FOCUS_MODE_NOTIFICATION, false); } function setShouldUseStagingServer(shouldUseStagingServer: boolean) { @@ -655,15 +671,14 @@ function generateStatementPDF(period: string) { }, }, ]; - API.read( - 'GetStatementPDF', - {period}, - { - optimisticData, - successData, - failureData, - }, - ); + + type GetStatementPDFParams = {period: string}; + + API.read('GetStatementPDF', {period} as GetStatementPDFParams, { + optimisticData, + successData, + failureData, + }); } /** @@ -796,7 +811,7 @@ function updateCustomStatus(status: CustomStatus) { key: ONYXKEYS.PERSONAL_DETAILS_LIST, value: { [currentUserAccountID]: { - status: status.text, + status, }, }, }, @@ -854,6 +869,7 @@ function clearDraftCustomStatus() { } export { + clearFocusModeNotification, closeAccount, resendValidateCode, requestContactMethodValidateCode, diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index 049a32a80874..b6e33358098a 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -11,6 +11,7 @@ type Timezone = { /** Whether timezone is automatically set */ automatic?: boolean; }; +type CustomStatus = {text: string; emojiCode: string; clearAfter?: string}; type PersonalDetails = { /** ID of the current user from their personal details */ @@ -70,9 +71,9 @@ type PersonalDetails = { fallbackIcon?: string; /** Status of the current user from their personal details */ - status?: string; + status?: CustomStatus; }; export default PersonalDetails; -export type {Timezone, SelectedTimezone}; +export type {Timezone, SelectedTimezone, CustomStatus}; From 4732fb775043172bd11f87914b0e4dfab182ea46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 12 Dec 2023 09:27:11 +0000 Subject: [PATCH 027/193] Rename QRShare files to TS --- .../QRShareWithDownload/{index.native.js => index.native.tsx} | 0 .../QRShare/QRShareWithDownload/{index.js => index.tsx} | 0 ...{getQrCodeDownloadFileName.js => getQrCodeDownloadFileName.ts} | 0 src/components/QRShare/{index.js => index.tsx} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename src/components/QRShare/QRShareWithDownload/{index.native.js => index.native.tsx} (100%) rename src/components/QRShare/QRShareWithDownload/{index.js => index.tsx} (100%) rename src/components/QRShare/{getQrCodeDownloadFileName.js => getQrCodeDownloadFileName.ts} (100%) rename src/components/QRShare/{index.js => index.tsx} (100%) diff --git a/src/components/QRShare/QRShareWithDownload/index.native.js b/src/components/QRShare/QRShareWithDownload/index.native.tsx similarity index 100% rename from src/components/QRShare/QRShareWithDownload/index.native.js rename to src/components/QRShare/QRShareWithDownload/index.native.tsx diff --git a/src/components/QRShare/QRShareWithDownload/index.js b/src/components/QRShare/QRShareWithDownload/index.tsx similarity index 100% rename from src/components/QRShare/QRShareWithDownload/index.js rename to src/components/QRShare/QRShareWithDownload/index.tsx diff --git a/src/components/QRShare/getQrCodeDownloadFileName.js b/src/components/QRShare/getQrCodeDownloadFileName.ts similarity index 100% rename from src/components/QRShare/getQrCodeDownloadFileName.js rename to src/components/QRShare/getQrCodeDownloadFileName.ts diff --git a/src/components/QRShare/index.js b/src/components/QRShare/index.tsx similarity index 100% rename from src/components/QRShare/index.js rename to src/components/QRShare/index.tsx From 010bc09c7b71bdf89808c3df7a58a4f6a516f7bb Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Wed, 13 Dec 2023 19:23:35 +0530 Subject: [PATCH 028/193] fix duplicate endpoints --- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/pages/iou/request/step/IOURequestStepDistance.js | 10 +++++++--- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index c4a481cb71c0..8b860372d730 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -612,6 +612,7 @@ export default { genericDeleteFailureMessage: 'Unexpected error deleting the money request, please try again later', genericEditFailureMessage: 'Unexpected error editing the money request, please try again later', genericSmartscanFailureMessage: 'Transaction is missing fields', + duplicateWaypointsErrorMessage: 'Please remove duplicate waypoints', atLeastTwoDifferentWaypoints: 'Please enter at least two different addresses', splitBillMultipleParticipantsErrorMessage: 'Split bill is only allowed between a single workspace or individual users. Please update your selection.', }, diff --git a/src/languages/es.ts b/src/languages/es.ts index a91a8768a3ee..b0bc725351aa 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -607,6 +607,7 @@ export default { genericDeleteFailureMessage: 'Error inesperado eliminando la solicitud de dinero. Por favor, inténtalo más tarde', genericEditFailureMessage: 'Error inesperado al guardar la solicitud de dinero. Por favor, inténtalo más tarde', genericSmartscanFailureMessage: 'La transacción tiene campos vacíos', + duplicateWaypointsErrorMessage: 'Por favor elimina los puntos de ruta duplicados', atLeastTwoDifferentWaypoints: 'Por favor introduce al menos dos direcciones diferentes', splitBillMultipleParticipantsErrorMessage: 'Solo puedes dividir una cuenta entre un único espacio de trabajo o con usuarios individuales. Por favor actualiza tu selección.', }, diff --git a/src/pages/iou/request/step/IOURequestStepDistance.js b/src/pages/iou/request/step/IOURequestStepDistance.js index 39e9d6f03afa..0270ec724e0d 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.js +++ b/src/pages/iou/request/step/IOURequestStepDistance.js @@ -131,6 +131,10 @@ function IOURequestStepDistance({ return ErrorUtils.getLatestErrorField(transaction, 'route'); } + if (_.keys(waypoints).length > 2 && _.size(validatedWaypoints) !== _.keys(waypoints).length) { + return {0: translate('iou.error.duplicateWaypointsErrorMessage')}; + } + if (_.size(validatedWaypoints) < 2) { return {0: translate('iou.error.atLeastTwoDifferentWaypoints')}; } @@ -159,12 +163,12 @@ function IOURequestStepDistance({ const submitWaypoints = useCallback(() => { // If there is any error or loading state, don't let user go to next page. - if (_.size(validatedWaypoints) < 2 || hasRouteError || isLoadingRoute || isLoading) { + if (_.size(validatedWaypoints) < 2 || (_.keys(waypoints).length > 2 && _.size(validatedWaypoints) !== _.keys(waypoints).length) || hasRouteError || isLoadingRoute || isLoading) { setHasError(true); return; } navigateToNextStep(); - }, [setHasError, hasRouteError, isLoadingRoute, isLoading, validatedWaypoints, navigateToNextStep]); + }, [setHasError, waypoints, hasRouteError, isLoadingRoute, isLoading, validatedWaypoints, navigateToNextStep]); return ( {/* Show error message if there is route error or there are less than 2 routes and user has tried submitting, */} - {((hasError && _.size(validatedWaypoints) < 2) || hasRouteError) && ( + {((hasError && _.size(validatedWaypoints) < 2) || (_.keys(waypoints).length > 2 && _.size(validatedWaypoints) !== _.keys(waypoints).length) || hasRouteError) && ( Date: Thu, 14 Dec 2023 15:24:54 +0700 Subject: [PATCH 029/193] Green line appear and remains when mark chat as unread and than mark it as read --- src/libs/actions/Report.ts | 1 + .../DeviceEventListenerRef.ts | 30 +++++++++++++ .../index.js} | 42 ++++++++++++------- 3 files changed, 58 insertions(+), 15 deletions(-) create mode 100644 src/pages/home/report/ReportActionsList/DeviceEventListenerRef.ts rename src/pages/home/report/{ReportActionsList.js => ReportActionsList/index.js} (93%) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index bea4ab8aed77..cbe192309625 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -933,6 +933,7 @@ function readNewestAction(reportID: string) { }; API.write('ReadNewestAction', parameters, {optimisticData}); + DeviceEventEmitter.emit(`readNewestAction_${reportID}`, lastReadTime); } /** diff --git a/src/pages/home/report/ReportActionsList/DeviceEventListenerRef.ts b/src/pages/home/report/ReportActionsList/DeviceEventListenerRef.ts new file mode 100644 index 000000000000..13dbb17ed5ae --- /dev/null +++ b/src/pages/home/report/ReportActionsList/DeviceEventListenerRef.ts @@ -0,0 +1,30 @@ +import {EmitterSubscription,DeviceEventEmitter} from 'react-native'; + +export default class DeviceEventListenerRef { + private typePrefix: string; + + private subscription: EmitterSubscription | null = null; + + constructor(typePrefix: string) { + this.typePrefix = typePrefix; + } + + static use(typePrefix: string): DeviceEventListenerRef { + return new DeviceEventListenerRef(typePrefix); + } + + add( + typeSuffix: string, // report.reportID + listener: (data: unknown) => void, + ): void { + this.subscription = DeviceEventEmitter.addListener(`${this.typePrefix}_${typeSuffix}`, listener); + } + + remove(): void { + if (!this.subscription) { + return; + } + this.subscription.remove(); + this.subscription = null; + } +} diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList/index.js similarity index 93% rename from src/pages/home/report/ReportActionsList.js rename to src/pages/home/report/ReportActionsList/index.js index 46abbfc71b84..f9a97185c76b 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList/index.js @@ -2,7 +2,6 @@ import {useRoute} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {DeviceEventEmitter} from 'react-native'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import _ from 'underscore'; import InvertedFlatList from '@components/InvertedFlatList'; @@ -21,10 +20,11 @@ import reportPropTypes from '@pages/reportPropTypes'; import variables from '@styles/variables'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; -import FloatingMessageCounter from './FloatingMessageCounter'; -import ListBoundaryLoader from './ListBoundaryLoader/ListBoundaryLoader'; -import reportActionPropTypes from './reportActionPropTypes'; -import ReportActionsListItemRenderer from './ReportActionsListItemRenderer'; +import FloatingMessageCounter from '@pages/home/report/FloatingMessageCounter'; +import ListBoundaryLoader from '@pages/home/report/ListBoundaryLoader/ListBoundaryLoader'; +import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; +import ReportActionsListItemRenderer from '@pages/home/report/ReportActionsListItemRenderer'; +import DeviceEventListenerRef from './DeviceEventListenerRef'; const propTypes = { /** The report currently being looked at */ @@ -143,7 +143,7 @@ function ReportActionsList({ const route = useRoute(); const opacity = useSharedValue(0); const userActiveSince = useRef(null); - const unreadActionSubscription = useRef(null); + const markerInit = () => { if (!cacheUnreadMarkers.has(report.reportID)) { return null; @@ -221,20 +221,32 @@ function ReportActionsList({ }, [report.lastReadTime, report.reportID]); useEffect(() => { - // If the reportID changes, we reset the userActiveSince to null, we need to do it because - // this component doesn't unmount when the reportID changes - if (unreadActionSubscription.current) { - unreadActionSubscription.current.remove(); - unreadActionSubscription.current = null; - } - - // Listen to specific reportID for unread event and set the marker to new message - unreadActionSubscription.current = DeviceEventEmitter.addListener(`unreadAction_${report.reportID}`, (newLastReadTime) => { + const resetUnreadMarker = (newLastReadTime) => { cacheUnreadMarkers.delete(report.reportID); lastReadTimeRef.current = newLastReadTime; setCurrentUnreadMarker(null); + }; + + // Listen to specific reportID for unread event and set the marker to new message + const unreadActionSubscription = DeviceEventListenerRef.use('unreadAction'); + unreadActionSubscription.add(report.reportID, (newLastReadTime) => { + resetUnreadMarker(newLastReadTime); setMessageManuallyMarkedUnread(new Date().getTime()); }); + + const readNewestActionSubscription = DeviceEventListenerRef.use('readNewestAction'); + // Listen to specific reportID for read newest action event and reset the marker + readNewestActionSubscription.add(report.reportID, (newLastReadTime) => { + resetUnreadMarker(newLastReadTime); + setMessageManuallyMarkedUnread(0); + }); + + return () => { + // If the reportID changes, we reset the userActiveSince to null, we need to do it because + // this component doesn't unmount when the reportID changes + unreadActionSubscription.remove(); + readNewestActionSubscription.remove(); + }; }, [report.reportID]); useEffect(() => { From b847b62502572c20e56a584ac52d0ecab7f2ab38 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 14 Dec 2023 15:30:17 +0700 Subject: [PATCH 030/193] run prettier --- .../report/ReportActionsList/DeviceEventListenerRef.ts | 2 +- src/pages/home/report/ReportActionsList/index.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/home/report/ReportActionsList/DeviceEventListenerRef.ts b/src/pages/home/report/ReportActionsList/DeviceEventListenerRef.ts index 13dbb17ed5ae..973087e07c4c 100644 --- a/src/pages/home/report/ReportActionsList/DeviceEventListenerRef.ts +++ b/src/pages/home/report/ReportActionsList/DeviceEventListenerRef.ts @@ -1,4 +1,4 @@ -import {EmitterSubscription,DeviceEventEmitter} from 'react-native'; +import {DeviceEventEmitter, EmitterSubscription} from 'react-native'; export default class DeviceEventListenerRef { private typePrefix: string; diff --git a/src/pages/home/report/ReportActionsList/index.js b/src/pages/home/report/ReportActionsList/index.js index f9a97185c76b..512f96b3f629 100644 --- a/src/pages/home/report/ReportActionsList/index.js +++ b/src/pages/home/report/ReportActionsList/index.js @@ -16,14 +16,14 @@ import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import reportPropTypes from '@pages/reportPropTypes'; -import variables from '@styles/variables'; -import * as Report from '@userActions/Report'; -import CONST from '@src/CONST'; import FloatingMessageCounter from '@pages/home/report/FloatingMessageCounter'; import ListBoundaryLoader from '@pages/home/report/ListBoundaryLoader/ListBoundaryLoader'; import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; import ReportActionsListItemRenderer from '@pages/home/report/ReportActionsListItemRenderer'; +import reportPropTypes from '@pages/reportPropTypes'; +import variables from '@styles/variables'; +import * as Report from '@userActions/Report'; +import CONST from '@src/CONST'; import DeviceEventListenerRef from './DeviceEventListenerRef'; const propTypes = { From a6363ec22e67f95803e7680e1c0088fbca96f608 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 14 Dec 2023 17:07:30 +0700 Subject: [PATCH 031/193] hide thread option in context menu when deleting acion in offline mode --- src/libs/ReportUtils.ts | 9 +++++++++ .../report/ContextMenu/ContextMenuActions.js | 16 +++++++++++++--- src/pages/home/report/ReportActionItem.js | 3 +-- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 63037416c923..fa8add8f666f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4314,6 +4314,14 @@ function navigateToPrivateNotes(report: Report, session: Session) { Navigation.navigate(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID)); } +/** + * Check whether should display thread reply + */ +function shouldDisplayThreadReplies(reportAction: ReportAction, reportID: string): boolean { + const hasReplies = (reportAction.childVisibleActionCount ?? 0) > 0; + return hasReplies && !!reportAction.childCommenterCount && !isThreadFirstChat(reportAction, reportID); +} + export { getReportParticipantsTitle, isReportMessageAttachment, @@ -4485,6 +4493,7 @@ export { canEditWriteCapability, hasSmartscanError, shouldAutoFocusOnKeyPress, + shouldDisplayThreadReplies, }; export type {OptionData, OptimisticChatReport}; diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index 5e6f2d46abda..38a2c1e5510e 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -126,12 +126,18 @@ export default [ if (type !== CONTEXT_MENU_TYPES.REPORT_ACTION) { return false; } + const isDeletedAction = ReportActionsUtils.isDeletedAction(reportAction); + const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(reportAction, reportID); const isCommentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT; const isReportPreviewAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; const isIOUAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction); const isModifiedExpenseAction = ReportActionsUtils.isModifiedExpenseAction(reportAction); const isTaskAction = ReportActionsUtils.isTaskAction(reportAction); - return (isCommentAction || isReportPreviewAction || isIOUAction || isModifiedExpenseAction || isTaskAction) && !ReportUtils.isThreadFirstChat(reportAction, reportID); + return ( + (isCommentAction || isReportPreviewAction || isIOUAction || isModifiedExpenseAction || isTaskAction) && + !ReportUtils.isThreadFirstChat(reportAction, reportID) && + (!isDeletedAction || shouldDisplayThreadReplies) + ); }, onPress: (closePopover, {reportAction, reportID}) => { if (closePopover) { @@ -158,11 +164,13 @@ export default [ const isActionCreator = ReportUtils.isActionCreator(reportAction); childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; } + const isDeletedAction = ReportActionsUtils.isDeletedAction(reportAction); + const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(reportAction, reportID); const subscribed = childReportNotificationPreference !== 'hidden'; const isCommentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportUtils.isThreadFirstChat(reportAction, reportID); const isReportPreviewAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; const isIOUAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction); - return !subscribed && (isCommentAction || isReportPreviewAction || isIOUAction); + return !subscribed && (isCommentAction || isReportPreviewAction || isIOUAction) && (!isDeletedAction || shouldDisplayThreadReplies); }, onPress: (closePopover, {reportAction, reportID}) => { let childReportNotificationPreference = lodashGet(reportAction, 'childReportNotificationPreference', ''); @@ -195,6 +203,8 @@ export default [ const isActionCreator = ReportUtils.isActionCreator(reportAction); childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; } + const isDeletedAction = ReportActionsUtils.isDeletedAction(reportAction); + const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(reportAction, reportID); const subscribed = childReportNotificationPreference !== 'hidden'; if (type !== CONTEXT_MENU_TYPES.REPORT_ACTION) { return false; @@ -202,7 +212,7 @@ export default [ const isCommentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportUtils.isThreadFirstChat(reportAction, reportID); const isReportPreviewAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; const isIOUAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction); - return subscribed && (isCommentAction || isReportPreviewAction || isIOUAction); + return subscribed && (isCommentAction || isReportPreviewAction || isIOUAction) && (!isDeletedAction || shouldDisplayThreadReplies); }, onPress: (closePopover, {reportAction, reportID}) => { let childReportNotificationPreference = lodashGet(reportAction, 'childReportNotificationPreference', ''); diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 2e888a5471b8..5fafda690d9f 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -491,9 +491,8 @@ function ReportActionItem(props) { ); } const numberOfThreadReplies = _.get(props, ['action', 'childVisibleActionCount'], 0); - const hasReplies = numberOfThreadReplies > 0; - const shouldDisplayThreadReplies = hasReplies && props.action.childCommenterCount && !ReportUtils.isThreadFirstChat(props.action, props.report.reportID); + const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(props.action, props.report.reportID); const oldestFourAccountIDs = _.map(lodashGet(props.action, 'childOldestFourAccountIDs', '').split(','), (accountID) => Number(accountID)); const draftMessageRightAlign = props.draftMessage ? styles.chatItemReactionsDraftRight : {}; From 3e5ac80bd1f8f5ecd691505b09fba7ea592f6f81 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Thu, 14 Dec 2023 12:29:16 +0100 Subject: [PATCH 032/193] Solved errors. --- src/libs/actions/Report.ts | 2 +- src/types/onyx/CustomStatusDraft.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index bea4ab8aed77..fd704adcf46a 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1809,7 +1809,7 @@ function shouldShowReportActionNotification(reportID: string, action: ReportActi // Only show notifications for supported types of report actions if (!ReportActionsUtils.isNotifiableReportAction(action)) { - Log.info(`${tag} No notification because this action type is not supported`, false, {actionName: action?.actionName}); + Log.info(`${tag} No notification because this action type is not supported`, false, {}); return false; } diff --git a/src/types/onyx/CustomStatusDraft.ts b/src/types/onyx/CustomStatusDraft.ts index b2801a1d89e0..4ea685b40cb5 100644 --- a/src/types/onyx/CustomStatusDraft.ts +++ b/src/types/onyx/CustomStatusDraft.ts @@ -7,6 +7,8 @@ type CustomStatusDraft = { /** ISO 8601 format string, which represents the time when the status should be cleared */ clearAfter: string; + + customDateTemporary: string; }; export default CustomStatusDraft; From 7b8f2d02a099531076409c52924403b5bab4acf6 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Fri, 15 Dec 2023 10:21:25 +0100 Subject: [PATCH 033/193] Requested changes. --- src/libs/actions/User.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 2c5871d7d45d..651212a17af9 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -75,7 +75,9 @@ function closeAccount(message: string) { type CloseAccountParams = {message: string}; - API.write('CloseAccount', {message} as CloseAccountParams, { + const parameters: CloseAccountParams = {message}; + + API.write('CloseAccount', parameters, { optimisticData, failureData, }); @@ -175,7 +177,9 @@ function updateNewsletterSubscription(isSubscribed: boolean) { type UpdateNewsletterSubscriptionParams = {isSubscribed: boolean}; - API.write('UpdateNewsletterSubscription', {isSubscribed} as UpdateNewsletterSubscriptionParams, { + const parameters: UpdateNewsletterSubscriptionParams = {isSubscribed}; + + API.write('UpdateNewsletterSubscription', parameters, { optimisticData, failureData, }); @@ -674,7 +678,9 @@ function generateStatementPDF(period: string) { type GetStatementPDFParams = {period: string}; - API.read('GetStatementPDF', {period} as GetStatementPDFParams, { + const parameters: GetStatementPDFParams = {period}; + + API.read('GetStatementPDF', parameters, { optimisticData, successData, failureData, From ca9c880fdae3c0cf6795eac2d748f1e82f6c5908 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Fri, 15 Dec 2023 10:28:49 +0100 Subject: [PATCH 034/193] Migrated changes from main. --- src/libs/actions/User.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 651212a17af9..a12b06152979 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -23,7 +23,6 @@ import * as OnyxUpdates from './OnyxUpdates'; import * as PersonalDetails from './PersonalDetails'; import * as Report from './Report'; import * as Session from './Session'; -import redirectToSignIn from './SignInRedirect'; type BlockedFromConciergeNVP = {expiresAt: number}; @@ -81,8 +80,6 @@ function closeAccount(message: string) { optimisticData, failureData, }); - // Run cleanup actions to prevent reconnection callbacks from blocking logging in again - redirectToSignIn(); } /** From b0c4771d04134ced5211167cc212cb84463d0280 Mon Sep 17 00:00:00 2001 From: s-alves10 Date: Fri, 15 Dec 2023 06:58:09 -0600 Subject: [PATCH 035/193] revert PR 32474 --- src/libs/ReportActionsUtils.ts | 2 +- src/pages/home/ReportScreen.js | 12 ++++-------- tests/unit/ReportActionsUtilsTest.js | 3 +-- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 9a3099ba6c02..21c382346e57 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -375,7 +375,7 @@ function shouldReportActionBeVisible(reportAction: OnyxEntry, key: // All other actions are displayed except thread parents, deleted, or non-pending actions const isDeleted = isDeletedAction(reportAction); - const isPending = !!reportAction.pendingAction && !(!isNetworkOffline && reportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + const isPending = !!reportAction.pendingAction; return !isDeleted || isPending || isDeletedParentAction(reportAction) || isReversedTransaction(reportAction); } diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index fd5caeea24f4..dc087c96a3cc 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -16,7 +16,6 @@ import TaskHeaderActionButton from '@components/TaskHeaderActionButton'; import withCurrentReportID, {withCurrentReportIDDefaultProps, withCurrentReportIDPropTypes} from '@components/withCurrentReportID'; import withViewportOffsetTop from '@components/withViewportOffsetTop'; import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -155,7 +154,6 @@ function ReportScreen({ const styles = useThemeStyles(); const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); - const {isOffline} = useNetwork(); const firstRenderRef = useRef(true); const flatListRef = useRef(); @@ -175,11 +173,8 @@ function ReportScreen({ const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; - // eslint-disable-next-line react-hooks/exhaustive-deps -- need to re-filter the report actions when network status changes - const filteredReportActions = useMemo(() => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), [isOffline, reportActions]); - // There are no reportActions at all to display and we are still in the process of loading the next set of actions. - const isLoadingInitialReportActions = _.isEmpty(filteredReportActions) && reportMetadata.isLoadingInitialReportActions; + const isLoadingInitialReportActions = _.isEmpty(reportActions) && reportMetadata.isLoadingInitialReportActions; const isOptimisticDelete = lodashGet(report, 'statusNum') === CONST.REPORT.STATUS.CLOSED; @@ -437,7 +432,7 @@ function ReportScreen({ > {isReportReadyForDisplay && !isLoadingInitialReportActions && !isLoading && ( `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, canEvict: false, + selector: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), }, report: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`, diff --git a/tests/unit/ReportActionsUtilsTest.js b/tests/unit/ReportActionsUtilsTest.js index 545d442e4799..b8b6eb5e7673 100644 --- a/tests/unit/ReportActionsUtilsTest.js +++ b/tests/unit/ReportActionsUtilsTest.js @@ -288,7 +288,7 @@ describe('ReportActionsUtils', () => { expect(result).toStrictEqual(input); }); - it('should filter out deleted and delete-pending comments', () => { + it('should filter out deleted, non-pending comments', () => { const input = [ { created: '2022-11-13 22:27:01.825', @@ -312,7 +312,6 @@ describe('ReportActionsUtils', () => { ]; const result = ReportActionsUtils.getSortedReportActionsForDisplay(input); input.pop(); - input.pop(); expect(result).toStrictEqual(input); }); }); From 7aa66e9b669b0983ae03e85055f07cbf999ba66b Mon Sep 17 00:00:00 2001 From: s-alves10 Date: Fri, 15 Dec 2023 06:59:15 -0600 Subject: [PATCH 036/193] fix: hide new marker for pending deleted action in online mode --- src/libs/ReportActionsUtils.ts | 11 +++++++++++ src/pages/home/report/ReportActionItem.js | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 21c382346e57..0f49cad2c9b9 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -379,6 +379,16 @@ function shouldReportActionBeVisible(reportAction: OnyxEntry, key: return !isDeleted || isPending || isDeletedParentAction(reportAction) || isReversedTransaction(reportAction); } +/** + * Checks if the new marker should be shown for the report action. + */ +function shouldShowNewMarker(reportAction: OnyxEntry): boolean { + if (!reportAction) { + return false; + } + return !(!isNetworkOffline && reportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); +} + /** * Checks if a reportAction is fit for display as report last action, meaning that * it satisfies shouldReportActionBeVisible, it's not whisper action and not deleted. @@ -816,6 +826,7 @@ export { isWhisperAction, isReimbursementQueuedAction, shouldReportActionBeVisible, + shouldShowNewMarker, shouldReportActionBeVisibleAsLastAction, hasRequestFromCurrentAccount, getFirstVisibleReportActionID, diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index a08f025e0530..f24701a2afbe 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -679,7 +679,7 @@ function ReportActionItem(props) { > {(hovered) => ( - {props.shouldDisplayNewMarker && } + {props.shouldDisplayNewMarker && ReportActionsUtils.shouldShowNewMarker(props.action) && } Date: Sat, 16 Dec 2023 14:08:13 +0800 Subject: [PATCH 037/193] add isOptimisticAction optimistically and remove on success --- src/libs/ReportUtils.ts | 3 ++- src/libs/actions/Report.ts | 2 +- src/libs/actions/Task.js | 2 +- src/types/onyx/ReportAction.ts | 3 +++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 24e795919649..d4f190d9ae6c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2470,6 +2470,7 @@ function buildOptimisticAddCommentReportAction(text?: string, file?: File): Opti attachmentInfo, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, shouldShow: true, + isOptimisticAction: true, }, }; } @@ -4143,7 +4144,7 @@ function getTaskAssigneeChatOnyxData( failureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${assigneeChatReportID}`, - value: {[optimisticAssigneeAddComment.reportAction.reportActionID ?? '']: {pendingAction: null}}, + value: {[optimisticAssigneeAddComment.reportAction.reportActionID ?? '']: {pendingAction: null, isOptimisticAction: null}}, }); } diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 135e616f7691..778e246bfa7f 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -363,7 +363,7 @@ function addActions(reportID: string, text = '', file?: File) { const successReportActions: OnyxCollection> = {}; Object.entries(optimisticReportActions).forEach(([actionKey]) => { - successReportActions[actionKey] = {pendingAction: null}; + successReportActions[actionKey] = {pendingAction: null, isOptimisticAction: null}; }); const successData: OnyxUpdate[] = [ diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js index 0fe6a528cda1..a980ec7c9a3b 100644 --- a/src/libs/actions/Task.js +++ b/src/libs/actions/Task.js @@ -183,7 +183,7 @@ function createTaskAndNavigate(parentReportID, title, description, assigneeEmail successData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, - value: {[optimisticAddCommentReport.reportAction.reportActionID]: {pendingAction: null}}, + value: {[optimisticAddCommentReport.reportAction.reportActionID]: {pendingAction: null, isOptimisticAction: null}}, }); // FOR PARENT REPORT (SHARE DESTINATION) diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 8e56aaa67345..e80e80f49f43 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -190,6 +190,9 @@ type ReportActionBase = { /** We manually add this field while sorting to detect the end of the list */ isNewestReportAction?: boolean; + + /** Flag for checking if data is from optimistic data */ + isOptimisticAction?: boolean; }; type ReportAction = ReportActionBase & OriginalMessage; From 9d06f4808c22e484bbfe1b7e9a9ba0b455c08767 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 16 Dec 2023 14:08:36 +0800 Subject: [PATCH 038/193] remove optimistic action from onyx --- src/libs/actions/ReportActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/ReportActions.ts b/src/libs/actions/ReportActions.ts index 7cd72fb4cd49..ebde0cef93af 100644 --- a/src/libs/actions/ReportActions.ts +++ b/src/libs/actions/ReportActions.ts @@ -13,7 +13,7 @@ function clearReportActionErrors(reportID: string, reportAction: ReportAction) { return; } - if (reportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { + if (reportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD || reportAction.isOptimisticAction) { // Delete the optimistic action Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, { [reportAction.reportActionID]: null, From 2445a22ae8e8b9c1e7cb773c11a71c01b3985c65 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 16 Dec 2023 14:08:59 +0800 Subject: [PATCH 039/193] fall back to add pending action if it's optimistic action --- src/pages/home/report/ReportActionItem.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index d4731d3b929b..bdd5e197cc49 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -694,7 +694,7 @@ function ReportActionItem(props) { ReportActions.clearReportActionErrors(props.report.reportID, props.action)} - pendingAction={props.draftMessage ? null : props.action.pendingAction} + pendingAction={props.draftMessage ? null : props.action.pendingAction || (props.action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : '')} shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(props.action, props.report.reportID)} errors={props.action.errors} errorRowStyles={[styles.ml10, styles.mr2]} From 6fb3faabe28a8e969bde8dea7f338d366ef7f29b Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 16 Dec 2023 14:30:55 +0800 Subject: [PATCH 040/193] clear isOptimisticAction on success data instead --- src/libs/ReportUtils.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d4f190d9ae6c..944ac2b10db4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4141,10 +4141,15 @@ function getTaskAssigneeChatOnyxData( value: optimisticAssigneeReport, }, ); + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${assigneeChatReportID}`, + value: {[optimisticAssigneeAddComment.reportAction.reportActionID ?? '']: {isOptimisticAction: null}}, + }); failureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${assigneeChatReportID}`, - value: {[optimisticAssigneeAddComment.reportAction.reportActionID ?? '']: {pendingAction: null, isOptimisticAction: null}}, + value: {[optimisticAssigneeAddComment.reportAction.reportActionID ?? '']: {pendingAction: null}}, }); } From 93c32e6f63fef7fedf41a66f7cdb003434d3838e Mon Sep 17 00:00:00 2001 From: s-alves10 Date: Sat, 16 Dec 2023 05:09:51 -0600 Subject: [PATCH 041/193] fix move new marker logic to ReportActionsList --- src/libs/ReportActionsUtils.ts | 4 ++-- src/pages/home/report/ReportActionItem.js | 2 +- src/pages/home/report/ReportActionsList.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 0f49cad2c9b9..cb04f1e42ed4 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -382,7 +382,7 @@ function shouldReportActionBeVisible(reportAction: OnyxEntry, key: /** * Checks if the new marker should be shown for the report action. */ -function shouldShowNewMarker(reportAction: OnyxEntry): boolean { +function shouldHideNewMarker(reportAction: OnyxEntry): boolean { if (!reportAction) { return false; } @@ -826,7 +826,7 @@ export { isWhisperAction, isReimbursementQueuedAction, shouldReportActionBeVisible, - shouldShowNewMarker, + shouldHideNewMarker, shouldReportActionBeVisibleAsLastAction, hasRequestFromCurrentAccount, getFirstVisibleReportActionID, diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index f24701a2afbe..a08f025e0530 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -679,7 +679,7 @@ function ReportActionItem(props) { > {(hovered) => ( - {props.shouldDisplayNewMarker && ReportActionsUtils.shouldShowNewMarker(props.action) && } + {props.shouldDisplayNewMarker && } ), [report, linkedReportActionID, sortedReportActions, mostRecentIOUReportActionID, shouldHideThreadDividerLine, shouldDisplayNewMarker], From 4fb31f2255272236cc2e97dd0af5b6f92d8fe500 Mon Sep 17 00:00:00 2001 From: s-alves10 Date: Sat, 16 Dec 2023 05:15:40 -0600 Subject: [PATCH 042/193] fix: wrong logic to hide new marker --- src/libs/ReportActionsUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index cb04f1e42ed4..61a34f44200c 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -384,9 +384,9 @@ function shouldReportActionBeVisible(reportAction: OnyxEntry, key: */ function shouldHideNewMarker(reportAction: OnyxEntry): boolean { if (!reportAction) { - return false; + return true; } - return !(!isNetworkOffline && reportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + return !isNetworkOffline && reportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; } /** From eb650a4386cddd768b69381a5b183ff699cd59de Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Mon, 18 Dec 2023 17:38:19 +0400 Subject: [PATCH 043/193] create new method --- src/libs/actions/IOU.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 802f0f00fffd..965122416054 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1028,6 +1028,21 @@ function updateMoneyRequestDate(transactionID, transactionThreadReportID, val) { API.write('UpdateMoneyRequestDate', params, onyxData); } +/** + * Updates the merchant field of a money request + * + * @param {String} transactionID + * @param {Number} transactionThreadReportID + * @param {String} val + */ +function updateMoneyRequestMerchant(transactionID, transactionThreadReportID, val) { + const transactionChanges = { + merchant: val, + }; + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, true); + API.write('UpdateMoneyRequestMerchant', params, onyxData); +} + /** * Edits an existing distance request * @@ -3403,6 +3418,7 @@ export { setUpDistanceTransaction, navigateToNextPage, updateMoneyRequestDate, + updateMoneyRequestMerchant, replaceReceipt, detachReceipt, getIOUReportID, From 02cc407639bbc53c92a309539dc0ca78f83c8bda Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Mon, 18 Dec 2023 17:42:45 +0400 Subject: [PATCH 044/193] use new method --- src/pages/EditRequestPage.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index 2bdf3d19f16a..54bef6a18d4c 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -133,6 +133,19 @@ function EditRequestPage({report, route, parentReport, policyCategories, policyT [transaction, report], ); + const saveMerchant = useCallback( + ({merchant: newMerchant}) => { + // If the value hasn't changed, don't request to save changes on the server and just close the modal + if (newMerchant === TransactionUtils.getMerchant(transaction)) { + Navigation.dismissModal(); + return; + } + IOU.updateMoneyRequestMerchant(transaction.transactionID, report.reportID, newMerchant); + Navigation.dismissModal(); + }, + [transaction, report], + ); + if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.DESCRIPTION) { return ( { - // In case the merchant hasn't been changed, do not make the API request. - if (transactionChanges.merchant.trim() === transactionMerchant) { - Navigation.dismissModal(); - return; - } - editMoneyRequest({merchant: transactionChanges.merchant.trim()}); - }} + onSubmit={saveMerchant} /> ); } From cc8c5edae9656b928d31ed76f9192e58b20b8ca0 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 18 Dec 2023 20:05:41 +0530 Subject: [PATCH 045/193] fixed mapbox zoom for single waypoint --- src/CONST.ts | 1 + src/components/MapView/MapView.tsx | 2 +- src/components/MapView/MapView.website.tsx | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index b29456ba170b..862cc25efbf2 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2931,6 +2931,7 @@ const CONST = { MAPBOX: { PADDING: 50, DEFAULT_ZOOM: 10, + SINGLE_MARKER_ZOOM: 15, DEFAULT_COORDINATE: [-122.4021, 37.7911], STYLE_URL: 'mapbox://styles/expensify/cllcoiqds00cs01r80kp34tmq', }, diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx index 7b3d73479dde..577325e53e7c 100644 --- a/src/components/MapView/MapView.tsx +++ b/src/components/MapView/MapView.tsx @@ -98,7 +98,7 @@ const MapView = forwardRef( if (waypoints.length === 1) { cameraRef.current?.setCamera({ - zoomLevel: 15, + zoomLevel: CONST.MAPBOX.SINGLE_MARKER_ZOOM, animationDuration: 1500, centerCoordinate: waypoints[0].coordinate, }); diff --git a/src/components/MapView/MapView.website.tsx b/src/components/MapView/MapView.website.tsx index 7910d7f93a29..55e99110737d 100644 --- a/src/components/MapView/MapView.website.tsx +++ b/src/components/MapView/MapView.website.tsx @@ -109,7 +109,7 @@ const MapView = forwardRef( if (waypoints.length === 1) { mapRef.flyTo({ center: waypoints[0].coordinate, - zoom: CONST.MAPBOX.DEFAULT_ZOOM, + zoom: CONST.MAPBOX.SINGLE_MARKER_ZOOM, }); return; } From ad89755432bf342dda66a139193f3b4076ab767b Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Mon, 18 Dec 2023 20:21:05 +0400 Subject: [PATCH 046/193] use udpateMoneyRequestBillable --- .../ReportActionItem/MoneyRequestView.js | 17 +++++++++++++++-- src/libs/actions/IOU.js | 16 ++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 817c88d456db..6ee5524b2868 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -1,7 +1,7 @@ import lodashGet from 'lodash/get'; import lodashValues from 'lodash/values'; import PropTypes from 'prop-types'; -import React, {useMemo} from 'react'; +import React, {useMemo, useCallback} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import categoryPropTypes from '@components/categoryPropTypes'; @@ -133,6 +133,19 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate let amountDescription = `${translate('iou.amount')}`; + const saveBillable = useCallback( + ({billable: newBillable}) => { + // If the value hasn't changed, don't request to save changes on the server and just close the modal + if (newBillable === TransactionUtils.getBillable(transaction)) { + Navigation.dismissModal(); + return; + } + IOU.updateMoneyRequestBillable(transaction.transactionID, report.reportID, newBillable); + Navigation.dismissModal(); + }, + [transaction, report], + ); + if (isCardTransaction) { if (formattedOriginalAmount) { amountDescription += ` • ${translate('iou.original')} ${formattedOriginalAmount}`; @@ -301,7 +314,7 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate IOU.editMoneyRequest(transaction, report.reportID, {billable: value})} + onToggle={saveBillable} /> )} diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 802f0f00fffd..73cbabe8df80 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1028,6 +1028,21 @@ function updateMoneyRequestDate(transactionID, transactionThreadReportID, val) { API.write('UpdateMoneyRequestDate', params, onyxData); } +/** + * Updates the billable field of a money request + * + * @param {String} transactionID + * @param {Number} transactionThreadReportID + * @param {String} val + */ +function updateMoneyRequestBillable(transactionID, transactionThreadReportID, val) { + const transactionChanges = { + billable: val, + }; + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, true); + API.write('UpdateMoneyRequestBillable', params, onyxData); +} + /** * Edits an existing distance request * @@ -3403,6 +3418,7 @@ export { setUpDistanceTransaction, navigateToNextPage, updateMoneyRequestDate, + updateMoneyRequestBillable, replaceReceipt, detachReceipt, getIOUReportID, From ac57d7a4bbc04b3a23c16b302210830ce1a73c1f Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 18 Dec 2023 17:30:12 +0100 Subject: [PATCH 047/193] migrate FloatingActionButton to TypeScript --- .../{FabPlusIcon.js => FabPlusIcon.tsx} | 8 ++--- .../{index.js => index.tsx} | 34 +++++++++++-------- 2 files changed, 22 insertions(+), 20 deletions(-) rename src/components/FloatingActionButton/{FabPlusIcon.js => FabPlusIcon.tsx} (88%) rename src/components/FloatingActionButton/{index.js => index.tsx} (73%) diff --git a/src/components/FloatingActionButton/FabPlusIcon.js b/src/components/FloatingActionButton/FabPlusIcon.tsx similarity index 88% rename from src/components/FloatingActionButton/FabPlusIcon.js rename to src/components/FloatingActionButton/FabPlusIcon.tsx index 09afa00f119d..c7fd7c2f49cd 100644 --- a/src/components/FloatingActionButton/FabPlusIcon.js +++ b/src/components/FloatingActionButton/FabPlusIcon.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React, {useEffect} from 'react'; import Animated, {Easing, interpolateColor, useAnimatedProps, useSharedValue, withTiming} from 'react-native-reanimated'; import Svg, {Path} from 'react-native-svg'; @@ -6,12 +5,12 @@ import useTheme from '@hooks/useTheme'; const AnimatedPath = Animated.createAnimatedComponent(Path); -const propTypes = { +type FabPlusIconProps = { /* Current state (active or not active) of the component */ - isActive: PropTypes.bool.isRequired, + isActive: boolean; }; -function FabPlusIcon({isActive}) { +function FabPlusIcon({isActive}: FabPlusIconProps) { const theme = useTheme(); const animatedValue = useSharedValue(isActive ? 1 : 0); @@ -43,7 +42,6 @@ function FabPlusIcon({isActive}) { ); } -FabPlusIcon.propTypes = propTypes; FabPlusIcon.displayName = 'FabPlusIcon'; export default FabPlusIcon; diff --git a/src/components/FloatingActionButton/index.js b/src/components/FloatingActionButton/index.tsx similarity index 73% rename from src/components/FloatingActionButton/index.js rename to src/components/FloatingActionButton/index.tsx index d341396c44b7..1ed968f59e15 100644 --- a/src/components/FloatingActionButton/index.js +++ b/src/components/FloatingActionButton/index.tsx @@ -1,6 +1,5 @@ -import PropTypes from 'prop-types'; -import React, {useEffect, useRef} from 'react'; -import {View} from 'react-native'; +import React, {ForwardedRef, useEffect, useRef} from 'react'; +import {GestureResponderEvent, Role, View} from 'react-native'; import Animated, {Easing, interpolateColor, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Tooltip from '@components/Tooltip/PopoverAnchorTooltip'; @@ -12,25 +11,25 @@ import FabPlusIcon from './FabPlusIcon'; const AnimatedPressable = Animated.createAnimatedComponent(PressableWithFeedback); AnimatedPressable.displayName = 'AnimatedPressable'; -const propTypes = { +type FloatingActionButtonProps = { /* Callback to fire on request to toggle the FloatingActionButton */ - onPress: PropTypes.func.isRequired, + onPress: (event: GestureResponderEvent | KeyboardEvent | undefined) => void; /* Current state (active or not active) of the component */ - isActive: PropTypes.bool.isRequired, + isActive: boolean; /* An accessibility label for the button */ - accessibilityLabel: PropTypes.string.isRequired, + accessibilityLabel: string; /* An accessibility role for the button */ - role: PropTypes.string.isRequired, + role: Role; }; -const FloatingActionButton = React.forwardRef(({onPress, isActive, accessibilityLabel, role}, ref) => { +function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: FloatingActionButtonProps, ref: ForwardedRef) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); - const fabPressable = useRef(null); + const fabPressable = useRef(null); const animatedValue = useSharedValue(isActive ? 1 : 0); const buttonRef = ref; @@ -57,7 +56,13 @@ const FloatingActionButton = React.forwardRef(({onPress, isActive, accessibility { fabPressable.current = el; - if (buttonRef) { + if (!buttonRef) { + return; + } + if (typeof buttonRef === 'function') { + buttonRef(el); + } + if (typeof buttonRef === 'object') { buttonRef.current = el; } }} @@ -66,7 +71,7 @@ const FloatingActionButton = React.forwardRef(({onPress, isActive, accessibility pressDimmingValue={1} onPress={(e) => { // Drop focus to avoid blue focus ring. - fabPressable.current.blur(); + fabPressable.current?.blur(); onPress(e); }} onLongPress={() => {}} @@ -77,9 +82,8 @@ const FloatingActionButton = React.forwardRef(({onPress, isActive, accessibility ); -}); +} -FloatingActionButton.propTypes = propTypes; FloatingActionButton.displayName = 'FloatingActionButton'; -export default FloatingActionButton; +export default React.forwardRef(FloatingActionButton); From 1de06fc1f2d7567cb06dd5e516ca39593ec19572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 19 Dec 2023 11:53:33 +0000 Subject: [PATCH 048/193] Migrate QRShare to TS --- src/components/QRCode.tsx | 16 +++---- .../QRShareWithDownload/index.native.tsx | 29 ++++--------- .../QRShare/QRShareWithDownload/index.tsx | 29 ++++--------- .../QRShare/QRShareWithDownload/types.ts | 5 +++ .../QRShare/getQrCodeDownloadFileName.ts | 2 +- src/components/QRShare/index.tsx | 32 +++++--------- src/components/QRShare/types.ts | 42 +++++++++++++++++++ 7 files changed, 85 insertions(+), 70 deletions(-) create mode 100644 src/components/QRShare/QRShareWithDownload/types.ts create mode 100644 src/components/QRShare/types.ts diff --git a/src/components/QRCode.tsx b/src/components/QRCode.tsx index 99a3a1f64118..19c4f3898267 100644 --- a/src/components/QRCode.tsx +++ b/src/components/QRCode.tsx @@ -1,12 +1,13 @@ -import React, {Ref} from 'react'; -import {ImageSourcePropType} from 'react-native'; +import React from 'react'; +import {ImageSourcePropType, NativeMethods} from 'react-native'; import QRCodeLibrary from 'react-native-qrcode-svg'; +import {Svg} from 'react-native-svg'; import useTheme from '@hooks/useTheme'; import CONST from '@src/CONST'; -type LogoRatio = typeof CONST.QR.DEFAULT_LOGO_SIZE_RATIO | typeof CONST.QR.EXPENSIFY_LOGO_SIZE_RATIO; +type QRCodeLogoRatio = typeof CONST.QR.DEFAULT_LOGO_SIZE_RATIO | typeof CONST.QR.EXPENSIFY_LOGO_SIZE_RATIO; -type LogoMarginRatio = typeof CONST.QR.DEFAULT_LOGO_MARGIN_RATIO | typeof CONST.QR.EXPENSIFY_LOGO_MARGIN_RATIO; +type QRCodeLogoMarginRatio = typeof CONST.QR.DEFAULT_LOGO_MARGIN_RATIO | typeof CONST.QR.EXPENSIFY_LOGO_MARGIN_RATIO; type QRCodeProps = { /** The QR code URL */ @@ -19,10 +20,10 @@ type QRCodeProps = { logo?: ImageSourcePropType; /** The size ratio of logo to QR code */ - logoRatio?: LogoRatio; + logoRatio?: QRCodeLogoRatio; /** The size ratio of margin around logo to QR code */ - logoMarginRatio?: LogoMarginRatio; + logoMarginRatio?: QRCodeLogoMarginRatio; /** The QRCode size */ size?: number; @@ -37,7 +38,7 @@ type QRCodeProps = { * Function to retrieve the internal component ref and be able to call it's * methods */ - getRef?: (ref: Ref) => Ref; + getRef?: (ref: Svg & NativeMethods) => Svg & NativeMethods; }; function QRCode({url, logo, getRef, size = 120, color, backgroundColor, logoRatio = CONST.QR.DEFAULT_LOGO_SIZE_RATIO, logoMarginRatio = CONST.QR.DEFAULT_LOGO_MARGIN_RATIO}: QRCodeProps) { @@ -61,3 +62,4 @@ function QRCode({url, logo, getRef, size = 120, color, backgroundColor, logoRati QRCode.displayName = 'QRCode'; export default QRCode; +export type {QRCodeLogoMarginRatio, QRCodeLogoRatio}; diff --git a/src/components/QRShare/QRShareWithDownload/index.native.tsx b/src/components/QRShare/QRShareWithDownload/index.native.tsx index e64c7b69df4a..91e473bba1fb 100644 --- a/src/components/QRShare/QRShareWithDownload/index.native.tsx +++ b/src/components/QRShare/QRShareWithDownload/index.native.tsx @@ -1,19 +1,20 @@ -import React, {forwardRef, useImperativeHandle, useRef} from 'react'; +import React, {ForwardedRef, forwardRef, useImperativeHandle, useRef} from 'react'; import ViewShot from 'react-native-view-shot'; import getQrCodeFileName from '@components/QRShare/getQrCodeDownloadFileName'; -import {qrShareDefaultProps, qrSharePropTypes} from '@components/QRShare/propTypes'; +import {QRShareProps} from '@components/QRShare/types'; import useNetwork from '@hooks/useNetwork'; import fileDownload from '@libs/fileDownload'; import QRShare from '..'; +import QRShareWithDownloadHandle from './types'; -function QRShareWithDownload({innerRef, ...props}) { +function QRShareWithDownload(props: QRShareProps, ref: ForwardedRef) { const {isOffline} = useNetwork(); - const qrCodeScreenshotRef = useRef(null); + const qrCodeScreenshotRef = useRef(null); useImperativeHandle( - innerRef, + ref, () => ({ - download: () => qrCodeScreenshotRef.current.capture().then((uri) => fileDownload(uri, getQrCodeFileName(props.title))), + download: () => qrCodeScreenshotRef.current?.capture?.().then((uri) => fileDownload(uri, getQrCodeFileName(props.title))), }), [props.title], ); @@ -23,24 +24,12 @@ function QRShareWithDownload({innerRef, ...props}) { ); } -QRShareWithDownload.propTypes = qrSharePropTypes; -QRShareWithDownload.defaultProps = qrShareDefaultProps; QRShareWithDownload.displayName = 'QRShareWithDownload'; -const QRShareWithDownloadWithRef = forwardRef((props, ref) => ( - -)); - -QRShareWithDownloadWithRef.displayName = 'QRShareWithDownloadWithRef'; - -export default QRShareWithDownloadWithRef; +export default forwardRef(QRShareWithDownload); diff --git a/src/components/QRShare/QRShareWithDownload/index.tsx b/src/components/QRShare/QRShareWithDownload/index.tsx index bf18a8eedaa4..beedf7ab0fb8 100644 --- a/src/components/QRShare/QRShareWithDownload/index.tsx +++ b/src/components/QRShare/QRShareWithDownload/index.tsx @@ -1,22 +1,23 @@ -import React, {forwardRef, useImperativeHandle, useRef} from 'react'; +import React, {ForwardedRef, forwardRef, useImperativeHandle, useRef} from 'react'; import getQrCodeFileName from '@components/QRShare/getQrCodeDownloadFileName'; -import {qrShareDefaultProps, qrSharePropTypes} from '@components/QRShare/propTypes'; +import {QRShareHandle, QRShareProps} from '@components/QRShare/types'; import useNetwork from '@hooks/useNetwork'; import fileDownload from '@libs/fileDownload'; import QRShare from '..'; +import QRShareWithDownloadHandle from './types'; -function QRShareWithDownload({innerRef, ...props}) { +function QRShareWithDownload(props: QRShareProps, ref: ForwardedRef) { const {isOffline} = useNetwork(); - const qrShareRef = useRef(null); + const qrShareRef = useRef(null); useImperativeHandle( - innerRef, + ref, () => ({ download: () => new Promise((resolve, reject) => { // eslint-disable-next-line es/no-optional-chaining const svg = qrShareRef.current?.getSvg(); - if (svg == null) { + if (!svg) { return reject(); } @@ -31,23 +32,11 @@ function QRShareWithDownload({innerRef, ...props}) { ref={qrShareRef} // eslint-disable-next-line react/jsx-props-no-spreading {...props} - logo={isOffline ? null : props.logo} + logo={isOffline ? undefined : props.logo} /> ); } -QRShareWithDownload.propTypes = qrSharePropTypes; -QRShareWithDownload.defaultProps = qrShareDefaultProps; QRShareWithDownload.displayName = 'QRShareWithDownload'; -const QRShareWithDownloadWithRef = forwardRef((props, ref) => ( - -)); - -QRShareWithDownloadWithRef.displayName = 'QRShareWithDownloadWithRef'; - -export default QRShareWithDownloadWithRef; +export default forwardRef(QRShareWithDownload); diff --git a/src/components/QRShare/QRShareWithDownload/types.ts b/src/components/QRShare/QRShareWithDownload/types.ts new file mode 100644 index 000000000000..c5df9cba55e2 --- /dev/null +++ b/src/components/QRShare/QRShareWithDownload/types.ts @@ -0,0 +1,5 @@ +type QRShareWithDownloadHandle = { + download: () => Promise | undefined; +}; + +export default QRShareWithDownloadHandle; diff --git a/src/components/QRShare/getQrCodeDownloadFileName.ts b/src/components/QRShare/getQrCodeDownloadFileName.ts index c1e73a1794fb..7041eac2b4b4 100644 --- a/src/components/QRShare/getQrCodeDownloadFileName.ts +++ b/src/components/QRShare/getQrCodeDownloadFileName.ts @@ -1,3 +1,3 @@ -const getQrCodeDownloadFileName = (title) => `${title}-ShareCode.png`; +const getQrCodeDownloadFileName = (title: string): string => `${title}-ShareCode.png`; export default getQrCodeDownloadFileName; diff --git a/src/components/QRShare/index.tsx b/src/components/QRShare/index.tsx index f644db844e53..0372b0a383c6 100644 --- a/src/components/QRShare/index.tsx +++ b/src/components/QRShare/index.tsx @@ -1,30 +1,30 @@ -import React, {forwardRef, useImperativeHandle, useRef, useState} from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; +import React, {ForwardedRef, forwardRef, useImperativeHandle, useRef, useState} from 'react'; +import {LayoutChangeEvent, NativeMethods, View} from 'react-native'; +import {Svg} from 'react-native-svg'; import ExpensifyWordmark from '@assets/images/expensify-wordmark.svg'; import QRCode from '@components/QRCode'; import Text from '@components/Text'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; -import {qrShareDefaultProps, qrSharePropTypes} from './propTypes'; +import {QRShareHandle, QRShareProps} from './types'; -function QRShare({innerRef, url, title, subtitle, logo, logoRatio, logoMarginRatio}) { +function QRShare({url, title, subtitle, logo, logoRatio, logoMarginRatio}: QRShareProps, ref: ForwardedRef) { const styles = useThemeStyles(); const theme = useTheme(); const [qrCodeSize, setQrCodeSize] = useState(1); - const svgRef = useRef(null); + const svgRef = useRef(); useImperativeHandle( - innerRef, + ref, () => ({ getSvg: () => svgRef.current, }), [], ); - const onLayout = (event) => { + const onLayout = (event: LayoutChangeEvent) => { const containerWidth = event.nativeEvent.layout.width - variables.qrShareHorizontalPadding * 2 || 0; setQrCodeSize(Math.max(1, containerWidth)); }; @@ -60,7 +60,7 @@ function QRShare({innerRef, url, title, subtitle, logo, logoRatio, logoMarginRat {title} - {!_.isEmpty(subtitle) && ( + {subtitle && ( ( - -)); - -QRShareWithRef.displayName = 'QRShareWithRef'; - -export default QRShareWithRef; +export default forwardRef(QRShare); diff --git a/src/components/QRShare/types.ts b/src/components/QRShare/types.ts new file mode 100644 index 000000000000..a1757f882619 --- /dev/null +++ b/src/components/QRShare/types.ts @@ -0,0 +1,42 @@ +import {ImageSourcePropType, NativeMethods} from 'react-native'; +import {Svg} from 'react-native-svg'; +import {QRCodeLogoMarginRatio, QRCodeLogoRatio} from '@components/QRCode'; + +type QRShareProps = { + /** + * The QR code URL + */ + url: string; + + /** + * The title that is displayed below the QR Code (usually the user or report name) + */ + title: string; + + /** + * The subtitle which will be shown below the title (usually user email or workspace name) + * */ + subtitle?: string; + + /** + * The logo which will be display in the middle of the QR code + */ + // logo: PropTypes.oneOfType([PropTypes.shape({uri: PropTypes.string}), PropTypes.number, PropTypes.string]), + logo?: ImageSourcePropType; + + /** + * The size ratio of logo to QR code + */ + logoRatio?: QRCodeLogoRatio; + + /** + * The size ratio of margin around logo to QR code + */ + logoMarginRatio?: QRCodeLogoMarginRatio; +}; + +type QRShareHandle = { + getSvg: () => (Svg & NativeMethods) | undefined; +}; + +export type {QRShareHandle, QRShareProps}; From 804228292db22058084c83df603fe1fe7a868736 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 19 Dec 2023 15:44:29 +0100 Subject: [PATCH 049/193] remove autoscrollToTopThreshold --- src/components/InvertedFlatList/BaseInvertedFlatList.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.js b/src/components/InvertedFlatList/BaseInvertedFlatList.js index 4206d5086a9e..abfad0f04be1 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.js +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.js @@ -2,8 +2,6 @@ import PropTypes from 'prop-types'; import React, {forwardRef} from 'react'; import FlatList from '@components/FlatList'; -const AUTOSCROLL_TO_TOP_THRESHOLD = 128; - const propTypes = { /** Same as FlatList can be any array of anything */ // eslint-disable-next-line react/forbid-prop-types @@ -25,7 +23,6 @@ const BaseInvertedFlatList = forwardRef((props, ref) => ( windowSize={15} maintainVisibleContentPosition={{ minIndexForVisible: 0, - autoscrollToTopThreshold: AUTOSCROLL_TO_TOP_THRESHOLD, }} inverted /> From a22fabdff0390d35716213d14318a9485c503d44 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 19 Dec 2023 15:47:30 +0100 Subject: [PATCH 050/193] automatically scroll to the bottom of the chat when the component mounts --- src/pages/home/report/ReportActionsList.js | 3 +-- src/pages/home/report/ReportActionsView.js | 8 ++++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 69977d72feda..a7f7e82ceca4 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -11,7 +11,6 @@ import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultPro import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import useReportScrollManager from '@hooks/useReportScrollManager'; import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; @@ -136,9 +135,9 @@ function ReportActionsList({ loadOlderChats, onLayout, isComposerFullSize, + reportScrollManager, }) { const styles = useThemeStyles(); - const reportScrollManager = useReportScrollManager(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); const route = useRoute(); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index e7b293babdf5..9fd81026b724 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -2,6 +2,7 @@ import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useContext, useEffect, useMemo, useRef} from 'react'; +import {InteractionManager} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import networkPropTypes from '@components/networkPropTypes'; @@ -11,6 +12,7 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withW import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; import useInitialValue from '@hooks/useInitialValue'; import usePrevious from '@hooks/usePrevious'; +import useReportScrollManager from '@hooks/useReportScrollManager'; import compose from '@libs/compose'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; import Performance from '@libs/Performance'; @@ -95,6 +97,7 @@ function ReportActionsView(props) { const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); const isFocused = useIsFocused(); + const reportScrollManager = useReportScrollManager(); const reportID = props.report.reportID; const hasNewestReportAction = lodashGet(props.reportActions[0], 'isNewestReportAction'); @@ -115,6 +118,10 @@ function ReportActionsView(props) { useEffect(() => { openReportIfNecessary(); + + InteractionManager.runAfterInteractions(() => { + reportScrollManager.scrollToBottom(); + }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -255,6 +262,7 @@ function ReportActionsView(props) { <> Date: Tue, 19 Dec 2023 15:50:45 +0100 Subject: [PATCH 051/193] fix MVCPFlatList --- src/components/FlatList/MVCPFlatList.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js index c9ec3c6a95c1..b6199e9174dd 100644 --- a/src/components/FlatList/MVCPFlatList.js +++ b/src/components/FlatList/MVCPFlatList.js @@ -1,7 +1,7 @@ /* eslint-disable es/no-optional-chaining, es/no-nullish-coalescing-operators, react/prop-types */ import PropTypes from 'prop-types'; import React from 'react'; -import {FlatList} from 'react-native'; +import {FlatList, InteractionManager} from 'react-native'; function mergeRefs(...args) { return function forwardRef(node) { @@ -137,8 +137,10 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont }, [adjustForMaintainVisibleContentPosition, getContentView, getScrollOffset, scrollToOffset]); React.useEffect(() => { - prepareForMaintainVisibleContentPosition(); - setupMutationObserver(); + InteractionManager.runAfterInteractions(() => { + prepareForMaintainVisibleContentPosition(); + setupMutationObserver(); + }); }, [prepareForMaintainVisibleContentPosition, setupMutationObserver]); const setMergedRef = useMergeRefs(scrollRef, forwardedRef); From fc60723e70dac9bdd07e318fd7ce096a6818a4a1 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Wed, 20 Dec 2023 03:04:46 +0530 Subject: [PATCH 052/193] fix the issue #33006 --- .../DatePicker/CalendarPicker/index.js | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/components/DatePicker/CalendarPicker/index.js b/src/components/DatePicker/CalendarPicker/index.js index a404c4746397..a4aedcf9a6be 100644 --- a/src/components/DatePicker/CalendarPicker/index.js +++ b/src/components/DatePicker/CalendarPicker/index.js @@ -112,14 +112,46 @@ class CalendarPicker extends React.PureComponent { * Handles the user pressing the previous month arrow of the calendar picker. */ moveToPrevMonth() { - this.setState((prev) => ({currentDateView: subMonths(new Date(prev.currentDateView), 1)})); + this.setState((prev) => { + const prevMonth = subMonths(new Date(prev.currentDateView), 1); + // if year is subtracted, we need to update the years list + let newYears = prev.years; + if (prevMonth.getFullYear() < prev.currentDateView.getFullYear()) { + newYears = _.map(prev.years, (item) => ({ + ...item, + isSelected: item.value === prevMonth.getFullYear(), + })); + } + + return { + ...prev, + currentDateView: prevMonth, + years: newYears, + }; + }); } /** * Handles the user pressing the next month arrow of the calendar picker. */ moveToNextMonth() { - this.setState((prev) => ({currentDateView: addMonths(new Date(prev.currentDateView), 1)})); + this.setState((prev) => { + const nextMonth = addMonths(new Date(prev.currentDateView), 1); + // if year is added, we need to update the years list + let newYears = prev.years; + if (nextMonth.getFullYear() > prev.currentDateView.getFullYear()) { + newYears = _.map(prev.years, (item) => ({ + ...item, + isSelected: item.value === nextMonth.getFullYear(), + })); + } + + return { + ...prev, + currentDateView: nextMonth, + years: newYears, + }; + }); } render() { From 71c493ecd93482e8e9ecd001b20e90c51c726b2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 20 Dec 2023 12:05:06 +0000 Subject: [PATCH 053/193] Simplify typings --- src/components/QRCode.tsx | 4 ++-- src/components/QRShare/index.tsx | 4 ++-- src/components/QRShare/types.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/QRCode.tsx b/src/components/QRCode.tsx index 19c4f3898267..d059da36e963 100644 --- a/src/components/QRCode.tsx +++ b/src/components/QRCode.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {ImageSourcePropType, NativeMethods} from 'react-native'; +import {ImageSourcePropType} from 'react-native'; import QRCodeLibrary from 'react-native-qrcode-svg'; import {Svg} from 'react-native-svg'; import useTheme from '@hooks/useTheme'; @@ -38,7 +38,7 @@ type QRCodeProps = { * Function to retrieve the internal component ref and be able to call it's * methods */ - getRef?: (ref: Svg & NativeMethods) => Svg & NativeMethods; + getRef?: (ref: Svg) => Svg; }; function QRCode({url, logo, getRef, size = 120, color, backgroundColor, logoRatio = CONST.QR.DEFAULT_LOGO_SIZE_RATIO, logoMarginRatio = CONST.QR.DEFAULT_LOGO_MARGIN_RATIO}: QRCodeProps) { diff --git a/src/components/QRShare/index.tsx b/src/components/QRShare/index.tsx index 0372b0a383c6..298ee7f23015 100644 --- a/src/components/QRShare/index.tsx +++ b/src/components/QRShare/index.tsx @@ -1,5 +1,5 @@ import React, {ForwardedRef, forwardRef, useImperativeHandle, useRef, useState} from 'react'; -import {LayoutChangeEvent, NativeMethods, View} from 'react-native'; +import {LayoutChangeEvent, View} from 'react-native'; import {Svg} from 'react-native-svg'; import ExpensifyWordmark from '@assets/images/expensify-wordmark.svg'; import QRCode from '@components/QRCode'; @@ -14,7 +14,7 @@ function QRShare({url, title, subtitle, logo, logoRatio, logoMarginRatio}: QRSha const theme = useTheme(); const [qrCodeSize, setQrCodeSize] = useState(1); - const svgRef = useRef(); + const svgRef = useRef(); useImperativeHandle( ref, diff --git a/src/components/QRShare/types.ts b/src/components/QRShare/types.ts index a1757f882619..753aab6ba966 100644 --- a/src/components/QRShare/types.ts +++ b/src/components/QRShare/types.ts @@ -1,4 +1,4 @@ -import {ImageSourcePropType, NativeMethods} from 'react-native'; +import {ImageSourcePropType} from 'react-native'; import {Svg} from 'react-native-svg'; import {QRCodeLogoMarginRatio, QRCodeLogoRatio} from '@components/QRCode'; @@ -36,7 +36,7 @@ type QRShareProps = { }; type QRShareHandle = { - getSvg: () => (Svg & NativeMethods) | undefined; + getSvg: () => Svg | undefined; }; export type {QRShareHandle, QRShareProps}; From 5ec10c8a5d0259bc496f67b82b2b0742e836a38c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 20 Dec 2023 12:09:18 +0000 Subject: [PATCH 054/193] Remove comment --- src/components/QRShare/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/QRShare/types.ts b/src/components/QRShare/types.ts index 753aab6ba966..3b964296ca4c 100644 --- a/src/components/QRShare/types.ts +++ b/src/components/QRShare/types.ts @@ -21,7 +21,6 @@ type QRShareProps = { /** * The logo which will be display in the middle of the QR code */ - // logo: PropTypes.oneOfType([PropTypes.shape({uri: PropTypes.string}), PropTypes.number, PropTypes.string]), logo?: ImageSourcePropType; /** From 3ab46b8dfb31440137a9f47dadf79a1807270c48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 20 Dec 2023 12:33:37 +0000 Subject: [PATCH 055/193] Minor fix to avoid console errors when subtitle is empty string --- src/components/QRShare/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/QRShare/index.tsx b/src/components/QRShare/index.tsx index 298ee7f23015..0f39c96c82be 100644 --- a/src/components/QRShare/index.tsx +++ b/src/components/QRShare/index.tsx @@ -60,7 +60,7 @@ function QRShare({url, title, subtitle, logo, logoRatio, logoMarginRatio}: QRSha {title} - {subtitle && ( + {!!subtitle && ( Date: Wed, 20 Dec 2023 15:03:28 +0100 Subject: [PATCH 056/193] Refactor get physical card flow --- src/components/CheckboxWithLabel.tsx | 2 +- src/pages/EnablePayments/AdditionalDetailsStep.js | 4 ++-- src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js | 6 +++--- src/pages/settings/Wallet/Card/GetPhysicalCardName.js | 7 +++++-- src/pages/settings/Wallet/Card/GetPhysicalCardPhone.js | 4 +++- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/components/CheckboxWithLabel.tsx b/src/components/CheckboxWithLabel.tsx index 9660c9e1a2e5..1dfe137269e4 100644 --- a/src/components/CheckboxWithLabel.tsx +++ b/src/components/CheckboxWithLabel.tsx @@ -40,7 +40,7 @@ type CheckboxWithLabelProps = RequiredLabelProps & { /** Error text to display */ errorText?: string; - /** Value for checkbox. This prop is intended to be set by Form.js only */ + /** Value for checkbox. This prop is intended to be set by FormProvider only */ value?: boolean; /** The default value for the checkbox */ diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.js b/src/pages/EnablePayments/AdditionalDetailsStep.js index d937be615370..faa525a318ab 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.js +++ b/src/pages/EnablePayments/AdditionalDetailsStep.js @@ -87,7 +87,7 @@ function AdditionalDetailsStep({walletAdditionalDetails, translate, currentUserP const shouldAskForFullSSN = walletAdditionalDetails.errorCode === CONST.WALLET.ERROR.SSN; /** - * @param {Object} values The values object is passed from Form.js and contains info for each form element that has an inputID + * @param {Object} values The values object is passed from FormProvider and contains info for each form element that has an inputID * @returns {Object} */ const validate = (values) => { @@ -128,7 +128,7 @@ function AdditionalDetailsStep({walletAdditionalDetails, translate, currentUserP }; /** - * @param {Object} values The values object is passed from Form.js and contains info for each form element that has an inputID + * @param {Object} values The values object is passed from FormProvider and contains info for each form element that has an inputID */ const activateWallet = (values) => { const personalDetails = { diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js index 1d1ce906189b..cd1f4591a61a 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js @@ -3,7 +3,7 @@ import React, {useCallback, useEffect, useRef} from 'react'; import {Text} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; -import Form from '@components/Form'; +import FormProvider from '@components/Form/FormProvider'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -114,7 +114,7 @@ const defaultProps = { loginList: {}, isConfirmation: false, renderContent: (onSubmit, submitButtonText, styles, children = () => {}, onValidate = () => ({})) => ( -
{children} -
+ ), onValidate: () => ({}), }; diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardName.js b/src/pages/settings/Wallet/Card/GetPhysicalCardName.js index 0040dac8b75f..5b954d432cce 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardName.js +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardName.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; +import InputWrapper from '@components/Form/InputWrapper'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -72,7 +73,8 @@ function GetPhysicalCardName({ title={translate('getPhysicalCard.header')} onValidate={onValidate} > - - - Date: Wed, 20 Dec 2023 13:19:43 -0600 Subject: [PATCH 057/193] Update Reimbursing-Reports.md Adding images to the help article per this GH: https://github.com/Expensify/Expensify/issues/311453 --- .../expensify-classic/send-payments/Reimbursing-Reports.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/articles/expensify-classic/send-payments/Reimbursing-Reports.md b/docs/articles/expensify-classic/send-payments/Reimbursing-Reports.md index e55d99d70827..47c367cef2f0 100644 --- a/docs/articles/expensify-classic/send-payments/Reimbursing-Reports.md +++ b/docs/articles/expensify-classic/send-payments/Reimbursing-Reports.md @@ -19,6 +19,8 @@ To reimburse directly in Expensify, the following needs to be already configured If all of those settings are in place, to reimburse a report, you will click **Reimburse** on the report and then select **Via Direct Deposit (ACH)**. +![Reimbursing Reports Dropdown]({{site.url}}/assets/images/ReimbursingReportsDropdown.png){:width="100%"} + ## Indirect or Manual Reimbursement If you don't have the option to utilize direct reimbursement, you can choose to mark a report as reimbursed by clicking the **Reimburse** button at the top of the report and then selecting **I’ll do it manually – just mark as reimbursed**. @@ -73,3 +75,5 @@ Only a workspace admin who has added a verified business bank account to their E Instead of a bulk reimbursement option, you can set up automatic reimbursement. With this configured, reports below a certain threshold (defined by you) will be automatically reimbursed via ACH as soon as they're "final approved." To set your manual reimbursement threshold, head to **Settings > Workspace > Group > _[Workspace Name]_ > Reimbursement > Manual Reimbursement**. + +![Manual Reimbursement]({{site.url}}/assets/images/ReimbursingManual.png){:width="100%"} From 0446d429f73ec5eb970322641da2d29c9408e104 Mon Sep 17 00:00:00 2001 From: Srikar Parsi Date: Wed, 20 Dec 2023 19:00:21 -0500 Subject: [PATCH 058/193] Dont show hidden notif pref in profile page --- src/pages/ProfilePage.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index 10ca3cd79190..d0c30b7dc672 100755 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -133,7 +133,8 @@ function ProfilePage(props) { const navigateBackTo = lodashGet(props.route, 'params.backTo', ROUTES.HOME); - const notificationPreference = !_.isEmpty(props.report) ? props.translate(`notificationPreferencesPage.notificationPreferences.${props.report.notificationPreference}`) : ''; + const shouldShowNotificationPreference = !_.isEmpty(props.report) && props.report.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; + const notificationPreference = shouldShowNotificationPreference ? props.translate(`notificationPreferencesPage.notificationPreferences.${props.report.notificationPreference}`) : ''; // eslint-disable-next-line rulesdir/prefer-early-return useEffect(() => { @@ -226,7 +227,7 @@ function ProfilePage(props) { ) : null} {shouldShowLocalTime && }
- {!_.isEmpty(props.report) && notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN && ( + {shouldShowNotificationPreference && ( Date: Thu, 21 Dec 2023 16:09:13 +0700 Subject: [PATCH 059/193] revert class file --- .../index.js => ReportActionsList.js} | 22 +++++++------- .../DeviceEventListenerRef.ts | 30 ------------------- 2 files changed, 11 insertions(+), 41 deletions(-) rename src/pages/home/report/{ReportActionsList/index.js => ReportActionsList.js} (96%) delete mode 100644 src/pages/home/report/ReportActionsList/DeviceEventListenerRef.ts diff --git a/src/pages/home/report/ReportActionsList/index.js b/src/pages/home/report/ReportActionsList.js similarity index 96% rename from src/pages/home/report/ReportActionsList/index.js rename to src/pages/home/report/ReportActionsList.js index ac3d4375bcfa..9e573723619d 100644 --- a/src/pages/home/report/ReportActionsList/index.js +++ b/src/pages/home/report/ReportActionsList.js @@ -2,6 +2,7 @@ import {useRoute} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {DeviceEventEmitter} from 'react-native'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import _ from 'underscore'; import InvertedFlatList from '@components/InvertedFlatList'; @@ -16,16 +17,15 @@ import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import FloatingMessageCounter from '@pages/home/report/FloatingMessageCounter'; -import ListBoundaryLoader from '@pages/home/report/ListBoundaryLoader/ListBoundaryLoader'; -import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; -import ReportActionsListItemRenderer from '@pages/home/report/ReportActionsListItemRenderer'; import Visibility from '@libs/Visibility'; import reportPropTypes from '@pages/reportPropTypes'; import variables from '@styles/variables'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; -import DeviceEventListenerRef from './DeviceEventListenerRef'; +import FloatingMessageCounter from './FloatingMessageCounter'; +import ListBoundaryLoader from './ListBoundaryLoader/ListBoundaryLoader'; +import reportActionPropTypes from './reportActionPropTypes'; +import ReportActionsListItemRenderer from './ReportActionsListItemRenderer'; const propTypes = { /** The report currently being looked at */ @@ -144,6 +144,8 @@ function ReportActionsList({ const route = useRoute(); const opacity = useSharedValue(0); const userActiveSince = useRef(null); + const unreadActionSubscription = useRef(null); + const readNewestActionSubscription = useRef(null); const markerInit = () => { if (!cacheUnreadMarkers.has(report.reportID)) { @@ -229,15 +231,13 @@ function ReportActionsList({ }; // Listen to specific reportID for unread event and set the marker to new message - const unreadActionSubscription = DeviceEventListenerRef.use('unreadAction'); - unreadActionSubscription.add(report.reportID, (newLastReadTime) => { + unreadActionSubscription.current = DeviceEventEmitter.addListener(`unreadAction_${report.reportID}`, (newLastReadTime) => { resetUnreadMarker(newLastReadTime); setMessageManuallyMarkedUnread(new Date().getTime()); }); - const readNewestActionSubscription = DeviceEventListenerRef.use('readNewestAction'); // Listen to specific reportID for read newest action event and reset the marker - readNewestActionSubscription.add(report.reportID, (newLastReadTime) => { + readNewestActionSubscription.current = DeviceEventEmitter.addListener(`readNewestAction_${report.reportID}`, (newLastReadTime) => { resetUnreadMarker(newLastReadTime); setMessageManuallyMarkedUnread(0); }); @@ -245,8 +245,8 @@ function ReportActionsList({ return () => { // If the reportID changes, we reset the userActiveSince to null, we need to do it because // this component doesn't unmount when the reportID changes - unreadActionSubscription.remove(); - readNewestActionSubscription.remove(); + unreadActionSubscription.current.remove(); + readNewestActionSubscription.current.remove(); }; }, [report.reportID]); diff --git a/src/pages/home/report/ReportActionsList/DeviceEventListenerRef.ts b/src/pages/home/report/ReportActionsList/DeviceEventListenerRef.ts deleted file mode 100644 index 973087e07c4c..000000000000 --- a/src/pages/home/report/ReportActionsList/DeviceEventListenerRef.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {DeviceEventEmitter, EmitterSubscription} from 'react-native'; - -export default class DeviceEventListenerRef { - private typePrefix: string; - - private subscription: EmitterSubscription | null = null; - - constructor(typePrefix: string) { - this.typePrefix = typePrefix; - } - - static use(typePrefix: string): DeviceEventListenerRef { - return new DeviceEventListenerRef(typePrefix); - } - - add( - typeSuffix: string, // report.reportID - listener: (data: unknown) => void, - ): void { - this.subscription = DeviceEventEmitter.addListener(`${this.typePrefix}_${typeSuffix}`, listener); - } - - remove(): void { - if (!this.subscription) { - return; - } - this.subscription.remove(); - this.subscription = null; - } -} From 747f3ad8a7cfc5c46c25119182345c396c884276 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 21 Dec 2023 16:39:55 +0700 Subject: [PATCH 060/193] remove trailing slash in route --- src/ROUTES.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index ca1fe9f0e81a..32b7e915b10a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -314,61 +314,61 @@ const ROUTES = { getRoute: (iouType: ValueOf, transactionID: string, reportID: string) => `create/${iouType}/start/${transactionID}/${reportID}` as const, }, MONEY_REQUEST_STEP_CONFIRMATION: { - route: 'create/:iouType/confirmation/:transactionID/:reportID/', + route: 'create/:iouType/confirmation/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string) => `create/${iouType}/confirmation/${transactionID}/${reportID}/` as const, }, MONEY_REQUEST_STEP_AMOUNT: { - route: 'create/:iouType/amount/:transactionID/:reportID/', + route: 'create/:iouType/amount/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => getUrlWithBackToParam(`create/${iouType}/amount/${transactionID}/${reportID}/`, backTo), }, MONEY_REQUEST_STEP_CATEGORY: { - route: 'create/:iouType/category/:transactionID/:reportID/', + route: 'create/:iouType/category/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => getUrlWithBackToParam(`create/${iouType}/category/${transactionID}/${reportID}/`, backTo), }, MONEY_REQUEST_STEP_CURRENCY: { - route: 'create/:iouType/currency/:transactionID/:reportID/:pageIndex?/', + route: 'create/:iouType/currency/:transactionID/:reportID/:pageIndex?', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, pageIndex = '', backTo = '') => getUrlWithBackToParam(`create/${iouType}/currency/${transactionID}/${reportID}/${pageIndex}`, backTo), }, MONEY_REQUEST_STEP_DATE: { - route: 'create/:iouType/date/:transactionID/:reportID/', + route: 'create/:iouType/date/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => getUrlWithBackToParam(`create/${iouType}/date/${transactionID}/${reportID}/`, backTo), }, MONEY_REQUEST_STEP_DESCRIPTION: { - route: 'create/:iouType/description/:transactionID/:reportID/', + route: 'create/:iouType/description/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => getUrlWithBackToParam(`create/${iouType}/description/${transactionID}/${reportID}/`, backTo), }, MONEY_REQUEST_STEP_DISTANCE: { - route: 'create/:iouType/distance/:transactionID/:reportID/', + route: 'create/:iouType/distance/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => getUrlWithBackToParam(`create/${iouType}/distance/${transactionID}/${reportID}/`, backTo), }, MONEY_REQUEST_STEP_MERCHANT: { - route: 'create/:iouType/merchante/:transactionID/:reportID/', + route: 'create/:iouType/merchante/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => getUrlWithBackToParam(`create/${iouType}/merchante/${transactionID}/${reportID}/`, backTo), }, MONEY_REQUEST_STEP_PARTICIPANTS: { - route: 'create/:iouType/participants/:transactionID/:reportID/', + route: 'create/:iouType/participants/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => getUrlWithBackToParam(`create/${iouType}/participants/${transactionID}/${reportID}/`, backTo), }, MONEY_REQUEST_STEP_SCAN: { - route: 'create/:iouType/scan/:transactionID/:reportID/', + route: 'create/:iouType/scan/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => getUrlWithBackToParam(`create/${iouType}/scan/${transactionID}/${reportID}/`, backTo), }, MONEY_REQUEST_STEP_TAG: { - route: 'create/:iouType/tag/:transactionID/:reportID/', + route: 'create/:iouType/tag/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => getUrlWithBackToParam(`create/${iouType}/tag/${transactionID}/${reportID}/`, backTo), }, MONEY_REQUEST_STEP_WAYPOINT: { - route: 'create/:iouType/waypoint/:transactionID/:reportID/:pageIndex/', + route: 'create/:iouType/waypoint/:transactionID/:reportID/:pageIndex', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, pageIndex = '', backTo = '') => getUrlWithBackToParam(`create/${iouType}/waypoint/${transactionID}/${reportID}/${pageIndex}`, backTo), }, From dfcff5e7ea389eb73f4557562a8ee59346298aab Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Thu, 21 Dec 2023 13:58:50 +0100 Subject: [PATCH 061/193] simplify ref assignment --- src/components/FloatingActionButton/index.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/components/FloatingActionButton/index.tsx b/src/components/FloatingActionButton/index.tsx index 1ed968f59e15..8a1ba2761d86 100644 --- a/src/components/FloatingActionButton/index.tsx +++ b/src/components/FloatingActionButton/index.tsx @@ -56,13 +56,8 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo { fabPressable.current = el; - if (!buttonRef) { - return; - } - if (typeof buttonRef === 'function') { - buttonRef(el); - } - if (typeof buttonRef === 'object') { + + if (buttonRef && 'current' in buttonRef) { buttonRef.current = el; } }} From 62ed31692dc8e49b8cb71f4a73f4218a0a600ca3 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 21 Dec 2023 18:18:03 +0100 Subject: [PATCH 062/193] post-merge updates --- src/components/FlatList/MVCPFlatList.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js index b6199e9174dd..e8bc88b70e16 100644 --- a/src/components/FlatList/MVCPFlatList.js +++ b/src/components/FlatList/MVCPFlatList.js @@ -1,7 +1,7 @@ /* eslint-disable es/no-optional-chaining, es/no-nullish-coalescing-operators, react/prop-types */ import PropTypes from 'prop-types'; import React from 'react'; -import {FlatList, InteractionManager} from 'react-native'; +import {FlatList} from 'react-native'; function mergeRefs(...args) { return function forwardRef(node) { @@ -137,7 +137,7 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont }, [adjustForMaintainVisibleContentPosition, getContentView, getScrollOffset, scrollToOffset]); React.useEffect(() => { - InteractionManager.runAfterInteractions(() => { + requestAnimationFrame(() => { prepareForMaintainVisibleContentPosition(); setupMutationObserver(); }); From 24f0fdc252c71c61eab25cdada4f7d80c38e2ab1 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 21 Dec 2023 12:45:57 -0500 Subject: [PATCH 063/193] Remove rn web invertion patch --- .../react-native-web+0.19.9+001+initial.patch | 872 +++++++++++++----- ...react-native-web+0.19.9+002+fix-mvcp.patch | 687 -------------- ...tive-web+0.19.9+002+measureInWindow.patch} | 0 ...e-web+0.19.9+003+fix-pointer-events.patch} | 0 4 files changed, 617 insertions(+), 942 deletions(-) delete mode 100644 patches/react-native-web+0.19.9+002+fix-mvcp.patch rename patches/{react-native-web+0.19.9+003+measureInWindow.patch => react-native-web+0.19.9+002+measureInWindow.patch} (100%) rename patches/{react-native-web+0.19.9+004+fix-pointer-events.patch => react-native-web+0.19.9+003+fix-pointer-events.patch} (100%) diff --git a/patches/react-native-web+0.19.9+001+initial.patch b/patches/react-native-web+0.19.9+001+initial.patch index d88ef83d4bcd..91ba6bfd59c0 100644 --- a/patches/react-native-web+0.19.9+001+initial.patch +++ b/patches/react-native-web+0.19.9+001+initial.patch @@ -1,286 +1,648 @@ diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -index c879838..288316c 100644 +index c879838..0c9dfcb 100644 --- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -@@ -117,6 +117,14 @@ function findLastWhere(arr, predicate) { - * - */ - class VirtualizedList extends StateSafePureComponent { -+ pushOrUnshift(input, item) { -+ if (this.props.inverted) { -+ input.unshift(item); -+ } else { -+ input.push(item); +@@ -285,7 +285,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[missing-local-annot] + + constructor(_props) { +- var _this$props$updateCel; ++ var _this$props$updateCel, _this$props$maintainV, _this$props$maintainV2; + super(_props); + this._getScrollMetrics = () => { + return this._scrollMetrics; +@@ -520,6 +520,11 @@ class VirtualizedList extends StateSafePureComponent { + visibleLength, + zoomScale + }; ++ if (this.state.pendingScrollUpdateCount > 0) { ++ this.setState(state => ({ ++ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1 ++ })); ++ } + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + if (!this.props) { + return; +@@ -569,7 +574,7 @@ class VirtualizedList extends StateSafePureComponent { + this._updateCellsToRender = () => { + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + this.setState((state, props) => { +- var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport); ++ var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport, state.pendingScrollUpdateCount); + var renderMask = VirtualizedList._createRenderMask(props, cellsAroundViewport, this._getNonViewportRenderRegions(props)); + if (cellsAroundViewport.first === state.cellsAroundViewport.first && cellsAroundViewport.last === state.cellsAroundViewport.last && renderMask.equals(state.renderMask)) { + return null; +@@ -589,7 +594,7 @@ class VirtualizedList extends StateSafePureComponent { + return { + index, + item, +- key: this._keyExtractor(item, index, props), ++ key: VirtualizedList._keyExtractor(item, index, props), + isViewable + }; + }; +@@ -621,12 +626,10 @@ class VirtualizedList extends StateSafePureComponent { + }; + this._getFrameMetrics = (index, props) => { + var data = props.data, +- getItem = props.getItem, + getItemCount = props.getItemCount, + getItemLayout = props.getItemLayout; + invariant(index >= 0 && index < getItemCount(data), 'Tried to get frame for out of range index ' + index); +- var item = getItem(data, index); +- var frame = this._frames[this._keyExtractor(item, index, props)]; ++ var frame = this._frames[VirtualizedList._getItemKey(props, index)]; + if (!frame || frame.index !== index) { + if (getItemLayout) { + /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment +@@ -650,7 +653,7 @@ class VirtualizedList extends StateSafePureComponent { + + // The last cell we rendered may be at a new index. Bail if we don't know + // where it is. +- if (focusedCellIndex >= itemCount || this._keyExtractor(props.getItem(props.data, focusedCellIndex), focusedCellIndex, props) !== this._lastFocusedCellKey) { ++ if (focusedCellIndex >= itemCount || VirtualizedList._getItemKey(props, focusedCellIndex) !== this._lastFocusedCellKey) { + return []; + } + var first = focusedCellIndex; +@@ -690,9 +693,15 @@ class VirtualizedList extends StateSafePureComponent { + } + } + var initialRenderRegion = VirtualizedList._initialRenderRegion(_props); ++ var minIndexForVisible = (_this$props$maintainV = (_this$props$maintainV2 = this.props.maintainVisibleContentPosition) == null ? void 0 : _this$props$maintainV2.minIndexForVisible) !== null && _this$props$maintainV !== void 0 ? _this$props$maintainV : 0; + this.state = { + cellsAroundViewport: initialRenderRegion, +- renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion) ++ renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion), ++ firstVisibleItemKey: this.props.getItemCount(this.props.data) > minIndexForVisible ? VirtualizedList._getItemKey(this.props, minIndexForVisible) : null, ++ // When we have a non-zero initialScrollIndex, we will receive a ++ // scroll event later so this will prevent the window from updating ++ // until we get a valid offset. ++ pendingScrollUpdateCount: this.props.initialScrollIndex != null && this.props.initialScrollIndex > 0 ? 1 : 0 + }; + + // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. +@@ -748,6 +757,26 @@ class VirtualizedList extends StateSafePureComponent { + } + } + } ++ static _findItemIndexWithKey(props, key, hint) { ++ var itemCount = props.getItemCount(props.data); ++ if (hint != null && hint >= 0 && hint < itemCount) { ++ var curKey = VirtualizedList._getItemKey(props, hint); ++ if (curKey === key) { ++ return hint; ++ } ++ } ++ for (var ii = 0; ii < itemCount; ii++) { ++ var _curKey = VirtualizedList._getItemKey(props, ii); ++ if (_curKey === key) { ++ return ii; ++ } + } ++ return null; + } -+ - // scrollToEnd may be janky without getItemLayout prop - scrollToEnd(params) { - var animated = params ? params.animated : true; -@@ -350,6 +358,7 @@ class VirtualizedList extends StateSafePureComponent { - }; - this._defaultRenderScrollComponent = props => { - var onRefresh = props.onRefresh; -+ var inversionStyle = this.props.inverted ? this.props.horizontal ? styles.rowReverse : styles.columnReverse : null; - if (this._isNestedWithSameOrientation()) { - // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors - return /*#__PURE__*/React.createElement(View, props); -@@ -367,13 +376,16 @@ class VirtualizedList extends StateSafePureComponent { - refreshing: props.refreshing, - onRefresh: onRefresh, - progressViewOffset: props.progressViewOffset -- }) : props.refreshControl -+ }) : props.refreshControl, -+ contentContainerStyle: [inversionStyle, this.props.contentContainerStyle] - })) - ); - } else { - // $FlowFixMe[prop-missing] Invalid prop usage - // $FlowFixMe[incompatible-use] -- return /*#__PURE__*/React.createElement(ScrollView, props); -+ return /*#__PURE__*/React.createElement(ScrollView, _extends({}, props, { -+ contentContainerStyle: [inversionStyle, this.props.contentContainerStyle] -+ })); ++ static _getItemKey(props, index) { ++ var item = props.getItem(props.data, index); ++ return VirtualizedList._keyExtractor(item, index, props); ++ } + static _createRenderMask(props, cellsAroundViewport, additionalRegions) { + var itemCount = props.getItemCount(props.data); + invariant(cellsAroundViewport.first >= 0 && cellsAroundViewport.last >= cellsAroundViewport.first - 1 && cellsAroundViewport.last < itemCount, "Invalid cells around viewport \"[" + cellsAroundViewport.first + ", " + cellsAroundViewport.last + "]\" was passed to VirtualizedList._createRenderMask"); +@@ -796,7 +825,7 @@ class VirtualizedList extends StateSafePureComponent { + } + } + } +- _adjustCellsAroundViewport(props, cellsAroundViewport) { ++ _adjustCellsAroundViewport(props, cellsAroundViewport, pendingScrollUpdateCount) { + var data = props.data, + getItemCount = props.getItemCount; + var onEndReachedThreshold = onEndReachedThresholdOrDefault(props.onEndReachedThreshold); +@@ -819,17 +848,9 @@ class VirtualizedList extends StateSafePureComponent { + last: Math.min(cellsAroundViewport.last + renderAhead, getItemCount(data) - 1) + }; + } else { +- // If we have a non-zero initialScrollIndex and run this before we've scrolled, +- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. +- // So let's wait until we've scrolled the view to the right place. And until then, +- // we will trust the initialScrollIndex suggestion. +- +- // Thus, we want to recalculate the windowed render limits if any of the following hold: +- // - initialScrollIndex is undefined or is 0 +- // - initialScrollIndex > 0 AND scrolling is complete +- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case +- // where the list is shorter than the visible area) +- if (props.initialScrollIndex && !this._scrollMetrics.offset && Math.abs(distanceFromEnd) >= Number.EPSILON) { ++ // If we have a pending scroll update, we should not adjust the render window as it ++ // might override the correct window. ++ if (pendingScrollUpdateCount > 0) { + return cellsAroundViewport.last >= getItemCount(data) ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) : cellsAroundViewport; } + newCellsAroundViewport = computeWindowedRenderLimits(props, maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), windowSizeOrDefault(props.windowSize), cellsAroundViewport, this.__getFrameMetricsApprox, this._scrollMetrics); +@@ -902,16 +923,36 @@ class VirtualizedList extends StateSafePureComponent { + } + } + static getDerivedStateFromProps(newProps, prevState) { ++ var _newProps$maintainVis, _newProps$maintainVis2; + // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make + // sure we're rendering a reasonable range here. + var itemCount = newProps.getItemCount(newProps.data); + if (itemCount === prevState.renderMask.numCells()) { + return prevState; + } +- var constrainedCells = VirtualizedList._constrainToItemCount(prevState.cellsAroundViewport, newProps); ++ var maintainVisibleContentPositionAdjustment = null; ++ var prevFirstVisibleItemKey = prevState.firstVisibleItemKey; ++ var minIndexForVisible = (_newProps$maintainVis = (_newProps$maintainVis2 = newProps.maintainVisibleContentPosition) == null ? void 0 : _newProps$maintainVis2.minIndexForVisible) !== null && _newProps$maintainVis !== void 0 ? _newProps$maintainVis : 0; ++ var newFirstVisibleItemKey = newProps.getItemCount(newProps.data) > minIndexForVisible ? VirtualizedList._getItemKey(newProps, minIndexForVisible) : null; ++ if (newProps.maintainVisibleContentPosition != null && prevFirstVisibleItemKey != null && newFirstVisibleItemKey != null) { ++ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { ++ // Fast path if items were added at the start of the list. ++ var hint = itemCount - prevState.renderMask.numCells() + minIndexForVisible; ++ var firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey(newProps, prevFirstVisibleItemKey, hint); ++ maintainVisibleContentPositionAdjustment = firstVisibleItemIndex != null ? firstVisibleItemIndex - minIndexForVisible : null; ++ } else { ++ maintainVisibleContentPositionAdjustment = null; ++ } ++ } ++ var constrainedCells = VirtualizedList._constrainToItemCount(maintainVisibleContentPositionAdjustment != null ? { ++ first: prevState.cellsAroundViewport.first + maintainVisibleContentPositionAdjustment, ++ last: prevState.cellsAroundViewport.last + maintainVisibleContentPositionAdjustment ++ } : prevState.cellsAroundViewport, newProps); + return { + cellsAroundViewport: constrainedCells, +- renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells) ++ renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), ++ firstVisibleItemKey: newFirstVisibleItemKey, ++ pendingScrollUpdateCount: maintainVisibleContentPositionAdjustment != null ? prevState.pendingScrollUpdateCount + 1 : prevState.pendingScrollUpdateCount }; - this._onCellLayout = (e, cellKey, index) => { -@@ -683,7 +695,7 @@ class VirtualizedList extends StateSafePureComponent { - onViewableItemsChanged = _this$props3.onViewableItemsChanged, - viewabilityConfig = _this$props3.viewabilityConfig; - if (onViewableItemsChanged) { -- this._viewabilityTuples.push({ -+ this.pushOrUnshift(this._viewabilityTuples, { - viewabilityHelper: new ViewabilityHelper(viewabilityConfig), - onViewableItemsChanged: onViewableItemsChanged - }); -@@ -937,10 +949,10 @@ class VirtualizedList extends StateSafePureComponent { - var key = _this._keyExtractor(item, ii, _this.props); + } + _pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, first, last, inversionStyle) { +@@ -934,7 +975,7 @@ class VirtualizedList extends StateSafePureComponent { + last = Math.min(end, last); + var _loop = function _loop() { + var item = getItem(data, ii); +- var key = _this._keyExtractor(item, ii, _this.props); ++ var key = VirtualizedList._keyExtractor(item, ii, _this.props); _this._indicesToKeys.set(ii, key); if (stickyIndicesFromProps.has(ii + stickyOffset)) { -- stickyHeaderIndices.push(cells.length); -+ _this.pushOrUnshift(stickyHeaderIndices, cells.length); - } - var shouldListenForLayout = getItemLayout == null || debug || _this._fillRateHelper.enabled(); -- cells.push( /*#__PURE__*/React.createElement(CellRenderer, _extends({ -+ _this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(CellRenderer, _extends({ - CellRendererComponent: CellRendererComponent, - ItemSeparatorComponent: ii < end ? ItemSeparatorComponent : undefined, - ListItemComponent: ListItemComponent, -@@ -1012,14 +1024,14 @@ class VirtualizedList extends StateSafePureComponent { - // 1. Add cell for ListHeaderComponent - if (ListHeaderComponent) { - if (stickyIndicesFromProps.has(0)) { -- stickyHeaderIndices.push(0); -+ this.pushOrUnshift(stickyHeaderIndices, 0); - } - var _element = /*#__PURE__*/React.isValidElement(ListHeaderComponent) ? ListHeaderComponent : - /*#__PURE__*/ - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - React.createElement(ListHeaderComponent, null); -- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { -+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { + stickyHeaderIndices.push(cells.length); +@@ -969,20 +1010,23 @@ class VirtualizedList extends StateSafePureComponent { + } + static _constrainToItemCount(cells, props) { + var itemCount = props.getItemCount(props.data); +- var last = Math.min(itemCount - 1, cells.last); ++ var lastPossibleCellIndex = itemCount - 1; ++ ++ // Constraining `last` may significantly shrink the window. Adjust `first` ++ // to expand the window if the new `last` results in a new window smaller ++ // than the number of cells rendered per batch. + var maxToRenderPerBatch = maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch); ++ var maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); + return { +- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), +- last ++ first: clamp(0, cells.first, maxFirst), ++ last: Math.min(lastPossibleCellIndex, cells.last) + }; + } + _isNestedWithSameOrientation() { + var nestedContext = this.context; + return !!(nestedContext && !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal)); + } +- _keyExtractor(item, index, props +- // $FlowFixMe[missing-local-annot] +- ) { ++ static _keyExtractor(item, index, props) { + if (props.keyExtractor != null) { + return props.keyExtractor(item, index); + } +@@ -1022,7 +1066,12 @@ class VirtualizedList extends StateSafePureComponent { + cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { cellKey: this._getCellKey() + '-header', key: "$header" - }, /*#__PURE__*/React.createElement(View, { -@@ -1038,7 +1050,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - React.createElement(ListEmptyComponent, null); -- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { -+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { - cellKey: this._getCellKey() + '-empty', - key: "$empty" - }, /*#__PURE__*/React.cloneElement(_element2, { -@@ -1077,7 +1089,7 @@ class VirtualizedList extends StateSafePureComponent { - var firstMetrics = this.__getFrameMetricsApprox(section.first, this.props); - var lastMetrics = this.__getFrameMetricsApprox(last, this.props); - var spacerSize = lastMetrics.offset + lastMetrics.length - firstMetrics.offset; -- cells.push( /*#__PURE__*/React.createElement(View, { -+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(View, { - key: "$spacer-" + section.first, - style: { - [spacerKey]: spacerSize -@@ -1100,7 +1112,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - React.createElement(ListFooterComponent, null); -- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { -+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { - cellKey: this._getFooterCellKey(), - key: "$footer" - }, /*#__PURE__*/React.createElement(View, { -@@ -1266,7 +1278,7 @@ class VirtualizedList extends StateSafePureComponent { - * suppresses an error found when Flow v0.68 was deployed. To see the - * error delete this comment and run Flow. */ - if (frame.inLayout) { -- framesInLayout.push(frame); -+ this.pushOrUnshift(framesInLayout, frame); - } +- }, /*#__PURE__*/React.createElement(View, { ++ }, /*#__PURE__*/React.createElement(View ++ // We expect that header component will be a single native view so make it ++ // not collapsable to avoid this view being flattened and make this assumption ++ // no longer true. ++ , { ++ collapsable: false, + onLayout: this._onLayoutHeader, + style: [inversionStyle, this.props.ListHeaderComponentStyle] + }, +@@ -1124,7 +1173,11 @@ class VirtualizedList extends StateSafePureComponent { + // TODO: Android support + invertStickyHeaders: this.props.invertStickyHeaders !== undefined ? this.props.invertStickyHeaders : this.props.inverted, + stickyHeaderIndices, +- style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style ++ style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style, ++ maintainVisibleContentPosition: this.props.maintainVisibleContentPosition != null ? _objectSpread(_objectSpread({}, this.props.maintainVisibleContentPosition), {}, { ++ // Adjust index to account for ListHeaderComponent. ++ minIndexForVisible: this.props.maintainVisibleContentPosition.minIndexForVisible + (this.props.ListHeaderComponent ? 1 : 0) ++ }) : undefined + }); + this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; + var innerRet = /*#__PURE__*/React.createElement(VirtualizedListContextProvider, { +@@ -1307,8 +1360,12 @@ class VirtualizedList extends StateSafePureComponent { + onStartReached = _this$props8.onStartReached, + onStartReachedThreshold = _this$props8.onStartReachedThreshold, + onEndReached = _this$props8.onEndReached, +- onEndReachedThreshold = _this$props8.onEndReachedThreshold, +- initialScrollIndex = _this$props8.initialScrollIndex; ++ onEndReachedThreshold = _this$props8.onEndReachedThreshold; ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the edge reached callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + var _this$_scrollMetrics2 = this._scrollMetrics, + contentLength = _this$_scrollMetrics2.contentLength, + visibleLength = _this$_scrollMetrics2.visibleLength, +@@ -1348,16 +1405,10 @@ class VirtualizedList extends StateSafePureComponent { + // and call onStartReached only once for a given content length, + // and only if onEndReached is not being executed + else if (onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this._scrollMetrics.contentLength !== this._sentStartForContentLength) { +- // On initial mount when using initialScrollIndex the offset will be 0 initially +- // and will trigger an unexpected onStartReached. To avoid this we can use +- // timestamp to differentiate between the initial scroll metrics and when we actually +- // received the first scroll event. +- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { +- this._sentStartForContentLength = this._scrollMetrics.contentLength; +- onStartReached({ +- distanceFromStart +- }); +- } ++ this._sentStartForContentLength = this._scrollMetrics.contentLength; ++ onStartReached({ ++ distanceFromStart ++ }); + } + + // If the user scrolls away from the start or end and back again, +@@ -1412,6 +1463,11 @@ class VirtualizedList extends StateSafePureComponent { } - var windowTop = this.__getFrameMetricsApprox(this.state.cellsAroundViewport.first, this.props).offset; -@@ -1452,6 +1464,12 @@ var styles = StyleSheet.create({ - left: 0, - borderColor: 'red', - borderWidth: 2 -+ }, -+ rowReverse: { -+ flexDirection: 'row-reverse' -+ }, -+ columnReverse: { -+ flexDirection: 'column-reverse' } - }); - export default VirtualizedList; -\ No newline at end of file + _updateViewableItems(props, cellsAroundViewport) { ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the visibility callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.onUpdate(props, this._scrollMetrics.offset, this._scrollMetrics.visibleLength, this._getFrameMetrics, this._createViewToken, tuple.onViewableItemsChanged, cellsAroundViewport); + }); diff --git a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -index c7d68bb..46b3fc9 100644 +index c7d68bb..43f9653 100644 --- a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +++ b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -@@ -167,6 +167,14 @@ function findLastWhere( - class VirtualizedList extends StateSafePureComponent { - static contextType: typeof VirtualizedListContext = VirtualizedListContext; +@@ -75,6 +75,10 @@ type ViewabilityHelperCallbackTuple = { + type State = { + renderMask: CellRenderMask, + cellsAroundViewport: {first: number, last: number}, ++ // Used to track items added at the start of the list for maintainVisibleContentPosition. ++ firstVisibleItemKey: ?string, ++ // When > 0 the scroll position available in JS is considered stale and should not be used. ++ pendingScrollUpdateCount: number, + }; + + /** +@@ -447,9 +451,24 @@ class VirtualizedList extends StateSafePureComponent { + + const initialRenderRegion = VirtualizedList._initialRenderRegion(props); -+ pushOrUnshift(input: Array, item: Item) { -+ if (this.props.inverted) { -+ input.unshift(item) -+ } else { -+ input.push(item) ++ const minIndexForVisible = ++ this.props.maintainVisibleContentPosition?.minIndexForVisible ?? 0; ++ + this.state = { + cellsAroundViewport: initialRenderRegion, + renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion), ++ firstVisibleItemKey: ++ this.props.getItemCount(this.props.data) > minIndexForVisible ++ ? VirtualizedList._getItemKey(this.props, minIndexForVisible) ++ : null, ++ // When we have a non-zero initialScrollIndex, we will receive a ++ // scroll event later so this will prevent the window from updating ++ // until we get a valid offset. ++ pendingScrollUpdateCount: ++ this.props.initialScrollIndex != null && ++ this.props.initialScrollIndex > 0 ++ ? 1 ++ : 0, + }; + + // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. +@@ -534,6 +553,40 @@ class VirtualizedList extends StateSafePureComponent { + } + } + ++ static _findItemIndexWithKey( ++ props: Props, ++ key: string, ++ hint: ?number, ++ ): ?number { ++ const itemCount = props.getItemCount(props.data); ++ if (hint != null && hint >= 0 && hint < itemCount) { ++ const curKey = VirtualizedList._getItemKey(props, hint); ++ if (curKey === key) { ++ return hint; ++ } + } ++ for (let ii = 0; ii < itemCount; ii++) { ++ const curKey = VirtualizedList._getItemKey(props, ii); ++ if (curKey === key) { ++ return ii; ++ } ++ } ++ return null; ++ } ++ ++ static _getItemKey( ++ props: { ++ data: Props['data'], ++ getItem: Props['getItem'], ++ keyExtractor: Props['keyExtractor'], ++ ... ++ }, ++ index: number, ++ ): string { ++ const item = props.getItem(props.data, index); ++ return VirtualizedList._keyExtractor(item, index, props); + } + - // scrollToEnd may be janky without getItemLayout prop - scrollToEnd(params?: ?{animated?: ?boolean, ...}) { - const animated = params ? params.animated : true; -@@ -438,7 +446,7 @@ class VirtualizedList extends StateSafePureComponent { + static _createRenderMask( + props: Props, + cellsAroundViewport: {first: number, last: number}, +@@ -617,6 +670,7 @@ class VirtualizedList extends StateSafePureComponent { + _adjustCellsAroundViewport( + props: Props, + cellsAroundViewport: {first: number, last: number}, ++ pendingScrollUpdateCount: number, + ): {first: number, last: number} { + const {data, getItemCount} = props; + const onEndReachedThreshold = onEndReachedThresholdOrDefault( +@@ -648,21 +702,9 @@ class VirtualizedList extends StateSafePureComponent { + ), + }; } else { - const {onViewableItemsChanged, viewabilityConfig} = this.props; - if (onViewableItemsChanged) { -- this._viewabilityTuples.push({ -+ this.pushOrUnshift(this._viewabilityTuples, { - viewabilityHelper: new ViewabilityHelper(viewabilityConfig), - onViewableItemsChanged: onViewableItemsChanged, - }); -@@ -814,13 +822,13 @@ class VirtualizedList extends StateSafePureComponent { +- // If we have a non-zero initialScrollIndex and run this before we've scrolled, +- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. +- // So let's wait until we've scrolled the view to the right place. And until then, +- // we will trust the initialScrollIndex suggestion. +- +- // Thus, we want to recalculate the windowed render limits if any of the following hold: +- // - initialScrollIndex is undefined or is 0 +- // - initialScrollIndex > 0 AND scrolling is complete +- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case +- // where the list is shorter than the visible area) +- if ( +- props.initialScrollIndex && +- !this._scrollMetrics.offset && +- Math.abs(distanceFromEnd) >= Number.EPSILON +- ) { ++ // If we have a pending scroll update, we should not adjust the render window as it ++ // might override the correct window. ++ if (pendingScrollUpdateCount > 0) { + return cellsAroundViewport.last >= getItemCount(data) + ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) + : cellsAroundViewport; +@@ -771,14 +813,59 @@ class VirtualizedList extends StateSafePureComponent { + return prevState; + } + ++ let maintainVisibleContentPositionAdjustment: ?number = null; ++ const prevFirstVisibleItemKey = prevState.firstVisibleItemKey; ++ const minIndexForVisible = ++ newProps.maintainVisibleContentPosition?.minIndexForVisible ?? 0; ++ const newFirstVisibleItemKey = ++ newProps.getItemCount(newProps.data) > minIndexForVisible ++ ? VirtualizedList._getItemKey(newProps, minIndexForVisible) ++ : null; ++ if ( ++ newProps.maintainVisibleContentPosition != null && ++ prevFirstVisibleItemKey != null && ++ newFirstVisibleItemKey != null ++ ) { ++ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { ++ // Fast path if items were added at the start of the list. ++ const hint = ++ itemCount - prevState.renderMask.numCells() + minIndexForVisible; ++ const firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey( ++ newProps, ++ prevFirstVisibleItemKey, ++ hint, ++ ); ++ maintainVisibleContentPositionAdjustment = ++ firstVisibleItemIndex != null ++ ? firstVisibleItemIndex - minIndexForVisible ++ : null; ++ } else { ++ maintainVisibleContentPositionAdjustment = null; ++ } ++ } ++ + const constrainedCells = VirtualizedList._constrainToItemCount( +- prevState.cellsAroundViewport, ++ maintainVisibleContentPositionAdjustment != null ++ ? { ++ first: ++ prevState.cellsAroundViewport.first + ++ maintainVisibleContentPositionAdjustment, ++ last: ++ prevState.cellsAroundViewport.last + ++ maintainVisibleContentPositionAdjustment, ++ } ++ : prevState.cellsAroundViewport, + newProps, + ); + + return { + cellsAroundViewport: constrainedCells, + renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), ++ firstVisibleItemKey: newFirstVisibleItemKey, ++ pendingScrollUpdateCount: ++ maintainVisibleContentPositionAdjustment != null ++ ? prevState.pendingScrollUpdateCount + 1 ++ : prevState.pendingScrollUpdateCount, + }; + } + +@@ -810,7 +897,7 @@ class VirtualizedList extends StateSafePureComponent { + + for (let ii = first; ii <= last; ii++) { + const item = getItem(data, ii); +- const key = this._keyExtractor(item, ii, this.props); ++ const key = VirtualizedList._keyExtractor(item, ii, this.props); this._indicesToKeys.set(ii, key); if (stickyIndicesFromProps.has(ii + stickyOffset)) { -- stickyHeaderIndices.push(cells.length); -+ this.pushOrUnshift(stickyHeaderIndices, (cells.length)); - } +@@ -853,15 +940,19 @@ class VirtualizedList extends StateSafePureComponent { + props: Props, + ): {first: number, last: number} { + const itemCount = props.getItemCount(props.data); +- const last = Math.min(itemCount - 1, cells.last); ++ const lastPossibleCellIndex = itemCount - 1; - const shouldListenForLayout = - getItemLayout == null || debug || this._fillRateHelper.enabled(); ++ // Constraining `last` may significantly shrink the window. Adjust `first` ++ // to expand the window if the new `last` results in a new window smaller ++ // than the number of cells rendered per batch. + const maxToRenderPerBatch = maxToRenderPerBatchOrDefault( + props.maxToRenderPerBatch, + ); ++ const maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); -- cells.push( -+ this.pushOrUnshift(cells, - { - // 1. Add cell for ListHeaderComponent - if (ListHeaderComponent) { - if (stickyIndicesFromProps.has(0)) { -- stickyHeaderIndices.push(0); -+ this.pushOrUnshift(stickyHeaderIndices, 0); - } - const element = React.isValidElement(ListHeaderComponent) ? ( - ListHeaderComponent -@@ -932,7 +940,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[incompatible-type-arg] - - ); -- cells.push( -+ this.pushOrUnshift(cells, - { + _getSpacerKey = (isVertical: boolean): string => + isVertical ? 'height' : 'width'; + +- _keyExtractor( ++ static _keyExtractor( + item: Item, + index: number, + props: { + keyExtractor?: ?(item: Item, index: number) => string, + ... + }, +- // $FlowFixMe[missing-local-annot] +- ) { ++ ): string { + if (props.keyExtractor != null) { + return props.keyExtractor(item, index); + } +@@ -937,6 +1027,10 @@ class VirtualizedList extends StateSafePureComponent { cellKey={this._getCellKey() + '-header'} key="$header"> -@@ -963,7 +971,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[incompatible-type-arg] - - )): any); -- cells.push( -+ this.pushOrUnshift(cells, - -@@ -1017,7 +1025,7 @@ class VirtualizedList extends StateSafePureComponent { - const lastMetrics = this.__getFrameMetricsApprox(last, this.props); - const spacerSize = - lastMetrics.offset + lastMetrics.length - firstMetrics.offset; -- cells.push( -+ this.pushOrUnshift(cells, - { - // $FlowFixMe[incompatible-type-arg] - - ); -- cells.push( -+ this.pushOrUnshift(cells, - -@@ -1246,6 +1254,12 @@ class VirtualizedList extends StateSafePureComponent { - * LTI update could not be added via codemod */ - _defaultRenderScrollComponent = props => { - const onRefresh = props.onRefresh; -+ const inversionStyle = this.props.inverted -+ ? this.props.horizontal -+ ? styles.rowReverse -+ : styles.columnReverse -+ : null; -+ - if (this._isNestedWithSameOrientation()) { - // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors - return ; -@@ -1273,12 +1287,24 @@ class VirtualizedList extends StateSafePureComponent { - props.refreshControl - ) - } -+ contentContainerStyle={[ -+ inversionStyle, -+ this.props.contentContainerStyle, -+ ]} - /> - ); - } else { - // $FlowFixMe[prop-missing] Invalid prop usage - // $FlowFixMe[incompatible-use] -- return ; -+ return ( -+ -+ ); - } - }; + { + style: inversionStyle + ? [inversionStyle, this.props.style] + : this.props.style, ++ maintainVisibleContentPosition: ++ this.props.maintainVisibleContentPosition != null ++ ? { ++ ...this.props.maintainVisibleContentPosition, ++ // Adjust index to account for ListHeaderComponent. ++ minIndexForVisible: ++ this.props.maintainVisibleContentPosition.minIndexForVisible + ++ (this.props.ListHeaderComponent ? 1 : 0), ++ } ++ : undefined, + }; -@@ -1432,7 +1458,7 @@ class VirtualizedList extends StateSafePureComponent { - * suppresses an error found when Flow v0.68 was deployed. To see the - * error delete this comment and run Flow. */ - if (frame.inLayout) { -- framesInLayout.push(frame); -+ this.pushOrUnshift(framesInLayout, frame); - } + this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; +@@ -1516,8 +1620,12 @@ class VirtualizedList extends StateSafePureComponent { + onStartReachedThreshold, + onEndReached, + onEndReachedThreshold, +- initialScrollIndex, + } = this.props; ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the edge reached callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + const {contentLength, visibleLength, offset} = this._scrollMetrics; + let distanceFromStart = offset; + let distanceFromEnd = contentLength - visibleLength - offset; +@@ -1569,14 +1677,8 @@ class VirtualizedList extends StateSafePureComponent { + isWithinStartThreshold && + this._scrollMetrics.contentLength !== this._sentStartForContentLength + ) { +- // On initial mount when using initialScrollIndex the offset will be 0 initially +- // and will trigger an unexpected onStartReached. To avoid this we can use +- // timestamp to differentiate between the initial scroll metrics and when we actually +- // received the first scroll event. +- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { +- this._sentStartForContentLength = this._scrollMetrics.contentLength; +- onStartReached({distanceFromStart}); +- } ++ this._sentStartForContentLength = this._scrollMetrics.contentLength; ++ onStartReached({distanceFromStart}); } - const windowTop = this.__getFrameMetricsApprox( -@@ -2044,6 +2070,12 @@ const styles = StyleSheet.create({ - borderColor: 'red', - borderWidth: 2, - }, -+ rowReverse: { -+ flexDirection: 'row-reverse', -+ }, -+ columnReverse: { -+ flexDirection: 'column-reverse', -+ }, - }); - export default VirtualizedList; -\ No newline at end of file + // If the user scrolls away from the start or end and back again, +@@ -1703,6 +1805,11 @@ class VirtualizedList extends StateSafePureComponent { + visibleLength, + zoomScale, + }; ++ if (this.state.pendingScrollUpdateCount > 0) { ++ this.setState(state => ({ ++ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1, ++ })); ++ } + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + if (!this.props) { + return; +@@ -1818,6 +1925,7 @@ class VirtualizedList extends StateSafePureComponent { + const cellsAroundViewport = this._adjustCellsAroundViewport( + props, + state.cellsAroundViewport, ++ state.pendingScrollUpdateCount, + ); + const renderMask = VirtualizedList._createRenderMask( + props, +@@ -1848,7 +1956,7 @@ class VirtualizedList extends StateSafePureComponent { + return { + index, + item, +- key: this._keyExtractor(item, index, props), ++ key: VirtualizedList._keyExtractor(item, index, props), + isViewable, + }; + }; +@@ -1909,13 +2017,12 @@ class VirtualizedList extends StateSafePureComponent { + inLayout?: boolean, + ... + } => { +- const {data, getItem, getItemCount, getItemLayout} = props; ++ const {data, getItemCount, getItemLayout} = props; + invariant( + index >= 0 && index < getItemCount(data), + 'Tried to get frame for out of range index ' + index, + ); +- const item = getItem(data, index); +- const frame = this._frames[this._keyExtractor(item, index, props)]; ++ const frame = this._frames[VirtualizedList._getItemKey(props, index)]; + if (!frame || frame.index !== index) { + if (getItemLayout) { + /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment +@@ -1950,11 +2057,8 @@ class VirtualizedList extends StateSafePureComponent { + // where it is. + if ( + focusedCellIndex >= itemCount || +- this._keyExtractor( +- props.getItem(props.data, focusedCellIndex), +- focusedCellIndex, +- props, +- ) !== this._lastFocusedCellKey ++ VirtualizedList._getItemKey(props, focusedCellIndex) !== ++ this._lastFocusedCellKey + ) { + return []; + } +@@ -1995,6 +2099,11 @@ class VirtualizedList extends StateSafePureComponent { + props: FrameMetricProps, + cellsAroundViewport: {first: number, last: number}, + ) { ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the visibility callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.onUpdate( + props, diff --git a/patches/react-native-web+0.19.9+002+fix-mvcp.patch b/patches/react-native-web+0.19.9+002+fix-mvcp.patch deleted file mode 100644 index afd681bba3b0..000000000000 --- a/patches/react-native-web+0.19.9+002+fix-mvcp.patch +++ /dev/null @@ -1,687 +0,0 @@ -diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -index a6fe142..faeb323 100644 ---- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -+++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -@@ -293,7 +293,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[missing-local-annot] - - constructor(_props) { -- var _this$props$updateCel; -+ var _this$props$updateCel, _this$props$maintainV, _this$props$maintainV2; - super(_props); - this._getScrollMetrics = () => { - return this._scrollMetrics; -@@ -532,6 +532,11 @@ class VirtualizedList extends StateSafePureComponent { - visibleLength, - zoomScale - }; -+ if (this.state.pendingScrollUpdateCount > 0) { -+ this.setState(state => ({ -+ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1 -+ })); -+ } - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - if (!this.props) { - return; -@@ -581,7 +586,7 @@ class VirtualizedList extends StateSafePureComponent { - this._updateCellsToRender = () => { - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - this.setState((state, props) => { -- var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport); -+ var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport, state.pendingScrollUpdateCount); - var renderMask = VirtualizedList._createRenderMask(props, cellsAroundViewport, this._getNonViewportRenderRegions(props)); - if (cellsAroundViewport.first === state.cellsAroundViewport.first && cellsAroundViewport.last === state.cellsAroundViewport.last && renderMask.equals(state.renderMask)) { - return null; -@@ -601,7 +606,7 @@ class VirtualizedList extends StateSafePureComponent { - return { - index, - item, -- key: this._keyExtractor(item, index, props), -+ key: VirtualizedList._keyExtractor(item, index, props), - isViewable - }; - }; -@@ -633,12 +638,10 @@ class VirtualizedList extends StateSafePureComponent { - }; - this._getFrameMetrics = (index, props) => { - var data = props.data, -- getItem = props.getItem, - getItemCount = props.getItemCount, - getItemLayout = props.getItemLayout; - invariant(index >= 0 && index < getItemCount(data), 'Tried to get frame for out of range index ' + index); -- var item = getItem(data, index); -- var frame = this._frames[this._keyExtractor(item, index, props)]; -+ var frame = this._frames[VirtualizedList._getItemKey(props, index)]; - if (!frame || frame.index !== index) { - if (getItemLayout) { - /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment -@@ -662,7 +665,7 @@ class VirtualizedList extends StateSafePureComponent { - - // The last cell we rendered may be at a new index. Bail if we don't know - // where it is. -- if (focusedCellIndex >= itemCount || this._keyExtractor(props.getItem(props.data, focusedCellIndex), focusedCellIndex, props) !== this._lastFocusedCellKey) { -+ if (focusedCellIndex >= itemCount || VirtualizedList._getItemKey(props, focusedCellIndex) !== this._lastFocusedCellKey) { - return []; - } - var first = focusedCellIndex; -@@ -702,9 +705,15 @@ class VirtualizedList extends StateSafePureComponent { - } - } - var initialRenderRegion = VirtualizedList._initialRenderRegion(_props); -+ var minIndexForVisible = (_this$props$maintainV = (_this$props$maintainV2 = this.props.maintainVisibleContentPosition) == null ? void 0 : _this$props$maintainV2.minIndexForVisible) !== null && _this$props$maintainV !== void 0 ? _this$props$maintainV : 0; - this.state = { - cellsAroundViewport: initialRenderRegion, -- renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion) -+ renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion), -+ firstVisibleItemKey: this.props.getItemCount(this.props.data) > minIndexForVisible ? VirtualizedList._getItemKey(this.props, minIndexForVisible) : null, -+ // When we have a non-zero initialScrollIndex, we will receive a -+ // scroll event later so this will prevent the window from updating -+ // until we get a valid offset. -+ pendingScrollUpdateCount: this.props.initialScrollIndex != null && this.props.initialScrollIndex > 0 ? 1 : 0 - }; - - // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. -@@ -715,7 +724,7 @@ class VirtualizedList extends StateSafePureComponent { - var clientLength = this.props.horizontal ? ev.target.clientWidth : ev.target.clientHeight; - var isEventTargetScrollable = scrollLength > clientLength; - var delta = this.props.horizontal ? ev.deltaX || ev.wheelDeltaX : ev.deltaY || ev.wheelDeltaY; -- var leftoverDelta = delta; -+ var leftoverDelta = delta * 0.5; - if (isEventTargetScrollable) { - leftoverDelta = delta < 0 ? Math.min(delta + scrollOffset, 0) : Math.max(delta - (scrollLength - clientLength - scrollOffset), 0); - } -@@ -760,6 +769,26 @@ class VirtualizedList extends StateSafePureComponent { - } - } - } -+ static _findItemIndexWithKey(props, key, hint) { -+ var itemCount = props.getItemCount(props.data); -+ if (hint != null && hint >= 0 && hint < itemCount) { -+ var curKey = VirtualizedList._getItemKey(props, hint); -+ if (curKey === key) { -+ return hint; -+ } -+ } -+ for (var ii = 0; ii < itemCount; ii++) { -+ var _curKey = VirtualizedList._getItemKey(props, ii); -+ if (_curKey === key) { -+ return ii; -+ } -+ } -+ return null; -+ } -+ static _getItemKey(props, index) { -+ var item = props.getItem(props.data, index); -+ return VirtualizedList._keyExtractor(item, index, props); -+ } - static _createRenderMask(props, cellsAroundViewport, additionalRegions) { - var itemCount = props.getItemCount(props.data); - invariant(cellsAroundViewport.first >= 0 && cellsAroundViewport.last >= cellsAroundViewport.first - 1 && cellsAroundViewport.last < itemCount, "Invalid cells around viewport \"[" + cellsAroundViewport.first + ", " + cellsAroundViewport.last + "]\" was passed to VirtualizedList._createRenderMask"); -@@ -808,7 +837,7 @@ class VirtualizedList extends StateSafePureComponent { - } - } - } -- _adjustCellsAroundViewport(props, cellsAroundViewport) { -+ _adjustCellsAroundViewport(props, cellsAroundViewport, pendingScrollUpdateCount) { - var data = props.data, - getItemCount = props.getItemCount; - var onEndReachedThreshold = onEndReachedThresholdOrDefault(props.onEndReachedThreshold); -@@ -831,17 +860,9 @@ class VirtualizedList extends StateSafePureComponent { - last: Math.min(cellsAroundViewport.last + renderAhead, getItemCount(data) - 1) - }; - } else { -- // If we have a non-zero initialScrollIndex and run this before we've scrolled, -- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. -- // So let's wait until we've scrolled the view to the right place. And until then, -- // we will trust the initialScrollIndex suggestion. -- -- // Thus, we want to recalculate the windowed render limits if any of the following hold: -- // - initialScrollIndex is undefined or is 0 -- // - initialScrollIndex > 0 AND scrolling is complete -- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case -- // where the list is shorter than the visible area) -- if (props.initialScrollIndex && !this._scrollMetrics.offset && Math.abs(distanceFromEnd) >= Number.EPSILON) { -+ // If we have a pending scroll update, we should not adjust the render window as it -+ // might override the correct window. -+ if (pendingScrollUpdateCount > 0) { - return cellsAroundViewport.last >= getItemCount(data) ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) : cellsAroundViewport; - } - newCellsAroundViewport = computeWindowedRenderLimits(props, maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), windowSizeOrDefault(props.windowSize), cellsAroundViewport, this.__getFrameMetricsApprox, this._scrollMetrics); -@@ -914,16 +935,36 @@ class VirtualizedList extends StateSafePureComponent { - } - } - static getDerivedStateFromProps(newProps, prevState) { -+ var _newProps$maintainVis, _newProps$maintainVis2; - // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make - // sure we're rendering a reasonable range here. - var itemCount = newProps.getItemCount(newProps.data); - if (itemCount === prevState.renderMask.numCells()) { - return prevState; - } -- var constrainedCells = VirtualizedList._constrainToItemCount(prevState.cellsAroundViewport, newProps); -+ var maintainVisibleContentPositionAdjustment = null; -+ var prevFirstVisibleItemKey = prevState.firstVisibleItemKey; -+ var minIndexForVisible = (_newProps$maintainVis = (_newProps$maintainVis2 = newProps.maintainVisibleContentPosition) == null ? void 0 : _newProps$maintainVis2.minIndexForVisible) !== null && _newProps$maintainVis !== void 0 ? _newProps$maintainVis : 0; -+ var newFirstVisibleItemKey = newProps.getItemCount(newProps.data) > minIndexForVisible ? VirtualizedList._getItemKey(newProps, minIndexForVisible) : null; -+ if (newProps.maintainVisibleContentPosition != null && prevFirstVisibleItemKey != null && newFirstVisibleItemKey != null) { -+ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { -+ // Fast path if items were added at the start of the list. -+ var hint = itemCount - prevState.renderMask.numCells() + minIndexForVisible; -+ var firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey(newProps, prevFirstVisibleItemKey, hint); -+ maintainVisibleContentPositionAdjustment = firstVisibleItemIndex != null ? firstVisibleItemIndex - minIndexForVisible : null; -+ } else { -+ maintainVisibleContentPositionAdjustment = null; -+ } -+ } -+ var constrainedCells = VirtualizedList._constrainToItemCount(maintainVisibleContentPositionAdjustment != null ? { -+ first: prevState.cellsAroundViewport.first + maintainVisibleContentPositionAdjustment, -+ last: prevState.cellsAroundViewport.last + maintainVisibleContentPositionAdjustment -+ } : prevState.cellsAroundViewport, newProps); - return { - cellsAroundViewport: constrainedCells, -- renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells) -+ renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), -+ firstVisibleItemKey: newFirstVisibleItemKey, -+ pendingScrollUpdateCount: maintainVisibleContentPositionAdjustment != null ? prevState.pendingScrollUpdateCount + 1 : prevState.pendingScrollUpdateCount - }; - } - _pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, first, last, inversionStyle) { -@@ -946,7 +987,7 @@ class VirtualizedList extends StateSafePureComponent { - last = Math.min(end, last); - var _loop = function _loop() { - var item = getItem(data, ii); -- var key = _this._keyExtractor(item, ii, _this.props); -+ var key = VirtualizedList._keyExtractor(item, ii, _this.props); - _this._indicesToKeys.set(ii, key); - if (stickyIndicesFromProps.has(ii + stickyOffset)) { - _this.pushOrUnshift(stickyHeaderIndices, cells.length); -@@ -981,20 +1022,23 @@ class VirtualizedList extends StateSafePureComponent { - } - static _constrainToItemCount(cells, props) { - var itemCount = props.getItemCount(props.data); -- var last = Math.min(itemCount - 1, cells.last); -+ var lastPossibleCellIndex = itemCount - 1; -+ -+ // Constraining `last` may significantly shrink the window. Adjust `first` -+ // to expand the window if the new `last` results in a new window smaller -+ // than the number of cells rendered per batch. - var maxToRenderPerBatch = maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch); -+ var maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); - return { -- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), -- last -+ first: clamp(0, cells.first, maxFirst), -+ last: Math.min(lastPossibleCellIndex, cells.last) - }; - } - _isNestedWithSameOrientation() { - var nestedContext = this.context; - return !!(nestedContext && !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal)); - } -- _keyExtractor(item, index, props -- // $FlowFixMe[missing-local-annot] -- ) { -+ static _keyExtractor(item, index, props) { - if (props.keyExtractor != null) { - return props.keyExtractor(item, index); - } -@@ -1034,7 +1078,12 @@ class VirtualizedList extends StateSafePureComponent { - this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { - cellKey: this._getCellKey() + '-header', - key: "$header" -- }, /*#__PURE__*/React.createElement(View, { -+ }, /*#__PURE__*/React.createElement(View -+ // We expect that header component will be a single native view so make it -+ // not collapsable to avoid this view being flattened and make this assumption -+ // no longer true. -+ , { -+ collapsable: false, - onLayout: this._onLayoutHeader, - style: [inversionStyle, this.props.ListHeaderComponentStyle] - }, -@@ -1136,7 +1185,11 @@ class VirtualizedList extends StateSafePureComponent { - // TODO: Android support - invertStickyHeaders: this.props.invertStickyHeaders !== undefined ? this.props.invertStickyHeaders : this.props.inverted, - stickyHeaderIndices, -- style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style -+ style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style, -+ maintainVisibleContentPosition: this.props.maintainVisibleContentPosition != null ? _objectSpread(_objectSpread({}, this.props.maintainVisibleContentPosition), {}, { -+ // Adjust index to account for ListHeaderComponent. -+ minIndexForVisible: this.props.maintainVisibleContentPosition.minIndexForVisible + (this.props.ListHeaderComponent ? 1 : 0) -+ }) : undefined - }); - this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; - var innerRet = /*#__PURE__*/React.createElement(VirtualizedListContextProvider, { -@@ -1319,8 +1372,12 @@ class VirtualizedList extends StateSafePureComponent { - onStartReached = _this$props8.onStartReached, - onStartReachedThreshold = _this$props8.onStartReachedThreshold, - onEndReached = _this$props8.onEndReached, -- onEndReachedThreshold = _this$props8.onEndReachedThreshold, -- initialScrollIndex = _this$props8.initialScrollIndex; -+ onEndReachedThreshold = _this$props8.onEndReachedThreshold; -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the edge reached callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - var _this$_scrollMetrics2 = this._scrollMetrics, - contentLength = _this$_scrollMetrics2.contentLength, - visibleLength = _this$_scrollMetrics2.visibleLength, -@@ -1360,16 +1417,10 @@ class VirtualizedList extends StateSafePureComponent { - // and call onStartReached only once for a given content length, - // and only if onEndReached is not being executed - else if (onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this._scrollMetrics.contentLength !== this._sentStartForContentLength) { -- // On initial mount when using initialScrollIndex the offset will be 0 initially -- // and will trigger an unexpected onStartReached. To avoid this we can use -- // timestamp to differentiate between the initial scroll metrics and when we actually -- // received the first scroll event. -- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { -- this._sentStartForContentLength = this._scrollMetrics.contentLength; -- onStartReached({ -- distanceFromStart -- }); -- } -+ this._sentStartForContentLength = this._scrollMetrics.contentLength; -+ onStartReached({ -+ distanceFromStart -+ }); - } - - // If the user scrolls away from the start or end and back again, -@@ -1424,6 +1475,11 @@ class VirtualizedList extends StateSafePureComponent { - } - } - _updateViewableItems(props, cellsAroundViewport) { -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the visibility callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.onUpdate(props, this._scrollMetrics.offset, this._scrollMetrics.visibleLength, this._getFrameMetrics, this._createViewToken, tuple.onViewableItemsChanged, cellsAroundViewport); - }); -diff --git a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -index d896fb1..f303b31 100644 ---- a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -+++ b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -@@ -75,6 +75,10 @@ type ViewabilityHelperCallbackTuple = { - type State = { - renderMask: CellRenderMask, - cellsAroundViewport: {first: number, last: number}, -+ // Used to track items added at the start of the list for maintainVisibleContentPosition. -+ firstVisibleItemKey: ?string, -+ // When > 0 the scroll position available in JS is considered stale and should not be used. -+ pendingScrollUpdateCount: number, - }; - - /** -@@ -455,9 +459,24 @@ class VirtualizedList extends StateSafePureComponent { - - const initialRenderRegion = VirtualizedList._initialRenderRegion(props); - -+ const minIndexForVisible = -+ this.props.maintainVisibleContentPosition?.minIndexForVisible ?? 0; -+ - this.state = { - cellsAroundViewport: initialRenderRegion, - renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion), -+ firstVisibleItemKey: -+ this.props.getItemCount(this.props.data) > minIndexForVisible -+ ? VirtualizedList._getItemKey(this.props, minIndexForVisible) -+ : null, -+ // When we have a non-zero initialScrollIndex, we will receive a -+ // scroll event later so this will prevent the window from updating -+ // until we get a valid offset. -+ pendingScrollUpdateCount: -+ this.props.initialScrollIndex != null && -+ this.props.initialScrollIndex > 0 -+ ? 1 -+ : 0, - }; - - // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. -@@ -470,7 +489,7 @@ class VirtualizedList extends StateSafePureComponent { - const delta = this.props.horizontal - ? ev.deltaX || ev.wheelDeltaX - : ev.deltaY || ev.wheelDeltaY; -- let leftoverDelta = delta; -+ let leftoverDelta = delta * 5; - if (isEventTargetScrollable) { - leftoverDelta = delta < 0 - ? Math.min(delta + scrollOffset, 0) -@@ -542,6 +561,40 @@ class VirtualizedList extends StateSafePureComponent { - } - } - -+ static _findItemIndexWithKey( -+ props: Props, -+ key: string, -+ hint: ?number, -+ ): ?number { -+ const itemCount = props.getItemCount(props.data); -+ if (hint != null && hint >= 0 && hint < itemCount) { -+ const curKey = VirtualizedList._getItemKey(props, hint); -+ if (curKey === key) { -+ return hint; -+ } -+ } -+ for (let ii = 0; ii < itemCount; ii++) { -+ const curKey = VirtualizedList._getItemKey(props, ii); -+ if (curKey === key) { -+ return ii; -+ } -+ } -+ return null; -+ } -+ -+ static _getItemKey( -+ props: { -+ data: Props['data'], -+ getItem: Props['getItem'], -+ keyExtractor: Props['keyExtractor'], -+ ... -+ }, -+ index: number, -+ ): string { -+ const item = props.getItem(props.data, index); -+ return VirtualizedList._keyExtractor(item, index, props); -+ } -+ - static _createRenderMask( - props: Props, - cellsAroundViewport: {first: number, last: number}, -@@ -625,6 +678,7 @@ class VirtualizedList extends StateSafePureComponent { - _adjustCellsAroundViewport( - props: Props, - cellsAroundViewport: {first: number, last: number}, -+ pendingScrollUpdateCount: number, - ): {first: number, last: number} { - const {data, getItemCount} = props; - const onEndReachedThreshold = onEndReachedThresholdOrDefault( -@@ -656,21 +710,9 @@ class VirtualizedList extends StateSafePureComponent { - ), - }; - } else { -- // If we have a non-zero initialScrollIndex and run this before we've scrolled, -- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. -- // So let's wait until we've scrolled the view to the right place. And until then, -- // we will trust the initialScrollIndex suggestion. -- -- // Thus, we want to recalculate the windowed render limits if any of the following hold: -- // - initialScrollIndex is undefined or is 0 -- // - initialScrollIndex > 0 AND scrolling is complete -- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case -- // where the list is shorter than the visible area) -- if ( -- props.initialScrollIndex && -- !this._scrollMetrics.offset && -- Math.abs(distanceFromEnd) >= Number.EPSILON -- ) { -+ // If we have a pending scroll update, we should not adjust the render window as it -+ // might override the correct window. -+ if (pendingScrollUpdateCount > 0) { - return cellsAroundViewport.last >= getItemCount(data) - ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) - : cellsAroundViewport; -@@ -779,14 +821,59 @@ class VirtualizedList extends StateSafePureComponent { - return prevState; - } - -+ let maintainVisibleContentPositionAdjustment: ?number = null; -+ const prevFirstVisibleItemKey = prevState.firstVisibleItemKey; -+ const minIndexForVisible = -+ newProps.maintainVisibleContentPosition?.minIndexForVisible ?? 0; -+ const newFirstVisibleItemKey = -+ newProps.getItemCount(newProps.data) > minIndexForVisible -+ ? VirtualizedList._getItemKey(newProps, minIndexForVisible) -+ : null; -+ if ( -+ newProps.maintainVisibleContentPosition != null && -+ prevFirstVisibleItemKey != null && -+ newFirstVisibleItemKey != null -+ ) { -+ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { -+ // Fast path if items were added at the start of the list. -+ const hint = -+ itemCount - prevState.renderMask.numCells() + minIndexForVisible; -+ const firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey( -+ newProps, -+ prevFirstVisibleItemKey, -+ hint, -+ ); -+ maintainVisibleContentPositionAdjustment = -+ firstVisibleItemIndex != null -+ ? firstVisibleItemIndex - minIndexForVisible -+ : null; -+ } else { -+ maintainVisibleContentPositionAdjustment = null; -+ } -+ } -+ - const constrainedCells = VirtualizedList._constrainToItemCount( -- prevState.cellsAroundViewport, -+ maintainVisibleContentPositionAdjustment != null -+ ? { -+ first: -+ prevState.cellsAroundViewport.first + -+ maintainVisibleContentPositionAdjustment, -+ last: -+ prevState.cellsAroundViewport.last + -+ maintainVisibleContentPositionAdjustment, -+ } -+ : prevState.cellsAroundViewport, - newProps, - ); - - return { - cellsAroundViewport: constrainedCells, - renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), -+ firstVisibleItemKey: newFirstVisibleItemKey, -+ pendingScrollUpdateCount: -+ maintainVisibleContentPositionAdjustment != null -+ ? prevState.pendingScrollUpdateCount + 1 -+ : prevState.pendingScrollUpdateCount, - }; - } - -@@ -818,11 +905,11 @@ class VirtualizedList extends StateSafePureComponent { - - for (let ii = first; ii <= last; ii++) { - const item = getItem(data, ii); -- const key = this._keyExtractor(item, ii, this.props); -+ const key = VirtualizedList._keyExtractor(item, ii, this.props); - - this._indicesToKeys.set(ii, key); - if (stickyIndicesFromProps.has(ii + stickyOffset)) { -- this.pushOrUnshift(stickyHeaderIndices, (cells.length)); -+ this.pushOrUnshift(stickyHeaderIndices, cells.length); - } - - const shouldListenForLayout = -@@ -861,15 +948,19 @@ class VirtualizedList extends StateSafePureComponent { - props: Props, - ): {first: number, last: number} { - const itemCount = props.getItemCount(props.data); -- const last = Math.min(itemCount - 1, cells.last); -+ const lastPossibleCellIndex = itemCount - 1; - -+ // Constraining `last` may significantly shrink the window. Adjust `first` -+ // to expand the window if the new `last` results in a new window smaller -+ // than the number of cells rendered per batch. - const maxToRenderPerBatch = maxToRenderPerBatchOrDefault( - props.maxToRenderPerBatch, - ); -+ const maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); - - return { -- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), -- last, -+ first: clamp(0, cells.first, maxFirst), -+ last: Math.min(lastPossibleCellIndex, cells.last), - }; - } - -@@ -891,15 +982,14 @@ class VirtualizedList extends StateSafePureComponent { - _getSpacerKey = (isVertical: boolean): string => - isVertical ? 'height' : 'width'; - -- _keyExtractor( -+ static _keyExtractor( - item: Item, - index: number, - props: { - keyExtractor?: ?(item: Item, index: number) => string, - ... - }, -- // $FlowFixMe[missing-local-annot] -- ) { -+ ): string { - if (props.keyExtractor != null) { - return props.keyExtractor(item, index); - } -@@ -945,6 +1035,10 @@ class VirtualizedList extends StateSafePureComponent { - cellKey={this._getCellKey() + '-header'} - key="$header"> - { - style: inversionStyle - ? [inversionStyle, this.props.style] - : this.props.style, -+ maintainVisibleContentPosition: -+ this.props.maintainVisibleContentPosition != null -+ ? { -+ ...this.props.maintainVisibleContentPosition, -+ // Adjust index to account for ListHeaderComponent. -+ minIndexForVisible: -+ this.props.maintainVisibleContentPosition.minIndexForVisible + -+ (this.props.ListHeaderComponent ? 1 : 0), -+ } -+ : undefined, - }; - - this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; -@@ -1255,11 +1359,10 @@ class VirtualizedList extends StateSafePureComponent { - _defaultRenderScrollComponent = props => { - const onRefresh = props.onRefresh; - const inversionStyle = this.props.inverted -- ? this.props.horizontal -- ? styles.rowReverse -- : styles.columnReverse -- : null; -- -+ ? this.props.horizontal -+ ? styles.rowReverse -+ : styles.columnReverse -+ : null; - if (this._isNestedWithSameOrientation()) { - // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors - return ; -@@ -1542,8 +1645,12 @@ class VirtualizedList extends StateSafePureComponent { - onStartReachedThreshold, - onEndReached, - onEndReachedThreshold, -- initialScrollIndex, - } = this.props; -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the edge reached callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - const {contentLength, visibleLength, offset} = this._scrollMetrics; - let distanceFromStart = offset; - let distanceFromEnd = contentLength - visibleLength - offset; -@@ -1595,14 +1702,8 @@ class VirtualizedList extends StateSafePureComponent { - isWithinStartThreshold && - this._scrollMetrics.contentLength !== this._sentStartForContentLength - ) { -- // On initial mount when using initialScrollIndex the offset will be 0 initially -- // and will trigger an unexpected onStartReached. To avoid this we can use -- // timestamp to differentiate between the initial scroll metrics and when we actually -- // received the first scroll event. -- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { -- this._sentStartForContentLength = this._scrollMetrics.contentLength; -- onStartReached({distanceFromStart}); -- } -+ this._sentStartForContentLength = this._scrollMetrics.contentLength; -+ onStartReached({distanceFromStart}); - } - - // If the user scrolls away from the start or end and back again, -@@ -1729,6 +1830,11 @@ class VirtualizedList extends StateSafePureComponent { - visibleLength, - zoomScale, - }; -+ if (this.state.pendingScrollUpdateCount > 0) { -+ this.setState(state => ({ -+ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1, -+ })); -+ } - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - if (!this.props) { - return; -@@ -1844,6 +1950,7 @@ class VirtualizedList extends StateSafePureComponent { - const cellsAroundViewport = this._adjustCellsAroundViewport( - props, - state.cellsAroundViewport, -+ state.pendingScrollUpdateCount, - ); - const renderMask = VirtualizedList._createRenderMask( - props, -@@ -1874,7 +1981,7 @@ class VirtualizedList extends StateSafePureComponent { - return { - index, - item, -- key: this._keyExtractor(item, index, props), -+ key: VirtualizedList._keyExtractor(item, index, props), - isViewable, - }; - }; -@@ -1935,13 +2042,12 @@ class VirtualizedList extends StateSafePureComponent { - inLayout?: boolean, - ... - } => { -- const {data, getItem, getItemCount, getItemLayout} = props; -+ const {data, getItemCount, getItemLayout} = props; - invariant( - index >= 0 && index < getItemCount(data), - 'Tried to get frame for out of range index ' + index, - ); -- const item = getItem(data, index); -- const frame = this._frames[this._keyExtractor(item, index, props)]; -+ const frame = this._frames[VirtualizedList._getItemKey(props, index)]; - if (!frame || frame.index !== index) { - if (getItemLayout) { - /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment -@@ -1976,11 +2082,8 @@ class VirtualizedList extends StateSafePureComponent { - // where it is. - if ( - focusedCellIndex >= itemCount || -- this._keyExtractor( -- props.getItem(props.data, focusedCellIndex), -- focusedCellIndex, -- props, -- ) !== this._lastFocusedCellKey -+ VirtualizedList._getItemKey(props, focusedCellIndex) !== -+ this._lastFocusedCellKey - ) { - return []; - } -@@ -2021,6 +2124,11 @@ class VirtualizedList extends StateSafePureComponent { - props: FrameMetricProps, - cellsAroundViewport: {first: number, last: number}, - ) { -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the visibility callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.onUpdate( - props, diff --git a/patches/react-native-web+0.19.9+003+measureInWindow.patch b/patches/react-native-web+0.19.9+002+measureInWindow.patch similarity index 100% rename from patches/react-native-web+0.19.9+003+measureInWindow.patch rename to patches/react-native-web+0.19.9+002+measureInWindow.patch diff --git a/patches/react-native-web+0.19.9+004+fix-pointer-events.patch b/patches/react-native-web+0.19.9+003+fix-pointer-events.patch similarity index 100% rename from patches/react-native-web+0.19.9+004+fix-pointer-events.patch rename to patches/react-native-web+0.19.9+003+fix-pointer-events.patch From 8a04fb11bb73c799133340feb031bce5cad197e8 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 22 Dec 2023 03:00:45 +0700 Subject: [PATCH 064/193] remove redundant slashes in route --- src/libs/Navigation/Navigation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index a3e89a983f98..85e4c0abb1cd 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -117,7 +117,7 @@ function getActiveRoute(): string { */ function isActiveRoute(routePath: Route): boolean { // We remove First forward slash from the URL before matching - return getActiveRoute().substring(1) === routePath; + return getActiveRoute().substring(1) === routePath.replace(/\/{2,}/, '/').replace(/\/$/, ''); } /** From 2cd3a4a9dabd143a155e109ed6fc2a692d17e7c5 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Fri, 22 Dec 2023 01:12:00 +0200 Subject: [PATCH 065/193] Parse welcomeNote before sending to Web --- src/libs/actions/Policy.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index f33e6637e2de..0cf31d8b5666 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -18,6 +18,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ExpensiMark from 'expensify-common/lib/ExpensiMark'; const allPolicies = {}; Onyx.connect({ @@ -601,10 +602,7 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs, welcomeNote, policyID) const params = { employees: JSON.stringify(_.map(logins, (login) => ({email: login}))), - - // Do not escape HTML special chars for welcomeNote as this will be handled in the backend. - // See https://github.com/Expensify/App/issues/20081 for more details. - welcomeNote, + welcomeNote: new ExpensiMark().replace(welcomeNote), policyID, }; if (!_.isEmpty(membersChats.reportCreationData)) { From 3be2115c18707b373817d25608e7a307320ce921 Mon Sep 17 00:00:00 2001 From: Cristi Paval Date: Fri, 22 Dec 2023 01:25:59 +0200 Subject: [PATCH 066/193] Run prettier --- src/libs/actions/Policy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 0cf31d8b5666..da5580c19bfd 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -1,4 +1,5 @@ import {PUBLIC_DOMAINS} from 'expensify-common/lib/CONST'; +import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; import {escapeRegExp} from 'lodash'; import filter from 'lodash/filter'; @@ -18,7 +19,6 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ExpensiMark from 'expensify-common/lib/ExpensiMark'; const allPolicies = {}; Onyx.connect({ From bb85de6f3bb9f70d8428a66fcb4a920eb609dad5 Mon Sep 17 00:00:00 2001 From: dukenv0307 <129500732+dukenv0307@users.noreply.github.com> Date: Fri, 22 Dec 2023 14:29:11 +0700 Subject: [PATCH 067/193] Update src/libs/ReportUtils.ts Co-authored-by: Eugene Voloshchak --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index fa8add8f666f..ab60cd650545 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4315,7 +4315,7 @@ function navigateToPrivateNotes(report: Report, session: Session) { } /** - * Check whether should display thread reply + * Checks if thread replies should be displayed */ function shouldDisplayThreadReplies(reportAction: ReportAction, reportID: string): boolean { const hasReplies = (reportAction.childVisibleActionCount ?? 0) > 0; From e7e7762b6faa94896b9b4028369ebb417d2f5b20 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 22 Dec 2023 15:02:38 +0700 Subject: [PATCH 068/193] global regex to replace all rendundant slashes --- src/libs/Navigation/Navigation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 85e4c0abb1cd..657ee68b1206 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -117,7 +117,7 @@ function getActiveRoute(): string { */ function isActiveRoute(routePath: Route): boolean { // We remove First forward slash from the URL before matching - return getActiveRoute().substring(1) === routePath.replace(/\/{2,}/, '/').replace(/\/$/, ''); + return getActiveRoute().substring(1) === routePath.replace(/\/{2,}/g, '/').replace(/\/$/, ''); } /** From bdedd0b45b8056bcd4adfbf291833a9eafd62ec4 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 22 Dec 2023 16:06:18 +0700 Subject: [PATCH 069/193] refactor code --- src/pages/home/report/ReportActionsList.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 9e573723619d..c9d60e607ba9 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -144,8 +144,6 @@ function ReportActionsList({ const route = useRoute(); const opacity = useSharedValue(0); const userActiveSince = useRef(null); - const unreadActionSubscription = useRef(null); - const readNewestActionSubscription = useRef(null); const markerInit = () => { if (!cacheUnreadMarkers.has(report.reportID)) { @@ -231,22 +229,20 @@ function ReportActionsList({ }; // Listen to specific reportID for unread event and set the marker to new message - unreadActionSubscription.current = DeviceEventEmitter.addListener(`unreadAction_${report.reportID}`, (newLastReadTime) => { + const unreadActionSubscription = DeviceEventEmitter.addListener(`unreadAction_${report.reportID}`, (newLastReadTime) => { resetUnreadMarker(newLastReadTime); setMessageManuallyMarkedUnread(new Date().getTime()); }); // Listen to specific reportID for read newest action event and reset the marker - readNewestActionSubscription.current = DeviceEventEmitter.addListener(`readNewestAction_${report.reportID}`, (newLastReadTime) => { + const readNewestActionSubscription = DeviceEventEmitter.addListener(`readNewestAction_${report.reportID}`, (newLastReadTime) => { resetUnreadMarker(newLastReadTime); setMessageManuallyMarkedUnread(0); }); return () => { - // If the reportID changes, we reset the userActiveSince to null, we need to do it because - // this component doesn't unmount when the reportID changes - unreadActionSubscription.current.remove(); - readNewestActionSubscription.current.remove(); + unreadActionSubscription.remove(); + readNewestActionSubscription.remove(); }; }, [report.reportID]); From f46f2b724a2ecf2b16c7135f67614615eaeed016 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Fri, 22 Dec 2023 17:59:31 +0530 Subject: [PATCH 070/193] added requested changes --- src/components/DatePicker/CalendarPicker/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/DatePicker/CalendarPicker/index.js b/src/components/DatePicker/CalendarPicker/index.js index a4aedcf9a6be..7df7d81b961d 100644 --- a/src/components/DatePicker/CalendarPicker/index.js +++ b/src/components/DatePicker/CalendarPicker/index.js @@ -124,7 +124,6 @@ class CalendarPicker extends React.PureComponent { } return { - ...prev, currentDateView: prevMonth, years: newYears, }; @@ -147,7 +146,6 @@ class CalendarPicker extends React.PureComponent { } return { - ...prev, currentDateView: nextMonth, years: newYears, }; From 760eba2344ff7dd1222bb295e5070127de9bb850 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Fri, 22 Dec 2023 11:47:18 -0500 Subject: [PATCH 071/193] Inverted fix --- src/components/FlatList/MVCPFlatList.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js index e8bc88b70e16..0abb1dc4a873 100644 --- a/src/components/FlatList/MVCPFlatList.js +++ b/src/components/FlatList/MVCPFlatList.js @@ -31,7 +31,7 @@ function useMergeRefs(...args) { ); } -const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizontal, inverted, onScroll, ...props}, forwardedRef) => { +const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizontal, onScroll, ...props}, forwardedRef) => { const {minIndexForVisible: mvcpMinIndexForVisible, autoscrollToTopThreshold: mvcpAutoscrollToTopThreshold} = maintainVisibleContentPosition ?? {}; const scrollRef = React.useRef(null); const prevFirstVisibleOffsetRef = React.useRef(null); @@ -70,7 +70,7 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont const contentViewLength = contentView.childNodes.length; for (let i = mvcpMinIndexForVisible; i < contentViewLength; i++) { - const subview = contentView.childNodes[inverted ? contentViewLength - i - 1 : i]; + const subview = contentView.childNodes[i]; const subviewOffset = horizontal ? subview.offsetLeft : subview.offsetTop; if (subviewOffset > scrollOffset || i === contentViewLength - 1) { prevFirstVisibleOffsetRef.current = subviewOffset; @@ -78,7 +78,7 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont break; } } - }, [getContentView, getScrollOffset, mvcpMinIndexForVisible, horizontal, inverted]); + }, [getContentView, getScrollOffset, mvcpMinIndexForVisible, horizontal]); const adjustForMaintainVisibleContentPosition = React.useCallback(() => { if (mvcpMinIndexForVisible == null) { @@ -183,7 +183,6 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont {...props} maintainVisibleContentPosition={maintainVisibleContentPosition} horizontal={horizontal} - inverted={inverted} onScroll={onScrollInternal} scrollEventThrottle={1} ref={onRef} From a2fd4f81dc00e37922f36146a4e52f09abd2019e Mon Sep 17 00:00:00 2001 From: Yauheni Date: Fri, 22 Dec 2023 22:30:17 +0300 Subject: [PATCH 072/193] Fix bug with Send money text displayed in workspace chat --- src/components/ReportWelcomeText.js | 6 ++---- src/languages/en.ts | 8 +++++++- src/languages/es.ts | 8 +++++++- src/languages/types.ts | 5 +++++ 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/components/ReportWelcomeText.js b/src/components/ReportWelcomeText.js index a204d0c59aaf..868dde4693da 100644 --- a/src/components/ReportWelcomeText.js +++ b/src/components/ReportWelcomeText.js @@ -11,7 +11,6 @@ import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import reportPropTypes from '@pages/reportPropTypes'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import Text from './Text'; @@ -71,6 +70,7 @@ function ReportWelcomeText(props) { const isUserPolicyAdmin = PolicyUtils.isPolicyAdmin(props.policy); const roomWelcomeMessage = ReportUtils.getRoomWelcomeMessage(props.report, isUserPolicyAdmin); const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(props.report, participantAccountIDs); + const additionalText = _.map(moneyRequestOptions, (item) => props.translate(`reportActionsView.iouTypes.${item}`)).join(', '); return ( <> @@ -130,9 +130,7 @@ function ReportWelcomeText(props) { ))} )} - {(moneyRequestOptions.includes(CONST.IOU.TYPE.SEND) || moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)) && ( - {props.translate('reportActionsView.usePlusButton')} - )} + {!!additionalText && {props.translate('reportActionsView.usePlusButton', {additionalText})}} ); diff --git a/src/languages/en.ts b/src/languages/en.ts index 71d27e341cac..ab1a1aad2fa8 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -73,6 +73,7 @@ import type { UntilTimeParams, UpdatedTheDistanceParams, UpdatedTheRequestParams, + UsePlusButtonParams, UserIsAlreadyMemberParams, WaitingOnBankAccountParams, WalletProgramParams, @@ -465,7 +466,12 @@ export default { chatWithAccountManager: 'Chat with your account manager here', sayHello: 'Say hello!', welcomeToRoom: ({roomName}: WelcomeToRoomParams) => `Welcome to ${roomName}!`, - usePlusButton: '\n\nYou can also use the + button to send money, request money, or assign a task!', + usePlusButton: ({additionalText}: UsePlusButtonParams) => `\n\nYou can also use the + button to ${additionalText}, or assign a task!`, + iouTypes: { + send: 'send money', + split: 'split bill', + request: 'request money', + }, }, reportAction: { asCopilot: 'as copilot for', diff --git a/src/languages/es.ts b/src/languages/es.ts index 85223c559f81..02f0505be1e0 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -72,6 +72,7 @@ import type { UntilTimeParams, UpdatedTheDistanceParams, UpdatedTheRequestParams, + UsePlusButtonParams, UserIsAlreadyMemberParams, WaitingOnBankAccountParams, WalletProgramParams, @@ -458,7 +459,12 @@ export default { chatWithAccountManager: 'Chatea con tu gestor de cuenta aquí', sayHello: '¡Saluda!', welcomeToRoom: ({roomName}: WelcomeToRoomParams) => `¡Bienvenido a ${roomName}!`, - usePlusButton: '\n\n¡También puedes usar el botón + de abajo para enviar dinero, pedir dinero, o asignar una tarea!', + usePlusButton: ({additionalText}: UsePlusButtonParams) => `\n\n¡También puedes usar el botón + de abajo para ${additionalText}, o asignar una tarea!`, + iouTypes: { + send: 'enviar dinero', + split: 'dividir factura', + request: 'redir dinero', + }, }, reportAction: { asCopilot: 'como copiloto de', diff --git a/src/languages/types.ts b/src/languages/types.ts index 8e72c700a9cc..6e5021421e06 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -74,6 +74,10 @@ type WelcomeToRoomParams = { roomName: string; }; +type UsePlusButtonParams = { + additionalText: string; +}; + type ReportArchiveReasonsClosedParams = { displayName: string; }; @@ -333,4 +337,5 @@ export type { UpdatedTheDistanceParams, WalletProgramParams, TaskCreatedActionParams, + UsePlusButtonParams, }; From 072c07d6bb2c7276c9be16c613f772601e384779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Fri, 22 Dec 2023 17:42:59 -0600 Subject: [PATCH 073/193] Use the global reportID as optimistic reportID for the TU room --- src/libs/actions/TeachersUnite.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/TeachersUnite.js b/src/libs/actions/TeachersUnite.js index 8bcd0ea1e42f..8bde0c657771 100644 --- a/src/libs/actions/TeachersUnite.js +++ b/src/libs/actions/TeachersUnite.js @@ -29,17 +29,17 @@ Onyx.connect({ * @param {String} firstName * @param {String} lastName * @param {String} policyID - * @param {String} publicRoomReportID + * @param {String} publicRoomReportID - This is the global reportID for the public room, we'll ignore the optimistic one */ function referTeachersUniteVolunteer(partnerUserID, firstName, lastName, policyID, publicRoomReportID) { const optimisticPublicRoom = ReportUtils.buildOptimisticChatReport([], CONST.TEACHERS_UNITE.PUBLIC_ROOM_NAME, CONST.REPORT.CHAT_TYPE.POLICY_ROOM, policyID); const optimisticData = [ { onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticPublicRoom.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${publicRoomReportID}`, value: { ...optimisticPublicRoom, - reportID: optimisticPublicRoom.reportID, + reportID: publicRoomReportID, policyName: CONST.TEACHERS_UNITE.POLICY_NAME, }, }, @@ -47,7 +47,7 @@ function referTeachersUniteVolunteer(partnerUserID, firstName, lastName, policyI API.write( 'ReferTeachersUniteVolunteer', { - publicRoomReportID: optimisticPublicRoom.reportID, + publicRoomReportID, firstName, lastName, partnerUserID, From 6da2729869de1705f62a7591b87aa9f6a54d4d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Fri, 22 Dec 2023 18:55:16 -0600 Subject: [PATCH 074/193] Change param to reportID --- src/libs/actions/TeachersUnite.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/TeachersUnite.js b/src/libs/actions/TeachersUnite.js index 8bde0c657771..ef4fc27b964e 100644 --- a/src/libs/actions/TeachersUnite.js +++ b/src/libs/actions/TeachersUnite.js @@ -47,7 +47,7 @@ function referTeachersUniteVolunteer(partnerUserID, firstName, lastName, policyI API.write( 'ReferTeachersUniteVolunteer', { - publicRoomReportID, + reportID: publicRoomReportID, firstName, lastName, partnerUserID, From 6513b91ce95c86fee8945bfec6aad7e3c015d996 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Sat, 23 Dec 2023 06:51:53 +0530 Subject: [PATCH 075/193] use collapsible FAQ in all pages --- .../account-settings/Account-Details.md | 4 +++- .../account-settings/Close-Account.md | 4 +++- .../account-settings/Copilot.md | 4 +++- .../account-settings/Merge-Accounts.md | 4 +++- .../International-Reimbursements.md | 3 ++- .../Personal-Credit-Cards.md | 4 +++- .../Business-Bank-Accounts-AUD.md | 2 +- .../Business-Bank-Accounts-USD.md | 4 +++- .../company-cards/CSV-Import.md | 4 +++- .../company-cards/Commercial-Card-Feeds.md | 4 +++- .../company-cards/Company-Card-Settings.md | 4 +++- .../company-cards/Connect-ANZ.md | 4 +++- .../company-cards/Direct-Bank-Connections.md | 4 +++- .../company-cards/Reconciliation.md | 4 +++- .../deposit-accounts/Deposit-Accounts-USD.md | 4 +++- .../Billing-Overview.md | 4 +++- .../Billing-Owner.md | 4 +++- .../Change-Plan-Or-Subscription.md | 4 +++- .../Consolidated-Domain-Billing.md | 4 +++- .../billing-and-subscriptions/Free-Trial.md | 4 +++- .../Individual-Subscription.md | 4 +++- .../Pay-Per-Use-Subscription.md | 4 ++-- .../billing-and-subscriptions/Tax-Exempt.md | 4 +++- .../Attendee-Tracking.md | 3 ++- .../expense-and-report-features/Currency.md | 5 ++--- .../Expense-Rules.md | 4 ++-- .../Expense-Types.md | 4 +++- .../Report-Audit-Log-and-Comments.md | 4 +++- .../The-Expenses-Page.md | 3 ++- .../The-Reports-Page.md | 3 ++- .../Admin-Card-Settings-and-Features.md | 4 +++- .../expensify-card/Auto-Reconciliation.md | 4 +++- .../Cardholder-Settings-and-Features.md | 4 +++- .../expensify-card/Dispute-A-Transaction.md | 3 ++- .../expensify-card/Request-the-Card.md | 4 +++- .../Set-Up-the-Card-for-Your-Company.md | 4 +++- .../Partner-Billing-Guide.md | 4 +++- .../get-paid-back/Per-Diem-Expenses.md | 4 +++- .../get-paid-back/Referral-Program.md | 4 +++- .../expensify-classic/get-paid-back/Trips.md | 4 +++- .../get-paid-back/expenses/Apply-Tax.md | 3 ++- .../get-paid-back/expenses/Merge-Expenses.md | 2 +- .../get-paid-back/expenses/Upload-Receipts.md | 4 +++- .../get-paid-back/reports/Create-A-Report.md | 4 +++- .../getting-started/Invite-Members.md | 4 +++- .../getting-started/Plan-Types.md | 4 +++- ...e-Share-For-Expensify-Approved-Partners.md | 2 +- .../Your-Expensify-Partner-Manager.md | 6 ++++-- .../support/Expensify-Support.md | 4 +++- .../Default-Export-Templates.md | 3 ++- .../insights-and-custom-reporting/Insights.md | 6 ++++-- .../Other-Export-Options.md | 4 +++- .../integrations/HR-integrations/ADP.md | 4 +++- .../integrations/HR-integrations/Gusto.md | 4 +++- .../accounting-integrations/Certinia.md | 4 +++- .../Indirect-Accounting-Integrations.md | 4 +++- .../accounting-integrations/NetSuite.md | 4 +++- .../QuickBooks-Desktop.md | 4 +++- .../QuickBooks-Online.md | 4 +++- .../accounting-integrations/Sage-Intacct.md | 4 +++- .../accounting-integrations/Xero.md | 4 +++- .../Additional-Travel-Integrations.md | 4 +++- .../integrations/travel-integrations/Navan.md | 4 +++- .../Removing-Members.md | 4 +++- .../Vacation-Delegate.md | 5 ++--- .../send-payments/Pay-Bills.md | 21 +++++++++++-------- .../send-payments/Reimbursing-Reports.md | 3 ++- .../send-payments/Third-Party-Payments.md | 4 +++- .../workspace-and-domain-settings/Budgets.md | 3 ++- .../Categories.md | 3 ++- .../Domains-Overview.md | 3 ++- .../workspace-and-domain-settings/Expenses.md | 3 ++- .../workspace-and-domain-settings/Per-Diem.md | 3 ++- .../Reimbursement.md | 3 ++- .../workspace-and-domain-settings/SAML-SSO.md | 3 ++- .../workspace-and-domain-settings/Tags.md | 4 ++-- .../bank-accounts/Connect-a-Bank-Account.md | 3 ++- .../billing-and-plan-types/The-Free-Plan.md | 4 +++- .../chat/Introducing-Expensify-Chat.md | 3 ++- .../get-paid-back/Distance-Requests.md | 3 ++- .../get-paid-back/Referral-Program.md | 3 ++- .../get-paid-back/Request-Money.md | 3 ++- .../Domain-Settings-Overview.md | 4 +++- 83 files changed, 229 insertions(+), 100 deletions(-) diff --git a/docs/articles/expensify-classic/account-settings/Account-Details.md b/docs/articles/expensify-classic/account-settings/Account-Details.md index bc4b94bf8a51..535e74eeb701 100644 --- a/docs/articles/expensify-classic/account-settings/Account-Details.md +++ b/docs/articles/expensify-classic/account-settings/Account-Details.md @@ -60,10 +60,12 @@ Is your Secondary Login (personal email) invalidated in your company account? If 4. Head to your personal email account and follow the prompts 5. You'll receive a link in the email to click that will unlink the two accounts -# FAQ +{% include faq-begin.md %} ## The profile picture on my account updated automatically. Why did this happen? Our focus is always on making your experience user-friendly and saving you valuable time. One of the ways we achieve this is by utilizing a public API to retrieve public data linked to your email address. This tool searches for public accounts or profiles associated with your email address, such as on LinkedIn. When it identifies one, it pulls in the uploaded profile picture and name to Expensify. While this automated process is generally accurate, there may be instances where it's not entirely correct. If this happens, we apologize for any inconvenience caused. The good news is that rectifying such situations is a straightforward process. You can quickly update your information manually by following the directions provided above, ensuring your data is accurate and up to date in no time. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/account-settings/Close-Account.md b/docs/articles/expensify-classic/account-settings/Close-Account.md index c25c22de9704..9b1e886fc94a 100644 --- a/docs/articles/expensify-classic/account-settings/Close-Account.md +++ b/docs/articles/expensify-classic/account-settings/Close-Account.md @@ -114,10 +114,12 @@ Here's how to do it: By following these steps, you can easily verify your email or phone number and close an unwanted Expensify account. -# FAQ +{% include faq-begin.md %} ## What should I do if I'm not directed to my account when clicking the validate option from my phone or email? It's possible your browser has blocked this, either because of some existing cache or extension. In this case, you should follow the Reset Password flow to reset the password and manually gain access with the new password, along with your email address. ## Why don't I see the Close Account option? It's possible your account is on a managed company domain. In this case, only the admins from that company can close it. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/account-settings/Copilot.md b/docs/articles/expensify-classic/account-settings/Copilot.md index 4fac402b7ced..31bc0eff60e6 100644 --- a/docs/articles/expensify-classic/account-settings/Copilot.md +++ b/docs/articles/expensify-classic/account-settings/Copilot.md @@ -59,7 +59,7 @@ To ensure a receipt is routed to the Expensify account in which you are a copilo 3. Send -# FAQ +{% include faq-begin.md %} ## Can a Copilot's Secondary Login be used to forward receipts? Yes! A Copilot can use any of the email addresses tied to their account to forward receipts into the account of the person they're assisting. @@ -67,3 +67,5 @@ Yes! A Copilot can use any of the email addresses tied to their account to forwa No, only the original account holder can add another Copilot to the account. ## Is there a restriction on the number of Copilots I can have or the number of users for whom I can act as a Copilot? There is no limit! You can have as many Copilots as you like, and you can be a Copilot for as many users as you need. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/account-settings/Merge-Accounts.md b/docs/articles/expensify-classic/account-settings/Merge-Accounts.md index abb218c74118..34bf422aa983 100644 --- a/docs/articles/expensify-classic/account-settings/Merge-Accounts.md +++ b/docs/articles/expensify-classic/account-settings/Merge-Accounts.md @@ -19,7 +19,7 @@ Merging two accounts together is fairly straightforward. Let’s go over how to 8. Paste the code into the required field If you have any questions about this process, feel free to reach out to Concierge for some assistance! -# FAQ +{% include faq-begin.md %} ## Can you merge accounts from the mobile app? No, accounts can only be merged from the full website at expensify.com. ## Can I administratively merge two accounts together? @@ -34,3 +34,5 @@ Yes! Please see below: - If you have two accounts with two different verified domains, you cannot merge them together. ## What happens to my “personal” Individual workspace when merging accounts? The old “personal” Individual workspace is deleted. If you plan to submit reports under a different workspace in the future, ensure that any reports on the Individual workspace in the old account are marked as Open before merging the accounts. You can typically do this by selecting “Undo Submit” on any submitted reports. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/International-Reimbursements.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/International-Reimbursements.md index 7313c73ac6e6..5a5827149a4f 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/International-Reimbursements.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/International-Reimbursements.md @@ -75,7 +75,7 @@ Examples of additional requested information: - An authorization letter - An independently certified documentation such as shareholder agreement from a lawyer, notary, or public accountant if an individual owns more than 25% of the company -# FAQ +{% include faq-begin.md %} ## How many people can send reimbursements internationally? @@ -103,3 +103,4 @@ This is the person who will process international reimbursements. The authorized You can leave this form section blank since the “User” is Expensify. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Credit-Cards.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Credit-Cards.md index c41178b4aa7f..05149ebf868e 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Credit-Cards.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Credit-Cards.md @@ -59,7 +59,7 @@ Expenses can be imported as either reimbursable or non-reimbursable. Select the *Remove a card*: If you need to remove a card, you can select the red trash can icon. Please remember this will remove all unreported and un-submitted transactions from your account that are tied to this card, so be careful! -# FAQ +{% include faq-begin.md %} *Is the bank/credit card import option right for me?* If you incur expenses using your personal or business card and need to get them accounted for in your company’s accounting, then you might want to import your bank/credit card. Please note, if you have a company-assigned corporate card, check with your company's Expensify admin on how to handle these cards. Often, admins will take care of card assignments, and you won't need to import them yourself. @@ -74,3 +74,5 @@ If you aren't able to see the expenses imported when you click “View expenses *How do I remove an imported spreadsheet?* If you need to remove an imported spreadsheet, you can select the red trash can icon. Please remember this will remove all unreported and unsubmitted transactions from your account that are tied to this import, so be careful! + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-AUD.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-AUD.md index b59f68a65ce6..8c5ead911da4 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-AUD.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-AUD.md @@ -36,7 +36,7 @@ You can complete this process either via the web app (on a computer), or via the If you are new to using Batch Payments in Australia, to reimburse your staff or process payroll, you may want to check out these bank-specific instructions for how to upload your .aba file: - ANZ Bank - [Import a file for payroll payments](https://www.anz.com.au/support/internet-banking/pay-transfer-business/payroll/import-file/) -- CommBank - [Importing and using
 Direct Entry (EFT) files](https://www.commbank.com.au/business/pds/003-279-importing-a-de-file.pdf) +- CommBank - [Importing and using Direct Entry (EFT) files](https://www.commbank.com.au/business/pds/003-279-importing-a-de-file.pdf) - Westpac - [Importing Payment Files](https://www.westpac.com.au/business-banking/online-banking/support-faqs/import-files/) - NAB - [Quick Reference Guide - Upload a payment file](https://www.nab.com.au/business/online-banking/nab-connect/help) - Bendigo Bank - [Bulk payments user guide](https://www.bendigobank.com.au/globalassets/documents/business/bulk-payments-user-guide.pdf) diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-USD.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-USD.md index 2fbdac02e85c..4ae2c669561f 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-USD.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-USD.md @@ -127,7 +127,7 @@ If you get a generic error message that indicates, "Something's gone wrong", ple 8. If you have another phone available, try to follow these steps on that device If the issue persists, please contact your Account Manager or Concierge for further troubleshooting assistance. -# FAQ +{% include faq-begin.md %} ## What is a Beneficial Owner? A Beneficial Owner refers to an **individual** who owns 25% or more of the business. If no individual owns 25% or more of the business, the company does not have a Beneficial Owner. @@ -157,3 +157,5 @@ It's a good idea to wait till the end of that second business day. If you still Make sure to reach out to your Account Manager or to Concierge once you have done so and our team will be able to re-trigger those 3 transactions! + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/CSV-Import.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/CSV-Import.md index fc1e83701caf..fd50c245d568 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/CSV-Import.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/CSV-Import.md @@ -93,9 +93,11 @@ Then, try to upload the revised spreadsheet again: 3. Check the row count again on the Output Preview to confirm it matches the spreadsheet 4. Click **Submit Spreadsheet** -# FAQ +{% include faq-begin.md %} ## Why can't I see my CSV transactions immediately after uploading them? Don't worry! You'll typically need to wait 1-2 minutes after clicking **I understand, I'll wait!** ## I'm trying to import a credit. Why isn't it uploading? Negative expenses shouldn't include a minus sign. Instead, they should just be wrapped in parentheses. For example, to indicate "-335.98," you'll want to make sure it's formatted as "(335.98)." + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Commercial-Card-Feeds.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Commercial-Card-Feeds.md index 741def35581e..ab426aa616cc 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Commercial-Card-Feeds.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Commercial-Card-Feeds.md @@ -91,7 +91,7 @@ To completely remove the card connection, unassign every card from the list and Note: If expenses are Processing and then rejected, they will also be deleted when they're returned to an Open state as the card they're linked to no longer exists. -# FAQ +{% include faq-begin.md %} ## My Commercial Card feed is set up. Why is a specific card not coming up when I try to assign it to an employee? Cards will appear in the drop-down when activated and have at least one posted transaction. If the card is activated and has been used for a while and you're still not seeing it, please reach out to your Account Manager or message concierge@expensify.com for further assistance. @@ -116,3 +116,5 @@ If your company uses a Commercial Card program that isn’t with one of our Appr - Stripe - Brex + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Company-Card-Settings.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Company-Card-Settings.md index fa5879d85ea8..bc9801060223 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Company-Card-Settings.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Company-Card-Settings.md @@ -84,8 +84,10 @@ Expensify eReceipts serve as digital substitutes for paper receipts in your purc To ensure seamless automatic importation, it's essential to maintain your transactions in US Dollars. Additionally, eReceipts can be directly imported from your bank account. Please be aware that CSV/OFX imported files of bank transactions do not support eReceipts. It's important to note that eReceipts are not generated for lodging expenses. Moreover, due to incomplete or inaccurate category information from certain banks, there may be instances of invalid eReceipts being generated for hotel purchases. If you choose to re-categorize expenses, a similar situation may arise. It's crucial to remember that our Expensify eReceipt Guarantee excludes coverage for hotel and motel expenses. -# FAQ +{% include faq-begin.md %} ## What plan/subscription is required in order to manage corporate cards? Group Policy (Collect or Control plan only) ## When do my company card transactions import to Expensify? Credit card transactions are imported to Expensify once they’re posted to the bank account. This usually takes 1-3 business days between the point of purchase and when the transactions populate in your account. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Connect-ANZ.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Connect-ANZ.md index 59104ce36a41..9844622f8539 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Connect-ANZ.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Connect-ANZ.md @@ -24,7 +24,7 @@ Importing your ANZ Visa into Expensify will allow your card transactions to flow 4. Once you’ve filled out and submitted your Internet Banking data authority form or ANZ Direct Online authority form, ANZ will set up the feed and send all the details directly to Expensify. 5. Then, we’ll add the card feed to your Expensify account and send you a message to let you know that it has been set up. We'll also include some webinar training resources to ensure you have all the information you need! -# FAQ +{% include faq-begin.md %} ## Are there discounts available for ANZ customers? As ANZ’s preferred receipt tracking and expense management partner, Expensify offers ANZ business customers a few special perks: @@ -44,3 +44,5 @@ After the free trial, you’ll get preferred pricing at 50% off the current rate ## Do I need to sign up for a specific period in order to receive the discount? There is no obligation to sign up for a certain period to receive the discount. After your free trial, the 50% discount for the first 12 months, will be applied automatically to your account. After the initial 12 months, the 15% discount will also be applied automatically. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Direct-Bank-Connections.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Direct-Bank-Connections.md index 372edd8f14ec..c9720177a8fc 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Direct-Bank-Connections.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Direct-Bank-Connections.md @@ -72,7 +72,7 @@ If you need to connect a separate card program from the same bank (that's access To fix this, you would need to contact your bank and request to combine all of your cards under a single set of login credentials. That way, you can connect all of your cards from that bank to Expensify using a single set of login credentials. -# FAQ +{% include faq-begin.md %} ## How can I connect and manage my company’s cards centrally if I’m not a domain admin? If you cannot access Domains, you must request Domain Admin access to an existing Domain Admin (usually the workspace owner). @@ -112,3 +112,5 @@ If you've answered "yes" to any of these questions, you'll need to update this i A Domain Admin can fix the connection by heading to **Settings > Domains > _Domain Name_ > Company Cards > Fix**. You will be prompted to enter the new credentials/updated information, and this should reestablish the connection. If you are still experiencing issues with the card connection, please search for company card troubleshooting or contact Expensify Support for help. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Reconciliation.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Reconciliation.md index d6de2ca66ade..2cb684a2240b 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Reconciliation.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Reconciliation.md @@ -51,7 +51,7 @@ If there are still unapproved expenses when you want to close your books for the - Match Approved Total to Company Card Liability account in your accounting system. - Unapproved Total becomes the Accrual amount (provided the first two amounts are correct). -# FAQ +{% include faq-begin.md %} ## Who can view and access the Reconciliation tab? @@ -67,3 +67,5 @@ If a cardholder reports expenses as missing, we first recommend using the Reconc If after updating, the expense still hasn’t appeared, you should reach out to Concierge with the missing expense specifics (merchant, date, amount and last four digits of the card number). Please note, only posted transactions will import. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/deposit-accounts/Deposit-Accounts-USD.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/deposit-accounts/Deposit-Accounts-USD.md index a4ff7503f7bb..0bc5cb0ad955 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/deposit-accounts/Deposit-Accounts-USD.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/deposit-accounts/Deposit-Accounts-USD.md @@ -56,7 +56,7 @@ You should be all set! The bank account will display as a deposit-only business 1. Navigate to **Settings > Account > Payments > Bank Accounts** 2. Click the **Delete** next to the bank account you want to remove -# FAQ +{% include faq-begin.md %} ## **What happens if my bank requires an additional security check before adding it to a third-party?** @@ -73,3 +73,5 @@ There are a few reasons a reimbursement may be unsuccessful. The first step is t - Your account wasn’t set up for Direct Deposit/ACH. You may want to contact your bank to confirm. If you aren’t sure, please reach out to Concierge and we can assist! + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Overview.md b/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Overview.md index 30a507a1f9df..09dd4de2867b 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Overview.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Overview.md @@ -26,7 +26,7 @@ If at least 50% of your approved USD spend in a given month is on your company Additionally, every month, you receive 1% cash back on all Expensify Card purchases, and 2% if the spend across your Expensify Cards is $250k or more. Any cash back from the Expensify Card is first applied to your Expensify bill, further reducing your price per member. Any leftover cash back is deposited directly into your connected bank account. ## Savings calculator To see how much money you can save (and even earn!) by using the Expensify Card, check out our [savings calculator](https://use.expensify.com/price-savings-calculator). Just enter a few details and see how much you’ll save! -# FAQ +{% include faq-begin.md %} ## What if we put less than 50% of our total spend on the Expensify Card? If you put less than 50% of your total USD spend on your Expensify Card, your bill gets discounted on a sliding scale based on the percentage of use. So if you don't use the Expensify Card at all, you'll be charged the full rate for each member based on your plan and subscription. Example: @@ -36,3 +36,5 @@ Example: You save 70% on the price per member on your bill for that month. Note: USD spend refers to approved USD transactions on the Expensify Card in any given month, compared to all approved USD spend on workspaces in that same month. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Owner.md b/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Owner.md index 4fd7ef71c2e7..49a369c3cb51 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Owner.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Billing-Owner.md @@ -38,7 +38,7 @@ To take over billing for the entire domain, you must: 1. Go to **Settings > Domains > _Domain Name_ > Domain Admins** and enable Consolidated Domain Billing. Currently, Consolidated Domain Billing simply consolidates the amounts due for each Group Workspace Billing Owner (listed on the **Settings > Workspaces > Group** page). If you want to use the Annual Subscription across all Workspaces on the domain, you must also be the Billing Owner of all Group Workspaces. -# FAQ +{% include faq-begin.md %} ## Why can't I see the option to take over billing? There could be two reasons: 1. You may not have the role of Workspace Admin. If you can't click on the Workspace name (if it's not a blue hyperlink), you're not a Workspace Admin. Another Workspace Admin for that Workspace must change your role before you can proceed. @@ -47,3 +47,5 @@ There could be two reasons: There are two ways to resolve this: 1. Have your IT dept. gain access to the account so that you can make yourself an admin. Your IT department may need to recreate the ex-employee's email address. Once your IT department has access to the employee's Home page, you can request a magic link to be sent to that email address to gain access to the account. 1. Have another admin make you a Workspace admin. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Change-Plan-Or-Subscription.md b/docs/articles/expensify-classic/billing-and-subscriptions/Change-Plan-Or-Subscription.md index f01bb963bacf..1e631a53b0b3 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Change-Plan-Or-Subscription.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Change-Plan-Or-Subscription.md @@ -76,9 +76,11 @@ Note: Refunds apply to Collect or Control Group Workspaces with one month of bil Once you’ve successfully downgraded to a free Expensify account, your Workspace will be deleted and you will see a refund line item added to your Billing History. -# FAQ +{% include faq-begin.md %} ## Will I be charged for a monthly subscription even if I don't use SmartScans? Yes, the Monthly Subscription is prepaid and not based on activity, so you'll be charged regardless of usage. ## I'm on a group policy; do I need the monthly subscription too? Probably not. Group policy members already have unlimited SmartScans, so there's usually no need to buy the subscription. However, you can use it for personal use if you leave your company's Workspace. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Consolidated-Domain-Billing.md b/docs/articles/expensify-classic/billing-and-subscriptions/Consolidated-Domain-Billing.md index 35f6a428e0af..2e829c0785d3 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Consolidated-Domain-Billing.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Consolidated-Domain-Billing.md @@ -16,8 +16,10 @@ When a Domain Admin enables Consolidated Domain Billing, all Group workspaces ow If you don’t have multiple billing owners across your organization, or if you want to keep billing separate for any reason, then this feature isn’t necessary. If you have an Annual Subscription and enable Consolidated Domain Billing, the Consolidated Domain Billing feature will gather the amounts due for each Group workspace Billing Owner (listed under **Settings > Workspaces > Group**). To make full use of the Annual Subscription for all workspaces in your domain, you should also be the billing owner for all Group workspaces. -# FAQ +{% include faq-begin.md %} ## How do I take over the billing of a workspace with Consolidated Domain Billing enabled? You’ll have to toggle off Consolidated Domain Billing, take over ownership of the workspace, and then toggle it back on. ## Can I use Consolidated Domain Billing to cover the bill for some workspaces, but not others? No, this feature means that you’ll be paying the bill for all domain members who choose a subscription. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Free-Trial.md b/docs/articles/expensify-classic/billing-and-subscriptions/Free-Trial.md index 4f660588d432..e6d8f2fedb73 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Free-Trial.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Free-Trial.md @@ -31,7 +31,7 @@ To access these extra free weeks, all you need to do is complete the tasks on yo - Establish a connection between Expensify and your accounting system from the outset. By doing this early, you can start testing Expensify comprehensively from end to end. -# FAQ +{% include faq-begin.md %} ## What happens when my Free Trial ends? If you’ve already added a billing card to Expensify, you will automatically start your organization’s Expensify subscription after your Free Trial ends. At the beginning of the following month, we'll bill the card you have on file for your subscription, adjusting the charge to exclude the Free Trial period. If your Free Trial concludes without a billing card on file, you will see a notification on your Home page saying, 'Your Free Trial has expired.' @@ -42,3 +42,5 @@ If you continue without adding a billing card, you will be granted a five-day gr If you’d like to downgrade to an individual account after your Free Trial has ended, you will need to delete any Group Workspace that you have created. This action will remove the Workspaces, subscription, and any amount owed. You can do this in one of two ways from the Expensify web app: - Select the “Downgrade” option on the billing card task on your Home page. - Go to **Settings > Workspaces > [Workspace name]**, then click the gear button next to the Workspace and select Delete. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription.md b/docs/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription.md index aa08340dd7a6..1d952cb15b1c 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription.md @@ -48,7 +48,7 @@ After purchasing the subscription from the App Store, remember to sync your app The subscription renewal date is the same as the purchase date. For instance, if you sign up for the subscription on September 7th, it will renew automatically on October 7th. You can cancel your subscription anytime during the month if you no longer need unlimited SmartScans. If you do cancel, keep in mind that your subscription (and your ability to SmartScan) will continue until the last day of the billing cycle. -# FAQ +{% include faq-begin.md %} ## Can I use an Individual Subscription while on a Collect or Control Plan? You can! If you want to track expenses separately from your organization’s Workspace, you can sign up for an Individual Subscription. However, only Submit and Track Workspace plans are available when on an Individual Subscription. Collect and Control Workspace plans require an annual or pay-per-use subscription. For more information, visit expensify.com/pricing. @@ -65,3 +65,5 @@ Your subscription is a pre-purchase for 30 days of unlimited SmartScanning. This ## How can I cancel my subscription from the iOS app? If you signed up for the Monthly Subscription via iOS and your iTunes account, you will need to log into iTunes and locate the subscription there in order to cancel it. The ability to cancel an Expensify subscription started via iOS is strictly limited to your iTunes account. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription.md b/docs/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription.md index 2133e8c7da46..326ce7fe33ab 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Pay-Per-Use-Subscription.md @@ -11,7 +11,7 @@ Pay-per-use is a billing option for people who prefer to use Expensify month to 1. Create a Group Workspace if you haven’t already by going to **Settings > Workspaces > Group > New Workspace** 2. Once you’ve created your Workspace, under the “Subscription” section on the Group Workspace page, select “Pay-per-use”. -# FAQ +{% include faq-begin.md %} ## What is considered an active user? An active user is anyone who chats, creates, modifies, submits, approves, reimburses, or exports a report in Expensify. This includes actions taken by a Copilot and Workspace automation (such as Scheduled Submit and automated reimbursement). If no one on your Group Workspace uses Expensify in a given month, you will not be billed for that month. @@ -26,4 +26,4 @@ If you expect to have an increased number of users for more than 3 out of 12 mon ## Will billing only be in USD currency? While USD is the default billing currency, we also have GBP, AUD, and NZD billing currencies. You can see the rates on our [pricing](https://www.expensify.com/pricing) page. - +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Tax-Exempt.md b/docs/articles/expensify-classic/billing-and-subscriptions/Tax-Exempt.md index 33fbec003a91..92c92e4e3a44 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Tax-Exempt.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Tax-Exempt.md @@ -15,6 +15,8 @@ Once your account is marked as tax-exempt, the corresponding state tax will no l If you need to remove your tax-exempt status, let your Account Manager know or contact Concierge. -# FAQ +{% include faq-begin.md %} ## What happens to my past Expensify bills that incorrectly had tax added to them? Expensify can provide a refund for the tax you were charged on your previous bills. Please let your Account Manager know or contact Concierge if this is the case. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expense-and-report-features/Attendee-Tracking.md b/docs/articles/expensify-classic/expense-and-report-features/Attendee-Tracking.md index 7f3d83af1e6e..a0bd2c442dbb 100644 --- a/docs/articles/expensify-classic/expense-and-report-features/Attendee-Tracking.md +++ b/docs/articles/expensify-classic/expense-and-report-features/Attendee-Tracking.md @@ -20,7 +20,7 @@ Every expense has an Attendees field and will list the expense creator’s name ![image of an expense with attendee tracking]({{site.url}}/assets/images/attendee-tracking.png){:width="100%"} -# FAQ +{% include faq-begin.md %} ## Can I turn off attendee tracking? Attendee tracking is a standard field on all expenses and cannot be turned off. @@ -49,3 +49,4 @@ There is no limit. ## How can I remove attendees from an expense? You can add or remove attendees from an expense as long as they are on a Draft report. Expenses on submitted reports cannot be edited, so you cannot remove attendees from these. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expense-and-report-features/Currency.md b/docs/articles/expensify-classic/expense-and-report-features/Currency.md index eb6ca9bb2d40..77b5fbbb3ebc 100644 --- a/docs/articles/expensify-classic/expense-and-report-features/Currency.md +++ b/docs/articles/expensify-classic/expense-and-report-features/Currency.md @@ -46,7 +46,7 @@ Then, set the default currency for that workspace to match the currency in which For example, if you have employees in the US, France, Japan, and India, you’d want to create four separate workspaces, add the employees to each, and then set the corresponding currency for each workspace. -# FAQ +{% include faq-begin.md %} ## I have expenses in several different currencies. How will this show up on a report? @@ -60,5 +60,4 @@ Expenses entered in a foreign currency are automatically converted to the defaul If you want to bypass the exchange rate conversion, you can manually enter an expense in your default currency instead. - - +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expense-and-report-features/Expense-Rules.md b/docs/articles/expensify-classic/expense-and-report-features/Expense-Rules.md index ae6a9ca77db1..295aa8d00cc9 100644 --- a/docs/articles/expensify-classic/expense-and-report-features/Expense-Rules.md +++ b/docs/articles/expensify-classic/expense-and-report-features/Expense-Rules.md @@ -45,11 +45,11 @@ In general, your expense rules will be applied in order, from **top to bottom**, 4. If you belong to a workspace that is tied to an accounting integration, the configuration settings for this connection may update your expense details upon export, even if the expense rules were successfully applied to the expense. -# FAQ +{% include faq-begin.md %} ## How can I use Expense Rules to vendor match when exporting to an accounting package? When exporting non-reimbursable expenses to your connected accounting package, the payee field will list "Credit Card Misc." if the merchant name on the expense in Expensify is not an exact match to a vendor in the accounting package. When an exact match is unavailable, "Credit Card Misc." prevents multiple variations of the same vendor (e.g., Starbucks and Starbucks #1234, as is often seen in credit card statements) from being created in your accounting package. For repeated expenses, the best practice is to use Expense Rules, which will automatically update the merchant name without having to do it manually each time. This only works for connections to QuickBooks Online, Desktop, and Xero. Vendor matching cannot be performed in this manner for NetSuite or Sage Intacct due to limitations in the API of the accounting package. - +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expense-and-report-features/Expense-Types.md b/docs/articles/expensify-classic/expense-and-report-features/Expense-Types.md index 795a895e81f0..9d19dbb4f9ba 100644 --- a/docs/articles/expensify-classic/expense-and-report-features/Expense-Types.md +++ b/docs/articles/expensify-classic/expense-and-report-features/Expense-Types.md @@ -26,7 +26,7 @@ Each report will show the total amount for all expenses in the upper right. Unde - **Time Expenses:** Employees or jobs are billed based on an hourly rate that you can set within Expensify. - **Distance Expenses:** These expenses are related to travel for work. -# FAQ +{% include faq-begin.md %} ## What’s the difference between a receipt, an expense, and a report attachment? @@ -40,3 +40,5 @@ In Expensify, a credit is displayed as an expense with a minus (ex. -$1.00) in f If a report includes a credit or a refund expense, it will offset the total amount on the report. For example, the report has two reimbursable expenses, $400 and $500. The total Reimbursable is $900. Conversely, a -$400 and $500 will be a total Reimbursable amount of $500 + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expense-and-report-features/Report-Audit-Log-and-Comments.md b/docs/articles/expensify-classic/expense-and-report-features/Report-Audit-Log-and-Comments.md index 229ca4ec1fe4..04183608e3d1 100644 --- a/docs/articles/expensify-classic/expense-and-report-features/Report-Audit-Log-and-Comments.md +++ b/docs/articles/expensify-classic/expense-and-report-features/Report-Audit-Log-and-Comments.md @@ -49,7 +49,7 @@ Report comments initially trigger a mobile app notification to report participan Comments can be formatted with bold, italics, or strikethrough using basic Markdown formatting. You can also add receipts and supporting documents to a report by clicking the paperclip icon on the right side of the comment field. -# FAQ +{% include faq-begin.md %} ## Why don’t some timestamps in Expensify match up with what’s shown in the report audit log? @@ -58,3 +58,5 @@ While the audit log is localized to your own timezone, some other features in Ex ## Is commenting on a report a billable action? Yes. If you comment on a report, you become a billable actor for the current month. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expense-and-report-features/The-Expenses-Page.md b/docs/articles/expensify-classic/expense-and-report-features/The-Expenses-Page.md index 5431355dd790..57a7f7de298c 100644 --- a/docs/articles/expensify-classic/expense-and-report-features/The-Expenses-Page.md +++ b/docs/articles/expensify-classic/expense-and-report-features/The-Expenses-Page.md @@ -55,7 +55,7 @@ Select the expenses you want to export by checking the box to the left of each e Then, click **Export To** in the upper right corner of the page, and choose our default CSV format or create your own custom CSV template. -# FAQ +{% include faq-begin.md %} ## Can I use the filters and analytics features on the mobile app? The various features on the Expenses Page are only available while logged into your web account. @@ -71,3 +71,4 @@ We have more about company card expense reconciliation in this [support article] ## Can I edit multiple expenses at once? Yes! Select the expenses you want to edit and click **Edit Multiple**. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expense-and-report-features/The-Reports-Page.md b/docs/articles/expensify-classic/expense-and-report-features/The-Reports-Page.md index ff9e2105ffac..9c55cd9b4b8d 100644 --- a/docs/articles/expensify-classic/expense-and-report-features/The-Reports-Page.md +++ b/docs/articles/expensify-classic/expense-and-report-features/The-Reports-Page.md @@ -31,7 +31,7 @@ To export a report to a CSV file, follow these steps on the Reports page: 2. Navigate to the upper right corner of the page and click the "Export to" button. 3. From the drop-down options that appear, select your preferred export format. -# FAQ +{% include faq-begin.md %} ## What does it mean if the integration icon for a report is grayed out? If the integration icon for a report appears grayed out, the report has yet to be fully exported. To address this, consider these options: @@ -41,3 +41,4 @@ To address this, consider these options: ## How can I see a specific expense on a report? To locate a specific expense within a report, click on the Report from the Reports page and then click on an expense to view the expense details. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md b/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md index 3e2eb2deec46..bdc9010bc3e0 100644 --- a/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md +++ b/docs/articles/expensify-classic/expensify-card/Admin-Card-Settings-and-Features.md @@ -148,7 +148,7 @@ Here are some reasons an Expensify Card transaction might be declined: 5. The merchant is located in a restricted country - Some countries may be off-limits for transactions. If a merchant or their headquarters (billing address) are physically located in one of these countries, Expensify Card purchases will be declined. This list may change at any time, so be sure to check back frequently: Belarus, Burundi, Cambodia, Central African Republic, Democratic Republic of the Congo, Cuba, Iran, Iraq, North Korea, Lebanon, Libya, Russia, Somalia, South Sudan, Syrian Arab Republic, Tanzania, Ukraine, Venezuela, Yemen, and Zimbabwe. -# FAQ +{% include faq-begin.md %} ## What happens when I reject an Expensify Card expense? Rejecting an Expensify Card expense from an Expensify report will simply allow it to be reported on a different report. You cannot undo a credit card charge. @@ -170,3 +170,5 @@ If a transaction is pending and has a receipt attached (excluding eReceipts), a - Partial refunds: If a transaction is pending, a partial refund will reduce the amount of the transaction. - If a transaction is posted, a partial refund will create a negative transaction for the refund amount. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md index bc8a8d8bf184..181cc6cef13b 100644 --- a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md +++ b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md @@ -168,7 +168,7 @@ If Auto-Reconciliation is disabled for your company's Expensify Cards, a Domain 2. Each time a monthly settlement occurs, Expensify calculates the total purchase amount since the last settlement and creates a Journal Entry. This entry credits the settlement bank account (GL Account) and debits the Expensify Liability Account in Intacct. 3. As expenses are approved and exported to Intacct, Expensify credits the Liability Account and debits the appropriate expense categories. -# FAQ +{% include faq-begin.md %} ## What are the timeframes for auto-reconciliation in Expensify? We offer either daily or monthly auto-reconciliation: @@ -209,3 +209,5 @@ To address this, please follow these steps: 2. Go to the General Ledger (GL) account where your daily Expensify Card settlement withdrawals are recorded, and locate entries for the dates identified in Step 1. 3. Adjust each settlement entry so that it now posts to the Clearing Account. 4. Create a Journal Entry or Receive Money Transaction to clear the balance in the Liability Account using the funds currently held in the Clearing Account, which was set up in Step 2. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expensify-card/Cardholder-Settings-and-Features.md b/docs/articles/expensify-classic/expensify-card/Cardholder-Settings-and-Features.md index bc0cb28f42b1..770ded163d39 100644 --- a/docs/articles/expensify-classic/expensify-card/Cardholder-Settings-and-Features.md +++ b/docs/articles/expensify-classic/expensify-card/Cardholder-Settings-and-Features.md @@ -76,9 +76,11 @@ There was suspicious activity - If the spending looks suspicious, we may complete a manual due diligence check, and our team will do this as quickly as possible - your cards will all be locked while this happens. - The merchant is located in a restricted country -# FAQ +{% include faq-begin.md %} ## Can I use Smart Limits with a free Expensify account? If you're on the Free plan, you won't have the option to use Smart Limits. Your card limit will simply reset at the end of each calendar month. ## I still haven't received my Expensify Card. What should I do? For more information on why your card hasn't arrived, you can check out this resource on [Requesting a Card](https://help.expensify.com/articles/expensify-classic/expensify-card/Request-the-Card#what-if-i-havent-received-my-card-after-multiple-weeks). + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expensify-card/Dispute-A-Transaction.md b/docs/articles/expensify-classic/expensify-card/Dispute-A-Transaction.md index 12dad0c7084d..caf540152063 100644 --- a/docs/articles/expensify-classic/expensify-card/Dispute-A-Transaction.md +++ b/docs/articles/expensify-classic/expensify-card/Dispute-A-Transaction.md @@ -46,7 +46,7 @@ To ensure the dispute process goes smoothly, please: - If you recognize the merchant but not the charge, and you've transacted with them before, contact the merchant directly, as it may be a non-fraudulent error. - Include supporting documentation like receipts or cancellation confirmations when submitting your dispute to enhance the likelihood of a favorable resolution. -# FAQ +{% include faq-begin.md %} ## **How am I protected from fraud using the Expensify Card?** Real-time push notifications alert you of every card charge upfront, helping identify potential issues immediately. Expensify also leverages sophisticated algorithms to detect and/or block unusual card activity. @@ -59,3 +59,4 @@ The dispute process can take a few days to a few months. It depends on the type ## **Can I cancel a dispute?** Contact Concierge if you've filed a dispute and want to cancel it. You might do this if you recognize a previously reported unauthorized charge or if the merchant has already resolved the issue. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md index 4830c0fffbcd..72d584fccee4 100644 --- a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md +++ b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md @@ -38,7 +38,7 @@ If you need to cancel your Expensify Card and cannot access the website or mobil It's not possible to order a replacement card over the phone, so, if applicable, you would need to handle this step from your Expensify account. -# FAQ +{% include faq-begin.md %} ## What if I haven’t received my card after multiple weeks? @@ -47,3 +47,5 @@ Reach out to support, and we can locate a tracking number for the card. If the c ## I’m self-employed. Can I set up the Expensify Card as an individual? Yep! As long as you have a business bank account and have registered your company with the IRS, you are eligible to use the Expensify Card as an individual business owner. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md b/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md index 8f87b36ef3d9..ad1443030c31 100644 --- a/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md +++ b/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md @@ -46,7 +46,7 @@ If you have a validated domain, you can set a limit for multiple members by sett The Company Cards page will act as a hub to view all employees who have been issued a card and where you can view and edit the individual card limits. You’ll also be able to see anyone who has requested a card but doesn’t have one yet. -# FAQ +{% include faq-begin.md %} ## Are there foreign transaction fees? @@ -65,3 +65,5 @@ The Expensify Card is a free corporate card, and no fees are associated with it. As long as the verified bank account used to apply for the Expensify Card is a US bank account, your cardholders can be anywhere in the world. Otherwise, the Expensify Card is not available for customers using non-US banks. With that said, launching international support is a top priority for us. Let us know if you’re interested in contacting support, and we’ll reach out as soon as the Expensify Card is available outside the United States. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expensify-partner-program/Partner-Billing-Guide.md b/docs/articles/expensify-classic/expensify-partner-program/Partner-Billing-Guide.md index 750a1fc10e77..86dbfe5d0720 100644 --- a/docs/articles/expensify-classic/expensify-partner-program/Partner-Billing-Guide.md +++ b/docs/articles/expensify-classic/expensify-partner-program/Partner-Billing-Guide.md @@ -63,7 +63,7 @@ Using client IDs for Optimized Billing in Expensify: A unique identifier feature - Using client IDs for all Workspaces: It's beneficial to use client IDs for all Workspaces to ensure each one is easily recognizable. - Benefits of itemized billing receipts: Employing client IDs offers itemized billing by client, with each itemization detailing unique active users. -# FAQ +{% include faq-begin.md %} **Do I automatically get the special billing rate as an ExpensifyApproved! Partner?** - Yes, when you join the ExpensifyApproved! program, you will automatically get the special billing rate. To join the ExpensifyApproved! Program, you need to enroll in ExpensifyApproved! University. @@ -85,3 +85,5 @@ Using client IDs for Optimized Billing in Expensify: A unique identifier feature **Where can I see the Billing Receipts?** - All billing owners receive an emailed PDF of their monthly billing receipt, but a CSV version can also be downloaded from the platform. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/get-paid-back/Per-Diem-Expenses.md b/docs/articles/expensify-classic/get-paid-back/Per-Diem-Expenses.md index 1b537839af77..e7a43c1d1d61 100644 --- a/docs/articles/expensify-classic/get-paid-back/Per-Diem-Expenses.md +++ b/docs/articles/expensify-classic/get-paid-back/Per-Diem-Expenses.md @@ -29,7 +29,7 @@ You can include meal deductions or overnight lodging costs if your jurisdiction ### Step 6: Submit for Approval Finally, submit your Per Diem expense for approval, and you'll be on your way to getting reimbursed! -# FAQ +{% include faq-begin.md %} ## Can I edit my per diem expenses? Per Diems cannot be amended. To make changes, delete the expense and recreate it as needed. @@ -43,3 +43,5 @@ Reach out to your internal Admin team, as they've configured the rates in your p ## Can I add start and end times to per diems? Unfortunately, you cannot add start and end times to Per Diems in Expensify. By following these steps, you can efficiently create and manage your Per Diem expenses in Expensify, making the process of tracking and getting reimbursed hassle-free. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/get-paid-back/Referral-Program.md b/docs/articles/expensify-classic/get-paid-back/Referral-Program.md index b4a2b4a7de74..4cc646c613a1 100644 --- a/docs/articles/expensify-classic/get-paid-back/Referral-Program.md +++ b/docs/articles/expensify-classic/get-paid-back/Referral-Program.md @@ -25,7 +25,7 @@ The best way to start is to submit any receipt to your manager (you'll get paid Referral rewards for the Spring/Summer 2023 campaign will be paid by direct deposit. -# FAQ +{% include faq-begin.md %} - **How will I know if I am the first person to refer a company to Expensify?** @@ -52,3 +52,5 @@ Please send a message to concierge@expensify.com with the billing owner of the c Expensify members who are opted-in for our newsletters will have received an email containing their unique referral link. On the mobile app, go to **Settings** > **Invite a Friend** > **Share Invite Link** to retrieve your referral link. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/get-paid-back/Trips.md b/docs/articles/expensify-classic/get-paid-back/Trips.md index a65a8bfb8eec..ccfbe1592291 100644 --- a/docs/articles/expensify-classic/get-paid-back/Trips.md +++ b/docs/articles/expensify-classic/get-paid-back/Trips.md @@ -28,10 +28,12 @@ To view details about your past or upcoming trips, follow these steps within the 2. Navigate to the "Menu" option (top left ≡ icon) 3. Select **Trips** -# FAQ +{% include faq-begin.md %} ## How do I capture Trip receipts sent to my personal email address? If you received your receipt in an email that is not associated with your Expensify account, you can add this email as a [secondary login](https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details#how-to-add-a-secondary-login) to directly forward the receipt into your account. ## How do I upload Trip receipts that were not sent to me by email? If your trip receipt was not sent to you by email, you can manually upload the receipt to your account. Check out this resource for more information on [manually uploading receipts](https://help.expensify.com/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts#manually-upload). + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/get-paid-back/expenses/Apply-Tax.md b/docs/articles/expensify-classic/get-paid-back/expenses/Apply-Tax.md index b5f5ec8be048..c89176bcc0e8 100644 --- a/docs/articles/expensify-classic/get-paid-back/expenses/Apply-Tax.md +++ b/docs/articles/expensify-classic/get-paid-back/expenses/Apply-Tax.md @@ -19,7 +19,7 @@ There may be multiple tax rates set up within your Workspace, so if the tax on y If the tax amount on your receipt is different to the calculated amount or the tax rate doesn’t show up, you can always manually type in the correct tax amount. -# FAQ +{% include faq-begin.md %} ## How do I set up multiple taxes (GST/PST/QST) on indirect connections? Expenses sometimes have more than one tax applied to them - for example in Canada, expenses can have both a Federal GST and a provincial PST or QST. @@ -37,3 +37,4 @@ Many tax authorities do not require the reporting of tax amounts by rate and the Alternatively, you can apply each specific tax rate by splitting the expense into the components that each rate will be applied to. To do this, click on **Split Expense** and apply the correct tax rate to each part. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/get-paid-back/expenses/Merge-Expenses.md b/docs/articles/expensify-classic/get-paid-back/expenses/Merge-Expenses.md index a8444b98c951..a26146536e42 100644 --- a/docs/articles/expensify-classic/get-paid-back/expenses/Merge-Expenses.md +++ b/docs/articles/expensify-classic/get-paid-back/expenses/Merge-Expenses.md @@ -37,7 +37,7 @@ On the mobile app, merging is prompted when you see the message _"Potential dupl If the expenses exist on two different reports, you will be asked which report you'd like the newly created single expense to be reported onto. -# FAQ +{% include faq-begin.md %} ## Can you merge expenses across different reports? diff --git a/docs/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts.md b/docs/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts.md index 29380dab5a5b..b0e3ee1b9ade 100644 --- a/docs/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts.md +++ b/docs/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts.md @@ -19,7 +19,7 @@ To SmartScan a receipt on your mobile app, tap the green camera button, point an ## Manually Upload To upload receipts on the web, simply navigate to the Expenses page and click on **New Expense**. Select **Scan Receipt** and choose the file you would like to upload, or drag-and-drop your image directly into the Expenses page, and that will start the SmartScanning process! -# FAQ +{% include faq-begin.md %} ## How do you SmartScan multiple receipts? You can utilize the Rapid Fire Mode to quickly SmartScan multiple receipts at once! @@ -34,3 +34,5 @@ Once that email address has been added as a Secondary Login, simply forward your You can crop and rotate a receipt image on the web app, and you can only edit one expense at a time. Navigate to your Expenses page and locate the expense whose receipt image you'd like to edit, then click the expense to open the Edit screen. If there is an image file associated with the receipt, you will see the Rotate and Crop buttons. Alternatively, you can also navigate to your Reports page, click on a report, and locate the individual expense. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/get-paid-back/reports/Create-A-Report.md b/docs/articles/expensify-classic/get-paid-back/reports/Create-A-Report.md index ea808695e7cd..88ec2b730d1e 100644 --- a/docs/articles/expensify-classic/get-paid-back/reports/Create-A-Report.md +++ b/docs/articles/expensify-classic/get-paid-back/reports/Create-A-Report.md @@ -147,7 +147,7 @@ As you go through each violation, click View to look at the expense in more deta Click Next to move on to the next item. Click Finish to complete the review process when you’re done. -# FAQ +{% include faq-begin.md %} ## Is there a difference between Expense Reports, Bills, and Invoices? @@ -164,3 +164,5 @@ If someone external to the business sends you an invoice for their services, you ## When should I submit my report? Your Company Admin can answer this one, and they may have configured the workspace’s [Scheduled Submit] setting to enforce a regular cadence for you. If not, you can still set this up under your [Individual workspace]. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/getting-started/Invite-Members.md b/docs/articles/expensify-classic/getting-started/Invite-Members.md index 5b3c17c2e8fb..5a27f58cf2e8 100644 --- a/docs/articles/expensify-classic/getting-started/Invite-Members.md +++ b/docs/articles/expensify-classic/getting-started/Invite-Members.md @@ -51,7 +51,7 @@ Here's how it works: If a colleague signs up with a work email address that matc To enable this feature, go to **Settings > Workspace > Group > *Workspace Name* > People**. -# FAQ +{% include faq-begin.md %} ## Who can invite members to Expensify Any Workspace Admin can add members to a Group Workspace using any of the above methods. @@ -60,3 +60,5 @@ Under **Settings > Workspace > Group > *Workspace Name* > People > Invite** you ## How can I invite members via the API? If you would like to integrate an open API HR software, you can use our [Advanced Employee Updater API](https://integrations.expensify.com/Integration-Server/doc/employeeUpdater/) to invite members to your Workspace. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/getting-started/Plan-Types.md b/docs/articles/expensify-classic/getting-started/Plan-Types.md index 90c632ffa5cc..4f8c52c2e1a1 100644 --- a/docs/articles/expensify-classic/getting-started/Plan-Types.md +++ b/docs/articles/expensify-classic/getting-started/Plan-Types.md @@ -20,7 +20,7 @@ The Track plan is tailored for solo Expensify users who don't require expense su ## Individual Submit Plan The Submit plan is designed for individuals who need to keep track of their expenses and share them with someone else, such as their boss, accountant, or even a housemate. It's specifically tailored for single users who want to both track and submit their expenses efficiently. -# FAQ +{% include faq-begin.md %} ## How can I change Individual plans? You have the flexibility to switch between a Track and Submit plan, or vice versa, at any time by navigating to **Settings > Workspaces > Individual > *Workspace Name* > Plan**. This allows you to adapt your expense management approach as needed. @@ -30,3 +30,5 @@ You can easily upgrade from a Collect to a Control plan at any time by going to ## How does pricing work if I have two types of Group Workspace plans? If you have a Control and Collect Workspace, you will be charged at the Control Workspace rate. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md b/docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md index a8e1b0690b72..189ff671b213 100644 --- a/docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md +++ b/docs/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners.md @@ -10,7 +10,7 @@ Start making more with us! We're thrilled to announce a new incentive for our US # How-to To benefit from this program, all you need to do is ensure that you are listed as a domain admin on your client's Expensify account. If you're not currently a domain admin, your client can follow the instructions outlined in [our help article](https://community.expensify.com/discussion/5749/how-to-add-and-remove-domain-admins#:~:text=Domain%20Admins%20have%20total%20control,a%20member%20of%20the%20domain.) to assign you this role. -# FAQ +{% include faq-begin.md %} - What if my firm is not permitted to accept revenue share from our clients?

We understand that different firms may have different policies. If your firm is unable to accept this revenue share, you can pass the revenue share back to your client to give them an additional 0.5% of cash back using your own internal payment tools.

- What if my firm does not wish to participate in the program?
diff --git a/docs/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager.md b/docs/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager.md index 104cd49daf96..fb3cb5341f61 100644 --- a/docs/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager.md +++ b/docs/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager.md @@ -22,7 +22,7 @@ You can contact your Partner Manager by: - Signing in to new.expensify.com and searching for your Partner Manager - Replying to or clicking the chat link on any email you get from your Partner Manager -# FAQs +{% include faq-begin.md %} ## How do I know if my Partner Manager is online? You will be able to see if they are online via their status in new.expensify.com, which will either say “online” or have their working hours. @@ -32,4 +32,6 @@ If you’re unable to contact your Partner Manager (i.e., they're out of office ## Can I get on a call with my Partner Manager? Of course! You can ask your Partner Manager to schedule a call whenever you think one might be helpful. Partner Managers can discuss client onboarding strategies, firm wide training, and client setups. -We recommend continuing to work with Concierge for **general support questions**, as this team is always online and available to help immediately. \ No newline at end of file +We recommend continuing to work with Concierge for **general support questions**, as this team is always online and available to help immediately. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/getting-started/support/Expensify-Support.md b/docs/articles/expensify-classic/getting-started/support/Expensify-Support.md index f4a6acdd8571..870edf959b32 100644 --- a/docs/articles/expensify-classic/getting-started/support/Expensify-Support.md +++ b/docs/articles/expensify-classic/getting-started/support/Expensify-Support.md @@ -91,7 +91,7 @@ Your Partner Manager should reach out to you once you've completed ExpensifyAppr - **Be Clear and Specific**: When asking questions or reporting issues, provide specific examples like affected users' email addresses or report IDs. This makes it easier for us to assist you effectively. - **Practice Kindness**: Remember that we're here to help. Please be polite, considerate, and patient as we work together to resolve any concerns you have. -# FAQ +{% include faq-begin.md %} ## Who gets an Account Manager? Members who have 10 or more active users, or clients of ExpensifyApproved! Accounts are automatically assigned a dedicated Account Manager. @@ -115,3 +115,5 @@ We recommend working with Concierge on general support questions, as this team i ## Who gets assigned a Setup Specialist? This feature is specifically for new members! Whenever you start a free trial, a product Setup Specialist will be assigned to guide you through configuring your Expensify account. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/insights-and-custom-reporting/Default-Export-Templates.md b/docs/articles/expensify-classic/insights-and-custom-reporting/Default-Export-Templates.md index f6043aaea2eb..b89dca85df04 100644 --- a/docs/articles/expensify-classic/insights-and-custom-reporting/Default-Export-Templates.md +++ b/docs/articles/expensify-classic/insights-and-custom-reporting/Default-Export-Templates.md @@ -20,7 +20,7 @@ Below is a breakdown of the available default templates. 3. Click the **Export to** in the top right corner 4. Select the export template you’d like to use -# FAQ +{% include faq-begin.md %} ## Why are my numbers exporting in a weird format? Do your numbers look something like this: 1.7976931348623157e+308? This means that your spreadsheet program is formatting long numbers in an exponential or scientific format. If that happens, you can correct it by changing the data to Plain Text or a Number in your spreadsheet program. ## Why are my leading zeros missing? @@ -28,3 +28,4 @@ Is the export showing “1” instead of “01”? This means that your spreadsh ## I want a report that is not in the default list, how can I build that? For a guide on building your own custom template check out Exports > Custom Exports in the Help pages! +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/insights-and-custom-reporting/Insights.md b/docs/articles/expensify-classic/insights-and-custom-reporting/Insights.md index 6c71630015c5..ce07f4b56450 100644 --- a/docs/articles/expensify-classic/insights-and-custom-reporting/Insights.md +++ b/docs/articles/expensify-classic/insights-and-custom-reporting/Insights.md @@ -35,7 +35,7 @@ The Insights dashboard allows you to monitor all aspects of company spend across 2. Build up a report using these [formulas](https://community.expensify.com/discussion/5795/deep-dive-expense-level-formula/p1?new=1) 3. If you need any help, click the **Support** button on the top left to contact your Account Manager -# FAQs +{% include faq-begin.md %} #### Can I share my custom export report? @@ -98,4 +98,6 @@ We’ve built a huge variety of custom reports for customers, so make sure to re - Unposted Travel Aging Report - Vendor Spend - … or anything you can imagine! -{% endraw %} \ No newline at end of file +{% endraw %} + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/insights-and-custom-reporting/Other-Export-Options.md b/docs/articles/expensify-classic/insights-and-custom-reporting/Other-Export-Options.md index 7ba84cef6b94..9d752dec3eb9 100644 --- a/docs/articles/expensify-classic/insights-and-custom-reporting/Other-Export-Options.md +++ b/docs/articles/expensify-classic/insights-and-custom-reporting/Other-Export-Options.md @@ -30,10 +30,12 @@ The PDF will include all expenses, any attached receipts, and all report notes. 3. Click on **Details** in the top right of the report 4. Click the **print icon** -# FAQ +{% include faq-begin.md %} ## Why isn’t my report exporting? Big reports with lots of expenses may cause the PDF download to fail due to images with large resolutions. In that case, try breaking the report into multiple smaller reports. Also, please note that a report must have at least one expense to be exported or saved as a PDF. ## Can I download multiple PDFs at once? No, you can’t download multiple reports as PDFs at the same time. If you’d like to export multiple reports, an alternative to consider is the CSV export option. ## The data exported to Excel is showing incorrectly. How can I fix this? When opening a CSV file export from Expensify in Excel, it’ll automatically register report IDs and transaction IDs as numbers and assign the number format to the report ID column. If a number is greater than a certain length, Excel will contract the number and display it in exponential form. To prevent this, the number needs to be imported as text, which can be done by opening Excel and clicking File > Import > select your CSV file > follow the prompts and on step 3 set the report ID/transactionID column to import as Text. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/integrations/HR-integrations/ADP.md b/docs/articles/expensify-classic/integrations/HR-integrations/ADP.md index 65b276796c2a..47cbd2fdc1f3 100644 --- a/docs/articles/expensify-classic/integrations/HR-integrations/ADP.md +++ b/docs/articles/expensify-classic/integrations/HR-integrations/ADP.md @@ -70,7 +70,7 @@ You can set Custom Fields and Payroll Codes in bulk using a CSV upload in Expens If you have additional requirements for your ADP upload, for example, additional headings or datasets, reach out to your Expensify Account Manager who will assist you in customizing your ADP export. Expensify Account Managers are trained to accommodate your data requests and help you retrieve them from the system. -# FAQ +{% include faq-begin.md %} - Do I need to convert my employee list into new column headings so I can upload it to Expensify? @@ -79,3 +79,5 @@ Yes, you’ll need to convert your ADP employee data to the same headings as the - Can I add special fields/items to my ADP Payroll Custom Export Format? Yes! You can ask your Expensify Account Manager to help you prepare your ADP Payroll export so that it meets your specific requirements. Just reach out to them via the Chat option in Expensify and they’ll help you get set up. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/integrations/HR-integrations/Gusto.md b/docs/articles/expensify-classic/integrations/HR-integrations/Gusto.md index f7a5127c9c0e..33a174325bf7 100644 --- a/docs/articles/expensify-classic/integrations/HR-integrations/Gusto.md +++ b/docs/articles/expensify-classic/integrations/HR-integrations/Gusto.md @@ -34,7 +34,7 @@ Expensify's direct integration with Gusto will automatically: 2. Click **Save** in the bottom right corner to sync employees into Expensify 3. If the connection is successful, you'll see a summary of how many employees were synced. If any employees were skipped, we'll tell you why. -# FAQ +{% include faq-begin.md %} ## Can I import different sets of employees into different Expensify workspaces? No - Gusto will add all employees to one Expensify workspace, so if you have more than one workspace, you'll need to choose when connecting. @@ -53,3 +53,5 @@ If your employees are set up in Expensify with their company emails, but with th To resolve this, you can ask each affected employee to merge their existing Expensify account with the new Expensify account by navigating to **Settings > Account > Account Details** and scrolling down to **Merge Accounts**. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/Certinia.md b/docs/articles/expensify-classic/integrations/accounting-integrations/Certinia.md index 65361ba1af9a..963e8a408585 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/Certinia.md +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/Certinia.md @@ -87,7 +87,7 @@ When exporting to Certinia PSA/SRP you may see up to three different currencies * Amount field on the Expense line: this currency is derived from the Expensify workspace default report currency. * Reimbursable Amount on the Expense line: this currency is derived from the currency of the resource with an email matching the report submitter. -# FAQ +{% include faq-begin.md %} ## What happens if the report can’t be exported to Certinia? * The preferred exporter will receive an email outlining the issue and any specific error messages * Any error messages preventing the export from taking place will be recorded in the report’s history @@ -148,3 +148,5 @@ Log into Certinia and go to Setup > Manage Users > Users and find the user whose * Enable Modify All Data and save Sync the connection within Expensify by going to **Settings** > **Workspaces** > **Groups** > _[Workspace Name]_ > **Connections** > **Sync Now** and then attempt to export the report again + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/Indirect-Accounting-Integrations.md b/docs/articles/expensify-classic/integrations/accounting-integrations/Indirect-Accounting-Integrations.md index 852db0b7f7c0..09fad1b0ed1a 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/Indirect-Accounting-Integrations.md +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/Indirect-Accounting-Integrations.md @@ -30,7 +30,7 @@ To export a report, click **Export To** in the top-left of a report and select y To export multiple reports, tick the checkbox next to the reports on the **Reports** page, then click **Export To** and select your accounting package from the dropdown menu. -# FAQ +{% include faq-begin.md %} ## Which accounting packages offer this indirect integration with Expensify? @@ -46,3 +46,5 @@ We support a pre-configured flat-file integration for the following accounting p If your accounting package isn’t listed, but it still accepts a flat-file import, select **Other** when completing the Accounting Software task on your Home page or head to **Settings** > **Workspaces** > **Group** > _Your desired workspace_ > **Export Formats**. This option allows you to create your own templates to export your expense and report data into a format compatible with your accounting system. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md b/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md index 8092ed9c6dd6..3ce0d07cb65d 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md @@ -558,7 +558,7 @@ Here's how you can send them to us: Send these two files to your Account Manager or Concierge so we can continue troubleshooting! -# FAQ +{% include faq-begin.md %} ## What type of Expensify plan is required for connecting to NetSuite? @@ -573,3 +573,5 @@ If a report is exported to NetSuite and then marked as paid in NetSuite, the rep ## If I enable Auto Sync, what happens to existing approved and reimbursed reports? If you previously had Auto Sync disabled but want to allow that feature to be used going forward, you can safely turn on Auto Sync without affecting existing reports. Auto Sync will only take effect for reports created after enabling that feature. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Desktop.md b/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Desktop.md index 958e423273ce..8fe31f3ec4f4 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Desktop.md +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Desktop.md @@ -88,7 +88,7 @@ You can bring in Customers/Projects from QuickBooks into Expensify in two ways: ## Items Items can be imported from QuickBooks as categories alongside your expense accounts. -# FAQ +{% include faq-begin.md %} ## How do I sync my connection? 1: Ensure that both the Expensify Sync Manager and QuickBooks Desktop are running. 2: On the Expensify website, navigate to **Settings** > **Policies** > **Group** > _[Policy Name]_ > **Connections** > **QuickBooks Desktop**, and click **Sync now**. @@ -143,3 +143,5 @@ To resolve this error, follow these steps: Verify that the Sync Manager's status is **Connected**. 3. If the Sync Manager status is already **Connected**, click **Edit** and then *Save* to refresh the connection. Afterwards, try syncing your policy again. 4. If the error persists, double-check that the token you see in the Sync Manager matches the token in your connection settings. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Online.md b/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Online.md index 4075aaf18016..623e5f1dd997 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Online.md +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/QuickBooks-Online.md @@ -302,7 +302,7 @@ Here are the QuickBooks Online fields that can be mapped as a report field withi - Customers/Projects - Locations -# FAQ +{% include faq-begin.md %} ## What happens if the report can't be exported to QuickBooks Online automatically? @@ -320,3 +320,5 @@ To ensure reports are reviewed before export, set up your Workspaces with the ap - If a report has been exported and reimbursed via ACH, it will be automatically marked as paid in QuickBooks Online during the next sync. - If a report has been exported and marked as paid in QuickBooks Online, it will be automatically marked as reimbursed in Expensify during the next sync. - Reports that have yet to be exported to QuickBooks Online won't be automatically exported. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct.md b/docs/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct.md index ac0a90ba6d37..560a65d0d722 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct.md +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct.md @@ -550,7 +550,7 @@ When ACH reimbursement is enabled, the "Sync Reimbursed Reports" feature will ad Intacct requires that the target account for the Bill Payment be a Cash and Cash Equivalents account type. If you aren't seeing the account you want in that list, please first confirm that the category on the account is Cash and Cash Equivalents. -# FAQ +{% include faq-begin.md %} ## What if my report isn't automatically exported to Sage Intacct? There are a number of factors that can cause automatic export to fail. If this happens, the preferred exporter will receive an email and an Inbox task outlining the issue and any associated error messages. The same information will be populated in the comments section of the report. @@ -566,3 +566,5 @@ If your workspace has been connected to Intacct with Auto Sync disabled, you can If a report has been exported to Intacct and reimbursed via ACH in Expensify, we'll automatically mark it as paid in Intacct during the next sync. If a report has been exported to Intacct and marked as paid in Intacct, we'll automatically mark it as reimbursed in Expensify during the next sync. If a report has not been exported to Intacct, it will not be exported to Intacct automatically. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/Xero.md b/docs/articles/expensify-classic/integrations/accounting-integrations/Xero.md index 98cc6f2bfdf6..9dd479e90cf1 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/Xero.md +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/Xero.md @@ -236,7 +236,7 @@ If we can't find a match, we'll create a new customer record in Xero. And that's it! You've successfully set up and managed your invoice exports to Xero, making your tracking smooth and efficient. -# FAQ +{% include faq-begin.md %} ## Will receipt images be exported to Xero? @@ -258,3 +258,5 @@ It will be automatically marked as reimbursed in Expensify during the next sync. Reports that haven't been exported to Xero won't be sent automatically. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Additional-Travel-Integrations.md b/docs/articles/expensify-classic/integrations/travel-integrations/Additional-Travel-Integrations.md index ac37a01b3e6b..7dcc8e5e9c29 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/Additional-Travel-Integrations.md +++ b/docs/articles/expensify-classic/integrations/travel-integrations/Additional-Travel-Integrations.md @@ -52,7 +52,7 @@ You can automatically import receipts from many travel platforms into Expensify, - From your account settings, choose whether expenses should be sent to Expensify automatically or manually. - We recommend sending them automatically, so you can travel without even thinking about your expense reports. -# FAQ +{% include faq-begin.md %} **Q: What if I don’t have the option for Send to Expensify in Trainline?** @@ -69,3 +69,5 @@ A: Yes, you can set a weekly or monthly cadence for SpotHero expenses to be emai **Q: Do I need to select a specific profile before booking in Bolt Work and Grab?** A: Yes, ensure you have selected your work or business profile as the payment method before booking. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Navan.md b/docs/articles/expensify-classic/integrations/travel-integrations/Navan.md index 237047fa270e..b1bf3c9745ff 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/Navan.md +++ b/docs/articles/expensify-classic/integrations/travel-integrations/Navan.md @@ -20,7 +20,7 @@ Once you complete these steps, any flights you book through Navan will automatic If you booked your Navan flight using your Expensify Card, the Navan expense will automatically merge with the card expense. Learn more about the Expensify Card [here](https://use.expensify.com/company-credit-card). -# FAQ +{% include faq-begin.md %} ## How do I expense a prepaid hotel booking in Expensify using the Navan integration? Bookings that weren’t made in Navan directly (such as a prepaid hotel booking) won’t auto-import into Expensify. To import these trips into Expensify, follow these steps: @@ -45,3 +45,5 @@ Costs depend on your subscription plans with Expensify and Navan. Expensify does ## How do I disconnect the integration? To disconnect the integration, navigate to the integrations section in Navan, find Expensify, and select the option to disable the integration. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Removing-Members.md b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Removing-Members.md index 33ffe7172603..65acc3630582 100644 --- a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Removing-Members.md +++ b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Removing-Members.md @@ -13,7 +13,7 @@ Removing a member from a workspace disables their ability to use the workspace. ![image of members table in a workspace]({{site.url}}/assets/images/ExpensifyHelp_RemovingMembers.png){:width="100%"} -# FAQ +{% include faq-begin.md %} ## Will reports from this member on this workspace still be available? Yes, as long as the reports have been submitted. You can navigate to the Reports page and enter the member's email in the search field to find them. However, Draft reports will be removed from the workspace, so these will no longer be visible to the Workspace Admin. @@ -34,3 +34,5 @@ If a member is a **preferred exporter, billing owner, report approver** or has * ## How do I remove a user completely from a company account? If you have a Control Workspace and have Domain Control enabled, you will need to remove them from the domain to delete members' accounts entirely and deactivate the Expensify Card. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate.md b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate.md index 7c21b12a83e1..4727b1c4a38b 100644 --- a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate.md +++ b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate.md @@ -38,7 +38,7 @@ Your delegate's actions will be noted in the history and comments of each report The system records every action your vacation delegate takes on your behalf in the **Report History and Comments**. So, you can see when they approved an expense report for you. -# FAQs +{% include faq-begin.md %} ## Why can't my Vacation Delegate reimburse reports that they approve? @@ -50,5 +50,4 @@ If they do not have access to the reimbursement account used on your workspace, Don't worry, your delegate can also pick their own **Vacation Delegate**. This way, expense reports continue to get approved even if multiple people are away. - - +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/send-payments/Pay-Bills.md b/docs/articles/expensify-classic/send-payments/Pay-Bills.md index 8a5c7c5c7f88..81dcf3488462 100644 --- a/docs/articles/expensify-classic/send-payments/Pay-Bills.md +++ b/docs/articles/expensify-classic/send-payments/Pay-Bills.md @@ -70,7 +70,17 @@ To mark a Bill as paid outside of Expensify: **Fees:** None -# FAQ +# Deep Dive: How company bills and vendor invoices are processed in Expensify + +Here is how a vendor or supplier bill goes from received to paid in Expensify: + +1. When a vendor or supplier bill is received in Expensify via, the document is SmartScanned automatically and a Bill is created. The Bill is owned by the primary domain contact, who will see the Bill on the Reports page on their default group policy. +2. When the Bill is ready for processing, it is submitted and follows the primary domain contact’s approval workflow. Each time the Bill is approved, it is visible in the next approver's Inbox. +3. The final approver pays the Bill from their Expensify account on the web via one of the methods. +4. The Bill is coded with the relevant imported GL codes from a connected accounting software. After it has finished going through the approval workflow the Bill can be exported back to the accounting package connected to the policy. + + +{% include faq-begin.md %} ## What is my company's billing intake email? Your billing intake email is [yourdomain.com]@expensify.cash. Example, if your domain is `company.io` your billing email is `company.io@expensify.cash`. @@ -100,11 +110,4 @@ Payments are currently only supported for users paying in United States Dollars A Bill is a payable which represents an amount owed to a payee (usually a vendor or supplier), and is usually created from a vendor invoice. An Invoice is a receivable, and indicates an amount owed to you by someone else. -# Deep Dive: How company bills and vendor invoices are processed in Expensify - -Here is how a vendor or supplier bill goes from received to paid in Expensify: - -1. When a vendor or supplier bill is received in Expensify via, the document is SmartScanned automatically and a Bill is created. The Bill is owned by the primary domain contact, who will see the Bill on the Reports page on their default group policy. -2. When the Bill is ready for processing, it is submitted and follows the primary domain contact’s approval workflow. Each time the Bill is approved, it is visible in the next approver's Inbox. -3. The final approver pays the Bill from their Expensify account on the web via one of the methods. -4. The Bill is coded with the relevant imported GL codes from a connected accounting software. After it has finished going through the approval workflow the Bill can be exported back to the accounting package connected to the policy. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/send-payments/Reimbursing-Reports.md b/docs/articles/expensify-classic/send-payments/Reimbursing-Reports.md index e55d99d70827..120699675159 100644 --- a/docs/articles/expensify-classic/send-payments/Reimbursing-Reports.md +++ b/docs/articles/expensify-classic/send-payments/Reimbursing-Reports.md @@ -63,7 +63,7 @@ If either limit has been reached, you can expect funds deposited within your ban Rapid Reimbursement is not available for non-US-based reimbursement. If you are receiving a reimbursement to a non-US-based deposit account, you should expect to see the funds deposited in your bank account within four business days. -# FAQ +{% include faq-begin.md %} ## Who can reimburse reports? Only a workspace admin who has added a verified business bank account to their Expensify account can reimburse employees. @@ -73,3 +73,4 @@ Only a workspace admin who has added a verified business bank account to their E Instead of a bulk reimbursement option, you can set up automatic reimbursement. With this configured, reports below a certain threshold (defined by you) will be automatically reimbursed via ACH as soon as they're "final approved." To set your manual reimbursement threshold, head to **Settings > Workspace > Group > _[Workspace Name]_ > Reimbursement > Manual Reimbursement**. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/send-payments/Third-Party-Payments.md b/docs/articles/expensify-classic/send-payments/Third-Party-Payments.md index 1a567dbe6fa3..cae289a0526a 100644 --- a/docs/articles/expensify-classic/send-payments/Third-Party-Payments.md +++ b/docs/articles/expensify-classic/send-payments/Third-Party-Payments.md @@ -42,7 +42,7 @@ Once you've set up your third party payment option, you can start using it to re 4. **Track Payment Status**: You can track the status of payments and view transaction details within your Expensify account. -# FAQ’s +{% include faq-begin.md %} ## Q: Are there any fees associated with using third party payment options in Expensify? @@ -57,3 +57,5 @@ A: Expensify allows you to link multiple payment providers if needed. You can se A: The reimbursement limit may depend on the policies and settings configured within your Expensify account and the limits imposed by your chosen payment provider. With Expensify's third party payment options, you can simplify your expense management and reimbursement processes. By following the steps outlined in this guide, you can set up and use third party payments efficiently. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md index 3c5bc0fe2421..30adac589dc0 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/Budgets.md @@ -42,7 +42,7 @@ Expensify’s Budgets feature allows you to: - **Per individual budget**: you can enter an amount if you want to set a budget per person - **Notification threshold** - this is the % in which you will be notified as the budgets are hit -# FAQ +{% include faq-begin.md %} ## Can I import budgets as a CSV? At this time, you cannot import budgets via CSV since we don’t import categories or tags from direct accounting integrations. @@ -54,3 +54,4 @@ Notifications are sent twice: ## How will I be notified when a budget is hit? A message will be sent in the #admins room of the Workspace. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Categories.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Categories.md index 783bd50f17a3..0cd7ba790a9c 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/Categories.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/Categories.md @@ -143,7 +143,7 @@ Category violations can happen for the following reasons: If Scheduled Submit is enabled on a workspace, expenses with category violations will not be auto-submitted unless the expense has a comment added. -# FAQ +{% include faq-begin.md %} ## The correct category list isn't showing when one of my employees is categorizing their expenses. Why is this happening? Its possible the employee is defaulted to their personal workspace so the expenses are not pulling the correct categories to choose from. Check to be sure the report is listed under the correct workspace by looking under the details section on top right of report. @@ -151,3 +151,4 @@ Its possible the employee is defaulted to their personal workspace so the expens ## Will the account numbers from our accounting system (QuickBooks Online, Sage Intacct, etc.) show in the Category list when employees are choosing what chart of accounts category to code their expense to? The GL account numbers will be visible in the workspace settings when connected to a Control-level workspace for workspace admins to see. We do not provide this information in an employee-facing capacity because most employees do not have access to that information within the accounting integration. If you wish to have this information available to your employees when they are categorizing their expenses, you can edit the account name in your accounting software to include the GL number — i.e. **Accounts Payable - 12345** +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Domains-Overview.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Domains-Overview.md index cf2f0f59a4a0..6cafe3dccfaf 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/Domains-Overview.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/Domains-Overview.md @@ -131,10 +131,11 @@ To enable SAML SSO in Expensify you will first need to claim and verify your dom - For disputing digital Expensify Card purchases, two-factor authentication must be enabled. - It might take up to 2 hours for domain-level enforcement to take effect, and users will be prompted to configure their individual 2FA settings on their next login to Expensify. -# FAQ +{% include faq-begin.md %} ## How many domains can I have? You can manage multiple domains by adding them through **Settings > Domains > New Domain**. However, to verify additional domains, you must be a Workspace Admin on a Control Workspace. Keep in mind that the Collect plan allows verification for just one domain. ## What’s the difference between claiming a domain and verifying a domain? Claiming a domain is limited to users with matching email domains, and allows Workspace Admins with a company email to manage bills, company cards, and reconciliation. Verifying a domain offers extra features and security. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Expenses.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Expenses.md index 388bb5d5cbc9..ea701dc09d3e 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/Expenses.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/Expenses.md @@ -106,7 +106,7 @@ If you enable tax but don’t select a tax rate or enter a tax reclaimable amoun Note: _Expensify won’t automatically track cumulative mileage. If you need to track cumulative mileage per employee, we recommend building a mileage report using our custom export formulas._ -# FAQs +{% include faq-begin.md %} ## Why do I see eReceipts for expenses greater than $75? @@ -116,3 +116,4 @@ An eReceipt is generated for Expensify card purchases of any amount in the follo Expensify does not update mileage rates to match the rate provided by the IRS. An admin of the workspace will need to update the rate or create a new rate in the workspace. This is because Expensify has customers worldwide, not just in the United States, and most companies want to communicate the change with employees and control the timing. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Per-Diem.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Per-Diem.md index fcb1c8018613..87aef233aeb1 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/Per-Diem.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/Per-Diem.md @@ -86,7 +86,7 @@ When you _Export to CSV_, Expensify also assigns a Rate ID to each existing rate Note: _This rate ID corresponds to the Destination+Subrate. You cannot overwrite Destinations, but you can overwrite the Subrate within a Destination by using this rate ID. Always use the “Clear Rate” option with a fresh upload when removing large numbers of rates rather than deleting them individually._ -# FAQs +{% include faq-begin.md %} ## How do I report on my team's Per Diem expenses? @@ -95,3 +95,4 @@ Great question! We’ve added a Per Diem export for users to export Per Diem exp ## What if I need help setting the exact rate amounts and currencies? Right now, Expensify can't help determine what these should be. They vary widely based on your country of origin, the state within that jurisdiction, your company workspace, and the time (usually year) you traveled. There's a demonstration spreadsheet [here](https://s3-us-west-1.amazonaws.com/concierge-responses-expensify-com/uploads%2F1596692482998-Germany+-+Per+Diem.csv), but it shouldn't be used for actual claims unless verified by your internal finance team or accountants. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Reimbursement.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Reimbursement.md index a1916465fca8..ed2384d12006 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/Reimbursement.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/Reimbursement.md @@ -32,7 +32,7 @@ A Workspace admin can enanble indirect reimbursement via **Settings > Workspaces **Additional features under Reimbursement > Indirect:** If you reimburse through a seperate system or through payroll, Expensify can collect and export employee bank account details for you. Just reach out to your Account Manager or concierge@expensify.com for us to add the Reimbursement Details Export format to the account. -# FAQ +{% include faq-begin.md %} ## How do I export employee bank account details once the Reimbursement Details Export format is added to my account? @@ -45,3 +45,4 @@ Bank account names can be updated via **Settings > Accounts > Payments** and cli ## What is the benefit of setting a default reimburser? The main benefit of being defined as the "reimburser" in the Workspace settings is that this user will receive notifications on their Home page alerting them when reports need to be reimbursed. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/SAML-SSO.md b/docs/articles/expensify-classic/workspace-and-domain-settings/SAML-SSO.md index 758cb70067e1..e4b27b238e46 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/SAML-SSO.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/SAML-SSO.md @@ -77,7 +77,7 @@ To enable SSO with Microsoft ADFS follow these steps: Assuming you’ve also set up Expensify SAML configuration with your metadata, SAML logins on Expensify.com should now work. For reference, ADFS’ default metadata path is: https://yourservicename.yourdomainname.com/FederationMetadata/2007-06/FederationMetadata.xml. -# FAQ +{% include faq-begin.md %} ## What should I do if I’m getting an error when trying to set up SSO? You can double check your configuration data for errors using samltool.com. If you’re still having issues, you can reach out to your Account Manager or contact Concierge for assistance. @@ -87,3 +87,4 @@ The entityID for Expensify is https://expensify.com. Remember not to copy and pa ## Can you have multiple domains with only one entityID? Yes. Please send a message to Concierge or your account manager and we will enable the ability to use the same entityID with multiple domains. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Tags.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Tags.md index 2e6bd335ce4c..d802a183c8ba 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/Tags.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/Tags.md @@ -78,7 +78,7 @@ Alternatively, if you update the tag details in your accounting integration, be # Deep Dive ## Make tags required You can require tags for any workspace expenses by enabling People must tag expenses on the Tags page by navigating to Settings > Workspace > Group > [Workspace Name] > Tags. -# FAQ +{% include faq-begin.md %} ## What are the different tag options? If you want your second tag to depend on the first one, use dependent tags. Include GL codes if needed, especially when using accounting integrations. @@ -91,4 +91,4 @@ Multi-level tagging is only available with the Control type policy. ## I can’t see "Do you want to use multiple level tags" feature on my company's expense workspace. Why is that? If you are connected to an accounting integration, you will not see this feature. You will need to add those tags in your integration first, then sync the connection. - +{% include faq-end.md %} diff --git a/docs/articles/new-expensify/bank-accounts/Connect-a-Bank-Account.md b/docs/articles/new-expensify/bank-accounts/Connect-a-Bank-Account.md index de66315f2d79..307641c9c605 100644 --- a/docs/articles/new-expensify/bank-accounts/Connect-a-Bank-Account.md +++ b/docs/articles/new-expensify/bank-accounts/Connect-a-Bank-Account.md @@ -114,7 +114,7 @@ If you get a generic error message that indicates, "Something's gone wrong", ple 8. If you have another phone available, try to follow these steps on that device If the issue persists, please contact your Account Manager or Concierge for further troubleshooting assistance. -# FAQ +{% include faq-begin.md %} ## What is a Beneficial Owner? A Beneficial Owner refers to an **individual** who owns 25% or more of the business. If no individual owns 25% or more of the business, the company does not have a Beneficial Owner. @@ -140,3 +140,4 @@ It's a good idea to wait till the end of that second business day. If you still Make sure to reach out to your Account Manager or Concierge once that's all set, and our team will be able to re-trigger those three test transactions! +{% include faq-end.md %} diff --git a/docs/articles/new-expensify/billing-and-plan-types/The-Free-Plan.md b/docs/articles/new-expensify/billing-and-plan-types/The-Free-Plan.md index 3b79072aa393..b036c5b087d2 100644 --- a/docs/articles/new-expensify/billing-and-plan-types/The-Free-Plan.md +++ b/docs/articles/new-expensify/billing-and-plan-types/The-Free-Plan.md @@ -60,7 +60,7 @@ Request an edit an expense or remove an expense before you pay, you can let your - Automatic submission is already set up, so your admin can pay you back immediately once you create an expense. - Your admin will get a notification when you send them a new expense, but you can also remind them to pay you by making a comment in the Report History section of your Processing report or chatting with them on new.expensify.com. -# FAQs +{% include faq-begin.md %} ## Do I need a business bank account to use the Free Plan? @@ -145,3 +145,5 @@ Depending on how quickly you report it to us, we may be able to help cancel a re ## As an admin, can I edit users’ expenses and delete them from reports? No. Only users can edit and delete expenses on the Free plan. Admin control of submitted expenses on a workspace is a feature of our paid plans. If you need something changed, let the user know by commenting in the Report History section of the report on www.expensify.com or by chatting with them in new.expensify.com. + +{% include faq-end.md %} diff --git a/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md b/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md index 25ccdefad261..c7ae49e02292 100644 --- a/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md +++ b/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md @@ -136,7 +136,7 @@ You will receive a whisper from Concierge any time your content has been flagged *Note: Any message sent in public chat rooms are automatically reviewed by an automated system looking for offensive content and sent to our moderators for final decisions if it is found.* -# FAQs +{% include faq-begin.md %} ## What are the #announce and #admins rooms? @@ -162,3 +162,4 @@ The way your chats display in the left-hand menu is customizable. We offer two d - #focus mode will display only unread and pinned chats, and will sort them alphabetically. This setting is perfect for when you need to cut distractions and focus on a crucial project. You can find your display mode by clicking on your Profile > Preferences > Priority Mode. +{% include faq-end.md %} diff --git a/docs/articles/new-expensify/get-paid-back/Distance-Requests.md b/docs/articles/new-expensify/get-paid-back/Distance-Requests.md index 91b88409be8b..899cb48fd1f5 100644 --- a/docs/articles/new-expensify/get-paid-back/Distance-Requests.md +++ b/docs/articles/new-expensify/get-paid-back/Distance-Requests.md @@ -20,8 +20,9 @@ Expensify allows you to request reimbursement for mileage by creating a distance -# FAQs +{% include faq-begin.md %} ## Is there an easy way to reuse recent locations? Yes! We save your recently used locations and list them out on the page where you select the Start and Finish. +{% include faq-end.md %} diff --git a/docs/articles/new-expensify/get-paid-back/Referral-Program.md b/docs/articles/new-expensify/get-paid-back/Referral-Program.md index 6ffb923aeb76..a1b1043dff47 100644 --- a/docs/articles/new-expensify/get-paid-back/Referral-Program.md +++ b/docs/articles/new-expensify/get-paid-back/Referral-Program.md @@ -31,7 +31,7 @@ The sky's the limit for this referral program! Your referral can be anyone - a f For now, referral rewards will be paid via direct deposit into bank accounts that are connected to Expensify. -# FAQ +{% include faq-begin.md %} - **How will I know if I'm the first person to refer a company to Expensify?** @@ -54,3 +54,4 @@ Expensify reserves the right to modify the terms of the referral program at any - **Where can I find my referral link?** In New Expensify, go to **Settings** > **Share code** > **Get $250** to retrieve your invite link. +{% include faq-end.md %} diff --git a/docs/articles/new-expensify/get-paid-back/Request-Money.md b/docs/articles/new-expensify/get-paid-back/Request-Money.md index 43a72a075de7..9aac4787484c 100644 --- a/docs/articles/new-expensify/get-paid-back/Request-Money.md +++ b/docs/articles/new-expensify/get-paid-back/Request-Money.md @@ -31,6 +31,7 @@ These two features ensure you can live in the moment and settle up afterward. - Enter a reason for the split - The split is then shared equally between the attendees -# FAQs +{% include faq-begin.md %} ## Can I request money from more than one person at a time? If you need to request money for more than one person at a time, you’ll want to use the Split Bill feature. The Request Money option is for one-to-one payments between two people. +{% include faq-end.md %} diff --git a/docs/articles/new-expensify/workspace-and-domain-settings/Domain-Settings-Overview.md b/docs/articles/new-expensify/workspace-and-domain-settings/Domain-Settings-Overview.md index cf2f0f59a4a0..40d759479390 100644 --- a/docs/articles/new-expensify/workspace-and-domain-settings/Domain-Settings-Overview.md +++ b/docs/articles/new-expensify/workspace-and-domain-settings/Domain-Settings-Overview.md @@ -131,10 +131,12 @@ To enable SAML SSO in Expensify you will first need to claim and verify your dom - For disputing digital Expensify Card purchases, two-factor authentication must be enabled. - It might take up to 2 hours for domain-level enforcement to take effect, and users will be prompted to configure their individual 2FA settings on their next login to Expensify. -# FAQ +{% include faq-begin.md %} ## How many domains can I have? You can manage multiple domains by adding them through **Settings > Domains > New Domain**. However, to verify additional domains, you must be a Workspace Admin on a Control Workspace. Keep in mind that the Collect plan allows verification for just one domain. ## What’s the difference between claiming a domain and verifying a domain? Claiming a domain is limited to users with matching email domains, and allows Workspace Admins with a company email to manage bills, company cards, and reconciliation. Verifying a domain offers extra features and security. + +{% include faq-end.md %} From f4708f9b20887b975d86d678b691ab2a2f672f52 Mon Sep 17 00:00:00 2001 From: sourcecodedeveloper Date: Sun, 24 Dec 2023 03:13:24 -0800 Subject: [PATCH 076/193] Add regex for accented chars --- src/CONST.ts | 1 + src/libs/ValidationUtils.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/CONST.ts b/src/CONST.ts index aca59bd831e6..e5d369026714 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1352,6 +1352,7 @@ const CONST = { DIGITS_AND_PLUS: /^\+?[0-9]*$/, ALPHABETIC_AND_LATIN_CHARS: /^[\p{Script=Latin} ]*$/u, NON_ALPHABETIC_AND_NON_LATIN_CHARS: /[^\p{Script=Latin}]/gu, + ACCENT_LATIN_CHARS: /[\u00C0-\u017F]/g, POSITIVE_INTEGER: /^\d+$/, PO_BOX: /\b[P|p]?(OST|ost)?\.?\s*[O|o|0]?(ffice|FFICE)?\.?\s*[B|b][O|o|0]?[X|x]?\.?\s+[#]?(\d+)\b/, ANY_VALUE: /^.+$/, diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index ba977312fcfb..88d8438df511 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -317,7 +317,8 @@ function isValidDisplayName(name: string): boolean { * Checks that the provided legal name doesn't contain special characters */ function isValidLegalName(name: string): boolean { - return CONST.REGEX.ALPHABETIC_AND_LATIN_CHARS.test(name); + const hasAccentedChars = Boolean(name.match(CONST.REGEX.ACCENT_LATIN_CHARS)); + return CONST.REGEX.ALPHABETIC_AND_LATIN_CHARS.test(name) && !hasAccentedChars; } /** From 477ca8fa7180815d918cf1e1218212f22f7f209e Mon Sep 17 00:00:00 2001 From: honnamkuan Date: Tue, 26 Dec 2023 15:23:41 +0800 Subject: [PATCH 077/193] do not focus on text input on select row using touch screen device --- src/pages/NewChatPage.js | 4 ++-- .../MoneyTemporaryForRefactorRequestParticipantsSelector.js | 4 ++-- .../MoneyRequestParticipantsSelector.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/NewChatPage.js b/src/pages/NewChatPage.js index d7abbab6e93f..c76984dd136b 100755 --- a/src/pages/NewChatPage.js +++ b/src/pages/NewChatPage.js @@ -13,7 +13,7 @@ import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import * as Browser from '@libs/Browser'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import compose from '@libs/compose'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Permissions from '@libs/Permissions'; @@ -248,7 +248,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, translate, i onChangeText={setSearchTermAndSearchInServer} headerMessage={headerMessage} boldStyle - shouldPreventDefaultFocusOnSelectRow={!Browser.isMobile()} + shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} shouldShowOptions={isOptionsDataReady} shouldShowConfirmButton shouldShowReferralCTA diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 4db9c4ce3fb7..cc9f1ea52057 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -12,7 +12,7 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Report from '@libs/actions/Report'; -import * as Browser from '@libs/Browser'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import compose from '@libs/compose'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -321,7 +321,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ shouldShowOptions={isOptionsDataReady} shouldShowReferralCTA referralContentType={iouType === 'send' ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST} - shouldPreventDefaultFocusOnSelectRow={!Browser.isMobile()} + shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} shouldDelayFocus footerContent={isAllowedToSplit && footerContent} isLoadingNewOptions={isSearchingForReports} diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index f51e6d7e9fdd..ed69bcd61943 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -13,7 +13,7 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Report from '@libs/actions/Report'; -import * as Browser from '@libs/Browser'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import compose from '@libs/compose'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -334,7 +334,7 @@ function MoneyRequestParticipantsSelector({ shouldShowOptions={isOptionsDataReady} shouldShowReferralCTA referralContentType={iouType === 'send' ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST} - shouldPreventDefaultFocusOnSelectRow={!Browser.isMobile()} + shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} shouldDelayFocus footerContent={isAllowedToSplit && footerContent} isLoadingNewOptions={isSearchingForReports} From 09cec5486e9e83d7d1a4b216e961663a6412b383 Mon Sep 17 00:00:00 2001 From: honnamkuan Date: Tue, 26 Dec 2023 17:04:13 +0800 Subject: [PATCH 078/193] apply prettier diff --- src/pages/NewChatPage.js | 2 +- .../MoneyTemporaryForRefactorRequestParticipantsSelector.js | 2 +- .../MoneyRequestParticipantsSelector.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/NewChatPage.js b/src/pages/NewChatPage.js index c76984dd136b..0a00f39552e4 100755 --- a/src/pages/NewChatPage.js +++ b/src/pages/NewChatPage.js @@ -13,8 +13,8 @@ import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import compose from '@libs/compose'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Permissions from '@libs/Permissions'; import * as ReportUtils from '@libs/ReportUtils'; diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index cc9f1ea52057..61b042052b05 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -12,8 +12,8 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Report from '@libs/actions/Report'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import compose from '@libs/compose'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; import personalDetailsPropType from '@pages/personalDetailsPropType'; diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index ed69bcd61943..38f7ac7cd6ef 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -13,8 +13,8 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Report from '@libs/actions/Report'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import compose from '@libs/compose'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; import reportPropTypes from '@pages/reportPropTypes'; From fada9bf77ae60fdae58a56c268179e483bf1830a Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 26 Dec 2023 23:10:43 +0530 Subject: [PATCH 079/193] fixed plaid account icon --- src/components/AddPlaidBankAccount.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js index 656694a785a3..71d461d6d76b 100644 --- a/src/components/AddPlaidBankAccount.js +++ b/src/components/AddPlaidBankAccount.js @@ -249,7 +249,7 @@ function AddPlaidBankAccount({ height={iconSize} width={iconSize} additionalStyles={iconStyles} - fill={theme.icon} + displayInDefaultIconColor /> {bankName}
From 1bdc86667e1e097e64ec323420ff54ffcc6fc128 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 27 Dec 2023 15:06:41 +0700 Subject: [PATCH 080/193] add regex to CONST --- src/CONST.ts | 1 + src/libs/Navigation/Navigation.ts | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 812fb4d5335d..3527902dff40 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1406,6 +1406,7 @@ const CONST = { ROUTES: { VALIDATE_LOGIN: /\/v($|(\/\/*))/, UNLINK_LOGIN: /\/u($|(\/\/*))/, + REDUNDANT_SLASHES: /(\/{2,})|(\/$)/g, }, TIME_STARTS_01: /^01:\d{2} [AP]M$/, diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index e62b2e057d44..d1c6459a8aee 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -114,8 +114,9 @@ function getActiveRoute(): string { * @return is active */ function isActiveRoute(routePath: Route): boolean { - // We remove First forward slash from the URL before matching - return getActiveRoute().substring(1) === routePath.replace(/\/{2,}/g, '/').replace(/\/$/, ''); + // We remove First forward slash from the URL + // And redundant (consecutive and trailing) slashes from path before matching + return getActiveRoute().substring(1) === routePath.replace(CONST.REGEX.ROUTES.REDUNDANT_SLASHES, (_, p1) => (p1 ? '/' : '')); } /** From fa4a8e442b22ff07c518d443549bca5861860480 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 27 Dec 2023 15:41:04 +0700 Subject: [PATCH 081/193] fix lint --- src/libs/Navigation/Navigation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index d1c6459a8aee..bc8c998a9f87 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -116,7 +116,7 @@ function getActiveRoute(): string { function isActiveRoute(routePath: Route): boolean { // We remove First forward slash from the URL // And redundant (consecutive and trailing) slashes from path before matching - return getActiveRoute().substring(1) === routePath.replace(CONST.REGEX.ROUTES.REDUNDANT_SLASHES, (_, p1) => (p1 ? '/' : '')); + return getActiveRoute().substring(1) === routePath.replace(CONST.REGEX.ROUTES.REDUNDANT_SLASHES, (match, p1) => (p1 ? '/' : '')); } /** From a6a4cddbe3f0e056c237fc5f1a9912bdad419fee Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Wed, 27 Dec 2023 20:06:30 +0100 Subject: [PATCH 082/193] Use lastMessageText for the task alternate text during search --- src/libs/OptionsListUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 6e84ef4dca27..fe70040d1bd8 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -529,7 +529,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { } else if (result.isChatRoom || result.isPolicyExpenseChat) { result.alternateText = showChatPreviewLine && !forcePolicyNamePreview && lastMessageText ? lastMessageText : subtitle; } else if (result.isTaskReport) { - result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageTextFromReport : Localize.translate(preferredLocale, 'report.noActivityYet'); + result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); } else { result.alternateText = showChatPreviewLine && lastMessageText ? lastMessageText : LocalePhoneNumber.formatPhoneNumber(personalDetail.login); } From 5b6d67a607d1ab51c2c55caea2d5297d57e92d73 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Wed, 27 Dec 2023 13:55:36 -0700 Subject: [PATCH 083/193] Remove deprecated methods and refactor code to be a little cleaner --- src/pages/FlagCommentPage.js | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/pages/FlagCommentPage.js b/src/pages/FlagCommentPage.js index 6c6421593837..22931ec844d1 100644 --- a/src/pages/FlagCommentPage.js +++ b/src/pages/FlagCommentPage.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React, {useCallback} from 'react'; +import React, {useEffect, useRef} from 'react'; import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -13,7 +13,6 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as Report from '@userActions/Report'; import * as Session from '@userActions/Session'; @@ -115,28 +114,31 @@ function FlagCommentPage(props) { }, ]; - const getActionToFlag = useCallback(() => { - let reportAction = props.reportActions[`${props.route.params.reportActionID.toString()}`]; + // The report action that gets flagged is either the report action that was passed in via the route params + // or the parent report action if the report action is a thread, since threads can't be flagged themselves. + const reportActionToFlag = useRef(null); + useEffect(() => { + reportActionToFlag.current = props.reportActions[`${props.route.params.reportActionID}`]; - // Handle threads if needed - if (reportAction === undefined || reportAction.reportActionID === undefined) { - reportAction = ReportActionsUtils.getParentReportAction(props.report); + // If the reportActionToFlag is not a thread, then return early + if (reportActionToFlag.current && reportActionToFlag.current.reportActionID !== undefined) { + return; } - return reportAction; - }, [props.report, props.reportActions, props.route.params.reportActionID]); + // If the reportActionToFlag is a thread, then the action to flag is the parent report action + reportActionToFlag.current = props.parentReportActions[`${props.report.parentReportActionID}`]; + }, [props.report, props.reportActions, props.route.params.reportActionID, props.parentReportActions]); const flagComment = (severity) => { let reportID = getReportID(props.route); - const reportAction = getActionToFlag(); // Handle threads if needed - if (ReportUtils.isChatThread(props.report) && reportAction.reportActionID === ReportActionsUtils.getParentReportAction(props.report).reportActionID) { - reportID = ReportUtils.getParentReport(props.report).reportID; + if (ReportUtils.isChatThread(props.report) && reportActionToFlag.current.reportActionID === props.report.parentReportActionID) { + reportID = props.report.parentReportID; } - if (ReportUtils.canFlagReportAction(reportAction, reportID)) { - Report.flagComment(reportID, reportAction, severity); + if (ReportUtils.canFlagReportAction(reportActionToFlag.current, reportID)) { + Report.flagComment(reportID, reportActionToFlag.current, severity); } Navigation.dismissModal(); @@ -161,7 +163,7 @@ function FlagCommentPage(props) { testID={FlagCommentPage.displayName} > {({safeAreaPaddingBottomStyle}) => ( - + Date: Thu, 28 Dec 2023 17:38:03 +0700 Subject: [PATCH 084/193] fix: Red dot does not appear in LHN conversation when IOU deletion error occurs --- src/libs/actions/IOU.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index d43fefca20bc..068a542838fe 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2488,7 +2488,10 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`, value: { - [reportPreviewAction.reportActionID]: reportPreviewAction, + [reportPreviewAction.reportActionID]: { + ...reportPreviewAction, + errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericDeleteFailureMessage'), + }, }, }, ...(shouldDeleteIOUReport From bb27db19ab118951be25125d29b4d2139f6e13bf Mon Sep 17 00:00:00 2001 From: fvlvte Date: Thu, 28 Dec 2023 12:54:03 +0100 Subject: [PATCH 085/193] Aligned to Onyx types. --- src/libs/actions/User.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 9f67087dba6a..fa106e2f9c37 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -36,7 +36,7 @@ Onyx.connect({ }, }); -let myPersonalDetails: OnyxPersonalDetails | Record = {}; +let myPersonalDetails: OnyxPersonalDetails | Record | null = {}; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (value) => { @@ -537,7 +537,7 @@ function subscribeToUserEvents() { /** * Sync preferredSkinTone with Onyx and Server */ -function updatePreferredSkinTone(skinTone: string) { +function updatePreferredSkinTone(skinTone: number) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.SET, @@ -547,7 +547,7 @@ function updatePreferredSkinTone(skinTone: string) { ]; type UpdatePreferredEmojiSkinToneParams = { - value: string; + value: number; }; const parameters: UpdatePreferredEmojiSkinToneParams = {value: skinTone}; From 1fbc2525c54522a8afa940e31979fb972f3fccb1 Mon Sep 17 00:00:00 2001 From: fvlvte Date: Thu, 28 Dec 2023 12:59:05 +0100 Subject: [PATCH 086/193] Requested change. --- src/libs/actions/User.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index fa106e2f9c37..ff3d5d00a6eb 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -36,7 +36,7 @@ Onyx.connect({ }, }); -let myPersonalDetails: OnyxPersonalDetails | Record | null = {}; +let myPersonalDetails: OnyxEntry | Record = {}; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (value) => { From 47e8c870b7340383b6fac660e0d5ab035fbb1191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Thu, 28 Dec 2023 18:45:01 -0600 Subject: [PATCH 087/193] Pass policyID to check if we're using test TU policyID --- src/libs/actions/TeachersUnite.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/TeachersUnite.js b/src/libs/actions/TeachersUnite.js index ef4fc27b964e..45bd70528c32 100644 --- a/src/libs/actions/TeachersUnite.js +++ b/src/libs/actions/TeachersUnite.js @@ -87,6 +87,7 @@ function addSchoolPrincipal(firstName, partnerUserID, lastName, policyID) { firstName, lastName, partnerUserID, + policyID, reportCreationData: JSON.stringify(reportCreationData), }, { From b73747da526f698bb46e27d8bea4ab2451b70fdd Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Fri, 29 Dec 2023 09:36:29 +0530 Subject: [PATCH 088/193] fix focus on footer link selection --- src/pages/signin/LoginForm/BaseLoginForm.js | 9 +++++++ src/pages/signin/LoginForm/index.js | 25 +++++++++++------ src/pages/signin/LoginForm/index.native.js | 30 ++++++++++++++++++--- src/pages/signin/SignInPage.js | 8 ++++++ src/pages/signin/SignInPageLayout/Footer.js | 18 ++++--------- src/pages/signin/SignInPageLayout/index.js | 4 +-- 6 files changed, 68 insertions(+), 26 deletions(-) diff --git a/src/pages/signin/LoginForm/BaseLoginForm.js b/src/pages/signin/LoginForm/BaseLoginForm.js index de2f2900c58d..e3216aa04618 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.js +++ b/src/pages/signin/LoginForm/BaseLoginForm.js @@ -245,6 +245,15 @@ function LoginForm(props) { isInputFocused() { return input.current && input.current.isFocused(); }, + clearDataAndFocus(clearLogin = true) { + if (!input.current) { + return; + } + if (clearLogin) { + Session.clearSignInData(); + } + input.current.focus(); + }, })); const formErrorText = useMemo(() => (formError ? translate(formError) : ''), [formError, translate]); diff --git a/src/pages/signin/LoginForm/index.js b/src/pages/signin/LoginForm/index.js index 91aba70a866f..8ae31d80b10b 100644 --- a/src/pages/signin/LoginForm/index.js +++ b/src/pages/signin/LoginForm/index.js @@ -10,17 +10,26 @@ const defaultProps = { scrollPageToTop: undefined, }; -function LoginForm(props) { - return ( - - ); +function LoginForm({innerRef, ...props}) { + } LoginForm.displayName = 'LoginForm'; LoginForm.propTypes = propTypes; LoginForm.defaultProps = defaultProps; -export default LoginForm; +const LoginFormWithRef = React.forwardRef((props, ref) => ( + +)); + +LoginFormWithRef.displayName = 'LoginFormWithRef'; + +export default LoginFormWithRef; diff --git a/src/pages/signin/LoginForm/index.native.js b/src/pages/signin/LoginForm/index.native.js index 87258e69165f..caadbde7e172 100644 --- a/src/pages/signin/LoginForm/index.native.js +++ b/src/pages/signin/LoginForm/index.native.js @@ -1,17 +1,23 @@ import PropTypes from 'prop-types'; import React, {useEffect, useRef} from 'react'; +import _ from 'underscore'; import AppStateMonitor from '@libs/AppStateMonitor'; +import refPropTypes from '@components/refPropTypes'; import BaseLoginForm from './BaseLoginForm'; const propTypes = { /** Function used to scroll to the top of the page */ scrollPageToTop: PropTypes.func, + + /** A reference so we can expose scrollPageToTop */ + innerRef: refPropTypes, }; const defaultProps = { scrollPageToTop: undefined, + innerRef: () => {}, }; -function LoginForm(props) { +function LoginForm({innerRef, ...props}) { const loginFormRef = useRef(); const {scrollPageToTop} = props; @@ -36,7 +42,15 @@ function LoginForm(props) { (loginFormRef.current = ref)} + ref={(ref) => { + loginFormRef.current = ref + if (typeof innerRef === "function") { + innerRef(ref); + } else if (innerRef && _.has(innerRef, 'current')) { + // eslint-disable-next-line no-param-reassign + innerRef.current = ref; + } + }} /> ); } @@ -45,4 +59,14 @@ LoginForm.displayName = 'LoginForm'; LoginForm.propTypes = propTypes; LoginForm.defaultProps = defaultProps; -export default LoginForm; +const LoginFormWithRef = React.forwardRef((props, ref) => ( + +)); + +LoginFormWithRef.displayName = 'LoginFormWithRef'; + +export default LoginFormWithRef; diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.js index 8cb0ef9907af..4ee8694cc403 100644 --- a/src/pages/signin/SignInPage.js +++ b/src/pages/signin/SignInPage.js @@ -140,6 +140,7 @@ function SignInPageInner({credentials, account, isInModal, activeClients, prefer const shouldShowSmallScreen = isSmallScreenWidth || isInModal; const safeAreaInsets = useSafeAreaInsets(); const signInPageLayoutRef = useRef(); + const loginFormRef = useRef(); /** This state is needed to keep track of if user is using recovery code instead of 2fa code, * and we need it here since welcome text(`welcomeText`) also depends on it */ const [isUsingRecoveryCode, setIsUsingRecoveryCode] = useState(false); @@ -242,6 +243,11 @@ function SignInPageInner({credentials, account, isInModal, activeClients, prefer Log.warn('SignInPage in unexpected state!'); } + const navigateFocus = () => { + signInPageLayoutRef.current.scrollPageToTop(); + loginFormRef.current.clearDataAndFocus(); + } + return ( // Bottom SafeAreaView is removed so that login screen svg displays correctly on mobile. // The SVG should flow under the Home Indicator on iOS. @@ -253,10 +259,12 @@ function SignInPageInner({credentials, account, isInModal, activeClients, prefer shouldShowWelcomeText={shouldShowWelcomeText} ref={signInPageLayoutRef} shouldShowSmallScreen={shouldShowSmallScreen} + navigateFocus={navigateFocus} > {/* LoginForm must use the isVisible prop. This keeps it mounted, but visually hidden so that password managers can access the values. Conditionally rendering this component will break this feature. */} { - scrollPageToTop(); - - // We need to clear sign in data in case the user is already in the ValidateCodeForm or PasswordForm pages - Session.clearSignInData(); -}; - -const columns = ({scrollPageToTop}) => [ +const columns = ({navigateFocus}) => [ { translationPath: 'footer.features', rows: [ @@ -135,11 +127,11 @@ const columns = ({scrollPageToTop}) => [ translationPath: 'footer.getStarted', rows: [ { - onPress: () => navigateHome(scrollPageToTop), + onPress: () => navigateFocus(), translationPath: 'footer.createAccount', }, { - onPress: () => navigateHome(scrollPageToTop), + onPress: () => navigateFocus(), translationPath: 'footer.logIn', }, ], @@ -172,7 +164,7 @@ function Footer(props) { ) : null} - {_.map(columns({scrollPageToTop: props.scrollPageToTop}), (column, i) => ( + {_.map(columns({navigateFocus: props.navigateFocus}), (column, i) => ( -