From 8768859e04357c3bfd0992c30de0e21c2d7d5605 Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Fri, 20 Sep 2024 09:42:33 +0700 Subject: [PATCH 001/210] fix: deleted workspace with invoices is accessible by url --- src/pages/workspace/WorkspaceInitialPage.tsx | 11 ++++++----- .../workspace/WorkspacePageWithSections.tsx | 19 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index fd7a45e31acb..7f51af6192a5 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -94,6 +94,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyCategories const [isCurrencyModalOpen, setIsCurrencyModalOpen] = useState(false); const hasPolicyCreationError = !!(policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && !isEmptyObject(policy.errors)); const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy?.id}`); + const [currentUserLogin] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email}); const hasSyncError = PolicyUtils.hasSyncError(policy, isConnectionInProgress(connectionSyncProgress, policy)); const waitForNavigate = useWaitForNavigation(); const {singleExecution, isExecuting} = useSingleExecution(); @@ -306,11 +307,11 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyCategories const prevProtectedMenuItems = usePrevious(protectedCollectPolicyMenuItems); const enabledItem = protectedCollectPolicyMenuItems.find((curItem) => !prevProtectedMenuItems.some((prevItem) => curItem.routeName === prevItem.routeName)); + const shouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(policy, isOffline, currentUserLogin), [policy, isOffline, currentUserLogin]); + const prevShouldShowPolicy = usePrevious(shouldShowPolicy); + // We check shouldShowPolicy and prevShouldShowPolicy to prevent the NotFound view from showing right after we delete the workspace // eslint-disable-next-line rulesdir/no-negated-variables - const shouldShowNotFoundPage = - isEmptyObject(policy) || - // We check isPendingDelete for both policy and prevPolicy to prevent the NotFound view from showing right after we delete the workspace - (PolicyUtils.isPendingDeletePolicy(policy) && PolicyUtils.isPendingDeletePolicy(prevPolicy)); + const shouldShowNotFoundPage = isEmptyObject(policy) || (!shouldShowPolicy && !prevShouldShowPolicy); useEffect(() => { if (isEmptyObject(prevPolicy) || PolicyUtils.isPendingDeletePolicy(prevPolicy) || !PolicyUtils.isPendingDeletePolicy(policy)) { @@ -360,7 +361,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyCategories onBackButtonPress={Navigation.dismissModal} onLinkPress={Navigation.resetToHome} shouldShow={shouldShowNotFoundPage} - subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} + subtitleKey={shouldShowPolicy ? 'workspace.common.notAuthorized' : undefined} > fetchData(policyID, shouldSkipVBBACall)}); + const {isOffline} = useNetwork({onReconnect: () => fetchData(policyID, shouldSkipVBBACall)}); + const [currentUserLogin] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email}); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const isLoading = (reimbursementAccount?.isLoading || isPageLoading) ?? true; @@ -148,7 +149,6 @@ function WorkspacePageWithSections({ const {shouldUseNarrowLayout} = useResponsiveLayout(); const firstRender = useRef(showLoadingAsFirstRender); const isFocused = useIsFocused(); - const prevPolicy = usePrevious(policy); useEffect(() => { // Because isLoading is false before merging in Onyx, we need firstRender ref to display loading page as well before isLoading is change to true @@ -161,19 +161,18 @@ function WorkspacePageWithSections({ }, [policyID, shouldSkipVBBACall]), ); + const shouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(policy, isOffline, currentUserLogin), [policy, isOffline, currentUserLogin]); + const prevShouldShowPolicy = usePrevious(shouldShowPolicy); const shouldShow = useMemo(() => { // If the policy object doesn't exist or contains only error data, we shouldn't display it. if (((isEmptyObject(policy) || (Object.keys(policy).length === 1 && !isEmptyObject(policy.errors))) && isEmptyObject(policyDraft)) || shouldShowNotFoundPage) { return true; } - // We check isPendingDelete for both policy and prevPolicy to prevent the NotFound view from showing right after we delete the workspace - return ( - (!isEmptyObject(policy) && !PolicyUtils.isPolicyAdmin(policy) && !shouldShowNonAdmin) || - (PolicyUtils.isPendingDeletePolicy(policy) && PolicyUtils.isPendingDeletePolicy(prevPolicy)) - ); + // We check shouldShowPolicy and prevShouldShowPolicy to prevent the NotFound view from showing right after we delete the workspace + return (!isEmptyObject(policy) && !PolicyUtils.isPolicyAdmin(policy) && !shouldShowNonAdmin) || (!shouldShowPolicy && !prevShouldShowPolicy); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [policy, shouldShowNonAdmin]); + }, [policy, shouldShowNonAdmin, shouldShowPolicy, prevShouldShowPolicy]); return ( Date: Sat, 28 Sep 2024 01:33:25 +0700 Subject: [PATCH 002/210] use prevPolicy --- src/pages/workspace/WorkspaceInitialPage.tsx | 2 +- src/pages/workspace/WorkspacePageWithSections.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 33330be8d9fb..156282c9f281 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -298,7 +298,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac const enabledItem = protectedCollectPolicyMenuItems.find((curItem) => !prevProtectedMenuItems.some((prevItem) => curItem.routeName === prevItem.routeName)); const shouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(policy, isOffline, currentUserLogin), [policy, isOffline, currentUserLogin]); - const prevShouldShowPolicy = usePrevious(shouldShowPolicy); + const prevShouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(prevPolicy, isOffline, currentUserLogin), [prevPolicy, isOffline, currentUserLogin]); // We check shouldShowPolicy and prevShouldShowPolicy to prevent the NotFound view from showing right after we delete the workspace // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = isEmptyObject(policy) || (!shouldShowPolicy && !prevShouldShowPolicy); diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index fec440da970a..cf473ebec0ba 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -139,6 +139,7 @@ function WorkspacePageWithSections({ const {shouldUseNarrowLayout} = useResponsiveLayout(); const firstRender = useRef(showLoadingAsFirstRender); const isFocused = useIsFocused(); + const prevPolicy = usePrevious(policy); useEffect(() => { // Because isLoading is false before merging in Onyx, we need firstRender ref to display loading page as well before isLoading is change to true @@ -152,7 +153,7 @@ function WorkspacePageWithSections({ ); const shouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(policy, isOffline, currentUserLogin), [policy, isOffline, currentUserLogin]); - const prevShouldShowPolicy = usePrevious(shouldShowPolicy); + const prevShouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(prevPolicy, isOffline, currentUserLogin), [prevPolicy, isOffline, currentUserLogin]); const shouldShow = useMemo(() => { // If the policy object doesn't exist or contains only error data, we shouldn't display it. if (((isEmptyObject(policy) || (Object.keys(policy).length === 1 && !isEmptyObject(policy.errors))) && isEmptyObject(policyDraft)) || shouldShowNotFoundPage) { From 093f361d0cee939407b5872c889ddc565ff85881 Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Sat, 28 Sep 2024 01:33:41 +0700 Subject: [PATCH 003/210] remove redundant changes --- src/pages/workspace/WorkspacePageWithSections.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index cf473ebec0ba..26175c9793d9 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -140,7 +140,6 @@ function WorkspacePageWithSections({ const firstRender = useRef(showLoadingAsFirstRender); const isFocused = useIsFocused(); const prevPolicy = usePrevious(policy); - useEffect(() => { // Because isLoading is false before merging in Onyx, we need firstRender ref to display loading page as well before isLoading is change to true firstRender.current = false; From ac0f5b90eda70154b6612310766f1cc394678458 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Wed, 2 Oct 2024 05:55:42 +0530 Subject: [PATCH 004/210] =?UTF-8?q?feat:=20Implement=20to=20use=20a=20?= =?UTF-8?q?=F0=9F=91=8Dicon=20next=20to=20approved=20report=20preview.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: krishna2323 --- .../ReportActionItem/ReportPreview.tsx | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 87f06f43d82a..94755ebb6944 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -145,12 +145,18 @@ function ReportPreview({ transform: [{scale: checkMarkScale.value}], })); + const isApproved = ReportUtils.isReportApproved(iouReport, action); + const thumbsUpScale = useSharedValue(isApproved ? 1 : 0.25); + const thumbsUpStyle = useAnimatedStyle(() => ({ + ...styles.defaultCheckmarkWrapper, + transform: [{scale: thumbsUpScale.value}], + })); + const moneyRequestComment = action?.childLastMoneyRequestComment ?? ''; const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(chatReport); const isInvoiceRoom = ReportUtils.isInvoiceRoom(chatReport); const isOpenExpenseReport = isPolicyExpenseChat && ReportUtils.isOpenExpenseReport(iouReport); - const isApproved = ReportUtils.isReportApproved(iouReport, action); const canAllowSettlement = ReportUtils.hasUpdatedTotal(iouReport, policy); const numberOfRequests = allTransactions.length; const transactionsWithReceipts = ReportUtils.getTransactionsWithReceipts(iouReportID); @@ -433,6 +439,14 @@ function ReportPreview({ } }, [isPaidAnimationRunning, iouSettled, checkMarkScale]); + useEffect(() => { + if (!isApproved) { + return; + } + + thumbsUpScale.value = withSpring(1, {duration: 200}); + }, [isApproved, thumbsUpScale]); + return ( )} + {isApproved && ( + + + + )} {shouldShowSubtitle && supportText && ( From 51842c47fb01dc9373f19ef7537aec5c20d88a35 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sun, 20 Oct 2024 18:03:09 +0530 Subject: [PATCH 005/210] minor updates. Signed-off-by: krishna2323 --- src/components/ReportActionItem/ReportPreview.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 6c87d9f3d559..5097d34111c7 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -139,7 +139,7 @@ function ReportPreview({ const checkMarkScale = useSharedValue(iouSettled ? 1 : 0); const isApproved = ReportUtils.isReportApproved(iouReport, action); - const thumbsUpScale = useSharedValue(isApproved ? 1 : 0.25); + const thumbsUpScale = useSharedValue(isApproved ? 1 : 0); const thumbsUpStyle = useAnimatedStyle(() => ({ ...styles.defaultCheckmarkWrapper, transform: [{scale: thumbsUpScale.value}], @@ -483,7 +483,7 @@ function ReportPreview({ - {previewMessage} + {previewMessage} {shouldShowRBR && ( Date: Sun, 20 Oct 2024 18:07:14 +0530 Subject: [PATCH 006/210] make animation subtle. Signed-off-by: krishna2323 --- src/components/ReportActionItem/ReportPreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 5097d34111c7..9ebcf792ed5e 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -139,7 +139,7 @@ function ReportPreview({ const checkMarkScale = useSharedValue(iouSettled ? 1 : 0); const isApproved = ReportUtils.isReportApproved(iouReport, action); - const thumbsUpScale = useSharedValue(isApproved ? 1 : 0); + const thumbsUpScale = useSharedValue(isApproved ? 1 : 0.25); const thumbsUpStyle = useAnimatedStyle(() => ({ ...styles.defaultCheckmarkWrapper, transform: [{scale: thumbsUpScale.value}], From eee882cb68a3299a1d348997db5bbebea1cf1bcd Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 22 Oct 2024 11:08:17 +0700 Subject: [PATCH 007/210] fix: show video control when video ended --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 012537b75108..1970a2692e90 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -70,6 +70,7 @@ function BaseVideoPlayer({ const [position, setPosition] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const [isLoading, setIsLoading] = useState(true); + const [isEnded, setIsEnded] = useState(false); const [isBuffering, setIsBuffering] = useState(true); // we add "#t=0.001" at the end of the URL to skip first milisecond of the video and always be able to show proper video preview when video is paused at the beginning const [sourceURL] = useState(VideoUtils.addSkipTimeTagToURL(url.includes('blob:') || url.includes('file:///') ? url : addEncryptedAuthTokenToURL(url), 0.001)); @@ -199,6 +200,8 @@ function BaseVideoPlayer({ return; } + setIsEnded(status.didJustFinish && !status.isLooping); + if (prevIsMutedRef.current && prevVolumeRef.current === 0 && !status.isMuted) { updateVolume(0.25); } @@ -456,7 +459,7 @@ function BaseVideoPlayer({ {((isLoading && !isOffline) || (isBuffering && !isPlaying)) && } {isLoading && (isOffline || !isBuffering) && } - {controlStatusState !== CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE && !isLoading && (isPopoverVisible || isHovered || canUseTouchScreen) && ( + {controlStatusState !== CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE && !isLoading && (isPopoverVisible || isHovered || canUseTouchScreen || isEnded) && ( Date: Tue, 22 Oct 2024 11:43:00 +0700 Subject: [PATCH 008/210] fix: sorted suggestion emoji --- src/libs/EmojiUtils.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 7c042bbefe67..8f901ac0ed74 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -1,4 +1,5 @@ import {Str} from 'expensify-common'; +import lodashSortBy from 'lodash/sortBy'; import Onyx from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import * as Emojis from '@assets/emojis'; @@ -424,7 +425,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO for (const node of nodes) { if (node.metaData?.code && !matching.find((obj) => obj.name === node.name)) { if (matching.length === limit) { - return matching; + return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); } matching.push({code: node.metaData.code, name: node.name, types: node.metaData.types}); } @@ -434,7 +435,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO } for (const suggestion of suggestions) { if (matching.length === limit) { - return matching; + return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); } if (!matching.find((obj) => obj.name === suggestion.name)) { @@ -442,7 +443,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO } } } - return matching; + return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); } /** From 596f16f64f64b75595f29efb76e0cea93c7d891e Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 22 Oct 2024 15:34:55 +0700 Subject: [PATCH 009/210] fix test --- src/libs/EmojiUtils.ts | 2 +- src/libs/Firebase/index.web.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 8f901ac0ed74..bf5d611b1a73 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -425,7 +425,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO for (const node of nodes) { if (node.metaData?.code && !matching.find((obj) => obj.name === node.name)) { if (matching.length === limit) { - return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); + return matching; } matching.push({code: node.metaData.code, name: node.name, types: node.metaData.types}); } diff --git a/src/libs/Firebase/index.web.ts b/src/libs/Firebase/index.web.ts index 2d42154d3c26..d643dc48ab27 100644 --- a/src/libs/Firebase/index.web.ts +++ b/src/libs/Firebase/index.web.ts @@ -21,9 +21,9 @@ const startTrace: StartTrace = (customEventName) => { const attributes = utils.getAttributes(); - Object.entries(attributes).forEach(([name, value]) => { - perfTrace.putAttribute(name, value); - }); + // Object.entries(attributes).forEach(([name, value]) => { + // perfTrace.putAttribute(name, value); + // }); traceMap[customEventName] = { trace: perfTrace, From 313d716179a0f588b114cefab506ef5043a64f0d Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 22 Oct 2024 15:35:19 +0700 Subject: [PATCH 010/210] chore --- src/libs/Firebase/index.web.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/Firebase/index.web.ts b/src/libs/Firebase/index.web.ts index d643dc48ab27..2d42154d3c26 100644 --- a/src/libs/Firebase/index.web.ts +++ b/src/libs/Firebase/index.web.ts @@ -21,9 +21,9 @@ const startTrace: StartTrace = (customEventName) => { const attributes = utils.getAttributes(); - // Object.entries(attributes).forEach(([name, value]) => { - // perfTrace.putAttribute(name, value); - // }); + Object.entries(attributes).forEach(([name, value]) => { + perfTrace.putAttribute(name, value); + }); traceMap[customEventName] = { trace: perfTrace, From 8e6b36067b820412f56f7d6a95c2315affbd08d3 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 22 Oct 2024 15:47:59 +0700 Subject: [PATCH 011/210] fix test --- src/libs/EmojiUtils.ts | 2 +- src/libs/Firebase/index.web.ts | 6 +++--- tests/unit/EmojiTest.ts | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index bf5d611b1a73..8f901ac0ed74 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -425,7 +425,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO for (const node of nodes) { if (node.metaData?.code && !matching.find((obj) => obj.name === node.name)) { if (matching.length === limit) { - return matching; + return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); } matching.push({code: node.metaData.code, name: node.name, types: node.metaData.types}); } diff --git a/src/libs/Firebase/index.web.ts b/src/libs/Firebase/index.web.ts index 2d42154d3c26..d643dc48ab27 100644 --- a/src/libs/Firebase/index.web.ts +++ b/src/libs/Firebase/index.web.ts @@ -21,9 +21,9 @@ const startTrace: StartTrace = (customEventName) => { const attributes = utils.getAttributes(); - Object.entries(attributes).forEach(([name, value]) => { - perfTrace.putAttribute(name, value); - }); + // Object.entries(attributes).forEach(([name, value]) => { + // perfTrace.putAttribute(name, value); + // }); traceMap[customEventName] = { trace: perfTrace, diff --git a/tests/unit/EmojiTest.ts b/tests/unit/EmojiTest.ts index c96228b49fbc..2033085c5694 100644 --- a/tests/unit/EmojiTest.ts +++ b/tests/unit/EmojiTest.ts @@ -154,6 +154,11 @@ describe('EmojiTest', () => { it('correct suggests emojis accounting for keywords', () => { const thumbEmojisEn: Emoji[] = [ + { + name: 'hand_with_index_finger_and_thumb_crossed', + code: '🫰', + types: ['🫰🏿', '🫰🏾', '🫰🏽', '🫰🏼', '🫰🏻'], + }, { code: '👍', name: '+1', @@ -164,11 +169,6 @@ describe('EmojiTest', () => { name: '-1', types: ['👎🏿', '👎🏾', '👎🏽', '👎🏼', '👎🏻'], }, - { - name: 'hand_with_index_finger_and_thumb_crossed', - code: '🫰', - types: ['🫰🏿', '🫰🏾', '🫰🏽', '🫰🏼', '🫰🏻'], - }, ]; const thumbEmojisEs: Emoji[] = [ From 7cacc00521af3c4b7bd97df155db2fab95e075da Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 22 Oct 2024 15:49:51 +0700 Subject: [PATCH 012/210] fix lint --- src/libs/Firebase/index.web.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/Firebase/index.web.ts b/src/libs/Firebase/index.web.ts index d643dc48ab27..2d42154d3c26 100644 --- a/src/libs/Firebase/index.web.ts +++ b/src/libs/Firebase/index.web.ts @@ -21,9 +21,9 @@ const startTrace: StartTrace = (customEventName) => { const attributes = utils.getAttributes(); - // Object.entries(attributes).forEach(([name, value]) => { - // perfTrace.putAttribute(name, value); - // }); + Object.entries(attributes).forEach(([name, value]) => { + perfTrace.putAttribute(name, value); + }); traceMap[customEventName] = { trace: perfTrace, From 791649a506ef26c28ce729be918f762468ef350c Mon Sep 17 00:00:00 2001 From: truph01 Date: Fri, 25 Oct 2024 06:14:34 +0700 Subject: [PATCH 013/210] fix: incorrect message displayed when deleting workspace --- src/pages/workspace/WorkspacesListPage.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index 82503134b09e..f3cbdd388e21 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -124,7 +124,11 @@ function WorkspacesListPage() { const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policyIDToDelete ?? '-1'); const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`); const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`); - const hasCardFeedOrExpensifyCard = !isEmptyObject(cardFeeds) || !isEmptyObject(cardsList); + const hasCardFeedOrExpensifyCard = + !isEmptyObject(cardFeeds) || + !isEmptyObject(cardsList) || + ((PolicyUtils.getPolicy(policyIDToDelete)?.areExpensifyCardsEnabled || PolicyUtils.getPolicy(policyIDToDelete)?.areCompanyCardsEnabled) && + PolicyUtils.getPolicy(policyIDToDelete)?.workspaceAccountID); const confirmDeleteAndHideModal = () => { if (!policyIDToDelete || !policyNameToDelete) { From cf582b458f571ed64722c10056b3b86fd797ed0f Mon Sep 17 00:00:00 2001 From: truph01 Date: Wed, 30 Oct 2024 08:15:52 +0700 Subject: [PATCH 014/210] fix: Update second Allow location access modal on web --- src/components/LocationPermissionModal/index.tsx | 15 ++++++++++----- src/components/LocationPermissionModal/types.ts | 4 +--- src/languages/en.ts | 1 + src/languages/es.ts | 1 + 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/components/LocationPermissionModal/index.tsx b/src/components/LocationPermissionModal/index.tsx index 0e500a9b7cc4..fcb7cdacbd4c 100644 --- a/src/components/LocationPermissionModal/index.tsx +++ b/src/components/LocationPermissionModal/index.tsx @@ -39,10 +39,10 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe if (hasError) { if (Linking.openSettings) { Linking.openSettings(); + } else { + onDeny?.(); } setShowModal(false); - setHasError(false); - resetPermissionFlow(); return; } cb(); @@ -54,7 +54,7 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe if (status === RESULTS.GRANTED || status === RESULTS.LIMITED) { onGrant(); } else { - onDeny(status); + onDeny(); } }) .finally(() => { @@ -64,7 +64,7 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe }); const skipLocationPermission = () => { - onDeny(RESULTS.DENIED); + onDeny(); setShowModal(false); setHasError(false); }; @@ -83,13 +83,17 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe }; return ( { + setHasError(false); + resetPermissionFlow(); + }} isVisible={showModal} onConfirm={grantLocationPermission} onCancel={skipLocationPermission} onBackdropPress={closeModal} confirmText={getConfirmText()} cancelText={translate('common.notNow')} - prompt={translate(hasError ? 'receipt.locationErrorMessage' : 'receipt.locationAccessMessage')} promptStyles={[styles.textLabelSupportingEmptyValue, styles.mb4]} title={translate(hasError ? 'receipt.locationErrorTitle' : 'receipt.locationAccessTitle')} titleContainerStyles={[styles.mt2, styles.mb0]} @@ -100,6 +104,7 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe iconHeight={120} shouldCenterIcon shouldReverseStackedButtons + prompt={translate(hasError ? (isWeb ? 'receipt.allowLocationFromSetting' : 'receipt.locationErrorMessage') : 'receipt.locationAccessMessage')} /> ); } diff --git a/src/components/LocationPermissionModal/types.ts b/src/components/LocationPermissionModal/types.ts index ec603bfdb8c1..eb18e1d71c13 100644 --- a/src/components/LocationPermissionModal/types.ts +++ b/src/components/LocationPermissionModal/types.ts @@ -1,11 +1,9 @@ -import type {PermissionStatus} from 'react-native-permissions'; - type LocationPermissionModalProps = { /** A callback to call when the permission has been granted */ onGrant: () => void; /** A callback to call when the permission has been denied */ - onDeny: (permission: PermissionStatus) => void; + onDeny: () => void; /** Should start the permission flow? */ startPermissionFlow: boolean; diff --git a/src/languages/en.ts b/src/languages/en.ts index 4c68da68ede6..3b2563e73dda 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -786,6 +786,7 @@ const translations = { locationAccessMessage: 'Location access helps us keep your timezone and currency accurate wherever you go.', locationErrorTitle: 'Allow location access', locationErrorMessage: 'Location access helps us keep your timezone and currency accurate wherever you go.', + allowLocationFromSetting: `Location access helps us keep your timezone and currency accurate wherever you go. Please allow location access from your device's permission settings.`, dropTitle: 'Let it go', dropMessage: 'Drop your file here', flash: 'flash', diff --git a/src/languages/es.ts b/src/languages/es.ts index 4aed242db5fa..d9b0e96e0390 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -779,6 +779,7 @@ const translations = { locationAccessMessage: 'El acceso a la ubicación nos ayuda a mantener tu zona horaria y moneda precisas dondequiera que vayas.', locationErrorTitle: 'Permitir acceso a la ubicación', locationErrorMessage: 'El acceso a la ubicación nos ayuda a mantener tu zona horaria y moneda precisas dondequiera que vayas.', + allowLocationFromSetting: `Location access helps us keep your timezone and currency accurate wherever you go. Please allow location access from your device's permission settings.`, cameraErrorMessage: 'Se ha producido un error al hacer una foto. Por favor, inténtalo de nuevo.', dropTitle: 'Suéltalo', dropMessage: 'Suelta tu archivo aquí', From 379f9c56197048cf04a6d6d74d75f2561c24a6ad Mon Sep 17 00:00:00 2001 From: truph01 Date: Wed, 30 Oct 2024 08:23:17 +0700 Subject: [PATCH 015/210] fix: type --- src/components/LocationPermissionModal/index.android.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/LocationPermissionModal/index.android.tsx b/src/components/LocationPermissionModal/index.android.tsx index 30896cf37084..6e4e6877c540 100644 --- a/src/components/LocationPermissionModal/index.android.tsx +++ b/src/components/LocationPermissionModal/index.android.tsx @@ -50,7 +50,7 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe setHasError(true); return; } else { - onDeny(status); + onDeny(); } setShowModal(false); setHasError(false); @@ -58,7 +58,7 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe }); const skipLocationPermission = () => { - onDeny(RESULTS.DENIED); + onDeny(); setShowModal(false); setHasError(false); }; From a34d504ff4c0e33ea26f5a2a109e34539a124c28 Mon Sep 17 00:00:00 2001 From: truph01 Date: Wed, 30 Oct 2024 08:37:00 +0700 Subject: [PATCH 016/210] fix: lint --- src/components/LocationPermissionModal/index.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/LocationPermissionModal/index.tsx b/src/components/LocationPermissionModal/index.tsx index fcb7cdacbd4c..45e3f5b22d1b 100644 --- a/src/components/LocationPermissionModal/index.tsx +++ b/src/components/LocationPermissionModal/index.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import {Linking} from 'react-native'; import {RESULTS} from 'react-native-permissions'; import ConfirmModal from '@components/ConfirmModal'; @@ -81,6 +81,9 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe setShowModal(false); resetPermissionFlow(); }; + + const locationErrorMessage = useMemo(() => (isWeb ? 'receipt.allowLocationFromSetting' : 'receipt.locationErrorMessage'), [isWeb]); + return ( ); } From c0a7a2f31e8b5c5a943720f9cfb045c18a876121 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 30 Oct 2024 16:50:58 +0700 Subject: [PATCH 017/210] create a function to remove dup code --- src/libs/EmojiUtils.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 8f901ac0ed74..f9fb5f226280 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -24,6 +24,8 @@ const findEmojiByName = (name: string): Emoji => Emojis.emojiNameTable[name]; const findEmojiByCode = (code: string): Emoji => Emojis.emojiCodeTableWithSkinTones[code]; +const sortByName = (emoji: Emoji, emojiData: RegExpMatchArray) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1)); + let frequentlyUsedEmojis: FrequentlyUsedEmoji[] = []; Onyx.connect({ key: ONYXKEYS.FREQUENTLY_USED_EMOJIS, @@ -425,7 +427,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO for (const node of nodes) { if (node.metaData?.code && !matching.find((obj) => obj.name === node.name)) { if (matching.length === limit) { - return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); + return lodashSortBy(matching, (emoji) => sortByName(emoji, emojiData)); } matching.push({code: node.metaData.code, name: node.name, types: node.metaData.types}); } @@ -435,7 +437,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO } for (const suggestion of suggestions) { if (matching.length === limit) { - return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); + return lodashSortBy(matching, (emoji) => sortByName(emoji, emojiData)); } if (!matching.find((obj) => obj.name === suggestion.name)) { @@ -443,7 +445,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO } } } - return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); + return lodashSortBy(matching, (emoji) => sortByName(emoji, emojiData)); } /** From 4d20eaa6e1b42b0d4e94f21e603cfe0cd42a3fc0 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 30 Oct 2024 23:35:13 +0700 Subject: [PATCH 018/210] fix show video controler in native --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 1970a2692e90..fad641e696ae 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -96,6 +96,7 @@ function BaseVideoPlayer({ const shouldUseNewRate = typeof source === 'number' || !source || source.uri !== sourceURL; const togglePlayCurrentVideo = useCallback(() => { + setIsEnded(false); videoResumeTryNumberRef.current = 0; if (!isCurrentlyURLSet) { updateCurrentlyPlayingURL(url); @@ -107,9 +108,12 @@ function BaseVideoPlayer({ }, [isCurrentlyURLSet, isPlaying, pauseVideo, playVideo, updateCurrentlyPlayingURL, url, videoResumeTryNumberRef]); const hideControl = useCallback(() => { + if (isEnded) { + return; + } // eslint-disable-next-line react-compiler/react-compiler controlsOpacity.value = withTiming(0, {duration: 500}, () => runOnJS(setControlStatusState)(CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE)); - }, [controlsOpacity]); + }, [controlsOpacity, isEnded]); const debouncedHideControl = useMemo(() => debounce(hideControl, 1500), [hideControl]); useEffect(() => { @@ -199,8 +203,11 @@ function BaseVideoPlayer({ onPlaybackStatusUpdate?.(status); return; } - - setIsEnded(status.didJustFinish && !status.isLooping); + if (status.didJustFinish) { + setIsEnded(status.didJustFinish && !status.isLooping); + setControlStatusState(CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW); + controlsOpacity.value = 1; + } if (prevIsMutedRef.current && prevVolumeRef.current === 0 && !status.isMuted) { updateVolume(0.25); From 3ff8ba053cc4f2c732f93617e7f26d2f462b955e Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sat, 2 Nov 2024 01:13:20 +0530 Subject: [PATCH 019/210] feat: animation after approving an expense. Signed-off-by: krishna2323 --- src/components/ProcessMoneyReportHoldMenu.tsx | 3 +++ .../ReportActionItem/ReportPreview.tsx | 23 +++++++++++++--- .../AnimatedSettlementButton.tsx | 27 +++++++++++++++---- 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx index 3d6ad9006dc5..ba320a594135 100644 --- a/src/components/ProcessMoneyReportHoldMenu.tsx +++ b/src/components/ProcessMoneyReportHoldMenu.tsx @@ -66,6 +66,9 @@ function ProcessMoneyReportHoldMenu({ const onSubmit = (full: boolean) => { if (isApprove) { + if (startAnimation) { + startAnimation(); + } IOU.approveMoneyRequest(moneyRequestReport, full); if (!full && isLinkedTransactionHeld(Navigation.getTopmostReportActionId() ?? '-1', moneyRequestReport?.reportID ?? '')) { Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(moneyRequestReport?.reportID ?? '')); diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index dc4e396ee75e..c2be6db0a4aa 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -120,6 +120,7 @@ function ReportPreview({ ); const [isPaidAnimationRunning, setIsPaidAnimationRunning] = useState(false); + const [isApprovedAnimationRunning, setIsApprovedAnimationRunning] = useState(false); const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false); const [requestType, setRequestType] = useState(); const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(iouReport, policy); @@ -200,11 +201,18 @@ function ReportPreview({ const {isDelegateAccessRestricted, delegatorEmail} = useDelegateUserDetails(); const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); - const stopAnimation = useCallback(() => setIsPaidAnimationRunning(false), []); + const stopAnimation = useCallback(() => { + setIsPaidAnimationRunning(false); + setIsApprovedAnimationRunning(false); + }, []); const startAnimation = useCallback(() => { setIsPaidAnimationRunning(true); HapticFeedback.longPress(); }, []); + const startApprovedAnimation = useCallback(() => { + setIsApprovedAnimationRunning(true); + HapticFeedback.longPress(); + }, []); const confirmPayment = useCallback( (type: PaymentMethodType | undefined, payAsBusiness?: boolean) => { if (!type) { @@ -236,6 +244,8 @@ function ReportPreview({ } else if (ReportUtils.hasHeldExpenses(iouReport?.reportID)) { setIsHoldMenuVisible(true); } else { + setIsApprovedAnimationRunning(true); + HapticFeedback.longPress(); IOU.approveMoneyRequest(iouReport, true); } }; @@ -427,7 +437,7 @@ function ReportPreview({ const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin && ReportUtils.canBeExported(iouReport); useEffect(() => { - if (!isPaidAnimationRunning) { + if (!isPaidAnimationRunning || isApprovedAnimationRunning) { return; } @@ -556,6 +566,7 @@ function ReportPreview({ { + if (requestType === 'approve') { + startApprovedAnimation(); + } else { + startAnimation(); + } + }} /> )} diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx index 5de528d741a2..375e76a33582 100644 --- a/src/components/SettlementButton/AnimatedSettlementButton.tsx +++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx @@ -11,9 +11,10 @@ import type SettlementButtonProps from './types'; type AnimatedSettlementButtonProps = SettlementButtonProps & { isPaidAnimationRunning: boolean; onAnimationFinish: () => void; + isApprovedAnimationRunning: boolean; }; -function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, isDisabled, ...settlementButtonProps}: AnimatedSettlementButtonProps) { +function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, isApprovedAnimationRunning, isDisabled, ...settlementButtonProps}: AnimatedSettlementButtonProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const buttonScale = useSharedValue(1); @@ -38,7 +39,7 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is overflow: 'hidden', marginTop: buttonMarginTop.value, })); - const buttonDisabledStyle = isPaidAnimationRunning + const buttonDisabledStyle = isApprovedAnimationRunning ? { opacity: 1, ...styles.cursorDefault, @@ -56,7 +57,7 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is }, [buttonScale, buttonOpacity, paymentCompleteTextScale, paymentCompleteTextOpacity, height, buttonMarginTop, styles.expenseAndReportPreviewTextButtonContainer.gap]); useEffect(() => { - if (!isPaidAnimationRunning) { + if (!isApprovedAnimationRunning && !isPaidAnimationRunning) { resetAnimation(); return; } @@ -73,7 +74,18 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is ); buttonMarginTop.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})); paymentCompleteTextOpacity.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})); - }, [isPaidAnimationRunning, onAnimationFinish, buttonOpacity, buttonScale, height, paymentCompleteTextOpacity, paymentCompleteTextScale, buttonMarginTop, resetAnimation]); + }, [ + isPaidAnimationRunning, + isApprovedAnimationRunning, + onAnimationFinish, + buttonOpacity, + buttonScale, + height, + paymentCompleteTextOpacity, + paymentCompleteTextScale, + buttonMarginTop, + resetAnimation, + ]); return ( @@ -82,11 +94,16 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is {translate('iou.paymentComplete')} )} + {isApprovedAnimationRunning && ( + + {translate('iou.approved')} + + )} From 31ddb55edabc6b07126cd908af2631fc63633e8e Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sat, 2 Nov 2024 01:38:27 +0530 Subject: [PATCH 020/210] minor fixes. Signed-off-by: krishna2323 --- .../ReportActionItem/ReportPreview.tsx | 6 +++- .../AnimatedSettlementButton.tsx | 34 ++++++++++++++----- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index c2be6db0a4aa..27068ff2f80f 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -203,6 +203,9 @@ function ReportPreview({ const stopAnimation = useCallback(() => { setIsPaidAnimationRunning(false); + }, []); + + const stopApprovedAnimation = useCallback(() => { setIsApprovedAnimationRunning(false); }, []); const startAnimation = useCallback(() => { @@ -567,6 +570,7 @@ function ReportPreview({ onlyShowPayElsewhere={onlyShowPayElsewhere} isPaidAnimationRunning={isPaidAnimationRunning} isApprovedAnimationRunning={isApprovedAnimationRunning} + onApprovedAnimationFinish={stopApprovedAnimation} onAnimationFinish={stopAnimation} formattedAmount={getSettlementAmount() ?? ''} currency={iouReport?.currency} @@ -636,7 +640,7 @@ function ReportPreview({ moneyRequestReport={iouReport} transactionCount={numberOfRequests} startAnimation={() => { - if (requestType === 'approve') { + if (requestType === CONST.IOU.REPORT_ACTION_TYPE.APPROVE) { startApprovedAnimation(); } else { startAnimation(); diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx index 375e76a33582..f8205a1b1ab0 100644 --- a/src/components/SettlementButton/AnimatedSettlementButton.tsx +++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx @@ -12,9 +12,17 @@ type AnimatedSettlementButtonProps = SettlementButtonProps & { isPaidAnimationRunning: boolean; onAnimationFinish: () => void; isApprovedAnimationRunning: boolean; + onApprovedAnimationFinish: () => void; }; -function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, isApprovedAnimationRunning, isDisabled, ...settlementButtonProps}: AnimatedSettlementButtonProps) { +function AnimatedSettlementButton({ + isPaidAnimationRunning, + onAnimationFinish, + isApprovedAnimationRunning, + onApprovedAnimationFinish, + isDisabled, + ...settlementButtonProps +}: AnimatedSettlementButtonProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const buttonScale = useSharedValue(1); @@ -39,12 +47,13 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is overflow: 'hidden', marginTop: buttonMarginTop.value, })); - const buttonDisabledStyle = isApprovedAnimationRunning - ? { - opacity: 1, - ...styles.cursorDefault, - } - : undefined; + const buttonDisabledStyle = + isPaidAnimationRunning || isApprovedAnimationRunning + ? { + opacity: 1, + ...styles.cursorDefault, + } + : undefined; const resetAnimation = useCallback(() => { // eslint-disable-next-line react-compiler/react-compiler @@ -70,7 +79,15 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is const totalDelay = CONST.ANIMATION_PAID_DURATION + CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY; height.value = withDelay( totalDelay, - withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}, () => runOnJS(onAnimationFinish)()), + withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}, () => + runOnJS(() => { + if (isApprovedAnimationRunning) { + onApprovedAnimationFinish(); + } else { + onAnimationFinish(); + } + })(), + ), ); buttonMarginTop.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})); paymentCompleteTextOpacity.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})); @@ -85,6 +102,7 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is paymentCompleteTextScale, buttonMarginTop, resetAnimation, + onApprovedAnimationFinish, ]); return ( From a8154c46aaadca4f78d3bdf1747177ef4927eea9 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sat, 2 Nov 2024 14:31:12 +0530 Subject: [PATCH 021/210] feat: Update Default / Custom Workspace Invite Behavior. Signed-off-by: krishna2323 --- .../workspace/WorkspaceInviteMessagePage.tsx | 16 ++++++++-------- src/pages/workspace/WorkspaceInvitePage.tsx | 2 ++ src/pages/workspace/WorkspaceMembersPage.tsx | 2 ++ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index 89cab963fb43..f0317284e8f9 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -1,5 +1,4 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import lodashDebounce from 'lodash/debounce'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {Keyboard, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; @@ -20,6 +19,7 @@ import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useViewportOffsetTop from '@hooks/useViewportOffsetTop'; +import * as FormActions from '@libs/actions/FormActions'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Parser from '@libs/Parser'; @@ -48,6 +48,7 @@ type WorkspaceInviteMessagePageProps = WithPolicyAndFullscreenLoadingProps & function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: WorkspaceInviteMessagePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const [formData] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM_DRAFT); const viewportOffsetTop = useViewportOffsetTop(); const [welcomeNote, setWelcomeNote] = useState(); @@ -66,6 +67,8 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: const getDefaultWelcomeNote = useCallback(() => { return ( + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + formData?.[INPUT_IDS.WELCOME_MESSAGE] || // workspaceInviteMessageDraft can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing workspaceInviteMessageDraft || @@ -76,7 +79,7 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: workspaceName: policy?.name ?? '', }) ); - }, [workspaceInviteMessageDraft, policy, translate]); + }, [workspaceInviteMessageDraft, policy, translate, formData]); useEffect(() => { if (isOnyxLoading) { @@ -93,16 +96,13 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isOnyxLoading]); - const debouncedSaveDraft = lodashDebounce((newDraft: string | null) => { - Policy.setWorkspaceInviteMessageDraft(route.params.policyID, newDraft); - }); - const sendInvitation = () => { Keyboard.dismiss(); const policyMemberAccountIDs = Object.values(PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList, false, false)); // Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details Member.addMembersToWorkspace(invitedEmailsToAccountIDsDraft ?? {}, `${welcomeNoteSubject}\n\n${welcomeNote}`, route.params.policyID, policyMemberAccountIDs); - debouncedSaveDraft(null); + Policy.setWorkspaceInviteMessageDraft(route.params.policyID, welcomeNote ?? null); + FormActions.clearDraftValues(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM); Navigation.dismissModal(); }; @@ -194,7 +194,6 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: value={welcomeNote} onChangeText={(text: string) => { setWelcomeNote(text); - debouncedSaveDraft(text); }} ref={(element: AnimatedTextInputRef) => { if (!element) { @@ -205,6 +204,7 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: } inputCallbackRef(element); }} + shouldSaveDraft /> diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index ad48d15aa9df..bfa13ef3f65d 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -16,6 +16,7 @@ import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as FormActions from '@libs/actions/FormActions'; import * as ReportActions from '@libs/actions/Report'; import {READ_COMMANDS} from '@libs/API/types'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; @@ -78,6 +79,7 @@ function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, poli useEffect(() => { return () => { Member.setWorkspaceInviteMembersDraft(route.params.policyID, {}); + FormActions.clearDraftValues(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM); }; // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [route.params.policyID]); diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 96b6d31e5a2e..cb914591a59d 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -30,6 +30,7 @@ import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as FormActions from '@libs/actions/FormActions'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; @@ -417,6 +418,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson const invitedEmails = Object.values(invitedEmailsToAccountIDsDraft).map(String); selectionListRef.current?.scrollAndHighlightItem?.(invitedEmails, 1500); Member.setWorkspaceInviteMembersDraft(route.params.policyID, {}); + FormActions.clearDraftValues(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM); }, [invitedEmailsToAccountIDsDraft, route.params.policyID, isFocused, accountIDs, prevAccountIDs]); const getHeaderMessage = () => { From c744ec566bd88219433decba6f74db3d00e62965 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Tue, 5 Nov 2024 12:33:34 +0700 Subject: [PATCH 022/210] fix image from opening in both a new tab and app modal --- src/ROUTES.ts | 6 +++-- .../BaseAnchorForCommentsOnly.tsx | 6 ++--- src/components/AnchorForCommentsOnly/types.ts | 2 ++ src/components/AttachmentModal.tsx | 22 ++++++++++++++++++ .../AttachmentCarousel/extractAttachments.ts | 13 +++++++++++ src/components/Attachments/types.ts | 2 ++ .../HTMLRenderers/AnchorRenderer.tsx | 2 ++ .../HTMLRenderers/ImageRenderer.tsx | 3 ++- src/components/Header.tsx | 23 +++++++++++++++++-- src/components/HeaderWithBackButton/index.tsx | 3 +++ src/components/HeaderWithBackButton/types.ts | 2 ++ src/libs/Navigation/types.ts | 1 + src/pages/home/report/ReportAttachments.tsx | 6 +++-- 13 files changed, 81 insertions(+), 10 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 45501bf46374..fbdfbd937717 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -310,11 +310,13 @@ const ROUTES = { }, ATTACHMENTS: { route: 'attachment', - getRoute: (reportID: string, type: ValueOf, url: string, accountID?: number, isAuthTokenRequired?: boolean) => { + getRoute: (reportID: string, type: ValueOf, url: string, accountID?: number, isAuthTokenRequired?: boolean, imageHrefLink?: string) => { const reportParam = reportID ? `&reportID=${reportID}` : ''; const accountParam = accountID ? `&accountID=${accountID}` : ''; const authTokenParam = isAuthTokenRequired ? '&isAuthTokenRequired=true' : ''; - return `attachment?source=${encodeURIComponent(url)}&type=${type}${reportParam}${accountParam}${authTokenParam}` as const; + const imageHrefLinkParam = imageHrefLink ? `&imageHrefLink=${imageHrefLink}` : ''; + + return `attachment?source=${encodeURIComponent(url)}&type=${type}${reportParam}${accountParam}${authTokenParam}${imageHrefLinkParam}` as const; }, }, REPORT_PARTICIPANTS: { diff --git a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx index 4c470858292c..3b1427f71e8b 100644 --- a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx +++ b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx @@ -17,7 +17,7 @@ import type {BaseAnchorForCommentsOnlyProps, LinkProps} from './types'; /* * This is a default anchor component for regular links. */ -function BaseAnchorForCommentsOnly({onPressIn, onPressOut, href = '', rel = '', target = '', children = null, style, onPress, ...rest}: BaseAnchorForCommentsOnlyProps) { +function BaseAnchorForCommentsOnly({onPressIn, onPressOut, href = '', rel = '', target = '', children = null, style, onPress, containsImageLink, ...rest}: BaseAnchorForCommentsOnlyProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const linkRef = useRef(null); @@ -62,7 +62,7 @@ function BaseAnchorForCommentsOnly({onPressIn, onPressOut, href = '', rel = '', role={CONST.ROLE.LINK} accessibilityLabel={href} > - + void; + + containsImageLink?: boolean; }; type BaseAnchorForCommentsOnlyProps = AnchorForCommentsOnlyProps & { diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 0bc233812ca7..ebd5e6ef8c9c 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -134,6 +134,8 @@ type AttachmentModalProps = { canEditReceipt?: boolean; shouldDisableSendButton?: boolean; + + imageHrefLink?: string; }; function AttachmentModal({ @@ -161,6 +163,7 @@ function AttachmentModal({ type = undefined, accountID = undefined, shouldDisableSendButton = false, + imageHrefLink = '', }: AttachmentModalProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -185,6 +188,7 @@ function AttachmentModal({ const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '-1', report?.parentReportActionID ?? '-1'); const transactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? '-1' : '-1'; const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); + const [attachmentCarouselImageHref, setAttachmentCarouselImageHref] = useState(''); const [file, setFile] = useState( originalFileName @@ -211,6 +215,7 @@ function AttachmentModal({ setFile(attachment.file); setIsAuthTokenRequiredState(attachment.isAuthTokenRequired ?? false); onCarouselAttachmentChange(attachment); + setAttachmentCarouselImageHref(attachment?.imageHrefLink ?? ''); }, [onCarouselAttachmentChange], ); @@ -482,6 +487,22 @@ function AttachmentModal({ const submitRef = useRef(null); + const getImageHrefLink = () => { + if (shouldShowNotFoundPage) { + return ''; + } + + if (!isEmptyObject(report) && !isReceiptAttachment) { + return attachmentCarouselImageHref; + } + + if (!isAuthTokenRequired && imageHrefLink) { + return imageHrefLink; + } + + return ''; + }; + return ( <> {isLoading && } diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index 81ee6d08934b..30042df76a54 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -28,8 +28,13 @@ function extractAttachments( // and navigating back (<) shows the image preceding the first instance, not the selected duplicate's position. const uniqueSources = new Set(); + let currentLinkHref = ''; + const htmlParser = new HtmlParser({ onopentag: (name, attribs) => { + if (name === 'a' && attribs.href) { + currentLinkHref = attribs.href; + } if (name === 'video') { const source = tryResolveUrlFromApiRoot(attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]); if (uniqueSources.has(source)) { @@ -81,9 +86,17 @@ function extractAttachments( file: {name: fileName, width, height}, isReceipt: false, hasBeenFlagged: attribs['data-flagged'] === 'true', + imageHrefLink: currentLinkHref, }); } }, + onclosetag: (name) => { + if (!(name === 'a') || !currentLinkHref) { + return; + } + + currentLinkHref = ''; + }, }); if (type === CONST.ATTACHMENT_TYPE.NOTE) { diff --git a/src/components/Attachments/types.ts b/src/components/Attachments/types.ts index 8bac4cc53af6..e148389ff244 100644 --- a/src/components/Attachments/types.ts +++ b/src/components/Attachments/types.ts @@ -28,6 +28,8 @@ type Attachment = { isReceipt?: boolean; duration?: number; + + imageHrefLink?: string; }; export type {AttachmentSource, Attachment}; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx index 122db1e7877b..6cb23b0dd045 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx @@ -30,6 +30,7 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) { const internalNewExpensifyPath = Link.getInternalNewExpensifyPath(attrHref); const internalExpensifyPath = Link.getInternalExpensifyPath(attrHref); const isVideo = attrHref && Str.isVideo(attrHref); + const containsImageLink = tnode.tagName === 'a' && tnode.children.some((child) => child.tagName === 'img'); const isDeleted = HTMLEngineUtils.isDeletedNode(tnode); const textDecorationLineStyle = isDeleted ? styles.underlineLineThrough : {}; @@ -73,6 +74,7 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) { key={key} // Only pass the press handler for internal links. For public links or whitelisted internal links fallback to default link handling onPress={internalNewExpensifyPath || internalExpensifyPath ? () => Link.openLink(attrHref, environmentURL, isAttachment) : undefined} + containsImageLink={containsImageLink} > { diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 86a7a9cabcb6..bd3f32abfd1e 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,10 +1,11 @@ import type {ReactNode} from 'react'; import React, {useMemo} from 'react'; import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; -import {View} from 'react-native'; +import {Linking, View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import EnvironmentBadge from './EnvironmentBadge'; import Text from './Text'; +import TextLink from './TextLink'; type HeaderProps = { /** Title of the Header */ @@ -21,9 +22,11 @@ type HeaderProps = { /** Additional header container styles */ containerStyles?: StyleProp; + + imageHrefLink?: string; }; -function Header({title = '', subtitle = '', textStyles = [], containerStyles = [], shouldShowEnvironmentBadge = false}: HeaderProps) { +function Header({title = '', subtitle = '', textStyles = [], containerStyles = [], shouldShowEnvironmentBadge = false, imageHrefLink = ''}: HeaderProps) { const styles = useThemeStyles(); const renderedSubtitle = useMemo( () => ( @@ -43,6 +46,21 @@ function Header({title = '', subtitle = '', textStyles = [], containerStyles = [ ), [subtitle, styles], ); + + const renderedImageHrefLink = () => { + return ( + + { + Linking.openURL(imageHrefLink); + }} + > + {imageHrefLink} + + + ); + }; + return ( @@ -57,6 +75,7 @@ function Header({title = '', subtitle = '', textStyles = [], containerStyles = [ ) : title} {renderedSubtitle} + {imageHrefLink && renderedImageHrefLink()} {shouldShowEnvironmentBadge && } diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index 0d307aa8728d..8e12f47af106 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -65,6 +65,7 @@ function HeaderWithBackButton({ shouldDisplaySearchRouter = false, progressBarPercentage, style, + imageHrefLink = '', }: HeaderWithBackButtonProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -108,10 +109,12 @@ function HeaderWithBackButton({ title={title} subtitle={stepCounter ? translate('stepCounter', stepCounter) : subtitle} textStyles={[titleColor ? StyleUtils.getTextColorStyle(titleColor) : {}, isCentralPaneSettings && styles.textHeadlineH2]} + imageHrefLink={imageHrefLink} /> ); }, [ StyleUtils, + imageHrefLink, isCentralPaneSettings, policy, progressBarPercentage, diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts index 12dc1aa9684b..c9fa0ba4d7b5 100644 --- a/src/components/HeaderWithBackButton/types.ts +++ b/src/components/HeaderWithBackButton/types.ts @@ -142,6 +142,8 @@ type HeaderWithBackButtonProps = Partial & { /** Additional styles to add to the component */ style?: StyleProp; + + imageHrefLink?: string; }; export type {ThreeDotsMenuItem}; diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 3eae46ac2855..4b231668ef70 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1549,6 +1549,7 @@ type AuthScreensParamList = CentralPaneScreensParamList & type: ValueOf; accountID: string; isAuthTokenRequired?: string; + imageHrefLink?: string; }; [SCREENS.PROFILE_AVATAR]: { accountID: string; diff --git a/src/pages/home/report/ReportAttachments.tsx b/src/pages/home/report/ReportAttachments.tsx index d30d8e9aabc1..fe95cab404ce 100644 --- a/src/pages/home/report/ReportAttachments.tsx +++ b/src/pages/home/report/ReportAttachments.tsx @@ -18,6 +18,7 @@ function ReportAttachments({route}: ReportAttachmentsProps) { const type = route.params.type; const accountID = route.params.accountID; const isAuthTokenRequired = route.params.isAuthTokenRequired; + const imageHrefLink = route.params.imageHrefLink; const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID || -1}`); const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP); @@ -26,10 +27,10 @@ function ReportAttachments({route}: ReportAttachmentsProps) { const onCarouselAttachmentChange = useCallback( (attachment: Attachment) => { - const routeToNavigate = ROUTES.ATTACHMENTS.getRoute(reportID, type, String(attachment.source), Number(accountID)); + const routeToNavigate = ROUTES.ATTACHMENTS.getRoute(reportID, type, String(attachment.source), Number(accountID), !!isAuthTokenRequired, imageHrefLink); Navigation.navigate(routeToNavigate); }, - [reportID, accountID, type], + [reportID, type, accountID, isAuthTokenRequired, imageHrefLink], ); return ( @@ -48,6 +49,7 @@ function ReportAttachments({route}: ReportAttachmentsProps) { onCarouselAttachmentChange={onCarouselAttachmentChange} shouldShowNotFoundPage={!isLoadingApp && type !== CONST.ATTACHMENT_TYPE.SEARCH && !report?.reportID} isAuthTokenRequired={!!isAuthTokenRequired} + imageHrefLink={imageHrefLink ?? ''} /> ); } From d5490b83d4059c4b940f874cd76578ac92f97f6f Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 5 Nov 2024 16:01:11 +0700 Subject: [PATCH 023/210] update isEnd to false when the video is playing --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index fad641e696ae..4a18f9abeb69 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -188,6 +188,8 @@ function BaseVideoPlayer({ [playVideo, videoResumeTryNumberRef], ); + console.log(isEnded); + const prevIsMutedRef = useRef(false); const prevVolumeRef = useRef(0); @@ -207,6 +209,8 @@ function BaseVideoPlayer({ setIsEnded(status.didJustFinish && !status.isLooping); setControlStatusState(CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW); controlsOpacity.value = 1; + } else if (status.isPlaying && isEnded) { + setIsEnded(false); } if (prevIsMutedRef.current && prevVolumeRef.current === 0 && !status.isMuted) { From 4acf40ca91ed28432d3a0e56cbd8812d22367140 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 5 Nov 2024 16:02:05 +0700 Subject: [PATCH 024/210] remove log --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 4a18f9abeb69..1e46c595610e 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -188,8 +188,6 @@ function BaseVideoPlayer({ [playVideo, videoResumeTryNumberRef], ); - console.log(isEnded); - const prevIsMutedRef = useRef(false); const prevVolumeRef = useRef(0); From df706737f05689a4ca64c5e537790c3caaa17439 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Tue, 5 Nov 2024 11:08:12 +0100 Subject: [PATCH 025/210] fix window dimensions --- src/hooks/useWindowDimensions/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useWindowDimensions/index.ts b/src/hooks/useWindowDimensions/index.ts index 4997fc4b01a7..a1bea6a80644 100644 --- a/src/hooks/useWindowDimensions/index.ts +++ b/src/hooks/useWindowDimensions/index.ts @@ -130,7 +130,7 @@ export default function (useCachedViewportHeight = false): WindowDimensions { const didScreenReturnToOriginalSize = lockedWindowDimensionsRef.current.windowWidth === windowWidth && lockedWindowDimensionsRef.current.windowHeight === windowHeight; // if video exits fullscreen mode, unlock the window dimensions - if (lockedWindowDimensionsRef.current && !isFullScreenRef.current && didScreenReturnToOriginalSize) { + if (lockedWindowDimensionsRef.current && !isFullScreenRef.current) { const lastLockedWindowDimensions = {...lockedWindowDimensionsRef.current}; unlockWindowDimensions(); return {windowWidth: lastLockedWindowDimensions.windowWidth, windowHeight: lastLockedWindowDimensions.windowHeight}; From 9e37309782b252f70c289e98090b13d5afe1422e Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Tue, 5 Nov 2024 11:22:25 +0100 Subject: [PATCH 026/210] fix lint --- src/hooks/useWindowDimensions/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/hooks/useWindowDimensions/index.ts b/src/hooks/useWindowDimensions/index.ts index a1bea6a80644..6e7b74bf8df5 100644 --- a/src/hooks/useWindowDimensions/index.ts +++ b/src/hooks/useWindowDimensions/index.ts @@ -127,8 +127,6 @@ export default function (useCachedViewportHeight = false): WindowDimensions { return windowDimensions; } - const didScreenReturnToOriginalSize = lockedWindowDimensionsRef.current.windowWidth === windowWidth && lockedWindowDimensionsRef.current.windowHeight === windowHeight; - // if video exits fullscreen mode, unlock the window dimensions if (lockedWindowDimensionsRef.current && !isFullScreenRef.current) { const lastLockedWindowDimensions = {...lockedWindowDimensionsRef.current}; From 7e6f6c14e0d5d20969e35874b4790df7c5b0c725 Mon Sep 17 00:00:00 2001 From: truph01 Date: Tue, 5 Nov 2024 20:04:21 +0700 Subject: [PATCH 027/210] fix: update trans --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index da158b15cd77..a2d230b141ad 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -779,7 +779,7 @@ const translations = { locationAccessMessage: 'El acceso a la ubicación nos ayuda a mantener tu zona horaria y moneda precisas dondequiera que vayas.', locationErrorTitle: 'Permitir acceso a la ubicación', locationErrorMessage: 'El acceso a la ubicación nos ayuda a mantener tu zona horaria y moneda precisas dondequiera que vayas.', - allowLocationFromSetting: `Location access helps us keep your timezone and currency accurate wherever you go. Please allow location access from your device's permission settings.`, + allowLocationFromSetting: `El acceso a la ubicación nos ayuda a mantener tu zona horaria y moneda precisas dondequiera que estés. Por favor, permite el acceso a la ubicación en la configuración de permisos de tu dispositivo.`, cameraErrorMessage: 'Se ha producido un error al hacer una foto. Por favor, inténtalo de nuevo.', dropTitle: 'Suéltalo', dropMessage: 'Suelta tu archivo aquí', From 5dbc039f0deec5e66f35432dad8984452bef7347 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Tue, 5 Nov 2024 22:22:09 +0700 Subject: [PATCH 028/210] rename variables and functions for clarity --- src/ROUTES.ts | 6 +++--- .../BaseAnchorForCommentsOnly.tsx | 6 +++--- src/components/AnchorForCommentsOnly/types.ts | 3 ++- src/components/AttachmentModal.tsx | 18 +++++++++--------- .../AttachmentCarousel/extractAttachments.ts | 10 +++++----- src/components/Attachments/types.ts | 2 +- .../HTMLRenderers/AnchorRenderer.tsx | 4 ++-- .../HTMLRenderers/ImageRenderer.tsx | 4 ++-- src/components/Header.tsx | 13 +++++++------ src/components/HeaderWithBackButton/index.tsx | 6 +++--- src/components/HeaderWithBackButton/types.ts | 3 ++- src/libs/Navigation/types.ts | 2 +- src/pages/home/report/ReportAttachments.tsx | 8 ++++---- 13 files changed, 44 insertions(+), 41 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index fbdfbd937717..0cff24fadf8a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -310,13 +310,13 @@ const ROUTES = { }, ATTACHMENTS: { route: 'attachment', - getRoute: (reportID: string, type: ValueOf, url: string, accountID?: number, isAuthTokenRequired?: boolean, imageHrefLink?: string) => { + getRoute: (reportID: string, type: ValueOf, url: string, accountID?: number, isAuthTokenRequired?: boolean, imageLink?: string) => { const reportParam = reportID ? `&reportID=${reportID}` : ''; const accountParam = accountID ? `&accountID=${accountID}` : ''; const authTokenParam = isAuthTokenRequired ? '&isAuthTokenRequired=true' : ''; - const imageHrefLinkParam = imageHrefLink ? `&imageHrefLink=${imageHrefLink}` : ''; + const imageLinkParam = imageLink ? `&imageLink=${imageLink}` : ''; - return `attachment?source=${encodeURIComponent(url)}&type=${type}${reportParam}${accountParam}${authTokenParam}${imageHrefLinkParam}` as const; + return `attachment?source=${encodeURIComponent(url)}&type=${type}${reportParam}${accountParam}${authTokenParam}${imageLinkParam}` as const; }, }, REPORT_PARTICIPANTS: { diff --git a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx index 3b1427f71e8b..d31dcb5df7b6 100644 --- a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx +++ b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx @@ -17,7 +17,7 @@ import type {BaseAnchorForCommentsOnlyProps, LinkProps} from './types'; /* * This is a default anchor component for regular links. */ -function BaseAnchorForCommentsOnly({onPressIn, onPressOut, href = '', rel = '', target = '', children = null, style, onPress, containsImageLink, ...rest}: BaseAnchorForCommentsOnlyProps) { +function BaseAnchorForCommentsOnly({onPressIn, onPressOut, href = '', rel = '', target = '', children = null, style, onPress, isLinkHasImage, ...rest}: BaseAnchorForCommentsOnlyProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const linkRef = useRef(null); @@ -62,7 +62,7 @@ function BaseAnchorForCommentsOnly({onPressIn, onPressOut, href = '', rel = '', role={CONST.ROLE.LINK} accessibilityLabel={href} > - + void; - containsImageLink?: boolean; + /** Indicates whether an image is wrapped in an anchor (``) tag with an `href` link */ + isLinkHasImage?: boolean; }; type BaseAnchorForCommentsOnlyProps = AnchorForCommentsOnlyProps & { diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index ebd5e6ef8c9c..a343cd556149 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -135,7 +135,7 @@ type AttachmentModalProps = { shouldDisableSendButton?: boolean; - imageHrefLink?: string; + imageLink?: string; }; function AttachmentModal({ @@ -163,7 +163,7 @@ function AttachmentModal({ type = undefined, accountID = undefined, shouldDisableSendButton = false, - imageHrefLink = '', + imageLink = '', }: AttachmentModalProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -188,7 +188,7 @@ function AttachmentModal({ const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '-1', report?.parentReportActionID ?? '-1'); const transactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? '-1' : '-1'; const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); - const [attachmentCarouselImageHref, setAttachmentCarouselImageHref] = useState(''); + const [attachmentCarouselImageLink, setAttachmentCarouselImageLink] = useState(''); const [file, setFile] = useState( originalFileName @@ -215,7 +215,7 @@ function AttachmentModal({ setFile(attachment.file); setIsAuthTokenRequiredState(attachment.isAuthTokenRequired ?? false); onCarouselAttachmentChange(attachment); - setAttachmentCarouselImageHref(attachment?.imageHrefLink ?? ''); + setAttachmentCarouselImageLink(attachment?.imageLink ?? ''); }, [onCarouselAttachmentChange], ); @@ -487,17 +487,17 @@ function AttachmentModal({ const submitRef = useRef(null); - const getImageHrefLink = () => { + const getSubTitleLink = () => { if (shouldShowNotFoundPage) { return ''; } if (!isEmptyObject(report) && !isReceiptAttachment) { - return attachmentCarouselImageHref; + return attachmentCarouselImageLink; } - if (!isAuthTokenRequired && imageHrefLink) { - return imageHrefLink; + if (!isAuthTokenRequired && imageLink) { + return imageLink; } return ''; @@ -548,7 +548,7 @@ function AttachmentModal({ threeDotsAnchorPosition={styles.threeDotsPopoverOffsetAttachmentModal(windowWidth)} threeDotsMenuItems={threeDotsMenuItems} shouldOverlayDots - imageHrefLink={getImageHrefLink()} + subTitleLink={getSubTitleLink()} /> {isLoading && } diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index 30042df76a54..251bbf64e5cd 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -28,12 +28,12 @@ function extractAttachments( // and navigating back (<) shows the image preceding the first instance, not the selected duplicate's position. const uniqueSources = new Set(); - let currentLinkHref = ''; + let currentImageLink = ''; const htmlParser = new HtmlParser({ onopentag: (name, attribs) => { if (name === 'a' && attribs.href) { - currentLinkHref = attribs.href; + currentImageLink = attribs.href; } if (name === 'video') { const source = tryResolveUrlFromApiRoot(attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]); @@ -86,16 +86,16 @@ function extractAttachments( file: {name: fileName, width, height}, isReceipt: false, hasBeenFlagged: attribs['data-flagged'] === 'true', - imageHrefLink: currentLinkHref, + imageLink: currentImageLink, }); } }, onclosetag: (name) => { - if (!(name === 'a') || !currentLinkHref) { + if (!(name === 'a') || !currentImageLink) { return; } - currentLinkHref = ''; + currentImageLink = ''; }, }); diff --git a/src/components/Attachments/types.ts b/src/components/Attachments/types.ts index e148389ff244..a819e3ee9075 100644 --- a/src/components/Attachments/types.ts +++ b/src/components/Attachments/types.ts @@ -29,7 +29,7 @@ type Attachment = { duration?: number; - imageHrefLink?: string; + imageLink?: string; }; export type {AttachmentSource, Attachment}; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx index 6cb23b0dd045..1af172d07eea 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx @@ -30,7 +30,7 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) { const internalNewExpensifyPath = Link.getInternalNewExpensifyPath(attrHref); const internalExpensifyPath = Link.getInternalExpensifyPath(attrHref); const isVideo = attrHref && Str.isVideo(attrHref); - const containsImageLink = tnode.tagName === 'a' && tnode.children.some((child) => child.tagName === 'img'); + const isLinkHasImage = tnode.tagName === 'a' && tnode.children.some((child) => child.tagName === 'img'); const isDeleted = HTMLEngineUtils.isDeletedNode(tnode); const textDecorationLineStyle = isDeleted ? styles.underlineLineThrough : {}; @@ -74,7 +74,7 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) { key={key} // Only pass the press handler for internal links. For public links or whitelisted internal links fallback to default link handling onPress={internalNewExpensifyPath || internalExpensifyPath ? () => Link.openLink(attrHref, environmentURL, isAttachment) : undefined} - containsImageLink={containsImageLink} + isLinkHasImage={isLinkHasImage} > { diff --git a/src/components/Header.tsx b/src/components/Header.tsx index bd3f32abfd1e..c0e020120511 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -23,10 +23,11 @@ type HeaderProps = { /** Additional header container styles */ containerStyles?: StyleProp; - imageHrefLink?: string; + /** The URL link associated with the attachment's subtitle, if available */ + subTitleLink?: string; }; -function Header({title = '', subtitle = '', textStyles = [], containerStyles = [], shouldShowEnvironmentBadge = false, imageHrefLink = ''}: HeaderProps) { +function Header({title = '', subtitle = '', textStyles = [], containerStyles = [], shouldShowEnvironmentBadge = false, subTitleLink = ''}: HeaderProps) { const styles = useThemeStyles(); const renderedSubtitle = useMemo( () => ( @@ -47,15 +48,15 @@ function Header({title = '', subtitle = '', textStyles = [], containerStyles = [ [subtitle, styles], ); - const renderedImageHrefLink = () => { + const renderedSubTitleLink = () => { return ( { - Linking.openURL(imageHrefLink); + Linking.openURL(subTitleLink); }} > - {imageHrefLink} + {subTitleLink} ); @@ -75,7 +76,7 @@ function Header({title = '', subtitle = '', textStyles = [], containerStyles = [ ) : title} {renderedSubtitle} - {imageHrefLink && renderedImageHrefLink()} + {subTitleLink && renderedSubTitleLink()} {shouldShowEnvironmentBadge && } diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index 8e12f47af106..2c07c48d52b7 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -65,7 +65,7 @@ function HeaderWithBackButton({ shouldDisplaySearchRouter = false, progressBarPercentage, style, - imageHrefLink = '', + subTitleLink = '', }: HeaderWithBackButtonProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -109,12 +109,12 @@ function HeaderWithBackButton({ title={title} subtitle={stepCounter ? translate('stepCounter', stepCounter) : subtitle} textStyles={[titleColor ? StyleUtils.getTextColorStyle(titleColor) : {}, isCentralPaneSettings && styles.textHeadlineH2]} - imageHrefLink={imageHrefLink} + subTitleLink={subTitleLink} /> ); }, [ StyleUtils, - imageHrefLink, + subTitleLink, isCentralPaneSettings, policy, progressBarPercentage, diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts index c9fa0ba4d7b5..6eef2b072eee 100644 --- a/src/components/HeaderWithBackButton/types.ts +++ b/src/components/HeaderWithBackButton/types.ts @@ -143,7 +143,8 @@ type HeaderWithBackButtonProps = Partial & { /** Additional styles to add to the component */ style?: StyleProp; - imageHrefLink?: string; + /** The URL link associated with the attachment's subtitle, if available */ + subTitleLink?: string; }; export type {ThreeDotsMenuItem}; diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 4b231668ef70..0ef81d10dede 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1549,7 +1549,7 @@ type AuthScreensParamList = CentralPaneScreensParamList & type: ValueOf; accountID: string; isAuthTokenRequired?: string; - imageHrefLink?: string; + imageLink?: string; }; [SCREENS.PROFILE_AVATAR]: { accountID: string; diff --git a/src/pages/home/report/ReportAttachments.tsx b/src/pages/home/report/ReportAttachments.tsx index fe95cab404ce..c002d109504f 100644 --- a/src/pages/home/report/ReportAttachments.tsx +++ b/src/pages/home/report/ReportAttachments.tsx @@ -18,7 +18,7 @@ function ReportAttachments({route}: ReportAttachmentsProps) { const type = route.params.type; const accountID = route.params.accountID; const isAuthTokenRequired = route.params.isAuthTokenRequired; - const imageHrefLink = route.params.imageHrefLink; + const imageLink = route.params.imageLink; const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID || -1}`); const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP); @@ -27,10 +27,10 @@ function ReportAttachments({route}: ReportAttachmentsProps) { const onCarouselAttachmentChange = useCallback( (attachment: Attachment) => { - const routeToNavigate = ROUTES.ATTACHMENTS.getRoute(reportID, type, String(attachment.source), Number(accountID), !!isAuthTokenRequired, imageHrefLink); + const routeToNavigate = ROUTES.ATTACHMENTS.getRoute(reportID, type, String(attachment.source), Number(accountID), !!isAuthTokenRequired, imageLink); Navigation.navigate(routeToNavigate); }, - [reportID, type, accountID, isAuthTokenRequired, imageHrefLink], + [reportID, type, accountID, isAuthTokenRequired, imageLink], ); return ( @@ -49,7 +49,7 @@ function ReportAttachments({route}: ReportAttachmentsProps) { onCarouselAttachmentChange={onCarouselAttachmentChange} shouldShowNotFoundPage={!isLoadingApp && type !== CONST.ATTACHMENT_TYPE.SEARCH && !report?.reportID} isAuthTokenRequired={!!isAuthTokenRequired} - imageHrefLink={imageHrefLink ?? ''} + imageLink={imageLink ?? ''} /> ); } From dbc52033d86ffc50a7ce8d917a57dd535b9cc56f Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Tue, 5 Nov 2024 22:27:54 +0700 Subject: [PATCH 029/210] update parameters for route in onCarouselAttachmentChange function --- src/pages/home/report/ReportAttachments.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportAttachments.tsx b/src/pages/home/report/ReportAttachments.tsx index c002d109504f..372c5fd1f5ec 100644 --- a/src/pages/home/report/ReportAttachments.tsx +++ b/src/pages/home/report/ReportAttachments.tsx @@ -27,10 +27,10 @@ function ReportAttachments({route}: ReportAttachmentsProps) { const onCarouselAttachmentChange = useCallback( (attachment: Attachment) => { - const routeToNavigate = ROUTES.ATTACHMENTS.getRoute(reportID, type, String(attachment.source), Number(accountID), !!isAuthTokenRequired, imageLink); + const routeToNavigate = ROUTES.ATTACHMENTS.getRoute(reportID, type, String(attachment.source), Number(accountID), attachment.isAuthTokenRequired, attachment.imageLink); Navigation.navigate(routeToNavigate); }, - [reportID, type, accountID, isAuthTokenRequired, imageLink], + [reportID, type, accountID], ); return ( From 9070620537f3ca0bd022dee4c8f495a904b7f469 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Wed, 6 Nov 2024 02:36:29 +0530 Subject: [PATCH 030/210] minor update. Signed-off-by: krishna2323 --- src/pages/workspace/WorkspaceInviteMessagePage.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index f0317284e8f9..4b437e1ffd78 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -68,16 +68,16 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: const getDefaultWelcomeNote = useCallback(() => { return ( // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - formData?.[INPUT_IDS.WELCOME_MESSAGE] || + formData?.[INPUT_IDS.WELCOME_MESSAGE] ?? // workspaceInviteMessageDraft can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - workspaceInviteMessageDraft || + workspaceInviteMessageDraft ?? // policy?.description can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - Parser.htmlToMarkdown(policy?.description ?? '') || - translate('workspace.common.welcomeNote', { - workspaceName: policy?.name ?? '', - }) + (Parser.htmlToMarkdown(policy?.description ?? '') || + translate('workspace.common.welcomeNote', { + workspaceName: policy?.name ?? '', + })) ); }, [workspaceInviteMessageDraft, policy, translate, formData]); From 88cd7752bfb26bf72e61ab75e639d7650534b042 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Wed, 6 Nov 2024 12:16:50 +0530 Subject: [PATCH 031/210] fix height when approve button changes to pay. Signed-off-by: krishna2323 --- .../ReportActionItem/ReportPreview.tsx | 11 ++++----- .../AnimatedSettlementButton.tsx | 23 ++++++++----------- src/libs/actions/IOU.ts | 3 ++- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 27068ff2f80f..cbb50243dfe5 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -203,11 +203,9 @@ function ReportPreview({ const stopAnimation = useCallback(() => { setIsPaidAnimationRunning(false); - }, []); - - const stopApprovedAnimation = useCallback(() => { setIsApprovedAnimationRunning(false); }, []); + const startAnimation = useCallback(() => { setIsPaidAnimationRunning(true); HapticFeedback.longPress(); @@ -346,14 +344,15 @@ function ReportPreview({ const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport); const getCanIOUBePaid = useCallback( - (onlyShowPayElsewhere = false) => IOU.canIOUBePaid(iouReport, chatReport, policy, allTransactions, onlyShowPayElsewhere), + (onlyShowPayElsewhere = false, shouldCheckApprovedState = true) => IOU.canIOUBePaid(iouReport, chatReport, policy, allTransactions, onlyShowPayElsewhere, shouldCheckApprovedState), [iouReport, chatReport, policy, allTransactions], ); const canIOUBePaid = useMemo(() => getCanIOUBePaid(), [getCanIOUBePaid]); + const canIOUBePaidAndApproved = useMemo(() => getCanIOUBePaid(false, false), [getCanIOUBePaid]); const onlyShowPayElsewhere = useMemo(() => !canIOUBePaid && getCanIOUBePaid(true), [canIOUBePaid, getCanIOUBePaid]); const shouldShowPayButton = isPaidAnimationRunning || canIOUBePaid || onlyShowPayElsewhere; - const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(iouReport, policy), [iouReport, policy]); + const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(iouReport, policy), [iouReport, policy]) || isApprovedAnimationRunning; const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(iouReport); @@ -570,7 +569,7 @@ function ReportPreview({ onlyShowPayElsewhere={onlyShowPayElsewhere} isPaidAnimationRunning={isPaidAnimationRunning} isApprovedAnimationRunning={isApprovedAnimationRunning} - onApprovedAnimationFinish={stopApprovedAnimation} + canIOUBePaid={canIOUBePaidAndApproved || isPaidAnimationRunning} onAnimationFinish={stopAnimation} formattedAmount={getSettlementAmount() ?? ''} currency={iouReport?.currency} diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx index f8205a1b1ab0..7e42c8cdc45c 100644 --- a/src/components/SettlementButton/AnimatedSettlementButton.tsx +++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx @@ -12,15 +12,15 @@ type AnimatedSettlementButtonProps = SettlementButtonProps & { isPaidAnimationRunning: boolean; onAnimationFinish: () => void; isApprovedAnimationRunning: boolean; - onApprovedAnimationFinish: () => void; + canIOUBePaid: boolean; }; function AnimatedSettlementButton({ isPaidAnimationRunning, onAnimationFinish, isApprovedAnimationRunning, - onApprovedAnimationFinish, isDisabled, + canIOUBePaid, ...settlementButtonProps }: AnimatedSettlementButtonProps) { const styles = useThemeStyles(); @@ -77,19 +77,15 @@ function AnimatedSettlementButton({ // Wait for the above animation + 1s delay before hiding the component const totalDelay = CONST.ANIMATION_PAID_DURATION + CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY; + const willShowPaymentButton = canIOUBePaid && isApprovedAnimationRunning; height.value = withDelay( totalDelay, - withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}, () => - runOnJS(() => { - if (isApprovedAnimationRunning) { - onApprovedAnimationFinish(); - } else { - onAnimationFinish(); - } - })(), - ), + withTiming(willShowPaymentButton ? variables.componentSizeNormal : 0, {duration: CONST.ANIMATION_PAID_DURATION}, () => runOnJS(onAnimationFinish)()), + ); + buttonMarginTop.value = withDelay( + totalDelay, + withTiming(willShowPaymentButton ? styles.expenseAndReportPreviewTextButtonContainer.gap : 0, {duration: CONST.ANIMATION_PAID_DURATION}), ); - buttonMarginTop.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})); paymentCompleteTextOpacity.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})); }, [ isPaidAnimationRunning, @@ -102,7 +98,8 @@ function AnimatedSettlementButton({ paymentCompleteTextScale, buttonMarginTop, resetAnimation, - onApprovedAnimationFinish, + canIOUBePaid, + styles.expenseAndReportPreviewTextButtonContainer.gap, ]); return ( diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 7a72df9f1d87..6fafe3c59a4f 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7060,6 +7060,7 @@ function canIOUBePaid( policy: OnyxTypes.OnyxInputOrEntry, transactions?: OnyxTypes.Transaction[], onlyShowPayElsewhere = false, + shouldCheckApprovedState = true, ) { const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(chatReport); const reportNameValuePairs = ReportUtils.getReportNameValuePairs(chatReport?.reportID); @@ -7113,7 +7114,7 @@ function canIOUBePaid( reimbursableSpend !== 0 && !isChatReportArchived && !isAutoReimbursable && - !shouldBeApproved && + (!shouldBeApproved || !shouldCheckApprovedState) && !isPayAtEndExpenseReport ); } From 62cd803d4e4afeacb9bf5d4f4035fcefd533bbbb Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 6 Nov 2024 08:28:20 +0100 Subject: [PATCH 032/210] call onHideCallback directly. --- src/pages/home/report/ContextMenu/ReportActionContextMenu.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts index df500a967cfd..df1f2cd5c9b8 100644 --- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts +++ b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts @@ -64,8 +64,7 @@ function hideContextMenu(shouldDelay?: boolean, onHideCallback = () => {}) { return; } if (!shouldDelay) { - contextMenuRef.current.hideContextMenu(onHideCallback); - + onHideCallback(); return; } From 36a05508d77891d00ceccfaf73bf0290a1e47753 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 6 Nov 2024 08:30:20 +0100 Subject: [PATCH 033/210] undo windowDimensions changes --- src/hooks/useWindowDimensions/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/hooks/useWindowDimensions/index.ts b/src/hooks/useWindowDimensions/index.ts index 6e7b74bf8df5..4997fc4b01a7 100644 --- a/src/hooks/useWindowDimensions/index.ts +++ b/src/hooks/useWindowDimensions/index.ts @@ -127,8 +127,10 @@ export default function (useCachedViewportHeight = false): WindowDimensions { return windowDimensions; } + const didScreenReturnToOriginalSize = lockedWindowDimensionsRef.current.windowWidth === windowWidth && lockedWindowDimensionsRef.current.windowHeight === windowHeight; + // if video exits fullscreen mode, unlock the window dimensions - if (lockedWindowDimensionsRef.current && !isFullScreenRef.current) { + if (lockedWindowDimensionsRef.current && !isFullScreenRef.current && didScreenReturnToOriginalSize) { const lastLockedWindowDimensions = {...lockedWindowDimensionsRef.current}; unlockWindowDimensions(); return {windowWidth: lastLockedWindowDimensions.windowWidth, windowHeight: lastLockedWindowDimensions.windowHeight}; From ab94ee9076b5708e2deb91e6bff5e9da38cb00de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 6 Nov 2024 08:55:25 +0000 Subject: [PATCH 034/210] Change guidelines to forbid defaulting values --- contributingGuides/STYLE.md | 57 ++++++++++++++++++++++++++++++++----- src/CONST.ts | 7 +++++ 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md index e6660d848129..755f5228a8a7 100644 --- a/contributingGuides/STYLE.md +++ b/contributingGuides/STYLE.md @@ -477,20 +477,63 @@ if (ref.current && 'getBoundingClientRect' in ref.current) { ### Default value for inexistent IDs - Use `'-1'` or `-1` when there is a possibility that the ID property of an Onyx value could be `null` or `undefined`. +Use `CONST.DEFAULT_NUMBER_ID` when there is a possibility that the number ID property of an Onyx value could be `null` or `undefined`. **Do not default string IDs to any value unless absolutely necessary**, in case it's necessary use `CONST.DEFAULT_STRING_ID` instead. ``` ts // BAD -const foo = report?.reportID ?? ''; -const bar = report?.reportID ?? '0'; +const accountID = report?.ownerAccountID ?? -1; +const accountID = report?.ownerAccountID ?? 0; +const reportID = report?.reportID ?? '-1'; -report ? report.reportID : '0'; -report ? report.reportID : ''; +// BAD +report ? report.ownerAccountID : -1; + +// GOOD +const accountID = report?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID; +const accountID = report?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID; +const reportID = report?.reportID; // GOOD -const foo = report?.reportID ?? '-1'; +report ? report.ownerAccountID : CONST.DEFAULT_NUMBER_ID; +``` + +Here are some common cases you may face when fixing your code to remove the default values. + +#### **Case 1**: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. + +```diff +-Report.getNewerActions(newestActionCurrentReport?.reportID ?? '-1', newestActionCurrentReport?.reportActionID ?? '-1'); ++Report.getNewerActions(newestActionCurrentReport?.reportID, newestActionCurrentReport?.reportActionID); +``` + +> error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable to type 'string'. + +We need to change `Report.getNewerActions()` arguments to allow `undefined`. By doing that we could add a condition that return early if one of the parameters are falsy, preventing the code (which is expecting defined IDs) from executing. + +```diff +-function getNewerActions(reportID: string, reportActionID: string) { ++function getNewerActions(reportID: string | undefined, reportActionID: string | undefined) { ++ if (!reportID || !reportActionID) { ++ return; ++ } +``` + +#### **Case 2**: Type 'undefined' cannot be used as an index type. + +```diff +function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = false, updatedTransaction, isFromReviewDuplicates = false}: MoneyRequestViewProps) { +- const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '-1']; ++ const parentReportAction = parentReportActions?.[report?.parentReportActionID]; +``` + +> error TS2538: Type 'undefined' cannot be used as an index type. + +This error is inside a component, so we can't just make conditions with early returns here. We can instead use `String(report?.parentReportActionID)` to try to convert the value to `string`. If the value is `undefined` the result string will be `'undefined'`, which will be used to find a record inside `parentReportActions` and, same as `-1`, would find nothing. -report ? report.reportID : '-1'; +```diff +function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = false, updatedTransaction, isFromReviewDuplicates = false}: MoneyRequestViewProps) { +- const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '-1']; ++ const parentReportAction = parentReportActions?.[String(report?.parentReportActionID)]; ``` ### Extract complex types diff --git a/src/CONST.ts b/src/CONST.ts index ddf9ebad5b66..89276c51dd13 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -16,6 +16,11 @@ import type PlaidBankAccount from './types/onyx/PlaidBankAccount'; const EMPTY_ARRAY = Object.freeze([]); const EMPTY_OBJECT = Object.freeze({}); +const DEFAULT_NUMBER_ID = 0; + +/** Only default a string ID to this value if absolutely necessary! */ +const DEFAULT_STRING_ID = ''; + const CLOUDFRONT_DOMAIN = 'cloudfront.net'; const CLOUDFRONT_URL = `https://d2k5nsl2zxldvw.${CLOUDFRONT_DOMAIN}`; const ACTIVE_EXPENSIFY_URL = Url.addTrailingForwardSlash(Config?.NEW_EXPENSIFY_URL ?? 'https://new.expensify.com'); @@ -833,6 +838,8 @@ const CONST = { CLOUDFRONT_URL, EMPTY_ARRAY, EMPTY_OBJECT, + DEFAULT_NUMBER_ID, + DEFAULT_STRING_ID, USE_EXPENSIFY_URL, GOOGLE_MEET_URL_ANDROID: 'https://meet.google.com', GOOGLE_DOC_IMAGE_LINK_MATCH: 'googleusercontent.com', From c61b46549ca19e6e710e24ff04b6009919b4fc0a Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Thu, 7 Nov 2024 02:41:57 +0700 Subject: [PATCH 035/210] add label style for subtitle and useMemo for getSubtitle function --- src/components/AttachmentModal.tsx | 6 +++--- .../AttachmentCarousel/extractAttachments.ts | 2 +- src/components/Header.tsx | 16 ++++++++++------ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index a343cd556149..1beba9f8ccf2 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -487,7 +487,7 @@ function AttachmentModal({ const submitRef = useRef(null); - const getSubTitleLink = () => { + const getSubTitleLink = useMemo(() => { if (shouldShowNotFoundPage) { return ''; } @@ -501,7 +501,7 @@ function AttachmentModal({ } return ''; - }; + }, [shouldShowNotFoundPage, report, isReceiptAttachment, attachmentCarouselImageLink, isAuthTokenRequired, imageLink]); return ( <> @@ -548,7 +548,7 @@ function AttachmentModal({ threeDotsAnchorPosition={styles.threeDotsPopoverOffsetAttachmentModal(windowWidth)} threeDotsMenuItems={threeDotsMenuItems} shouldOverlayDots - subTitleLink={getSubTitleLink()} + subTitleLink={getSubTitleLink} /> {isLoading && } diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index 251bbf64e5cd..94843c0769a6 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -91,7 +91,7 @@ function extractAttachments( } }, onclosetag: (name) => { - if (!(name === 'a') || !currentImageLink) { + if (name !== 'a' || !currentImageLink) { return; } diff --git a/src/components/Header.tsx b/src/components/Header.tsx index c0e020120511..801e0cd55ee9 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -48,9 +48,12 @@ function Header({title = '', subtitle = '', textStyles = [], containerStyles = [ [subtitle, styles], ); - const renderedSubTitleLink = () => { - return ( - + const renderedSubTitleLink = useMemo( + () => ( + { Linking.openURL(subTitleLink); @@ -59,8 +62,9 @@ function Header({title = '', subtitle = '', textStyles = [], containerStyles = [ {subTitleLink} - ); - }; + ), + [styles.label, subTitleLink], + ); return ( @@ -76,7 +80,7 @@ function Header({title = '', subtitle = '', textStyles = [], containerStyles = [ ) : title} {renderedSubtitle} - {subTitleLink && renderedSubTitleLink()} + {subTitleLink && renderedSubTitleLink} {shouldShowEnvironmentBadge && } From c210da8a4850a9536dd93baff35503280ad0363a Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:16:20 +0700 Subject: [PATCH 036/210] Rename variable to attachmentLink --- src/ROUTES.ts | 6 +++--- src/components/AttachmentModal.tsx | 16 ++++++++-------- .../AttachmentCarousel/extractAttachments.ts | 10 +++++----- src/components/Attachments/types.ts | 2 +- .../HTMLRenderers/ImageRenderer.tsx | 4 ++-- src/components/Header.tsx | 17 +++++++---------- src/libs/Navigation/types.ts | 2 +- src/pages/home/report/ReportAttachments.tsx | 6 +++--- 8 files changed, 30 insertions(+), 33 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 0cff24fadf8a..81f43c73551e 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -310,13 +310,13 @@ const ROUTES = { }, ATTACHMENTS: { route: 'attachment', - getRoute: (reportID: string, type: ValueOf, url: string, accountID?: number, isAuthTokenRequired?: boolean, imageLink?: string) => { + getRoute: (reportID: string, type: ValueOf, url: string, accountID?: number, isAuthTokenRequired?: boolean, attachmentLink?: string) => { const reportParam = reportID ? `&reportID=${reportID}` : ''; const accountParam = accountID ? `&accountID=${accountID}` : ''; const authTokenParam = isAuthTokenRequired ? '&isAuthTokenRequired=true' : ''; - const imageLinkParam = imageLink ? `&imageLink=${imageLink}` : ''; + const attachmentLinkParam = attachmentLink ? `&attachmentLink=${attachmentLink}` : ''; - return `attachment?source=${encodeURIComponent(url)}&type=${type}${reportParam}${accountParam}${authTokenParam}${imageLinkParam}` as const; + return `attachment?source=${encodeURIComponent(url)}&type=${type}${reportParam}${accountParam}${authTokenParam}${attachmentLinkParam}` as const; }, }, REPORT_PARTICIPANTS: { diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 1beba9f8ccf2..685b208b061c 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -135,7 +135,7 @@ type AttachmentModalProps = { shouldDisableSendButton?: boolean; - imageLink?: string; + attachmentLink?: string; }; function AttachmentModal({ @@ -163,7 +163,7 @@ function AttachmentModal({ type = undefined, accountID = undefined, shouldDisableSendButton = false, - imageLink = '', + attachmentLink = '', }: AttachmentModalProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -188,7 +188,7 @@ function AttachmentModal({ const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '-1', report?.parentReportActionID ?? '-1'); const transactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? '-1' : '-1'; const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); - const [attachmentCarouselImageLink, setAttachmentCarouselImageLink] = useState(''); + const [currentAttachmentLink, setCurrentAttachmentLink] = useState(''); const [file, setFile] = useState( originalFileName @@ -215,7 +215,7 @@ function AttachmentModal({ setFile(attachment.file); setIsAuthTokenRequiredState(attachment.isAuthTokenRequired ?? false); onCarouselAttachmentChange(attachment); - setAttachmentCarouselImageLink(attachment?.imageLink ?? ''); + setCurrentAttachmentLink(attachment?.attachmentLink ?? ''); }, [onCarouselAttachmentChange], ); @@ -493,15 +493,15 @@ function AttachmentModal({ } if (!isEmptyObject(report) && !isReceiptAttachment) { - return attachmentCarouselImageLink; + return currentAttachmentLink; } - if (!isAuthTokenRequired && imageLink) { - return imageLink; + if (!isAuthTokenRequired && attachmentLink) { + return attachmentLink; } return ''; - }, [shouldShowNotFoundPage, report, isReceiptAttachment, attachmentCarouselImageLink, isAuthTokenRequired, imageLink]); + }, [shouldShowNotFoundPage, report, isReceiptAttachment, currentAttachmentLink, isAuthTokenRequired, attachmentLink]); return ( <> diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index 94843c0769a6..52ec56345f80 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -28,12 +28,12 @@ function extractAttachments( // and navigating back (<) shows the image preceding the first instance, not the selected duplicate's position. const uniqueSources = new Set(); - let currentImageLink = ''; + let currentLink = ''; const htmlParser = new HtmlParser({ onopentag: (name, attribs) => { if (name === 'a' && attribs.href) { - currentImageLink = attribs.href; + currentLink = attribs.href; } if (name === 'video') { const source = tryResolveUrlFromApiRoot(attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]); @@ -86,16 +86,16 @@ function extractAttachments( file: {name: fileName, width, height}, isReceipt: false, hasBeenFlagged: attribs['data-flagged'] === 'true', - imageLink: currentImageLink, + attachmentLink: currentLink, }); } }, onclosetag: (name) => { - if (name !== 'a' || !currentImageLink) { + if (name !== 'a' || !currentLink) { return; } - currentImageLink = ''; + currentLink = ''; }, }); diff --git a/src/components/Attachments/types.ts b/src/components/Attachments/types.ts index a819e3ee9075..88b8420745eb 100644 --- a/src/components/Attachments/types.ts +++ b/src/components/Attachments/types.ts @@ -29,7 +29,7 @@ type Attachment = { duration?: number; - imageLink?: string; + attachmentLink?: string; }; export type {AttachmentSource, Attachment}; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx index a6c89c838d17..ddce928222fb 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx @@ -101,8 +101,8 @@ function ImageRenderer({tnode}: ImageRendererProps) { return; } - const imageLink = tnode.parent?.attributes?.href; - const route = ROUTES.ATTACHMENTS?.getRoute(reportID ?? '-1', type, source, accountID, isAttachmentOrReceipt, imageLink); + const attachmentLink = tnode.parent?.attributes?.href; + const route = ROUTES.ATTACHMENTS?.getRoute(reportID ?? '-1', type, source, accountID, isAttachmentOrReceipt, attachmentLink); Navigation.navigate(route); }} onLongPress={(event) => { diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 801e0cd55ee9..960d5647127b 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -50,18 +50,15 @@ function Header({title = '', subtitle = '', textStyles = [], containerStyles = [ const renderedSubTitleLink = useMemo( () => ( - { + Linking.openURL(subTitleLink); + }} numberOfLines={1} style={styles.label} > - { - Linking.openURL(subTitleLink); - }} - > - {subTitleLink} - - + {subTitleLink} + ), [styles.label, subTitleLink], ); @@ -80,7 +77,7 @@ function Header({title = '', subtitle = '', textStyles = [], containerStyles = [ ) : title} {renderedSubtitle} - {subTitleLink && renderedSubTitleLink} + {!!subTitleLink && renderedSubTitleLink} {shouldShowEnvironmentBadge && } diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 0ef81d10dede..ee368e15de98 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1549,7 +1549,7 @@ type AuthScreensParamList = CentralPaneScreensParamList & type: ValueOf; accountID: string; isAuthTokenRequired?: string; - imageLink?: string; + attachmentLink?: string; }; [SCREENS.PROFILE_AVATAR]: { accountID: string; diff --git a/src/pages/home/report/ReportAttachments.tsx b/src/pages/home/report/ReportAttachments.tsx index 372c5fd1f5ec..2fdec372c610 100644 --- a/src/pages/home/report/ReportAttachments.tsx +++ b/src/pages/home/report/ReportAttachments.tsx @@ -18,7 +18,7 @@ function ReportAttachments({route}: ReportAttachmentsProps) { const type = route.params.type; const accountID = route.params.accountID; const isAuthTokenRequired = route.params.isAuthTokenRequired; - const imageLink = route.params.imageLink; + const attachmentLink = route.params.attachmentLink; const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID || -1}`); const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP); @@ -27,7 +27,7 @@ function ReportAttachments({route}: ReportAttachmentsProps) { const onCarouselAttachmentChange = useCallback( (attachment: Attachment) => { - const routeToNavigate = ROUTES.ATTACHMENTS.getRoute(reportID, type, String(attachment.source), Number(accountID), attachment.isAuthTokenRequired, attachment.imageLink); + const routeToNavigate = ROUTES.ATTACHMENTS.getRoute(reportID, type, String(attachment.source), Number(accountID), attachment.isAuthTokenRequired, attachment.attachmentLink); Navigation.navigate(routeToNavigate); }, [reportID, type, accountID], @@ -49,7 +49,7 @@ function ReportAttachments({route}: ReportAttachmentsProps) { onCarouselAttachmentChange={onCarouselAttachmentChange} shouldShowNotFoundPage={!isLoadingApp && type !== CONST.ATTACHMENT_TYPE.SEARCH && !report?.reportID} isAuthTokenRequired={!!isAuthTokenRequired} - imageLink={imageLink ?? ''} + attachmentLink={attachmentLink ?? ''} /> ); } From 088f1c34b73889f2a4e9c07e4d1d77f4f0b24f41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 7 Nov 2024 10:10:11 +0000 Subject: [PATCH 037/210] Address comments --- contributingGuides/STYLE.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md index 755f5228a8a7..61e848835138 100644 --- a/contributingGuides/STYLE.md +++ b/contributingGuides/STYLE.md @@ -490,14 +490,13 @@ report ? report.ownerAccountID : -1; // GOOD const accountID = report?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID; -const accountID = report?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID; const reportID = report?.reportID; // GOOD report ? report.ownerAccountID : CONST.DEFAULT_NUMBER_ID; ``` -Here are some common cases you may face when fixing your code to remove the default values. +Here are some common cases you may face when fixing your code to remove the old/bad default values. #### **Case 1**: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. @@ -522,13 +521,16 @@ We need to change `Report.getNewerActions()` arguments to allow `undefined`. By ```diff function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = false, updatedTransaction, isFromReviewDuplicates = false}: MoneyRequestViewProps) { -- const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '-1']; -+ const parentReportAction = parentReportActions?.[report?.parentReportActionID]; + const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, { + canEvict: false, + }); +- const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '-1']; ++ const parentReportAction = parentReportActions?.[report?.parentReportActionID]; ``` > error TS2538: Type 'undefined' cannot be used as an index type. -This error is inside a component, so we can't just make conditions with early returns here. We can instead use `String(report?.parentReportActionID)` to try to convert the value to `string`. If the value is `undefined` the result string will be `'undefined'`, which will be used to find a record inside `parentReportActions` and, same as `-1`, would find nothing. +This error is inside a component, so we can't simply use early return conditions here. Instead, we can use `String(report?.parentReportActionID)` to try to convert the value to `string`. If the value is `undefined`, the result string will be `'undefined'`, which will be used to find a record inside `parentReportActions` and, just like `-1`, would find nothing. ```diff function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = false, updatedTransaction, isFromReviewDuplicates = false}: MoneyRequestViewProps) { From 49f2a49db137054a54ed8c42c6daaa587172d4b2 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Thu, 7 Nov 2024 16:17:23 +0100 Subject: [PATCH 038/210] remove isPolicyExpenseChat from Report --- src/libs/DebugUtils.ts | 1 - src/libs/OptionsListUtils.ts | 7 ++++++- src/libs/ReportUtils.ts | 6 +++--- src/libs/actions/IOU.ts | 4 ++-- src/pages/ProfilePage.tsx | 1 - src/pages/home/ReportScreen.tsx | 1 - src/types/onyx/Report.ts | 3 --- 7 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index d63758761c3c..110a7c604618 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -59,7 +59,6 @@ const REPORT_BOOLEAN_PROPERTIES: Array = [ 'hasOutstandingChildRequest', 'hasOutstandingChildTask', 'isOwnPolicyExpenseChat', - 'isPolicyExpenseChat', 'isPinned', 'hasParentAccess', 'isDeletedParentAction', diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 1296a64e571d..939d2a0704ad 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -833,7 +833,7 @@ function getReportOption(participant: Participant): ReportUtils.OptionData { * Get the option for a policy expense report. */ function getPolicyExpenseReportOption(participant: Participant | ReportUtils.OptionData): ReportUtils.OptionData { - const expenseReport = ReportUtils.isPolicyExpenseChat(participant) ? ReportUtils.getReportOrDraftReport(participant.reportID) : null; + const expenseReport = isOptionPolicyExpenseChat(participant) ? ReportUtils.getReportOrDraftReport(participant.reportID) : null; const visibleParticipantAccountIDs = Object.entries(expenseReport?.participants ?? {}) .filter(([, reportParticipant]) => reportParticipant && reportParticipant.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) @@ -2612,6 +2612,10 @@ function shouldUseBoldText(report: ReportUtils.OptionData): boolean { return report.isUnread === true && notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE && notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; } +function isOptionPolicyExpenseChat(option: ReportUtils.OptionData | Participant): boolean { + return option?.isPolicyExpenseChat ?? false; +} + export { getAvatarsForAccountIDs, isCurrentUser, @@ -2658,6 +2662,7 @@ export { getAttendeeOptions, getAlternateText, hasReportErrors, + isOptionPolicyExpenseChat, }; export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, Tax, TaxRatesOption, Option, OptionTree}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a62716975c01..53358b090ae9 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -488,7 +488,7 @@ type OptionData = { searchText?: string; isIOUReportOwner?: boolean | null; shouldShowSubscript?: boolean | null; - isPolicyExpenseChat?: boolean | null; + isPolicyExpenseChat?: boolean; isMoneyRequestReport?: boolean | null; isInvoiceReport?: boolean; isExpenseRequest?: boolean | null; @@ -1040,8 +1040,8 @@ function isUserCreatedPolicyRoom(report: OnyxEntry): boolean { /** * Whether the provided report is a Policy Expense chat. */ -function isPolicyExpenseChat(report: OnyxInputOrEntry | Participant): boolean { - return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT || (report?.isPolicyExpenseChat ?? false); +function isPolicyExpenseChat(report: OnyxInputOrEntry): boolean { + return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT; } function isInvoiceRoom(report: OnyxEntry): boolean { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 1db2555f3393..e060c3e71a1d 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4224,7 +4224,7 @@ function createSplitsAndOnyxData( const hasMultipleParticipants = participants.length > 1; participants.forEach((participant) => { // In a case when a participant is a workspace, even when a current user is not an owner of the workspace - const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(participant); + const isPolicyExpenseChat = OptionsListUtils.isOptionPolicyExpenseChat(participant); const splitAmount = splitShares?.[participant.accountID ?? -1]?.amount ?? IOUUtils.calculateAmount(participants.length, amount, currency, false); const splitTaxAmount = IOUUtils.calculateAmount(participants.length, taxAmount, currency, false); @@ -4844,7 +4844,7 @@ function startSplitBill({ }); participants.forEach((participant) => { - const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(participant); + const isPolicyExpenseChat = OptionsListUtils.isOptionPolicyExpenseChat(participant); if (!isPolicyExpenseChat) { return; } diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index e99fb0118d4e..b9746126135d 100755 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -73,7 +73,6 @@ const chatReportSelector = (report: OnyxEntry): OnyxEntry => parentReportActionID: report.parentReportActionID, type: report.type, chatType: report.chatType, - isPolicyExpenseChat: report.isPolicyExpenseChat, }; function ProfilePage({route}: ProfilePageProps) { diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 4c3ed5c705a5..4c86e66322ef 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -179,7 +179,6 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro writeCapability: reportOnyx.writeCapability, type: reportOnyx.type, errorFields: reportOnyx.errorFields, - isPolicyExpenseChat: reportOnyx.isPolicyExpenseChat, parentReportID: reportOnyx.parentReportID, parentReportActionID: reportOnyx.parentReportActionID, chatType: reportOnyx.chatType, diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 43c82cfdc227..fff12e60c5cd 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -89,9 +89,6 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< /** Whether the user is not an admin of policyExpenseChat chat */ isOwnPolicyExpenseChat?: boolean; - /** Whether the report is policyExpenseChat */ - isPolicyExpenseChat?: boolean; - /** Indicates if the report is pinned to the LHN or not */ isPinned?: boolean; From 2f00af7341289af78debe2f9a513f7fa9e065277 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Fri, 8 Nov 2024 14:26:03 +0700 Subject: [PATCH 039/210] fix: ValidateCodeActionModal is not dismissed on backdrop press --- src/components/ValidateCodeActionModal/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/ValidateCodeActionModal/index.tsx b/src/components/ValidateCodeActionModal/index.tsx index 461c780a50d0..0a2e74c5bccc 100644 --- a/src/components/ValidateCodeActionModal/index.tsx +++ b/src/components/ValidateCodeActionModal/index.tsx @@ -7,6 +7,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import useSafePaddingBottomStyle from '@hooks/useSafePaddingBottomStyle'; import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ValidateCodeActionModalProps} from './type'; @@ -54,6 +55,7 @@ function ValidateCodeActionModal({ isVisible={isVisible} onClose={hide} onModalHide={onModalHide ?? hide} + onBackdropPress={() => Navigation.dismissModal()} hideModalContentWhileAnimating useNativeDriver shouldUseModalPaddingStyle={false} From 471adcfde7157bf3f77dad8537a42c6030c6e98b Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Fri, 8 Nov 2024 10:26:13 +0100 Subject: [PATCH 040/210] feat: Step 4 UI --- src/CONST.ts | 24 ++ src/languages/en.ts | 39 +++ src/languages/es.ts | 39 +++ src/languages/params.ts | 5 + src/libs/ValidationUtils.ts | 26 ++ .../BeneficialOwnerCheck.tsx | 65 ++++ .../Address.tsx | 88 ++++++ .../Confirmation.tsx | 89 ++++++ .../DateOfBirth.tsx | 46 +++ .../Last4SSN.tsx | 67 ++++ .../Name.tsx | 52 +++ .../OwnershipPercentage.tsx | 73 +++++ .../BeneficialOwnerInfo.tsx | 295 +++++++++++++++++- .../BeneficialOwnersList.tsx | 115 +++++++ .../UploadOwnershipChart.tsx | 87 ++++++ .../ReimbursementAccount/NonUSD/WhyLink.tsx | 44 +++ .../utils/getValuesForBeneficialOwner.ts | 63 ++++ src/types/form/ReimbursementAccountForm.ts | 17 + src/types/onyx/ReimbursementAccount.ts | 13 + 19 files changed, 1232 insertions(+), 15 deletions(-) create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerCheck.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Address.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Confirmation.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/DateOfBirth.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Last4SSN.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Name.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/OwnershipPercentage.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnersList.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/UploadOwnershipChart.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/WhyLink.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/utils/getValuesForBeneficialOwner.ts diff --git a/src/CONST.ts b/src/CONST.ts index 23a220e88ddb..0b618a67c72b 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -612,6 +612,30 @@ const CONST = { AGREEMENTS: 'AgreementsStep', FINISH: 'FinishStep', }, + BENEFICIAL_OWNER_INFO_STEP: { + SUBSTEP: { + IS_USER_BENEFICIAL_OWNER: 1, + IS_ANYONE_ELSE_BENEFICIAL_OWNER: 2, + BENEFICIAL_OWNER_DETAILS_FORM: 3, + ARE_THERE_MORE_BENEFICIAL_OWNERS: 4, + OWNERSHIP_CHART: 5, + BENEFICIAL_OWNERS_LIST: 6, + }, + BENEFICIAL_OWNER_DATA: { + BENEFICIAL_OWNER_KEYS: 'beneficialOwnerKeys', + PREFIX: 'beneficialOwner', + FIRST_NAME: 'firstName', + LAST_NAME: 'lastName', + OWNERSHIP_PERCENTAGE: 'ownershipPercentage', + DOB: 'dob', + SSN_LAST_4: 'ssnLast4', + STREET: 'street', + CITY: 'city', + STATE: 'state', + ZIP_CODE: 'zipCode', + COUNTRY: 'country', + }, + }, STEP_NAMES: ['1', '2', '3', '4', '5', '6'], STEP_HEADER_HEIGHT: 40, }, diff --git a/src/languages/en.ts b/src/languages/en.ts index b45243b65af5..5111b691254e 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -41,6 +41,7 @@ import type { CharacterLimitParams, CompanyCardBankName, CompanyCardFeedNameParams, + CompanyNameParams, ConfirmThatParams, ConnectionNameParams, ConnectionParams, @@ -1944,6 +1945,7 @@ const translations = { lastName: 'Please enter a valid last name.', noDefaultDepositAccountOrDebitCardAvailable: 'Please add a default deposit account or debit card.', validationAmounts: 'The validation amounts you entered are incorrect. Please double check your bank statement and try again.', + ownershipPercentage: 'Please enter a valid percentage number that is greater than 25', }, }, addPersonalBankAccountPage: { @@ -2219,6 +2221,43 @@ const translations = { byAddingThisBankAccount: "By adding this bank account, you confirm that you've read, understand, and accept", owners: 'Owners', }, + ownershipInfoStep: { + ownerInfo: 'Owner info', + businessOwner: 'Business owner', + signerInfo: 'Signer info', + doYouOwn: ({companyName}: CompanyNameParams) => `Do you own 25% or more of ${companyName}`, + doesAnyoneOwn: ({companyName}: CompanyNameParams) => `Does any individuals own 25% or more of ${companyName}`, + regulationsRequire: 'Regulations require us to verify the identity of any individual who owns more than 25% of the business.', + legalFirstName: 'Legal first name', + legalLastName: 'Legal last name', + whatsTheOwnersName: "What's the owner's legal name?", + whatsYourName: "What's your legal name?", + whatPercentage: 'What percentage of the business belongs to the owner?', + whatsYoursPercentage: 'What percentage of the business do you own?', + ownership: 'Ownership', + whatsTheOwnersDOB: "What's the owner's date of birth?", + whatsYourDOB: "What's your date of birth?", + whatsTheOwnersAddress: "What's the owner's address?", + whatsYourAddress: "What's your address?", + whatAreTheLast: "What are the last 4 digits of the owner's Social Security Number?", + whatsYourLast: 'What are the last 4 digits of your Social Security Number?', + dontWorry: "Don't worry, we don't do any personal credit checks!", + last4: 'Last 4 of SSN', + whyDoWeAsk: 'Why do we ask for this?', + letsDoubleCheck: 'Let’s double check that everything looks right.', + legalName: 'Legal name', + ownershipPercentage: 'Ownership percentage', + areThereOther: ({companyName}: CompanyNameParams) => `Are there other individuals who own 25% or more of ${companyName}`, + owners: 'Owners', + addCertified: 'Add a certified org chart that shows the beneficial owners', + regulationRequiresChart: 'Regulation requires us to collect a certified copy of the ownership chart that shows every individual or entity who owns 25% or more of the business.', + uploadEntity: 'Upload entity ownership chart', + noteEntity: 'Note: Entity ownership chart must be signed by your accountant, legal counsel, or notarized.', + certified: 'Certified entity ownership chart', + selectCountry: 'Select country', + findCountry: 'Find country', + address: 'Address', + }, validationStep: { headerTitle: 'Validate bank account', buttonText: 'Finish setup', diff --git a/src/languages/es.ts b/src/languages/es.ts index 31e713582168..449c7a04d719 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -39,6 +39,7 @@ import type { CharacterLimitParams, CompanyCardBankName, CompanyCardFeedNameParams, + CompanyNameParams, ConfirmThatParams, ConnectionNameParams, ConnectionParams, @@ -1964,6 +1965,7 @@ const translations = { lastName: 'Por favor, introduce los apellidos.', noDefaultDepositAccountOrDebitCardAvailable: 'Por favor, añade una cuenta bancaria para depósitos o una tarjeta de débito.', validationAmounts: 'Los importes de validación que introduciste son incorrectos. Por favor, comprueba tu cuenta bancaria e inténtalo de nuevo.', + ownershipPercentage: 'Por favor, ingrese un número de porcentaje válido que sea mayor a 25', }, }, addPersonalBankAccountPage: { @@ -2242,6 +2244,43 @@ const translations = { byAddingThisBankAccount: 'Al añadir esta cuenta bancaria, confirmas que has leído, comprendido y aceptado', owners: 'Dueños', }, + ownershipInfoStep: { + ownerInfo: 'Información del propietario', + businessOwner: 'Propietario del negocio', + signerInfo: 'Información del firmante', + doYouOwn: ({companyName}: CompanyNameParams) => `¿Posee el 25% o más de ${companyName}?`, + doesAnyoneOwn: ({companyName}: CompanyNameParams) => `¿Alguien posee el 25% o más de ${companyName}?`, + regulationsRequire: 'Las regulaciones requieren que verifiquemos la identidad de cualquier persona que posea más del 25% del negocio.', + legalFirstName: 'Nombre legal', + legalLastName: 'Apellido legal', + whatsTheOwnersName: '¿Cuál es el nombre legal del propietario?', + whatsYourName: '¿Cuál es su nombre legal?', + whatPercentage: '¿Qué porcentaje del negocio pertenece al propietario?', + whatsYoursPercentage: '¿Qué porcentaje del negocio posee?', + ownership: 'Propiedad', + whatsTheOwnersDOB: '¿Cuál es la fecha de nacimiento del propietario?', + whatsYourDOB: '¿Cuál es su fecha de nacimiento?', + whatsTheOwnersAddress: '¿Cuál es la dirección del propietario?', + whatsYourAddress: '¿Cuál es su dirección?', + whatAreTheLast: '¿Cuáles son los últimos 4 dígitos del número de seguro social del propietario?', + whatsYourLast: '¿Cuáles son los últimos 4 dígitos de su número de seguro social?', + dontWorry: 'No se preocupe, ¡no realizamos ninguna verificación de crédito personal!', + last4: 'Últimos 4 del SSN', + whyDoWeAsk: '¿Por qué solicitamos esto?', + letsDoubleCheck: 'Verifiquemos que todo esté correcto.', + legalName: 'Nombre legal', + ownershipPercentage: 'Porcentaje de propiedad', + areThereOther: ({companyName}: CompanyNameParams) => `¿Hay otras personas que posean el 25% o más de ${companyName}?`, + owners: 'Propietarios', + addCertified: 'Agregue un organigrama certificado que muestre los propietarios beneficiarios', + regulationRequiresChart: 'La regulación nos exige recopilar una copia certificada del organigrama que muestre a cada persona o entidad que posea el 25% o más del negocio.', + uploadEntity: 'Subir organigrama de propiedad de la entidad', + noteEntity: 'Nota: El organigrama de propiedad de la entidad debe estar firmado por su contador, asesor legal o notariado.', + certified: 'Organigrama certificado de propiedad de la entidad', + selectCountry: 'Seleccionar país', + findCountry: 'Buscar país', + address: 'Dirección', + }, validationStep: { headerTitle: 'Validar cuenta bancaria', buttonText: 'Finalizar configuración', diff --git a/src/languages/params.ts b/src/languages/params.ts index 2d60c13c4dd0..c40bc01447c3 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -551,6 +551,10 @@ type CurrencyCodeParams = { currencyCode: string; }; +type CompanyNameParams = { + companyName: string; +}; + export type { AuthenticationErrorParams, ImportMembersSuccessfullDescriptionParams, @@ -751,4 +755,5 @@ export type { AssignCardParams, ImportedTypesParams, CurrencyCodeParams, + CompanyNameParams, }; diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 47e44bc049d2..e5ec86d03ba7 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -530,6 +530,31 @@ function isValidZipCodeInternational(zipCode: string): boolean { return /^[a-z0-9][a-z0-9\- ]{0,10}[a-z0-9]$/.test(zipCode); } +/** + * Validates the given value if it is correct ownership percentage + * @param value + * @param totalOwnedPercentage + * @param ownerBeingModifiedID + */ +function isValidOwnershipPercentage(value: string, totalOwnedPercentage: Record, ownerBeingModifiedID: string): boolean { + const parsedValue = Number(value); + const isValidNumber = !Number.isNaN(parsedValue) && parsedValue >= 25 && parsedValue <= 100; + + let totalOwnedPercentageSum = 0; + const totalOwnedPercentageKeys = Object.keys(totalOwnedPercentage); + totalOwnedPercentageKeys.forEach((key) => { + if (key === ownerBeingModifiedID) { + return; + } + + totalOwnedPercentageSum += totalOwnedPercentage[key]; + }); + + const isTotalSumValid = totalOwnedPercentageSum + parsedValue <= 100; + + return isValidNumber && isTotalSumValid; +} + export { meetsMinimumAgeRequirement, meetsMaximumAgeRequirement, @@ -577,4 +602,5 @@ export { isValidEmail, isValidPhoneInternational, isValidZipCodeInternational, + isValidOwnershipPercentage, }; diff --git a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerCheck.tsx b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerCheck.tsx new file mode 100644 index 000000000000..4d108de6dae1 --- /dev/null +++ b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerCheck.tsx @@ -0,0 +1,65 @@ +import React, {useMemo, useState} from 'react'; +import FormProvider from '@components/Form/FormProvider'; +import type {Choice} from '@components/RadioButtons'; +import RadioButtons from '@components/RadioButtons'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type BeneficialOwnerCheckProps = { + /** The title of the question */ + title: string; + + /** The default value of the radio button */ + defaultValue: boolean; + + /** Callback when the value is selected */ + onSelectedValue: (value: boolean) => void; +}; + +function BeneficialOwnerCheck({title, onSelectedValue, defaultValue}: BeneficialOwnerCheckProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const [value, setValue] = useState(defaultValue); + + const handleSubmit = () => { + onSelectedValue(value); + }; + const handleSelectValue = (newValue: string) => setValue(newValue === 'true'); + const options = useMemo( + () => [ + { + label: translate('common.yes'), + value: 'true', + }, + { + label: translate('common.no'), + value: 'false', + }, + ], + [translate], + ); + + return ( + + {title} + {translate('ownershipInfoStep.regulationsRequire')} + + + ); +} + +BeneficialOwnerCheck.displayName = 'BeneficialOwnerCheck'; + +export default BeneficialOwnerCheck; diff --git a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Address.tsx b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Address.tsx new file mode 100644 index 000000000000..1629b90a5308 --- /dev/null +++ b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Address.tsx @@ -0,0 +1,88 @@ +import React, {useMemo, useState} from 'react'; +import {useOnyx} from 'react-native-onyx'; +import AddressStep from '@components/SubStepForms/AddressStep'; +import useLocalize from '@hooks/useLocalize'; +import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import CONST from '@src/CONST'; +import type {Country} from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type NameProps = SubStepProps & {isUserEnteringHisOwnData: boolean; ownerBeingModifiedID: string}; + +const {STREET, CITY, STATE, ZIP_CODE, COUNTRY, PREFIX} = CONST.NON_USD_BANK_ACCOUNT.BENEFICIAL_OWNER_INFO_STEP.BENEFICIAL_OWNER_DATA; + +function Address({onNext, isEditing, onMove, isUserEnteringHisOwnData, ownerBeingModifiedID}: NameProps) { + const {translate} = useLocalize(); + const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); + + const countryInputKey: `beneficialOwner_${string}_${string}` = `${PREFIX}_${ownerBeingModifiedID}_${COUNTRY}`; + const inputKeys = { + street: `${PREFIX}_${ownerBeingModifiedID}_${STREET}`, + city: `${PREFIX}_${ownerBeingModifiedID}_${CITY}`, + state: `${PREFIX}_${ownerBeingModifiedID}_${STATE}`, + zipCode: `${PREFIX}_${ownerBeingModifiedID}_${ZIP_CODE}`, + country: countryInputKey, + } as const; + + const defaultValues = { + street: reimbursementAccountDraft?.[inputKeys.street] ?? '', + city: reimbursementAccountDraft?.[inputKeys.city] ?? '', + state: reimbursementAccountDraft?.[inputKeys.state] ?? '', + zipCode: reimbursementAccountDraft?.[inputKeys.zipCode] ?? '', + country: (reimbursementAccountDraft?.[inputKeys.country] ?? '') as Country | '', + }; + + const formTitle = translate(isUserEnteringHisOwnData ? 'ownershipInfoStep.whatsYourAddress' : 'ownershipInfoStep.whatsTheOwnersAddress'); + + // Has to be stored in state and updated on country change due to the fact that we can't relay on onyxValues when user is editing the form (draft values are not being saved in that case) + const [shouldDisplayStateSelector, setShouldDisplayStateSelector] = useState( + defaultValues.country === CONST.COUNTRY.US || defaultValues.country === CONST.COUNTRY.CA || defaultValues.country === '', + ); + + const stepFieldsWithState = useMemo( + () => [inputKeys.street, inputKeys.city, inputKeys.state, inputKeys.zipCode, countryInputKey], + [countryInputKey, inputKeys.city, inputKeys.state, inputKeys.street, inputKeys.zipCode], + ); + const stepFieldsWithoutState = useMemo( + () => [inputKeys.street, inputKeys.city, inputKeys.zipCode, countryInputKey], + [countryInputKey, inputKeys.city, inputKeys.street, inputKeys.zipCode], + ); + + const stepFields = shouldDisplayStateSelector ? stepFieldsWithState : stepFieldsWithoutState; + + const handleCountryChange = (country: unknown) => { + if (typeof country !== 'string' || country === '') { + return; + } + setShouldDisplayStateSelector(country === CONST.COUNTRY.US || country === CONST.COUNTRY.CA); + }; + + const handleSubmit = useReimbursementAccountStepFormSubmit({ + fieldIds: stepFields, + onNext, + shouldSaveDraft: isEditing, + }); + + return ( + + isEditing={isEditing} + onNext={onNext} + onMove={onMove} + formID={ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM} + formTitle={formTitle} + formPOBoxDisclaimer={translate('common.noPO')} + onSubmit={handleSubmit} + stepFields={stepFields} + inputFieldsIDs={inputKeys} + defaultValues={defaultValues} + onCountryChange={handleCountryChange} + shouldDisplayStateSelector={shouldDisplayStateSelector} + shouldDisplayCountrySelector + /> + ); +} + +Address.displayName = 'Address'; + +export default Address; diff --git a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Confirmation.tsx b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Confirmation.tsx new file mode 100644 index 000000000000..6b880e8b3ad1 --- /dev/null +++ b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Confirmation.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import Button from '@components/Button'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import SafeAreaConsumer from '@components/SafeAreaConsumer'; +import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import useThemeStyles from '@hooks/useThemeStyles'; +import getValuesForBeneficialOwner from '@pages/ReimbursementAccount/NonUSD/utils/getValuesForBeneficialOwner'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type ConfirmationProps = SubStepProps & {ownerBeingModifiedID: string}; + +function Confirmation({onNext, onMove, ownerBeingModifiedID}: ConfirmationProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); + const values = getValuesForBeneficialOwner(ownerBeingModifiedID, reimbursementAccountDraft); + + return ( + + {({safeAreaPaddingBottomStyle}) => ( + + {translate('ownershipInfoStep.letsDoubleCheck')} + { + onMove(0); + }} + /> + { + onMove(1); + }} + /> + { + onMove(2); + }} + /> + { + onMove(4); + }} + /> + { + onMove(3); + }} + /> + +