From 8768859e04357c3bfd0992c30de0e21c2d7d5605 Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Fri, 20 Sep 2024 09:42:33 +0700 Subject: [PATCH 001/128] 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/128] 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/128] 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/128] =?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/128] 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/128] 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/128] 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/128] 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/128] 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/128] 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/128] 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/128] 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 cf582b458f571ed64722c10056b3b86fd797ed0f Mon Sep 17 00:00:00 2001 From: truph01 Date: Wed, 30 Oct 2024 08:15:52 +0700 Subject: [PATCH 013/128] 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 014/128] 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 015/128] 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 016/128] 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 017/128] 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 018/128] 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 019/128] 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 020/128] 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 d5490b83d4059c4b940f874cd76578ac92f97f6f Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 5 Nov 2024 16:01:11 +0700 Subject: [PATCH 021/128] 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 022/128] 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 7e6f6c14e0d5d20969e35874b4790df7c5b0c725 Mon Sep 17 00:00:00 2001 From: truph01 Date: Tue, 5 Nov 2024 20:04:21 +0700 Subject: [PATCH 023/128] 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 9070620537f3ca0bd022dee4c8f495a904b7f469 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Wed, 6 Nov 2024 02:36:29 +0530 Subject: [PATCH 024/128] 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 c57030110d6b8c9ba0a9a453352c4b417f856935 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Wed, 6 Nov 2024 09:00:19 +0700 Subject: [PATCH 025/128] Fix list not scrolled up when search query empty --- src/components/Search/SearchRouter/SearchRouter.tsx | 7 ++++++- src/components/SelectionList/BaseSelectionList.tsx | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 83d7d5d89b20..6f62a08db00f 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -37,6 +37,7 @@ import ROUTES from '@src/ROUTES'; import SearchRouterInput from './SearchRouterInput'; import SearchRouterList from './SearchRouterList'; import type {ItemWithQuery} from './SearchRouterList'; +import isEmpty from 'lodash/isEmpty'; type SearchRouterProps = { onRouterClose: () => void; @@ -293,6 +294,7 @@ function SearchRouter({onRouterClose}: SearchRouterProps) { ], ); + const prevUserQueryRef = useRef(null); const onSearchChange = useCallback( (userQuery: string) => { let newUserQuery = userQuery; @@ -302,11 +304,14 @@ function SearchRouter({onRouterClose}: SearchRouterProps) { setTextInputValue(newUserQuery); const autocompleteParsedQuery = parseForAutocomplete(newUserQuery); updateAutocomplete(autocompleteParsedQuery?.autocomplete?.value ?? '', autocompleteParsedQuery?.ranges ?? [], autocompleteParsedQuery?.autocomplete?.key); - if (newUserQuery) { + if (newUserQuery || !isEmpty(prevUserQueryRef.current)) { listRef.current?.updateAndScrollToFocusedIndex(0); } else { listRef.current?.updateAndScrollToFocusedIndex(-1); } + + // Store the previous newUserQuery + prevUserQueryRef.current = newUserQuery; }, [autocompleteSuggestions, setTextInputValue, updateAutocomplete], ); diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 3e1b3a3c2d70..ffb6c64a0fc5 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -572,8 +572,9 @@ function BaseSelectionList( } // Remove the focus if the search input is empty or selected options length is changed (and allOptions length remains the same) // else focus on the first non disabled item + const newSelectedIndex = - textInputValue === '' || (flattenedSections.selectedOptions.length !== prevSelectedOptionsLength && prevAllOptionsLength === flattenedSections.allOptions.length) ? -1 : 0; + (isEmpty(prevTextInputValue) && textInputValue === '') || (flattenedSections.selectedOptions.length !== prevSelectedOptionsLength && prevAllOptionsLength === flattenedSections.allOptions.length) ? -1 : 0; // reseting the currrent page to 1 when the user types something setCurrentPage(1); From 88cd7752bfb26bf72e61ab75e639d7650534b042 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Wed, 6 Nov 2024 12:16:50 +0530 Subject: [PATCH 026/128] 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 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 027/128] 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 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 028/128] 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 2f00af7341289af78debe2f9a513f7fa9e065277 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Fri, 8 Nov 2024 14:26:03 +0700 Subject: [PATCH 029/128] 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 030/128] 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); + }} + /> + +