From 800c2d6221bfdc55cd91528b5fa6ffe586dcb073 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 29 May 2024 13:18:10 +0800 Subject: [PATCH 001/270] shows a different message when approving all hold expenses --- src/components/ProcessMoneyReportHoldMenu.tsx | 16 ++++++++++++++-- src/languages/en.ts | 1 + 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx index 6e81c9d57bc8..e5240b6997c0 100644 --- a/src/components/ProcessMoneyReportHoldMenu.tsx +++ b/src/components/ProcessMoneyReportHoldMenu.tsx @@ -1,9 +1,10 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import {isLinkedTransactionHeld} from '@libs/ReportActionsUtils'; import * as IOU from '@userActions/IOU'; +import {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; @@ -64,12 +65,23 @@ function ProcessMoneyReportHoldMenu({ onClose(); }; + const promptText = useMemo(() => { + let promptTranslation: TranslationPaths; + if (nonHeldAmount) { + promptTranslation = isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount'; + } else { + promptTranslation = isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAmount'; + } + + return translate(promptTranslation); + }, [nonHeldAmount]); + return ( onSubmit(false)} diff --git a/src/languages/en.ts b/src/languages/en.ts index 0f5822b9f411..9122d9292a96 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -753,6 +753,7 @@ export default { expenseOnHold: 'This expense was put on hold. Review the comments for next steps.', confirmApprove: 'Confirm approval amount', confirmApprovalAmount: "Approve what's not on hold, or approve the entire report.", + confirmApprovalAllHoldAmount: 'All expenses on this report are on hold. Do you want to unhold them and approve?', confirmPay: 'Confirm payment amount', confirmPayAmount: "Pay what's not on hold, or pay all out-of-pocket spend.", payOnly: 'Pay only', From 9229520889e84d7d54be9e9d7c24c8813a2a9204 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 23 May 2024 09:44:21 +0200 Subject: [PATCH 002/270] Support invoice paying as business --- src/components/MoneyReportHeader.tsx | 4 +- .../ReportActionItem/ReportPreview.tsx | 4 +- src/components/SettlementButton.tsx | 23 ++++++++++- src/libs/API/parameters/PayInvoiceParams.ts | 1 + src/libs/ReportUtils.ts | 5 +++ src/libs/actions/IOU.ts | 38 +++++++++++++------ src/libs/actions/Policy/Policy.ts | 2 +- 7 files changed, 60 insertions(+), 17 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index ec52a6158ad7..43b2ca6729ea 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -119,7 +119,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const displayedAmount = ReportUtils.hasHeldExpenses(moneyRequestReport.reportID) && canAllowSettlement ? nonHeldAmount : formattedAmount; const isMoreContentShown = shouldShowNextStep || hasScanningReceipt || (shouldShowAnyButton && shouldUseNarrowLayout); - const confirmPayment = (type?: PaymentMethodType | undefined) => { + const confirmPayment = (type?: PaymentMethodType | undefined, payAsBusiness?: boolean) => { if (!type || !chatReport) { return; } @@ -128,7 +128,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea if (ReportUtils.hasHeldExpenses(moneyRequestReport.reportID)) { setIsHoldMenuVisible(true); } else if (ReportUtils.isInvoiceReport(moneyRequestReport)) { - IOU.payInvoice(type, chatReport, moneyRequestReport); + IOU.payInvoice(type, chatReport, moneyRequestReport, payAsBusiness); } else { IOU.payMoneyRequest(type, chatReport, moneyRequestReport, true); } diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 4677563d204f..abdb963530de 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -280,12 +280,12 @@ function ReportPreview({ }; }, [formattedMerchant, formattedDescription, moneyRequestComment, translate, numberOfRequests, numberOfScanningReceipts, numberOfPendingRequests]); - const confirmPayment = (paymentMethodType?: PaymentMethodType) => { + const confirmPayment = (paymentMethodType?: PaymentMethodType, payAsBusiness?: boolean) => { if (!paymentMethodType || !chatReport || !iouReport) { return; } if (ReportUtils.isInvoiceReport(iouReport)) { - IOU.payInvoice(paymentMethodType, chatReport, iouReport); + IOU.payInvoice(paymentMethodType, chatReport, iouReport, payAsBusiness); } else { IOU.payMoneyRequest(paymentMethodType, chatReport, iouReport); } diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx index b6e2a753c829..c5eaa0dec336 100644 --- a/src/components/SettlementButton.tsx +++ b/src/components/SettlementButton.tsx @@ -1,14 +1,16 @@ import React, {useEffect, useMemo} from 'react'; import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import * as BankAccounts from '@userActions/BankAccounts'; import * as IOU from '@userActions/IOU'; import * as PaymentMethods from '@userActions/PaymentMethods'; +import * as PolicyActions from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; @@ -143,6 +145,9 @@ function SettlementButton({ }: SettlementButtonProps) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); + + const primaryPolicy = useMemo(() => PolicyActions.getPrimaryPolicy(activePolicyID) ?? null, [activePolicyID]); useEffect(() => { PaymentMethods.openWalletPage(); @@ -216,6 +221,22 @@ function SettlementButton({ }, ], }); + + if (PolicyUtils.isPolicyAdmin(primaryPolicy)) { + buttonOptions.push({ + text: translate('iou.settleBusiness', {formattedAmount}), + icon: Expensicons.Building, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + subMenuItems: [ + { + text: translate('iou.payElsewhere', {formattedAmount: ''}), + icon: Expensicons.Cash, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + onSelected: () => onPress(CONST.IOU.PAYMENT_TYPE.ELSEWHERE, true), + }, + ], + }); + } } if (shouldShowApproveButton) { diff --git a/src/libs/API/parameters/PayInvoiceParams.ts b/src/libs/API/parameters/PayInvoiceParams.ts index 4c6633749adb..a6b9746d87bc 100644 --- a/src/libs/API/parameters/PayInvoiceParams.ts +++ b/src/libs/API/parameters/PayInvoiceParams.ts @@ -4,6 +4,7 @@ type PayInvoiceParams = { reportID: string; reportActionID: string; paymentMethodType: PaymentMethodType; + payAsBusiness: boolean; }; export default PayInvoiceParams; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7e99c60cb618..7f06ebad8601 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -905,6 +905,10 @@ function isInvoiceRoom(report: OnyxEntry | EmptyObject): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.INVOICE; } +function isIndividualInvoiceRoom(report: OnyxEntry): boolean { + return isInvoiceRoom(report) && report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL; +} + function isCurrentUserInvoiceReceiver(report: OnyxEntry): boolean { if (report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { return currentUserAccountID === report.invoiceReceiver.accountID; @@ -7065,6 +7069,7 @@ export { isCurrentUserInvoiceReceiver, isDraftReport, createDraftWorkspaceAndNavigateToConfirmationScreen, + isIndividualInvoiceRoom, }; export type { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index a98a9c315173..6e8d065fb625 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -285,6 +285,12 @@ Onyx.connect({ }, }); +let primaryPolicyID: OnyxEntry; +Onyx.connect({ + key: ONYXKEYS.NVP_ACTIVE_POLICY_ID, + callback: (value) => (primaryPolicyID = value), +}); + /** * Find the report preview action from given chat report and iou report */ @@ -5850,6 +5856,7 @@ function getPayMoneyRequestParams( recipient: Participant, paymentMethodType: PaymentMethodType, full: boolean, + payAsBusiness?: boolean, ): PayMoneyRequestData { const isInvoiceReport = ReportUtils.isInvoiceReport(iouReport); @@ -5884,19 +5891,27 @@ function getPayMoneyRequestParams( optimisticNextStep = NextStepUtils.buildNextStep(iouReport, CONST.REPORT.STATUS_NUM.REIMBURSED, {isPaidWithExpensify: paymentMethodType === CONST.IOU.PAYMENT_TYPE.VBBA}); } + const optimisticChatReport = { + ...chatReport, + lastReadTime: DateUtils.getDBTime(), + lastVisibleActionCreated: optimisticIOUReportAction.created, + hasOutstandingChildRequest: false, + iouReportID: null, + lastMessageText: optimisticIOUReportAction.message?.[0]?.text, + lastMessageHtml: optimisticIOUReportAction.message?.[0]?.html, + }; + if (ReportUtils.isIndividualInvoiceRoom(chatReport) && payAsBusiness && primaryPolicyID) { + optimisticChatReport.invoiceReceiver = { + type: CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, + policyID: primaryPolicyID, + }; + } + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, - value: { - ...chatReport, - lastReadTime: DateUtils.getDBTime(), - lastVisibleActionCreated: optimisticIOUReportAction.created, - hasOutstandingChildRequest: false, - iouReportID: null, - lastMessageText: optimisticIOUReportAction.message?.[0]?.text, - lastMessageHtml: optimisticIOUReportAction.message?.[0]?.html, - }, + value: optimisticChatReport, }, { onyxMethod: Onyx.METHOD.MERGE, @@ -6495,19 +6510,20 @@ function payMoneyRequest(paymentType: PaymentMethodType, chatReport: OnyxTypes.R Navigation.dismissModalWithReport(chatReport); } -function payInvoice(paymentMethodType: PaymentMethodType, chatReport: OnyxTypes.Report, invoiceReport: OnyxTypes.Report) { +function payInvoice(paymentMethodType: PaymentMethodType, chatReport: OnyxTypes.Report, invoiceReport: OnyxTypes.Report, payAsBusiness = false) { const recipient = {accountID: invoiceReport.ownerAccountID}; const { optimisticData, successData, failureData, params: {reportActionID}, - } = getPayMoneyRequestParams(chatReport, invoiceReport, recipient, paymentMethodType, true); + } = getPayMoneyRequestParams(chatReport, invoiceReport, recipient, paymentMethodType, true, payAsBusiness); const params: PayInvoiceParams = { reportID: invoiceReport.reportID, reportActionID, paymentMethodType, + payAsBusiness, }; API.write(WRITE_COMMANDS.PAY_INVOICE, params, {optimisticData, successData, failureData}); diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 4d918352ba91..f3d152d81c40 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -235,7 +235,7 @@ function getPolicy(policyID: string | undefined): Policy | EmptyObject { */ function getPrimaryPolicy(activePolicyID?: OnyxEntry): Policy | undefined { const activeAdminWorkspaces = PolicyUtils.getActiveAdminWorkspaces(allPolicies); - const primaryPolicy: Policy | null | undefined = allPolicies?.[activePolicyID ?? '']; + const primaryPolicy: Policy | null | undefined = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`]; return primaryPolicy ?? activeAdminWorkspaces[0]; } From 7f6e6eb93d212a88964851efa876122a10549adb Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 24 May 2024 14:33:03 +0200 Subject: [PATCH 003/270] UI updates to support B2B invoices rooms --- .../ReportActionItem/ReportPreview.tsx | 12 +++- src/libs/ReportUtils.ts | 70 ++++++++++++++++++- .../home/report/ReportActionItemSingle.tsx | 20 +----- 3 files changed, 80 insertions(+), 22 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index abdb963530de..36831e26291e 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -125,6 +125,7 @@ function ReportPreview({ const iouSettled = ReportUtils.isSettled(iouReportID); 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); @@ -205,7 +206,16 @@ function ReportPreview({ if (isScanning) { return translate('common.receipt'); } - let payerOrApproverName = isPolicyExpenseChat ? ReportUtils.getPolicyName(chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true); + + let payerOrApproverName; + if (isPolicyExpenseChat) { + payerOrApproverName = ReportUtils.getPolicyName(chatReport); + } else if (isInvoiceRoom) { + payerOrApproverName = ReportUtils.getInvoicePayerName(chatReport); + } else { + payerOrApproverName = ReportUtils.getDisplayNameForParticipant(managerID, true); + } + if (isApproved) { return translate('iou.managerApproved', {manager: payerOrApproverName}); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7f06ebad8601..b2a18925c9b4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -914,6 +914,11 @@ function isCurrentUserInvoiceReceiver(report: OnyxEntry): boolean { return currentUserAccountID === report.invoiceReceiver.accountID; } + if (report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS) { + const policy = PolicyUtils.getPolicy(report.invoiceReceiver.policyID); + return PolicyUtils.isPolicyAdmin(policy); + } + return false; } @@ -1855,6 +1860,45 @@ function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = f return shouldUseShortForm ? shortName : longName; } +function getSecondaryAvatar(chatReport: OnyxEntry, iouReport: OnyxEntry, displayAllActors: boolean, isWorkspaceActor: boolean, actorAccountID?: number): Icon { + let secondaryAvatar: Icon; + + if (displayAllActors) { + if (!isIndividualInvoiceRoom(chatReport)) { + const secondaryPolicyID = chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : '-1'; + const secondaryPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${secondaryPolicyID}`]; + const avatar = secondaryPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(secondaryPolicy?.name); + + secondaryAvatar = { + source: avatar, + type: CONST.ICON_TYPE_WORKSPACE, + name: secondaryPolicy?.name, + id: secondaryPolicyID, + }; + } else { + const secondaryAccountId = iouReport?.ownerAccountID === actorAccountID || isInvoiceReport(iouReport) ? iouReport?.managerID : iouReport?.ownerAccountID; + const secondaryUserAvatar = allPersonalDetails?.[secondaryAccountId ?? -1]?.avatar ?? ''; + const secondaryDisplayName = getDisplayNameForParticipant(secondaryAccountId); + + secondaryAvatar = { + source: UserUtils.getAvatar(secondaryUserAvatar, secondaryAccountId), + type: CONST.ICON_TYPE_AVATAR, + name: secondaryDisplayName ?? '', + id: secondaryAccountId, + }; + } + } else if (!isWorkspaceActor) { + const avatarIconIndex = chatReport?.isOwnPolicyExpenseChat || isPolicyExpenseChat(chatReport) ? 0 : 1; + const reportIcons = getIcons(chatReport, {}); + + secondaryAvatar = reportIcons[avatarIconIndex]; + } else { + secondaryAvatar = {name: '', source: '', type: 'avatar'}; + } + + return secondaryAvatar; +} + function getParticipantAccountIDs(reportID: string) { const report = getReport(reportID); if (!report || !report.participants) { @@ -2018,7 +2062,12 @@ function getIcons( } else { const receiverPolicy = getPolicy(report?.invoiceReceiver?.policyID); if (!isEmptyObject(receiverPolicy)) { - icons.push(getWorkspaceIcon(report, receiverPolicy)); + icons.push({ + source: receiverPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(receiverPolicy.name), + type: CONST.ICON_TYPE_WORKSPACE, + name: receiverPolicy.name, + id: receiverPolicy.id, + }); } } } @@ -2093,7 +2142,12 @@ function getIcons( const receiverPolicy = getPolicy(invoiceRoomReport?.invoiceReceiver?.policyID); if (!isEmptyObject(receiverPolicy)) { - icons.push(getWorkspaceIcon(invoiceRoomReport, receiverPolicy)); + icons.push({ + source: receiverPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(receiverPolicy.name), + type: CONST.ICON_TYPE_WORKSPACE, + name: receiverPolicy.name, + id: receiverPolicy.id, + }); } return icons; @@ -2531,7 +2585,16 @@ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry< const moneyRequestTotal = getMoneyRequestSpendBreakdown(report).totalDisplaySpend; const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report?.currency); - let payerOrApproverName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID) ?? ''; + let payerOrApproverName; + if (isExpenseReport(report)) { + payerOrApproverName = getPolicyName(report, false, policy); + } else if (isInvoiceReport(report)) { + const chatReport = getReport(report?.chatReportID); + payerOrApproverName = getInvoicePayerName(chatReport); + } else { + payerOrApproverName = getDisplayNameForParticipant(report?.managerID) ?? ''; + } + const payerPaidAmountMessage = Localize.translateLocal('iou.payerPaidAmount', { payer: payerOrApproverName, amount: formattedAmount, @@ -7070,6 +7133,7 @@ export { isDraftReport, createDraftWorkspaceAndNavigateToConfirmationScreen, isIndividualInvoiceRoom, + getSecondaryAvatar, }; export type { diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index f71db06c2d44..e6a870879598 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -84,7 +84,7 @@ function ReportActionItemSingle({ const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID ?? -1] ?? {}; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); - const displayAllActors = useMemo(() => action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && iouReport, [action?.actionName, iouReport]); + const displayAllActors = useMemo(() => action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && !!iouReport, [action?.actionName, iouReport]); const isInvoiceReport = ReportUtils.isInvoiceReport(iouReport ?? {}); const isWorkspaceActor = isInvoiceReport || (ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors)); let avatarSource = avatar; @@ -107,32 +107,16 @@ function ReportActionItemSingle({ } // If this is a report preview, display names and avatars of both people involved - let secondaryAvatar: Icon; + const secondaryAvatar = ReportUtils.getSecondaryAvatar(report, iouReport ?? null, displayAllActors, isWorkspaceActor, actorAccountID); const primaryDisplayName = displayName; if (displayAllActors) { // The ownerAccountID and actorAccountID can be the same if the user submits an expense back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice const secondaryAccountId = iouReport?.ownerAccountID === actorAccountID || isInvoiceReport ? iouReport?.managerID : iouReport?.ownerAccountID; - const secondaryUserAvatar = personalDetails?.[secondaryAccountId ?? -1]?.avatar ?? FallbackAvatar; const secondaryDisplayName = ReportUtils.getDisplayNameForParticipant(secondaryAccountId); if (!isInvoiceReport) { displayName = `${primaryDisplayName} & ${secondaryDisplayName}`; } - - secondaryAvatar = { - source: secondaryUserAvatar, - type: CONST.ICON_TYPE_AVATAR, - name: secondaryDisplayName ?? '', - id: secondaryAccountId, - }; - } else if (!isWorkspaceActor) { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const avatarIconIndex = report.isOwnPolicyExpenseChat || ReportUtils.isPolicyExpenseChat(report) ? 0 : 1; - const reportIcons = ReportUtils.getIcons(report, {}); - - secondaryAvatar = reportIcons[avatarIconIndex]; - } else { - secondaryAvatar = {name: '', source: '', type: 'avatar'}; } const icon = { source: avatarSource ?? FallbackAvatar, From 506482133b00fc16f1e9bf82f331b48e5bc6127d Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 4 Jun 2024 16:35:35 +0200 Subject: [PATCH 004/270] Minor improvements --- src/components/SettlementButton.tsx | 4 ++-- src/libs/ReportUtils.ts | 9 +++++---- src/pages/home/report/ReportActionItemSingle.tsx | 1 - 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx index c5eaa0dec336..62b0c2ee89ac 100644 --- a/src/components/SettlementButton.tsx +++ b/src/components/SettlementButton.tsx @@ -43,7 +43,7 @@ type SettlementButtonOnyxProps = { type SettlementButtonProps = SettlementButtonOnyxProps & { /** Callback to execute when this button is pressed. Receives a single payment type argument. */ - onPress: (paymentType?: PaymentMethodType) => void; + onPress: (paymentType?: PaymentMethodType, payAsBusiness?: boolean) => void; /** The route to redirect if user does not have a payment method setup */ enablePaymentsRoute: EnablePaymentsRoute; @@ -278,7 +278,7 @@ function SettlementButton({ return ( onPress(paymentType)} enablePaymentsRoute={enablePaymentsRoute} addBankAccountRoute={addBankAccountRoute} addDebitCardRoute={addDebitCardRoute} diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b2a18925c9b4..68f1cf9c830a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1867,21 +1867,22 @@ function getSecondaryAvatar(chatReport: OnyxEntry, iouReport: OnyxEntry< if (!isIndividualInvoiceRoom(chatReport)) { const secondaryPolicyID = chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : '-1'; const secondaryPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${secondaryPolicyID}`]; - const avatar = secondaryPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(secondaryPolicy?.name); + const secondaryPolicyAvatar = secondaryPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(secondaryPolicy?.name); secondaryAvatar = { - source: avatar, + source: secondaryPolicyAvatar, type: CONST.ICON_TYPE_WORKSPACE, name: secondaryPolicy?.name, id: secondaryPolicyID, }; } else { + // The ownerAccountID and actorAccountID can be the same if the user submits an expense back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice const secondaryAccountId = iouReport?.ownerAccountID === actorAccountID || isInvoiceReport(iouReport) ? iouReport?.managerID : iouReport?.ownerAccountID; - const secondaryUserAvatar = allPersonalDetails?.[secondaryAccountId ?? -1]?.avatar ?? ''; + const secondaryUserAvatar = allPersonalDetails?.[secondaryAccountId ?? -1]?.avatar ?? FallbackAvatar; const secondaryDisplayName = getDisplayNameForParticipant(secondaryAccountId); secondaryAvatar = { - source: UserUtils.getAvatar(secondaryUserAvatar, secondaryAccountId), + source: secondaryUserAvatar, type: CONST.ICON_TYPE_AVATAR, name: secondaryDisplayName ?? '', id: secondaryAccountId, diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index e6a870879598..eee4f570ef1f 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -23,7 +23,6 @@ import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Report, ReportAction} from '@src/types/onyx'; -import type {Icon} from '@src/types/onyx/OnyxCommon'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import ReportActionItemDate from './ReportActionItemDate'; import ReportActionItemFragment from './ReportActionItemFragment'; From 74e0b2510b903952acfd601de4436e1dc52aebed Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 4 Jun 2024 16:59:50 +0200 Subject: [PATCH 005/270] Lint fix --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 68f1cf9c830a..2569bc56a331 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1889,7 +1889,7 @@ function getSecondaryAvatar(chatReport: OnyxEntry, iouReport: OnyxEntry< }; } } else if (!isWorkspaceActor) { - const avatarIconIndex = chatReport?.isOwnPolicyExpenseChat || isPolicyExpenseChat(chatReport) ? 0 : 1; + const avatarIconIndex = !!chatReport?.isOwnPolicyExpenseChat || isPolicyExpenseChat(chatReport) ? 0 : 1; const reportIcons = getIcons(chatReport, {}); secondaryAvatar = reportIcons[avatarIconIndex]; From 6e28975fcd764baaeefc1c3a0767f870c9a82020 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 5 Jun 2024 16:48:35 +0700 Subject: [PATCH 006/270] fix selected option is not highlighted --- src/components/SelectionList/BaseSelectionList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index f3f7f56be44f..8edb51967cd5 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -157,7 +157,7 @@ function BaseSelectionList( itemLayouts.push({length: fullItemHeight, offset}); offset += fullItemHeight; - if (item.isSelected) { + if (item.isSelected && !selectedOptions.find((option) => option.text === item.text)) { selectedOptions.push(item); } }); From 1ea212ef4c480260dea571b1aaf881e08413f956 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 7 Jun 2024 12:01:49 +0200 Subject: [PATCH 007/270] Minor UI fixes --- src/components/SettlementButton.tsx | 1 + src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/ReportUtils.ts | 10 ++++++---- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx index 62b0c2ee89ac..d06d514d5657 100644 --- a/src/components/SettlementButton.tsx +++ b/src/components/SettlementButton.tsx @@ -227,6 +227,7 @@ function SettlementButton({ text: translate('iou.settleBusiness', {formattedAmount}), icon: Expensicons.Building, value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + backButtonText: translate('iou.business'), subMenuItems: [ { text: translate('iou.payElsewhere', {formattedAmount: ''}), diff --git a/src/languages/en.ts b/src/languages/en.ts index 71148ce876e0..5cfa2a165fd1 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -679,6 +679,7 @@ export default { settledExpensify: 'Paid', settledElsewhere: 'Paid elsewhere', individual: 'Individual', + business: 'Business', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} with Expensify` : `Pay with Expensify`), settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} as an individual` : `Pay as an individual`), settlePayment: ({formattedAmount}: SettleExpensifyCardParams) => `Pay ${formattedAmount}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 0e88abd18860..275184d6d5a2 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -673,6 +673,7 @@ export default { settledExpensify: 'Pagado', settledElsewhere: 'Pagado de otra forma', individual: 'Individual', + business: 'Empresa', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} con Expensify` : `Pagar con Expensify`), settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pago ${formattedAmount} como individuo` : `Pago individual`), settlePayment: ({formattedAmount}: SettleExpensifyCardParams) => `Pagar ${formattedAmount}`, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d53f0ba2186d..8681f52d2538 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2074,13 +2074,14 @@ function getIcons( if (report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { icons.push(...getIconsForParticipants([report?.invoiceReceiver.accountID], personalDetails)); } else { - const receiverPolicy = getPolicy(report?.invoiceReceiver?.policyID); + const receiverPolicyID = report?.invoiceReceiver?.policyID; + const receiverPolicy = getPolicy(receiverPolicyID); if (!isEmptyObject(receiverPolicy)) { icons.push({ source: receiverPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(receiverPolicy.name), type: CONST.ICON_TYPE_WORKSPACE, name: receiverPolicy.name, - id: receiverPolicy.id, + id: receiverPolicyID, }); } } @@ -2153,14 +2154,15 @@ function getIcons( return icons; } - const receiverPolicy = getPolicy(invoiceRoomReport?.invoiceReceiver?.policyID); + const receiverPolicyID = invoiceRoomReport?.invoiceReceiver?.policyID; + const receiverPolicy = getPolicy(receiverPolicyID); if (!isEmptyObject(receiverPolicy)) { icons.push({ source: receiverPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(receiverPolicy.name), type: CONST.ICON_TYPE_WORKSPACE, name: receiverPolicy.name, - id: receiverPolicy.id, + id: receiverPolicyID, }); } From 97bf8d647d2caa81cce8d2bd51f45a0edb4a3fec Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 10 Jun 2024 10:07:04 +0200 Subject: [PATCH 008/270] Improve the check --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8681f52d2538..3ae370112a0c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1872,7 +1872,7 @@ function getSecondaryAvatar(chatReport: OnyxEntry, iouReport: OnyxEntry< let secondaryAvatar: Icon; if (displayAllActors) { - if (!isIndividualInvoiceRoom(chatReport)) { + if (isInvoiceRoom(chatReport) && !isIndividualInvoiceRoom(chatReport)) { const secondaryPolicyID = chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : '-1'; const secondaryPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${secondaryPolicyID}`]; const secondaryPolicyAvatar = secondaryPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(secondaryPolicy?.name); From 7bd1d725feb70cb5cfee306828df1f968523f644 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 11 Jun 2024 13:04:09 +0800 Subject: [PATCH 009/270] add new copy for paying a report with all hold expenses --- src/components/ProcessMoneyReportHoldMenu.tsx | 2 +- src/languages/en.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx index 12e2d818b715..7e2b60a868bc 100644 --- a/src/components/ProcessMoneyReportHoldMenu.tsx +++ b/src/components/ProcessMoneyReportHoldMenu.tsx @@ -74,7 +74,7 @@ function ProcessMoneyReportHoldMenu({ if (nonHeldAmount) { promptTranslation = isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount'; } else { - promptTranslation = isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAmount'; + promptTranslation = isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount'; } return translate(promptTranslation); diff --git a/src/languages/en.ts b/src/languages/en.ts index a90d0a5bb4b9..0569c3b1c551 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -769,6 +769,7 @@ export default { confirmApprovalAllHoldAmount: 'All expenses on this report are on hold. Do you want to unhold them and approve?', confirmPay: 'Confirm payment amount', confirmPayAmount: "Pay what's not on hold, or pay all out-of-pocket spend.", + confirmPayAllHoldAmount: 'All expenses on this report are on hold. Do you want to unhold them and pay?', payOnly: 'Pay only', approveOnly: 'Approve only', holdEducationalTitle: 'This expense is on', From 7890b03a6ba2630c1e95f27bd09cde8ff353f5cc Mon Sep 17 00:00:00 2001 From: burczu Date: Wed, 12 Jun 2024 10:38:10 +0200 Subject: [PATCH 010/270] export onyx state button added --- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/pages/settings/Troubleshoot/TroubleshootPage.tsx | 9 +++++++++ 3 files changed, 11 insertions(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index 1d1b5852f384..c776f7ba0ebd 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -951,6 +951,7 @@ export default { deviceCredentials: 'Device credentials', invalidate: 'Invalidate', destroy: 'Destroy', + exportOnyxState: 'Export Onyx state', }, debugConsole: { saveLog: 'Save log', diff --git a/src/languages/es.ts b/src/languages/es.ts index ad2748a25fa2..814431129ed7 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -947,6 +947,7 @@ export default { deviceCredentials: 'Credenciales del dispositivo', invalidate: 'Invalidar', destroy: 'Destruir', + exportOnyxState: 'Exportar estado Onyx', }, debugConsole: { saveLog: 'Guardar registro', diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index 0424682c7afb..34d75d9279ad 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -53,6 +53,10 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { const {isSmallScreenWidth} = useWindowDimensions(); const illustrationStyle = getLightbulbIllustrationStyle(); + const exportOnyxState = () => { + console.log('export state here'); + }; + const menuItems = useMemo(() => { const debugConsoleItem: BaseMenuItem = { translationKey: 'initialSettingsPage.troubleshoot.viewConsole', @@ -66,6 +70,11 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { icon: Expensicons.RotateLeft, action: () => setIsConfirmationModalVisible(true), }, + { + translationKey: 'initialSettingsPage.troubleshoot.exportOnyxState', + icon: Expensicons.Download, + action: exportOnyxState + } ]; if (shouldStoreLogs) { From bd8910c38b37105f005c7feb85db6531928f6e68 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 01:00:44 +0200 Subject: [PATCH 011/270] Add Handle image zoom for mobile browser apps --- .../AttachmentCarousel/CarouselItem.tsx | 6 +- .../Attachments/AttachmentCarousel/index.tsx | 5 +- .../AttachmentViewImage/index.tsx | 6 +- .../Attachments/AttachmentView/index.tsx | 5 + src/components/ImageView/index.tsx | 200 ++++++++++++++++-- src/components/ImageView/types.ts | 3 + 6 files changed, 204 insertions(+), 21 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx index 2ec1883fd7de..c0b3714ff5d8 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx @@ -19,6 +19,9 @@ type CarouselItemProps = { /** onPress callback */ onPress?: () => void; + /** onClose callback */ + onClose?: () => void; + /** Whether attachment carousel modal is hovered over */ isModalHovered?: boolean; @@ -26,7 +29,7 @@ type CarouselItemProps = { isFocused: boolean; }; -function CarouselItem({item, onPress, isFocused, isModalHovered}: CarouselItemProps) { +function CarouselItem({item, onPress, onClose, isFocused, isModalHovered}: CarouselItemProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isAttachmentHidden} = useContext(ReportAttachmentsContext); @@ -77,6 +80,7 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}: CarouselItemPr file={item.file} isAuthTokenRequired={item.isAuthTokenRequired} onPress={onPress} + onClose={onClose} transactionID={item.transactionID} reportActionID={item.reportActionID} isHovered={isModalHovered} diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index 947569538d32..f2ed12fc85eb 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -33,7 +33,7 @@ const viewabilityConfig = { const MIN_FLING_VELOCITY = 500; -function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, type, accountID}: AttachmentCarouselProps) { +function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, onClose, setDownloadButtonVisibility, type, accountID}: AttachmentCarouselProps) { const theme = useTheme(); const {translate} = useLocalize(); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); @@ -174,6 +174,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, ({item}: ListRenderItemInfo) => ( setShouldShowArrows((oldState) => !oldState) : undefined} @@ -181,7 +182,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, /> ), - [activeSource, canUseTouchScreen, cellWidth, setShouldShowArrows, shouldShowArrows, styles.h100], + [activeSource, canUseTouchScreen, onClose, cellWidth, setShouldShowArrows, shouldShowArrows, styles.h100], ); /** Pan gesture handing swiping through attachments on touch screen devices */ const pan = useMemo( diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx index c195c1e34554..6756742a978c 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx +++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx @@ -15,14 +15,18 @@ type AttachmentViewImageProps = Pick void; + + /** Function for handle on close */ + onClose?: () => void; }; -function AttachmentViewImage({url, file, isAuthTokenRequired, loadComplete, onPress, onError, isImage}: AttachmentViewImageProps) { +function AttachmentViewImage({url, file, isAuthTokenRequired, loadComplete, onPress, onError, onClose, isImage}: AttachmentViewImageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const children = ( void; + /** Function for handle on close */ + onClose?: () => void | undefined; + /** Whether this AttachmentView is shown as part of a AttachmentCarousel */ isUsedInCarousel?: boolean; @@ -84,6 +87,7 @@ function AttachmentView({ isFocused, isUsedInCarousel, isUsedInAttachmentModal, + onClose, isWorkspaceAvatar, maybeIcon, fallbackSource, @@ -220,6 +224,7 @@ function AttachmentView({ isAuthTokenRequired={isAuthTokenRequired} loadComplete={loadComplete} isImage={isImage} + onClose={onClose} onPress={onPress} onError={() => { setImageError(true); diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index f08941ef7d77..63ffa8fc93e0 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -1,16 +1,20 @@ import type {SyntheticEvent} from 'react'; -import React, {useCallback, useEffect, useRef, useState} from 'react'; -import type {GestureResponderEvent, LayoutChangeEvent} from 'react-native'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import type {GestureResponderEvent, LayoutChangeEvent, ViewStyle} from 'react-native'; import {View} from 'react-native'; +import {Gesture, GestureDetector} from 'react-native-gesture-handler'; +import Animated, {useAnimatedStyle, useDerivedValue, useSharedValue, withSpring} from 'react-native-reanimated'; import AttachmentOfflineIndicator from '@components/AttachmentOfflineIndicator'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Image from '@components/Image'; import RESIZE_MODES from '@components/Image/resizeModes'; import type {ImageOnLoadEvent} from '@components/Image/types'; +import {SPRING_CONFIG} from '@components/MultiGestureCanvas/constants'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import CONST from '@src/CONST'; import viewRef from '@src/types/utils/viewRef'; @@ -18,7 +22,7 @@ import type ImageViewProps from './types'; type ZoomDelta = {offsetX: number; offsetY: number}; -function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageViewProps) { +function ImageView({isAuthTokenRequired = false, url, fileName, onError, onSwipeDown}: ImageViewProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [isLoading, setIsLoading] = useState(true); @@ -32,6 +36,8 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV const [initialX, setInitialX] = useState(0); const [initialY, setInitialY] = useState(0); const [imgWidth, setImgWidth] = useState(0); + const [imgContainerHeight, setImgContainerHeight] = useState(0); + const [imgContainerWidth, setImgContainerWidth] = useState(0); const [imgHeight, setImgHeight] = useState(0); const [zoomScale, setZoomScale] = useState(0); const [zoomDelta, setZoomDelta] = useState(); @@ -47,6 +53,131 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV const newZoomScale = Math.min(newContainerWidth / newImageWidth, newContainerHeight / newImageHeight); setZoomScale(newZoomScale); }; + const scale = useSharedValue(1); + const deltaScale = useSharedValue(1); + const minScale = 1.0; + const maxScale = 20; + const translationX = useSharedValue(1); + const translationY = useSharedValue(1); + const prevTranslationX = useSharedValue(0); + const prevTranslationY = useSharedValue(0); + const zoomedContentWidth = useDerivedValue(() => imgContainerWidth * scale.value, [imgContainerWidth, scale.value]); + const zoomedContentHeight = useDerivedValue(() => imgContainerHeight * scale.value, [imgContainerHeight, scale.value]); + const maxTranslateX = useMemo(() => imgContainerWidth / 2, [imgContainerWidth]); + const maxTranslateY = useMemo(() => containerHeight / 2, [containerHeight]); + const horizontalBoundaries = useMemo(() => { + let horizontalBoundary = 0; + if (containerWidth < zoomedContentWidth.value) { + horizontalBoundary = Math.abs(containerWidth - zoomedContentWidth.value) / 2; + } + return {min: -horizontalBoundary, max: horizontalBoundary}; + }, [containerWidth, zoomedContentWidth.value]); + const verticalBoundaries = useMemo(() => { + let verticalBoundary = 0; + if (containerHeight < zoomedContentHeight.value) { + verticalBoundary = Math.abs(zoomedContentHeight.value - containerHeight) / 2; + } + return {min: -verticalBoundary, max: verticalBoundary}; + }, [containerHeight, zoomedContentHeight.value]); + const pinchGesture = Gesture.Pinch() + .onStart(() => { + deltaScale.value = scale.value; + }) + .onUpdate((e) => { + if (scale.value < minScale / 2) { + return; + } + scale.value = deltaScale.value * e.scale; + }) + .onEnd(() => { + if (scale.value < minScale) { + scale.value = withSpring(minScale, SPRING_CONFIG); + translationX.value = 0; + translationY.value = 0; + } + if (scale.value > maxScale) { + scale.value = withSpring(maxScale, SPRING_CONFIG); + } + deltaScale.value = scale.value; + }) + .runOnJS(true); + const clamp = (val: number, min: number, max: number) => { + 'worklet'; + + return Math.min(Math.max(val, min), max); + }; + const panGesture = Gesture.Pan() + .onStart(() => { + 'worklet'; + + prevTranslationX.value = translationX.value; + prevTranslationY.value = translationY.value; + }) + .onUpdate((e) => { + 'worklet'; + + if (scale.value === minScale) { + if (e.translationX === 0 && e.translationY > 0) { + translationY.value = clamp(prevTranslationY.value + e.translationY, -maxTranslateY, maxTranslateY); + } else { + return; + } + } + translationX.value = clamp(prevTranslationX.value + e.translationX, -maxTranslateX, maxTranslateX); + if (zoomedContentHeight.value < containerHeight) { + return; + } + translationY.value = clamp(prevTranslationY.value + e.translationY, -maxTranslateY, maxTranslateY); + }) + .onEnd(() => { + 'worklet'; + + const swipeDownPadding = 150; + const dy = translationY.value + swipeDownPadding; + if (dy >= maxTranslateY && scale.value === minScale) { + if (onSwipeDown) { + onSwipeDown(); + } + } else if (scale.value === minScale) { + translationY.value = withSpring(0, SPRING_CONFIG); + translationX.value = withSpring(0, SPRING_CONFIG); + return; + } + const tsx = translationX.value * scale.value; + const tsy = translationY.value * scale.value; + const inHorizontalBoundaries = tsx >= horizontalBoundaries.min && tsx <= horizontalBoundaries.max; + const inVerticalBoundaries = tsy >= verticalBoundaries.min && tsy <= verticalBoundaries.max; + if (!inHorizontalBoundaries) { + const halfx = zoomedContentWidth.value / 2; + const diffx = halfx - translationX.value * scale.value; + const valx = maxTranslateX - diffx; + if (valx > 0) { + const p = (translationX.value * scale.value - valx) / scale.value; + translationX.value = withSpring(p, SPRING_CONFIG); + } + if (valx < 0) { + const p = (translationX.value * scale.value - valx) / scale.value; + translationX.value = withSpring(-p, SPRING_CONFIG); + } + } + if (!inVerticalBoundaries) { + if (zoomedContentHeight?.value < containerHeight) { + return; + } + const halfy = zoomedContentHeight.value / 2; + const diffy = halfy - translationY.value * scale.value; + const valy = maxTranslateY - diffy; + if (valy > 0) { + const p = (translationY.value * scale.value - valy) / scale.value; + translationY.value = withSpring(p, SPRING_CONFIG); + } + if (valy < 0) { + const p = (translationY.value * scale.value - valy) / scale.value; + translationY.value = withSpring(-p, SPRING_CONFIG); + } + } + }) + .runOnJS(true); const onContainerLayoutChanged = (e: LayoutChangeEvent) => { const {width, height} = e.nativeEvent.layout; @@ -195,25 +326,60 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV }; }, [canUseTouchScreen, trackMovement, trackPointerPosition]); + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{scale: scale.value}, {translateX: translationX.value}, {translateY: translationY.value}], + })); + + const imgContainerStyle = useMemo(() => { + if (imgWidth >= imgHeight || imgHeight < containerHeight) { + const imgStyle: ViewStyle[] = [{width: imgWidth < containerWidth ? '100%' : '100%', aspectRatio: (imgHeight && imgWidth / imgHeight) || 1}]; + return imgStyle; + } + if (imgHeight > imgWidth) { + const imgStyle: ViewStyle[] = [{height: '100%', aspectRatio: (imgHeight && imgWidth / imgHeight) || 1}]; + return imgStyle; + } + }, [imgWidth, imgHeight, containerWidth, containerHeight]); + if (canUseTouchScreen) { return ( - 1 ? RESIZE_MODES.center : RESIZE_MODES.contain} - onLoadStart={imageLoadingStart} - onLoad={imageLoad} - onError={onError} - /> - {((isLoading && !isOffline) || (!isLoading && zoomScale === 0)) && } + + { + const {width, height} = e.nativeEvent.layout; + setImgContainerHeight(height); + setImgContainerWidth(width); + }} + > + { + const {width, height} = e.nativeEvent.source; + const params = { + nativeEvent: { + width, + height, + }, + }; + imageLoad(params); + }} + onError={onError} + /> + + + {isLoading && !isOffline && } {isLoading && } ); diff --git a/src/components/ImageView/types.ts b/src/components/ImageView/types.ts index b19e6b228cbd..aac6b994b5bc 100644 --- a/src/components/ImageView/types.ts +++ b/src/components/ImageView/types.ts @@ -14,6 +14,9 @@ type ImageViewProps = { /** Handles errors while displaying the image */ onError?: () => void; + /** Function to call when an user swipes down */ + onSwipeDown?: () => void; + /** Additional styles to add to the component */ style?: StyleProp; From 2bd1a2ae167ba58a6e1b8075b3f49e6c5bf75f45 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 01:11:54 +0200 Subject: [PATCH 012/270] Make a little refactoring --- src/components/ImageView/index.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index 63ffa8fc93e0..59212a6fd317 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -331,15 +331,16 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError, onSwipe })); const imgContainerStyle = useMemo(() => { + const aspectRatio = (imgHeight && imgWidth / imgHeight) || 1; if (imgWidth >= imgHeight || imgHeight < containerHeight) { - const imgStyle: ViewStyle[] = [{width: imgWidth < containerWidth ? '100%' : '100%', aspectRatio: (imgHeight && imgWidth / imgHeight) || 1}]; + const imgStyle: ViewStyle[] = [{width: '100%', aspectRatio}]; return imgStyle; } if (imgHeight > imgWidth) { - const imgStyle: ViewStyle[] = [{height: '100%', aspectRatio: (imgHeight && imgWidth / imgHeight) || 1}]; + const imgStyle: ViewStyle[] = [{height: '100%', aspectRatio}]; return imgStyle; } - }, [imgWidth, imgHeight, containerWidth, containerHeight]); + }, [imgWidth, imgHeight, containerHeight]); if (canUseTouchScreen) { return ( From ca69b0fd30728774612e405cb780923348d38c0a Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 01:42:11 +0200 Subject: [PATCH 013/270] Fix bug with close modal on send attachment screen --- src/components/AttachmentModal.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 54a073e30567..f0317dd904cb 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -547,6 +547,7 @@ function AttachmentModal({ source={sourceForAttachmentView} isAuthTokenRequired={isAuthTokenRequiredState} file={file} + onClose={closeModal} onToggleKeyboard={updateConfirmButtonVisibility} isWorkspaceAvatar={isWorkspaceAvatar} maybeIcon={maybeIcon} From 2e343765c5285ab19f0633ed3a61a95c1b547c00 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 01:46:25 +0200 Subject: [PATCH 014/270] Reset image position when onSwipeDown is undefined --- src/components/ImageView/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index 59212a6fd317..deb44048d47e 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -137,6 +137,9 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError, onSwipe if (dy >= maxTranslateY && scale.value === minScale) { if (onSwipeDown) { onSwipeDown(); + } else { + translationY.value = withSpring(0, SPRING_CONFIG); + translationX.value = withSpring(0, SPRING_CONFIG); } } else if (scale.value === minScale) { translationY.value = withSpring(0, SPRING_CONFIG); From ef5f19b413715ca2c5931f7cc98d99fb6c24f4c8 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 13 Jun 2024 08:38:26 +0200 Subject: [PATCH 015/270] Fix ts error --- src/pages/home/report/ReportActionItemSingle.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index b537d43e3df3..51d1df35f9ac 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -106,7 +106,7 @@ function ReportActionItemSingle({ } // If this is a report preview, display names and avatars of both people involved - const secondaryAvatar = ReportUtils.getSecondaryAvatar(report, iouReport ?? null, displayAllActors, isWorkspaceActor, actorAccountID); + const secondaryAvatar = ReportUtils.getSecondaryAvatar(report, iouReport, displayAllActors, isWorkspaceActor, actorAccountID); const primaryDisplayName = displayName; if (displayAllActors) { // The ownerAccountID and actorAccountID can be the same if the user submits an expense back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice From 58949066c8475b3faec960c5909ca7767cd742cb Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 12:21:02 +0200 Subject: [PATCH 016/270] Refactor code --- src/components/AttachmentModal.tsx | 34 +-- .../AttachmentCarousel/CarouselItem.tsx | 6 +- .../Pager/AttachmentCarouselPagerContext.ts | 15 +- .../Attachments/AttachmentCarousel/index.tsx | 14 +- .../Attachments/AttachmentCarousel/types.ts | 8 + .../AttachmentViewImage/index.tsx | 6 +- .../Attachments/AttachmentView/index.tsx | 5 - src/components/ImageView/index.tsx | 201 +----------------- src/components/MultiGestureCanvas/index.tsx | 4 +- 9 files changed, 65 insertions(+), 228 deletions(-) diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index f0317dd904cb..2f5c85b10fb3 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -1,6 +1,7 @@ import {Str} from 'expensify-common'; -import React, {memo, useCallback, useEffect, useMemo, useState} from 'react'; +import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {Animated, Keyboard, View} from 'react-native'; +import type {GestureType} from 'react-native-gesture-handler'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -185,6 +186,8 @@ function AttachmentModal({ const nope = useSharedValue(false); const isOverlayModalVisible = (isReceiptAttachment && isDeleteReceiptConfirmModalVisible) || (!isReceiptAttachment && isAttachmentInvalid); const iouType = useMemo(() => (isTrackExpenseAction ? CONST.IOU.TYPE.TRACK : CONST.IOU.TYPE.SUBMIT), [isTrackExpenseAction]); + const pagerRef = useRef(null); + const [zoomScale, setZoomScale] = useState(1); const [file, setFile] = useState( originalFileName @@ -469,11 +472,13 @@ function AttachmentModal({ () => ({ pagerItems: [{source: sourceForAttachmentView, index: 0, isActive: true}], activePage: 0, - pagerRef: undefined, + pagerRef, isPagerScrolling: nope, isScrollEnabled: nope, onTap: () => {}, - onScaleChanged: () => {}, + onScaleChanged: (value: number) => { + setZoomScale(value); + }, onSwipeDown: closeModal, }), [closeModal, nope, sourceForAttachmentView], @@ -528,15 +533,19 @@ function AttachmentModal({ )} {!shouldShowNotFoundPage && (!isEmptyObject(report) && !isReceiptAttachment ? ( - + + + ) : ( !!sourceForAttachmentView && shouldLoadAttachment && @@ -547,7 +556,6 @@ function AttachmentModal({ source={sourceForAttachmentView} isAuthTokenRequired={isAuthTokenRequiredState} file={file} - onClose={closeModal} onToggleKeyboard={updateConfirmButtonVisibility} isWorkspaceAvatar={isWorkspaceAvatar} maybeIcon={maybeIcon} diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx index c0b3714ff5d8..2ec1883fd7de 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx @@ -19,9 +19,6 @@ type CarouselItemProps = { /** onPress callback */ onPress?: () => void; - /** onClose callback */ - onClose?: () => void; - /** Whether attachment carousel modal is hovered over */ isModalHovered?: boolean; @@ -29,7 +26,7 @@ type CarouselItemProps = { isFocused: boolean; }; -function CarouselItem({item, onPress, onClose, isFocused, isModalHovered}: CarouselItemProps) { +function CarouselItem({item, onPress, isFocused, isModalHovered}: CarouselItemProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isAttachmentHidden} = useContext(ReportAttachmentsContext); @@ -80,7 +77,6 @@ function CarouselItem({item, onPress, onClose, isFocused, isModalHovered}: Carou file={item.file} isAuthTokenRequired={item.isAuthTokenRequired} onPress={onPress} - onClose={onClose} transactionID={item.transactionID} reportActionID={item.reportActionID} isHovered={isModalHovered} diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index 87a9108d5f2e..c597b07487f0 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -1,5 +1,6 @@ import type {ForwardedRef} from 'react'; import {createContext} from 'react'; +import type {GestureType} from 'react-native-gesture-handler'; import type PagerView from 'react-native-pager-view'; import type {SharedValue} from 'react-native-reanimated'; import type {AttachmentSource} from '@components/Attachments/types'; @@ -22,11 +23,23 @@ type AttachmentCarouselPagerContextValue = { /** The index of the active page */ activePage: number; - pagerRef?: ForwardedRef; + + /** The ref of the active attachment */ + pagerRef?: ForwardedRef; + + /** The scroll state of the attachment */ isPagerScrolling: SharedValue; + + /** The scroll active of the attachment */ isScrollEnabled: SharedValue; + + /** The function to call after tap */ onTap: () => void; + + /** The function to call after scale */ onScaleChanged: (scale: number) => void; + + /** The function to call after swipe down */ onSwipeDown: () => void; }; diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index f2ed12fc85eb..611e79622075 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -1,7 +1,9 @@ import isEqual from 'lodash/isEqual'; +import type {MutableRefObject} from 'react'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {ListRenderItemInfo} from 'react-native'; import {Keyboard, PixelRatio, View} from 'react-native'; +import type {GestureType} from 'react-native-gesture-handler'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; import {withOnyx} from 'react-native-onyx'; import Animated, {scrollTo, useAnimatedRef} from 'react-native-reanimated'; @@ -33,7 +35,7 @@ const viewabilityConfig = { const MIN_FLING_VELOCITY = 500; -function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, onClose, setDownloadButtonVisibility, type, accountID}: AttachmentCarouselProps) { +function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, type, accountID, pagerRef, zoomScale}: AttachmentCarouselProps) { const theme = useTheme(); const {translate} = useLocalize(); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); @@ -174,7 +176,6 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, ({item}: ListRenderItemInfo) => ( setShouldShowArrows((oldState) => !oldState) : undefined} @@ -182,13 +183,13 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, /> ), - [activeSource, canUseTouchScreen, onClose, cellWidth, setShouldShowArrows, shouldShowArrows, styles.h100], + [activeSource, canUseTouchScreen, cellWidth, setShouldShowArrows, shouldShowArrows, styles.h100], ); /** Pan gesture handing swiping through attachments on touch screen devices */ const pan = useMemo( () => Gesture.Pan() - .enabled(canUseTouchScreen) + .enabled(canUseTouchScreen && zoomScale === 1) .onUpdate(({translationX}) => scrollTo(scrollRef, page * cellWidth - translationX, 0, false)) .onEnd(({translationX, velocityX}) => { let newIndex; @@ -205,8 +206,9 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, } scrollTo(scrollRef, newIndex * cellWidth, 0, true); - }), - [attachments.length, canUseTouchScreen, cellWidth, page, scrollRef], + }) + .withRef(pagerRef as MutableRefObject), + [attachments.length, canUseTouchScreen, cellWidth, page, pagerRef, scrollRef, zoomScale], ); return ( diff --git a/src/components/Attachments/AttachmentCarousel/types.ts b/src/components/Attachments/AttachmentCarousel/types.ts index d31ebbd328cd..984f914dfd33 100644 --- a/src/components/Attachments/AttachmentCarousel/types.ts +++ b/src/components/Attachments/AttachmentCarousel/types.ts @@ -1,4 +1,6 @@ +import type {ForwardedRef} from 'react'; import type {ViewToken} from 'react-native'; +import type {GestureType} from 'react-native-gesture-handler'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {Attachment, AttachmentSource} from '@components/Attachments/types'; @@ -38,6 +40,12 @@ type AttachmentCarouselProps = AttachmentCaraouselOnyxProps & { /** A callback that is called when swipe-down-to-close gesture happens */ onClose: () => void; + + /** The ref of the pager */ + pagerRef: ForwardedRef; + + /** The zoom scale of the attachment */ + zoomScale?: number; }; export type {AttachmentCarouselProps, UpdatePageProps, AttachmentCaraouselOnyxProps}; diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx index 6756742a978c..c195c1e34554 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx +++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx @@ -15,18 +15,14 @@ type AttachmentViewImageProps = Pick void; - - /** Function for handle on close */ - onClose?: () => void; }; -function AttachmentViewImage({url, file, isAuthTokenRequired, loadComplete, onPress, onError, onClose, isImage}: AttachmentViewImageProps) { +function AttachmentViewImage({url, file, isAuthTokenRequired, loadComplete, onPress, onError, isImage}: AttachmentViewImageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const children = ( void; - /** Function for handle on close */ - onClose?: () => void | undefined; - /** Whether this AttachmentView is shown as part of a AttachmentCarousel */ isUsedInCarousel?: boolean; @@ -87,7 +84,6 @@ function AttachmentView({ isFocused, isUsedInCarousel, isUsedInAttachmentModal, - onClose, isWorkspaceAvatar, maybeIcon, fallbackSource, @@ -224,7 +220,6 @@ function AttachmentView({ isAuthTokenRequired={isAuthTokenRequired} loadComplete={loadComplete} isImage={isImage} - onClose={onClose} onPress={onPress} onError={() => { setImageError(true); diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index deb44048d47e..fa8f5fba993e 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -1,20 +1,17 @@ import type {SyntheticEvent} from 'react'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import type {GestureResponderEvent, LayoutChangeEvent, ViewStyle} from 'react-native'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; +import type {GestureResponderEvent, LayoutChangeEvent} from 'react-native'; import {View} from 'react-native'; -import {Gesture, GestureDetector} from 'react-native-gesture-handler'; -import Animated, {useAnimatedStyle, useDerivedValue, useSharedValue, withSpring} from 'react-native-reanimated'; import AttachmentOfflineIndicator from '@components/AttachmentOfflineIndicator'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Image from '@components/Image'; import RESIZE_MODES from '@components/Image/resizeModes'; import type {ImageOnLoadEvent} from '@components/Image/types'; -import {SPRING_CONFIG} from '@components/MultiGestureCanvas/constants'; +import Lightbox from '@components/Lightbox'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import CONST from '@src/CONST'; import viewRef from '@src/types/utils/viewRef'; @@ -22,7 +19,7 @@ import type ImageViewProps from './types'; type ZoomDelta = {offsetX: number; offsetY: number}; -function ImageView({isAuthTokenRequired = false, url, fileName, onError, onSwipeDown}: ImageViewProps) { +function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageViewProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [isLoading, setIsLoading] = useState(true); @@ -36,8 +33,6 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError, onSwipe const [initialX, setInitialX] = useState(0); const [initialY, setInitialY] = useState(0); const [imgWidth, setImgWidth] = useState(0); - const [imgContainerHeight, setImgContainerHeight] = useState(0); - const [imgContainerWidth, setImgContainerWidth] = useState(0); const [imgHeight, setImgHeight] = useState(0); const [zoomScale, setZoomScale] = useState(0); const [zoomDelta, setZoomDelta] = useState(); @@ -53,134 +48,6 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError, onSwipe const newZoomScale = Math.min(newContainerWidth / newImageWidth, newContainerHeight / newImageHeight); setZoomScale(newZoomScale); }; - const scale = useSharedValue(1); - const deltaScale = useSharedValue(1); - const minScale = 1.0; - const maxScale = 20; - const translationX = useSharedValue(1); - const translationY = useSharedValue(1); - const prevTranslationX = useSharedValue(0); - const prevTranslationY = useSharedValue(0); - const zoomedContentWidth = useDerivedValue(() => imgContainerWidth * scale.value, [imgContainerWidth, scale.value]); - const zoomedContentHeight = useDerivedValue(() => imgContainerHeight * scale.value, [imgContainerHeight, scale.value]); - const maxTranslateX = useMemo(() => imgContainerWidth / 2, [imgContainerWidth]); - const maxTranslateY = useMemo(() => containerHeight / 2, [containerHeight]); - const horizontalBoundaries = useMemo(() => { - let horizontalBoundary = 0; - if (containerWidth < zoomedContentWidth.value) { - horizontalBoundary = Math.abs(containerWidth - zoomedContentWidth.value) / 2; - } - return {min: -horizontalBoundary, max: horizontalBoundary}; - }, [containerWidth, zoomedContentWidth.value]); - const verticalBoundaries = useMemo(() => { - let verticalBoundary = 0; - if (containerHeight < zoomedContentHeight.value) { - verticalBoundary = Math.abs(zoomedContentHeight.value - containerHeight) / 2; - } - return {min: -verticalBoundary, max: verticalBoundary}; - }, [containerHeight, zoomedContentHeight.value]); - const pinchGesture = Gesture.Pinch() - .onStart(() => { - deltaScale.value = scale.value; - }) - .onUpdate((e) => { - if (scale.value < minScale / 2) { - return; - } - scale.value = deltaScale.value * e.scale; - }) - .onEnd(() => { - if (scale.value < minScale) { - scale.value = withSpring(minScale, SPRING_CONFIG); - translationX.value = 0; - translationY.value = 0; - } - if (scale.value > maxScale) { - scale.value = withSpring(maxScale, SPRING_CONFIG); - } - deltaScale.value = scale.value; - }) - .runOnJS(true); - const clamp = (val: number, min: number, max: number) => { - 'worklet'; - - return Math.min(Math.max(val, min), max); - }; - const panGesture = Gesture.Pan() - .onStart(() => { - 'worklet'; - - prevTranslationX.value = translationX.value; - prevTranslationY.value = translationY.value; - }) - .onUpdate((e) => { - 'worklet'; - - if (scale.value === minScale) { - if (e.translationX === 0 && e.translationY > 0) { - translationY.value = clamp(prevTranslationY.value + e.translationY, -maxTranslateY, maxTranslateY); - } else { - return; - } - } - translationX.value = clamp(prevTranslationX.value + e.translationX, -maxTranslateX, maxTranslateX); - if (zoomedContentHeight.value < containerHeight) { - return; - } - translationY.value = clamp(prevTranslationY.value + e.translationY, -maxTranslateY, maxTranslateY); - }) - .onEnd(() => { - 'worklet'; - - const swipeDownPadding = 150; - const dy = translationY.value + swipeDownPadding; - if (dy >= maxTranslateY && scale.value === minScale) { - if (onSwipeDown) { - onSwipeDown(); - } else { - translationY.value = withSpring(0, SPRING_CONFIG); - translationX.value = withSpring(0, SPRING_CONFIG); - } - } else if (scale.value === minScale) { - translationY.value = withSpring(0, SPRING_CONFIG); - translationX.value = withSpring(0, SPRING_CONFIG); - return; - } - const tsx = translationX.value * scale.value; - const tsy = translationY.value * scale.value; - const inHorizontalBoundaries = tsx >= horizontalBoundaries.min && tsx <= horizontalBoundaries.max; - const inVerticalBoundaries = tsy >= verticalBoundaries.min && tsy <= verticalBoundaries.max; - if (!inHorizontalBoundaries) { - const halfx = zoomedContentWidth.value / 2; - const diffx = halfx - translationX.value * scale.value; - const valx = maxTranslateX - diffx; - if (valx > 0) { - const p = (translationX.value * scale.value - valx) / scale.value; - translationX.value = withSpring(p, SPRING_CONFIG); - } - if (valx < 0) { - const p = (translationX.value * scale.value - valx) / scale.value; - translationX.value = withSpring(-p, SPRING_CONFIG); - } - } - if (!inVerticalBoundaries) { - if (zoomedContentHeight?.value < containerHeight) { - return; - } - const halfy = zoomedContentHeight.value / 2; - const diffy = halfy - translationY.value * scale.value; - const valy = maxTranslateY - diffy; - if (valy > 0) { - const p = (translationY.value * scale.value - valy) / scale.value; - translationY.value = withSpring(p, SPRING_CONFIG); - } - if (valy < 0) { - const p = (translationY.value * scale.value - valy) / scale.value; - translationY.value = withSpring(-p, SPRING_CONFIG); - } - } - }) - .runOnJS(true); const onContainerLayoutChanged = (e: LayoutChangeEvent) => { const {width, height} = e.nativeEvent.layout; @@ -329,63 +196,13 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError, onSwipe }; }, [canUseTouchScreen, trackMovement, trackPointerPosition]); - const animatedStyle = useAnimatedStyle(() => ({ - transform: [{scale: scale.value}, {translateX: translationX.value}, {translateY: translationY.value}], - })); - - const imgContainerStyle = useMemo(() => { - const aspectRatio = (imgHeight && imgWidth / imgHeight) || 1; - if (imgWidth >= imgHeight || imgHeight < containerHeight) { - const imgStyle: ViewStyle[] = [{width: '100%', aspectRatio}]; - return imgStyle; - } - if (imgHeight > imgWidth) { - const imgStyle: ViewStyle[] = [{height: '100%', aspectRatio}]; - return imgStyle; - } - }, [imgWidth, imgHeight, containerHeight]); - if (canUseTouchScreen) { return ( - - - { - const {width, height} = e.nativeEvent.layout; - setImgContainerHeight(height); - setImgContainerWidth(width); - }} - > - { - const {width, height} = e.nativeEvent.source; - const params = { - nativeEvent: { - width, - height, - }, - }; - imageLoad(params); - }} - onError={onError} - /> - - - {isLoading && !isOffline && } - {isLoading && } - + ); } return ( diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx index 31a1f7a2c3d8..2e5cf8018c70 100644 --- a/src/components/MultiGestureCanvas/index.tsx +++ b/src/components/MultiGestureCanvas/index.tsx @@ -1,6 +1,7 @@ import type {ForwardedRef} from 'react'; import React, {useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; +import type {GestureType} from 'react-native-gesture-handler'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; import type {GestureRef} from 'react-native-gesture-handler/lib/typescript/handlers/gestures/gesture'; import type PagerView from 'react-native-pager-view'; @@ -40,7 +41,7 @@ type MultiGestureCanvasProps = ChildrenProps & { shouldDisableTransformationGestures?: SharedValue; /** If there is a pager wrapping the canvas, we need to disable the pan gesture in case the pager is swiping */ - pagerRef?: ForwardedRef; // TODO: For TS migration: Exclude + pagerRef?: ForwardedRef; // TODO: For TS migration: Exclude /** Handles scale changed event */ onScaleChanged?: OnScaleChangedCallback; @@ -48,6 +49,7 @@ type MultiGestureCanvasProps = ChildrenProps & { /** Handles scale changed event */ onTap?: OnTapCallback; + /** Handles swipe down event */ onSwipeDown?: OnSwipeDownCallback; }; From e30a3bbc8e9c1040fb5b32740081f303ff21552d Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 12:29:45 +0200 Subject: [PATCH 017/270] Make some minnor changes --- src/components/AttachmentModal.tsx | 11 ++++++++--- .../Pager/AttachmentCarouselPagerContext.ts | 16 ++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 2f5c85b10fb3..5838577bcc5d 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -397,6 +397,13 @@ function AttachmentModal({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [onModalClose]); + /** + * scale handler for attachment + */ + const scaleChanged = (value: number) => { + setZoomScale(value); + }; + /** * open the modal */ @@ -476,9 +483,7 @@ function AttachmentModal({ isPagerScrolling: nope, isScrollEnabled: nope, onTap: () => {}, - onScaleChanged: (value: number) => { - setZoomScale(value); - }, + onScaleChanged: scaleChanged, onSwipeDown: closeModal, }), [closeModal, nope, sourceForAttachmentView], diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts index c597b07487f0..7af38f300532 100644 --- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts +++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts @@ -18,28 +18,28 @@ type AttachmentCarouselPagerItems = { }; type AttachmentCarouselPagerContextValue = { - /** The list of items that are shown in the pager */ + /** List of items displayed in the attachment */ pagerItems: AttachmentCarouselPagerItems[]; - /** The index of the active page */ + /** Index of the currently active page */ activePage: number; - /** The ref of the active attachment */ + /** Ref to the active attachment */ pagerRef?: ForwardedRef; - /** The scroll state of the attachment */ + /** Indicates if the pager is currently scrolling */ isPagerScrolling: SharedValue; - /** The scroll active of the attachment */ + /** Indicates if scrolling is enabled for the attachment */ isScrollEnabled: SharedValue; - /** The function to call after tap */ + /** Function to call after a tap event */ onTap: () => void; - /** The function to call after scale */ + /** Function to call when the scale changes */ onScaleChanged: (scale: number) => void; - /** The function to call after swipe down */ + /** Function to call after a swipe down event */ onSwipeDown: () => void; }; From 2f40b541ad23de47838eca6cf9fb6d91cfc9c7ea Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 12:58:04 +0200 Subject: [PATCH 018/270] Update animation for changing attachment item --- src/components/Attachments/AttachmentCarousel/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index 611e79622075..ebb43ef45eb9 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -190,7 +190,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, () => Gesture.Pan() .enabled(canUseTouchScreen && zoomScale === 1) - .onUpdate(({translationX}) => scrollTo(scrollRef, page * cellWidth - translationX, 0, false)) + .onUpdate(({translationX}) => scrollTo(scrollRef, page * cellWidth - translationX * 2, 0, false)) .onEnd(({translationX, velocityX}) => { let newIndex; if (velocityX > MIN_FLING_VELOCITY) { From ebb0eb4d3c8989e0d5e1fb2665fb46897f9ac4db Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 13 Jun 2024 13:33:16 +0200 Subject: [PATCH 019/270] Remove unnecessary type --- src/components/ImageView/types.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/ImageView/types.ts b/src/components/ImageView/types.ts index aac6b994b5bc..b19e6b228cbd 100644 --- a/src/components/ImageView/types.ts +++ b/src/components/ImageView/types.ts @@ -14,9 +14,6 @@ type ImageViewProps = { /** Handles errors while displaying the image */ onError?: () => void; - /** Function to call when an user swipes down */ - onSwipeDown?: () => void; - /** Additional styles to add to the component */ style?: StyleProp; From 62ef24417b2170691bc02b45e9f57abe9b0fcbda Mon Sep 17 00:00:00 2001 From: burczu Date: Thu, 13 Jun 2024 15:14:44 +0200 Subject: [PATCH 020/270] fetching onyx store key/value pairs PoC --- .../Troubleshoot/TroubleshootPage.tsx | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index 34d75d9279ad..a895cf61e3b2 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -1,6 +1,6 @@ import React, {useMemo, useState} from 'react'; import {View} from 'react-native'; -import Onyx, {withOnyx} from 'react-native-onyx'; +import Onyx, {useOnyx, withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import type {SvgProps} from 'react-native-svg'; import ClientSideLoggingToolMenu from '@components/ClientSideLoggingToolMenu'; @@ -26,6 +26,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as App from '@userActions/App'; import * as Report from '@userActions/Report'; import type {TranslationPaths} from '@src/languages/types'; +import type {OnyxKey} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; @@ -53,8 +54,51 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { const {isSmallScreenWidth} = useWindowDimensions(); const illustrationStyle = getLightbulbIllustrationStyle(); + const getOnyxKeys = (keysObject: Record) => { + const keys: string[] = []; + + Object.keys(keysObject).forEach((key) => { + if (typeof keysObject[key] === 'object') { + keys.push(...getOnyxKeys(keysObject[key] as Record)); + return; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + keys.push(keysObject[key] as string); + }); + + return keys; + } + + const getOnyxValues = () => { + const keys = getOnyxKeys(ONYXKEYS); + const promises: Array>> = []; + + keys.forEach((key) => { + promises.push(new Promise((resolve) => { + // eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs + const connectionID = Onyx.connect({ + key: key as OnyxKey, + callback: (value) => { + if (!value) { + resolve(null); + return; + } + + resolve({key, value}); + Onyx.disconnect(connectionID); + }, + }); + })); + }); + + return Promise.all(promises); + }; + const exportOnyxState = () => { - console.log('export state here'); + getOnyxValues().then((value) => { + console.log('exported onyx state: ', value.filter(Boolean)); + }); }; const menuItems = useMemo(() => { From 95e118b39567ad95b4dd664778483029b36738c5 Mon Sep 17 00:00:00 2001 From: burczu Date: Thu, 13 Jun 2024 15:37:15 +0200 Subject: [PATCH 021/270] direct access to indexedDB instance possibility added --- .../Troubleshoot/TroubleshootPage.tsx | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index a895cf61e3b2..b622dc81ed9d 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -95,7 +95,39 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { return Promise.all(promises); }; + const readFromIndexedDB = () => new Promise((resolve) => { + let db: IDBDatabase; + const openRequest = indexedDB.open('OnyxDB', 1); + openRequest.onsuccess = () => { + db = openRequest.result; + const transaction = db.transaction('keyvaluepairs'); + const objectStore = transaction.objectStore('keyvaluepairs'); + const cursor = objectStore.openCursor(); + + const queryResult: Record = {}; + + cursor.onerror = () => { + console.error('Error reading cursor'); + } + + cursor.onsuccess = (event) => { + const { result } = event.target as IDBRequest; + if (result) { + queryResult[result.primaryKey as string] = result.value; + result.continue(); + } + else { + resolve(queryResult); + } + } + }; + }); + const exportOnyxState = () => { + readFromIndexedDB().then((value) => { + console.log('exported indexedDB state: ', value); + }); + getOnyxValues().then((value) => { console.log('exported onyx state: ', value.filter(Boolean)); }); From fca2b12db0c625967fb9725ce774c8924e009b27 Mon Sep 17 00:00:00 2001 From: burczu Date: Thu, 13 Jun 2024 16:01:04 +0200 Subject: [PATCH 022/270] distinction between web and native way of storing Onyx data taken into account --- src/libs/ExportOnyxState/index.native.ts | 19 +++++++++++ src/libs/ExportOnyxState/index.ts | 31 +++++++++++++++++ .../Troubleshoot/TroubleshootPage.tsx | 33 ++----------------- 3 files changed, 53 insertions(+), 30 deletions(-) create mode 100644 src/libs/ExportOnyxState/index.native.ts create mode 100644 src/libs/ExportOnyxState/index.ts diff --git a/src/libs/ExportOnyxState/index.native.ts b/src/libs/ExportOnyxState/index.native.ts new file mode 100644 index 000000000000..3b8b019a32c0 --- /dev/null +++ b/src/libs/ExportOnyxState/index.native.ts @@ -0,0 +1,19 @@ +import {open} from 'react-native-quick-sqlite'; + +const readFromIndexedDB = () => new Promise((resolve) => { + const db = open({name: 'OnyxDB'}); + const query = 'SELECT * FROM keyvaluepairs'; + + db.executeAsync(query, []).then(({rows}) => { + // eslint-disable-next-line no-underscore-dangle + const result = rows?._array.map((row) => ({[row.record_key]: JSON.parse(row.valueJSON as string)})); + + resolve(result); + }); + + db.close(); +}); + +export default { + readFromIndexedDB, +} diff --git a/src/libs/ExportOnyxState/index.ts b/src/libs/ExportOnyxState/index.ts new file mode 100644 index 000000000000..6b61143f9e74 --- /dev/null +++ b/src/libs/ExportOnyxState/index.ts @@ -0,0 +1,31 @@ +const readFromIndexedDB = () => new Promise((resolve) => { + let db: IDBDatabase; + const openRequest = indexedDB.open('OnyxDB', 1); + openRequest.onsuccess = () => { + db = openRequest.result; + const transaction = db.transaction('keyvaluepairs'); + const objectStore = transaction.objectStore('keyvaluepairs'); + const cursor = objectStore.openCursor(); + + const queryResult: Record = {}; + + cursor.onerror = () => { + console.error('Error reading cursor'); + } + + cursor.onsuccess = (event) => { + const { result } = event.target as IDBRequest; + if (result) { + queryResult[result.primaryKey as string] = result.value; + result.continue(); + } + else { + resolve(queryResult); + } + } + }; +}); + +export default { + readFromIndexedDB, +} diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index b622dc81ed9d..e80f1a8b3e79 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -1,6 +1,6 @@ import React, {useMemo, useState} from 'react'; import {View} from 'react-native'; -import Onyx, {useOnyx, withOnyx} from 'react-native-onyx'; +import Onyx, {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import type {SvgProps} from 'react-native-svg'; import ClientSideLoggingToolMenu from '@components/ClientSideLoggingToolMenu'; @@ -31,6 +31,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import getLightbulbIllustrationStyle from './getLightbulbIllustrationStyle'; +import ExportOnyxState from '@libs/ExportOnyxState'; type BaseMenuItem = { translationKey: TranslationPaths; @@ -95,36 +96,8 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { return Promise.all(promises); }; - const readFromIndexedDB = () => new Promise((resolve) => { - let db: IDBDatabase; - const openRequest = indexedDB.open('OnyxDB', 1); - openRequest.onsuccess = () => { - db = openRequest.result; - const transaction = db.transaction('keyvaluepairs'); - const objectStore = transaction.objectStore('keyvaluepairs'); - const cursor = objectStore.openCursor(); - - const queryResult: Record = {}; - - cursor.onerror = () => { - console.error('Error reading cursor'); - } - - cursor.onsuccess = (event) => { - const { result } = event.target as IDBRequest; - if (result) { - queryResult[result.primaryKey as string] = result.value; - result.continue(); - } - else { - resolve(queryResult); - } - } - }; - }); - const exportOnyxState = () => { - readFromIndexedDB().then((value) => { + ExportOnyxState.readFromIndexedDB().then((value) => { console.log('exported indexedDB state: ', value); }); From db1aa8a488fe60f4bb73936c860ec01ba774586e Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 14 Jun 2024 10:27:52 +0200 Subject: [PATCH 023/270] Minor fix --- src/pages/iou/request/step/IOURequestStepConfirmation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 1683ef3e3df2..55cd658b4afd 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -139,7 +139,7 @@ function IOURequestStepConfirmation({ const participants = useMemo( () => transaction?.participants?.map((participant) => { - const participantAccountID = participant.accountID ?? -1; + const participantAccountID = participant.accountID; if (participant.isSender && iouType === CONST.IOU.TYPE.INVOICE) { return participant; From 7eed2813dddc08e9e54b365e557745c218fc3b8b Mon Sep 17 00:00:00 2001 From: burczu Date: Fri, 14 Jun 2024 13:24:17 +0200 Subject: [PATCH 024/270] saving as txt file added --- src/libs/ExportOnyxState/index.native.ts | 25 +++++++++- src/libs/ExportOnyxState/index.ts | 15 ++++++ .../Troubleshoot/TroubleshootPage.tsx | 50 ++----------------- 3 files changed, 43 insertions(+), 47 deletions(-) diff --git a/src/libs/ExportOnyxState/index.native.ts b/src/libs/ExportOnyxState/index.native.ts index 3b8b019a32c0..2d53b9c39b5f 100644 --- a/src/libs/ExportOnyxState/index.native.ts +++ b/src/libs/ExportOnyxState/index.native.ts @@ -1,4 +1,6 @@ import {open} from 'react-native-quick-sqlite'; +import RNFS from "react-native-fs"; +import Share from "react-native-share"; const readFromIndexedDB = () => new Promise((resolve) => { const db = open({name: 'OnyxDB'}); @@ -9,11 +11,32 @@ const readFromIndexedDB = () => new Promise((resolve) => { const result = rows?._array.map((row) => ({[row.record_key]: JSON.parse(row.valueJSON as string)})); resolve(result); + db.close(); }); - db.close(); }); +// eslint-disable-next-line @lwc/lwc/no-async-await +const shareAsFile = async (value: string) => { + try { + // Define new filename and path for the app info file + const infoFileName = `onyx-state.txt`; + const infoFilePath = `${RNFS.DocumentDirectoryPath}/${infoFileName}`; + const actualInfoFile = `file://${infoFilePath}`; + + await RNFS.writeFile(infoFilePath, value, 'utf8'); + + const shareOptions = { + urls: [actualInfoFile], + }; + + await Share.open(shareOptions); + } catch (error) { + console.error('Error renaming and sharing file:', error); + } +} + export default { readFromIndexedDB, + shareAsFile, } diff --git a/src/libs/ExportOnyxState/index.ts b/src/libs/ExportOnyxState/index.ts index 6b61143f9e74..e7427b097ae2 100644 --- a/src/libs/ExportOnyxState/index.ts +++ b/src/libs/ExportOnyxState/index.ts @@ -26,6 +26,21 @@ const readFromIndexedDB = () => new Promise((resolve) => { }; }); +// eslint-disable-next-line @lwc/lwc/no-async-await,@typescript-eslint/require-await +const shareAsFile = async (value: string) => { + const element = document.createElement('a'); + element.setAttribute('href', `data:text/plain;charset=utf-8,${ encodeURIComponent(value)}`); + element.setAttribute('download', 'onyx-state.txt'); + + element.style.display = 'none'; + document.body.appendChild(element); + + element.click(); + + document.body.removeChild(element); +} + export default { readFromIndexedDB, + shareAsFile, } diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index e80f1a8b3e79..8b9e174697e5 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -26,12 +26,11 @@ import Navigation from '@libs/Navigation/Navigation'; import * as App from '@userActions/App'; import * as Report from '@userActions/Report'; import type {TranslationPaths} from '@src/languages/types'; -import type {OnyxKey} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; -import getLightbulbIllustrationStyle from './getLightbulbIllustrationStyle'; import ExportOnyxState from '@libs/ExportOnyxState'; +import getLightbulbIllustrationStyle from './getLightbulbIllustrationStyle'; type BaseMenuItem = { translationKey: TranslationPaths; @@ -55,54 +54,13 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { const {isSmallScreenWidth} = useWindowDimensions(); const illustrationStyle = getLightbulbIllustrationStyle(); - const getOnyxKeys = (keysObject: Record) => { - const keys: string[] = []; - - Object.keys(keysObject).forEach((key) => { - if (typeof keysObject[key] === 'object') { - keys.push(...getOnyxKeys(keysObject[key] as Record)); - return; - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - keys.push(keysObject[key] as string); - }); - - return keys; - } - - const getOnyxValues = () => { - const keys = getOnyxKeys(ONYXKEYS); - const promises: Array>> = []; - - keys.forEach((key) => { - promises.push(new Promise((resolve) => { - // eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs - const connectionID = Onyx.connect({ - key: key as OnyxKey, - callback: (value) => { - if (!value) { - resolve(null); - return; - } - - resolve({key, value}); - Onyx.disconnect(connectionID); - }, - }); - })); - }); - - return Promise.all(promises); - }; - const exportOnyxState = () => { ExportOnyxState.readFromIndexedDB().then((value) => { console.log('exported indexedDB state: ', value); - }); - getOnyxValues().then((value) => { - console.log('exported onyx state: ', value.filter(Boolean)); + ExportOnyxState.shareAsFile(JSON.stringify(value)).then(() => { + console.log('exported indexedDB state as file'); + }); }); }; From aa9bd53c3907b4371c9cc7c56f43428b52c74bbf Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 14 Jun 2024 15:34:25 +0200 Subject: [PATCH 025/270] Hide Pay as individual option in B2B room --- src/components/SettlementButton.tsx | 32 +++++++++++++++-------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx index b1386e1a6fa3..de1197c78f32 100644 --- a/src/components/SettlementButton.tsx +++ b/src/components/SettlementButton.tsx @@ -202,20 +202,22 @@ function SettlementButton({ } if (isInvoiceReport) { - buttonOptions.push({ - text: translate('iou.settlePersonal', {formattedAmount}), - icon: Expensicons.User, - value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, - backButtonText: translate('iou.individual'), - subMenuItems: [ - { - text: translate('iou.payElsewhere', {formattedAmount: ''}), - icon: Expensicons.Cash, - value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, - onSelected: () => onPress(CONST.IOU.PAYMENT_TYPE.ELSEWHERE), - }, - ], - }); + if (ReportUtils.isIndividualInvoiceRoom(chatReport)) { + buttonOptions.push({ + text: translate('iou.settlePersonal', {formattedAmount}), + icon: Expensicons.User, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + backButtonText: translate('iou.individual'), + subMenuItems: [ + { + text: translate('iou.payElsewhere', {formattedAmount: ''}), + icon: Expensicons.Cash, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + onSelected: () => onPress(CONST.IOU.PAYMENT_TYPE.ELSEWHERE), + }, + ], + }); + } if (PolicyUtils.isPolicyAdmin(primaryPolicy)) { buttonOptions.push({ @@ -246,7 +248,7 @@ function SettlementButton({ return buttonOptions; // We don't want to reorder the options when the preferred payment method changes while the button is still visible // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currency, formattedAmount, iouReport, policyID, translate, shouldHidePaymentOptions, shouldShowApproveButton, shouldDisableApproveButton]); + }, [currency, formattedAmount, iouReport, chatReport, policyID, translate, shouldHidePaymentOptions, shouldShowApproveButton, shouldDisableApproveButton]); const selectPaymentType = (event: KYCFlowEvent, iouPaymentType: PaymentMethodType, triggerKYCFlow: TriggerKYCFlow) => { if (iouPaymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY || iouPaymentType === CONST.IOU.PAYMENT_TYPE.VBBA) { From fec109e91133849daedd3c52eb4b03010242802a Mon Sep 17 00:00:00 2001 From: Yauheni Date: Fri, 14 Jun 2024 21:05:55 +0200 Subject: [PATCH 026/270] Fix minnor issues related with arrows --- .../Attachments/AttachmentCarousel/index.tsx | 13 ++++++++--- .../AttachmentCarousel/useCarouselArrows.ts | 23 ++++++++++++++++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index ebb43ef45eb9..99332e703790 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -53,7 +53,14 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const [page, setPage] = useState(0); const [attachments, setAttachments] = useState([]); const [activeSource, setActiveSource] = useState(source); - const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows(); + const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows, onChangeArrowsState} = useCarouselArrows(); + + useEffect(() => { + if (!canUseTouchScreen || zoomScale !== 1) { + return; + } + setShouldShowArrows(true); + }, [canUseTouchScreen, page, setShouldShowArrows, zoomScale]); const compareImage = useCallback((attachment: Attachment) => attachment.source === source, [source]); @@ -178,12 +185,12 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, setShouldShowArrows((oldState) => !oldState) : undefined} + onPress={canUseTouchScreen ? () => onChangeArrowsState(zoomScale === 1) : undefined} isModalHovered={shouldShowArrows} /> ), - [activeSource, canUseTouchScreen, cellWidth, setShouldShowArrows, shouldShowArrows, styles.h100], + [activeSource, canUseTouchScreen, cellWidth, zoomScale, onChangeArrowsState, shouldShowArrows, styles.h100], ); /** Pan gesture handing swiping through attachments on touch screen devices */ const pan = useMemo( diff --git a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts index 12ca3db4e2ff..1b21a3af6000 100644 --- a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts +++ b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts @@ -7,6 +7,7 @@ function useCarouselArrows() { const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); const [shouldShowArrows, setShouldShowArrowsInternal] = useState(canUseTouchScreen); const autoHideArrowTimeout = useRef(null); + const singleTapRef = useRef(null); /** * Cancels the automatic hiding of the arrows. @@ -45,7 +46,27 @@ function useCarouselArrows() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - return {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows}; + const onChangeArrowsState = useCallback( + (enabled: boolean) => { + if (!enabled) { + return; + } + + if (singleTapRef.current) { + clearTimeout(singleTapRef.current); + singleTapRef.current = null; + return; + } + + singleTapRef.current = setTimeout(() => { + setShouldShowArrows((oldState) => !oldState); + singleTapRef.current = null; + }, 200); + }, + [setShouldShowArrows], + ); + + return {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows, onChangeArrowsState}; } export default useCarouselArrows; From 6c6390b443aadf6c863b06f907dace581357af4f Mon Sep 17 00:00:00 2001 From: Yauheni Date: Fri, 14 Jun 2024 22:14:59 +0200 Subject: [PATCH 027/270] Fix bug with animation for carousel --- src/components/Attachments/AttachmentCarousel/index.tsx | 2 +- src/components/MultiGestureCanvas/usePanGesture.ts | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index 99332e703790..35accf837caa 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -197,7 +197,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, () => Gesture.Pan() .enabled(canUseTouchScreen && zoomScale === 1) - .onUpdate(({translationX}) => scrollTo(scrollRef, page * cellWidth - translationX * 2, 0, false)) + .onUpdate(({translationX}) => scrollTo(scrollRef, page * cellWidth - translationX, 0, false)) .onEnd(({translationX, velocityX}) => { let newIndex; if (velocityX > MIN_FLING_VELOCITY) { diff --git a/src/components/MultiGestureCanvas/usePanGesture.ts b/src/components/MultiGestureCanvas/usePanGesture.ts index 903f384dd525..c236393027ef 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.ts +++ b/src/components/MultiGestureCanvas/usePanGesture.ts @@ -206,10 +206,6 @@ const usePanGesture = ({ panVelocityX.value = evt.velocityX; panVelocityY.value = evt.velocityY; - if (!isSwipingDownToClose.value) { - panTranslateX.value += evt.changeX; - } - if (enableSwipeDownToClose.value || isSwipingDownToClose.value) { panTranslateY.value += evt.changeY; } From 879efb76df8de3316fed4489b73259945551ae98 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 15 Jun 2024 11:54:05 +0800 Subject: [PATCH 028/270] update translation --- src/components/MoneyReportHeader.tsx | 1 + src/components/ProcessMoneyReportHoldMenu.tsx | 17 ++++++++++------- .../ReportActionItem/ReportPreview.tsx | 1 + src/languages/en.ts | 5 +++-- src/languages/types.ts | 3 +++ 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 90952157f179..38ef7812c117 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -356,6 +356,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea paymentType={paymentType} chatReport={chatReport} moneyRequestReport={moneyRequestReport} + transactionCount={transactionIDs.length} /> )} { - let promptTranslation: TranslationPaths; if (nonHeldAmount) { - promptTranslation = isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount'; + return translate(isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount'); } else { - promptTranslation = isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount'; + return translate( + isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount', + {transactionCount} + ); } - - return translate(promptTranslation); - }, [nonHeldAmount]); + }, [nonHeldAmount, transactionCount, translate, isApprove]); return ( )} diff --git a/src/languages/en.ts b/src/languages/en.ts index 59c9da46a36d..3b2ac5c46d8d 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -13,6 +13,7 @@ import type { BeginningOfChatHistoryDomainRoomPartOneParams, CanceledRequestParams, CharacterLimitParams, + ConfirmHoldExpenseParams, ConfirmThatParams, DateShouldBeAfterParams, DateShouldBeBeforeParams, @@ -766,10 +767,10 @@ export default { reviewDuplicates: 'Review duplicates', confirmApprove: 'Confirm approval amount', confirmApprovalAmount: "Approve what's not on hold, or approve the entire report.", - confirmApprovalAllHoldAmount: 'All expenses on this report are on hold. Do you want to unhold them and approve?', + confirmApprovalAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to approve anyway?`, confirmPay: 'Confirm payment amount', confirmPayAmount: "Pay what's not on hold, or pay all out-of-pocket spend.", - confirmPayAllHoldAmount: 'All expenses on this report are on hold. Do you want to unhold them and pay?', + confirmPayAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to pay anyway?`, payOnly: 'Pay only', approveOnly: 'Approve only', holdEducationalTitle: 'This expense is on', diff --git a/src/languages/types.ts b/src/languages/types.ts index de9b1d2dadeb..b6553be196e1 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -298,6 +298,8 @@ type DistanceRateOperationsParams = {count: number}; type ReimbursementRateParams = {unit: Unit}; +type ConfirmHoldExpenseParams = {transactionCount: number}; + export type { AddressLineParams, AdminCanceledRequestParams, @@ -309,6 +311,7 @@ export type { BeginningOfChatHistoryDomainRoomPartOneParams, CanceledRequestParams, CharacterLimitParams, + ConfirmHoldExpenseParams, ConfirmThatParams, DateShouldBeAfterParams, DateShouldBeBeforeParams, From fc6515ed4fe59e189333d85b00f31fd249928c87 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 15 Jun 2024 11:56:02 +0800 Subject: [PATCH 029/270] prettier --- src/components/ProcessMoneyReportHoldMenu.tsx | 5 +---- src/languages/en.ts | 6 ++++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx index 8986083ee3c6..9f3839fe2a13 100644 --- a/src/components/ProcessMoneyReportHoldMenu.tsx +++ b/src/components/ProcessMoneyReportHoldMenu.tsx @@ -76,10 +76,7 @@ function ProcessMoneyReportHoldMenu({ if (nonHeldAmount) { return translate(isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount'); } else { - return translate( - isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount', - {transactionCount} - ); + return translate(isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount', {transactionCount}); } }, [nonHeldAmount, transactionCount, translate, isApprove]); diff --git a/src/languages/en.ts b/src/languages/en.ts index 3b2ac5c46d8d..0a47ac58bcbd 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -767,10 +767,12 @@ export default { reviewDuplicates: 'Review duplicates', confirmApprove: 'Confirm approval amount', confirmApprovalAmount: "Approve what's not on hold, or approve the entire report.", - confirmApprovalAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to approve anyway?`, + confirmApprovalAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => + `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to approve anyway?`, confirmPay: 'Confirm payment amount', confirmPayAmount: "Pay what's not on hold, or pay all out-of-pocket spend.", - confirmPayAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to pay anyway?`, + confirmPayAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => + `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to pay anyway?`, payOnly: 'Pay only', approveOnly: 'Approve only', holdEducationalTitle: 'This expense is on', From 0cc82d7849b171ed04d5126aec2f2ee520a2c588 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 15 Jun 2024 12:25:33 +0800 Subject: [PATCH 030/270] lint --- src/components/ProcessMoneyReportHoldMenu.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx index 9f3839fe2a13..872464d8a5b0 100644 --- a/src/components/ProcessMoneyReportHoldMenu.tsx +++ b/src/components/ProcessMoneyReportHoldMenu.tsx @@ -75,9 +75,8 @@ function ProcessMoneyReportHoldMenu({ const promptText = useMemo(() => { if (nonHeldAmount) { return translate(isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount'); - } else { - return translate(isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount', {transactionCount}); } + return translate(isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount', {transactionCount}); }, [nonHeldAmount, transactionCount, translate, isApprove]); return ( From 4fa559f121748d35ac88aae7646ac7f72166b57c Mon Sep 17 00:00:00 2001 From: Yauheni Date: Sat, 15 Jun 2024 08:50:31 +0200 Subject: [PATCH 031/270] Fix bug with animation for carousel x2 --- src/components/MultiGestureCanvas/usePanGesture.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/MultiGestureCanvas/usePanGesture.ts b/src/components/MultiGestureCanvas/usePanGesture.ts index c236393027ef..08d0d94d64d6 100644 --- a/src/components/MultiGestureCanvas/usePanGesture.ts +++ b/src/components/MultiGestureCanvas/usePanGesture.ts @@ -3,6 +3,7 @@ import {Dimensions} from 'react-native'; import type {PanGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; import {runOnJS, useDerivedValue, useSharedValue, useWorkletCallback, withDecay, withSpring} from 'react-native-reanimated'; +import * as Browser from '@libs/Browser'; import {SPRING_CONFIG} from './constants'; import type {MultiGestureCanvasVariables} from './types'; import * as MultiGestureCanvasUtils from './utils'; @@ -206,6 +207,10 @@ const usePanGesture = ({ panVelocityX.value = evt.velocityX; panVelocityY.value = evt.velocityY; + if (!isSwipingDownToClose.value && !Browser.isMobile()) { + panTranslateX.value += evt.changeX; + } + if (enableSwipeDownToClose.value || isSwipingDownToClose.value) { panTranslateY.value += evt.changeY; } From d133a3dee507affa61811186f9303a9350baa418 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 18 Jun 2024 12:25:12 +0800 Subject: [PATCH 032/270] update copy --- src/languages/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index b24c8b244b4d..293df4aa294c 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -770,7 +770,7 @@ export default { confirmApprovalAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to approve anyway?`, confirmPay: 'Confirm payment amount', - confirmPayAmount: "Pay what's not on hold, or pay all out-of-pocket spend.", + confirmPayAmount: "Pay what's not on hold, or pay the entire report.", confirmPayAllHoldAmount: ({transactionCount}: ConfirmHoldExpenseParams) => `${Str.pluralize('This expense is', 'These expenses are', transactionCount)} on hold. Do you want to pay anyway?`, payOnly: 'Pay only', From 3005ecca952e7edf234ba251592c9706be7c3a07 Mon Sep 17 00:00:00 2001 From: burczu Date: Tue, 18 Jun 2024 11:32:39 +0200 Subject: [PATCH 033/270] masking fragile data added --- src/libs/ExportOnyxState/index.native.ts | 7 +++-- src/libs/ExportOnyxState/index.ts | 29 +++++++++++++++++-- .../Troubleshoot/TroubleshootPage.tsx | 8 ++--- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/libs/ExportOnyxState/index.native.ts b/src/libs/ExportOnyxState/index.native.ts index 2d53b9c39b5f..8b2145900949 100644 --- a/src/libs/ExportOnyxState/index.native.ts +++ b/src/libs/ExportOnyxState/index.native.ts @@ -1,6 +1,7 @@ import {open} from 'react-native-quick-sqlite'; import RNFS from "react-native-fs"; import Share from "react-native-share"; +import * as main from './index'; const readFromIndexedDB = () => new Promise((resolve) => { const db = open({name: 'OnyxDB'}); @@ -16,8 +17,7 @@ const readFromIndexedDB = () => new Promise((resolve) => { }); -// eslint-disable-next-line @lwc/lwc/no-async-await -const shareAsFile = async (value: string) => { +const shareAsFile = (value: string) => { try { // Define new filename and path for the app info file const infoFileName = `onyx-state.txt`; @@ -30,13 +30,14 @@ const shareAsFile = async (value: string) => { urls: [actualInfoFile], }; - await Share.open(shareOptions); + Share.open(shareOptions); } catch (error) { console.error('Error renaming and sharing file:', error); } } export default { + maskFragileData: main.default.maskFragileData, readFromIndexedDB, shareAsFile, } diff --git a/src/libs/ExportOnyxState/index.ts b/src/libs/ExportOnyxState/index.ts index e7427b097ae2..1d610812d40d 100644 --- a/src/libs/ExportOnyxState/index.ts +++ b/src/libs/ExportOnyxState/index.ts @@ -1,4 +1,6 @@ -const readFromIndexedDB = () => new Promise((resolve) => { +import {Str} from "expensify-common"; + +const readFromIndexedDB = () => new Promise>((resolve) => { let db: IDBDatabase; const openRequest = indexedDB.open('OnyxDB', 1); openRequest.onsuccess = () => { @@ -26,8 +28,28 @@ const readFromIndexedDB = () => new Promise((resolve) => { }; }); -// eslint-disable-next-line @lwc/lwc/no-async-await,@typescript-eslint/require-await -const shareAsFile = async (value: string) => { +const maskFragileData = (data: Record): Record => { + const maskedData: Record = {}; + + for (const key in data) { + if (Object.prototype.hasOwnProperty.call(data, key)) { + const value = data[key]; + if (typeof value === 'string' && (Str.isValidEmail(value) || key === 'authToken' || key === 'encryptedAuthToken')) { + maskedData[key] = '***'; + } + else if (typeof value === 'object') { + maskedData[key] = maskFragileData(value as Record); + } + else { + maskedData[key] = value; + } + } + } + + return maskedData; +} + +const shareAsFile = (value: string) => { const element = document.createElement('a'); element.setAttribute('href', `data:text/plain;charset=utf-8,${ encodeURIComponent(value)}`); element.setAttribute('download', 'onyx-state.txt'); @@ -41,6 +63,7 @@ const shareAsFile = async (value: string) => { } export default { + maskFragileData, readFromIndexedDB, shareAsFile, } diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index 8b9e174697e5..a87ffbcb8980 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -55,12 +55,10 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { const illustrationStyle = getLightbulbIllustrationStyle(); const exportOnyxState = () => { - ExportOnyxState.readFromIndexedDB().then((value) => { - console.log('exported indexedDB state: ', value); + ExportOnyxState.readFromIndexedDB().then((value: Record) => { + const maskedData = ExportOnyxState.maskFragileData(value); - ExportOnyxState.shareAsFile(JSON.stringify(value)).then(() => { - console.log('exported indexedDB state as file'); - }); + ExportOnyxState.shareAsFile(JSON.stringify(maskedData)); }); }; From ed03efe2f73e0151c532c2525c659638cefa32cb Mon Sep 17 00:00:00 2001 From: burczu Date: Tue, 18 Jun 2024 11:52:07 +0200 Subject: [PATCH 034/270] prettier --- src/libs/ExportOnyxState/index.ts | 64 +++++++++---------- .../Troubleshoot/TroubleshootPage.tsx | 6 +- 2 files changed, 34 insertions(+), 36 deletions(-) diff --git a/src/libs/ExportOnyxState/index.ts b/src/libs/ExportOnyxState/index.ts index 1d610812d40d..5c1817281934 100644 --- a/src/libs/ExportOnyxState/index.ts +++ b/src/libs/ExportOnyxState/index.ts @@ -1,32 +1,32 @@ -import {Str} from "expensify-common"; +import {Str} from 'expensify-common'; -const readFromIndexedDB = () => new Promise>((resolve) => { - let db: IDBDatabase; - const openRequest = indexedDB.open('OnyxDB', 1); - openRequest.onsuccess = () => { - db = openRequest.result; - const transaction = db.transaction('keyvaluepairs'); - const objectStore = transaction.objectStore('keyvaluepairs'); - const cursor = objectStore.openCursor(); +const readFromIndexedDB = () => + new Promise>((resolve) => { + let db: IDBDatabase; + const openRequest = indexedDB.open('OnyxDB', 1); + openRequest.onsuccess = () => { + db = openRequest.result; + const transaction = db.transaction('keyvaluepairs'); + const objectStore = transaction.objectStore('keyvaluepairs'); + const cursor = objectStore.openCursor(); - const queryResult: Record = {}; + const queryResult: Record = {}; - cursor.onerror = () => { - console.error('Error reading cursor'); - } + cursor.onerror = () => { + console.error('Error reading cursor'); + }; - cursor.onsuccess = (event) => { - const { result } = event.target as IDBRequest; - if (result) { - queryResult[result.primaryKey as string] = result.value; - result.continue(); - } - else { - resolve(queryResult); - } - } - }; -}); + cursor.onsuccess = (event) => { + const {result} = event.target as IDBRequest; + if (result) { + queryResult[result.primaryKey as string] = result.value; + result.continue(); + } else { + resolve(queryResult); + } + }; + }; + }); const maskFragileData = (data: Record): Record => { const maskedData: Record = {}; @@ -36,22 +36,20 @@ const maskFragileData = (data: Record): Record const value = data[key]; if (typeof value === 'string' && (Str.isValidEmail(value) || key === 'authToken' || key === 'encryptedAuthToken')) { maskedData[key] = '***'; - } - else if (typeof value === 'object') { + } else if (typeof value === 'object') { maskedData[key] = maskFragileData(value as Record); - } - else { + } else { maskedData[key] = value; } } } return maskedData; -} +}; const shareAsFile = (value: string) => { const element = document.createElement('a'); - element.setAttribute('href', `data:text/plain;charset=utf-8,${ encodeURIComponent(value)}`); + element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(value)}`); element.setAttribute('download', 'onyx-state.txt'); element.style.display = 'none'; @@ -60,10 +58,10 @@ const shareAsFile = (value: string) => { element.click(); document.body.removeChild(element); -} +}; export default { maskFragileData, readFromIndexedDB, shareAsFile, -} +}; diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index a87ffbcb8980..ce6586d8f275 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -22,6 +22,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import ExportOnyxState from '@libs/ExportOnyxState'; import Navigation from '@libs/Navigation/Navigation'; import * as App from '@userActions/App'; import * as Report from '@userActions/Report'; @@ -29,7 +30,6 @@ import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; -import ExportOnyxState from '@libs/ExportOnyxState'; import getLightbulbIllustrationStyle from './getLightbulbIllustrationStyle'; type BaseMenuItem = { @@ -78,8 +78,8 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { { translationKey: 'initialSettingsPage.troubleshoot.exportOnyxState', icon: Expensicons.Download, - action: exportOnyxState - } + action: exportOnyxState, + }, ]; if (shouldStoreLogs) { From 4160cc878a62a3b7bc49176fc252d459e25f3ab9 Mon Sep 17 00:00:00 2001 From: burczu Date: Wed, 19 Jun 2024 10:28:11 +0200 Subject: [PATCH 035/270] common part extracted --- src/libs/ExportOnyxState/common.ts | 24 +++++++++++++ src/libs/ExportOnyxState/index.native.ts | 36 +++++++++---------- src/libs/ExportOnyxState/index.ts | 30 ++++------------ .../Troubleshoot/TroubleshootPage.tsx | 2 +- 4 files changed, 49 insertions(+), 43 deletions(-) create mode 100644 src/libs/ExportOnyxState/common.ts diff --git a/src/libs/ExportOnyxState/common.ts b/src/libs/ExportOnyxState/common.ts new file mode 100644 index 000000000000..09b2d955400e --- /dev/null +++ b/src/libs/ExportOnyxState/common.ts @@ -0,0 +1,24 @@ +import {Str} from 'expensify-common'; + +const maskFragileData = (data: Record): Record => { + const maskedData: Record = {}; + + for (const key in data) { + if (Object.prototype.hasOwnProperty.call(data, key)) { + const value = data[key]; + if (typeof value === 'string' && (Str.isValidEmail(value) || key === 'authToken' || key === 'encryptedAuthToken')) { + maskedData[key] = '***'; + } else if (typeof value === 'object') { + maskedData[key] = maskFragileData(value as Record); + } else { + maskedData[key] = value; + } + } + } + + return maskedData; +}; + +export default { + maskFragileData, +}; diff --git a/src/libs/ExportOnyxState/index.native.ts b/src/libs/ExportOnyxState/index.native.ts index 8b2145900949..de9c34153c5c 100644 --- a/src/libs/ExportOnyxState/index.native.ts +++ b/src/libs/ExportOnyxState/index.native.ts @@ -1,23 +1,23 @@ +import RNFS from 'react-native-fs'; import {open} from 'react-native-quick-sqlite'; -import RNFS from "react-native-fs"; -import Share from "react-native-share"; -import * as main from './index'; +import Share from 'react-native-share'; +import common from './common'; -const readFromIndexedDB = () => new Promise((resolve) => { - const db = open({name: 'OnyxDB'}); - const query = 'SELECT * FROM keyvaluepairs'; +const readFromOnyxDatabase = () => + new Promise((resolve) => { + const db = open({name: 'OnyxDB'}); + const query = 'SELECT * FROM keyvaluepairs'; - db.executeAsync(query, []).then(({rows}) => { - // eslint-disable-next-line no-underscore-dangle - const result = rows?._array.map((row) => ({[row.record_key]: JSON.parse(row.valueJSON as string)})); + db.executeAsync(query, []).then(({rows}) => { + // eslint-disable-next-line no-underscore-dangle + const result = rows?._array.map((row) => ({[row.record_key]: JSON.parse(row.valueJSON as string)})); - resolve(result); - db.close(); + resolve(result); + }); }); -}); - -const shareAsFile = (value: string) => { +// eslint-disable-next-line @lwc/lwc/no-async-await +const shareAsFile = async (value: string) => { try { // Define new filename and path for the app info file const infoFileName = `onyx-state.txt`; @@ -34,10 +34,10 @@ const shareAsFile = (value: string) => { } catch (error) { console.error('Error renaming and sharing file:', error); } -} +}; export default { - maskFragileData: main.default.maskFragileData, - readFromIndexedDB, + maskFragileData: common.maskFragileData, + readFromOnyxDatabase, shareAsFile, -} +}; diff --git a/src/libs/ExportOnyxState/index.ts b/src/libs/ExportOnyxState/index.ts index 5c1817281934..840fc26eeb20 100644 --- a/src/libs/ExportOnyxState/index.ts +++ b/src/libs/ExportOnyxState/index.ts @@ -1,6 +1,6 @@ -import {Str} from 'expensify-common'; +import common from './common'; -const readFromIndexedDB = () => +const readFromOnyxDatabase = () => new Promise>((resolve) => { let db: IDBDatabase; const openRequest = indexedDB.open('OnyxDB', 1); @@ -28,26 +28,8 @@ const readFromIndexedDB = () => }; }); -const maskFragileData = (data: Record): Record => { - const maskedData: Record = {}; - - for (const key in data) { - if (Object.prototype.hasOwnProperty.call(data, key)) { - const value = data[key]; - if (typeof value === 'string' && (Str.isValidEmail(value) || key === 'authToken' || key === 'encryptedAuthToken')) { - maskedData[key] = '***'; - } else if (typeof value === 'object') { - maskedData[key] = maskFragileData(value as Record); - } else { - maskedData[key] = value; - } - } - } - - return maskedData; -}; - -const shareAsFile = (value: string) => { +// eslint-disable-next-line @lwc/lwc/no-async-await,@typescript-eslint/require-await +const shareAsFile = async (value: string) => { const element = document.createElement('a'); element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(value)}`); element.setAttribute('download', 'onyx-state.txt'); @@ -61,7 +43,7 @@ const shareAsFile = (value: string) => { }; export default { - maskFragileData, - readFromIndexedDB, + maskFragileData: common.maskFragileData, + readFromOnyxDatabase, shareAsFile, }; diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index ce6586d8f275..bfe512df8526 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -55,7 +55,7 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { const illustrationStyle = getLightbulbIllustrationStyle(); const exportOnyxState = () => { - ExportOnyxState.readFromIndexedDB().then((value: Record) => { + ExportOnyxState.readFromOnyxDatabase().then((value: Record) => { const maskedData = ExportOnyxState.maskFragileData(value); ExportOnyxState.shareAsFile(JSON.stringify(maskedData)); From 934346de737ddca157970adfc4e441e34d33ca6b Mon Sep 17 00:00:00 2001 From: burczu Date: Wed, 19 Jun 2024 10:35:40 +0200 Subject: [PATCH 036/270] useCallback usage added --- src/pages/settings/Troubleshoot/TroubleshootPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index bfe512df8526..0a763ad75562 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useState} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import {View} from 'react-native'; import Onyx, {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -54,13 +54,13 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { const {isSmallScreenWidth} = useWindowDimensions(); const illustrationStyle = getLightbulbIllustrationStyle(); - const exportOnyxState = () => { + const exportOnyxState = useCallback(() => { ExportOnyxState.readFromOnyxDatabase().then((value: Record) => { const maskedData = ExportOnyxState.maskFragileData(value); ExportOnyxState.shareAsFile(JSON.stringify(maskedData)); }); - }; + }, []); const menuItems = useMemo(() => { const debugConsoleItem: BaseMenuItem = { From 6c0574db7f10eb54a7f3ee3f725b280ea0374d2a Mon Sep 17 00:00:00 2001 From: burczu Date: Wed, 19 Jun 2024 10:48:17 +0200 Subject: [PATCH 037/270] const values extracted --- src/CONST.ts | 3 +++ src/libs/ExportOnyxState/common.ts | 5 +++++ src/libs/ExportOnyxState/index.native.ts | 22 +++++++++++----------- src/libs/ExportOnyxState/index.ts | 12 ++++++------ 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 3d67a951111e..4cdcb61da6d4 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -74,6 +74,9 @@ const onboardingChoices = { type OnboardingPurposeType = ValueOf; const CONST = { + DEFAULT_DB_NAME: 'OnyxDB', + DEFAULT_TABLE_NAME: 'keyvaluepairs', + DEFAULT_ONYX_DUMP_FILE_NAME: 'onyx-state.txt', DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL], // Note: Group and Self-DM excluded as these are not tied to a Workspace diff --git a/src/libs/ExportOnyxState/common.ts b/src/libs/ExportOnyxState/common.ts index 09b2d955400e..b85daa27615d 100644 --- a/src/libs/ExportOnyxState/common.ts +++ b/src/libs/ExportOnyxState/common.ts @@ -1,5 +1,8 @@ import {Str} from 'expensify-common'; +const ONYX_DB_KEY = 'OnyxDB'; +const DEFAULT_FILE_NAME = 'onyx-state.txt'; + const maskFragileData = (data: Record): Record => { const maskedData: Record = {}; @@ -21,4 +24,6 @@ const maskFragileData = (data: Record): Record export default { maskFragileData, + ONYX_DB_KEY, + DEFAULT_FILE_NAME, }; diff --git a/src/libs/ExportOnyxState/index.native.ts b/src/libs/ExportOnyxState/index.native.ts index de9c34153c5c..ff8ff9e4f730 100644 --- a/src/libs/ExportOnyxState/index.native.ts +++ b/src/libs/ExportOnyxState/index.native.ts @@ -1,12 +1,13 @@ import RNFS from 'react-native-fs'; import {open} from 'react-native-quick-sqlite'; import Share from 'react-native-share'; +import CONST from '@src/CONST'; import common from './common'; const readFromOnyxDatabase = () => new Promise((resolve) => { - const db = open({name: 'OnyxDB'}); - const query = 'SELECT * FROM keyvaluepairs'; + const db = open({name: CONST.DEFAULT_DB_NAME}); + const query = `SELECT * FROM ${CONST.DEFAULT_TABLE_NAME}`; db.executeAsync(query, []).then(({rows}) => { // eslint-disable-next-line no-underscore-dangle @@ -16,21 +17,20 @@ const readFromOnyxDatabase = () => }); }); -// eslint-disable-next-line @lwc/lwc/no-async-await -const shareAsFile = async (value: string) => { +const shareAsFile = (value: string) => { try { // Define new filename and path for the app info file - const infoFileName = `onyx-state.txt`; + const infoFileName = CONST.DEFAULT_ONYX_DUMP_FILE_NAME; const infoFilePath = `${RNFS.DocumentDirectoryPath}/${infoFileName}`; const actualInfoFile = `file://${infoFilePath}`; - await RNFS.writeFile(infoFilePath, value, 'utf8'); + RNFS.writeFile(infoFilePath, value, 'utf8').then(() => { + const shareOptions = { + urls: [actualInfoFile], + }; - const shareOptions = { - urls: [actualInfoFile], - }; - - Share.open(shareOptions); + Share.open(shareOptions); + }); } catch (error) { console.error('Error renaming and sharing file:', error); } diff --git a/src/libs/ExportOnyxState/index.ts b/src/libs/ExportOnyxState/index.ts index 840fc26eeb20..b47b3a01fb8b 100644 --- a/src/libs/ExportOnyxState/index.ts +++ b/src/libs/ExportOnyxState/index.ts @@ -1,13 +1,14 @@ +import CONST from '@src/CONST'; import common from './common'; const readFromOnyxDatabase = () => new Promise>((resolve) => { let db: IDBDatabase; - const openRequest = indexedDB.open('OnyxDB', 1); + const openRequest = indexedDB.open(CONST.DEFAULT_DB_NAME, 1); openRequest.onsuccess = () => { db = openRequest.result; - const transaction = db.transaction('keyvaluepairs'); - const objectStore = transaction.objectStore('keyvaluepairs'); + const transaction = db.transaction(CONST.DEFAULT_TABLE_NAME); + const objectStore = transaction.objectStore(CONST.DEFAULT_TABLE_NAME); const cursor = objectStore.openCursor(); const queryResult: Record = {}; @@ -28,11 +29,10 @@ const readFromOnyxDatabase = () => }; }); -// eslint-disable-next-line @lwc/lwc/no-async-await,@typescript-eslint/require-await -const shareAsFile = async (value: string) => { +const shareAsFile = (value: string) => { const element = document.createElement('a'); element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(value)}`); - element.setAttribute('download', 'onyx-state.txt'); + element.setAttribute('download', CONST.DEFAULT_ONYX_DUMP_FILE_NAME); element.style.display = 'none'; document.body.appendChild(element); From 56fe0cda27afac0f8ee3a5033e8f8ebfffead9f4 Mon Sep 17 00:00:00 2001 From: burczu Date: Wed, 19 Jun 2024 10:50:49 +0200 Subject: [PATCH 038/270] unnecessary variables removed --- src/libs/ExportOnyxState/common.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/libs/ExportOnyxState/common.ts b/src/libs/ExportOnyxState/common.ts index b85daa27615d..09b2d955400e 100644 --- a/src/libs/ExportOnyxState/common.ts +++ b/src/libs/ExportOnyxState/common.ts @@ -1,8 +1,5 @@ import {Str} from 'expensify-common'; -const ONYX_DB_KEY = 'OnyxDB'; -const DEFAULT_FILE_NAME = 'onyx-state.txt'; - const maskFragileData = (data: Record): Record => { const maskedData: Record = {}; @@ -24,6 +21,4 @@ const maskFragileData = (data: Record): Record export default { maskFragileData, - ONYX_DB_KEY, - DEFAULT_FILE_NAME, }; From 8423971c1ec3e4555ee97c75d4487efc67ebeea2 Mon Sep 17 00:00:00 2001 From: burczu Date: Wed, 19 Jun 2024 10:51:01 +0200 Subject: [PATCH 039/270] useMemo deps array updated --- src/pages/settings/Troubleshoot/TroubleshootPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index 0a763ad75562..1253c96b8758 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -95,7 +95,7 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { wrapperStyle: [styles.sectionMenuItemTopDescription], })) .reverse(); - }, [shouldStoreLogs, translate, waitForNavigate, styles.sectionMenuItemTopDescription]); + }, [waitForNavigate, exportOnyxState, shouldStoreLogs, translate, styles.sectionMenuItemTopDescription]); return ( Date: Wed, 19 Jun 2024 10:54:00 +0200 Subject: [PATCH 040/270] additional comment added --- src/libs/ExportOnyxState/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/ExportOnyxState/index.ts b/src/libs/ExportOnyxState/index.ts index b47b3a01fb8b..148548ce5d1c 100644 --- a/src/libs/ExportOnyxState/index.ts +++ b/src/libs/ExportOnyxState/index.ts @@ -23,6 +23,7 @@ const readFromOnyxDatabase = () => queryResult[result.primaryKey as string] = result.value; result.continue(); } else { + // no results mean the cursor has reached the end of the data resolve(queryResult); } }; From d023cf52b5fa1d4b9a4afedbf95f82ee53f4dd08 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 19 Jun 2024 18:44:07 +0800 Subject: [PATCH 041/270] remove autoFocus prop --- src/pages/signin/LoginForm/BaseLoginForm.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/signin/LoginForm/BaseLoginForm.tsx b/src/pages/signin/LoginForm/BaseLoginForm.tsx index 1c8084eb12e2..49e7479c0435 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.tsx +++ b/src/pages/signin/LoginForm/BaseLoginForm.tsx @@ -267,7 +267,6 @@ function BaseLoginForm({account, credentials, closeAccount, blurOnSubmit = false onSubmitEditing={validateAndSubmitForm} autoCapitalize="none" autoCorrect={false} - autoFocus inputMode={CONST.INPUT_MODE.EMAIL} errorText={formError} hasError={shouldShowServerError} From aea756f9a9c3de08fca8b91268fe6755eb179554 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 19 Jun 2024 18:44:21 +0800 Subject: [PATCH 042/270] don't set initial focus for sign in modal --- src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts b/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts index 2a77b52e3116..ec2977ea592c 100644 --- a/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts +++ b/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts @@ -10,6 +10,7 @@ const SCREENS_WITH_AUTOFOCUS: string[] = [ SCREENS.SETTINGS.PROFILE.PRONOUNS, SCREENS.NEW_TASK.DETAILS, SCREENS.MONEY_REQUEST.CREATE, + SCREENS.SIGN_IN_ROOT, ]; export default SCREENS_WITH_AUTOFOCUS; From 8099d789bba5efe7b2533d92ac63c2240eebf7ed Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 20 Jun 2024 14:43:02 +0800 Subject: [PATCH 043/270] don't set initial focus for sign in page --- .../FocusTrap/FocusTrapForScreen/index.web.tsx | 10 +++++++--- src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts | 10 +++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx index 6a1409ab4a93..5e41e64b3115 100644 --- a/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx +++ b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx @@ -1,12 +1,14 @@ import {useFocusEffect, useIsFocused, useRoute} from '@react-navigation/native'; import FocusTrap from 'focus-trap-react'; import React, {useCallback, useMemo} from 'react'; +import {useOnyx} from 'react-native-onyx'; import BOTTOM_TAB_SCREENS from '@components/FocusTrap/BOTTOM_TAB_SCREENS'; -import SCREENS_WITH_AUTOFOCUS from '@components/FocusTrap/SCREENS_WITH_AUTOFOCUS'; +import getScreenWithAutofocus from '@components/FocusTrap/SCREENS_WITH_AUTOFOCUS'; import sharedTrapStack from '@components/FocusTrap/sharedTrapStack'; import TOP_TAB_SCREENS from '@components/FocusTrap/TOP_TAB_SCREENS'; import WIDE_LAYOUT_INACTIVE_SCREENS from '@components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import ONYXKEYS from '@src/ONYXKEYS'; import type FocusTrapProps from './FocusTrapProps'; let activeRouteName = ''; @@ -14,6 +16,8 @@ function FocusTrapForScreen({children}: FocusTrapProps) { const isFocused = useIsFocused(); const route = useRoute(); const {isSmallScreenWidth} = useWindowDimensions(); + const [isAuthenticated] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => !!session?.authToken}); + const screensWithAutofocus = useMemo(() => getScreenWithAutofocus(isAuthenticated), [isAuthenticated]); const isActive = useMemo(() => { // Focus trap can't be active on bottom tab screens because it would block access to the tab bar. @@ -49,13 +53,13 @@ function FocusTrapForScreen({children}: FocusTrapProps) { fallbackFocus: document.body, // We don't want to ovverride autofocus on these screens. initialFocus: () => { - if (SCREENS_WITH_AUTOFOCUS.includes(activeRouteName)) { + if (screensWithAutofocus.includes(activeRouteName)) { return false; } return undefined; }, setReturnFocus: (element) => { - if (SCREENS_WITH_AUTOFOCUS.includes(activeRouteName)) { + if (screensWithAutofocus.includes(activeRouteName)) { return false; } return element; diff --git a/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts b/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts index ec2977ea592c..7af327d35ac4 100644 --- a/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts +++ b/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts @@ -1,4 +1,5 @@ import {CENTRAL_PANE_WORKSPACE_SCREENS} from '@libs/Navigation/AppNavigator/Navigators/FullScreenNavigator'; +import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; const SCREENS_WITH_AUTOFOCUS: string[] = [ @@ -13,4 +14,11 @@ const SCREENS_WITH_AUTOFOCUS: string[] = [ SCREENS.SIGN_IN_ROOT, ]; -export default SCREENS_WITH_AUTOFOCUS; +function getScreenWithAutofocus(isAuthenticated: boolean) { + if (!isAuthenticated) { + return [...SCREENS_WITH_AUTOFOCUS, NAVIGATORS.BOTTOM_TAB_NAVIGATOR]; + } + return SCREENS_WITH_AUTOFOCUS; +} + +export default getScreenWithAutofocus; From e30384394140cb717c20d039f66a6defac747996 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 20 Jun 2024 12:02:03 +0200 Subject: [PATCH 044/270] Update maxDelay for doubleTapGesture for mobile browsers --- src/components/MultiGestureCanvas/useTapGestures.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/MultiGestureCanvas/useTapGestures.ts b/src/components/MultiGestureCanvas/useTapGestures.ts index f550e93d6be2..9036ae7ae39a 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.ts +++ b/src/components/MultiGestureCanvas/useTapGestures.ts @@ -3,6 +3,7 @@ import {useMemo} from 'react'; import type {TapGesture} from 'react-native-gesture-handler'; import {Gesture} from 'react-native-gesture-handler'; import {runOnJS, useWorkletCallback, withSpring} from 'react-native-reanimated'; +import * as Browser from '@libs/Browser'; import {DOUBLE_TAP_SCALE, SPRING_CONFIG} from './constants'; import type {MultiGestureCanvasVariables} from './types'; import * as MultiGestureCanvasUtils from './utils'; @@ -129,7 +130,7 @@ const useTapGestures = ({ state.fail(); }) .numberOfTaps(2) - .maxDelay(150) + .maxDelay(Browser.isMobile() ? 300 : 150) .maxDistance(20) .onEnd((evt) => { const triggerScaleChangedEvent = () => { From e07a5ce25bd7bdf63b512a3fcbe51d5dd75b8e7b Mon Sep 17 00:00:00 2001 From: burczu Date: Thu, 20 Jun 2024 13:32:56 +0200 Subject: [PATCH 045/270] filtering report actions out added --- src/libs/ExportOnyxState/common.ts | 27 ++++++++++++++---------- src/libs/ExportOnyxState/index.native.ts | 8 +++---- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/libs/ExportOnyxState/common.ts b/src/libs/ExportOnyxState/common.ts index 09b2d955400e..b152ce89e3f4 100644 --- a/src/libs/ExportOnyxState/common.ts +++ b/src/libs/ExportOnyxState/common.ts @@ -1,20 +1,25 @@ import {Str} from 'expensify-common'; +import ONYXKEYS from '@src/ONYXKEYS'; const maskFragileData = (data: Record): Record => { const maskedData: Record = {}; - for (const key in data) { - if (Object.prototype.hasOwnProperty.call(data, key)) { - const value = data[key]; - if (typeof value === 'string' && (Str.isValidEmail(value) || key === 'authToken' || key === 'encryptedAuthToken')) { - maskedData[key] = '***'; - } else if (typeof value === 'object') { - maskedData[key] = maskFragileData(value as Record); - } else { - maskedData[key] = value; - } + const keys = Object.keys(data).filter((key) => !key.includes(ONYXKEYS.COLLECTION.REPORT_ACTIONS)); + keys.forEach((key) => { + if (!Object.prototype.hasOwnProperty.call(data, key)) { + return; } - } + + const value = data[key]; + + if (typeof value === 'string' && (Str.isValidEmail(value) || key === 'authToken' || key === 'encryptedAuthToken')) { + maskedData[key] = '***'; + } else if (typeof value === 'object') { + maskedData[key] = maskFragileData(value as Record); + } else { + maskedData[key] = value; + } + }); return maskedData; }; diff --git a/src/libs/ExportOnyxState/index.native.ts b/src/libs/ExportOnyxState/index.native.ts index ff8ff9e4f730..501a219f03bc 100644 --- a/src/libs/ExportOnyxState/index.native.ts +++ b/src/libs/ExportOnyxState/index.native.ts @@ -25,11 +25,9 @@ const shareAsFile = (value: string) => { const actualInfoFile = `file://${infoFilePath}`; RNFS.writeFile(infoFilePath, value, 'utf8').then(() => { - const shareOptions = { - urls: [actualInfoFile], - }; - - Share.open(shareOptions); + Share.open({ + url: actualInfoFile, + }); }); } catch (error) { console.error('Error renaming and sharing file:', error); From ee0f65bce82043d41855d65b58e6385f2b68d15c Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:28:02 +0200 Subject: [PATCH 046/270] add trip receipt case --- src/components/EReceiptThumbnail.tsx | 12 ++++++++- .../ReportActionItemImage.tsx | 12 +++++++++ src/libs/TripReservationUtils.ts | 25 ++++++++++++++++++- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx index f4216dcc9f8a..6762538b3c91 100644 --- a/src/components/EReceiptThumbnail.tsx +++ b/src/components/EReceiptThumbnail.tsx @@ -9,6 +9,7 @@ import colors from '@styles/theme/colors'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import * as TripReservationUtils from '@libs/TripReservationUtils'; import type {Transaction} from '@src/types/onyx'; import Icon from './Icon'; import * as eReceiptBGs from './Icon/EReceiptBGs'; @@ -56,7 +57,8 @@ const backgroundImages = { function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptThumbnail = false, centerIconV = true, iconSize = 'large'}: EReceiptThumbnailProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const colorCode = isReceiptThumbnail ? StyleUtils.getFileExtensionColorCode(fileExtension) : StyleUtils.getEReceiptColorCode(transaction); + const {tripIcon, tripBGColor} = TripReservationUtils.getTripEReceiptData(transaction); + const colorCode = tripBGColor ?? (isReceiptThumbnail ? StyleUtils.getFileExtensionColorCode(fileExtension) : StyleUtils.getEReceiptColorCode(transaction)); const backgroundImage = useMemo(() => backgroundImages[colorCode], [colorCode]); @@ -141,6 +143,14 @@ function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptT fill={primaryColor} /> ) : null} + {tripIcon && isReceiptThumbnail ? ( + + ) : null} diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index 1251be83994b..b0105039bc18 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -95,6 +95,8 @@ function ReportActionItemImage({ const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail ?? ''); const isEReceipt = transaction && TransactionUtils.hasEReceipt(transaction); + const shouldUseTripEReceiptThumbnail = transaction?.receipt?.reservationList?.length !== 0; + let propsObj: ReceiptImageProps; if (isEReceipt) { @@ -110,6 +112,16 @@ function ReportActionItemImage({ }; } else if (isLocalFile && filename && Str.isPDF(filename) && typeof attachmentModalSource === 'string') { propsObj = {isPDFThumbnail: true, source: attachmentModalSource}; + } else if (shouldUseTripEReceiptThumbnail) { + propsObj = { + isThumbnail, + transactionID: transaction?.transactionID, + ...(isThumbnail && {iconSize: (isSingleImage ? 'medium' : 'small') as IconSize, fileExtension}), + shouldUseThumbnailImage: true, + isAuthTokenRequired: false, + source: thumbnail ?? image ?? '', + shouldUseInitialObjectPosition: isDistanceRequest, + }; } else { propsObj = { isThumbnail, diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index ead786b8eafd..ced50c7ec0ac 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -3,6 +3,7 @@ import CONST from '@src/CONST'; import type {Reservation, ReservationType} from '@src/types/onyx/Transaction'; import type Transaction from '@src/types/onyx/Transaction'; import type IconAsset from '@src/types/utils/IconAsset'; +import { EReceiptColorName } from '@styles/utils/types'; function getTripReservationIcon(reservationType: ReservationType): IconAsset { switch (reservationType) { @@ -24,4 +25,26 @@ function getReservationsFromTripTransactions(transactions: Transaction[]): Reser .flat(); } -export {getTripReservationIcon, getReservationsFromTripTransactions}; +type TripEReceiptData = { + /** Icon asset associated with the type of trip reservation */ + tripIcon?: IconAsset, + + /** EReceipt background color associated with the type of trip reservation */ + tripBGColor?: EReceiptColorName, +} + +function getTripEReceiptData(transaction?: Transaction): TripEReceiptData { + const reservationType = transaction ? transaction.receipt?.reservationList?.[0]?.type : ''; + + switch (reservationType) { + case CONST.RESERVATION_TYPE.FLIGHT: + case CONST.RESERVATION_TYPE.CAR: + return {tripIcon: Expensicons.Plane, tripBGColor: CONST.ERECEIPT_COLORS.PINK}; + case CONST.RESERVATION_TYPE.HOTEL: + return {tripIcon: Expensicons.Plane, tripBGColor: CONST.ERECEIPT_COLORS.YELLOW}; + default: + return {}; + } +} + +export {getTripReservationIcon, getReservationsFromTripTransactions, getTripEReceiptData}; From 4e34eb15f41366c75698a3eb0567ae508d686bcd Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:29:11 +0200 Subject: [PATCH 047/270] fix prettier --- src/components/EReceiptThumbnail.tsx | 2 +- src/libs/TripReservationUtils.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx index 6762538b3c91..aebfff1f5a00 100644 --- a/src/components/EReceiptThumbnail.tsx +++ b/src/components/EReceiptThumbnail.tsx @@ -5,11 +5,11 @@ import {withOnyx} from 'react-native-onyx'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportUtils from '@libs/ReportUtils'; +import * as TripReservationUtils from '@libs/TripReservationUtils'; import colors from '@styles/theme/colors'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import * as TripReservationUtils from '@libs/TripReservationUtils'; import type {Transaction} from '@src/types/onyx'; import Icon from './Icon'; import * as eReceiptBGs from './Icon/EReceiptBGs'; diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index ced50c7ec0ac..23a1743d65f4 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -1,9 +1,9 @@ +import {EReceiptColorName} from '@styles/utils/types'; import * as Expensicons from '@src/components/Icon/Expensicons'; import CONST from '@src/CONST'; import type {Reservation, ReservationType} from '@src/types/onyx/Transaction'; import type Transaction from '@src/types/onyx/Transaction'; import type IconAsset from '@src/types/utils/IconAsset'; -import { EReceiptColorName } from '@styles/utils/types'; function getTripReservationIcon(reservationType: ReservationType): IconAsset { switch (reservationType) { @@ -27,11 +27,11 @@ function getReservationsFromTripTransactions(transactions: Transaction[]): Reser type TripEReceiptData = { /** Icon asset associated with the type of trip reservation */ - tripIcon?: IconAsset, + tripIcon?: IconAsset; /** EReceipt background color associated with the type of trip reservation */ - tripBGColor?: EReceiptColorName, -} + tripBGColor?: EReceiptColorName; +}; function getTripEReceiptData(transaction?: Transaction): TripEReceiptData { const reservationType = transaction ? transaction.receipt?.reservationList?.[0]?.type : ''; From 44a368f7cd09575073433837bee635a0dd2cf833 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Thu, 20 Jun 2024 17:04:47 +0200 Subject: [PATCH 048/270] correct type imports and lint --- src/libs/TripReservationUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index 23a1743d65f4..140347a99ed7 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -1,4 +1,4 @@ -import {EReceiptColorName} from '@styles/utils/types'; +import type {EReceiptColorName} from '@styles/utils/types'; import * as Expensicons from '@src/components/Icon/Expensicons'; import CONST from '@src/CONST'; import type {Reservation, ReservationType} from '@src/types/onyx/Transaction'; From 85f4941b0c71952a64e352ec9fbe60a1ad1419d2 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Thu, 20 Jun 2024 17:05:53 +0200 Subject: [PATCH 049/270] set correct icon for accommodation --- src/libs/TripReservationUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index 140347a99ed7..e937979ae7b9 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -41,7 +41,7 @@ function getTripEReceiptData(transaction?: Transaction): TripEReceiptData { case CONST.RESERVATION_TYPE.CAR: return {tripIcon: Expensicons.Plane, tripBGColor: CONST.ERECEIPT_COLORS.PINK}; case CONST.RESERVATION_TYPE.HOTEL: - return {tripIcon: Expensicons.Plane, tripBGColor: CONST.ERECEIPT_COLORS.YELLOW}; + return {tripIcon: Expensicons.Bed, tripBGColor: CONST.ERECEIPT_COLORS.YELLOW}; default: return {}; } From c42fcf555926da318d6a1903a07c6a3b63e3f2f5 Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Fri, 21 Jun 2024 07:23:25 +0200 Subject: [PATCH 050/270] feat: integrate retry billing button --- src/ONYXKEYS.ts | 8 +++ src/languages/en.ts | 2 + src/languages/es.ts | 2 + src/libs/API/types.ts | 1 + .../AppNavigator/getPartialStateDiff.ts | 4 +- src/libs/actions/Subscription.ts | 53 ++++++++++++++++++- .../Subscription/CardSection/CardSection.tsx | 2 + 7 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 8a20032b4f91..ca98e9ebbe0a 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -157,6 +157,12 @@ const ONYXKEYS = { /** Store the state of the subscription */ NVP_PRIVATE_SUBSCRIPTION: 'nvp_private_subscription', + /** Store retry billing successful status */ + SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL: 'subscriptionRetryBillingStatusSuccessful', + + /** Store retry billing failed status */ + SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED: 'subscriptionRetryBillingStatusFailed', + /** Store preferred skintone for emoji */ PREFERRED_EMOJI_SKIN_TONE: 'nvp_expensify_preferredEmojiSkinTone', @@ -672,6 +678,8 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS]: OnyxTypes.DismissedReferralBanners; [ONYXKEYS.NVP_HAS_SEEN_TRACK_TRAINING]: boolean; [ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION]: OnyxTypes.PrivateSubscription; + [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL]: boolean; + [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED]: boolean; [ONYXKEYS.USER_WALLET]: OnyxTypes.UserWallet; [ONYXKEYS.WALLET_ONFIDO]: OnyxTypes.WalletOnfido; [ONYXKEYS.WALLET_ADDITIONAL_DETAILS]: OnyxTypes.WalletAdditionalDetails; diff --git a/src/languages/en.ts b/src/languages/en.ts index 99ed3265f02b..af7516af6dcc 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3234,6 +3234,8 @@ export default { changeCurrency: 'Change payment currency', cardNotFound: 'No payment card added', retryPaymentButton: 'Retry payment', + success: 'Success!', + yourCardHasBeenBilled: 'Your card has been billed successfully.', }, yourPlan: { title: 'Your plan', diff --git a/src/languages/es.ts b/src/languages/es.ts index 96346458af37..87a5e3b182dc 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3738,6 +3738,8 @@ export default { changeCurrency: 'Cambiar moneda de pago', cardNotFound: 'No se ha añadido ninguna tarjeta de pago', retryPaymentButton: 'Reintentar el pago', + success: 'Éxito!', + yourCardHasBeenBilled: 'Tu tarjeta fue facturada correctamente.', }, yourPlan: { title: 'Tu plan', diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 8f093ee827c3..280f1c58306a 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -227,6 +227,7 @@ const WRITE_COMMANDS = { UPDATE_SUBSCRIPTION_AUTO_RENEW: 'UpdateSubscriptionAutoRenew', UPDATE_SUBSCRIPTION_ADD_NEW_USERS_AUTOMATICALLY: 'UpdateSubscriptionAddNewUsersAutomatically', UPDATE_SUBSCRIPTION_SIZE: 'UpdateSubscriptionSize', + CLEAR_OUTSTANDING_BALANCE: 'ClearOutstandingBalance', } as const; type WriteCommand = ValueOf; diff --git a/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts b/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts index 5061c7500742..17a8ee158219 100644 --- a/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts +++ b/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts @@ -72,8 +72,8 @@ function getPartialStateDiff(state: State, templateState: St (!stateTopmostFullScreen && templateStateTopmostFullScreen) || (stateTopmostFullScreen && templateStateTopmostFullScreen && - stateTopmostFullScreen.name !== templateStateTopmostFullScreen.name && - !shallowCompare(stateTopmostFullScreen.params as Record | undefined, templateStateTopmostFullScreen.params as Record | undefined)) + (stateTopmostFullScreen.name !== templateStateTopmostFullScreen.name || + !shallowCompare(stateTopmostFullScreen.params as Record | undefined, templateStateTopmostFullScreen.params as Record | undefined))) ) { diff[NAVIGATORS.FULL_SCREEN_NAVIGATOR] = fullScreenDiff; } diff --git a/src/libs/actions/Subscription.ts b/src/libs/actions/Subscription.ts index 19a3bf0c547e..46d71e9f3b81 100644 --- a/src/libs/actions/Subscription.ts +++ b/src/libs/actions/Subscription.ts @@ -231,4 +231,55 @@ function clearUpdateSubscriptionSizeError() { }); } -export {openSubscriptionPage, updateSubscriptionAutoRenew, updateSubscriptionAddNewUsersAutomatically, updateSubscriptionSize, clearUpdateSubscriptionSizeError, updateSubscriptionType}; +function clearOutstandingBalance() { + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, + value: true, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED, + value: false, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, + value: true, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED, + value: false, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, + value: false, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, + value: true, + }, + ], + }; + + API.write(WRITE_COMMANDS.CLEAR_OUTSTANDING_BALANCE, null, onyxData); +} + +export { + openSubscriptionPage, + updateSubscriptionAutoRenew, + updateSubscriptionAddNewUsersAutomatically, + updateSubscriptionSize, + clearUpdateSubscriptionSizeError, + updateSubscriptionType, + clearOutstandingBalance, +}; diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 7f80b189c517..3103b85363d5 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -20,6 +20,8 @@ function CardSection() { const styles = useThemeStyles(); const theme = useTheme(); const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); + const [retryBillingStatusSuccessful] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, {initWithStoredValues: false}); + const [retryBillingStatusFailed] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED); const defaultCard = useMemo(() => Object.values(fundList ?? {}).find((card) => card.isDefault), [fundList]); From 68fcbcd5fdc4d31e45b6da66c829dfed50cb5b0a Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Fri, 21 Jun 2024 13:42:08 +0200 Subject: [PATCH 051/270] before rebase --- src/ONYXKEYS.ts | 6 ----- src/languages/en.ts | 2 -- src/languages/es.ts | 2 -- src/libs/SubscriptionUtils.ts | 1 + src/libs/actions/Subscription.ts | 17 +++++--------- .../CardSection/BillingBanner.tsx | 20 +++++++++++++++++ .../Subscription/CardSection/CardSection.tsx | 22 +++++++++++++++---- .../Subscription/CardSection/utils.ts | 7 +++--- 8 files changed, 48 insertions(+), 29 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 0eff19d471ef..da6f58cc705c 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -169,12 +169,6 @@ const ONYXKEYS = { /** Store the billing status */ NVP_PRIVATE_BILLING_STATUS: 'nvp_private_billingStatus', - /** Store retry billing successful status */ - SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL: 'subscriptionRetryBillingStatusSuccessful', - - /** Store retry billing failed status */ - SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED: 'subscriptionRetryBillingStatusFailed', - /** Store preferred skintone for emoji */ PREFERRED_EMOJI_SKIN_TONE: 'nvp_expensify_preferredEmojiSkinTone', diff --git a/src/languages/en.ts b/src/languages/en.ts index edf83a1725f7..ad1509f1ac85 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3248,8 +3248,6 @@ export default { changeCurrency: 'Change payment currency', cardNotFound: 'No payment card added', retryPaymentButton: 'Retry payment', - success: 'Success!', - yourCardHasBeenBilled: 'Your card has been billed successfully.', }, yourPlan: { title: 'Your plan', diff --git a/src/languages/es.ts b/src/languages/es.ts index ca00913d573f..eb828882e192 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3753,8 +3753,6 @@ export default { changeCurrency: 'Cambiar moneda de pago', cardNotFound: 'No se ha añadido ninguna tarjeta de pago', retryPaymentButton: 'Reintentar el pago', - success: 'Éxito!', - yourCardHasBeenBilled: 'Tu tarjeta fue facturada correctamente.', }, yourPlan: { title: 'Tu plan', diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 988c83354efb..c924332dca7b 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -108,6 +108,7 @@ Onyx.connect({ let billingStatusSuccessful: OnyxValues[typeof ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL]; Onyx.connect({ key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, + initWithStoredValues: false, callback: (value) => { if (value === undefined) { return; diff --git a/src/libs/actions/Subscription.ts b/src/libs/actions/Subscription.ts index 46d71e9f3b81..558e4c0284a8 100644 --- a/src/libs/actions/Subscription.ts +++ b/src/libs/actions/Subscription.ts @@ -233,18 +233,6 @@ function clearUpdateSubscriptionSizeError() { function clearOutstandingBalance() { const onyxData: OnyxData = { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, - value: true, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED, - value: false, - }, - ], successData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -274,6 +262,10 @@ function clearOutstandingBalance() { API.write(WRITE_COMMANDS.CLEAR_OUTSTANDING_BALANCE, null, onyxData); } +function resetRetryBillingStatus() { + Onyx.merge(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, false); +} + export { openSubscriptionPage, updateSubscriptionAutoRenew, @@ -282,4 +274,5 @@ export { clearUpdateSubscriptionSizeError, updateSubscriptionType, clearOutstandingBalance, + resetRetryBillingStatus, }; diff --git a/src/pages/settings/Subscription/CardSection/BillingBanner.tsx b/src/pages/settings/Subscription/CardSection/BillingBanner.tsx index b3e4d8859249..8ea459b12aad 100644 --- a/src/pages/settings/Subscription/CardSection/BillingBanner.tsx +++ b/src/pages/settings/Subscription/CardSection/BillingBanner.tsx @@ -3,10 +3,14 @@ import {View} from 'react-native'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; +import {PressableWithoutFeedback} from '@components/Pressable'; import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; +import * as Subscription from '@userActions/Subscription'; +import CONST from '@src/CONST'; type BillingBannerProps = { title?: string; @@ -20,6 +24,7 @@ type BillingBannerProps = { function BillingBanner({title, subtitle, isError, shouldShowRedDotIndicator, shouldShowGreenDotIndicator, isTrialActive}: BillingBannerProps) { const styles = useThemeStyles(); const theme = useTheme(); + const {translate} = useLocalize(); const backgroundStyle = isTrialActive ? styles.trialBannerBackgroundColor : styles.hoveredComponentBG; @@ -48,6 +53,21 @@ function BillingBanner({title, subtitle, isError, shouldShowRedDotIndicator, sho fill={theme.success} /> )} + {!isError && ( + { + Subscription.resetRetryBillingStatus(); + }} + style={[styles.touchableButtonImage]} + role={CONST.ROLE.BUTTON} + accessibilityLabel={translate('common.close')} + > + + + )} ); } diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 5817e59ad60d..ad17ccddf369 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -1,13 +1,16 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; +import Button from '@components/Button'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import Section from '@components/Section'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; +import * as Subscription from '@userActions/Subscription'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import BillingBanner from './BillingBanner'; import CardSectionActions from './CardSectionActions'; @@ -18,14 +21,13 @@ function CardSection() { const {translate, preferredLocale} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); - const [retryBillingStatusSuccessful] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, {initWithStoredValues: false}); - const [retryBillingStatusFailed] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED); + const {isOffline} = useNetwork(); const defaultCard = CardSectionUtils.getCardForSubscriptionBilling(); const cardMonth = useMemo(() => DateUtils.getMonthNames(preferredLocale)[(defaultCard?.accountData?.cardMonth ?? 1) - 1], [defaultCard?.accountData?.cardMonth, preferredLocale]); - const {title, subtitle, isError, shouldShowRedDotIndicator, shouldShowGreenDotIndicator} = CardSectionUtils.getBillingStatus( + const {title, subtitle, isError, shouldShowRedDotIndicator, shouldShowGreenDotIndicator, isRetryAvailable} = CardSectionUtils.getBillingStatus( translate, preferredLocale, defaultCard?.accountData?.cardNumber ?? '', @@ -76,7 +78,19 @@ function CardSection() { )} - {isEmptyObject(defaultCard?.accountData) && } + {!isEmptyObject(defaultCard?.accountData) && } + {!isRetryAvailable && ( +