From 9bc2e6537f76807063e06741913dfc6c7d5ccd31 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Sat, 18 May 2024 17:31:44 +0100 Subject: [PATCH 001/183] simplify navigateToMostRecentReport --- src/libs/ReportUtils.ts | 34 ++++++++++++++----- src/libs/actions/Report.ts | 57 +++++++++----------------------- src/pages/NewChatConfirmPage.tsx | 2 +- 3 files changed, 42 insertions(+), 51 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b8e4c448fdc2..b46537e81142 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1079,7 +1079,7 @@ function doesReportBelongToWorkspace(report: OnyxEntry, policyMemberAcco /** * Given an array of reports, return them filtered by a policyID and policyMemberAccountIDs. */ -function filterReportsByPolicyIDAndMemberAccountIDs(reports: Report[], policyMemberAccountIDs: number[] = [], policyID?: string) { +function filterReportsByPolicyIDAndMemberAccountIDs(reports: Array>, policyMemberAccountIDs: number[] = [], policyID?: string) { return reports.filter((report) => !!report && doesReportBelongToWorkspace(report, policyMemberAccountIDs, policyID)); } @@ -1165,6 +1165,7 @@ function findLastAccessedReport( reportMetadata: OnyxCollection = {}, policyID?: string, policyMemberAccountIDs: number[] = [], + excludeReportID?: string, ): OnyxEntry { // If it's the user's first time using New Expensify, then they could either have: // - just a Concierge report, if so we'll return that @@ -1172,7 +1173,7 @@ function findLastAccessedReport( // If it's the latter, we'll use the deeplinked report over the Concierge report, // since the Concierge report would be incorrectly selected over the deep-linked report in the logic below. - let reportsValues = Object.values(reports ?? {}) as Report[]; + let reportsValues = Object.values(reports ?? {}); if (!!policyID || policyMemberAccountIDs.length > 0) { reportsValues = filterReportsByPolicyIDAndMemberAccountIDs(reportsValues, policyMemberAccountIDs, policyID); @@ -1188,13 +1189,28 @@ function findLastAccessedReport( }); } - if (ignoreDomainRooms) { - // We allow public announce rooms, admins, and announce rooms through since we bypass the default rooms beta for them. - // Check where ReportUtils.findLastAccessedReport is called in MainDrawerNavigator.js for more context. - // Domain rooms are now the only type of default room that are on the defaultRooms beta. - sortedReports = sortedReports.filter( - (report) => !isDomainRoom(report) || getPolicyType(report, policies) === CONST.POLICY.TYPE.FREE || hasExpensifyGuidesEmails(Object.keys(report?.participants ?? {}).map(Number)), - ); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const shouldFilter = excludeReportID || ignoreDomainRooms; + if (shouldFilter) { + sortedReports = sortedReports.filter((report) => { + if (excludeReportID && report?.reportID === excludeReportID) { + return false; + } + + // We allow public announce rooms, admins, and announce rooms through since we bypass the default rooms beta for them. + // Check where ReportUtils.findLastAccessedReport is called in MainDrawerNavigator.js for more context. + // Domain rooms are now the only type of default room that are on the defaultRooms beta. + if ( + ignoreDomainRooms && + isDomainRoom(report) && + getPolicyType(report, policies) !== CONST.POLICY.TYPE.FREE && + !hasExpensifyGuidesEmails(Object.keys(report?.participants ?? {}).map(Number)) + ) { + return false; + } + + return true; + }); } if (isFirstTimeNewExpensifyUser) { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 91a0ce5da930..6f6f8cfe1944 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -946,6 +946,7 @@ function openReport( function navigateToAndOpenReport( userLogins: string[], shouldDismissModal = true, + actionType?: string, reportName?: string, avatarUri?: string, avatarFile?: File | CustomRNImageManipulatorResult | undefined, @@ -977,7 +978,7 @@ function navigateToAndOpenReport( Navigation.dismissModalWithReport(report); } else { Navigation.navigateWithSwitchPolicyID({route: ROUTES.HOME}); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID), actionType); } } @@ -1972,7 +1973,7 @@ function updateWriteCapabilityAndNavigate(report: Report, newValue: WriteCapabil /** * Navigates to the 1:1 report with Concierge */ -function navigateToConciergeChat(shouldDismissModal = false, checkIfCurrentPageActive = () => true) { +function navigateToConciergeChat(shouldDismissModal = false, checkIfCurrentPageActive = () => true, actionType?: string) { // If conciergeChatReportID contains a concierge report ID, we navigate to the concierge chat using the stored report ID. // Otherwise, we would find the concierge chat and navigate to it. if (!conciergeChatReportID) { @@ -1983,12 +1984,12 @@ function navigateToConciergeChat(shouldDismissModal = false, checkIfCurrentPageA if (!checkIfCurrentPageActive()) { return; } - navigateToAndOpenReport([CONST.EMAIL.CONCIERGE], shouldDismissModal); + navigateToAndOpenReport([CONST.EMAIL.CONCIERGE], shouldDismissModal, actionType); }); } else if (shouldDismissModal) { Navigation.dismissModal(conciergeChatReportID); } else { - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(conciergeChatReportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(conciergeChatReportID), actionType); } } @@ -2507,46 +2508,20 @@ function getCurrentUserAccountID(): number { } function navigateToMostRecentReport(currentReport: OnyxEntry) { - const reportID = currentReport?.reportID; - const sortedReportsByLastRead = ReportUtils.sortReportsByLastRead(Object.values(allReports ?? {}) as Report[], reportMetadata); - - // We want to filter out the current report, hidden reports and empty chats - const filteredReportsByLastRead = sortedReportsByLastRead.filter( - (sortedReport) => - sortedReport?.reportID !== reportID && - sortedReport?.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN && - ReportUtils.shouldReportBeInOptionList({ - report: sortedReport, - currentReportId: '', - isInFocusMode: false, - betas: [], - policies: {}, - excludeEmptyChats: true, - doesReportHaveViolations: false, - includeSelfDM: true, - }), - ); - const lastAccessedReportID = filteredReportsByLastRead.at(-1)?.reportID; + const lastAccessedReportID = ReportUtils.findLastAccessedReport(allReports, false, null, false, false, reportMetadata, undefined, [], currentReport?.reportID)?.reportID; + const isChatThread = ReportUtils.isChatThread(currentReport); + // We are using index 1 here because on index 0, we'll always have the bottomTabNavigator. + const isFirstRoute = navigationRef?.current?.getState()?.index === 1; + // If it is not a chat thread we should call Navigation.goBack to pop the current route first before navigating to last accessed report. + if (!isChatThread && !isFirstRoute) { + Navigation.goBack(); + } + if (lastAccessedReportID) { - const lastAccessedReportRoute = ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID ?? ''); - // We are using index 1 here because on index 0, we'll always have the bottomTabNavigator. - const isFirstRoute = navigationRef?.current?.getState()?.index === 1; - // If it is not a chat thread we should call Navigation.goBack to pop the current route first before navigating to last accessed report. - if (!isChatThread && !isFirstRoute) { - Navigation.goBack(); - } - Navigation.navigate(lastAccessedReportRoute, CONST.NAVIGATION.TYPE.FORCED_UP); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID), CONST.NAVIGATION.TYPE.FORCED_UP); } else { - const participantAccountIDs = PersonalDetailsUtils.getAccountIDsByLogins([CONST.EMAIL.CONCIERGE]); - const chat = ReportUtils.getChatByParticipants([...participantAccountIDs, currentUserAccountID]); - if (chat?.reportID) { - // If it is not a chat thread we should call Navigation.goBack to pop the current route first before navigating to Concierge. - if (!isChatThread) { - Navigation.goBack(); - } - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(chat?.reportID), CONST.NAVIGATION.TYPE.FORCED_UP); - } + navigateToConciergeChat(false, () => true, CONST.NAVIGATION.TYPE.FORCED_UP); } } diff --git a/src/pages/NewChatConfirmPage.tsx b/src/pages/NewChatConfirmPage.tsx index 82f67eb40c86..0b2cdf3f5929 100644 --- a/src/pages/NewChatConfirmPage.tsx +++ b/src/pages/NewChatConfirmPage.tsx @@ -93,7 +93,7 @@ function NewChatConfirmPage({newGroupDraft, allPersonalDetails}: NewChatConfirmP } const logins: string[] = (newGroupDraft.participants ?? []).map((participant) => participant.login); - Report.navigateToAndOpenReport(logins, true, newGroupDraft.reportName ?? '', newGroupDraft.avatarUri ?? '', fileRef.current, optimisticReportID.current, true); + Report.navigateToAndOpenReport(logins, true, undefined, newGroupDraft.reportName ?? '', newGroupDraft.avatarUri ?? '', fileRef.current, optimisticReportID.current, true); }; const navigateBack = () => { From b760b53b71efe1cb858477dd447cf2b0851b3fdb Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 30 May 2024 18:20:52 +0700 Subject: [PATCH 002/183] Fix wrong amout is displayed when updating waypoint in offline --- src/libs/actions/IOU.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 08ce01263a83..ba18fa9d12a0 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -2390,7 +2390,7 @@ function calculateAmountForUpdatedWaypoint( ) { let updatedAmount: number = CONST.IOU.DEFAULT_AMOUNT; let updatedMerchant = Localize.translateLocal('iou.fieldPending'); - if (!isEmptyObject(transactionChanges?.routes)) { + if (!isEmptyObject(transactionChanges?.routes?.route0?.geometry)) { const customUnitRateID = TransactionUtils.getRateID(transaction) ?? ''; const mileageRates = DistanceRequestUtils.getMileageRates(policy); const policyCurrency = policy?.outputCurrency ?? PolicyUtils.getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD; From d1ddb5baffbba96ab39dce69978f22b4ed302bd2 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 10 Jun 2024 18:00:35 -0700 Subject: [PATCH 003/183] Fix last synced message --- src/languages/en.ts | 2 +- src/pages/workspace/accounting/PolicyAccountingPage.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 40c21a7fbfd0..c10a439be2f1 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2393,7 +2393,7 @@ export default { qbo: 'Quickbooks Online', xero: 'Xero', setup: 'Connect', - lastSync: 'Last synced just now', + lastSync: (relativeDate: string) => `Last synced ${relativeDate}`, import: 'Import', export: 'Export', advanced: 'Advanced', diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index bd7aa5cb4c1a..8faef761e377 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -189,7 +189,9 @@ function PolicyAccountingPage({policy, connectionSyncProgress}: PolicyAccounting errorText: shouldShowSynchronizationError ? translate('workspace.accounting.syncError', connectedIntegration) : undefined, errorTextStyle: [styles.mt5], shouldShowRedDotIndicator: true, - description: isSyncInProgress ? translate('workspace.accounting.connections.syncStageName', connectionSyncProgress.stageInProgress) : datetimeToRelative, + description: isSyncInProgress + ? translate('workspace.accounting.connections.syncStageName', connectionSyncProgress.stageInProgress) + : translate('workspace.accounting.lastSync', datetimeToRelative), rightComponent: isSyncInProgress ? ( Date: Fri, 14 Jun 2024 00:11:38 +0100 Subject: [PATCH 004/183] Fix: All categories disappear instead of crossed out when deleted offline --- .../workspace/categories/WorkspaceCategoriesPage.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 1239a9937fd3..cbba5c4f13ce 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -256,7 +256,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { const isLoading = !isOffline && policyCategories === null; - const shouldShowEmptyState = !categoryList.some((category) => category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) && !isLoading; + const hasVisibleCategories = categoryList.some((category) => category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline); const getHeaderText = () => ( @@ -309,7 +309,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { danger /> {shouldUseNarrowLayout && {getHeaderButtons()}} - {(!shouldUseNarrowLayout || shouldShowEmptyState || isLoading) && getHeaderText()} + {(!shouldUseNarrowLayout || (!hasVisibleCategories && !isLoading) || isLoading) && getHeaderText()} {isLoading && ( )} - {shouldShowEmptyState && ( + {!hasVisibleCategories && !isLoading && ( )} - {!shouldShowEmptyState && !isLoading && ( + {hasVisibleCategories && !isLoading && ( Date: Fri, 14 Jun 2024 23:25:27 +0200 Subject: [PATCH 005/183] implement subscription utils, add banner show logic --- src/ONYXKEYS.ts | 30 ++ src/components/Indicator.tsx | 4 +- src/languages/en.ts | 21 ++ src/libs/SubscriptionUtils.ts | 259 ++++++++++++++++++ .../CardSection/BillingBanner.tsx | 9 +- .../Subscription/CardSection/CardSection.tsx | 24 +- .../Subscription/CardSection/utils.ts | 143 ++++++++++ 7 files changed, 486 insertions(+), 4 deletions(-) create mode 100644 src/libs/SubscriptionUtils.ts create mode 100644 src/pages/settings/Subscription/CardSection/utils.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 0d22d3714fe6..5fdb3be1110b 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -157,6 +157,21 @@ const ONYXKEYS = { /** Store the state of the subscription */ NVP_PRIVATE_SUBSCRIPTION: 'nvp_private_subscription', + /** Store the amount of owed money */ + NVP_PRIVATE_AMOUNT_OWED: 'nvp_private_amountOwed', + + /** Store the stripe id status */ + NVP_PRIVATE_STRIPE_CUSTOMER_ID: 'nvp_private_stripeCustomerID', + + /** Store the billing dispute status */ + NVP_PRIVATE_BILLING_DISPUTE_PENDING: 'nvp_private_billingDisputePending', + + /** Store the billing status */ + NVP_PRIVATE_BILLING_STATUS: 'nvp_private_billingStatus', + + /** Store the grace period status */ + NVP_PRIVATE_BILLING_GRACE_PERIOD_END: 'nvp_private_billingGracePeriodEnd', + /** Store preferred skintone for emoji */ PREFERRED_EMOJI_SKIN_TONE: 'nvp_expensify_preferredEmojiSkinTone', @@ -652,6 +667,21 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS]: OnyxTypes.DismissedReferralBanners; [ONYXKEYS.NVP_HAS_SEEN_TRACK_TRAINING]: boolean; [ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION]: OnyxTypes.PrivateSubscription; + [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: number; + [ONYXKEYS.NVP_PRIVATE_STRIPE_CUSTOMER_ID]: { + paymentMethodID: string; + intentsID: string; + currency: string; + status: 'authentication_required' | 'intent_required' | 'succeeded'; + }; + [ONYXKEYS.NVP_PRIVATE_BILLING_DISPUTE_PENDING]: number; + [ONYXKEYS.NVP_PRIVATE_BILLING_STATUS]: { + action: string; + periodMonth: string; + periodYear: string; + declineReason: 'insufficient_funds' | 'expired_card'; + }; + [ONYXKEYS.NVP_PRIVATE_BILLING_GRACE_PERIOD_END]: string; [ONYXKEYS.USER_WALLET]: OnyxTypes.UserWallet; [ONYXKEYS.WALLET_ONFIDO]: OnyxTypes.WalletOnfido; [ONYXKEYS.WALLET_ADDITIONAL_DETAILS]: OnyxTypes.WalletAdditionalDetails; diff --git a/src/components/Indicator.tsx b/src/components/Indicator.tsx index 8830681bc55f..d1a85f9d558c 100644 --- a/src/components/Indicator.tsx +++ b/src/components/Indicator.tsx @@ -5,6 +5,7 @@ import {withOnyx} from 'react-native-onyx'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as PolicyUtils from '@libs/PolicyUtils'; +import SubscriptionUtils from '@libs/SubscriptionUtils'; import * as UserUtils from '@libs/UserUtils'; import * as PaymentMethods from '@userActions/PaymentMethods'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -54,6 +55,7 @@ function Indicator({reimbursementAccount, policies, bankAccountList, fundList, u () => Object.values(cleanPolicies).some(PolicyUtils.hasPolicyError), () => Object.values(cleanPolicies).some(PolicyUtils.hasCustomUnitsError), () => Object.values(cleanPolicies).some(PolicyUtils.hasEmployeeListError), + () => SubscriptionUtils.hasSubscriptionRedDotError(), () => Object.keys(reimbursementAccount?.errors ?? {}).length > 0, () => !!loginList && UserUtils.hasLoginListError(loginList), @@ -62,7 +64,7 @@ function Indicator({reimbursementAccount, policies, bankAccountList, fundList, u ]; const infoCheckingMethods: CheckingMethod[] = [() => !!loginList && UserUtils.hasLoginListInfo(loginList)]; const shouldShowErrorIndicator = errorCheckingMethods.some((errorCheckingMethod) => errorCheckingMethod()); - const shouldShowInfoIndicator = !shouldShowErrorIndicator && infoCheckingMethods.some((infoCheckingMethod) => infoCheckingMethod()); + const shouldShowInfoIndicator = !shouldShowErrorIndicator && (infoCheckingMethods.some((infoCheckingMethod) => infoCheckingMethod()) || SubscriptionUtils.hasSubscriptionGreenDotInfo()); const indicatorColor = shouldShowErrorIndicator ? theme.danger : theme.success; const indicatorStyles = [styles.alignItemsCenter, styles.justifyContentCenter, styles.statusIndicator(indicatorColor)]; diff --git a/src/languages/en.ts b/src/languages/en.ts index 5e58e4fdde9a..f606b605272f 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3211,6 +3211,27 @@ export default { }, subscription: { mobileReducedFunctionalityMessage: 'You can’t make changes to your subscription in the mobile app.', + billingBanner: { + outdatedInfo: 'Your payment info is outdated', + updatePaymentInformation: 'Please update your payment information.', + updateCardDataByDate: ({date}) => `Update your payment card by ${date} to continue using all of your favorite features.`, + paymentPastDuePayByDate: ({date}) => `Your payment is past due. Please pay your invoice by ${date} to avoid service interruption`, + paymentPastDue: 'Your payment is past due. Please pay your invoice.', + cardCouldNotBeCharged: 'Your card couldn’t be charged', + retryMessage: 'Before retrying, please call your bank directly to authorize Expensify charges and remove any holds. Otherwise, try adding a different payment card.', + cardNotFullyAuthenticated: ({cardEnding}) => + `Your payment card hasn’t been fully authenticated. Please complete the authentication process to activate your payment card ending in ${cardEnding}.`, + cardDeclinedDueToInsufficientFunds: ({amountOwed}) => + `Your payment card was declined due to insufficient funds. Please retry or add a new payment card to clear your ${amountOwed} outstanding balance.`, + cardExpired: ({amountOwed}) => `Your payment card expired. Please add a new payment card to clear your ${amountOwed} outstanding balance.`, + cardExpiringSoon: 'Your card is expiring soon', + cardWillExpireAtTheEndOfMonth: + 'Your payment card will expire at the end of this month. Click the three-dot menu below to update it and continue using all your favorite features.', + cardOnDispute: ({amountOwed, cardEnding}) => + `You disputed the ${amountOwed} charge on the card ending in ${cardEnding}. Your account will be locked until the dispute is resolved with your bank.`, + succeeded: 'Success!', + billedSuccessfully: 'Your card has been billed successfully.', + }, cardSection: { title: 'Payment', subtitle: 'Add a payment card to pay for your Expensify subscription.', diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts new file mode 100644 index 000000000000..a8679f002399 --- /dev/null +++ b/src/libs/SubscriptionUtils.ts @@ -0,0 +1,259 @@ +import Onyx from 'react-native-onyx'; +import type {OnyxValues} from '@src/ONYXKEYS'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Fund} from '@src/types/onyx'; + +const PAYMENT_STATUSES = { + POLICY_OWNER_WITH_AMOUNT_OWED: 'policy_owner_with_amount_owed', + POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE: 'policy_owner_with_amount_owed_overdue', + OWNER_OF_POLICY_UNDER_INVOICING: 'owner_of_policy_under_invoicing', + OWNER_OF_POLICY_UNDER_INVOICING_OVERDUE: 'owner_of_policy_under_invoicing_overdue', + BILLING_DISPUTE_PENDING: 'billing_dispute_pending', + CARD_AUTHENTICATION_REQUIRED: 'authentication_required', + INSUFFICIENT_FUNDS: 'insufficient_funds', + CARD_EXPIRED: 'expired_card', + CARD_EXPIRE_SOON: 'card_expire_soon', + RETRY_BILLING_SUCCESS: 'retry_billing_success', + RETRY_BILLING_ERROR: 'retry_billing_error', + GENERIC_API_ERROR: 'generic_api_error', +}; + +let amountOwed: OnyxValues[typeof ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]; +Onyx.connect({ + key: ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED, + callback: (value) => { + if (!value) { + return; + } + + amountOwed = value; + }, +}); + +let stripeCustomerId: OnyxValues[typeof ONYXKEYS.NVP_PRIVATE_STRIPE_CUSTOMER_ID]; +Onyx.connect({ + key: ONYXKEYS.NVP_PRIVATE_STRIPE_CUSTOMER_ID, + callback: (value) => { + if (!value) { + return; + } + + stripeCustomerId = value; + }, +}); + +let billingDisputePending: OnyxValues[typeof ONYXKEYS.NVP_PRIVATE_BILLING_DISPUTE_PENDING]; +Onyx.connect({ + key: ONYXKEYS.NVP_PRIVATE_BILLING_DISPUTE_PENDING, + callback: (value) => { + if (!value) { + return; + } + + billingDisputePending = value; + }, +}); + +let billingStatus: OnyxValues[typeof ONYXKEYS.NVP_PRIVATE_BILLING_STATUS]; +Onyx.connect({ + key: ONYXKEYS.NVP_PRIVATE_BILLING_STATUS, + callback: (value) => { + if (!value) { + return; + } + + billingStatus = value; + }, +}); + +let billingGracePeriod: OnyxValues[typeof ONYXKEYS.NVP_PRIVATE_BILLING_GRACE_PERIOD_END]; +Onyx.connect({ + key: ONYXKEYS.NVP_PRIVATE_BILLING_GRACE_PERIOD_END, + callback: (value) => { + if (!value) { + return; + } + + billingGracePeriod = value; + }, +}); + +let fundList: OnyxValues[typeof ONYXKEYS.FUND_LIST]; +Onyx.connect({ + key: ONYXKEYS.FUND_LIST, + callback: (value) => { + if (!value) { + return; + } + + fundList = value; + }, +}); + +function getOverdueGracePeriodDate(): string { + return billingGracePeriod ?? ''; +} + +function hasOverdueGracePeriod(): boolean { + return !!billingGracePeriod ?? false; +} + +function hasGracePeriodOverdue(): boolean { + return !!billingGracePeriod && Date.now() < new Date(billingGracePeriod).getTime(); +} + +/// getGracePeriodEndDate - return formatted date + +function getAmountOwed(): number { + return amountOwed ?? 0; +} + +function hasCardAuthenticatedError() { + return stripeCustomerId?.status === 'authentication_required' && amountOwed === 0; +} + +function hasBillingDisputePending() { + return !!billingDisputePending ?? false; +} + +function hasCardExpiredError() { + return billingStatus?.declineReason === 'expired_card' && amountOwed !== 0; +} + +function hasInsufficientFundsError() { + return billingStatus?.declineReason === 'insufficient_funds' && amountOwed !== 0; +} + +function getCardForSubscriptionBilling(): Fund | undefined { + return Object.values(fundList ?? {}).find((card) => card?.isDefault); +} + +function hasCardExpiringSoon(): boolean { + const card = getCardForSubscriptionBilling(); + + if (!card) { + return false; + } + + return !billingStatus && card?.accountData?.cardMonth === new Date().getMonth() + 1; +} + +// hasRetryBillingError - based on request response + +// hasCardExpiredSoon - based on card + +type SubscriptionStatus = { + status?: string; + isError?: boolean; + shouldShowRedDotIndicator?: boolean; + shouldShowGreenDotIndicator?: boolean; +}; + +function getSubscriptionStatus(): SubscriptionStatus { + if (hasOverdueGracePeriod()) { + if (amountOwed) { + // 1. Policy owner with amount owed, within grace period + if (hasGracePeriodOverdue() === false) { + return { + status: PAYMENT_STATUSES.POLICY_OWNER_WITH_AMOUNT_OWED, + isError: true, + shouldShowRedDotIndicator: true, + }; + } + + // 2. Policy owner with amount owed, overdue (past grace period) + if (hasGracePeriodOverdue()) { + return { + status: PAYMENT_STATUSES.POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE, + shouldShowRedDotIndicator: true, + }; + } + } else { + // 3. Owner of policy under invoicing, within grace period + if (hasGracePeriodOverdue() && !amountOwed) { + return { + status: PAYMENT_STATUSES.OWNER_OF_POLICY_UNDER_INVOICING, + + shouldShowRedDotIndicator: true, + }; + } + + // 4. Owner of policy under invoicing, overdue (past grace period) + if (hasGracePeriodOverdue() === false && amountOwed) { + return { + status: PAYMENT_STATUSES.OWNER_OF_POLICY_UNDER_INVOICING_OVERDUE, + shouldShowRedDotIndicator: true, + }; + } + } + } + + // 5. Billing disputed by cardholder + if (hasBillingDisputePending()) { + return { + status: PAYMENT_STATUSES.BILLING_DISPUTE_PENDING, + shouldShowRedDotIndicator: true, + }; + } + + // 6. Card not authenticated + if (hasCardAuthenticatedError()) { + return { + status: PAYMENT_STATUSES.CARD_AUTHENTICATION_REQUIRED, + shouldShowRedDotIndicator: true, + }; + } + + // 7. Insufficient funds + if (hasInsufficientFundsError()) { + return { + status: PAYMENT_STATUSES.INSUFFICIENT_FUNDS, + shouldShowRedDotIndicator: true, + }; + } + + // 8. Card expired + if (hasCardExpiredError()) { + return { + status: PAYMENT_STATUSES.CARD_EXPIRED, + shouldShowRedDotIndicator: true, + }; + } + + // 9. Card due to expire soon + if (hasCardExpiringSoon()) { + return { + status: PAYMENT_STATUSES.CARD_EXPIRE_SOON, + shouldShowGreenDotIndicator: true, + }; + } + + // 10. Retry billing success + if (false) { + return { + status: PAYMENT_STATUSES.GENERIC_API_ERROR, + isError: true, + shouldShowRedDotIndicator: true, + }; + } + + return {}; +} + +function hasSubscriptionRedDotError(): boolean { + return getSubscriptionStatus()?.shouldShowRedDotIndicator ?? false; +} + +function hasSubscriptionGreenDotInfo(): boolean { + return getSubscriptionStatus()?.shouldShowRedDotIndicator ?? false; +} + +export default { + getSubscriptionStatus, + hasSubscriptionRedDotError, + getAmountOwed, + getOverdueGracePeriodDate, + getCardForSubscriptionBilling, + hasSubscriptionGreenDotInfo, + PAYMENT_STATUSES, +}; diff --git a/src/pages/settings/Subscription/CardSection/BillingBanner.tsx b/src/pages/settings/Subscription/CardSection/BillingBanner.tsx index 163c43aa1359..b3e4d8859249 100644 --- a/src/pages/settings/Subscription/CardSection/BillingBanner.tsx +++ b/src/pages/settings/Subscription/CardSection/BillingBanner.tsx @@ -13,10 +13,11 @@ type BillingBannerProps = { subtitle?: string; isError?: boolean; shouldShowRedDotIndicator?: boolean; + shouldShowGreenDotIndicator?: boolean; isTrialActive?: boolean; }; -function BillingBanner({title, subtitle, isError, shouldShowRedDotIndicator, isTrialActive}: BillingBannerProps) { +function BillingBanner({title, subtitle, isError, shouldShowRedDotIndicator, shouldShowGreenDotIndicator, isTrialActive}: BillingBannerProps) { const styles = useThemeStyles(); const theme = useTheme(); @@ -41,6 +42,12 @@ function BillingBanner({title, subtitle, isError, shouldShowRedDotIndicator, isT fill={theme.danger} /> )} + {!isError && shouldShowGreenDotIndicator && ( + + )} ); } diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index da65c9d74f07..cd64153455d1 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -11,19 +11,28 @@ import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import BillingBanner from './BillingBanner'; import CardSectionActions from './CardSectionActions'; import CardSectionDataEmpty from './CardSectionDataEmpty'; +import CardSectionUtils from './utils'; function CardSection() { const {translate, preferredLocale} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); - const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); - const defaultCard = useMemo(() => Object.values(fundList ?? {}).find((card) => card.isDefault), [fundList]); + 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( + translate, + preferredLocale, + defaultCard?.accountData?.cardNumber ?? '', + ); + + const shouldShowBanner = !!title || !!subtitle; + return (
+ ) + } > {!isEmptyObject(defaultCard?.accountData) && ( diff --git a/src/pages/settings/Subscription/CardSection/utils.ts b/src/pages/settings/Subscription/CardSection/utils.ts new file mode 100644 index 000000000000..b4f12b567665 --- /dev/null +++ b/src/pages/settings/Subscription/CardSection/utils.ts @@ -0,0 +1,143 @@ +import {format} from 'date-fns'; +import type {Phrase, PhraseParameters} from '@libs/Localize'; +import SubscriptionUtils from '@libs/SubscriptionUtils'; +import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; +import type {Fund} from '@src/types/onyx'; + +type BillingStatusResult = { + title?: string; + subtitle?: string; + isError?: boolean; + shouldShowRedDotIndicator?: boolean; + shouldShowGreenDotIndicator?: boolean; + isTrialActive?: boolean; + isRetryAvailable?: boolean; + isAddButtonDark?: boolean; + isAuthenticatingRequired?: boolean; +}; + +function getBillingStatus( + translate: (phraseKey: TKey, ...phraseParameters: PhraseParameters>) => string, + locale: Locale, + cardEnding: string, +): BillingStatusResult { + const amountOwed = SubscriptionUtils.getAmountOwed(); + + const status = SubscriptionUtils.getSubscriptionStatus(); + + const endDate = SubscriptionUtils.getOverdueGracePeriodDate(); + + const endDateFormatted = format(new Date(endDate), CONST.DATE.MONTH_DAY_YEAR_FORMAT, {locale}); + + switch (status.status) { + case SubscriptionUtils.PAYMENT_STATUSES.POLICY_OWNER_WITH_AMOUNT_OWED: + return { + title: translate('subscription.billingBanner.outdatedInfo'), + subtitle: translate('subscription.billingBanner.updateCardDataByDate', {date: endDateFormatted}), + isError: true, + shouldShowRedDotIndicator: status.shouldShowRedDotIndicator, + isRetryAvailable: true, + }; + + case SubscriptionUtils.PAYMENT_STATUSES.POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE: + return { + title: translate('subscription.billingBanner.outdatedInfo'), + subtitle: translate('subscription.billingBanner.updatePaymentInformation'), + isError: true, + shouldShowRedDotIndicator: status.shouldShowRedDotIndicator, + }; + + case SubscriptionUtils.PAYMENT_STATUSES.OWNER_OF_POLICY_UNDER_INVOICING: + return { + title: translate('subscription.billingBanner.outdatedInfo'), + subtitle: translate('subscription.billingBanner.paymentPastDuePayByDate', {date: endDateFormatted}), + isError: true, + shouldShowRedDotIndicator: status.shouldShowRedDotIndicator, + isAddButtonDark: true, + }; + + case SubscriptionUtils.PAYMENT_STATUSES.OWNER_OF_POLICY_UNDER_INVOICING_OVERDUE: + return { + title: translate('subscription.billingBanner.outdatedInfo'), + subtitle: translate('subscription.billingBanner.paymentPastDue'), + isError: true, + shouldShowRedDotIndicator: status.shouldShowRedDotIndicator, + isAddButtonDark: true, + }; + + case SubscriptionUtils.PAYMENT_STATUSES.BILLING_DISPUTE_PENDING: + return { + title: translate('subscription.billingBanner.cardCouldNotBeCharged'), + subtitle: translate('subscription.billingBanner.cardOnDispute', {amountOwed, cardEnding}), + isError: true, + shouldShowRedDotIndicator: status.shouldShowRedDotIndicator, + isRetryAvailable: false, + }; + + case SubscriptionUtils.PAYMENT_STATUSES.CARD_AUTHENTICATION_REQUIRED: + return { + title: translate('subscription.billingBanner.cardCouldNotBeCharged'), + subtitle: translate('subscription.billingBanner.cardNotFullyAuthenticated', {cardEnding}), + isError: true, + shouldShowRedDotIndicator: status.shouldShowRedDotIndicator, + isAuthenticatingRequired: true, + }; + + case SubscriptionUtils.PAYMENT_STATUSES.INSUFFICIENT_FUNDS: + return { + title: translate('subscription.billingBanner.cardCouldNotBeCharged'), + subtitle: translate('subscription.billingBanner.cardDeclinedDueToInsufficientFunds', {amountOwed}), + isError: true, + shouldShowRedDotIndicator: status.shouldShowRedDotIndicator, + isRetryAvailable: true, + }; + + case SubscriptionUtils.PAYMENT_STATUSES.CARD_EXPIRED: + return { + title: translate('subscription.billingBanner.cardCouldNotBeCharged'), + subtitle: translate('subscription.billingBanner.cardExpired', {amountOwed}), + isError: true, + shouldShowRedDotIndicator: status.shouldShowRedDotIndicator, + isRetryAvailable: true, + }; + + case SubscriptionUtils.PAYMENT_STATUSES.CARD_EXPIRE_SOON: + return { + title: translate('subscription.billingBanner.cardExpiringSoon'), + subtitle: translate('subscription.billingBanner.cardWillExpireAtTheEndOfMonth'), + isError: false, + shouldShowGreenDotIndicator: status.shouldShowGreenDotIndicator, + }; + + case SubscriptionUtils.PAYMENT_STATUSES.RETRY_BILLING_SUCCESS: + return { + title: translate('subscription.billingBanner.succeeded'), + subtitle: translate('subscription.billingBanner.billedSuccessfully'), + isError: false, + shouldShowRedDotIndicator: status.shouldShowRedDotIndicator, + }; + + case SubscriptionUtils.PAYMENT_STATUSES.GENERIC_API_ERROR: + return { + title: translate('subscription.billingBanner.succeeded'), + subtitle: translate('subscription.billingBanner.billedSuccessfully'), + isError: false, + shouldShowRedDotIndicator: status.shouldShowRedDotIndicator, + }; + + default: + break; + } + + return {}; +} + +function getCardForSubscriptionBilling(): Fund | undefined { + return SubscriptionUtils.getCardForSubscriptionBilling(); +} + +export default { + getBillingStatus, + getCardForSubscriptionBilling, +}; From 2c22d474eabcc2e1b2ce9aa3d21ccd6f3c5348e2 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Fri, 14 Jun 2024 23:57:31 +0200 Subject: [PATCH 006/183] fix locale type --- src/pages/settings/Subscription/CardSection/utils.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pages/settings/Subscription/CardSection/utils.ts b/src/pages/settings/Subscription/CardSection/utils.ts index b4f12b567665..9595c281abaa 100644 --- a/src/pages/settings/Subscription/CardSection/utils.ts +++ b/src/pages/settings/Subscription/CardSection/utils.ts @@ -1,9 +1,11 @@ -import {format} from 'date-fns'; -import type {Phrase, PhraseParameters} from '@libs/Localize'; +import { format } from 'date-fns'; +import type { Phrase, PhraseParameters } from '@libs/Localize'; import SubscriptionUtils from '@libs/SubscriptionUtils'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; -import type {Fund} from '@src/types/onyx'; +import type { Fund } from '@src/types/onyx'; +import type Locale from '@src/types/onyx/Locale'; + type BillingStatusResult = { title?: string; From b16d864a8d1bc1f458c188acebff8ef0f2b09977 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Tue, 18 Jun 2024 23:05:27 +0700 Subject: [PATCH 007/183] mark GRB on task at highest --- src/libs/ReportUtils.ts | 12 +++- src/libs/SidebarUtils.ts | 4 ++ src/libs/actions/Report.ts | 2 + src/libs/actions/Task.ts | 110 ++++++++++++++++++++++++++++++++++++- src/types/onyx/Report.ts | 3 + 5 files changed, 128 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a02b24c20e35..9af1c13bf5e0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -335,6 +335,7 @@ type OptimisticTaskReport = Pick< | 'notificationPreference' | 'parentReportActionID' | 'lastVisibleActionCreated' + | 'hasParentAccess' >; type TransactionDetails = { @@ -2267,7 +2268,15 @@ function getLastVisibleMessage(reportID: string | undefined, actionsToMerge: Rep * @param [parentReportAction] - The parent report action of the report (Used to check if the task has been canceled) */ function isWaitingForAssigneeToCompleteTask(report: OnyxEntry, parentReportAction: OnyxEntry | EmptyObject = {}): boolean { - return isTaskReport(report) && isReportManager(report) && isOpenTaskReport(report, parentReportAction); + if (report?.hasOutstandingChildTask) { + return true; + } + + if (isOpenTaskReport(report, parentReportAction) && !report?.hasParentAccess && isReportManager(report)) { + return true; + } + + return false; } function isUnreadWithMention(reportOrOption: OnyxEntry | OptionData): boolean { @@ -5056,6 +5065,7 @@ function buildOptimisticTaskReport( statusNum: CONST.REPORT.STATUS_NUM.OPEN, notificationPreference, lastVisibleActionCreated: DateUtils.getDBTime(), + hasParentAccess: true, }; } diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index c96d3c511320..06daa2d34e01 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -246,6 +246,8 @@ function getOptionData({ searchText: undefined, isPinned: false, hasOutstandingChildRequest: false, + hasOutstandingChildTask: false, + hasParentAccess: undefined, isIOUReportOwner: null, isChatRoom: false, isArchivedRoom: false, @@ -307,6 +309,8 @@ function getOptionData({ result.isSelfDM = ReportUtils.isSelfDM(report); result.isOneOnOneChat = isOneOnOneChat; result.tooltipText = ReportUtils.getReportParticipantsTitle(visibleParticipantAccountIDs); + result.hasOutstandingChildTask = report.hasOutstandingChildTask; + result.hasParentAccess = report.hasParentAccess; const hasMultipleParticipants = participantPersonalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat || ReportUtils.isExpenseReport(report); const subtitle = ReportUtils.getChatRoomSubtitle(report); diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 192c6f720b5f..cfa058eb4c64 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3238,6 +3238,7 @@ function completeOnboarding( managerID: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }, isOptimisticReport: true, + managerID: currentUserAccountID, }, }, { @@ -3264,6 +3265,7 @@ function completeOnboarding( value: { stateNum: CONST.REPORT.STATE_NUM.APPROVED, statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + managerID: currentUserAccountID, }, }); } diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 51fcbb53bc5f..2efb27d0d3f9 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -133,12 +133,14 @@ function createTaskAndNavigate( const currentTime = DateUtils.getDBTimeWithSkew(); const lastCommentText = ReportUtils.formatReportLastMessageText(optimisticAddCommentReport?.reportAction?.message?.[0]?.text ?? ''); + const parentReport = ReportUtils.getReport(parentReportID); const optimisticParentReport = { lastVisibleActionCreated: optimisticAddCommentReport.reportAction.created, lastMessageText: lastCommentText, lastActorAccountID: currentUserAccountID, lastReadTime: currentTime, lastMessageTranslationKey: '', + hasOutstandingChildTask: assigneeAccountID === currentUserAccountID ? true : parentReport?.hasOutstandingChildTask, }; // We're only setting onyx data for the task report here because it's possible for the parent report to not exist yet (if you're assigning a task to someone you haven't chatted with before) @@ -274,7 +276,14 @@ function createTaskAndNavigate( }, }); - clearOutTaskInfo(); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`, + value: { + hasOutstandingChildTask: parentReport?.hasOutstandingChildTask, + }, + }), + clearOutTaskInfo(); const parameters: CreateTaskParams = { parentReportActionID: optimisticAddCommentReport.reportAction.reportActionID, @@ -296,6 +305,27 @@ function createTaskAndNavigate( Report.notifyNewAction(parentReportID, currentUserAccountID); } +/** + * @returns the object to update `report.hasOutstandingChildTask` + */ +function getOutstandingChildTask(taskReport: OnyxEntry) { + const parentReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${taskReport?.parentReportID}`] ?? {}; + return Object.values(parentReportActions).some((reportAction) => { + if (reportAction.childReportID === taskReport?.reportID) { + return false; + } + + if ( + reportAction.childType === CONST.REPORT.TYPE.TASK && + reportAction?.childStateNum === CONST.REPORT.STATE_NUM.OPEN && + reportAction?.childStatusNum === CONST.REPORT.STATUS_NUM.OPEN && + !reportAction?.message?.[0]?.isDeletedParentAction + ) { + return true; + } + }); +} + /** * Complete a task */ @@ -303,7 +333,7 @@ function completeTask(taskReport: OnyxEntry) { const taskReportID = taskReport?.reportID ?? '-1'; const message = `marked as complete`; const completedTaskReportAction = ReportUtils.buildOptimisticTaskReportAction(taskReportID, CONST.REPORT.ACTIONS.TYPE.TASK_COMPLETED, message); - + const parentReport = getParentReport(taskReport); const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -353,6 +383,24 @@ function completeTask(taskReport: OnyxEntry) { }, ]; + if (parentReport?.hasOutstandingChildTask) { + const hasOutstandingChildTask = getOutstandingChildTask(taskReport); + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${taskReport?.parentReportID}`, + value: { + hasOutstandingChildTask, + }, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${taskReport?.parentReportID}`, + value: { + hasOutstandingChildTask: parentReport?.hasOutstandingChildTask, + }, + }); + } + const parameters: CompleteTaskParams = { taskReportID, completedTaskReportActionID: completedTaskReportAction.reportActionID, @@ -370,6 +418,8 @@ function reopenTask(taskReport: OnyxEntry) { const taskReportID = taskReport?.reportID ?? '-1'; const message = `marked as incomplete`; const reopenedTaskReportAction = ReportUtils.buildOptimisticTaskReportAction(taskReportID, CONST.REPORT.ACTIONS.TYPE.TASK_REOPENED, message); + const parentReport = getParentReport(taskReport); + const hasOutstandingChildTask = taskReport?.managerID === currentUserAccountID ? true : parentReport?.hasOutstandingChildTask; const optimisticData: OnyxUpdate[] = [ { @@ -384,6 +434,13 @@ function reopenTask(taskReport: OnyxEntry) { lastReadTime: reopenedTaskReportAction.created, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${taskReport?.parentReportID}`, + value: { + hasOutstandingChildTask, + }, + }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${taskReportID}`, @@ -411,6 +468,13 @@ function reopenTask(taskReport: OnyxEntry) { statusNum: CONST.REPORT.STATUS_NUM.APPROVED, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${taskReport?.parentReportID}`, + value: { + hasOutstandingChildTask: taskReport?.hasOutstandingChildTask, + }, + }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${taskReportID}`, @@ -566,6 +630,39 @@ function editTaskAssignee(report: OnyxTypes.Report, ownerAccountID: number, assi }, ]; + if (currentUserAccountID === assigneeAccountID) { + const parentReport = getParentReport(report); + if (!isEmptyObject(parentReport)) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`, + value: {hasOutstandingChildTask: true}, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`, + value: {hasOutstandingChildTask: parentReport?.hasOutstandingChildTask}, + }); + } + } + + if (report.managerID === currentUserAccountID) { + const hasOutstandingChildTask = getOutstandingChildTask(report); + const parentReport = getParentReport(report); + if (!isEmptyObject(parentReport)) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`, + value: {hasOutstandingChildTask}, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`, + value: {hasOutstandingChildTask: parentReport?.hasOutstandingChildTask}, + }); + } + } + // If we make a change to the assignee, we want to add a comment to the assignee's chat // Check if the assignee actually changed if (assigneeAccountID && assigneeAccountID !== report.managerID && assigneeAccountID !== ownerAccountID && assigneeChatReport) { @@ -849,6 +946,7 @@ function deleteTask(report: OnyxEntry) { const optimisticReportActions = { [parentReportAction.reportActionID]: optimisticReportAction, }; + const hasOutstandingChildTask = getOutstandingChildTask(report); const optimisticData: OnyxUpdate[] = [ { @@ -867,6 +965,7 @@ function deleteTask(report: OnyxEntry) { value: { lastMessageText: ReportActionsUtils.getLastVisibleMessage(parentReport?.reportID ?? '-1', optimisticReportActions as OnyxTypes.ReportActions).lastMessageText ?? '', lastVisibleActionCreated: ReportActionsUtils.getLastVisibleAction(parentReport?.reportID ?? '-1', optimisticReportActions as OnyxTypes.ReportActions)?.created, + hasOutstandingChildTask, }, }, { @@ -929,6 +1028,13 @@ function deleteTask(report: OnyxEntry) { statusNum: report.statusNum ?? '', } as OnyxTypes.Report, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport?.reportID}`, + value: { + hasOutstandingChildTask: parentReport?.hasOutstandingChildTask, + }, + }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 9b0b35a6da7c..b5d0a0a15d13 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -77,6 +77,9 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< /** Whether the report has a child that is an outstanding expense that is awaiting action from the current user */ hasOutstandingChildRequest?: boolean; + /** Whether the report has a child task that is awaiting action from the current user */ + hasOutstandingChildTask?: boolean; + /** List of icons for report participants */ icons?: OnyxCommon.Icon[]; From 6fdaf2dcd258b43567065c4e46be9a670d76edef Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Tue, 18 Jun 2024 23:21:24 +0700 Subject: [PATCH 008/183] address some lint errors --- src/libs/actions/Task.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 2efb27d0d3f9..6a7a34bf7ec1 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -275,15 +275,15 @@ function createTaskAndNavigate( }, }, }); - failureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`, value: { hasOutstandingChildTask: parentReport?.hasOutstandingChildTask, }, - }), - clearOutTaskInfo(); + }); + + clearOutTaskInfo(); const parameters: CreateTaskParams = { parentReportActionID: optimisticAddCommentReport.reportAction.reportActionID, @@ -323,6 +323,8 @@ function getOutstandingChildTask(taskReport: OnyxEntry) { ) { return true; } + + return false; }); } From 2aacaf36872261c413e37bfbf256a66653b51e5f Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Wed, 19 Jun 2024 00:04:10 +0700 Subject: [PATCH 009/183] force type to compare correctly --- src/libs/actions/Task.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 6a7a34bf7ec1..35521945218a 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -311,7 +311,7 @@ function createTaskAndNavigate( function getOutstandingChildTask(taskReport: OnyxEntry) { const parentReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${taskReport?.parentReportID}`] ?? {}; return Object.values(parentReportActions).some((reportAction) => { - if (reportAction.childReportID === taskReport?.reportID) { + if (String(reportAction.childReportID) === String(taskReport?.reportID)) { return false; } From d511850bb4ca89ce787880ddf4e9283e26adf0db Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 19 Jun 2024 14:20:05 +0200 Subject: [PATCH 010/183] fix runtime errors --- src/languages/es.ts | 22 +++++++++++++++++++ .../Subscription/CardSection/utils.ts | 9 ++++---- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 4c900e23acc5..fa07f9a626e0 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3714,6 +3714,28 @@ export default { }, subscription: { mobileReducedFunctionalityMessage: 'No puedes hacer cambios en tu suscripción en la aplicación móvil.', + billingBanner: { + outdatedInfo: 'Tu información de pago está desactualizada', + updatePaymentInformation: 'Por favor, actualiza tu información de pago.', + updateCardDataByDate: ({date}) => `Actualiza tu tarjeta de pago antes del ${date} para continuar utilizando todas tus herramientas favoritas`, + paymentPastDuePayByDate: ({date}) => `Tu pago está vencido. Por favor, paga tu factura antes del ${date} para evitar la interrupción del servicio.`, + paymentPastDue: 'Tu pago está vencido. Por favor, paga tu factura.', + cardCouldNotBeCharged: 'No se ha podido realizar el cobro a tu tarjeta', + retryMessage: + 'Antes de volver a intentarlo, llama directamente a tu banco para que autorice los cargos de Expensify y elimine las retenciones. De lo contrario, añade una tarjeta de pago diferente.', + cardNotFullyAuthenticated: ({cardEnding}) => + `Tu tarjeta de pago no ha sido autenticada completamente. Por favor, completa el proceso de autenticación para activar tu tarjeta de pago que termina en ${cardEnding}.`, + cardDeclinedDueToInsufficientFunds: ({amountOwed}) => + `Tu tarjeta de pago fue rechazada por falta de fondos. Vuelve a intentarlo o añade una nueva tarjeta de pago para liquidar tu saldo pendiente de ${amountOwed}.`, + cardExpired: ({amountOwed}) => `Tu tarjeta de pago ha expirado. Por favor, añade una nueva tarjeta de pago para liquidar tu saldo pendiente de ${amountOwed}.`, + cardExpiringSoon: 'Tu tarjeta caducará pronto', + cardWillExpireAtTheEndOfMonth: + 'Tu tarjeta de pago caducará a finales de este mes. Haz clic en el menú de tres puntos que aparece a continuación para actualizarla y continuar utilizando todas tus herramientas favoritas.', + cardOnDispute: ({amountOwed, cardEnding}) => + `Has impugnado el cargo ${amountOwed} en la tarjeta terminada en ${cardEnding}. Tu cuenta estará bloqueada hasta que se resuelva la disputa con tu banco.`, + succeeded: 'Éxito!', + billedSuccessfully: 'Tu tarjeta fue facturada correctamente.', + }, cardSection: { title: 'Pago', subtitle: 'Añade una tarjeta de pago para abonar tu suscripción a Expensify', diff --git a/src/pages/settings/Subscription/CardSection/utils.ts b/src/pages/settings/Subscription/CardSection/utils.ts index 9595c281abaa..82a350ed923d 100644 --- a/src/pages/settings/Subscription/CardSection/utils.ts +++ b/src/pages/settings/Subscription/CardSection/utils.ts @@ -1,12 +1,11 @@ -import { format } from 'date-fns'; -import type { Phrase, PhraseParameters } from '@libs/Localize'; +import DateUtils from '@libs/DateUtils'; +import type {Phrase, PhraseParameters} from '@libs/Localize'; import SubscriptionUtils from '@libs/SubscriptionUtils'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; -import type { Fund } from '@src/types/onyx'; +import type {Fund} from '@src/types/onyx'; import type Locale from '@src/types/onyx/Locale'; - type BillingStatusResult = { title?: string; subtitle?: string; @@ -30,7 +29,7 @@ function getBillingStatus( const endDate = SubscriptionUtils.getOverdueGracePeriodDate(); - const endDateFormatted = format(new Date(endDate), CONST.DATE.MONTH_DAY_YEAR_FORMAT, {locale}); + const endDateFormatted = DateUtils.formatWithUTCTimeZone(endDate, CONST.DATE.MONTH_DAY_YEAR_FORMAT); switch (status.status) { case SubscriptionUtils.PAYMENT_STATUSES.POLICY_OWNER_WITH_AMOUNT_OWED: From 54bf99e8b6bcbd108e4807d2b04d89d5bb22fbf2 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 19 Jun 2024 16:13:11 +0200 Subject: [PATCH 011/183] add missing cases --- src/ONYXKEYS.ts | 8 +++ src/libs/SubscriptionUtils.ts | 49 +++++++++++++++++++ .../Subscription/CardSection/CardSection.tsx | 2 - .../Subscription/CardSection/utils.ts | 10 +++- 4 files changed, 66 insertions(+), 3 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 5fdb3be1110b..8987954196f1 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -339,6 +339,12 @@ const ONYXKEYS = { /** Holds the checks used while transferring the ownership of the workspace */ POLICY_OWNERSHIP_CHANGE_CHECKS: 'policyOwnershipChangeChecks', + /** Indicates whether ClearOutstandingBalance failed */ + SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED: 'subscriptionRetryBillingStatusFailed', + + /** Indicates whether ClearOutstandingBalance was successful */ + SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL: 'subscriptionRetryBillingStatusSuccessful', + /** Collection Keys */ COLLECTION: { DOWNLOAD: 'download_', @@ -729,6 +735,8 @@ type OnyxValuesMapping = { [ONYXKEYS.CACHED_PDF_PATHS]: Record; [ONYXKEYS.POLICY_OWNERSHIP_CHANGE_CHECKS]: Record; [ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE]: OnyxTypes.QuickAction; + [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED]: boolean; + [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL]: boolean; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index a8679f002399..6f6a6c1750a7 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -90,6 +90,30 @@ Onyx.connect({ }, }); +let billingStatusFailed: OnyxValues[typeof ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED]; +Onyx.connect({ + key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED, + callback: (value) => { + if (value === undefined) { + return; + } + + billingStatusFailed = value; + }, +}); + +let billingStatusSuccessful: OnyxValues[typeof ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL]; +Onyx.connect({ + key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, + callback: (value) => { + if (value === undefined) { + return; + } + + billingStatusSuccessful = value; + }, +}); + function getOverdueGracePeriodDate(): string { return billingGracePeriod ?? ''; } @@ -139,6 +163,13 @@ function hasCardExpiringSoon(): boolean { } // hasRetryBillingError - based on request response +function hasRetryBillingError(): boolean { + return !!billingStatusFailed; +} + +function isRetryBillingSuccessful(): boolean { + return !!billingStatusSuccessful; +} // hasCardExpiredSoon - based on card @@ -229,6 +260,23 @@ function getSubscriptionStatus(): SubscriptionStatus { } // 10. Retry billing success + if (isRetryBillingSuccessful()) { + return { + status: PAYMENT_STATUSES.RETRY_BILLING_SUCCESS, + isError: false, + }; + } + + // 11. Retry billing error + if (hasRetryBillingError()) { + return { + status: PAYMENT_STATUSES.RETRY_BILLING_ERROR, + isError: true, + shouldShowRedDotIndicator: true, + }; + } + + // 12. Generic API error if (false) { return { status: PAYMENT_STATUSES.GENERIC_API_ERROR, @@ -255,5 +303,6 @@ export default { getOverdueGracePeriodDate, getCardForSubscriptionBilling, hasSubscriptionGreenDotInfo, + hasRetryBillingError, PAYMENT_STATUSES, }; diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index cd64153455d1..c3971cdc4869 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -1,6 +1,5 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import Section from '@components/Section'; @@ -9,7 +8,6 @@ import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; -import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import BillingBanner from './BillingBanner'; import CardSectionActions from './CardSectionActions'; diff --git a/src/pages/settings/Subscription/CardSection/utils.ts b/src/pages/settings/Subscription/CardSection/utils.ts index 82a350ed923d..5ed2858e9cf5 100644 --- a/src/pages/settings/Subscription/CardSection/utils.ts +++ b/src/pages/settings/Subscription/CardSection/utils.ts @@ -107,7 +107,7 @@ function getBillingStatus( return { title: translate('subscription.billingBanner.cardExpiringSoon'), subtitle: translate('subscription.billingBanner.cardWillExpireAtTheEndOfMonth'), - isError: false, + isError: true, shouldShowGreenDotIndicator: status.shouldShowGreenDotIndicator, }; @@ -119,6 +119,14 @@ function getBillingStatus( shouldShowRedDotIndicator: status.shouldShowRedDotIndicator, }; + case SubscriptionUtils.PAYMENT_STATUSES.RETRY_BILLING_ERROR: + return { + title: translate('subscription.billingBanner.cardCouldNotBeCharged'), + subtitle: translate('subscription.billingBanner.retryMessage'), + isError: true, + shouldShowRedDotIndicator: status.shouldShowRedDotIndicator, + }; + case SubscriptionUtils.PAYMENT_STATUSES.GENERIC_API_ERROR: return { title: translate('subscription.billingBanner.succeeded'), From b4205472ffb9da085ea1198d19a69b399d4b6326 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Thu, 20 Jun 2024 13:33:01 +0200 Subject: [PATCH 012/183] lint the code --- src/libs/SubscriptionUtils.ts | 20 ++++++++++++------- .../Subscription/CardSection/utils.ts | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 25227ea69f16..8ab34b9a2829 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -1,10 +1,10 @@ -import Onyx from 'react-native-onyx'; -import type {OnyxValues} from '@src/ONYXKEYS'; -import ONYXKEYS from '@src/ONYXKEYS'; import {differenceInSeconds, fromUnixTime, isAfter, isBefore, parse as parseDate} from 'date-fns'; +import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; -import type {BillingGraceEndPeriod, Policy, Fund} from '@src/types/onyx'; +import type {OnyxValues} from '@src/ONYXKEYS'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {BillingGraceEndPeriod, Fund, Policy} from '@src/types/onyx'; const PAYMENT_STATUSES = { POLICY_OWNER_WITH_AMOUNT_OWED: 'policy_owner_with_amount_owed', @@ -21,7 +21,6 @@ const PAYMENT_STATUSES = { GENERIC_API_ERROR: 'generic_api_error', }; - let amountOwed: OnyxValues[typeof ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]; Onyx.connect({ key: ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED, @@ -415,11 +414,18 @@ function shouldRestrictUserBillableActions(policyID: string): boolean { return false; } -export {calculateRemainingFreeTrialDays, doesUserHavePaymentCardAdded, hasUserFreeTrialEnded, isUserOnFreeTrial, shouldRestrictUserBillableActions, getSubscriptionStatus, +export { + calculateRemainingFreeTrialDays, + doesUserHavePaymentCardAdded, + hasUserFreeTrialEnded, + isUserOnFreeTrial, + shouldRestrictUserBillableActions, + getSubscriptionStatus, hasSubscriptionRedDotError, getAmountOwed, getOverdueGracePeriodDate, getCardForSubscriptionBilling, hasSubscriptionGreenDotInfo, hasRetryBillingError, - PAYMENT_STATUSES,}; \ No newline at end of file + PAYMENT_STATUSES, +}; diff --git a/src/pages/settings/Subscription/CardSection/utils.ts b/src/pages/settings/Subscription/CardSection/utils.ts index 1b6f5cb98b7d..9628b2d229f4 100644 --- a/src/pages/settings/Subscription/CardSection/utils.ts +++ b/src/pages/settings/Subscription/CardSection/utils.ts @@ -1,3 +1,4 @@ +import {fromUnixTime} from 'date-fns'; import DateUtils from '@libs/DateUtils'; import type {Phrase, PhraseParameters} from '@libs/Localize'; import * as SubscriptionUtils from '@libs/SubscriptionUtils'; @@ -5,7 +6,6 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import type {Fund} from '@src/types/onyx'; import type Locale from '@src/types/onyx/Locale'; -import { fromUnixTime } from 'date-fns'; type BillingStatusResult = { title?: string; From c46f6b050990b68ccd1d88b3c526f07ac0007005 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Thu, 20 Jun 2024 14:09:06 +0200 Subject: [PATCH 013/183] remove unnecessary condition --- src/libs/SubscriptionUtils.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 8ab34b9a2829..988c83354efb 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -312,13 +312,13 @@ function getSubscriptionStatus(): SubscriptionStatus { } // 12. Generic API error - if (false) { - return { - status: PAYMENT_STATUSES.GENERIC_API_ERROR, - isError: true, - shouldShowRedDotIndicator: true, - }; - } + // if (false) { + // return { + // status: PAYMENT_STATUSES.GENERIC_API_ERROR, + // isError: true, + // shouldShowRedDotIndicator: true, + // }; + // } return {}; } From 9c7bf7348f65f2f9c05fdbf4bc2b8f39e11fadc4 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Thu, 20 Jun 2024 15:49:31 +0200 Subject: [PATCH 014/183] fix typecheck errors --- src/libs/actions/Policy/Policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index b15bcc93a6f5..79871ba38945 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -2241,8 +2241,8 @@ function createWorkspaceFromIOUPayment(iouReport: Report | EmptyObject): string }, }, }, - ...employeeWorkspaceChat.onyxSuccessData, ]; + successData.push(...employeeWorkspaceChat.onyxOptimisticData); const failureData: OnyxUpdate[] = [ { From 142a46a03c99e5225e5aef6e9d3647099336825b Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Fri, 21 Jun 2024 14:48:09 +0700 Subject: [PATCH 015/183] solve lint error after merging main --- src/libs/actions/Task.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index d4280eb07109..d2635e483d9d 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -133,7 +133,7 @@ function createTaskAndNavigate( const currentTime = DateUtils.getDBTimeWithSkew(); const lastCommentText = ReportUtils.formatReportLastMessageText(optimisticAddCommentReport?.reportAction?.message?.[0]?.text ?? ''); - const parentReport = ReportUtils.getReport(parentReportID); + const parentReport = getReport(parentReportID); const optimisticParentReport = { lastVisibleActionCreated: optimisticAddCommentReport.reportAction.created, lastMessageText: lastCommentText, @@ -912,6 +912,13 @@ function getParentReport(report: OnyxEntry | EmptyObject): Ony return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`] ?? {}; } +/** + * Returns the report + */ +function getReport(reportID: string): OnyxEntry | EmptyObject { + return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? {}; +} + /** * Cancels a task by setting the report state to SUBMITTED and status to CLOSED */ From f56f2e13c38963db06fbd27f64390c3a62be37aa Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Fri, 21 Jun 2024 15:07:49 +0700 Subject: [PATCH 016/183] chore --- src/libs/actions/Report.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 7edb0a6259a3..d842b1caca6b 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3225,6 +3225,8 @@ function completeOnboarding( description: taskDescription ?? '', })); + const hasOutstandingChildTask = tasksData.some((task) => !task.completedTaskReportAction); + const tasksForOptimisticData = tasksData.reduce((acc, {currentTask, taskCreatedAction, taskReportAction, taskDescription, completedTaskReportAction}) => { acc.push( { @@ -3359,6 +3361,7 @@ function completeOnboarding( key: `${ONYXKEYS.COLLECTION.REPORT}${targetChatReportID}`, value: { lastMentionedTime: DateUtils.getDBTime(), + hasOutstandingChildTask, }, }, { @@ -3390,6 +3393,7 @@ function completeOnboarding( lastMessageTranslationKey: '', lastMessageText: '', lastVisibleActionCreated: '', + hasOutstandingChildTask: false, }; const {lastMessageText = '', lastMessageTranslationKey = ''} = ReportActionsUtils.getLastVisibleMessage(targetChatReportID); if (lastMessageText || lastMessageTranslationKey) { From 4b112852a0e772a28bb324b580d983da27a7581a Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 21 Jun 2024 11:12:32 +0200 Subject: [PATCH 017/183] feat: add request refund button --- src/hooks/useIsEligibleForRefund.ts | 17 +++ src/languages/en.ts | 7 + src/languages/es.ts | 7 + src/libs/API/types.ts | 1 + src/libs/actions/User.ts | 5 + src/pages/settings/InitialSettingsPage.tsx | 3 + .../Subscription/CardSection/CardSection.tsx | 133 +++++++++++++----- src/types/onyx/Account.ts | 3 + 8 files changed, 142 insertions(+), 34 deletions(-) create mode 100644 src/hooks/useIsEligibleForRefund.ts diff --git a/src/hooks/useIsEligibleForRefund.ts b/src/hooks/useIsEligibleForRefund.ts new file mode 100644 index 000000000000..f07865592798 --- /dev/null +++ b/src/hooks/useIsEligibleForRefund.ts @@ -0,0 +1,17 @@ +import {useMemo} from 'react'; +import {useOnyx} from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; + +function useIsEligibleForRefund() { + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + + return useMemo(() => { + if (!account) { + return false; + } + + return account.isEligibleForRefund; + }, [account]); +} + +export default useIsEligibleForRefund; diff --git a/src/languages/en.ts b/src/languages/en.ts index 3a569801bb6a..30eadaba2c27 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3224,6 +3224,13 @@ export default { changeCurrency: 'Change payment currency', cardNotFound: 'No payment card added', retryPaymentButton: 'Retry payment', + requestRefund: 'Request refund', + requestRefundModal: { + phrase1: 'Getting a refund is easy, just downgrade your account before your next billing date and you’ll receive a refund.', + phrase2: + 'Heads up: Downgrading your account means your workspace(s) will be deleted. This action can’t be undone, but you can always create a new workspace if you change your mind.', + confirm: 'Delete workspace(s) and downgrade', + }, }, yourPlan: { title: 'Your plan', diff --git a/src/languages/es.ts b/src/languages/es.ts index a2118d55e43c..69fa2a39d01e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3728,6 +3728,13 @@ export default { changeCurrency: 'Cambiar moneda de pago', cardNotFound: 'No se ha añadido ninguna tarjeta de pago', retryPaymentButton: 'Reintentar el pago', + requestRefund: 'Solicitar reembolso', + requestRefundModal: { + phrase1: 'Obtener un reembolso es fácil, simplemente baja tu cuenta de categoría antes de la próxima fecha de facturación y recibirás un reembolso.', + phrase2: + 'Atención: Bajar tu cuenta de categoría significa que tu(s) espacio(s) de trabajo será(n) eliminado(s). Esta acción no se puede deshacer, pero siempre puedes crear un nuevo espacio de trabajo si cambias de opinión.', + confirm: 'Eliminar espacio(s) de trabajo y bajar de categoría', + }, }, yourPlan: { title: 'Tu plan', diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 8f093ee827c3..fc31c9df6370 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', + REQUEST_REFUND: 'User_RefundPurchase', } as const; type WriteCommand = ValueOf; diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index eb3d56158b0c..ec96441ec6e6 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -1020,6 +1020,10 @@ function dismissTrackTrainingModal() { }); } +function requestRefund() { + API.write(WRITE_COMMANDS.REQUEST_REFUND, {}); +} + export { clearFocusModeNotification, closeAccount, @@ -1051,4 +1055,5 @@ export { clearCustomStatus, updateDraftCustomStatus, clearDraftCustomStatus, + requestRefund, }; diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index 8fa152d5c06a..5d1f573af9f5 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -106,6 +106,9 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa const emojiCode = currentUserPersonalDetails?.status?.emojiCode ?? ''; const [shouldShowSignoutConfirmModal, setShouldShowSignoutConfirmModal] = useState(false); + useEffect(() => { + Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION); + }, []); useEffect(() => { Wallet.openInitialSettingsPage(); diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index da65c9d74f07..691e151d072b 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -1,13 +1,18 @@ -import React, {useMemo} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; +import Onyx, {useOnyx} from 'react-native-onyx'; +import ConfirmModal from '@components/ConfirmModal'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItem from '@components/MenuItem'; import Section from '@components/Section'; import Text from '@components/Text'; +import useIsEligibleForRefund from '@hooks/useIsEligibleForRefund'; import useLocalize from '@hooks/useLocalize'; +import useSubscriptionPlan from '@hooks/useSubscriptionPlan'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as User from '@libs/actions/User'; import DateUtils from '@libs/DateUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -15,50 +20,110 @@ import CardSectionActions from './CardSectionActions'; import CardSectionDataEmpty from './CardSectionDataEmpty'; function CardSection() { + const [isRequestRefundModalVisible, setIsRequestRefundModalVisible] = useState(false); const {translate, preferredLocale} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); + const plan = useSubscriptionPlan(); + const isEligibleForRefund = useIsEligibleForRefund(); const defaultCard = useMemo(() => Object.values(fundList ?? {}).find((card) => card.isDefault), [fundList]); const cardMonth = useMemo(() => DateUtils.getMonthNames(preferredLocale)[(defaultCard?.accountData?.cardMonth ?? 1) - 1], [defaultCard?.accountData?.cardMonth, preferredLocale]); + const requestRefund = useCallback(() => { + User.requestRefund(); + setIsRequestRefundModalVisible(false); + }, []); + + useEffect(() => { + Onyx.merge(ONYXKEYS.FUND_LIST, [ + { + accountData: { + cardMonth: 11, + + cardNumber: '1234', + + cardYear: 2026, + + currency: 'USD', + + addressName: 'John Doe', + }, + isDefault: true, + }, + ]); + }, [fundList]); return ( -
- - {!isEmptyObject(defaultCard?.accountData) && ( - <> - - - - {translate('subscription.cardSection.cardEnding', {cardNumber: defaultCard?.accountData?.cardNumber})} - - {translate('subscription.cardSection.cardInfo', { - name: defaultCard?.accountData?.addressName, - expiration: `${cardMonth} ${defaultCard?.accountData?.cardYear}`, - currency: defaultCard?.accountData?.currency, - })} - + <> +
+ + {!isEmptyObject(defaultCard?.accountData) && ( + <> + + + + {translate('subscription.cardSection.cardEnding', {cardNumber: defaultCard?.accountData?.cardNumber})} + + {translate('subscription.cardSection.cardInfo', { + name: defaultCard?.accountData?.addressName, + expiration: `${cardMonth} ${defaultCard?.accountData?.cardYear}`, + currency: defaultCard?.accountData?.currency, + })} + + - - - + + + )} + {isEmptyObject(defaultCard?.accountData) && } + + {!isEmptyObject(defaultCard?.accountData && plan) && ( + setIsRequestRefundModalVisible(true)} + /> )} - {isEmptyObject(defaultCard?.accountData) && } - -
+
+ + {/* {isEligibleForRefund && ( */} + { + setIsRequestRefundModalVisible(false); + }} + prompt={ + <> + {translate('subscription.cardSection.requestRefundModal.phrase1')} + {translate('subscription.cardSection.requestRefundModal.phrase2')} + + } + confirmText={translate('subscription.cardSection.requestRefundModal.confirm')} + cancelText={translate('common.cancel')} + danger + /> + {/* )} */} + ); } diff --git a/src/types/onyx/Account.ts b/src/types/onyx/Account.ts index dc9d0c937cd2..d442056ddd35 100644 --- a/src/types/onyx/Account.ts +++ b/src/types/onyx/Account.ts @@ -83,6 +83,9 @@ type Account = { /** Indicates whether the user can downgrade current subscription plan */ canDowngrade?: boolean; + + /** Indicates whether the user can downgrade current subscription plan */ + isEligibleForRefund?: boolean; }; export default Account; From 6180f1dc6ca9d80fc3f1114ecde427c1ead6af36 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 21 Jun 2024 11:36:41 +0200 Subject: [PATCH 018/183] fix: remove mb --- src/pages/settings/Subscription/CardSection/CardSection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 9824eba3e655..8f0de818397d 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -69,7 +69,7 @@ function CardSection() { subtitleMuted banner={BillingBanner} > - + {!isEmptyObject(defaultCard?.accountData) && ( Date: Fri, 21 Jun 2024 12:29:24 +0200 Subject: [PATCH 019/183] update and fix the error cases, show the right icons, handle errors --- src/libs/SubscriptionUtils.ts | 18 +---- .../CardSection/BillingBanner.tsx | 57 -------------- .../Subscription/CardSection/CardSection.tsx | 15 ++-- .../BillingBanner/BillingBanner.tsx | 76 +++++++++++++++++++ .../SubscriptionBillingBanner.tsx | 47 ++++++++++++ .../Subscription/CardSection/utils.ts | 29 +++---- 6 files changed, 140 insertions(+), 102 deletions(-) delete mode 100644 src/pages/settings/Subscription/CardSection/BillingBanner.tsx create mode 100644 src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/BillingBanner/BillingBanner.tsx create mode 100644 src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/BillingBanner/SubscriptionBillingBanner.tsx diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 988c83354efb..5db52c172561 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -211,8 +211,6 @@ function isRetryBillingSuccessful(): boolean { type SubscriptionStatus = { status?: string; isError?: boolean; - shouldShowRedDotIndicator?: boolean; - shouldShowGreenDotIndicator?: boolean; }; function getSubscriptionStatus(): SubscriptionStatus { @@ -223,7 +221,6 @@ function getSubscriptionStatus(): SubscriptionStatus { return { status: PAYMENT_STATUSES.POLICY_OWNER_WITH_AMOUNT_OWED, isError: true, - shouldShowRedDotIndicator: true, }; } @@ -231,7 +228,6 @@ function getSubscriptionStatus(): SubscriptionStatus { if (hasGracePeriodOverdue()) { return { status: PAYMENT_STATUSES.POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE, - shouldShowRedDotIndicator: true, }; } } else { @@ -239,8 +235,6 @@ function getSubscriptionStatus(): SubscriptionStatus { if (hasGracePeriodOverdue() && !amountOwed) { return { status: PAYMENT_STATUSES.OWNER_OF_POLICY_UNDER_INVOICING, - - shouldShowRedDotIndicator: true, }; } @@ -248,7 +242,6 @@ function getSubscriptionStatus(): SubscriptionStatus { if (hasGracePeriodOverdue() === false && amountOwed) { return { status: PAYMENT_STATUSES.OWNER_OF_POLICY_UNDER_INVOICING_OVERDUE, - shouldShowRedDotIndicator: true, }; } } @@ -258,7 +251,6 @@ function getSubscriptionStatus(): SubscriptionStatus { if (hasBillingDisputePending()) { return { status: PAYMENT_STATUSES.BILLING_DISPUTE_PENDING, - shouldShowRedDotIndicator: true, }; } @@ -266,7 +258,6 @@ function getSubscriptionStatus(): SubscriptionStatus { if (hasCardAuthenticatedError()) { return { status: PAYMENT_STATUSES.CARD_AUTHENTICATION_REQUIRED, - shouldShowRedDotIndicator: true, }; } @@ -274,7 +265,6 @@ function getSubscriptionStatus(): SubscriptionStatus { if (hasInsufficientFundsError()) { return { status: PAYMENT_STATUSES.INSUFFICIENT_FUNDS, - shouldShowRedDotIndicator: true, }; } @@ -282,7 +272,6 @@ function getSubscriptionStatus(): SubscriptionStatus { if (hasCardExpiredError()) { return { status: PAYMENT_STATUSES.CARD_EXPIRED, - shouldShowRedDotIndicator: true, }; } @@ -290,7 +279,6 @@ function getSubscriptionStatus(): SubscriptionStatus { if (hasCardExpiringSoon()) { return { status: PAYMENT_STATUSES.CARD_EXPIRE_SOON, - shouldShowGreenDotIndicator: true, }; } @@ -307,7 +295,6 @@ function getSubscriptionStatus(): SubscriptionStatus { return { status: PAYMENT_STATUSES.RETRY_BILLING_ERROR, isError: true, - shouldShowRedDotIndicator: true, }; } @@ -316,7 +303,6 @@ function getSubscriptionStatus(): SubscriptionStatus { // return { // status: PAYMENT_STATUSES.GENERIC_API_ERROR, // isError: true, - // shouldShowRedDotIndicator: true, // }; // } @@ -324,11 +310,11 @@ function getSubscriptionStatus(): SubscriptionStatus { } function hasSubscriptionRedDotError(): boolean { - return getSubscriptionStatus()?.shouldShowRedDotIndicator ?? false; + return getSubscriptionStatus()?.isError ?? false; } function hasSubscriptionGreenDotInfo(): boolean { - return getSubscriptionStatus()?.shouldShowRedDotIndicator ?? false; + return !getSubscriptionStatus()?.isError ?? false; } /** diff --git a/src/pages/settings/Subscription/CardSection/BillingBanner.tsx b/src/pages/settings/Subscription/CardSection/BillingBanner.tsx deleted file mode 100644 index b3e4d8859249..000000000000 --- a/src/pages/settings/Subscription/CardSection/BillingBanner.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; -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 Text from '@components/Text'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import variables from '@styles/variables'; - -type BillingBannerProps = { - title?: string; - subtitle?: string; - isError?: boolean; - shouldShowRedDotIndicator?: boolean; - shouldShowGreenDotIndicator?: boolean; - isTrialActive?: boolean; -}; - -function BillingBanner({title, subtitle, isError, shouldShowRedDotIndicator, shouldShowGreenDotIndicator, isTrialActive}: BillingBannerProps) { - const styles = useThemeStyles(); - const theme = useTheme(); - - const backgroundStyle = isTrialActive ? styles.trialBannerBackgroundColor : styles.hoveredComponentBG; - - const subtitleStyle = isTrialActive ? [] : styles.textSupporting; - - return ( - - - - {title && {title}} - {subtitle && {subtitle}} - - {isError && shouldShowRedDotIndicator && ( - - )} - {!isError && shouldShowGreenDotIndicator && ( - - )} - - ); -} - -BillingBanner.displayName = 'BillingBanner'; - -export default BillingBanner; diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index c3971cdc4869..124c2f256aea 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -9,9 +9,9 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import BillingBanner from './BillingBanner'; import CardSectionActions from './CardSectionActions'; import CardSectionDataEmpty from './CardSectionDataEmpty'; +import SubscriptionBillingBanner from './CardSectionDataEmpty/BillingBanner/SubscriptionBillingBanner'; import CardSectionUtils from './utils'; function CardSection() { @@ -23,11 +23,7 @@ function CardSection() { const cardMonth = useMemo(() => DateUtils.getMonthNames(preferredLocale)[(defaultCard?.accountData?.cardMonth ?? 1) - 1], [defaultCard?.accountData?.cardMonth, preferredLocale]); - const {title, subtitle, isError, shouldShowRedDotIndicator, shouldShowGreenDotIndicator} = CardSectionUtils.getBillingStatus( - translate, - preferredLocale, - defaultCard?.accountData?.cardNumber ?? '', - ); + const {title, subtitle, isError, icon, rightIcon} = CardSectionUtils.getBillingStatus(translate, defaultCard?.accountData?.cardNumber ?? ''); const shouldShowBanner = !!title || !!subtitle; @@ -40,12 +36,13 @@ function CardSection() { subtitleMuted banner={ shouldShowBanner && ( - ) } diff --git a/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/BillingBanner/BillingBanner.tsx b/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/BillingBanner/BillingBanner.tsx new file mode 100644 index 000000000000..cf11dfb1d2b6 --- /dev/null +++ b/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/BillingBanner/BillingBanner.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import type {ValueOf} from 'type-fest'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import Text from '@components/Text'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import type IconAsset from '@src/types/utils/IconAsset'; + +type BillingBannerProps = { + /** The title of the banner. */ + title: string | React.ReactNode; + + /** The subtitle of the banner. */ + subtitle: string | React.ReactNode; + + /** The icon to display in the banner. */ + icon: IconAsset; + + /** The type of brick road indicator to show. */ + brickRoadIndicator?: ValueOf; + + /** Styles to apply to the container. */ + style?: StyleProp; + + /** Styles to apply to the title. */ + titleStyle?: StyleProp; + + /** Styles to apply to the subtitle. */ + subtitleStyle?: StyleProp; + + /** An icon to be rendered instead of RBR/GBR */ + rightIcon?: IconAsset; +}; + +function BillingBanner({title, subtitle, icon, brickRoadIndicator, style, titleStyle, subtitleStyle, rightIcon}: BillingBannerProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + + return ( + + + + + {typeof title === 'string' ? {title} : title} + {typeof subtitle === 'string' ? {subtitle} : subtitle} + + {rightIcon ? ( + + ) : ( + !!brickRoadIndicator && ( + + ) + )} + + ); +} + +BillingBanner.displayName = 'BillingBanner'; + +export default BillingBanner; +export type {BillingBannerProps}; diff --git a/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/BillingBanner/SubscriptionBillingBanner.tsx b/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/BillingBanner/SubscriptionBillingBanner.tsx new file mode 100644 index 000000000000..925ef74aa87a --- /dev/null +++ b/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/BillingBanner/SubscriptionBillingBanner.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import * as Illustrations from '@components/Icon/Illustrations'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; +import type IconAsset from '@src/types/utils/IconAsset'; +import BillingBanner from './BillingBanner'; +import type {BillingBannerProps} from './BillingBanner'; + +type SubscriptionBillingBannerProps = Omit & { + isTrialActive?: boolean; + isError?: boolean; + icon?: IconAsset; +}; + +function SubscriptionBillingBanner({ + title, + subtitle, + rightIcon, + icon, + + isTrialActive, + isError, +}: SubscriptionBillingBannerProps) { + const styles = useThemeStyles(); + + const backgroundStyle = isTrialActive ? styles.trialBannerBackgroundColor : styles.hoveredComponentBG; + + const subtitleStyle = isTrialActive ? [] : styles.textSupporting; + + const iconAsset = icon ?? isError ? Illustrations.CreditCardEyes : Illustrations.CheckmarkCircle; + + return ( + + ); +} + +SubscriptionBillingBanner.displayName = 'SubscriptionBillingBanner'; + +export default SubscriptionBillingBanner; diff --git a/src/pages/settings/Subscription/CardSection/utils.ts b/src/pages/settings/Subscription/CardSection/utils.ts index 9628b2d229f4..5edf1399f0db 100644 --- a/src/pages/settings/Subscription/CardSection/utils.ts +++ b/src/pages/settings/Subscription/CardSection/utils.ts @@ -1,27 +1,28 @@ import {fromUnixTime} from 'date-fns'; +import * as Expensicons from '@components/Icon/Expensicons'; +import * as Illustrations from '@components/Icon/Illustrations'; import DateUtils from '@libs/DateUtils'; import type {Phrase, PhraseParameters} from '@libs/Localize'; import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import type {Fund} from '@src/types/onyx'; -import type Locale from '@src/types/onyx/Locale'; +import type IconAsset from '@src/types/utils/IconAsset'; type BillingStatusResult = { title?: string; subtitle?: string; isError?: boolean; - shouldShowRedDotIndicator?: boolean; - shouldShowGreenDotIndicator?: boolean; isTrialActive?: boolean; isRetryAvailable?: boolean; isAddButtonDark?: boolean; isAuthenticatingRequired?: boolean; + icon?: IconAsset; + rightIcon?: IconAsset; }; function getBillingStatus( translate: (phraseKey: TKey, ...phraseParameters: PhraseParameters>) => string, - locale: Locale, cardEnding: string, ): BillingStatusResult { const amountOwed = SubscriptionUtils.getAmountOwed(); @@ -38,7 +39,6 @@ function getBillingStatus( title: translate('subscription.billingBanner.outdatedInfo'), subtitle: translate('subscription.billingBanner.updateCardDataByDate', {date: endDateFormatted}), isError: true, - shouldShowRedDotIndicator: status.shouldShowRedDotIndicator, isRetryAvailable: true, }; @@ -47,7 +47,6 @@ function getBillingStatus( title: translate('subscription.billingBanner.outdatedInfo'), subtitle: translate('subscription.billingBanner.updatePaymentInformation'), isError: true, - shouldShowRedDotIndicator: status.shouldShowRedDotIndicator, }; case SubscriptionUtils.PAYMENT_STATUSES.OWNER_OF_POLICY_UNDER_INVOICING: @@ -55,7 +54,6 @@ function getBillingStatus( title: translate('subscription.billingBanner.outdatedInfo'), subtitle: translate('subscription.billingBanner.paymentPastDuePayByDate', {date: endDateFormatted}), isError: true, - shouldShowRedDotIndicator: status.shouldShowRedDotIndicator, isAddButtonDark: true, }; @@ -64,7 +62,6 @@ function getBillingStatus( title: translate('subscription.billingBanner.outdatedInfo'), subtitle: translate('subscription.billingBanner.paymentPastDue'), isError: true, - shouldShowRedDotIndicator: status.shouldShowRedDotIndicator, isAddButtonDark: true, }; @@ -73,7 +70,6 @@ function getBillingStatus( title: translate('subscription.billingBanner.cardCouldNotBeCharged'), subtitle: translate('subscription.billingBanner.cardOnDispute', {amountOwed, cardEnding}), isError: true, - shouldShowRedDotIndicator: status.shouldShowRedDotIndicator, isRetryAvailable: false, }; @@ -82,7 +78,6 @@ function getBillingStatus( title: translate('subscription.billingBanner.cardCouldNotBeCharged'), subtitle: translate('subscription.billingBanner.cardNotFullyAuthenticated', {cardEnding}), isError: true, - shouldShowRedDotIndicator: status.shouldShowRedDotIndicator, isAuthenticatingRequired: true, }; @@ -91,7 +86,6 @@ function getBillingStatus( title: translate('subscription.billingBanner.cardCouldNotBeCharged'), subtitle: translate('subscription.billingBanner.cardDeclinedDueToInsufficientFunds', {amountOwed}), isError: true, - shouldShowRedDotIndicator: status.shouldShowRedDotIndicator, isRetryAvailable: true, }; @@ -100,7 +94,6 @@ function getBillingStatus( title: translate('subscription.billingBanner.cardCouldNotBeCharged'), subtitle: translate('subscription.billingBanner.cardExpired', {amountOwed}), isError: true, - shouldShowRedDotIndicator: status.shouldShowRedDotIndicator, isRetryAvailable: true, }; @@ -108,8 +101,8 @@ function getBillingStatus( return { title: translate('subscription.billingBanner.cardExpiringSoon'), subtitle: translate('subscription.billingBanner.cardWillExpireAtTheEndOfMonth'), - isError: true, - shouldShowGreenDotIndicator: status.shouldShowGreenDotIndicator, + isError: false, + icon: Illustrations.CreditCardEyes, }; case SubscriptionUtils.PAYMENT_STATUSES.RETRY_BILLING_SUCCESS: @@ -117,7 +110,7 @@ function getBillingStatus( title: translate('subscription.billingBanner.succeeded'), subtitle: translate('subscription.billingBanner.billedSuccessfully'), isError: false, - shouldShowRedDotIndicator: status.shouldShowRedDotIndicator, + rightIcon: Expensicons.Close, }; case SubscriptionUtils.PAYMENT_STATUSES.RETRY_BILLING_ERROR: @@ -125,7 +118,6 @@ function getBillingStatus( title: translate('subscription.billingBanner.cardCouldNotBeCharged'), subtitle: translate('subscription.billingBanner.retryMessage'), isError: true, - shouldShowRedDotIndicator: status.shouldShowRedDotIndicator, }; case SubscriptionUtils.PAYMENT_STATUSES.GENERIC_API_ERROR: @@ -133,14 +125,11 @@ function getBillingStatus( title: translate('subscription.billingBanner.succeeded'), subtitle: translate('subscription.billingBanner.billedSuccessfully'), isError: false, - shouldShowRedDotIndicator: status.shouldShowRedDotIndicator, }; default: - break; + return {}; } - - return {}; } function getCardForSubscriptionBilling(): Fund | undefined { From d31b7e592bbe6679cb3708fff5db7cbc064691c1 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 21 Jun 2024 13:13:31 +0200 Subject: [PATCH 020/183] remove redundant BillingBanner component --- .../BillingBanner/BillingBanner.tsx | 20 +++-- .../SubscriptionBillingBanner.tsx | 0 .../Subscription/CardSection/CardSection.tsx | 2 +- .../BillingBanner/BillingBanner.tsx | 76 ------------------- 4 files changed, 16 insertions(+), 82 deletions(-) rename src/pages/settings/Subscription/CardSection/{CardSectionDataEmpty => }/BillingBanner/SubscriptionBillingBanner.tsx (100%) delete mode 100644 src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/BillingBanner/BillingBanner.tsx diff --git a/src/pages/settings/Subscription/CardSection/BillingBanner/BillingBanner.tsx b/src/pages/settings/Subscription/CardSection/BillingBanner/BillingBanner.tsx index 76e081c0c4c1..cf11dfb1d2b6 100644 --- a/src/pages/settings/Subscription/CardSection/BillingBanner/BillingBanner.tsx +++ b/src/pages/settings/Subscription/CardSection/BillingBanner/BillingBanner.tsx @@ -32,9 +32,12 @@ type BillingBannerProps = { /** Styles to apply to the subtitle. */ subtitleStyle?: StyleProp; + + /** An icon to be rendered instead of RBR/GBR */ + rightIcon?: IconAsset; }; -function BillingBanner({title, subtitle, icon, brickRoadIndicator, style, titleStyle, subtitleStyle}: BillingBannerProps) { +function BillingBanner({title, subtitle, icon, brickRoadIndicator, style, titleStyle, subtitleStyle, rightIcon}: BillingBannerProps) { const styles = useThemeStyles(); const theme = useTheme(); @@ -50,12 +53,18 @@ function BillingBanner({title, subtitle, icon, brickRoadIndicator, style, titleS {typeof title === 'string' ? {title} : title} {typeof subtitle === 'string' ? {subtitle} : subtitle} - - {!!brickRoadIndicator && ( + {rightIcon ? ( + ) : ( + !!brickRoadIndicator && ( + + ) )} ); @@ -64,3 +73,4 @@ function BillingBanner({title, subtitle, icon, brickRoadIndicator, style, titleS BillingBanner.displayName = 'BillingBanner'; export default BillingBanner; +export type {BillingBannerProps}; diff --git a/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/BillingBanner/SubscriptionBillingBanner.tsx b/src/pages/settings/Subscription/CardSection/BillingBanner/SubscriptionBillingBanner.tsx similarity index 100% rename from src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/BillingBanner/SubscriptionBillingBanner.tsx rename to src/pages/settings/Subscription/CardSection/BillingBanner/SubscriptionBillingBanner.tsx diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 124c2f256aea..7c88af8931de 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -9,9 +9,9 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import SubscriptionBillingBanner from './BillingBanner/SubscriptionBillingBanner'; import CardSectionActions from './CardSectionActions'; import CardSectionDataEmpty from './CardSectionDataEmpty'; -import SubscriptionBillingBanner from './CardSectionDataEmpty/BillingBanner/SubscriptionBillingBanner'; import CardSectionUtils from './utils'; function CardSection() { diff --git a/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/BillingBanner/BillingBanner.tsx b/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/BillingBanner/BillingBanner.tsx deleted file mode 100644 index cf11dfb1d2b6..000000000000 --- a/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/BillingBanner/BillingBanner.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; -import {View} from 'react-native'; -import type {ValueOf} from 'type-fest'; -import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; -import Text from '@components/Text'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import variables from '@styles/variables'; -import CONST from '@src/CONST'; -import type IconAsset from '@src/types/utils/IconAsset'; - -type BillingBannerProps = { - /** The title of the banner. */ - title: string | React.ReactNode; - - /** The subtitle of the banner. */ - subtitle: string | React.ReactNode; - - /** The icon to display in the banner. */ - icon: IconAsset; - - /** The type of brick road indicator to show. */ - brickRoadIndicator?: ValueOf; - - /** Styles to apply to the container. */ - style?: StyleProp; - - /** Styles to apply to the title. */ - titleStyle?: StyleProp; - - /** Styles to apply to the subtitle. */ - subtitleStyle?: StyleProp; - - /** An icon to be rendered instead of RBR/GBR */ - rightIcon?: IconAsset; -}; - -function BillingBanner({title, subtitle, icon, brickRoadIndicator, style, titleStyle, subtitleStyle, rightIcon}: BillingBannerProps) { - const styles = useThemeStyles(); - const theme = useTheme(); - - return ( - - - - - {typeof title === 'string' ? {title} : title} - {typeof subtitle === 'string' ? {subtitle} : subtitle} - - {rightIcon ? ( - - ) : ( - !!brickRoadIndicator && ( - - ) - )} - - ); -} - -BillingBanner.displayName = 'BillingBanner'; - -export default BillingBanner; -export type {BillingBannerProps}; From 73e3a9ce8d614f73d3c3cc427627dd9917aab2b5 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 21 Jun 2024 13:15:18 +0200 Subject: [PATCH 021/183] remove unnecessary edge case --- src/libs/SubscriptionUtils.ts | 8 -------- src/pages/settings/Subscription/CardSection/utils.ts | 7 ------- 2 files changed, 15 deletions(-) diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 5db52c172561..bc12951fee8e 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -298,14 +298,6 @@ function getSubscriptionStatus(): SubscriptionStatus { }; } - // 12. Generic API error - // if (false) { - // return { - // status: PAYMENT_STATUSES.GENERIC_API_ERROR, - // isError: true, - // }; - // } - return {}; } diff --git a/src/pages/settings/Subscription/CardSection/utils.ts b/src/pages/settings/Subscription/CardSection/utils.ts index 1f6d26e3f8a4..3cb9cb6d7675 100644 --- a/src/pages/settings/Subscription/CardSection/utils.ts +++ b/src/pages/settings/Subscription/CardSection/utils.ts @@ -120,13 +120,6 @@ function getBillingStatus( isError: true, }; - case SubscriptionUtils.PAYMENT_STATUSES.GENERIC_API_ERROR: - return { - title: translate('subscription.billingBanner.succeeded'), - subtitle: translate('subscription.billingBanner.billedSuccessfully'), - isError: false, - }; - default: return {}; } From 6d2cb3053902d2bd8d3d4830f9f23f5c5a1f467c Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 21 Jun 2024 13:28:20 +0200 Subject: [PATCH 022/183] remove unnecessary space in translation --- 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 3313b1fb9ff9..ec335c45ca38 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3235,7 +3235,7 @@ export default { cardOnDispute: ({amountOwed, cardEnding}) => `You disputed the ${amountOwed} charge on the card ending in ${cardEnding}. Your account will be locked until the dispute is resolved with your bank.`, succeeded: 'Success!', - billedSuccessfully: 'Your card has been billed successfully.', + billedSuccessfully: 'Your card has been billed successfully.', preTrial: { title: 'Start a free trial', subtitle: 'To get started, ', From 0a6905ccfa74a4d392dc796cf2db653a885bb2e8 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 21 Jun 2024 13:33:46 +0200 Subject: [PATCH 023/183] remove leftovers, apply suggested changes --- src/libs/SubscriptionUtils.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index bc12951fee8e..36f778ac9afb 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -115,6 +115,7 @@ Onyx.connect({ billingStatusSuccessful = value; }, + initWithStoredValues: false, }); let firstDayFreeTrial: OnyxEntry; @@ -161,8 +162,6 @@ function hasGracePeriodOverdue(): boolean { return !!billingGracePeriod && Date.now() < new Date(billingGracePeriod).getTime(); } -/// getGracePeriodEndDate - return formatted date - function getAmountOwed(): number { return amountOwed ?? 0; } @@ -197,7 +196,6 @@ function hasCardExpiringSoon(): boolean { return !billingStatus && card?.accountData?.cardMonth === new Date().getMonth() + 1; } -// hasRetryBillingError - based on request response function hasRetryBillingError(): boolean { return !!billingStatusFailed; } @@ -206,8 +204,6 @@ function isRetryBillingSuccessful(): boolean { return !!billingStatusSuccessful; } -// hasCardExpiredSoon - based on card - type SubscriptionStatus = { status?: string; isError?: boolean; From 70b30d4af14accf8d4ed0926d0f7bdbce68e1d96 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 21 Jun 2024 13:54:41 +0200 Subject: [PATCH 024/183] remove unnecessary empty line --- .../CardSection/BillingBanner/SubscriptionBillingBanner.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/settings/Subscription/CardSection/BillingBanner/SubscriptionBillingBanner.tsx b/src/pages/settings/Subscription/CardSection/BillingBanner/SubscriptionBillingBanner.tsx index 925ef74aa87a..84db69a758a3 100644 --- a/src/pages/settings/Subscription/CardSection/BillingBanner/SubscriptionBillingBanner.tsx +++ b/src/pages/settings/Subscription/CardSection/BillingBanner/SubscriptionBillingBanner.tsx @@ -17,7 +17,6 @@ function SubscriptionBillingBanner({ subtitle, rightIcon, icon, - isTrialActive, isError, }: SubscriptionBillingBannerProps) { From 30581bf2f8f3c2065d766876d9a8e001cc72342b Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Fri, 21 Jun 2024 19:17:43 +0530 Subject: [PATCH 025/183] Implemented NetSuite export pages --- src/CONST.ts | 124 ++++++++ src/ROUTES.ts | 57 ++++ src/SCREENS.ts | 13 + src/components/MenuItem.tsx | 35 ++- src/languages/en.ts | 88 ++++++ src/languages/es.ts | 88 ++++++ .../UpdateNetSuiteGenericTypeParams.ts | 8 + src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 32 +- .../ModalStackNavigators/index.tsx | 26 ++ .../FULL_SCREEN_TO_RHP_MAPPING.ts | 13 + src/libs/Navigation/linkingConfig/config.ts | 39 +++ src/libs/Navigation/types.ts | 44 +++ src/libs/Permissions.ts | 5 + src/libs/PolicyUtils.ts | 70 +++++ src/libs/actions/Policy/Policy.ts | 5 + .../actions/connections/NetSuiteCommands.ts | 274 ++++++++++++++++++ src/libs/actions/connections/index.ts | 9 +- .../export/NetSuiteDateSelectPage.tsx | 74 +++++ .../NetSuiteExportConfigurationPage.tsx | 265 +++++++++++++++++ ...iteExportExpensesDestinationSelectPage.tsx | 75 +++++ ...nsesJournalPostingPreferenceSelectPage.tsx | 74 +++++ .../export/NetSuiteExportExpensesPage.tsx | 173 +++++++++++ ...ExportExpensesPayableAccountSelectPage.tsx | 93 ++++++ ...NetSuiteExportExpensesVendorSelectPage.tsx | 84 ++++++ .../export/NetSuiteInvoiceItemSelectPage.tsx | 74 +++++ .../NetSuitePreferredExporterSelectPage.tsx | 104 +++++++ ...eProvincialTaxPostingAccountSelectPage.tsx | 83 ++++++ .../NetSuiteReceivableAccountSelectPage.tsx | 76 +++++ .../NetSuiteTaxPostingAccountSelectPage.tsx | 82 ++++++ src/types/onyx/Policy.ts | 48 +-- 31 files changed, 2206 insertions(+), 30 deletions(-) create mode 100644 src/libs/API/parameters/UpdateNetSuiteGenericTypeParams.ts create mode 100644 src/libs/actions/connections/NetSuiteCommands.ts create mode 100644 src/pages/workspace/accounting/netsuite/export/NetSuiteDateSelectPage.tsx create mode 100644 src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx create mode 100644 src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesDestinationSelectPage.tsx create mode 100644 src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx create mode 100644 src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx create mode 100644 src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPayableAccountSelectPage.tsx create mode 100644 src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx create mode 100644 src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemSelectPage.tsx create mode 100644 src/pages/workspace/accounting/netsuite/export/NetSuitePreferredExporterSelectPage.tsx create mode 100644 src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx create mode 100644 src/pages/workspace/accounting/netsuite/export/NetSuiteReceivableAccountSelectPage.tsx create mode 100644 src/pages/workspace/accounting/netsuite/export/NetSuiteTaxPostingAccountSelectPage.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 71ef5e26f7ae..8055125d5e76 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -365,6 +365,7 @@ const CONST = { XERO_ON_NEW_EXPENSIFY: 'xeroOnNewExpensify', NETSUITE_ON_NEW_EXPENSIFY: 'netsuiteOnNewExpensify', REPORT_FIELDS_FEATURE: 'reportFieldsFeature', + NETSUITE_USA_TAX: 'netsuiteUsaTax', }, BUTTON_STATES: { DEFAULT: 'default', @@ -1359,6 +1360,129 @@ const CONST = { REPORT_SUBMITTED: 'REPORT_SUBMITTED', }, + NETSUITE_CONFIG: { + EXPORTER: 'exporter', + EXPORT_DATE: 'exportDate', + REIMBURSABLE_EXPENSES_EXPORT_DESTINATION: 'reimbursableExpensesExportDestination', + NON_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION: 'nonreimbursableExpensesExportDestination', + DEFAULT_VENDOR: 'defaultVendor', + REIMBURSABLE_PAYABLE_ACCOUNT: 'reimbursablePayableAccount', + PAYABLE_ACCT: 'payableAcct', + JOURNAL_POSTING_PREFERENCE: 'journalPostingPreference', + RECEIVABLE_ACCOUNT: 'receivableAccount', + INVOICE_ITEM_PREFERENCE: 'invoiceItemPreference', + INVOICE_ITEM: 'invoiceItem', + TAX_POSTING_ACCOUNT: 'taxPostingAccount', + PROVINCIAL_TAX_POSTING_ACCOUNT: 'provincialTaxPostingAccount', + ALLOW_FOREIGN_CURRENCY: 'allowForeignCurrency', + EXPORT_TO_NEXT_OPEN_PERIOD: 'exportToNextOpenPeriod', + }, + + NETSUITE_EXPORT_DATE: { + SUBMITTED: 'SUBMITTED', + EXPORTED: 'EXPORTED', + LAST_EXPENSE: 'LAST_EXPENSE', + }, + + NETSUITE_EXPORT_DESTINATION: { + EXPENSE_REPORT: 'EXPENSE_REPORT', + VENDOR_BILL: 'VENDOR_BILL', + JOURNAL_ENTRY: 'JOURNAL_ENTRY', + }, + + NETSUITE_INVOICE_ITEM_PREFERENCE: { + CREATE: 'create', + SELECT: 'select', + }, + + NETSUITE_JOURNAL_POSTING_PREFERENCE: { + JOURNALS_POSTING_TOTAL_LINE: 'JOURNALS_POSTING_TOTAL_LINE', + JOURNALS_POSTING_INDIVIDUAL_LINE: 'JOURNALS_POSTING_INDIVIDUAL_LINE', + }, + + NETSUITE_EXPENSE_TYPE: { + REIMBURSABLE: 'reimbursable', + NON_REIMBURSABLE: 'nonreimbursable', + }, + + /** + * Countries where tax setting is permitted (Strings are in the format of Netsuite's Country type/enum) + * + * Should mirror the list on the OldDot. + */ + NETSUITE_TAX_COUNTRIES: [ + '_canada', + '_unitedKingdomGB', + '_unitedKingdom', + '_australia', + '_southAfrica', + '_india', + '_france', + '_netherlands', + '_germany', + '_singapore', + '_spain', + '_ireland', + '_denmark', + '_brazil', + '_japan', + '_philippines', + '_china', + '_argentina', + '_newZealand', + '_switzerland', + '_sweden', + '_portugal', + '_mexico', + '_israel', + '_thailand', + '_czechRepublic', + '_egypt', + '_ghana', + '_indonesia', + '_iranIslamicRepublicOf', + '_jordan', + '_kenya', + '_kuwait', + '_lebanon', + '_malaysia', + '_morocco', + '_myanmar', + '_nigeria', + '_pakistan', + '_saudiArabia', + '_sriLanka', + '_unitedArabEmirates', + '_vietnam', + '_austria', + '_bulgaria', + '_greece', + '_cyprus', + '_norway', + '_romania', + '_poland', + '_hongKong', + '_luxembourg', + '_lithuania', + '_malta', + '_finland', + '_koreaRepublicOf', + '_italy', + '_georgia', + '_hungary', + '_latvia', + '_estonia', + '_slovenia', + '_serbia', + '_croatiaHrvatska', + '_belgium', + '_turkey', + '_taiwan', + '_azerbaijan', + '_slovakRepublic', + '_costaRica', + ] as string[], + QUICKBOOKS_EXPORT_DATE: { LAST_EXPENSE: 'LAST_EXPENSE', REPORT_EXPORTED: 'REPORT_EXPORTED', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index c1fdd68951fa..6f3d8d5ee9cd 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -908,6 +908,63 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import/taxes', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import/taxes` as const, }, + POLICY_ACCOUNTING_NETSUITE_EXPORT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/` as const, + }, + POLICY_ACCOUNTING_NETSUITE_PREFERRED_EXPORTER_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/preferred-exporter/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/preferred-exporter/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_DATE_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/date/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/date/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/expenses/:expenseType', + getRoute: (policyID: string, expenseType: ValueOf) => + `settings/workspaces/${policyID}/connections/netsuite/export/expenses/${expenseType}` as const, + }, + POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_DESTINATION_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/expenses/:expenseType/destination/select', + getRoute: (policyID: string, expenseType: ValueOf) => + `settings/workspaces/${policyID}/connections/netsuite/export/expenses/${expenseType}/destination/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_VENDOR_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/expenses/:expenseType/vendor/select', + getRoute: (policyID: string, expenseType: ValueOf) => + `settings/workspaces/${policyID}/connections/netsuite/export/expenses/${expenseType}/vendor/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_PAYABLE_ACCOUNT_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/expenses/:expenseType/payable-account/select', + getRoute: (policyID: string, expenseType: ValueOf) => + `settings/workspaces/${policyID}/connections/netsuite/export/expenses/${expenseType}/payable-account/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_JOURNAL_POSTING_PREFERENCE_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/expenses/:expenseType/journal-posting-preference/select', + getRoute: (policyID: string, expenseType: ValueOf) => + `settings/workspaces/${policyID}/connections/netsuite/export/expenses/${expenseType}/journal-posting-preference/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_RECEIVABLE_ACCOUNT_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/receivable-account/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/receivable-account/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_INVOICE_ITEM_PREFERENCE_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/invoice-item-preference/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/invoice-item-preference/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_INVOICE_ITEM_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/invoice-item-preference/invoice-item/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/invoice-item-preference/invoice-item/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_TAX_POSTING_ACCOUNT_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/tax-posting-account/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/tax-posting-account/select` as const, + }, + POLICY_ACCOUNTING_NETSUITE_PROVINCIAL_TAX_POSTING_ACCOUNT_SELECT: { + route: 'settings/workspaces/:policyID/connections/netsuite/export/provincial-tax-posting-account/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/export/provincial-tax-posting-account/select` as const, + }, } as const; /** diff --git a/src/SCREENS.ts b/src/SCREENS.ts index f884cca94ef5..1e14e78f46e7 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -263,6 +263,19 @@ const SCREENS = { XERO_EXPORT_PREFERRED_EXPORTER_SELECT: 'Workspace_Accounting_Xero_Export_Preferred_Exporter_Select', XERO_BILL_PAYMENT_ACCOUNT_SELECTOR: 'Policy_Accounting_Xero_Bill_Payment_Account_Selector', XERO_EXPORT_BANK_ACCOUNT_SELECT: 'Policy_Accounting_Xero_Export_Bank_Account_Select', + NETSUITE_EXPORT: 'Policy_Accounting_NetSuite_Export', + NETSUITE_PREFERRED_EXPORTER_SELECT: 'Policy_Accounting_NetSuite_Preferred_Exporter_Select', + NETSUITE_DATE_SELECT: 'Policy_Accounting_NetSuite_Date_Select', + NETSUITE_EXPORT_EXPENSES: 'Policy_Accounting_NetSuite_Export_Expenses', + NETSUITE_EXPORT_EXPENSES_DESTINATION_SELECT: 'Policy_Accounting_NetSuite_Export_Expenses_Destination_Select', + NETSUITE_EXPORT_EXPENSES_VENDOR_SELECT: 'Policy_Accounting_NetSuite_Export_Expenses_Vendor_Select', + NETSUITE_EXPORT_EXPENSES_PAYABLE_ACCOUNT_SELECT: 'Policy_Accounting_NetSuite_Export_Expenses_Payable_Account_Select', + NETSUITE_EXPORT_EXPENSES_JOURNAL_POSTING_PREFERENCE_SELECT: 'Policy_Accounting_NetSuite_Export_Expenses_Journal_Posting_Preference_Select', + NETSUITE_RECEIVABLE_ACCOUNT_SELECT: 'Policy_Accounting_NetSuite_Receivable_Account_Select', + NETSUITE_INVOICE_ITEM_PREFERENCE_SELECT: 'Policy_Accounting_NetSuite_Invoice_Item_Preference_Select', + NETSUITE_INVOICE_ITEM_SELECT: 'Policy_Accounting_NetSuite_Invoice_Item_Select', + NETSUITE_TAX_POSTING_ACCOUNT_SELECT: 'Policy_Accounting_NetSuite_Tax_Posting_Account_Select', + NETSUITE_PROVINCIAL_TAX_POSTING_ACCOUNT_SELECT: 'Policy_Accounting_NetSuite_Provincial_Tax_Posting_Account_Select', }, INITIAL: 'Workspace_Initial', PROFILE: 'Workspace_Profile', diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index fa58b5cd5f5f..7d355ec66391 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -254,6 +254,9 @@ type MenuItemBaseProps = { /** Whether should render title as HTML or as Text */ shouldParseTitle?: boolean; + /** Whether should render helper text as HTML or as Text */ + shouldParseHelperText?: boolean; + /** Should check anonymous user in onPress function */ shouldCheckActionAllowedOnPress?: boolean; @@ -372,6 +375,7 @@ function MenuItem( isAnonymousAction = false, shouldBlockSelection = false, shouldParseTitle = false, + shouldParseHelperText = false, shouldCheckActionAllowedOnPress = true, onSecondaryInteraction, titleWithTooltips, @@ -429,6 +433,14 @@ function MenuItem( return parser.replace(title, {shouldEscapeText}); }, [title, shouldParseTitle, shouldEscapeText]); + const helperHtml = useMemo(() => { + if (!helperText || !shouldParseTitle) { + return ''; + } + const parser = new ExpensiMark(); + return parser.replace(helperText, {shouldEscapeText}); + }, [helperText, shouldParseTitle, shouldEscapeText]); + const processedTitle = useMemo(() => { let titleToWrap = ''; if (shouldRenderAsHTML) { @@ -442,6 +454,20 @@ function MenuItem( return titleToWrap ? `${titleToWrap}` : ''; }, [title, shouldRenderAsHTML, shouldParseTitle, html]); + const processedHelperText = useMemo(() => { + let textToWrap = ''; + // TODO: Discuss this. + // if (shouldRenderAsHTML) { + // textToWrap = helperText ? convertToLTR(helperText) : ''; + // } + + if (shouldParseHelperText) { + textToWrap = helperHtml; + } + + return textToWrap ? `${textToWrap}` : ''; + }, [shouldParseHelperText, helperHtml]); + const hasPressableRightComponent = iconRight || (shouldShowRightComponent && rightComponent); const renderTitleContent = () => { @@ -767,7 +793,14 @@ function MenuItem( )} - {!!helperText && {helperText}} + {!!helperText && + (shouldParseHelperText ? ( + + + + ) : ( + {helperText} + ))}
diff --git a/src/languages/en.ts b/src/languages/en.ts index 6a9a6e7ccab9..8b5dcfbddfa2 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2176,6 +2176,94 @@ export default { noAccountsFound: 'No accounts found', noAccountsFoundDescription: 'Add the account in Xero and sync the connection again.', }, + netsuite: { + export: 'Export', + exportDescription: 'Configure how Expensify data exports to NetSuite.', + exportReimbursable: 'Export reimbursable expenses as', + exportNonReimbursable: 'Export non-reimbursable expenses as', + preferredExporter: 'Preferred exporter', + exportInvoices: 'Export invoices to', + journalEntriesTaxPostingAccount: 'Journal entries tax posting account', + journalEntriesProvTaxPostingAccount: 'Journal entries provincial tax posting account', + foreignCurrencyAmount: 'Export foreign currency amount', + exportToNextOpenPeriod: 'Export to next open period', + exportPreferredExporterNote: + 'The preferred exporter can be any workspace admin, but must be a domain admin if you set different export accounts for individual company cards in domain settings.', + exportPreferredExporterSubNote: 'Once set, the preferred exporter will see reports for export in their account.', + exportAs: 'Export as', + defaultVendor: 'Default vendor', + nonReimbursableJournalPostingAccount: 'Non-reimbursable journal posting account', + reimbursableJournalPostingAccount: 'Reimbursable journal posting account', + journalPostingPreference: { + label: 'Journal entries posting preference', + values: { + [CONST.NETSUITE_JOURNAL_POSTING_PREFERENCE.JOURNALS_POSTING_INDIVIDUAL_LINE]: 'Single, itemized entry for each report', + [CONST.NETSUITE_JOURNAL_POSTING_PREFERENCE.JOURNALS_POSTING_TOTAL_LINE]: 'Single entry for each individual expense', + }, + }, + invoiceItem: { + label: 'Invoice item', + values: { + [CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.CREATE]: 'Create one for me', + [CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.SELECT]: 'Select existing', + }, + }, + exportDate: { + label: 'Export date', + description: 'Use this date when exporting reports to NetSuite.', + values: { + [CONST.NETSUITE_EXPORT_DATE.LAST_EXPENSE]: { + label: 'Date of last expense', + description: 'Date of the most recent expense on the report.', + }, + [CONST.NETSUITE_EXPORT_DATE.EXPORTED]: { + label: 'Export date', + description: 'Date the report was exported to NetSuite.', + }, + [CONST.NETSUITE_EXPORT_DATE.SUBMITTED]: { + label: 'Submitted date', + description: 'Date the report was submitted for approval.', + }, + }, + }, + exportDestination: { + values: { + [CONST.NETSUITE_EXPORT_DESTINATION.EXPENSE_REPORT]: { + label: 'Expense reports', + reimbursableDescription: 'Reimbursable expenses will export as expense reports to NetSuite.', + nonReimbursableDescription: 'Non-reimbursable expenses will export as expense reports to NetSuite.', + }, + [CONST.NETSUITE_EXPORT_DESTINATION.VENDOR_BILL]: { + label: 'Vendor bills', + reimbursableDescription: + 'Reimbursable expenses will export as bills payable to the NetSuite vendor specified below.\n' + + '\n' + + 'If you’d like to set a specific vendor for each card, go to *Settings > Domains > Company Cards*.', + nonReimbursableDescription: + 'Non-reimbursable expenses will export as bills payable to the NetSuite vendor specified below.\n' + + '\n' + + 'If you’d like to set a specific vendor for each card, go to *Settings > Domains > Company Cards*.', + }, + [CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY]: { + label: 'Journal Entries', + reimbursableDescription: + 'Reimbursable expenses will export as journal entries to the NetSuite account specified below.\n' + + '\n' + + 'If you’d like to set a specific vendor for each card, go to *Settings > Domains > Company Cards*.', + nonReimbursableDescription: + 'Non-reimbursable expenses will export as journal entries to the NetSuite account specified below.\n' + + '\n' + + 'If you’d like to set a specific vendor for each card, go to *Settings > Domains > Company Cards*.', + }, + }, + }, + noAccountsFound: 'No accounts found', + noAccountsFoundDescription: 'Add the account in NetSuite and sync the connection again.', + noVendorsFound: 'No vendors found', + noVendorsFoundDescription: 'Add vendors in NetSuite and sync the connection again.', + noItemsFound: 'No invoice items found', + noItemsFoundDescription: 'Add invoice items in NetSuite and sync the connection again.', + }, type: { free: 'Free', control: 'Control', diff --git a/src/languages/es.ts b/src/languages/es.ts index f007c1211190..234eebfd54d0 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2212,6 +2212,94 @@ export default { noAccountsFound: 'No se ha encontrado ninguna cuenta', noAccountsFoundDescription: 'Añade la cuenta en Xero y sincroniza de nuevo la conexión.', }, + netsuite: { + export: 'Export', + exportDescription: 'Configure how Expensify data exports to NetSuite.', + exportReimbursable: 'Export reimbursable expenses as', + exportNonReimbursable: 'Export non-reimbursable expenses as', + preferredExporter: 'Preferred exporter', + exportInvoices: 'Export invoices to', + journalEntriesTaxPostingAccount: 'Journal entries tax posting account', + journalEntriesProvTaxPostingAccount: 'Journal entries provincial tax posting account', + foreignCurrencyAmount: 'Export foreign currency amount', + exportToNextOpenPeriod: 'Export to next open period', + exportPreferredExporterNote: + 'The preferred exporter can be any workspace admin, but must be a domain admin if you set different export accounts for individual company cards in domain settings.', + exportPreferredExporterSubNote: 'Once set, the preferred exporter will see reports for export in their account.', + exportAs: 'Export as', + defaultVendor: 'Default vendor', + nonReimbursableJournalPostingAccount: 'Non-reimbursable journal posting account', + reimbursableJournalPostingAccount: 'Reimbursable journal posting account', + journalPostingPreference: { + label: 'Journal entries posting preference', + values: { + [CONST.NETSUITE_JOURNAL_POSTING_PREFERENCE.JOURNALS_POSTING_INDIVIDUAL_LINE]: 'Single, itemized entry for each report', + [CONST.NETSUITE_JOURNAL_POSTING_PREFERENCE.JOURNALS_POSTING_TOTAL_LINE]: 'Single entry for each individual expense', + }, + }, + invoiceItem: { + label: 'Invoice item', + values: { + [CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.CREATE]: 'Create one for me', + [CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.SELECT]: 'Select existing', + }, + }, + exportDate: { + label: 'Export date', + description: 'Use this date when exporting reports to NetSuite.', + values: { + [CONST.NETSUITE_EXPORT_DATE.LAST_EXPENSE]: { + label: 'Date of last expense', + description: 'Date of the most recent expense on the report.', + }, + [CONST.NETSUITE_EXPORT_DATE.EXPORTED]: { + label: 'Export date', + description: 'Date the report was exported to NetSuite.', + }, + [CONST.NETSUITE_EXPORT_DATE.SUBMITTED]: { + label: 'Submitted date', + description: 'Date the report was submitted for approval.', + }, + }, + }, + exportDestination: { + values: { + [CONST.NETSUITE_EXPORT_DESTINATION.EXPENSE_REPORT]: { + label: 'Expense reports', + reimbursableDescription: 'Reimbursable expenses will export as expense reports to NetSuite.', + nonReimbursableDescription: 'Non-reimbursable expenses will export as expense reports to NetSuite.', + }, + [CONST.NETSUITE_EXPORT_DESTINATION.VENDOR_BILL]: { + label: 'Vendor bills', + reimbursableDescription: + 'Reimbursable expenses will export as bills payable to the NetSuite vendor specified below.\n' + + '\n' + + 'If you’d like to set a specific vendor for each card, go to *Settings > Domains > Company Cards*.', + nonReimbursableDescription: + 'Non-reimbursable expenses will export as bills payable to the NetSuite vendor specified below.\n' + + '\n' + + 'If you’d like to set a specific vendor for each card, go to *Settings > Domains > Company Cards*.', + }, + [CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY]: { + label: 'Journal Entries', + reimbursableDescription: + 'Reimbursable expenses will export as journal entries to the NetSuite account specified below.\n' + + '\n' + + 'If you’d like to set a specific vendor for each card, go to *Settings > Domains > Company Cards*.', + nonReimbursableDescription: + 'Non-reimbursable expenses will export as journal entries to the NetSuite account specified below.\n' + + '\n' + + 'If you’d like to set a specific vendor for each card, go to *Settings > Domains > Company Cards*.', + }, + }, + }, + noAccountsFound: 'No accounts found', + noAccountsFoundDescription: 'Add the account in NetSuite and sync the connection again.', + noVendorsFound: 'No vendors found', + noVendorsFoundDescription: 'Add vendors in NetSuite and sync the connection again.', + noItemsFound: 'No invoice items found', + noItemsFoundDescription: 'Add invoice items in NetSuite and sync the connection again.', + }, type: { free: 'Gratis', control: 'Control', diff --git a/src/libs/API/parameters/UpdateNetSuiteGenericTypeParams.ts b/src/libs/API/parameters/UpdateNetSuiteGenericTypeParams.ts new file mode 100644 index 000000000000..57979b3fb0bb --- /dev/null +++ b/src/libs/API/parameters/UpdateNetSuiteGenericTypeParams.ts @@ -0,0 +1,8 @@ +type UpdateNetSuiteGenericTypeParams = { + [K2 in K]: Type; +} & { + policyID: string; + authToken: string; +}; + +export default UpdateNetSuiteGenericTypeParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 26df06fc6294..4d8d46238035 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -231,3 +231,4 @@ export type {default as UpdateSubscriptionAutoRenewParams} from './UpdateSubscri export type {default as UpdateSubscriptionAddNewUsersAutomaticallyParams} from './UpdateSubscriptionAddNewUsersAutomaticallyParams'; export type {default as GenerateSpotnanaTokenParams} from './GenerateSpotnanaTokenParams'; export type {default as UpdateSubscriptionSizeParams} from './UpdateSubscriptionSizeParams'; +export type {default as UpdateNetSuiteGenericTypeParams} from './UpdateNetSuiteGenericTypeParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 8f093ee827c3..7861f0efdfd2 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -227,6 +227,21 @@ const WRITE_COMMANDS = { UPDATE_SUBSCRIPTION_AUTO_RENEW: 'UpdateSubscriptionAutoRenew', UPDATE_SUBSCRIPTION_ADD_NEW_USERS_AUTOMATICALLY: 'UpdateSubscriptionAddNewUsersAutomatically', UPDATE_SUBSCRIPTION_SIZE: 'UpdateSubscriptionSize', + UPDATE_NETSUITE_EXPORTER: 'UpdateNetSuiteExporter', + UPDATE_NETSUITE_EXPORT_DATE: 'UpdateNetSuiteExportDate', + UPDATE_NETSUITE_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION: 'UpdateNetSuiteReimbursableExpensesExportDestination', + UPDATE_NETSUITE_NONREIMBURSABLE_EXPENSES_EXPORT_DESTINATION: 'UpdateNetSuiteNonreimbursableExpensesExportDestination', + UPDATE_NETSUITE_DEFAULT_VENDOR: 'UpdateNetSuiteDefaultVendor', + UPDATE_NETSUITE_REIMBURSABLE_PAYABLE_ACCOUNT: 'UpdateNetSuiteReimbursablePayableAccount', + UPDATE_NETSUITE_PAYABLE_ACCT: 'UpdateNetSuitePayableAcct', + UPDATE_NETSUITE_JOURNAL_POSTING_PREFERENCE: 'UpdateNetSuiteJournalPostingPreference', + UPDATE_NETSUITE_RECEIVABLE_ACCOUNT: 'UpdateNetSuiteReceivableAccount', + UPDATE_NETSUITE_INVOICE_ITEM_PREFERENCE: 'UpdateNetSuiteInvoiceItemPreference', + UPDATE_NETSUITE_INVOICE_ITEM: 'UpdateNetSuiteInvoiceItem', + UPDATE_NETSUITE_PROVINCIAL_TAX_POSTING_ACCOUNT: 'UpdateNetSuiteProvincialTaxPostingAccount', + UPDATE_NETSUITE_TAX_POSTING_ACCOUNT: 'UpdateNetSuiteTaxPostingAccount', + UPDATE_NETSUITE_ALLOW_FOREIGN_CURRENCY: 'UpdateNetSuiteAllowForeignCurrency', + UPDATE_NETSUITE_EXPORT_TO_NEXT_OPEN_PERIOD: 'UpdateNetSuiteExportToNextOpenPeriod', } as const; type WriteCommand = ValueOf; @@ -431,7 +446,6 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY]: Parameters.SetPolicyDistanceRatesDefaultCategoryParams; [WRITE_COMMANDS.ENABLE_DISTANCE_REQUEST_TAX]: Parameters.SetPolicyDistanceRatesDefaultCategoryParams; - // eslint-disable-next-line @typescript-eslint/no-explicit-any [WRITE_COMMANDS.UPDATE_POLICY_CONNECTION_CONFIG]: Parameters.UpdatePolicyConnectionConfigParams; [WRITE_COMMANDS.UPDATE_MANY_POLICY_CONNECTION_CONFIGS]: Parameters.UpdateManyPolicyConnectionConfigurationsParams; [WRITE_COMMANDS.REMOVE_POLICY_CONNECTION]: Parameters.RemovePolicyConnectionParams; @@ -455,6 +469,22 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_AUTO_RENEW]: Parameters.UpdateSubscriptionAutoRenewParams; [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_ADD_NEW_USERS_AUTOMATICALLY]: Parameters.UpdateSubscriptionAddNewUsersAutomaticallyParams; [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_SIZE]: Parameters.UpdateSubscriptionSizeParams; + + [WRITE_COMMANDS.UPDATE_NETSUITE_EXPORTER]: Parameters.UpdateNetSuiteGenericTypeParams<'email', string>; + [WRITE_COMMANDS.UPDATE_NETSUITE_EXPORT_DATE]: Parameters.UpdateNetSuiteGenericTypeParams<'value', ValueOf>; + [WRITE_COMMANDS.UPDATE_NETSUITE_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION]: Parameters.UpdateNetSuiteGenericTypeParams<'value', ValueOf>; + [WRITE_COMMANDS.UPDATE_NETSUITE_NONREIMBURSABLE_EXPENSES_EXPORT_DESTINATION]: Parameters.UpdateNetSuiteGenericTypeParams<'value', ValueOf>; + [WRITE_COMMANDS.UPDATE_NETSUITE_DEFAULT_VENDOR]: Parameters.UpdateNetSuiteGenericTypeParams<'vendorID', string>; + [WRITE_COMMANDS.UPDATE_NETSUITE_REIMBURSABLE_PAYABLE_ACCOUNT]: Parameters.UpdateNetSuiteGenericTypeParams<'bankAccountID', string>; + [WRITE_COMMANDS.UPDATE_NETSUITE_PAYABLE_ACCT]: Parameters.UpdateNetSuiteGenericTypeParams<'bankAccountID', string>; + [WRITE_COMMANDS.UPDATE_NETSUITE_JOURNAL_POSTING_PREFERENCE]: Parameters.UpdateNetSuiteGenericTypeParams<'value', ValueOf>; + [WRITE_COMMANDS.UPDATE_NETSUITE_RECEIVABLE_ACCOUNT]: Parameters.UpdateNetSuiteGenericTypeParams<'bankAccountID', string>; + [WRITE_COMMANDS.UPDATE_NETSUITE_INVOICE_ITEM_PREFERENCE]: Parameters.UpdateNetSuiteGenericTypeParams<'value', ValueOf>; + [WRITE_COMMANDS.UPDATE_NETSUITE_INVOICE_ITEM]: Parameters.UpdateNetSuiteGenericTypeParams<'itemID', string>; + [WRITE_COMMANDS.UPDATE_NETSUITE_PROVINCIAL_TAX_POSTING_ACCOUNT]: Parameters.UpdateNetSuiteGenericTypeParams<'bankAccountID', string>; + [WRITE_COMMANDS.UPDATE_NETSUITE_TAX_POSTING_ACCOUNT]: Parameters.UpdateNetSuiteGenericTypeParams<'bankAccountID', string>; + [WRITE_COMMANDS.UPDATE_NETSUITE_ALLOW_FOREIGN_CURRENCY]: Parameters.UpdateNetSuiteGenericTypeParams<'enabled', boolean>; + [WRITE_COMMANDS.UPDATE_NETSUITE_EXPORT_TO_NEXT_OPEN_PERIOD]: Parameters.UpdateNetSuiteGenericTypeParams<'enabled', boolean>; }; const READ_COMMANDS = { diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 0577fdcfc5aa..b70e2e3a1f41 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -317,6 +317,32 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/xero/advanced/XeroBillPaymentAccountSelectorPage').default as React.ComponentType, + + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT]: () => require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_PREFERRED_EXPORTER_SELECT]: () => + require('../../../../pages/workspace/accounting/netsuite/export/NetSuitePreferredExporterSelectPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_DATE_SELECT]: () => require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteDateSelectPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES]: () => + require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES_DESTINATION_SELECT]: () => + require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesDestinationSelectPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES_VENDOR_SELECT]: () => + require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES_PAYABLE_ACCOUNT_SELECT]: () => + require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPayableAccountSelectPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES_JOURNAL_POSTING_PREFERENCE_SELECT]: () => + require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_RECEIVABLE_ACCOUNT_SELECT]: () => + require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteReceivableAccountSelectPage').default as React.ComponentType, + // [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_INVOICE_ITEM_PREFERENCE_SELECT]: () => + // require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_INVOICE_ITEM_SELECT]: () => + require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemSelectPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_TAX_POSTING_ACCOUNT_SELECT]: () => + require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteTaxPostingAccountSelectPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_PROVINCIAL_TAX_POSTING_ACCOUNT_SELECT]: () => + require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage').default as React.ComponentType, + [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage').default as React.ComponentType, [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index f91d290639ff..6a4b971723c3 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -54,6 +54,19 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.ACCOUNTING.XERO_EXPORT_PREFERRED_EXPORTER_SELECT, SCREENS.WORKSPACE.ACCOUNTING.XERO_BILL_PAYMENT_ACCOUNT_SELECTOR, SCREENS.WORKSPACE.ACCOUNTING.XERO_EXPORT_BANK_ACCOUNT_SELECT, + SCREENS.WORKSPACE.ACCOUNTING.XERO_EXPORT, + SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_PREFERRED_EXPORTER_SELECT, + SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_DATE_SELECT, + SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES, + SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES_DESTINATION_SELECT, + SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES_VENDOR_SELECT, + SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES_PAYABLE_ACCOUNT_SELECT, + SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES_JOURNAL_POSTING_PREFERENCE_SELECT, + SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_RECEIVABLE_ACCOUNT_SELECT, + SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_INVOICE_ITEM_PREFERENCE_SELECT, + SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_INVOICE_ITEM_SELECT, + SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_TAX_POSTING_ACCOUNT_SELECT, + SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_PROVINCIAL_TAX_POSTING_ACCOUNT_SELECT, ], [SCREENS.WORKSPACE.TAXES]: [ SCREENS.WORKSPACE.TAXES_SETTINGS, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 1b4288a9b3a9..48e7d3e90db7 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -337,6 +337,45 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.ACCOUNTING.XERO_INVOICE_ACCOUNT_SELECTOR]: {path: ROUTES.POLICY_ACCOUNTING_XERO_INVOICE_SELECTOR.route}, [SCREENS.WORKSPACE.ACCOUNTING.XERO_EXPORT_PREFERRED_EXPORTER_SELECT]: {path: ROUTES.POLICY_ACCOUNTING_XERO_PREFERRED_EXPORTER_SELECT.route}, [SCREENS.WORKSPACE.ACCOUNTING.XERO_BILL_PAYMENT_ACCOUNT_SELECTOR]: {path: ROUTES.POLICY_ACCOUNTING_XERO_BILL_PAYMENT_ACCOUNT_SELECTOR.route}, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT]: { + path: ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.route, + }, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_PREFERRED_EXPORTER_SELECT]: { + path: ROUTES.POLICY_ACCOUNTING_NETSUITE_PREFERRED_EXPORTER_SELECT.route, + }, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_DATE_SELECT]: { + path: ROUTES.POLICY_ACCOUNTING_NETSUITE_DATE_SELECT.route, + }, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES]: { + path: ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES.route, + }, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES_DESTINATION_SELECT]: { + path: ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_DESTINATION_SELECT.route, + }, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES_VENDOR_SELECT]: { + path: ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_VENDOR_SELECT.route, + }, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES_PAYABLE_ACCOUNT_SELECT]: { + path: ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_PAYABLE_ACCOUNT_SELECT.route, + }, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES_JOURNAL_POSTING_PREFERENCE_SELECT]: { + path: ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_JOURNAL_POSTING_PREFERENCE_SELECT.route, + }, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_RECEIVABLE_ACCOUNT_SELECT]: { + path: ROUTES.POLICY_ACCOUNTING_NETSUITE_RECEIVABLE_ACCOUNT_SELECT.route, + }, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_INVOICE_ITEM_PREFERENCE_SELECT]: { + path: ROUTES.POLICY_ACCOUNTING_NETSUITE_INVOICE_ITEM_PREFERENCE_SELECT.route, + }, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_INVOICE_ITEM_SELECT]: { + path: ROUTES.POLICY_ACCOUNTING_NETSUITE_INVOICE_ITEM_SELECT.route, + }, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_TAX_POSTING_ACCOUNT_SELECT]: { + path: ROUTES.POLICY_ACCOUNTING_NETSUITE_TAX_POSTING_ACCOUNT_SELECT.route, + }, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_PROVINCIAL_TAX_POSTING_ACCOUNT_SELECT]: { + path: ROUTES.POLICY_ACCOUNTING_NETSUITE_PROVINCIAL_TAX_POSTING_ACCOUNT_SELECT.route, + }, [SCREENS.WORKSPACE.DESCRIPTION]: { path: ROUTES.WORKSPACE_PROFILE_DESCRIPTION.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index f90a91fe0f19..76dc8fd40fbf 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -385,6 +385,50 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.ACCOUNTING.XERO_BILL_PAYMENT_ACCOUNT_SELECTOR]: { policyID: string; }; + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT]: { + policyID: string; + }; + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_PREFERRED_EXPORTER_SELECT]: { + policyID: string; + }; + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_DATE_SELECT]: { + policyID: string; + }; + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES]: { + policyID: string; + expenseType: ValueOf; + }; + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES_DESTINATION_SELECT]: { + policyID: string; + expenseType: ValueOf; + }; + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES_VENDOR_SELECT]: { + policyID: string; + expenseType: ValueOf; + }; + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES_PAYABLE_ACCOUNT_SELECT]: { + policyID: string; + expenseType: ValueOf; + }; + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES_JOURNAL_POSTING_PREFERENCE_SELECT]: { + policyID: string; + expenseType: ValueOf; + }; + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_RECEIVABLE_ACCOUNT_SELECT]: { + policyID: string; + }; + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_INVOICE_ITEM_PREFERENCE_SELECT]: { + policyID: string; + }; + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_INVOICE_ITEM_SELECT]: { + policyID: string; + }; + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_TAX_POSTING_ACCOUNT_SELECT]: { + policyID: string; + }; + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_PROVINCIAL_TAX_POSTING_ACCOUNT_SELECT]: { + policyID: string; + }; [SCREENS.GET_ASSISTANCE]: { backTo: Routes; }; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 7a9db9f4fbbd..1a4e6fb0c716 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -56,6 +56,10 @@ function canUseReportFieldsFeature(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.REPORT_FIELDS_FEATURE) || canUseAllBetas(betas); } +function canUseNetSuiteUSATax(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.NETSUITE_USA_TAX) || canUseAllBetas(betas); +} + /** * Link previews are temporarily disabled. */ @@ -77,4 +81,5 @@ export default { canUseXeroIntegration, canUseNetSuiteIntegration, canUseReportFieldsFeature, + canUseNetSuiteUSATax, }; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index af3f3b264d13..455f1f50f7e0 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -461,6 +461,69 @@ function getXeroBankAccountsWithDefaultSelect(policy: Policy | undefined, select })); } +function getNetSuiteVendorOptions(policy: Policy | undefined, selectedVendorId: string | undefined): SelectorType[] { + const vendors = policy?.connections?.netsuite.options.data.vendors ?? []; + + return (vendors ?? []).map(({id, name}) => ({ + value: id, + text: name, + keyForList: id, + isSelected: selectedVendorId === id, + })); +} + +function getNetSuitePayableAccountOptions(policy: Policy | undefined, selectedBankAccountId: string | undefined): SelectorType[] { + const payableAccounts = policy?.connections?.netsuite.options.data.payableList ?? []; + + return (payableAccounts ?? []).map(({id, name}) => ({ + value: id, + text: name, + keyForList: id, + isSelected: selectedBankAccountId === id, + })); +} + +function getNetSuiteReceivableAccountOptions(policy: Policy | undefined, selectedBankAccountId: string | undefined): SelectorType[] { + const receivableAccounts = policy?.connections?.netsuite.options.data.receivableList ?? []; + + return (receivableAccounts ?? []).map(({id, name}) => ({ + value: id, + text: name, + keyForList: id, + isSelected: selectedBankAccountId === id, + })); +} + +function getNetSuiteInvoiceItemOptions(policy: Policy | undefined, selectedItemId: string | undefined): SelectorType[] { + const invoiceItems = policy?.connections?.netsuite.options.data.items ?? []; + + return (invoiceItems ?? []).map(({id, name}) => ({ + value: id, + text: name, + keyForList: id, + isSelected: selectedItemId === id, + })); +} + +function getNetSuiteTaxAccountOptions(policy: Policy | undefined, selectedAccountId: string | undefined): SelectorType[] { + const taxAccounts = policy?.connections?.netsuite.options.data.taxAccountsList ?? []; + + return (taxAccounts ?? []).map(({externalID, name}) => ({ + value: externalID, + text: name, + keyForList: externalID, + isSelected: selectedAccountId === externalID, + })); +} + +function canUseTaxNetSuite(canUseNetSuiteUSATax?: boolean, subsidiaryCountry?: string) { + return !!canUseNetSuiteUSATax || CONST.NETSUITE_TAX_COUNTRIES.includes(subsidiaryCountry ?? ''); +} + +function canUseProvTaxNetSuite(subsidiaryCountry?: string) { + return subsidiaryCountry === '_canada'; +} + /** * Sort the workspaces by their name, while keeping the selected one at the beginning. * @param workspace1 Details of the first workspace to be compared. @@ -546,6 +609,13 @@ export { findCurrentXeroOrganization, getCurrentXeroOrganizationName, getXeroBankAccountsWithDefaultSelect, + getNetSuiteVendorOptions, + canUseTaxNetSuite, + canUseProvTaxNetSuite, + getNetSuitePayableAccountOptions, + getNetSuiteReceivableAccountOptions, + getNetSuiteInvoiceItemOptions, + getNetSuiteTaxAccountOptions, getCustomUnit, getCustomUnitRate, sortWorkspacesBySelected, diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index e1044abb0c87..20f5d58cc660 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -575,6 +575,10 @@ function clearXeroErrorField(policyID: string, fieldName: string) { Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {connections: {xero: {config: {errorFields: {[fieldName]: null}}}}}); } +function clearNetSuiteErrorField(policyID: string, fieldName: string) { + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {connections: {netsuite: {options: {config: {errorFields: {[fieldName]: null}}}}}}); +} + function setWorkspaceReimbursement(policyID: string, reimbursementChoice: ValueOf, reimburserEmail: string) { const policy = getPolicy(policyID); @@ -3034,6 +3038,7 @@ export { generateCustomUnitID, clearQBOErrorField, clearXeroErrorField, + clearNetSuiteErrorField, clearWorkspaceReimbursementErrors, setWorkspaceCurrencyDefault, setForeignCurrencyDefault, diff --git a/src/libs/actions/connections/NetSuiteCommands.ts b/src/libs/actions/connections/NetSuiteCommands.ts new file mode 100644 index 000000000000..821afc82f4bc --- /dev/null +++ b/src/libs/actions/connections/NetSuiteCommands.ts @@ -0,0 +1,274 @@ +import type {OnyxUpdate} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import * as API from '@libs/API'; +import {WRITE_COMMANDS} from '@libs/API/types'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Connections} from '@src/types/onyx/Policy'; + +function updateNetSuiteOnyxData( + policyID: string, + settingName: TSettingName, + settingValue: Partial, + oldSettingValue: Partial, +) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + connections: { + netsuite: { + options: { + config: { + [settingName]: settingValue ?? null, + pendingFields: { + [settingName]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + errorFields: { + [settingName]: null, + }, + }, + }, + }, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + connections: { + netsuite: { + options: { + config: { + [settingName]: oldSettingValue ?? null, + pendingFields: { + [settingName]: null, + }, + errorFields: { + [settingName]: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }, + }, + }, + }, + }, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + connections: { + netsuite: { + options: { + config: { + pendingFields: { + [settingName]: null, + }, + errorFields: { + [settingName]: null, + }, + }, + }, + }, + }, + }, + }, + ]; + return {optimisticData, failureData, successData}; +} + +function updateNetSuiteExporter(policyID: string, tokenSecret: string, exporter: string, oldExporter: string) { + const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.EXPORTER, exporter, oldExporter); + + const parameters = { + policyID, + email: exporter, + authToken: tokenSecret, + }; + API.write(WRITE_COMMANDS.UPDATE_NETSUITE_EXPORTER, parameters, onyxData); +} + +function updateNetSuiteExportDate(policyID: string, tokenSecret: string, date: ValueOf, oldDate?: ValueOf) { + const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.EXPORT_DATE, date, oldDate); + + const parameters = { + policyID, + value: date, + authToken: tokenSecret, + }; + API.write(WRITE_COMMANDS.UPDATE_NETSUITE_EXPORT_DATE, parameters, onyxData); +} + +function updateNetSuiteReimbursableExpensesExportDestination( + policyID: string, + tokenSecret: string, + destination: ValueOf, + oldDestination: ValueOf, +) { + const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.REIMBURSABLE_EXPENSES_EXPORT_DESTINATION, destination, oldDestination); + + const parameters = { + policyID, + value: destination, + authToken: tokenSecret, + }; + API.write(WRITE_COMMANDS.UPDATE_NETSUITE_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION, parameters, onyxData); +} + +function updateNetSuiteNonReimbursableExpensesExportDestination( + policyID: string, + tokenSecret: string, + destination: ValueOf, + oldDestination: ValueOf, +) { + const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.NON_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION, destination, oldDestination); + + const parameters = { + policyID, + value: destination, + authToken: tokenSecret, + }; + API.write(WRITE_COMMANDS.UPDATE_NETSUITE_NONREIMBURSABLE_EXPENSES_EXPORT_DESTINATION, parameters, onyxData); +} + +function updateNetSuiteDefaultVendor(policyID: string, tokenSecret: string, vendorID: string, oldVendorID?: string) { + const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.DEFAULT_VENDOR, vendorID, oldVendorID); + + const parameters = { + policyID, + vendorID, + authToken: tokenSecret, + }; + API.write(WRITE_COMMANDS.UPDATE_NETSUITE_DEFAULT_VENDOR, parameters, onyxData); +} + +function updateNetSuiteReimbursablePayableAccount(policyID: string, tokenSecret: string, bankAccountID: string, oldBankAccountID: string) { + const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.REIMBURSABLE_PAYABLE_ACCOUNT, bankAccountID, oldBankAccountID); + + const parameters = { + policyID, + bankAccountID, + authToken: tokenSecret, + }; + API.write(WRITE_COMMANDS.UPDATE_NETSUITE_REIMBURSABLE_PAYABLE_ACCOUNT, parameters, onyxData); +} + +function updateNetSuitePayableAcct(policyID: string, tokenSecret: string, bankAccountID: string, oldBankAccountID: string) { + const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.PAYABLE_ACCT, bankAccountID, oldBankAccountID); + + const parameters = { + policyID, + bankAccountID, + authToken: tokenSecret, + }; + API.write(WRITE_COMMANDS.UPDATE_NETSUITE_PAYABLE_ACCT, parameters, onyxData); +} + +function updateNetSuiteJournalPostingPreference( + policyID: string, + tokenSecret: string, + postingPreference: ValueOf, + oldPostingPreference?: ValueOf, +) { + const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.JOURNAL_POSTING_PREFERENCE, postingPreference, oldPostingPreference); + + const parameters = { + policyID, + value: postingPreference, + authToken: tokenSecret, + }; + API.write(WRITE_COMMANDS.UPDATE_NETSUITE_JOURNAL_POSTING_PREFERENCE, parameters, onyxData); +} + +function updateNetSuiteReceivableAccount(policyID: string, tokenSecret: string, bankAccountID: string, oldBankAccountID?: string) { + const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.RECEIVABLE_ACCOUNT, bankAccountID, oldBankAccountID); + + const parameters = { + policyID, + bankAccountID, + authToken: tokenSecret, + }; + API.write(WRITE_COMMANDS.UPDATE_NETSUITE_RECEIVABLE_ACCOUNT, parameters, onyxData); +} + +function updateNetSuiteInvoiceItem(policyID: string, tokenSecret: string, itemID: string, oldItemID?: string) { + const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.INVOICE_ITEM, itemID, oldItemID); + + const parameters = { + policyID, + itemID, + authToken: tokenSecret, + }; + API.write(WRITE_COMMANDS.UPDATE_NETSUITE_INVOICE_ITEM, parameters, onyxData); +} + +function updateNetSuiteTaxPostingAccount(policyID: string, tokenSecret: string, bankAccountID: string, oldBankAccountID?: string) { + const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.TAX_POSTING_ACCOUNT, bankAccountID, oldBankAccountID); + + const parameters = { + policyID, + bankAccountID, + authToken: tokenSecret, + }; + API.write(WRITE_COMMANDS.UPDATE_NETSUITE_TAX_POSTING_ACCOUNT, parameters, onyxData); +} + +function updateNetSuiteProvincialTaxPostingAccount(policyID: string, tokenSecret: string, bankAccountID: string, oldBankAccountID?: string) { + const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.PROVINCIAL_TAX_POSTING_ACCOUNT, bankAccountID, oldBankAccountID); + + const parameters = { + policyID, + bankAccountID, + authToken: tokenSecret, + }; + API.write(WRITE_COMMANDS.UPDATE_NETSUITE_PROVINCIAL_TAX_POSTING_ACCOUNT, parameters, onyxData); +} + +function updateNetSuiteAllowForeignCurrency(policyID: string, tokenSecret: string, value: boolean, oldValue?: boolean) { + const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.ALLOW_FOREIGN_CURRENCY, value, oldValue); + + const parameters = { + policyID, + enabled: value, + authToken: tokenSecret, + }; + API.write(WRITE_COMMANDS.UPDATE_NETSUITE_ALLOW_FOREIGN_CURRENCY, parameters, onyxData); +} + +function updateNetSuiteExportToNextOpenPeriod(policyID: string, tokenSecret: string, value: boolean, oldValue: boolean) { + const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.EXPORT_TO_NEXT_OPEN_PERIOD, value, oldValue); + + const parameters = { + policyID, + enabled: value, + authToken: tokenSecret, + }; + API.write(WRITE_COMMANDS.UPDATE_NETSUITE_EXPORT_TO_NEXT_OPEN_PERIOD, parameters, onyxData); +} + +export { + updateNetSuiteExporter, + updateNetSuiteExportDate, + updateNetSuiteReimbursableExpensesExportDestination, + updateNetSuiteNonReimbursableExpensesExportDestination, + updateNetSuiteDefaultVendor, + updateNetSuiteReimbursablePayableAccount, + updateNetSuitePayableAcct, + updateNetSuiteJournalPostingPreference, + updateNetSuiteReceivableAccount, + updateNetSuiteInvoiceItem, + updateNetSuiteTaxPostingAccount, + updateNetSuiteProvincialTaxPostingAccount, + updateNetSuiteAllowForeignCurrency, + updateNetSuiteExportToNextOpenPeriod, +}; diff --git a/src/libs/actions/connections/index.ts b/src/libs/actions/connections/index.ts index d0c9fb762a4c..852be5d4e1c6 100644 --- a/src/libs/actions/connections/index.ts +++ b/src/libs/actions/connections/index.ts @@ -15,6 +15,8 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {ConnectionName, Connections, PolicyConnectionName} from '@src/types/onyx/Policy'; import type Policy from '@src/types/onyx/Policy'; +type ConnectionNameExceptNetSuite = Exclude; + function removePolicyConnection(policyID: string, connectionName: PolicyConnectionName) { const optimisticData: OnyxUpdate[] = [ { @@ -40,7 +42,7 @@ function removePolicyConnection(policyID: string, connectionName: PolicyConnecti API.write(WRITE_COMMANDS.REMOVE_POLICY_CONNECTION, parameters, {optimisticData}); } -function updatePolicyConnectionConfig( +function updatePolicyConnectionConfig( policyID: string, connectionName: TConnectionName, settingName: TSettingName, @@ -165,7 +167,7 @@ function syncConnection(policyID: string, connectionName: PolicyConnectionName | }); } -function updateManyPolicyConnectionConfigs>( +function updateManyPolicyConnectionConfigs>( policyID: string, connectionName: TConnectionName, configUpdate: TConfigUpdate, @@ -238,9 +240,8 @@ function updateManyPolicyConnectionConfigs, connectionName: PolicyConnectionName, isSyncInProgress: boolean): boolean { // NetSuite does not use the conventional lastSync object, so we need to check for lastErrorSyncDate if (connectionName === CONST.POLICY.CONNECTIONS.NAME.NETSUITE) { - return !isSyncInProgress && !!policy?.connections?.[CONST.POLICY.CONNECTIONS.NAME.NETSUITE].lastErrorSyncDate; + return !isSyncInProgress && policy?.connections?.[connectionName]?.verified === false; } - return !isSyncInProgress && policy?.connections?.[connectionName]?.lastSync?.isSuccessful === false; } diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteDateSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteDateSelectPage.tsx new file mode 100644 index 000000000000..0d681677385c --- /dev/null +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteDateSelectPage.tsx @@ -0,0 +1,74 @@ +import React, {useCallback, useMemo} from 'react'; +import {View} from 'react-native'; +import type {ValueOf} from 'type-fest'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import SelectionScreen from '@components/SelectionScreen'; +import type {SelectorType} from '@components/SelectionScreen'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Connections from '@libs/actions/connections/NetSuiteCommands'; +import Navigation from '@navigation/Navigation'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +type MenuListItem = ListItem & { + value: ValueOf; +}; + +function NetSuiteDateSelectPage({policy}: WithPolicyConnectionsProps) { + const {translate} = useLocalize(); + const policyID = policy?.id ?? '-1'; + const styles = useThemeStyles(); + const config = policy?.connections?.netsuite.options.config; + const data: MenuListItem[] = Object.values(CONST.NETSUITE_EXPORT_DATE).map((dateType) => ({ + value: dateType, + text: translate(`workspace.netsuite.exportDate.values.${dateType}.label`), + alternateText: translate(`workspace.netsuite.exportDate.values.${dateType}.description`), + keyForList: dateType, + isSelected: config?.exportDate === dateType, + })); + + const headerContent = useMemo( + () => ( + + {translate('workspace.netsuite.exportDate.description')} + + ), + [translate, styles.pb5, styles.ph5], + ); + + const selectExportDate = useCallback( + (row: MenuListItem) => { + if (row.value !== config?.exportDate) { + Connections.updateNetSuiteExportDate(policyID, policy?.connections?.netsuite.tokenSecret ?? '', row.value, config?.exportDate); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID)); + }, + [config?.exportDate, policy?.connections?.netsuite.tokenSecret, policyID], + ); + + return ( + selectExportDate(selection as MenuListItem)} + initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} + policyID={policyID} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID))} + connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} + /> + ); +} + +NetSuiteDateSelectPage.displayName = 'NetSuiteDateSelectPage'; + +export default withPolicyConnections(NetSuiteDateSelectPage); diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx new file mode 100644 index 000000000000..ac247c8de1cd --- /dev/null +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx @@ -0,0 +1,265 @@ +import React, {useMemo} from 'react'; +import {View} from 'react-native'; +import ConnectionLayout from '@components/ConnectionLayout'; +import type {MenuItemProps} from '@components/MenuItem'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import type {OfflineWithFeedbackProps} from '@components/OfflineWithFeedback'; +import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Connections from '@libs/actions/connections/NetSuiteCommands'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import {canUseProvTaxNetSuite, canUseTaxNetSuite} from '@libs/PolicyUtils'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import type {ToggleSettingOptionRowProps} from '@pages/workspace/workflows/ToggleSettingsOptionRow'; +import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; +import * as Policy from '@userActions/Policy/Policy'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; + +type MenuItem = MenuItemProps & { + pendingAction?: OfflineWithFeedbackProps['pendingAction']; + + shouldHide?: boolean; + + /** Any error message to show */ + errors?: Errors; + + /** Callback to close the error messages */ + onCloseError?: () => void; + + type: 'menuitem'; +}; + +type DividerLineItem = { + type: 'divider'; + + shouldHide?: boolean; +}; + +type ToggleItem = ToggleSettingOptionRowProps & { + type: 'toggle'; + + shouldHide?: boolean; +}; + +function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const policyID = policy?.id ?? '-1'; + const policyOwner = policy?.owner ?? ''; + const {canUseNetSuiteUSATax} = usePermissions(); + + const config = policy?.connections?.netsuite?.options.config; + + const {subsidiaryList, receivableList, taxAccountsList} = policy?.connections?.netsuite?.options?.data ?? {}; + const selectedSubsidiary = useMemo(() => { + const selectedSub = (subsidiaryList ?? []).find((subsidiary) => subsidiary.internalID === config?.subsidiaryID); + return selectedSub; + }, [subsidiaryList, config?.subsidiaryID]); + + const selectedReceivable = useMemo(() => { + const selectedRec = (receivableList ?? []).find((receivable) => receivable.id === config?.receivableAccount); + return selectedRec; + }, [receivableList, config?.receivableAccount]); + + const selectedTaxPostingAccount = useMemo(() => { + const selectedTaxAcc = (taxAccountsList ?? []).find((taxAccount) => taxAccount.externalID === config?.taxPostingAccount); + return selectedTaxAcc; + }, [taxAccountsList, config?.taxPostingAccount]); + + const selectedProvTaxPostingAccount = useMemo(() => { + const selectedTaxAcc = (taxAccountsList ?? []).find((taxAccount) => taxAccount.externalID === config?.provincialTaxPostingAccount); + return selectedTaxAcc; + }, [taxAccountsList, config?.provincialTaxPostingAccount]); + + const menuItems: Array = [ + { + type: 'menuitem', + description: translate('workspace.netsuite.preferredExporter'), + onPress: () => { + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_PREFERRED_EXPORTER_SELECT.getRoute(policyID)); + }, + brickRoadIndicator: config?.errorFields?.exporter ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + title: config?.exporter ?? policyOwner, + pendingAction: config?.pendingFields?.exporter, + errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.EXPORTER), + onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.EXPORTER), + }, + { + type: 'divider', + }, + { + type: 'menuitem', + description: translate('common.date'), + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_DATE_SELECT.getRoute(policyID)), + brickRoadIndicator: config?.errorFields?.exportDate ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + title: config?.exportDate ? translate(`workspace.netsuite.exportDate.values.${config.exportDate}.label`) : undefined, + pendingAction: config?.pendingFields?.exportDate, + errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.EXPORT_DATE), + onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.EXPORT_DATE), + }, + { + type: 'menuitem', + description: translate('workspace.netsuite.exportReimbursable'), + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES.getRoute(policyID, CONST.NETSUITE_EXPENSE_TYPE.REIMBURSABLE)), + brickRoadIndicator: config?.errorFields?.reimbursableExpensesExportDestination ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + title: config?.reimbursableExpensesExportDestination ? translate(`workspace.netsuite.exportDestination.values.${config.reimbursableExpensesExportDestination}.label`) : undefined, + pendingAction: config?.pendingFields?.reimbursableExpensesExportDestination, + errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.REIMBURSABLE_EXPENSES_EXPORT_DESTINATION), + onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.REIMBURSABLE_EXPENSES_EXPORT_DESTINATION), + }, + { + type: 'menuitem', + description: translate('workspace.netsuite.exportNonReimbursable'), + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES.getRoute(policyID, CONST.NETSUITE_EXPENSE_TYPE.NON_REIMBURSABLE)), + brickRoadIndicator: config?.errorFields?.nonreimbursableExpensesExportDestination ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + title: config?.nonreimbursableExpensesExportDestination + ? translate(`workspace.netsuite.exportDestination.values.${config.nonreimbursableExpensesExportDestination}.label`) + : undefined, + pendingAction: config?.pendingFields?.nonreimbursableExpensesExportDestination, + errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.NON_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION), + onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.NON_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION), + }, + { + type: 'divider', + }, + { + type: 'menuitem', + description: translate('workspace.netsuite.exportInvoices'), + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_RECEIVABLE_ACCOUNT_SELECT.getRoute(policyID)), + brickRoadIndicator: config?.errorFields?.receivableAccount ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + title: selectedReceivable ? selectedReceivable.name : undefined, + pendingAction: config?.pendingFields?.receivableAccount, + errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.RECEIVABLE_ACCOUNT), + onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.RECEIVABLE_ACCOUNT), + }, + { + type: 'menuitem', + description: translate('workspace.netsuite.invoiceItem.label'), + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_INVOICE_ITEM_PREFERENCE_SELECT.getRoute(policyID)), + brickRoadIndicator: config?.errorFields?.invoiceItemPreference ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + title: config?.invoiceItemPreference ? translate(`workspace.netsuite.invoiceItem.values.${config.invoiceItemPreference}`) : undefined, + pendingAction: config?.pendingFields?.invoiceItemPreference, + errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.INVOICE_ITEM_PREFERENCE), + onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.INVOICE_ITEM_PREFERENCE), + }, + { + type: 'divider', + }, + { + type: 'menuitem', + description: translate('workspace.netsuite.journalEntriesProvTaxPostingAccount'), + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_PROVINCIAL_TAX_POSTING_ACCOUNT_SELECT.getRoute(policyID)), + brickRoadIndicator: config?.errorFields?.provincialTaxPostingAccount ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + title: selectedProvTaxPostingAccount ? selectedProvTaxPostingAccount.name : undefined, + pendingAction: config?.pendingFields?.provincialTaxPostingAccount, + errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.PROVINCIAL_TAX_POSTING_ACCOUNT), + onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.PROVINCIAL_TAX_POSTING_ACCOUNT), + shouldHide: !!config?.suiteTaxEnabled || !config?.syncOptions.syncTax || !canUseProvTaxNetSuite(selectedSubsidiary?.country), + }, + { + type: 'menuitem', + description: translate('workspace.netsuite.journalEntriesTaxPostingAccount'), + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_TAX_POSTING_ACCOUNT_SELECT.getRoute(policyID)), + brickRoadIndicator: config?.errorFields?.taxPostingAccount ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + title: selectedTaxPostingAccount ? selectedTaxPostingAccount.name : undefined, + pendingAction: config?.pendingFields?.taxPostingAccount, + errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.TAX_POSTING_ACCOUNT), + onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.TAX_POSTING_ACCOUNT), + shouldHide: !!config?.suiteTaxEnabled || !config?.syncOptions.syncTax || !canUseTaxNetSuite(canUseNetSuiteUSATax, selectedSubsidiary?.country), + }, + { + type: 'toggle', + title: translate('workspace.netsuite.foreignCurrencyAmount'), + isActive: !!config?.allowForeignCurrency, + switchAccessibilityLabel: translate('workspace.netsuite.foreignCurrencyAmount'), + onToggle: () => + Connections.updateNetSuiteAllowForeignCurrency(policyID, policy?.connections?.netsuite.tokenSecret ?? '', !config?.allowForeignCurrency, config?.allowForeignCurrency), + onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.ALLOW_FOREIGN_CURRENCY), + pendingAction: config?.pendingFields?.allowForeignCurrency, + errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.ALLOW_FOREIGN_CURRENCY), + shouldHide: + config?.reimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.EXPENSE_REPORT && + config?.nonreimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.EXPENSE_REPORT, + }, + { + type: 'toggle', + title: translate('workspace.netsuite.exportToNextOpenPeriod'), + isActive: !!config?.exportToNextOpenPeriod, + switchAccessibilityLabel: translate('workspace.netsuite.exportToNextOpenPeriod'), + onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.EXPORT_TO_NEXT_OPEN_PERIOD), + onToggle: () => + Connections.updateNetSuiteExportToNextOpenPeriod( + policyID, + policy?.connections?.netsuite.tokenSecret ?? '', + !config?.exportToNextOpenPeriod, + config?.exportToNextOpenPeriod ?? false, + ), + pendingAction: config?.pendingFields?.exportToNextOpenPeriod, + errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.EXPORT_TO_NEXT_OPEN_PERIOD), + }, + ]; + + return ( + + {menuItems + .filter((item) => !item.shouldHide) + .map((item) => { + if (item.type === 'divider') { + return ; + } + if (item.type === 'toggle') { + const {type, shouldHide, ...rest} = item; + return ( + + ); + } + return ( + + + + ); + })} + + ); +} + +NetSuiteExportConfigurationPage.displayName = 'NetSuiteExportConfigurationPage'; + +export default withPolicyConnections(NetSuiteExportConfigurationPage); diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesDestinationSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesDestinationSelectPage.tsx new file mode 100644 index 000000000000..2075048ad884 --- /dev/null +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesDestinationSelectPage.tsx @@ -0,0 +1,75 @@ +import {useRoute} from '@react-navigation/native'; +import React, {useCallback} from 'react'; +import type {ValueOf} from 'type-fest'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import SelectionScreen from '@components/SelectionScreen'; +import type {SelectorType} from '@components/SelectionScreen'; +import useLocalize from '@hooks/useLocalize'; +import * as Connections from '@libs/actions/connections/NetSuiteCommands'; +import Navigation from '@navigation/Navigation'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +type MenuListItem = ListItem & { + value: ValueOf; +}; + +type RouteParams = { + expenseType: ValueOf; +}; + +function NetSuiteExportExpensesDestinationSelectPage({policy}: WithPolicyConnectionsProps) { + const {translate} = useLocalize(); + const policyID = policy?.id ?? '-1'; + const config = policy?.connections?.netsuite.options.config; + + const route = useRoute(); + const params = route.params as RouteParams; + const isReimbursable = params.expenseType === CONST.NETSUITE_EXPENSE_TYPE.REIMBURSABLE; + + const currentValue = isReimbursable ? config?.reimbursableExpensesExportDestination : config?.nonreimbursableExpensesExportDestination; + + const data: MenuListItem[] = Object.values(CONST.NETSUITE_EXPORT_DESTINATION).map((dateType) => ({ + value: dateType, + text: translate(`workspace.netsuite.exportDestination.values.${dateType}.label`), + keyForList: dateType, + isSelected: currentValue === dateType, + })); + + const selectDestination = useCallback( + (row: MenuListItem) => { + if (row.value !== currentValue) { + if (isReimbursable) { + Connections.updateNetSuiteReimbursableExpensesExportDestination(policyID, policy?.connections?.netsuite.tokenSecret ?? '', row.value, currentValue ?? 'EXPENSE_REPORT'); + } else { + Connections.updateNetSuiteNonReimbursableExpensesExportDestination(policyID, policy?.connections?.netsuite.tokenSecret ?? '', row.value, currentValue ?? 'VENDOR_BILL'); + } + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES.getRoute(policyID, params.expenseType)); + }, + [currentValue, isReimbursable, params.expenseType, policy?.connections?.netsuite.tokenSecret, policyID], + ); + + return ( + selectDestination(selection as MenuListItem)} + initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} + policyID={policyID} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES.getRoute(policyID, params.expenseType))} + connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} + /> + ); +} + +NetSuiteExportExpensesDestinationSelectPage.displayName = 'NetSuiteExportExpensesDestinationSelectPage'; + +export default withPolicyConnections(NetSuiteExportExpensesDestinationSelectPage); diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx new file mode 100644 index 000000000000..a5dc136f4b55 --- /dev/null +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx @@ -0,0 +1,74 @@ +import {useRoute} from '@react-navigation/native'; +import React, {useCallback} from 'react'; +import type {ValueOf} from 'type-fest'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import SelectionScreen from '@components/SelectionScreen'; +import type {SelectorType} from '@components/SelectionScreen'; +import useLocalize from '@hooks/useLocalize'; +import * as Connections from '@libs/actions/connections/NetSuiteCommands'; +import Navigation from '@navigation/Navigation'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +type MenuListItem = ListItem & { + value: ValueOf; +}; + +type RouteParams = { + expenseType: ValueOf; +}; + +function NetSuiteExportExpensesJournalPostingPreferenceSelectPage({policy}: WithPolicyConnectionsProps) { + const {translate} = useLocalize(); + const policyID = policy?.id ?? '-1'; + const config = policy?.connections?.netsuite.options.config; + + const route = useRoute(); + const params = route.params as RouteParams; + const isReimbursable = params.expenseType === CONST.NETSUITE_EXPENSE_TYPE.REIMBURSABLE; + + const data: MenuListItem[] = Object.values(CONST.NETSUITE_JOURNAL_POSTING_PREFERENCE).map((postingPreference) => ({ + value: postingPreference, + text: translate(`workspace.netsuite.journalPostingPreference.values.${postingPreference}`), + keyForList: postingPreference, + isSelected: config?.journalPostingPreference === postingPreference, + })); + + const selectPostingPreference = useCallback( + (row: MenuListItem) => { + if (row.value !== config?.journalPostingPreference) { + Connections.updateNetSuiteJournalPostingPreference(policyID, policy?.connections?.netsuite.tokenSecret ?? '', row.value, config?.journalPostingPreference); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES.getRoute(policyID, params.expenseType)); + }, + [config?.journalPostingPreference, params.expenseType, policy?.connections?.netsuite.tokenSecret, policyID], + ); + + return ( + selectPostingPreference(selection as MenuListItem)} + initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} + policyID={policyID} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES.getRoute(policyID, params.expenseType))} + connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} + shouldBeBlocked={ + isReimbursable + ? config?.reimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY + : config?.nonreimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY + } + /> + ); +} + +NetSuiteExportExpensesJournalPostingPreferenceSelectPage.displayName = 'NetSuiteExportExpensesJournalPostingPreferenceSelectPage'; + +export default withPolicyConnections(NetSuiteExportExpensesJournalPostingPreferenceSelectPage); diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx new file mode 100644 index 000000000000..0206f6f56c4e --- /dev/null +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx @@ -0,0 +1,173 @@ +import {useRoute} from '@react-navigation/native'; +import {isEmpty} from 'lodash'; +import React, {useMemo} from 'react'; +import type {ValueOf} from 'type-fest'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {MenuItemProps} from '@components/MenuItem'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import type {OfflineWithFeedbackProps} from '@components/OfflineWithFeedback'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import * as Policy from '@userActions/Policy/Policy'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; + +type MenuItem = MenuItemProps & { + pendingAction?: OfflineWithFeedbackProps['pendingAction']; + + shouldHide?: boolean; + + /** Any error message to show */ + errors?: Errors; + + /** Callback to close the error messages */ + onCloseError?: () => void; +}; + +type RouteParams = { + expenseType: ValueOf; +}; + +function NetSuiteExportExpensesPage({policy}: WithPolicyConnectionsProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const policyID = policy?.id ?? '-1'; + const route = useRoute(); + const params = route.params as RouteParams; + const isReimbursable = params.expenseType === CONST.NETSUITE_EXPENSE_TYPE.REIMBURSABLE; + + const config = policy?.connections?.netsuite?.options.config; + + const exportDestination = isReimbursable ? config?.reimbursableExpensesExportDestination : config?.nonreimbursableExpensesExportDestination; + const exportDestinationError = isReimbursable ? config?.errorFields?.reimbursableExpensesExportDestination : config?.errorFields?.nonreimbursableExpensesExportDestination; + const exportDestinationPending = isReimbursable ? config?.pendingFields?.reimbursableExpensesExportDestination : config?.pendingFields?.nonreimbursableExpensesExportDestination; + const helperTextType = isReimbursable ? 'reimbursableDescription' : 'nonReimbursableDescription'; + const configType = isReimbursable ? CONST.NETSUITE_CONFIG.REIMBURSABLE_EXPENSES_EXPORT_DESTINATION : CONST.NETSUITE_CONFIG.NON_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION; + + const {vendors, payableList} = policy?.connections?.netsuite?.options?.data ?? {}; + + const defaultVendor = useMemo(() => { + const selectedVendor = (vendors ?? []).find((vendor) => vendor.id === config?.defaultVendor); + return selectedVendor; + }, [vendors, config?.defaultVendor]); + + const selectedPayableAccount = useMemo(() => { + const selectedPayableAcc = (payableList ?? []).find((payableAccount) => payableAccount.id === config?.payableAcct); + return selectedPayableAcc; + }, [payableList, config?.payableAcct]); + + const selectedReimbursablePayableAccount = useMemo(() => { + const selectedPayableAcc = (payableList ?? []).find((payableAccount) => payableAccount.id === config?.reimbursablePayableAccount); + return selectedPayableAcc; + }, [payableList, config?.reimbursablePayableAccount]); + + const isConnectionEmpty = isEmpty(policy?.connections?.netsuite); + + const menuItems: MenuItem[] = [ + { + description: translate('workspace.netsuite.exportAs'), + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_DESTINATION_SELECT.getRoute(policyID, params.expenseType)), + brickRoadIndicator: exportDestinationError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + title: exportDestination ? translate(`workspace.netsuite.exportDestination.values.${exportDestination}.label`) : undefined, + pendingAction: exportDestinationPending, + errors: ErrorUtils.getLatestErrorField(config, configType), + onCloseError: () => Policy.clearNetSuiteErrorField(policyID, configType), + helperText: exportDestination ? translate(`workspace.netsuite.exportDestination.values.${exportDestination}.${helperTextType}`) : undefined, + shouldParseHelperText: true, + }, + { + description: translate('workspace.netsuite.defaultVendor'), + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_VENDOR_SELECT.getRoute(policyID, params.expenseType)), + brickRoadIndicator: config?.errorFields?.defaultVendor ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + title: defaultVendor ? defaultVendor.name : undefined, + pendingAction: config?.pendingFields?.defaultVendor, + errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.DEFAULT_VENDOR), + onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.DEFAULT_VENDOR), + shouldHide: isReimbursable || exportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.VENDOR_BILL, + }, + { + description: translate('workspace.netsuite.nonReimbursableJournalPostingAccount'), + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_PAYABLE_ACCOUNT_SELECT.getRoute(policyID, params.expenseType)), + brickRoadIndicator: config?.errorFields?.payableAcct ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + title: selectedPayableAccount ? selectedPayableAccount.name : undefined, + pendingAction: config?.pendingFields?.payableAcct, + errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.PAYABLE_ACCT), + onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.PAYABLE_ACCT), + shouldHide: exportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY, + }, + { + description: translate('workspace.netsuite.reimbursableJournalPostingAccount'), + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_PAYABLE_ACCOUNT_SELECT.getRoute(policyID, params.expenseType)), + brickRoadIndicator: config?.errorFields?.reimbursablePayableAccount ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + title: selectedReimbursablePayableAccount ? selectedReimbursablePayableAccount.name : undefined, + pendingAction: config?.pendingFields?.reimbursablePayableAccount, + errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.REIMBURSABLE_PAYABLE_ACCOUNT), + onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.REIMBURSABLE_PAYABLE_ACCOUNT), + shouldHide: exportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY, + }, + { + description: translate('workspace.netsuite.journalPostingPreference.label'), + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_JOURNAL_POSTING_PREFERENCE_SELECT.getRoute(policyID, params.expenseType)), + brickRoadIndicator: config?.errorFields?.journalPostingPreference ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + title: config?.journalPostingPreference ? translate(`workspace.netsuite.journalPostingPreference.values.${config.journalPostingPreference}`) : undefined, + pendingAction: config?.pendingFields?.journalPostingPreference, + errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.JOURNAL_POSTING_PREFERENCE), + onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.JOURNAL_POSTING_PREFERENCE), + shouldHide: exportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY, + }, + ]; + + return ( + + + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID))} + /> + {menuItems + .filter((item) => !item.shouldHide) + .map((item) => ( + + + + ))} + + + ); +} + +NetSuiteExportExpensesPage.displayName = 'NetSuiteExportExpensesPage'; + +export default withPolicyConnections(NetSuiteExportExpensesPage); diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPayableAccountSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPayableAccountSelectPage.tsx new file mode 100644 index 000000000000..5f82523cd257 --- /dev/null +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPayableAccountSelectPage.tsx @@ -0,0 +1,93 @@ +import {useRoute} from '@react-navigation/native'; +import React, {useCallback, useMemo} from 'react'; +import type {ValueOf} from 'type-fest'; +import BlockingView from '@components/BlockingViews/BlockingView'; +import * as Illustrations from '@components/Icon/Illustrations'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {SelectorType} from '@components/SelectionScreen'; +import SelectionScreen from '@components/SelectionScreen'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Connections from '@libs/actions/connections/NetSuiteCommands'; +import Navigation from '@libs/Navigation/Navigation'; +import {getNetSuitePayableAccountOptions} from '@libs/PolicyUtils'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +type RouteParams = { + expenseType: ValueOf; +}; + +function NetSuiteExportExpensesPayableAccountSelectPage({policy}: WithPolicyConnectionsProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const policyID = policy?.id ?? '-1'; + + const route = useRoute(); + const params = route.params as RouteParams; + const isReimbursable = params.expenseType === CONST.NETSUITE_EXPENSE_TYPE.REIMBURSABLE; + + const config = policy?.connections?.netsuite.options.config; + const currentValue = isReimbursable ? config?.reimbursablePayableAccount : config?.payableAcct; + const netsuitePayableAccountOptions = useMemo(() => getNetSuitePayableAccountOptions(policy ?? undefined, currentValue), [currentValue, policy]); + + const initiallyFocusedOptionKey = useMemo(() => netsuitePayableAccountOptions?.find((mode) => mode.isSelected)?.keyForList, [netsuitePayableAccountOptions]); + + const updatePayableAccount = useCallback( + ({value}: SelectorType) => { + if (currentValue !== value) { + if (isReimbursable) { + Connections.updateNetSuiteReimbursablePayableAccount(policyID, policy?.connections?.netsuite.tokenSecret ?? '', value, currentValue ?? ''); + } else { + Connections.updateNetSuitePayableAcct(policyID, policy?.connections?.netsuite.tokenSecret ?? '', value, currentValue ?? ''); + } + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES.getRoute(policyID, params.expenseType)); + }, + [currentValue, policyID, params.expenseType, isReimbursable, policy?.connections?.netsuite.tokenSecret], + ); + + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + + return ( + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES.getRoute(policyID, params.expenseType))} + title={isReimbursable ? 'workspace.netsuite.reimbursableJournalPostingAccount' : 'workspace.netsuite.nonReimbursableJournalPostingAccount'} + listEmptyContent={listEmptyContent} + connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} + shouldBeBlocked={ + isReimbursable + ? config?.reimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY + : config?.nonreimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY + } + /> + ); +} + +NetSuiteExportExpensesPayableAccountSelectPage.displayName = 'NetSuiteExportExpensesPayableAccountSelectPage'; + +export default withPolicyConnections(NetSuiteExportExpensesPayableAccountSelectPage); diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx new file mode 100644 index 000000000000..b36136bd3528 --- /dev/null +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx @@ -0,0 +1,84 @@ +import {useRoute} from '@react-navigation/native'; +import React, {useCallback, useMemo} from 'react'; +import type {ValueOf} from 'type-fest'; +import BlockingView from '@components/BlockingViews/BlockingView'; +import * as Illustrations from '@components/Icon/Illustrations'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {SelectorType} from '@components/SelectionScreen'; +import SelectionScreen from '@components/SelectionScreen'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Connections from '@libs/actions/connections/NetSuiteCommands'; +import Navigation from '@libs/Navigation/Navigation'; +import {getNetSuiteVendorOptions} from '@libs/PolicyUtils'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +type RouteParams = { + expenseType: ValueOf; +}; + +function NetSuiteExportExpensesVendorSelectPage({policy}: WithPolicyConnectionsProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const policyID = policy?.id ?? '-1'; + + const route = useRoute(); + const params = route.params as RouteParams; + const isReimbursable = params.expenseType === CONST.NETSUITE_EXPENSE_TYPE.REIMBURSABLE; + + const config = policy?.connections?.netsuite.options.config; + const netsuiteVendorOptions = useMemo(() => getNetSuiteVendorOptions(policy ?? undefined, config?.defaultVendor), [config?.defaultVendor, policy]); + + const initiallyFocusedOptionKey = useMemo(() => netsuiteVendorOptions?.find((mode) => mode.isSelected)?.keyForList, [netsuiteVendorOptions]); + + const updateDefaultVendor = useCallback( + ({value}: SelectorType) => { + if (config?.defaultVendor !== value) { + Connections.updateNetSuiteDefaultVendor(policyID, policy?.connections?.netsuite.tokenSecret ?? '', value, config?.defaultVendor); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES.getRoute(policyID, params.expenseType)); + }, + [config?.defaultVendor, policyID, params.expenseType, policy?.connections?.netsuite.tokenSecret], + ); + + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + + return ( + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES.getRoute(policyID, params.expenseType))} + title="workspace.netsuite.defaultVendor" + listEmptyContent={listEmptyContent} + connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} + shouldBeBlocked={isReimbursable || config?.nonreimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.VENDOR_BILL} + /> + ); +} + +NetSuiteExportExpensesVendorSelectPage.displayName = 'NetSuiteExportExpensesVendorSelectPage'; + +export default withPolicyConnections(NetSuiteExportExpensesVendorSelectPage); diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemSelectPage.tsx new file mode 100644 index 000000000000..06ff7b468a24 --- /dev/null +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemSelectPage.tsx @@ -0,0 +1,74 @@ +import React, {useCallback, useMemo} from 'react'; +import BlockingView from '@components/BlockingViews/BlockingView'; +import * as Illustrations from '@components/Icon/Illustrations'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {SelectorType} from '@components/SelectionScreen'; +import SelectionScreen from '@components/SelectionScreen'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Connections from '@libs/actions/connections/NetSuiteCommands'; +import Navigation from '@libs/Navigation/Navigation'; +import {getNetSuiteInvoiceItemOptions} from '@libs/PolicyUtils'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +function NetSuiteInvoiceItemSelectPage({policy}: WithPolicyConnectionsProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const policyID = policy?.id ?? '-1'; + + const config = policy?.connections?.netsuite.options.config; + const netsuiteInvoiceItemOptions = useMemo(() => getNetSuiteInvoiceItemOptions(policy ?? undefined, config?.invoiceItem), [config?.invoiceItem, policy]); + + const initiallyFocusedOptionKey = useMemo(() => netsuiteInvoiceItemOptions?.find((mode) => mode.isSelected)?.keyForList, [netsuiteInvoiceItemOptions]); + + const updateInvoiceItem = useCallback( + ({value}: SelectorType) => { + if (config?.invoiceItem !== value) { + Connections.updateNetSuiteInvoiceItem(policyID, policy?.connections?.netsuite.tokenSecret ?? '', value, config?.invoiceItem); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID)); + }, + [policyID, policy?.connections?.netsuite.tokenSecret, config?.invoiceItem], + ); + + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + + return ( + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID))} + title="workspace.netsuite.invoiceItem.label" + listEmptyContent={listEmptyContent} + connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} + shouldBeBlocked={config?.invoiceItemPreference !== CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.SELECT} + /> + ); +} + +NetSuiteInvoiceItemSelectPage.displayName = 'NetSuiteInvoiceItemSelectPage'; + +export default withPolicyConnections(NetSuiteInvoiceItemSelectPage); diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuitePreferredExporterSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuitePreferredExporterSelectPage.tsx new file mode 100644 index 000000000000..bedf6dad2d5e --- /dev/null +++ b/src/pages/workspace/accounting/netsuite/export/NetSuitePreferredExporterSelectPage.tsx @@ -0,0 +1,104 @@ +import {isEmpty} from 'lodash'; +import React, {useCallback, useMemo} from 'react'; +import {View} from 'react-native'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import SelectionScreen from '@components/SelectionScreen'; +import Text from '@components/Text'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Connections from '@libs/actions/connections/NetSuiteCommands'; +import {getAdminEmployees, isExpensifyTeam} from '@libs/PolicyUtils'; +import Navigation from '@navigation/Navigation'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +type CardListItem = ListItem & { + value: string; +}; + +function NetSuitePreferredExporterSelectPage({policy}: WithPolicyConnectionsProps) { + const config = policy?.connections?.netsuite.options.config; + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const policyOwner = policy?.owner ?? ''; + const exporters = getAdminEmployees(policy); + const {login: currentUserLogin} = useCurrentUserPersonalDetails(); + + const policyID = policy?.id ?? '-1'; + const data: CardListItem[] = useMemo(() => { + if (!isEmpty(policyOwner) && isEmpty(exporters)) { + return [ + { + value: policyOwner, + text: policyOwner, + keyForList: policyOwner, + isSelected: true, + }, + ]; + } + + return exporters?.reduce((options, exporter) => { + if (!exporter.email) { + return options; + } + + // Don't show guides if the current user is not a guide themselves or an Expensify employee + if (isExpensifyTeam(exporter.email) && !isExpensifyTeam(policyOwner) && !isExpensifyTeam(currentUserLogin)) { + return options; + } + + options.push({ + value: exporter.email, + text: exporter.email, + keyForList: exporter.email, + isSelected: config?.exporter === exporter.email, + }); + return options; + }, []); + }, [config?.exporter, exporters, policyOwner, currentUserLogin]); + + const selectExporter = useCallback( + (row: CardListItem) => { + if (row.value !== config?.exporter) { + Connections.updateNetSuiteExporter(policyID, policy?.connections?.netsuite.tokenSecret ?? '', row.value, config?.exporter ?? ''); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID)); + }, + [config?.exporter, policyID, policy?.connections?.netsuite.tokenSecret], + ); + + const headerContent = useMemo( + () => ( + + {translate('workspace.netsuite.exportPreferredExporterNote')} + {translate('workspace.netsuite.exportPreferredExporterSubNote')} + + ), + [translate, styles.pb2, styles.ph5, styles.pb5, styles.textNormal], + ); + + return ( + mode.isSelected)?.keyForList} + onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID))} + title="workspace.netsuite.preferredExporter" + connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} + /> + ); +} + +NetSuitePreferredExporterSelectPage.displayName = 'NetSuitePreferredExporterSelectPage'; + +export default withPolicyConnections(NetSuitePreferredExporterSelectPage); diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx new file mode 100644 index 000000000000..9e5c40630ab4 --- /dev/null +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx @@ -0,0 +1,83 @@ +import React, {useCallback, useMemo} from 'react'; +import BlockingView from '@components/BlockingViews/BlockingView'; +import * as Illustrations from '@components/Icon/Illustrations'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {SelectorType} from '@components/SelectionScreen'; +import SelectionScreen from '@components/SelectionScreen'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Connections from '@libs/actions/connections/NetSuiteCommands'; +import Navigation from '@libs/Navigation/Navigation'; +import {canUseProvTaxNetSuite, getNetSuiteTaxAccountOptions} from '@libs/PolicyUtils'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +function NetSuiteProvincialTaxPostingAccountSelectPage({policy}: WithPolicyConnectionsProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const policyID = policy?.id ?? '-1'; + + const config = policy?.connections?.netsuite.options.config; + const netsuiteTaxAccountOptions = useMemo( + () => getNetSuiteTaxAccountOptions(policy ?? undefined, config?.provincialTaxPostingAccount), + [config?.provincialTaxPostingAccount, policy], + ); + + const initiallyFocusedOptionKey = useMemo(() => netsuiteTaxAccountOptions?.find((mode) => mode.isSelected)?.keyForList, [netsuiteTaxAccountOptions]); + + const {subsidiaryList} = policy?.connections?.netsuite?.options?.data ?? {}; + const selectedSubsidiary = useMemo(() => { + const selectedSub = (subsidiaryList ?? []).find((subsidiary) => subsidiary.internalID === config?.subsidiaryID); + return selectedSub; + }, [subsidiaryList, config?.subsidiaryID]); + + const updateTaxAccount = useCallback( + ({value}: SelectorType) => { + if (config?.provincialTaxPostingAccount !== value) { + Connections.updateNetSuiteProvincialTaxPostingAccount(policyID, policy?.connections?.netsuite.tokenSecret ?? '', value, config?.provincialTaxPostingAccount); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID)); + }, + [policyID, policy?.connections?.netsuite.tokenSecret, config?.provincialTaxPostingAccount], + ); + + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + + return ( + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID))} + title="workspace.netsuite.journalEntriesProvTaxPostingAccount" + listEmptyContent={listEmptyContent} + connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} + shouldBeBlocked={!!config?.suiteTaxEnabled || !config?.syncOptions.syncTax || !canUseProvTaxNetSuite(selectedSubsidiary?.country)} + /> + ); +} + +NetSuiteProvincialTaxPostingAccountSelectPage.displayName = 'NetSuiteProvincialTaxPostingAccountSelectPage'; + +export default withPolicyConnections(NetSuiteProvincialTaxPostingAccountSelectPage); diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteReceivableAccountSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteReceivableAccountSelectPage.tsx new file mode 100644 index 000000000000..36616b6949c6 --- /dev/null +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteReceivableAccountSelectPage.tsx @@ -0,0 +1,76 @@ +import React, {useCallback, useMemo} from 'react'; +import BlockingView from '@components/BlockingViews/BlockingView'; +import * as Illustrations from '@components/Icon/Illustrations'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {SelectorType} from '@components/SelectionScreen'; +import SelectionScreen from '@components/SelectionScreen'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Connections from '@libs/actions/connections/NetSuiteCommands'; +import Navigation from '@libs/Navigation/Navigation'; +import {getNetSuiteReceivableAccountOptions} from '@libs/PolicyUtils'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +function NetSuiteReceivableAccountSelectPage({policy}: WithPolicyConnectionsProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const policyID = policy?.id ?? '-1'; + + const config = policy?.connections?.netsuite.options.config; + const netsuiteReceivableAccountOptions = useMemo( + () => getNetSuiteReceivableAccountOptions(policy ?? undefined, config?.receivableAccount), + [config?.receivableAccount, policy], + ); + + const initiallyFocusedOptionKey = useMemo(() => netsuiteReceivableAccountOptions?.find((mode) => mode.isSelected)?.keyForList, [netsuiteReceivableAccountOptions]); + + const updateReceivableAccount = useCallback( + ({value}: SelectorType) => { + if (config?.receivableAccount !== value) { + Connections.updateNetSuiteReceivableAccount(policyID, policy?.connections?.netsuite.tokenSecret ?? '', value, config?.receivableAccount); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID)); + }, + [policyID, policy?.connections?.netsuite.tokenSecret, config?.receivableAccount], + ); + + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + + return ( + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID))} + title="workspace.netsuite.exportInvoices" + listEmptyContent={listEmptyContent} + connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} + /> + ); +} + +NetSuiteReceivableAccountSelectPage.displayName = 'NetSuiteReceivableAccountSelectPage'; + +export default withPolicyConnections(NetSuiteReceivableAccountSelectPage); diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteTaxPostingAccountSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteTaxPostingAccountSelectPage.tsx new file mode 100644 index 000000000000..61e3315c2e67 --- /dev/null +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteTaxPostingAccountSelectPage.tsx @@ -0,0 +1,82 @@ +import React, {useCallback, useMemo} from 'react'; +import BlockingView from '@components/BlockingViews/BlockingView'; +import * as Illustrations from '@components/Icon/Illustrations'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {SelectorType} from '@components/SelectionScreen'; +import SelectionScreen from '@components/SelectionScreen'; +import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Connections from '@libs/actions/connections/NetSuiteCommands'; +import Navigation from '@libs/Navigation/Navigation'; +import {canUseTaxNetSuite, getNetSuiteTaxAccountOptions} from '@libs/PolicyUtils'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +function NetSuiteTaxPostingAccountSelectPage({policy}: WithPolicyConnectionsProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {canUseNetSuiteUSATax} = usePermissions(); + + const policyID = policy?.id ?? '-1'; + + const config = policy?.connections?.netsuite.options.config; + const netsuiteTaxAccountOptions = useMemo(() => getNetSuiteTaxAccountOptions(policy ?? undefined, config?.taxPostingAccount), [config?.taxPostingAccount, policy]); + + const initiallyFocusedOptionKey = useMemo(() => netsuiteTaxAccountOptions?.find((mode) => mode.isSelected)?.keyForList, [netsuiteTaxAccountOptions]); + + const {subsidiaryList} = policy?.connections?.netsuite?.options?.data ?? {}; + const selectedSubsidiary = useMemo(() => { + const selectedSub = (subsidiaryList ?? []).find((subsidiary) => subsidiary.internalID === config?.subsidiaryID); + return selectedSub; + }, [subsidiaryList, config?.subsidiaryID]); + + const updateTaxAccount = useCallback( + ({value}: SelectorType) => { + if (config?.taxPostingAccount !== value) { + Connections.updateNetSuiteTaxPostingAccount(policyID, policy?.connections?.netsuite.tokenSecret ?? '', value, config?.taxPostingAccount); + } + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID)); + }, + [policyID, policy?.connections?.netsuite.tokenSecret, config?.taxPostingAccount], + ); + + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.pb10], + ); + + return ( + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID))} + title="workspace.netsuite.journalEntriesTaxPostingAccount" + listEmptyContent={listEmptyContent} + connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} + shouldBeBlocked={!!config?.suiteTaxEnabled || !config?.syncOptions.syncTax || !canUseTaxNetSuite(canUseNetSuiteUSATax, selectedSubsidiary?.country)} + /> + ); +} + +NetSuiteTaxPostingAccountSelectPage.displayName = 'NetSuiteTaxPostingAccountSelectPage'; + +export default withPolicyConnections(NetSuiteTaxPostingAccountSelectPage); diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index bd8f5e7d33d9..d72f64cb4a4a 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -667,19 +667,19 @@ type NetSuiteConnectionData = { subsidiaryList: Subsidiary[]; /** Collection of receivable accounts */ - receivableList: NetSuiteAccount[]; + receivableList?: NetSuiteAccount[]; /** Collection of vendors */ - vendors: NetSuiteVendor[]; + vendors?: NetSuiteVendor[]; /** Collection of invoice items */ - items: InvoiceItem[]; + items?: InvoiceItem[]; /** Collection of the payable accounts */ payableList: NetSuiteAccount[]; /** Collection of tax accounts */ - taxAccountsList: NetSuiteTaxAccount[]; + taxAccountsList?: NetSuiteTaxAccount[]; }; /** NetSuite mapping values */ @@ -721,19 +721,19 @@ type NetSuiteCustomFormIDOptions = { /** User configuration for the NetSuite accounting integration. */ type NetSuiteConnectionConfig = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Invoice Item Preference */ - invoiceItemPreference: NetSuiteInvoiceItemPreferenceValues; + invoiceItemPreference?: NetSuiteInvoiceItemPreferenceValues; /** ID of the bank account for NetSuite invoice collections */ - receivableAccount: string; + receivableAccount?: string; /** ID of the bank account for NetSuite tax posting */ - taxPostingAccount: string; + taxPostingAccount?: string; /** Whether we should export to the most recent open period if the current one is closed */ exportToNextOpenPeriod: boolean; /** Whether we will include the original foreign amount of a transaction to NetSuite */ - allowForeignCurrency: boolean; + allowForeignCurrency?: boolean; /** Where to export reimbursable expenses */ reimbursableExpensesExportDestination: NetSuiteExportDestinationValues; @@ -768,7 +768,7 @@ type NetSuiteConnectionConfig = OnyxCommon.OnyxValueWithOfflineFeedback<{ syncApprovalWorkflow: boolean; /** Whether we import custom lists from NetSuite */ - syncCustomLists: boolean; + syncCustomLists?: boolean; /** The approval level we set for an Expense Report record created in NetSuite */ exportReportsTo: NetSuiteExpenseReportApprovalLevels; @@ -783,7 +783,7 @@ type NetSuiteConnectionConfig = OnyxCommon.OnyxValueWithOfflineFeedback<{ syncReimbursedReports: boolean; /** The relevant details of the custom segments we import into Expensify and code onto expenses */ - customSegments: Array<{ + customSegments?: Array<{ /** The name of the custom segment */ segmentName: string; @@ -801,7 +801,7 @@ type NetSuiteConnectionConfig = OnyxCommon.OnyxValueWithOfflineFeedback<{ syncPeople: boolean; /** Whether to enable a new Expense Category into Expensify */ - enableNewCategories: boolean; + enableNewCategories?: boolean; /** A now unused configuration saying whether a customer had toggled AutoSync yet. */ hasChosenAutoSyncOption: boolean; @@ -810,13 +810,13 @@ type NetSuiteConnectionConfig = OnyxCommon.OnyxValueWithOfflineFeedback<{ finalApprover: string; /** Whether to import tax groups from NetSuite */ - syncTax: boolean; + syncTax?: boolean; /** Whether to import custom segments from NetSuite */ - syncCustomSegments: boolean; + syncCustomSegments?: boolean; /** The relevant details of the custom lists we import into Expensify and code onto expenses */ - customLists: Array<{ + customLists?: Array<{ /** The name of the custom list in NetSuite */ listName: string; @@ -847,7 +847,7 @@ type NetSuiteConnectionConfig = OnyxCommon.OnyxValueWithOfflineFeedback<{ exporter: string; /** The transaction date to set upon export */ - exportDate: NetSuiteExportDateOptions; + exportDate?: NetSuiteExportDateOptions; /** The type of transaction in NetSuite we export non-reimbursable transactions to */ nonreimbursableExpensesExportDestination: NetSuiteExportDestinationValues; @@ -856,22 +856,22 @@ type NetSuiteConnectionConfig = OnyxCommon.OnyxValueWithOfflineFeedback<{ reimbursablePayableAccount: string; /** Whether we post Journals as individual separate entries or a single unified entry */ - journalPostingPreference: NetSuiteJournalPostingPreferences; + journalPostingPreference?: NetSuiteJournalPostingPreferences; /** The Item record to associate with lines on an invoice created via Expensify */ - invoiceItem: string; + invoiceItem?: string; /** The internaID of the selected subsidiary in NetSuite */ - subsidiaryID: string; + subsidiaryID?: string; /** The default vendor to use for Transactions in NetSuite */ - defaultVendor: string; + defaultVendor?: string; /** The provincial tax account for tax line items in NetSuite (only for Canadian Subsidiaries) */ - provincialTaxPostingAccount: string; + provincialTaxPostingAccount?: string; /** The account used for reimbursement in NetSuite */ - reimbursementAccountID: string; + reimbursementAccountID?: string; /** The account used for approvals in NetSuite */ approvalAccount: string; @@ -880,7 +880,7 @@ type NetSuiteConnectionConfig = OnyxCommon.OnyxValueWithOfflineFeedback<{ payableAcct: string; /** Configurations for customer to set custom forms for which reimbursable and non-reimbursable transactions will export to in NetSuite */ - customFormIDOptions: { + customFormIDOptions?: { /** The custom form selections for reimbursable transactions */ reimbursable: RequireExactlyOne; @@ -892,10 +892,10 @@ type NetSuiteConnectionConfig = OnyxCommon.OnyxValueWithOfflineFeedback<{ }; /** The account to use for Invoices export to NetSuite */ - collectionAccount: string; + collectionAccount?: string; /** Whether this account is using the newer version of tax in NetSuite, SuiteTax */ - suiteTaxEnabled: boolean; + suiteTaxEnabled?: boolean; /** Collection of errors coming from BE */ errors?: OnyxCommon.Errors; From 4d2b099bdc27d1eda92cbc512c75caa7d7ed8077 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 21 Jun 2024 17:04:11 +0200 Subject: [PATCH 026/183] fix: lint and ts --- .../API/parameters/RequestRefundParams.ts | 3 + src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 1 + src/pages/settings/InitialSettingsPage.tsx | 3 - .../Subscription/CardSection/CardSection.tsx | 61 +++++++------------ 5 files changed, 27 insertions(+), 42 deletions(-) create mode 100644 src/libs/API/parameters/RequestRefundParams.ts diff --git a/src/libs/API/parameters/RequestRefundParams.ts b/src/libs/API/parameters/RequestRefundParams.ts new file mode 100644 index 000000000000..697bc7a1263e --- /dev/null +++ b/src/libs/API/parameters/RequestRefundParams.ts @@ -0,0 +1,3 @@ +type RequestRefundParams = Record; + +export default RequestRefundParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 26df06fc6294..01a86f480a85 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -231,3 +231,4 @@ export type {default as UpdateSubscriptionAutoRenewParams} from './UpdateSubscri export type {default as UpdateSubscriptionAddNewUsersAutomaticallyParams} from './UpdateSubscriptionAddNewUsersAutomaticallyParams'; export type {default as GenerateSpotnanaTokenParams} from './GenerateSpotnanaTokenParams'; export type {default as UpdateSubscriptionSizeParams} from './UpdateSubscriptionSizeParams'; +export type {default as RequestRefundParams} from './RequestRefundParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index fc31c9df6370..89defe7393c8 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -456,6 +456,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_AUTO_RENEW]: Parameters.UpdateSubscriptionAutoRenewParams; [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_ADD_NEW_USERS_AUTOMATICALLY]: Parameters.UpdateSubscriptionAddNewUsersAutomaticallyParams; [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_SIZE]: Parameters.UpdateSubscriptionSizeParams; + [WRITE_COMMANDS.REQUEST_REFUND]: Parameters.RequestRefundParams; }; const READ_COMMANDS = { diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index 5d1f573af9f5..8fa152d5c06a 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -106,9 +106,6 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa const emojiCode = currentUserPersonalDetails?.status?.emojiCode ?? ''; const [shouldShowSignoutConfirmModal, setShouldShowSignoutConfirmModal] = useState(false); - useEffect(() => { - Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION); - }, []); useEffect(() => { Wallet.openInitialSettingsPage(); diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 8f0de818397d..b64323354696 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -1,6 +1,6 @@ -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import {View} from 'react-native'; -import Onyx, {useOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -40,25 +40,6 @@ function CardSection() { const BillingBanner = ; - useEffect(() => { - Onyx.merge(ONYXKEYS.FUND_LIST, [ - { - accountData: { - cardMonth: 11, - - cardNumber: '1234', - - cardYear: 2026, - - currency: 'USD', - - addressName: 'John Doe', - }, - isDefault: true, - }, - ]); - }, [fundList]); - return ( <>
} - {!isEmptyObject(defaultCard?.accountData && plan) && ( + {!isEmptyObject(defaultCard?.accountData && plan && isEligibleForRefund) && ( - { - setIsRequestRefundModalVisible(false); - }} - prompt={ - <> - {translate('subscription.cardSection.requestRefundModal.phrase1')} - {translate('subscription.cardSection.requestRefundModal.phrase2')} - - } - confirmText={translate('subscription.cardSection.requestRefundModal.confirm')} - cancelText={translate('common.cancel')} - danger - /> + {isEligibleForRefund && ( + { + setIsRequestRefundModalVisible(false); + }} + prompt={ + <> + {translate('subscription.cardSection.requestRefundModal.phrase1')} + {translate('subscription.cardSection.requestRefundModal.phrase2')} + + } + confirmText={translate('subscription.cardSection.requestRefundModal.confirm')} + cancelText={translate('common.cancel')} + danger + /> + )} ); } From 7a2754b257b1c40be8aa9e80a299a3c255553558 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Fri, 21 Jun 2024 18:05:03 +0100 Subject: [PATCH 027/183] feat: implement free trial banner and badges --- .../LHNOptionsList/OptionRowLHN.tsx | 9 ++++++++ src/languages/en.ts | 7 ++++++ src/languages/es.ts | 7 ++++++ src/pages/home/HeaderView.tsx | 8 +++++++ src/pages/settings/InitialSettingsPage.tsx | 9 ++++++-- .../BillingBanner/TrialBillingBanner.tsx | 22 +++++++++++++++++++ .../Subscription/CardSection/CardSection.tsx | 4 +++- src/styles/utils/sizing.ts | 4 ++++ 8 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 src/pages/settings/Subscription/CardSection/BillingBanner/TrialBillingBanner.tsx diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index c7797a37fd12..8a6e3b8cef2c 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -3,6 +3,7 @@ import React, {useCallback, useRef, useState} from 'react'; import type {GestureResponderEvent, ViewStyle} from 'react-native'; import {StyleSheet, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import Badge from '@components/Badge'; import DisplayNames from '@components/DisplayNames'; import Hoverable from '@components/Hoverable'; import Icon from '@components/Icon'; @@ -25,6 +26,7 @@ import * as OptionsListUtils from '@libs/OptionsListUtils'; import Performance from '@libs/Performance'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import * as ReportUtils from '@libs/ReportUtils'; +import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -227,6 +229,13 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti ReportUtils.isSystemChat(report) } /> + {ReportUtils.isChatUsedForOnboarding(report) && SubscriptionUtils.isUserOnFreeTrial() && ( + + )} {isStatusVisible && ( `Free trial: ${numOfDays} ${numOfDays === 1 ? 'day' : 'days'} left`, + }, billingBanner: { preTrial: { title: 'Start a free trial', subtitle: 'To get started, ', subtitleLink: 'complete your setup checklist here', }, + trial: { + title: ({numOfDays}) => `Free trial: ${numOfDays} ${numOfDays === 1 ? 'day' : 'days'} left!`, + subtitle: 'Add a payment card below to continue using all of your favorite features.', + }, }, cardSection: { title: 'Payment', diff --git a/src/languages/es.ts b/src/languages/es.ts index 1efff57d3e3c..21009d7ada7b 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3724,12 +3724,19 @@ export default { }, subscription: { mobileReducedFunctionalityMessage: 'No puedes hacer cambios en tu suscripción en la aplicación móvil.', + badge: { + trial: ({numOfDays}) => `Prueba gratuita: ${numOfDays === 1 ? `queda 1 día` : `quedan ${numOfDays} días`}`, + }, billingBanner: { preTrial: { title: 'Iniciar una prueba gratuita', subtitle: 'Para empezar, ', subtitleLink: 'completa la lista de configuración aquí', }, + trial: { + title: ({numOfDays}) => `Prueba gratuita: ¡${numOfDays === 1 ? `queda 1 día` : `quedan ${numOfDays} días`}!`, + subtitle: 'Añade una tarjeta de pago para seguir utilizando tus funciones favoritas.', + }, }, cardSection: { title: 'Pago', diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx index 699fbab345cb..08bbfe303c70 100644 --- a/src/pages/home/HeaderView.tsx +++ b/src/pages/home/HeaderView.tsx @@ -2,6 +2,7 @@ import React, {memo, useMemo} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import Badge from '@components/Badge'; import Button from '@components/Button'; import CaretWrapper from '@components/CaretWrapper'; import ConfirmModal from '@components/ConfirmModal'; @@ -27,6 +28,7 @@ import * as HeaderUtils from '@libs/HeaderUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import * as Link from '@userActions/Link'; import * as Report from '@userActions/Report'; import * as Session from '@userActions/Session'; @@ -346,6 +348,12 @@ function HeaderView({ )} + {ReportUtils.isChatUsedForOnboarding(report) && SubscriptionUtils.isUserOnFreeTrial() && ( + + )} {isTaskReport && !shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && } {canJoin && !shouldUseNarrowLayout && joinButton} {shouldShowThreeDotsButton && ( diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index 8fa152d5c06a..5a8513b37875 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -30,6 +30,7 @@ import useWaitForNavigation from '@hooks/useWaitForNavigation'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import shouldShowSubscriptionsMenu from '@libs/shouldShowSubscriptionsMenu'; +import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import * as UserUtils from '@libs/UserUtils'; import {hasGlobalWorkspaceSettingsRBR} from '@libs/WorkspacesSettingsUtils'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; @@ -90,6 +91,8 @@ type MenuData = { title?: string; shouldShowRightIcon?: boolean; iconRight?: IconAsset; + badgeText?: string; + badgeStyle?: ViewStyle; }; type Menu = {sectionStyle: StyleProp; sectionTranslationKey: TranslationPaths; items: MenuData[]}; @@ -209,6 +212,7 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa shouldShowRightIcon: true, iconRight: Expensicons.NewWindow, link: () => Link.buildOldDotURL(CONST.OLDDOT_URLS.ADMIN_POLICIES_URL), + badgeText: SubscriptionUtils.isUserOnFreeTrial() ? translate('subscription.badge.trial', {numOfDays: SubscriptionUtils.calculateRemainingFreeTrialDays()}) : '', }); } @@ -217,7 +221,7 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa sectionTranslationKey: 'common.workspaces', items, }; - }, [policies, styles.workspaceSettingsSectionContainer]); + }, [policies, styles.workspaceSettingsSectionContainer, translate]); /** * Retuns a list of menu items data for general section @@ -315,7 +319,8 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa } })} iconStyles={item.iconStyles} - badgeText={getWalletBalance(isPaymentItem)} + badgeText={item.badgeText ?? getWalletBalance(isPaymentItem)} + badgeStyle={item.badgeStyle} fallbackIcon={item.fallbackIcon} brickRoadIndicator={item.brickRoadIndicator} floatRightAvatars={item.floatRightAvatars} diff --git a/src/pages/settings/Subscription/CardSection/BillingBanner/TrialBillingBanner.tsx b/src/pages/settings/Subscription/CardSection/BillingBanner/TrialBillingBanner.tsx new file mode 100644 index 000000000000..cc8079859f83 --- /dev/null +++ b/src/pages/settings/Subscription/CardSection/BillingBanner/TrialBillingBanner.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import * as Illustrations from '@components/Icon/Illustrations'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import * as SubscriptionUtils from '@libs/SubscriptionUtils'; +import BillingBanner from './BillingBanner'; + +function TrialBillingBanner() { + const {translate} = useLocalize(); + + return ( + {translate('subscription.billingBanner.trial.subtitle')}} + icon={Illustrations.TreasureChest} + /> + ); +} + +TrialBillingBanner.displayName = 'PreTrialBillingBanner'; + +export default TrialBillingBanner; diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 7f80b189c517..e0950ffafdd0 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -9,9 +9,11 @@ import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; +import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import PreTrialBillingBanner from './BillingBanner/PreTrialBillingBanner'; +import TrialBillingBanner from './BillingBanner/TrialBillingBanner'; import CardSectionActions from './CardSectionActions'; import CardSectionDataEmpty from './CardSectionDataEmpty'; @@ -25,7 +27,7 @@ function CardSection() { const cardMonth = useMemo(() => DateUtils.getMonthNames(preferredLocale)[(defaultCard?.accountData?.cardMonth ?? 1) - 1], [defaultCard?.accountData?.cardMonth, preferredLocale]); - const BillingBanner = ; + const BillingBanner = SubscriptionUtils.isUserOnFreeTrial() ? : ; return (
Date: Fri, 21 Jun 2024 16:31:52 -0400 Subject: [PATCH 028/183] remove clearReportNotFoundErrors --- src/libs/actions/Report.ts | 13 ------------- src/pages/home/ReportScreen.tsx | 10 ---------- 2 files changed, 23 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index bcfe3f30f8f4..e8332dd28289 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2240,18 +2240,6 @@ function clearPolicyRoomNameErrors(reportID: string) { }); } -/** - * @param reportID The reportID of the report. - */ -// eslint-disable-next-line rulesdir/no-negated-variables -function clearReportNotFoundErrors(reportID: string) { - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { - errorFields: { - notFound: null, - }, - }); -} - function setIsComposerFullSize(reportID: string, isComposerFullSize: boolean) { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportID}`, isComposerFullSize); } @@ -3766,7 +3754,6 @@ export { toggleSubscribeToChildReport, updatePolicyRoomNameAndNavigate, clearPolicyRoomNameErrors, - clearReportNotFoundErrors, clearIOUError, subscribeToNewActionEvent, notifyNewAction, diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 37b9184a3250..5fbb00e8ebdf 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -400,16 +400,6 @@ function ReportScreen({ Report.openReport(reportIDFromRoute, reportActionIDFromRoute); }, [reportIDFromRoute, reportActionIDFromRoute]); - useEffect(() => { - if (!report.reportID) { - return; - } - - if (report?.errorFields?.notFound) { - Report.clearReportNotFoundErrors(report.reportID); - } - }, [report?.errorFields?.notFound, report.reportID]); - useEffect(() => { if (!report.reportID || !isFocused) { return; From bdbd9c3e824c2f050b3c5d43eebb445d53a0dd77 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Sat, 22 Jun 2024 05:06:36 +0700 Subject: [PATCH 029/183] update after merging main --- src/libs/actions/Task.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index cc2a6530cbb8..ebd00ee1f5e3 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -319,7 +319,7 @@ function getOutstandingChildTask(taskReport: OnyxEntry) { reportAction.childType === CONST.REPORT.TYPE.TASK && reportAction?.childStateNum === CONST.REPORT.STATE_NUM.OPEN && reportAction?.childStatusNum === CONST.REPORT.STATUS_NUM.OPEN && - !reportAction?.message?.[0]?.isDeletedParentAction + ReportActionsUtils.getReportActionMessage(reportAction)?.isDeletedParentAction ) { return true; } From 5eaf50c908ca958277c8594fb9b739c6a53481bd Mon Sep 17 00:00:00 2001 From: Shubham Agrawal <58412969+shubham1206agra@users.noreply.github.com> Date: Sat, 22 Jun 2024 06:48:58 +0530 Subject: [PATCH 030/183] Apply suggestions from code review --- src/components/MenuItem.tsx | 2 +- src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 7d355ec66391..38b6f3f9b658 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -434,7 +434,7 @@ function MenuItem( }, [title, shouldParseTitle, shouldEscapeText]); const helperHtml = useMemo(() => { - if (!helperText || !shouldParseTitle) { + if (!helperText || ! shouldParseHelperText) { return ''; } const parser = new ExpensiMark(); diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index 6a4b971723c3..c7b4ac4db9ce 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -54,7 +54,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.ACCOUNTING.XERO_EXPORT_PREFERRED_EXPORTER_SELECT, SCREENS.WORKSPACE.ACCOUNTING.XERO_BILL_PAYMENT_ACCOUNT_SELECTOR, SCREENS.WORKSPACE.ACCOUNTING.XERO_EXPORT_BANK_ACCOUNT_SELECT, - SCREENS.WORKSPACE.ACCOUNTING.XERO_EXPORT, + SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT, SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_PREFERRED_EXPORTER_SELECT, SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_DATE_SELECT, SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES, From 386aa845385afbfab229b316c71e9d0bb21e5fc7 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal <58412969+shubham1206agra@users.noreply.github.com> Date: Sat, 22 Jun 2024 06:50:22 +0530 Subject: [PATCH 031/183] Removed extra space --- src/components/MenuItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 38b6f3f9b658..86e72a5ab1fb 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -434,7 +434,7 @@ function MenuItem( }, [title, shouldParseTitle, shouldEscapeText]); const helperHtml = useMemo(() => { - if (!helperText || ! shouldParseHelperText) { + if (!helperText || !shouldParseHelperText) { return ''; } const parser = new ExpensiMark(); From aab6e9811fa1b5e506c0b017c989e0cd1db9b82f Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sat, 22 Jun 2024 06:58:20 +0530 Subject: [PATCH 032/183] Revert some wrong changes --- src/libs/actions/connections/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/connections/index.ts b/src/libs/actions/connections/index.ts index 852be5d4e1c6..4aaaf705527a 100644 --- a/src/libs/actions/connections/index.ts +++ b/src/libs/actions/connections/index.ts @@ -240,7 +240,7 @@ function updateManyPolicyConnectionConfigs, connectionName: PolicyConnectionName, isSyncInProgress: boolean): boolean { // NetSuite does not use the conventional lastSync object, so we need to check for lastErrorSyncDate if (connectionName === CONST.POLICY.CONNECTIONS.NAME.NETSUITE) { - return !isSyncInProgress && policy?.connections?.[connectionName]?.verified === false; + return !isSyncInProgress && !!policy?.connections?.[CONST.POLICY.CONNECTIONS.NAME.NETSUITE].lastErrorSyncDate; } return !isSyncInProgress && policy?.connections?.[connectionName]?.lastSync?.isSuccessful === false; } From 755d348529e732ffa591d8a7b23d3d90db3d5beb Mon Sep 17 00:00:00 2001 From: Shubham Agrawal <58412969+shubham1206agra@users.noreply.github.com> Date: Sat, 22 Jun 2024 07:00:47 +0530 Subject: [PATCH 033/183] Fix lint --- src/components/MenuItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 86e72a5ab1fb..cb1b916d93fe 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -439,7 +439,7 @@ function MenuItem( } const parser = new ExpensiMark(); return parser.replace(helperText, {shouldEscapeText}); - }, [helperText, shouldParseTitle, shouldEscapeText]); + }, [helperText, shouldParseHelperText, shouldEscapeText]); const processedTitle = useMemo(() => { let titleToWrap = ''; From c84a364d3b6b0eefad774e0ca5e6d97d9d1caecc Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sat, 22 Jun 2024 13:04:20 +0530 Subject: [PATCH 034/183] Fixed divider spacing and NetSuite type --- .../netsuite/export/NetSuiteExportConfigurationPage.tsx | 2 +- src/styles/index.ts | 9 +++++++++ src/types/onyx/Policy.ts | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx index ac247c8de1cd..aa1d7cf46bae 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx @@ -222,7 +222,7 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) { .filter((item) => !item.shouldHide) .map((item) => { if (item.type === 'divider') { - return ; + return ; } if (item.type === 'toggle') { const {type, shouldHide, ...rest} = item; diff --git a/src/styles/index.ts b/src/styles/index.ts index 7aacb77965c7..8af89a5afd8c 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -2912,6 +2912,15 @@ const styles = (theme: ThemeColors) => marginRight: 20, }, + dividerLine: { + height: 1, + backgroundColor: theme.border, + flexGrow: 1, + marginLeft: 20, + marginRight: 20, + marginVertical: 12, + }, + unreadIndicatorText: { color: theme.unreadIndicator, fontFamily: FontUtils.fontFamily.platform.EXP_NEUE_BOLD, diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index d72f64cb4a4a..55c5a26d11b5 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -616,7 +616,7 @@ type AccountTypeValues = '_accountsPayable' | '_otherCurrentLiability' | '_credi type NetSuiteAccount = { /** GL code assigned to the financial account */ // eslint-disable-next-line @typescript-eslint/naming-convention - 'GL Code': string; + 'GL Code'?: string; /** Name of the account */ name: string; From e47a86804c70ea42fe0a2040cedbbee1491a69c5 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sat, 22 Jun 2024 13:05:31 +0530 Subject: [PATCH 035/183] Fixed toggle spacing --- .../netsuite/export/NetSuiteExportConfigurationPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx index aa1d7cf46bae..c65f6b7ea628 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx @@ -231,7 +231,7 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) { key={rest.title} // eslint-disable-next-line react/jsx-props-no-spreading {...rest} - wrapperStyle={styles.mv3} + wrapperStyle={[styles.mv3, styles.ph5]} /> ); } From ba804f68f5cc0719900a6fca58882f94a74c4154 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sat, 22 Jun 2024 13:26:09 +0530 Subject: [PATCH 036/183] Fixed style of html helper text --- .../HTMLEngineProvider/BaseHTMLEngineProvider.tsx | 7 ++++++- src/components/MenuItem.tsx | 2 +- src/styles/index.ts | 5 +++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx index d95fc9e11a31..403c1caea0eb 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx @@ -42,6 +42,11 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim mixedUAStyles: {...styles.colorMuted, ...styles.mb0}, contentModel: HTMLContentModel.block, }), + 'muted-text-label': HTMLElementModel.fromCustomModel({ + tagName: 'muted-text-label', + mixedUAStyles: {...styles.colorMuted, ...styles.mb0, ...styles.labelNormal}, + contentModel: HTMLContentModel.block, + }), comment: HTMLElementModel.fromCustomModel({ tagName: 'comment', mixedUAStyles: {whiteSpace: 'pre'}, @@ -83,7 +88,7 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim contentModel: HTMLContentModel.block, }), }), - [styles.formError, styles.mb0, styles.colorMuted, styles.textLabelSupporting, styles.lh16, styles.textSupporting, styles.textLineThrough, styles.mt4], + [styles.formError, styles.mb0, styles.colorMuted, styles.textLabelSupporting, styles.lh16, styles.textSupporting, styles.textLineThrough, styles.mt4, styles.labelNormal], ); /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index cb1b916d93fe..c8147689228e 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -465,7 +465,7 @@ function MenuItem( textToWrap = helperHtml; } - return textToWrap ? `${textToWrap}` : ''; + return textToWrap ? `${textToWrap}` : ''; }, [shouldParseHelperText, helperHtml]); const hasPressableRightComponent = iconRight || (shouldShowRightComponent && rightComponent); diff --git a/src/styles/index.ts b/src/styles/index.ts index 8af89a5afd8c..4cf9af4e5125 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -369,6 +369,11 @@ const styles = (theme: ThemeColors) => lineHeight: variables.lineHeightLarge, }, + labelNormal: { + fontSize: variables.fontSizeLabel, + lineHeight: variables.lineHeightNormal, + }, + textLabel: { color: theme.text, fontSize: variables.fontSizeLabel, From cf199f1650663bafd04fbc8132d6b790fd620866 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sat, 22 Jun 2024 14:07:39 +0530 Subject: [PATCH 037/183] Fixed error style --- .../netsuite/export/NetSuiteExportConfigurationPage.tsx | 2 +- .../accounting/netsuite/export/NetSuiteExportExpensesPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx index c65f6b7ea628..b5336d5b2204 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx @@ -240,7 +240,7 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) { key={item.title} pendingAction={item.pendingAction} errors={item.errors} - errorRowStyles={[styles.mt2]} + errorRowStyles={[styles.ph5]} onClose={item.onCloseError} > Date: Sat, 22 Jun 2024 14:53:59 +0530 Subject: [PATCH 038/183] Fixed hide condition for posting account --- .../accounting/netsuite/export/NetSuiteExportExpensesPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx index 3575afd803b0..e7f7afbb5966 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx @@ -101,7 +101,7 @@ function NetSuiteExportExpensesPage({policy}: WithPolicyConnectionsProps) { pendingAction: config?.pendingFields?.payableAcct, errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.PAYABLE_ACCT), onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.PAYABLE_ACCT), - shouldHide: exportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY, + shouldHide: isReimbursable || exportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY, }, { description: translate('workspace.netsuite.reimbursableJournalPostingAccount'), @@ -111,7 +111,7 @@ function NetSuiteExportExpensesPage({policy}: WithPolicyConnectionsProps) { pendingAction: config?.pendingFields?.reimbursablePayableAccount, errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.REIMBURSABLE_PAYABLE_ACCOUNT), onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.REIMBURSABLE_PAYABLE_ACCOUNT), - shouldHide: exportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY, + shouldHide: !isReimbursable || exportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY, }, { description: translate('workspace.netsuite.journalPostingPreference.label'), From 11cd7df7ba1f2fc23f6d0474856582a3117cd208 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Mon, 24 Jun 2024 15:16:55 +0100 Subject: [PATCH 039/183] ts + lint --- src/libs/actions/Report.ts | 2 +- src/pages/NewChatConfirmPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index b1d120962e4c..f0bb402da881 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2551,7 +2551,7 @@ function getCurrentUserAccountID(): number { } function navigateToMostRecentReport(currentReport: OnyxEntry) { - const lastAccessedReportID = ReportUtils.findLastAccessedReport(allReports, false, null, false, false, reportMetadata, undefined, [], currentReport?.reportID)?.reportID; + const lastAccessedReportID = ReportUtils.findLastAccessedReport(allReports, false, undefined, false, false, reportMetadata, undefined, [], currentReport?.reportID)?.reportID; if (lastAccessedReportID) { const lastAccessedReportRoute = ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID ?? '-1'); diff --git a/src/pages/NewChatConfirmPage.tsx b/src/pages/NewChatConfirmPage.tsx index be810ff146d7..17e5708803cd 100644 --- a/src/pages/NewChatConfirmPage.tsx +++ b/src/pages/NewChatConfirmPage.tsx @@ -106,7 +106,7 @@ function NewChatConfirmPage({newGroupDraft, allPersonalDetails}: NewChatConfirmP const logins: string[] = (newGroupDraft.participants ?? []).map((participant) => participant.login); Report.navigateToAndOpenReport(logins, true, undefined, newGroupDraft.reportName ?? '', newGroupDraft.avatarUri ?? '', avatarFile, optimisticReportID.current, true); - }, [newGroupDraft]); + }, [newGroupDraft, avatarFile]); const stashedLocalAvatarImage = newGroupDraft?.avatarUri; From db927894fedf9b1949671bb705f716c0567d4ea8 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 24 Jun 2024 16:23:38 +0200 Subject: [PATCH 040/183] apply changes based on feedback from Fabio --- src/CONST.ts | 5 + src/ONYXKEYS.ts | 32 +---- src/components/Indicator.tsx | 4 +- src/libs/SubscriptionUtils.ts | 136 ++++++++++-------- .../BillingBanner/BillingBanner.tsx | 2 +- .../SubscriptionBillingBanner.tsx | 14 +- .../Subscription/CardSection/CardSection.tsx | 38 ++--- .../Subscription/CardSection/utils.ts | 26 ++-- src/types/onyx/BillingStatus.ts | 16 +++ src/types/onyx/RetryBillingStatus.ts | 7 + src/types/onyx/StripeCustomerID.ts | 16 +++ src/types/onyx/index.ts | 6 + tests/unit/SubscriptionUtilsTest.ts | 6 +- 13 files changed, 180 insertions(+), 128 deletions(-) create mode 100644 src/types/onyx/BillingStatus.ts create mode 100644 src/types/onyx/RetryBillingStatus.ts create mode 100644 src/types/onyx/StripeCustomerID.ts diff --git a/src/CONST.ts b/src/CONST.ts index c485268b55e2..6cf547f66fdd 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4904,6 +4904,11 @@ const CONST = { }, EXCLUDE_FROM_LAST_VISITED_PATH: [SCREENS.NOT_FOUND, SCREENS.SAML_SIGN_IN, SCREENS.VALIDATE_LOGIN] as string[], + + SUBSCRIPTION_RETRY_BILLING_STATUS: { + SUCCESS: 'success', + FAILED: 'failed', + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 676158885333..dfc405158d81 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -157,9 +157,6 @@ const ONYXKEYS = { /** Store the state of the subscription */ NVP_PRIVATE_SUBSCRIPTION: 'nvp_private_subscription', - /** Store the amount of owed money */ - NVP_PRIVATE_AMOUNT_OWED: 'nvp_private_amountOwed', - /** Store the stripe id status */ NVP_PRIVATE_STRIPE_CUSTOMER_ID: 'nvp_private_stripeCustomerID', @@ -191,7 +188,7 @@ const ONYXKEYS = { NVP_BILLING_FUND_ID: 'nvp_expensify_billingFundID', /** The amount owed by the workspace’s owner. */ - NVP_PRIVATE_AMOUNT_OWNED: 'nvp_private_amountOwed', + NVP_PRIVATE_AMOUNT_OWED: 'nvp_private_amountOwed', /** The end date (epoch timestamp) of the workspace owner’s grace period after the free trial ends. */ NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END: 'nvp_private_billingGracePeriodEnd', @@ -351,12 +348,9 @@ const ONYXKEYS = { /** Holds the checks used while transferring the ownership of the workspace */ POLICY_OWNERSHIP_CHANGE_CHECKS: 'policyOwnershipChangeChecks', - /** Indicates whether ClearOutstandingBalance failed */ - SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED: 'subscriptionRetryBillingStatusFailed', + /** Indicates the result of ClearOutstandingBalance, it can either be success or failed */ + SUBSCRIPTION_RETRY_BILLING_STATUS: 'subscriptionRetryBillingStatus', - /** Indicates whether ClearOutstandingBalance was successful */ - SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL: 'subscriptionRetryBillingStatusSuccessful', - /** Stores info during review duplicates flow */ REVIEW_DUPLICATES: 'reviewDuplicates', @@ -693,20 +687,9 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS]: OnyxTypes.DismissedReferralBanners; [ONYXKEYS.NVP_HAS_SEEN_TRACK_TRAINING]: boolean; [ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION]: OnyxTypes.PrivateSubscription; - [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: number; - [ONYXKEYS.NVP_PRIVATE_STRIPE_CUSTOMER_ID]: { - paymentMethodID: string; - intentsID: string; - currency: string; - status: 'authentication_required' | 'intent_required' | 'succeeded'; - }; + [ONYXKEYS.NVP_PRIVATE_STRIPE_CUSTOMER_ID]: OnyxTypes.StripeCustomerID; [ONYXKEYS.NVP_PRIVATE_BILLING_DISPUTE_PENDING]: number; - [ONYXKEYS.NVP_PRIVATE_BILLING_STATUS]: { - action: string; - periodMonth: string; - periodYear: string; - declineReason: 'insufficient_funds' | 'expired_card'; - }; + [ONYXKEYS.NVP_PRIVATE_BILLING_STATUS]: OnyxTypes.BillingStatus; [ONYXKEYS.USER_WALLET]: OnyxTypes.UserWallet; [ONYXKEYS.WALLET_ONFIDO]: OnyxTypes.WalletOnfido; [ONYXKEYS.WALLET_ADDITIONAL_DETAILS]: OnyxTypes.WalletAdditionalDetails; @@ -754,13 +737,12 @@ type OnyxValuesMapping = { [ONYXKEYS.CACHED_PDF_PATHS]: Record; [ONYXKEYS.POLICY_OWNERSHIP_CHANGE_CHECKS]: Record; [ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE]: OnyxTypes.QuickAction; - [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED]: boolean; - [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL]: boolean; + [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS]: OnyxTypes.RetryBillingStatus; [ONYXKEYS.REVIEW_DUPLICATES]: OnyxTypes.ReviewDuplicates; [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: string; [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: string; [ONYXKEYS.NVP_BILLING_FUND_ID]: number; - [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWNED]: number; + [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: number; [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: number; }; diff --git a/src/components/Indicator.tsx b/src/components/Indicator.tsx index 5acdc9fe42b0..0d269d1ca593 100644 --- a/src/components/Indicator.tsx +++ b/src/components/Indicator.tsx @@ -62,9 +62,9 @@ function Indicator({reimbursementAccount, policies, bankAccountList, fundList, u // Wallet term errors that are not caused by an IOU (we show the red brick indicator for those in the LHN instead) () => Object.keys(walletTerms?.errors ?? {}).length > 0 && !walletTerms?.chatReportID, ]; - const infoCheckingMethods: CheckingMethod[] = [() => !!loginList && UserUtils.hasLoginListInfo(loginList)]; + const infoCheckingMethods: CheckingMethod[] = [() => !!loginList && UserUtils.hasLoginListInfo(loginList), () => SubscriptionUtils.hasSubscriptionGreenDotInfo()]; const shouldShowErrorIndicator = errorCheckingMethods.some((errorCheckingMethod) => errorCheckingMethod()); - const shouldShowInfoIndicator = !shouldShowErrorIndicator && (infoCheckingMethods.some((infoCheckingMethod) => infoCheckingMethod()) || SubscriptionUtils.hasSubscriptionGreenDotInfo()); + const shouldShowInfoIndicator = !shouldShowErrorIndicator && infoCheckingMethods.some((infoCheckingMethod) => infoCheckingMethod()); const indicatorColor = shouldShowErrorIndicator ? theme.danger : theme.success; const indicatorStyles = [styles.alignItemsCenter, styles.justifyContentCenter, styles.statusIndicator(indicatorColor)]; diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 36f778ac9afb..29b4bfa92227 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -4,9 +4,9 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import type {OnyxValues} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {BillingGraceEndPeriod, Fund, Policy} from '@src/types/onyx'; +import type {BillingGraceEndPeriod, BillingStatus, Fund, FundList, Policy, StripeCustomerID} from '@src/types/onyx'; -const PAYMENT_STATUSES = { +const PAYMENT_STATUS = { POLICY_OWNER_WITH_AMOUNT_OWED: 'policy_owner_with_amount_owed', POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE: 'policy_owner_with_amount_owed_overdue', OWNER_OF_POLICY_UNDER_INVOICING: 'owner_of_policy_under_invoicing', @@ -19,21 +19,15 @@ const PAYMENT_STATUSES = { RETRY_BILLING_SUCCESS: 'retry_billing_success', RETRY_BILLING_ERROR: 'retry_billing_error', GENERIC_API_ERROR: 'generic_api_error', -}; +} as const; -let amountOwed: OnyxValues[typeof ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]; +let amountOwed: OnyxEntry; Onyx.connect({ key: ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED, - callback: (value) => { - if (!value) { - return; - } - - amountOwed = value; - }, + callback: (value) => (amountOwed = value), }); -let stripeCustomerId: OnyxValues[typeof ONYXKEYS.NVP_PRIVATE_STRIPE_CUSTOMER_ID]; +let stripeCustomerId: OnyxEntry; Onyx.connect({ key: ONYXKEYS.NVP_PRIVATE_STRIPE_CUSTOMER_ID, callback: (value) => { @@ -45,7 +39,7 @@ Onyx.connect({ }, }); -let billingDisputePending: OnyxValues[typeof ONYXKEYS.NVP_PRIVATE_BILLING_DISPUTE_PENDING]; +let billingDisputePending: OnyxEntry; Onyx.connect({ key: ONYXKEYS.NVP_PRIVATE_BILLING_DISPUTE_PENDING, callback: (value) => { @@ -57,7 +51,7 @@ Onyx.connect({ }, }); -let billingStatus: OnyxValues[typeof ONYXKEYS.NVP_PRIVATE_BILLING_STATUS]; +let billingStatus: OnyxEntry; Onyx.connect({ key: ONYXKEYS.NVP_PRIVATE_BILLING_STATUS, callback: (value) => { @@ -69,19 +63,13 @@ Onyx.connect({ }, }); -let billingGracePeriod: OnyxValues[typeof ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]; +let ownerBillingGraceEndPeriod: OnyxEntry; Onyx.connect({ key: ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END, - callback: (value) => { - if (value === undefined) { - return; - } - - billingGracePeriod = value; - }, + callback: (value) => (ownerBillingGraceEndPeriod = value), }); -let fundList: OnyxValues[typeof ONYXKEYS.FUND_LIST]; +let fundList: OnyxEntry; Onyx.connect({ key: ONYXKEYS.FUND_LIST, callback: (value) => { @@ -93,31 +81,18 @@ Onyx.connect({ }, }); -let billingStatusFailed: OnyxValues[typeof ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED]; +let retryBillingStatus: OnyxValues[typeof ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS]; Onyx.connect({ - key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED, + key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS, callback: (value) => { if (value === undefined) { return; } - billingStatusFailed = value; + retryBillingStatus = value; }, }); -let billingStatusSuccessful: OnyxValues[typeof ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL]; -Onyx.connect({ - key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, - callback: (value) => { - if (value === undefined) { - return; - } - - billingStatusSuccessful = value; - }, - initWithStoredValues: false, -}); - let firstDayFreeTrial: OnyxEntry; Onyx.connect({ key: ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL, @@ -150,42 +125,72 @@ Onyx.connect({ waitForCollectionCallback: true, }); -function getOverdueGracePeriodDate(): number { - return billingGracePeriod; +/** + * @returns The date when the grace period ends. + */ +function getOverdueGracePeriodDate(): OnyxEntry { + return ownerBillingGraceEndPeriod; } +/** + * @returns Whether the workspace owner has an overdue grace period. + */ function hasOverdueGracePeriod(): boolean { - return !!billingGracePeriod ?? false; + return !!ownerBillingGraceEndPeriod ?? false; } +/** + * @returns Whether the workspace owner's grace period is overdue. + */ function hasGracePeriodOverdue(): boolean { - return !!billingGracePeriod && Date.now() < new Date(billingGracePeriod).getTime(); + return !!ownerBillingGraceEndPeriod && Date.now() < new Date(ownerBillingGraceEndPeriod).getTime(); } +/** + * @returns The amount owed by the workspace owner. + */ function getAmountOwed(): number { return amountOwed ?? 0; } +/** + * @returns Whether there is a card authentication error. + */ function hasCardAuthenticatedError() { return stripeCustomerId?.status === 'authentication_required' && amountOwed === 0; } +/** + * @returns Whether there is a billing dispute pending. + */ function hasBillingDisputePending() { return !!billingDisputePending ?? false; } +/** + * @returns Whether there is a card expired error. + */ function hasCardExpiredError() { return billingStatus?.declineReason === 'expired_card' && amountOwed !== 0; } +/** + * @returns Whether there is an insufficient funds error. + */ function hasInsufficientFundsError() { return billingStatus?.declineReason === 'insufficient_funds' && amountOwed !== 0; } +/** + * @returns The card to be used for subscription billing. + */ function getCardForSubscriptionBilling(): Fund | undefined { return Object.values(fundList ?? {}).find((card) => card?.isDefault); } +/** + * @returns Whether the card is due to expire soon. + */ function hasCardExpiringSoon(): boolean { const card = getCardForSubscriptionBilling(); @@ -196,12 +201,18 @@ function hasCardExpiringSoon(): boolean { return !billingStatus && card?.accountData?.cardMonth === new Date().getMonth() + 1; } +/** + * @returns Whether there is a retry billing error. + */ function hasRetryBillingError(): boolean { - return !!billingStatusFailed; + return retryBillingStatus === CONST.SUBSCRIPTION_RETRY_BILLING_STATUS.FAILED; } +/** + * @returns Whether the retry billing was successful. + */ function isRetryBillingSuccessful(): boolean { - return !!billingStatusSuccessful; + return retryBillingStatus === CONST.SUBSCRIPTION_RETRY_BILLING_STATUS.SUCCESS; } type SubscriptionStatus = { @@ -209,13 +220,16 @@ type SubscriptionStatus = { isError?: boolean; }; +/** + * @returns The subscription status. + */ function getSubscriptionStatus(): SubscriptionStatus { if (hasOverdueGracePeriod()) { if (amountOwed) { // 1. Policy owner with amount owed, within grace period if (hasGracePeriodOverdue() === false) { return { - status: PAYMENT_STATUSES.POLICY_OWNER_WITH_AMOUNT_OWED, + status: PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED, isError: true, }; } @@ -223,21 +237,21 @@ function getSubscriptionStatus(): SubscriptionStatus { // 2. Policy owner with amount owed, overdue (past grace period) if (hasGracePeriodOverdue()) { return { - status: PAYMENT_STATUSES.POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE, + status: PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE, }; } } else { // 3. Owner of policy under invoicing, within grace period if (hasGracePeriodOverdue() && !amountOwed) { return { - status: PAYMENT_STATUSES.OWNER_OF_POLICY_UNDER_INVOICING, + status: PAYMENT_STATUS.OWNER_OF_POLICY_UNDER_INVOICING, }; } // 4. Owner of policy under invoicing, overdue (past grace period) if (hasGracePeriodOverdue() === false && amountOwed) { return { - status: PAYMENT_STATUSES.OWNER_OF_POLICY_UNDER_INVOICING_OVERDUE, + status: PAYMENT_STATUS.OWNER_OF_POLICY_UNDER_INVOICING_OVERDUE, }; } } @@ -246,42 +260,42 @@ function getSubscriptionStatus(): SubscriptionStatus { // 5. Billing disputed by cardholder if (hasBillingDisputePending()) { return { - status: PAYMENT_STATUSES.BILLING_DISPUTE_PENDING, + status: PAYMENT_STATUS.BILLING_DISPUTE_PENDING, }; } // 6. Card not authenticated if (hasCardAuthenticatedError()) { return { - status: PAYMENT_STATUSES.CARD_AUTHENTICATION_REQUIRED, + status: PAYMENT_STATUS.CARD_AUTHENTICATION_REQUIRED, }; } // 7. Insufficient funds if (hasInsufficientFundsError()) { return { - status: PAYMENT_STATUSES.INSUFFICIENT_FUNDS, + status: PAYMENT_STATUS.INSUFFICIENT_FUNDS, }; } // 8. Card expired if (hasCardExpiredError()) { return { - status: PAYMENT_STATUSES.CARD_EXPIRED, + status: PAYMENT_STATUS.CARD_EXPIRED, }; } // 9. Card due to expire soon if (hasCardExpiringSoon()) { return { - status: PAYMENT_STATUSES.CARD_EXPIRE_SOON, + status: PAYMENT_STATUS.CARD_EXPIRE_SOON, }; } // 10. Retry billing success if (isRetryBillingSuccessful()) { return { - status: PAYMENT_STATUSES.RETRY_BILLING_SUCCESS, + status: PAYMENT_STATUS.RETRY_BILLING_SUCCESS, isError: false, }; } @@ -289,7 +303,7 @@ function getSubscriptionStatus(): SubscriptionStatus { // 11. Retry billing error if (hasRetryBillingError()) { return { - status: PAYMENT_STATUSES.RETRY_BILLING_ERROR, + status: PAYMENT_STATUS.RETRY_BILLING_ERROR, isError: true, }; } @@ -297,10 +311,16 @@ function getSubscriptionStatus(): SubscriptionStatus { return {}; } +/** + * @returns Whether there is a subscription red dot error. + */ function hasSubscriptionRedDotError(): boolean { return getSubscriptionStatus()?.isError ?? false; } +/** + * @returns Whether there is a subscription green dot info. + */ function hasSubscriptionGreenDotInfo(): boolean { return !getSubscriptionStatus()?.isError ?? false; } @@ -381,7 +401,7 @@ function shouldRestrictUserBillableActions(policyID: string): boolean { // If it reached here it means that the user is actually the workspace's owner. // We should restrict the workspace's owner actions if it's past its grace period end date and it's owing some amount. - if (billingGracePeriod && amountOwed !== undefined && amountOwed > 0 && isAfter(currentDate, fromUnixTime(billingGracePeriod))) { + if (ownerBillingGraceEndPeriod && amountOwed !== undefined && amountOwed > 0 && isAfter(currentDate, fromUnixTime(ownerBillingGraceEndPeriod))) { return true; } @@ -401,5 +421,5 @@ export { getCardForSubscriptionBilling, hasSubscriptionGreenDotInfo, hasRetryBillingError, - PAYMENT_STATUSES, + PAYMENT_STATUS, }; diff --git a/src/pages/settings/Subscription/CardSection/BillingBanner/BillingBanner.tsx b/src/pages/settings/Subscription/CardSection/BillingBanner/BillingBanner.tsx index cf11dfb1d2b6..4587dfee2fe6 100644 --- a/src/pages/settings/Subscription/CardSection/BillingBanner/BillingBanner.tsx +++ b/src/pages/settings/Subscription/CardSection/BillingBanner/BillingBanner.tsx @@ -33,7 +33,7 @@ type BillingBannerProps = { /** Styles to apply to the subtitle. */ subtitleStyle?: StyleProp; - /** An icon to be rendered instead of RBR/GBR */ + /** An icon to be rendered instead of the RBR / GBR indicator. */ rightIcon?: IconAsset; }; diff --git a/src/pages/settings/Subscription/CardSection/BillingBanner/SubscriptionBillingBanner.tsx b/src/pages/settings/Subscription/CardSection/BillingBanner/SubscriptionBillingBanner.tsx index 84db69a758a3..fcad66c9b268 100644 --- a/src/pages/settings/Subscription/CardSection/BillingBanner/SubscriptionBillingBanner.tsx +++ b/src/pages/settings/Subscription/CardSection/BillingBanner/SubscriptionBillingBanner.tsx @@ -7,19 +7,17 @@ import BillingBanner from './BillingBanner'; import type {BillingBannerProps} from './BillingBanner'; type SubscriptionBillingBannerProps = Omit & { + /** Indicates whether there is an active trial */ isTrialActive?: boolean; + + /** Indicates whether there is an error */ isError?: boolean; + + /** An optional icon prop */ icon?: IconAsset; }; -function SubscriptionBillingBanner({ - title, - subtitle, - rightIcon, - icon, - isTrialActive, - isError, -}: SubscriptionBillingBannerProps) { +function SubscriptionBillingBanner({title, subtitle, rightIcon, icon, isTrialActive, isError}: SubscriptionBillingBannerProps) { const styles = useThemeStyles(); const backgroundStyle = isTrialActive ? styles.trialBannerBackgroundColor : styles.hoveredComponentBG; diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 0426969d0159..4d79f165169c 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -1,5 +1,6 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import Section from '@components/Section'; @@ -8,20 +9,18 @@ import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import { useOnyx } from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import PreTrialBillingBanner from './BillingBanner/PreTrialBillingBanner'; import SubscriptionBillingBanner from './BillingBanner/SubscriptionBillingBanner'; import CardSectionActions from './CardSectionActions'; import CardSectionDataEmpty from './CardSectionDataEmpty'; import CardSectionUtils from './utils'; -import PreTrialBillingBanner from './BillingBanner/PreTrialBillingBanner'; function CardSection() { const {translate, preferredLocale} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); - const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); const [privateSubscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION); const defaultCard = CardSectionUtils.getCardForSubscriptionBilling(); @@ -30,11 +29,25 @@ function CardSection() { const {title, subtitle, isError, icon, rightIcon} = CardSectionUtils.getBillingStatus(translate, defaultCard?.accountData?.cardNumber ?? ''); - const shouldShowBanner = !!title || !!subtitle; const nextPaymentDate = !isEmptyObject(privateSubscription) ? CardSectionUtils.getNextBillingDate() : undefined; const sectionSubtitle = defaultCard && !!nextPaymentDate ? translate('subscription.cardSection.cardNextPayment', {nextPaymentDate}) : translate('subscription.cardSection.subtitle'); - const BillingBanner = ; + + let BillingBanner: React.ReactNode | undefined; + if (!CardSectionUtils.shouldShowPreTrialBillingBanner()) { + BillingBanner = ; + } else if (title && subtitle) { + BillingBanner = ( + + ); + } return (
- ) - } + banner={BillingBanner} > {!isEmptyObject(defaultCard?.accountData) && ( diff --git a/src/pages/settings/Subscription/CardSection/utils.ts b/src/pages/settings/Subscription/CardSection/utils.ts index 3771e6e769c7..d7cf2972d18c 100644 --- a/src/pages/settings/Subscription/CardSection/utils.ts +++ b/src/pages/settings/Subscription/CardSection/utils.ts @@ -1,4 +1,4 @@ -import {fromUnixTime, addMonths, format, startOfMonth} from 'date-fns'; +import {addMonths, format, fromUnixTime, startOfMonth} from 'date-fns'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import DateUtils from '@libs/DateUtils'; @@ -31,10 +31,10 @@ function getBillingStatus( const endDate = SubscriptionUtils.getOverdueGracePeriodDate(); - const endDateFormatted = DateUtils.formatWithUTCTimeZone(fromUnixTime(endDate).toUTCString(), CONST.DATE.MONTH_DAY_YEAR_FORMAT); + const endDateFormatted = endDate ? DateUtils.formatWithUTCTimeZone(fromUnixTime(endDate).toUTCString(), CONST.DATE.MONTH_DAY_YEAR_FORMAT) : null; switch (status.status) { - case SubscriptionUtils.PAYMENT_STATUSES.POLICY_OWNER_WITH_AMOUNT_OWED: + case SubscriptionUtils.PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED: return { title: translate('subscription.billingBanner.outdatedInfo'), subtitle: translate('subscription.billingBanner.updateCardDataByDate', {date: endDateFormatted}), @@ -42,14 +42,14 @@ function getBillingStatus( isRetryAvailable: true, }; - case SubscriptionUtils.PAYMENT_STATUSES.POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE: + case SubscriptionUtils.PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE: return { title: translate('subscription.billingBanner.outdatedInfo'), subtitle: translate('subscription.billingBanner.updatePaymentInformation'), isError: true, }; - case SubscriptionUtils.PAYMENT_STATUSES.OWNER_OF_POLICY_UNDER_INVOICING: + case SubscriptionUtils.PAYMENT_STATUS.OWNER_OF_POLICY_UNDER_INVOICING: return { title: translate('subscription.billingBanner.outdatedInfo'), subtitle: translate('subscription.billingBanner.paymentPastDuePayByDate', {date: endDateFormatted}), @@ -57,7 +57,7 @@ function getBillingStatus( isAddButtonDark: true, }; - case SubscriptionUtils.PAYMENT_STATUSES.OWNER_OF_POLICY_UNDER_INVOICING_OVERDUE: + case SubscriptionUtils.PAYMENT_STATUS.OWNER_OF_POLICY_UNDER_INVOICING_OVERDUE: return { title: translate('subscription.billingBanner.outdatedInfo'), subtitle: translate('subscription.billingBanner.paymentPastDue'), @@ -65,7 +65,7 @@ function getBillingStatus( isAddButtonDark: true, }; - case SubscriptionUtils.PAYMENT_STATUSES.BILLING_DISPUTE_PENDING: + case SubscriptionUtils.PAYMENT_STATUS.BILLING_DISPUTE_PENDING: return { title: translate('subscription.billingBanner.cardCouldNotBeCharged'), subtitle: translate('subscription.billingBanner.cardOnDispute', {amountOwed, cardEnding}), @@ -73,7 +73,7 @@ function getBillingStatus( isRetryAvailable: false, }; - case SubscriptionUtils.PAYMENT_STATUSES.CARD_AUTHENTICATION_REQUIRED: + case SubscriptionUtils.PAYMENT_STATUS.CARD_AUTHENTICATION_REQUIRED: return { title: translate('subscription.billingBanner.cardCouldNotBeCharged'), subtitle: translate('subscription.billingBanner.cardNotFullyAuthenticated', {cardEnding}), @@ -81,7 +81,7 @@ function getBillingStatus( isAuthenticatingRequired: true, }; - case SubscriptionUtils.PAYMENT_STATUSES.INSUFFICIENT_FUNDS: + case SubscriptionUtils.PAYMENT_STATUS.INSUFFICIENT_FUNDS: return { title: translate('subscription.billingBanner.cardCouldNotBeCharged'), subtitle: translate('subscription.billingBanner.cardDeclinedDueToInsufficientFunds', {amountOwed}), @@ -89,7 +89,7 @@ function getBillingStatus( isRetryAvailable: true, }; - case SubscriptionUtils.PAYMENT_STATUSES.CARD_EXPIRED: + case SubscriptionUtils.PAYMENT_STATUS.CARD_EXPIRED: return { title: translate('subscription.billingBanner.cardCouldNotBeCharged'), subtitle: translate('subscription.billingBanner.cardExpired', {amountOwed}), @@ -97,7 +97,7 @@ function getBillingStatus( isRetryAvailable: true, }; - case SubscriptionUtils.PAYMENT_STATUSES.CARD_EXPIRE_SOON: + case SubscriptionUtils.PAYMENT_STATUS.CARD_EXPIRE_SOON: return { title: translate('subscription.billingBanner.cardExpiringSoon'), subtitle: translate('subscription.billingBanner.cardWillExpireAtTheEndOfMonth'), @@ -105,7 +105,7 @@ function getBillingStatus( icon: Illustrations.CreditCardEyes, }; - case SubscriptionUtils.PAYMENT_STATUSES.RETRY_BILLING_SUCCESS: + case SubscriptionUtils.PAYMENT_STATUS.RETRY_BILLING_SUCCESS: return { title: translate('subscription.billingBanner.succeeded'), subtitle: translate('subscription.billingBanner.billedSuccessfully'), @@ -113,7 +113,7 @@ function getBillingStatus( rightIcon: Expensicons.Close, }; - case SubscriptionUtils.PAYMENT_STATUSES.RETRY_BILLING_ERROR: + case SubscriptionUtils.PAYMENT_STATUS.RETRY_BILLING_ERROR: return { title: translate('subscription.billingBanner.cardCouldNotBeCharged'), subtitle: translate('subscription.billingBanner.retryMessage'), diff --git a/src/types/onyx/BillingStatus.ts b/src/types/onyx/BillingStatus.ts new file mode 100644 index 000000000000..620a5eb3e2df --- /dev/null +++ b/src/types/onyx/BillingStatus.ts @@ -0,0 +1,16 @@ +/** Billing status model */ +type BillingStatus = { + /** Status action */ + action: string; + + /** Billing's period month */ + periodMonth: string; + + /** Billing's period year */ + periodYear: string; + + /** Decline reason */ + declineReason: 'insufficient_funds' | 'expired_card'; +}; + +export default BillingStatus; diff --git a/src/types/onyx/RetryBillingStatus.ts b/src/types/onyx/RetryBillingStatus.ts new file mode 100644 index 000000000000..59a88a258516 --- /dev/null +++ b/src/types/onyx/RetryBillingStatus.ts @@ -0,0 +1,7 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +/** Indicates the status of ClearOutstandingBalance's response */ +type RetryBillingStatus = ValueOf; + +export default RetryBillingStatus; diff --git a/src/types/onyx/StripeCustomerID.ts b/src/types/onyx/StripeCustomerID.ts new file mode 100644 index 000000000000..0790b12c5c69 --- /dev/null +++ b/src/types/onyx/StripeCustomerID.ts @@ -0,0 +1,16 @@ +/** Model of stripe customer */ +type StripeCustomerID = { + /** Payment method's ID */ + paymentMethodID: string; + + /** Intent's ID */ + intentsID: string; + + /** Payment currency */ + currency: string; + + /** Payment status */ + status: 'authentication_required' | 'intent_required' | 'succeeded'; +}; + +export default StripeCustomerID; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index becb56cb09f0..0b845267d607 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -4,6 +4,7 @@ import type {BankAccountList} from './BankAccount'; import type BankAccount from './BankAccount'; import type Beta from './Beta'; import type BillingGraceEndPeriod from './BillingGraceEndPeriod'; +import type BillingStatus from './BillingStatus'; import type BlockedFromConcierge from './BlockedFromConcierge'; import type Card from './Card'; import type {CardList} from './Card'; @@ -66,12 +67,14 @@ import type ReportNextStep from './ReportNextStep'; import type ReportUserIsTyping from './ReportUserIsTyping'; import type Request from './Request'; import type Response from './Response'; +import type RetryBillingStatus from './RetryBillingStatus'; import type ReviewDuplicates from './ReviewDuplicates'; import type ScreenShareRequest from './ScreenShareRequest'; import type SearchResults from './SearchResults'; import type SecurityGroup from './SecurityGroup'; import type SelectedTabRequest from './SelectedTabRequest'; import type Session from './Session'; +import type StripeCustomerID from './StripeCustomerID'; import type Task from './Task'; import type Transaction from './Transaction'; import type {TransactionViolation, ViolationName} from './TransactionViolation'; @@ -190,4 +193,7 @@ export type { ReviewDuplicates, PrivateSubscription, BillingGraceEndPeriod, + RetryBillingStatus, + StripeCustomerID, + BillingStatus, }; diff --git a/tests/unit/SubscriptionUtilsTest.ts b/tests/unit/SubscriptionUtilsTest.ts index 7767ae9f387b..498cbef2dc57 100644 --- a/tests/unit/SubscriptionUtilsTest.ts +++ b/tests/unit/SubscriptionUtilsTest.ts @@ -150,7 +150,7 @@ describe('SubscriptionUtils', () => { await Onyx.multiSet({ [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END]: null, [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: null, - [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWNED]: null, + [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: null, [ONYXKEYS.COLLECTION.POLICY]: null, }); }); @@ -228,7 +228,7 @@ describe('SubscriptionUtils', () => { await Onyx.multiSet({ [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: getUnixTime(subDays(new Date(), 3)), // past due - [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWNED]: 0, + [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: 0, }); expect(SubscriptionUtils.shouldRestrictUserBillableActions(policyID)).toBeFalsy(); @@ -239,7 +239,7 @@ describe('SubscriptionUtils', () => { await Onyx.multiSet({ [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: getUnixTime(subDays(new Date(), 3)), // past due - [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWNED]: 8010, + [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: 8010, }); expect(SubscriptionUtils.shouldRestrictUserBillableActions(policyID)).toBeTruthy(); From fe00f32fd3000e3a52ec16452899db93cd306325 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 24 Jun 2024 21:17:50 +0530 Subject: [PATCH 041/183] Fixed API commands --- .../UpdateNetSuiteGenericTypeParams.ts | 1 - .../actions/connections/NetSuiteCommands.ts | 39 ++++++------------- .../export/NetSuiteDateSelectPage.tsx | 4 +- .../NetSuiteExportConfigurationPage.tsx | 11 +----- ...iteExportExpensesDestinationSelectPage.tsx | 6 +-- ...nsesJournalPostingPreferenceSelectPage.tsx | 4 +- ...ExportExpensesPayableAccountSelectPage.tsx | 6 +-- ...NetSuiteExportExpensesVendorSelectPage.tsx | 4 +- .../export/NetSuiteInvoiceItemSelectPage.tsx | 4 +- .../NetSuitePreferredExporterSelectPage.tsx | 4 +- ...eProvincialTaxPostingAccountSelectPage.tsx | 4 +- .../NetSuiteReceivableAccountSelectPage.tsx | 4 +- .../NetSuiteTaxPostingAccountSelectPage.tsx | 4 +- 13 files changed, 35 insertions(+), 60 deletions(-) diff --git a/src/libs/API/parameters/UpdateNetSuiteGenericTypeParams.ts b/src/libs/API/parameters/UpdateNetSuiteGenericTypeParams.ts index 57979b3fb0bb..3834dfbf7e66 100644 --- a/src/libs/API/parameters/UpdateNetSuiteGenericTypeParams.ts +++ b/src/libs/API/parameters/UpdateNetSuiteGenericTypeParams.ts @@ -2,7 +2,6 @@ type UpdateNetSuiteGenericTypeParams = [K2 in K]: Type; } & { policyID: string; - authToken: string; }; export default UpdateNetSuiteGenericTypeParams; diff --git a/src/libs/actions/connections/NetSuiteCommands.ts b/src/libs/actions/connections/NetSuiteCommands.ts index 821afc82f4bc..a7d1e8de60d3 100644 --- a/src/libs/actions/connections/NetSuiteCommands.ts +++ b/src/libs/actions/connections/NetSuiteCommands.ts @@ -87,31 +87,28 @@ function updateNetSuiteOnyxData, oldDate?: ValueOf) { +function updateNetSuiteExportDate(policyID: string, date: ValueOf, oldDate?: ValueOf) { const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.EXPORT_DATE, date, oldDate); const parameters = { policyID, value: date, - authToken: tokenSecret, }; API.write(WRITE_COMMANDS.UPDATE_NETSUITE_EXPORT_DATE, parameters, onyxData); } function updateNetSuiteReimbursableExpensesExportDestination( policyID: string, - tokenSecret: string, destination: ValueOf, oldDestination: ValueOf, ) { @@ -120,14 +117,12 @@ function updateNetSuiteReimbursableExpensesExportDestination( const parameters = { policyID, value: destination, - authToken: tokenSecret, }; API.write(WRITE_COMMANDS.UPDATE_NETSUITE_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION, parameters, onyxData); } function updateNetSuiteNonReimbursableExpensesExportDestination( policyID: string, - tokenSecret: string, destination: ValueOf, oldDestination: ValueOf, ) { @@ -136,47 +131,42 @@ function updateNetSuiteNonReimbursableExpensesExportDestination( const parameters = { policyID, value: destination, - authToken: tokenSecret, }; API.write(WRITE_COMMANDS.UPDATE_NETSUITE_NONREIMBURSABLE_EXPENSES_EXPORT_DESTINATION, parameters, onyxData); } -function updateNetSuiteDefaultVendor(policyID: string, tokenSecret: string, vendorID: string, oldVendorID?: string) { +function updateNetSuiteDefaultVendor(policyID: string, vendorID: string, oldVendorID?: string) { const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.DEFAULT_VENDOR, vendorID, oldVendorID); const parameters = { policyID, vendorID, - authToken: tokenSecret, }; API.write(WRITE_COMMANDS.UPDATE_NETSUITE_DEFAULT_VENDOR, parameters, onyxData); } -function updateNetSuiteReimbursablePayableAccount(policyID: string, tokenSecret: string, bankAccountID: string, oldBankAccountID: string) { +function updateNetSuiteReimbursablePayableAccount(policyID: string, bankAccountID: string, oldBankAccountID: string) { const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.REIMBURSABLE_PAYABLE_ACCOUNT, bankAccountID, oldBankAccountID); const parameters = { policyID, bankAccountID, - authToken: tokenSecret, }; API.write(WRITE_COMMANDS.UPDATE_NETSUITE_REIMBURSABLE_PAYABLE_ACCOUNT, parameters, onyxData); } -function updateNetSuitePayableAcct(policyID: string, tokenSecret: string, bankAccountID: string, oldBankAccountID: string) { +function updateNetSuitePayableAcct(policyID: string, bankAccountID: string, oldBankAccountID: string) { const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.PAYABLE_ACCT, bankAccountID, oldBankAccountID); const parameters = { policyID, bankAccountID, - authToken: tokenSecret, }; API.write(WRITE_COMMANDS.UPDATE_NETSUITE_PAYABLE_ACCT, parameters, onyxData); } function updateNetSuiteJournalPostingPreference( policyID: string, - tokenSecret: string, postingPreference: ValueOf, oldPostingPreference?: ValueOf, ) { @@ -185,73 +175,66 @@ function updateNetSuiteJournalPostingPreference( const parameters = { policyID, value: postingPreference, - authToken: tokenSecret, }; API.write(WRITE_COMMANDS.UPDATE_NETSUITE_JOURNAL_POSTING_PREFERENCE, parameters, onyxData); } -function updateNetSuiteReceivableAccount(policyID: string, tokenSecret: string, bankAccountID: string, oldBankAccountID?: string) { +function updateNetSuiteReceivableAccount(policyID: string, bankAccountID: string, oldBankAccountID?: string) { const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.RECEIVABLE_ACCOUNT, bankAccountID, oldBankAccountID); const parameters = { policyID, bankAccountID, - authToken: tokenSecret, }; API.write(WRITE_COMMANDS.UPDATE_NETSUITE_RECEIVABLE_ACCOUNT, parameters, onyxData); } -function updateNetSuiteInvoiceItem(policyID: string, tokenSecret: string, itemID: string, oldItemID?: string) { +function updateNetSuiteInvoiceItem(policyID: string, itemID: string, oldItemID?: string) { const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.INVOICE_ITEM, itemID, oldItemID); const parameters = { policyID, itemID, - authToken: tokenSecret, }; API.write(WRITE_COMMANDS.UPDATE_NETSUITE_INVOICE_ITEM, parameters, onyxData); } -function updateNetSuiteTaxPostingAccount(policyID: string, tokenSecret: string, bankAccountID: string, oldBankAccountID?: string) { +function updateNetSuiteTaxPostingAccount(policyID: string, bankAccountID: string, oldBankAccountID?: string) { const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.TAX_POSTING_ACCOUNT, bankAccountID, oldBankAccountID); const parameters = { policyID, bankAccountID, - authToken: tokenSecret, }; API.write(WRITE_COMMANDS.UPDATE_NETSUITE_TAX_POSTING_ACCOUNT, parameters, onyxData); } -function updateNetSuiteProvincialTaxPostingAccount(policyID: string, tokenSecret: string, bankAccountID: string, oldBankAccountID?: string) { +function updateNetSuiteProvincialTaxPostingAccount(policyID: string, bankAccountID: string, oldBankAccountID?: string) { const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.PROVINCIAL_TAX_POSTING_ACCOUNT, bankAccountID, oldBankAccountID); const parameters = { policyID, bankAccountID, - authToken: tokenSecret, }; API.write(WRITE_COMMANDS.UPDATE_NETSUITE_PROVINCIAL_TAX_POSTING_ACCOUNT, parameters, onyxData); } -function updateNetSuiteAllowForeignCurrency(policyID: string, tokenSecret: string, value: boolean, oldValue?: boolean) { +function updateNetSuiteAllowForeignCurrency(policyID: string, value: boolean, oldValue?: boolean) { const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.ALLOW_FOREIGN_CURRENCY, value, oldValue); const parameters = { policyID, enabled: value, - authToken: tokenSecret, }; API.write(WRITE_COMMANDS.UPDATE_NETSUITE_ALLOW_FOREIGN_CURRENCY, parameters, onyxData); } -function updateNetSuiteExportToNextOpenPeriod(policyID: string, tokenSecret: string, value: boolean, oldValue: boolean) { +function updateNetSuiteExportToNextOpenPeriod(policyID: string, value: boolean, oldValue: boolean) { const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.EXPORT_TO_NEXT_OPEN_PERIOD, value, oldValue); const parameters = { policyID, enabled: value, - authToken: tokenSecret, }; API.write(WRITE_COMMANDS.UPDATE_NETSUITE_EXPORT_TO_NEXT_OPEN_PERIOD, parameters, onyxData); } diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteDateSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteDateSelectPage.tsx index 0d681677385c..921d584eaed8 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteDateSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteDateSelectPage.tsx @@ -44,11 +44,11 @@ function NetSuiteDateSelectPage({policy}: WithPolicyConnectionsProps) { const selectExportDate = useCallback( (row: MenuListItem) => { if (row.value !== config?.exportDate) { - Connections.updateNetSuiteExportDate(policyID, policy?.connections?.netsuite.tokenSecret ?? '', row.value, config?.exportDate); + Connections.updateNetSuiteExportDate(policyID, row.value, config?.exportDate); } Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID)); }, - [config?.exportDate, policy?.connections?.netsuite.tokenSecret, policyID], + [config?.exportDate, policyID], ); return ( diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx index b5336d5b2204..b623e59e3553 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx @@ -178,8 +178,7 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) { title: translate('workspace.netsuite.foreignCurrencyAmount'), isActive: !!config?.allowForeignCurrency, switchAccessibilityLabel: translate('workspace.netsuite.foreignCurrencyAmount'), - onToggle: () => - Connections.updateNetSuiteAllowForeignCurrency(policyID, policy?.connections?.netsuite.tokenSecret ?? '', !config?.allowForeignCurrency, config?.allowForeignCurrency), + onToggle: () => Connections.updateNetSuiteAllowForeignCurrency(policyID, !config?.allowForeignCurrency, config?.allowForeignCurrency), onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.ALLOW_FOREIGN_CURRENCY), pendingAction: config?.pendingFields?.allowForeignCurrency, errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.ALLOW_FOREIGN_CURRENCY), @@ -193,13 +192,7 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) { isActive: !!config?.exportToNextOpenPeriod, switchAccessibilityLabel: translate('workspace.netsuite.exportToNextOpenPeriod'), onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.EXPORT_TO_NEXT_OPEN_PERIOD), - onToggle: () => - Connections.updateNetSuiteExportToNextOpenPeriod( - policyID, - policy?.connections?.netsuite.tokenSecret ?? '', - !config?.exportToNextOpenPeriod, - config?.exportToNextOpenPeriod ?? false, - ), + onToggle: () => Connections.updateNetSuiteExportToNextOpenPeriod(policyID, !config?.exportToNextOpenPeriod, config?.exportToNextOpenPeriod ?? false), pendingAction: config?.pendingFields?.exportToNextOpenPeriod, errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.EXPORT_TO_NEXT_OPEN_PERIOD), }, diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesDestinationSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesDestinationSelectPage.tsx index 2075048ad884..eed5e7ad70ec 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesDestinationSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesDestinationSelectPage.tsx @@ -43,14 +43,14 @@ function NetSuiteExportExpensesDestinationSelectPage({policy}: WithPolicyConnect (row: MenuListItem) => { if (row.value !== currentValue) { if (isReimbursable) { - Connections.updateNetSuiteReimbursableExpensesExportDestination(policyID, policy?.connections?.netsuite.tokenSecret ?? '', row.value, currentValue ?? 'EXPENSE_REPORT'); + Connections.updateNetSuiteReimbursableExpensesExportDestination(policyID, row.value, currentValue ?? 'EXPENSE_REPORT'); } else { - Connections.updateNetSuiteNonReimbursableExpensesExportDestination(policyID, policy?.connections?.netsuite.tokenSecret ?? '', row.value, currentValue ?? 'VENDOR_BILL'); + Connections.updateNetSuiteNonReimbursableExpensesExportDestination(policyID, row.value, currentValue ?? 'VENDOR_BILL'); } } Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES.getRoute(policyID, params.expenseType)); }, - [currentValue, isReimbursable, params.expenseType, policy?.connections?.netsuite.tokenSecret, policyID], + [currentValue, isReimbursable, params.expenseType, policyID], ); return ( diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx index a5dc136f4b55..b58da40b9547 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx @@ -40,11 +40,11 @@ function NetSuiteExportExpensesJournalPostingPreferenceSelectPage({policy}: With const selectPostingPreference = useCallback( (row: MenuListItem) => { if (row.value !== config?.journalPostingPreference) { - Connections.updateNetSuiteJournalPostingPreference(policyID, policy?.connections?.netsuite.tokenSecret ?? '', row.value, config?.journalPostingPreference); + Connections.updateNetSuiteJournalPostingPreference(policyID, row.value, config?.journalPostingPreference); } Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES.getRoute(policyID, params.expenseType)); }, - [config?.journalPostingPreference, params.expenseType, policy?.connections?.netsuite.tokenSecret, policyID], + [config?.journalPostingPreference, params.expenseType, policyID], ); return ( diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPayableAccountSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPayableAccountSelectPage.tsx index 5f82523cd257..472939f28a07 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPayableAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPayableAccountSelectPage.tsx @@ -41,14 +41,14 @@ function NetSuiteExportExpensesPayableAccountSelectPage({policy}: WithPolicyConn ({value}: SelectorType) => { if (currentValue !== value) { if (isReimbursable) { - Connections.updateNetSuiteReimbursablePayableAccount(policyID, policy?.connections?.netsuite.tokenSecret ?? '', value, currentValue ?? ''); + Connections.updateNetSuiteReimbursablePayableAccount(policyID, value, currentValue ?? ''); } else { - Connections.updateNetSuitePayableAcct(policyID, policy?.connections?.netsuite.tokenSecret ?? '', value, currentValue ?? ''); + Connections.updateNetSuitePayableAcct(policyID, value, currentValue ?? ''); } } Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES.getRoute(policyID, params.expenseType)); }, - [currentValue, policyID, params.expenseType, isReimbursable, policy?.connections?.netsuite.tokenSecret], + [currentValue, policyID, params.expenseType, isReimbursable], ); const listEmptyContent = useMemo( diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx index b36136bd3528..a356313fd7d2 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx @@ -39,11 +39,11 @@ function NetSuiteExportExpensesVendorSelectPage({policy}: WithPolicyConnectionsP const updateDefaultVendor = useCallback( ({value}: SelectorType) => { if (config?.defaultVendor !== value) { - Connections.updateNetSuiteDefaultVendor(policyID, policy?.connections?.netsuite.tokenSecret ?? '', value, config?.defaultVendor); + Connections.updateNetSuiteDefaultVendor(policyID, value, config?.defaultVendor); } Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES.getRoute(policyID, params.expenseType)); }, - [config?.defaultVendor, policyID, params.expenseType, policy?.connections?.netsuite.tokenSecret], + [config?.defaultVendor, policyID, params.expenseType], ); const listEmptyContent = useMemo( diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemSelectPage.tsx index 06ff7b468a24..f717a7e25633 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemSelectPage.tsx @@ -29,11 +29,11 @@ function NetSuiteInvoiceItemSelectPage({policy}: WithPolicyConnectionsProps) { const updateInvoiceItem = useCallback( ({value}: SelectorType) => { if (config?.invoiceItem !== value) { - Connections.updateNetSuiteInvoiceItem(policyID, policy?.connections?.netsuite.tokenSecret ?? '', value, config?.invoiceItem); + Connections.updateNetSuiteInvoiceItem(policyID, value, config?.invoiceItem); } Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID)); }, - [policyID, policy?.connections?.netsuite.tokenSecret, config?.invoiceItem], + [policyID, config?.invoiceItem], ); const listEmptyContent = useMemo( diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuitePreferredExporterSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuitePreferredExporterSelectPage.tsx index bedf6dad2d5e..d22635e71940 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuitePreferredExporterSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuitePreferredExporterSelectPage.tsx @@ -64,11 +64,11 @@ function NetSuitePreferredExporterSelectPage({policy}: WithPolicyConnectionsProp const selectExporter = useCallback( (row: CardListItem) => { if (row.value !== config?.exporter) { - Connections.updateNetSuiteExporter(policyID, policy?.connections?.netsuite.tokenSecret ?? '', row.value, config?.exporter ?? ''); + Connections.updateNetSuiteExporter(policyID, row.value, config?.exporter ?? ''); } Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID)); }, - [config?.exporter, policyID, policy?.connections?.netsuite.tokenSecret], + [config?.exporter, policyID], ); const headerContent = useMemo( diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx index 9e5c40630ab4..d2d1134babad 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx @@ -38,11 +38,11 @@ function NetSuiteProvincialTaxPostingAccountSelectPage({policy}: WithPolicyConne const updateTaxAccount = useCallback( ({value}: SelectorType) => { if (config?.provincialTaxPostingAccount !== value) { - Connections.updateNetSuiteProvincialTaxPostingAccount(policyID, policy?.connections?.netsuite.tokenSecret ?? '', value, config?.provincialTaxPostingAccount); + Connections.updateNetSuiteProvincialTaxPostingAccount(policyID, value, config?.provincialTaxPostingAccount); } Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID)); }, - [policyID, policy?.connections?.netsuite.tokenSecret, config?.provincialTaxPostingAccount], + [policyID, config?.provincialTaxPostingAccount], ); const listEmptyContent = useMemo( diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteReceivableAccountSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteReceivableAccountSelectPage.tsx index 36616b6949c6..3f5066d6cc71 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteReceivableAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteReceivableAccountSelectPage.tsx @@ -32,11 +32,11 @@ function NetSuiteReceivableAccountSelectPage({policy}: WithPolicyConnectionsProp const updateReceivableAccount = useCallback( ({value}: SelectorType) => { if (config?.receivableAccount !== value) { - Connections.updateNetSuiteReceivableAccount(policyID, policy?.connections?.netsuite.tokenSecret ?? '', value, config?.receivableAccount); + Connections.updateNetSuiteReceivableAccount(policyID, value, config?.receivableAccount); } Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID)); }, - [policyID, policy?.connections?.netsuite.tokenSecret, config?.receivableAccount], + [policyID, config?.receivableAccount], ); const listEmptyContent = useMemo( diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteTaxPostingAccountSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteTaxPostingAccountSelectPage.tsx index 61e3315c2e67..9e0ed8120772 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteTaxPostingAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteTaxPostingAccountSelectPage.tsx @@ -37,11 +37,11 @@ function NetSuiteTaxPostingAccountSelectPage({policy}: WithPolicyConnectionsProp const updateTaxAccount = useCallback( ({value}: SelectorType) => { if (config?.taxPostingAccount !== value) { - Connections.updateNetSuiteTaxPostingAccount(policyID, policy?.connections?.netsuite.tokenSecret ?? '', value, config?.taxPostingAccount); + Connections.updateNetSuiteTaxPostingAccount(policyID, value, config?.taxPostingAccount); } Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID)); }, - [policyID, policy?.connections?.netsuite.tokenSecret, config?.taxPostingAccount], + [policyID, config?.taxPostingAccount], ); const listEmptyContent = useMemo( From 04fd24cc182dc2e5bc4390de2757704faf28d557 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 24 Jun 2024 22:19:13 +0530 Subject: [PATCH 042/183] Added the invoice item preference page --- src/components/SelectionScreen.tsx | 5 ++ .../ModalStackNavigators/index.tsx | 4 +- .../actions/connections/NetSuiteCommands.ts | 15 ++++ ...etSuiteInvoiceItemPreferenceSelectPage.tsx | 86 +++++++++++++++++++ .../export/NetSuiteInvoiceItemSelectPage.tsx | 4 +- 5 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx diff --git a/src/components/SelectionScreen.tsx b/src/components/SelectionScreen.tsx index 20bc30f1d937..3dcc9887c275 100644 --- a/src/components/SelectionScreen.tsx +++ b/src/components/SelectionScreen.tsx @@ -19,6 +19,9 @@ type SelectorType = ListItem & { }; type SelectionScreenProps = { + /** React nodes that will be shown */ + children?: React.ReactNode; + /** Used to set the testID for tests */ displayName: string; @@ -63,6 +66,7 @@ type SelectionScreenProps = { }; function SelectionScreen({ + children, displayName, title, headerContent, @@ -108,6 +112,7 @@ function SelectionScreen({ initiallyFocusedOptionKey={initiallyFocusedOptionKey} listEmptyContent={listEmptyContent} /> + {children} ); diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index fa2e9567adbe..2bec431c2a81 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -335,8 +335,8 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteReceivableAccountSelectPage').default as React.ComponentType, - // [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_INVOICE_ITEM_PREFERENCE_SELECT]: () => - // require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_INVOICE_ITEM_PREFERENCE_SELECT]: () => + require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_INVOICE_ITEM_SELECT]: () => require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemSelectPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_TAX_POSTING_ACCOUNT_SELECT]: () => diff --git a/src/libs/actions/connections/NetSuiteCommands.ts b/src/libs/actions/connections/NetSuiteCommands.ts index a7d1e8de60d3..7d1ab3c326b0 100644 --- a/src/libs/actions/connections/NetSuiteCommands.ts +++ b/src/libs/actions/connections/NetSuiteCommands.ts @@ -189,6 +189,20 @@ function updateNetSuiteReceivableAccount(policyID: string, bankAccountID: string API.write(WRITE_COMMANDS.UPDATE_NETSUITE_RECEIVABLE_ACCOUNT, parameters, onyxData); } +function updateNetSuiteInvoiceItemPreference( + policyID: string, + value: ValueOf, + oldValue?: ValueOf, +) { + const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.INVOICE_ITEM_PREFERENCE, value, oldValue); + + const parameters = { + policyID, + value, + }; + API.write(WRITE_COMMANDS.UPDATE_NETSUITE_INVOICE_ITEM_PREFERENCE, parameters, onyxData); +} + function updateNetSuiteInvoiceItem(policyID: string, itemID: string, oldItemID?: string) { const onyxData = updateNetSuiteOnyxData(policyID, CONST.NETSUITE_CONFIG.INVOICE_ITEM, itemID, oldItemID); @@ -249,6 +263,7 @@ export { updateNetSuitePayableAcct, updateNetSuiteJournalPostingPreference, updateNetSuiteReceivableAccount, + updateNetSuiteInvoiceItemPreference, updateNetSuiteInvoiceItem, updateNetSuiteTaxPostingAccount, updateNetSuiteProvincialTaxPostingAccount, diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx new file mode 100644 index 000000000000..1b424d89a9cb --- /dev/null +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx @@ -0,0 +1,86 @@ +import React, {useCallback} from 'react'; +import type {ValueOf} from 'type-fest'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import SelectionScreen from '@components/SelectionScreen'; +import type {SelectorType} from '@components/SelectionScreen'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Connections from '@libs/actions/connections/NetSuiteCommands'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@navigation/Navigation'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import * as Policy from '@userActions/Policy/Policy'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +type MenuListItem = ListItem & { + value: ValueOf; +}; + +function NetSuiteInvoiceItemPreferenceSelectPage({policy}: WithPolicyConnectionsProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const policyID = policy?.id ?? '-1'; + const config = policy?.connections?.netsuite.options.config; + + const data: MenuListItem[] = Object.values(CONST.NETSUITE_INVOICE_ITEM_PREFERENCE).map((postingPreference) => ({ + value: postingPreference, + text: translate(`workspace.netsuite.invoiceItem.values.${postingPreference}`), + keyForList: postingPreference, + isSelected: config?.invoiceItemPreference === postingPreference, + })); + + const selectInvoicePreference = useCallback( + (row: MenuListItem) => { + if (row.value !== config?.invoiceItemPreference) { + Connections.updateNetSuiteInvoiceItemPreference(policyID, row.value, config?.invoiceItemPreference); + } + if (row.value === CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.CREATE) { + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID)); + } + }, + [config?.invoiceItemPreference, policyID], + ); + + return ( + selectInvoicePreference(selection as MenuListItem)} + initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} + policyID={policyID} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID))} + connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} + > + {config?.invoiceItemPreference === CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.SELECT && ( + Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.INVOICE_ITEM)} + > + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_INVOICE_ITEM_SELECT.getRoute(policyID))} + brickRoadIndicator={config?.errorFields?.invoiceItem ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + /> + + )} + + ); +} + +NetSuiteInvoiceItemPreferenceSelectPage.displayName = 'NetSuiteInvoiceItemPreferenceSelectPage'; + +export default withPolicyConnections(NetSuiteInvoiceItemPreferenceSelectPage); diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemSelectPage.tsx index f717a7e25633..e63347d9ab19 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemSelectPage.tsx @@ -31,7 +31,7 @@ function NetSuiteInvoiceItemSelectPage({policy}: WithPolicyConnectionsProps) { if (config?.invoiceItem !== value) { Connections.updateNetSuiteInvoiceItem(policyID, value, config?.invoiceItem); } - Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID)); + Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_INVOICE_ITEM_PREFERENCE_SELECT.getRoute(policyID)); }, [policyID, config?.invoiceItem], ); @@ -60,7 +60,7 @@ function NetSuiteInvoiceItemSelectPage({policy}: WithPolicyConnectionsProps) { listItem={RadioListItem} onSelectRow={updateInvoiceItem} initiallyFocusedOptionKey={initiallyFocusedOptionKey} - onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_INVOICE_ITEM_PREFERENCE_SELECT.getRoute(policyID))} title="workspace.netsuite.invoiceItem.label" listEmptyContent={listEmptyContent} connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} From b4a39e3b0d1814bebae479a3fe7577333d2ce1a4 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 24 Jun 2024 22:31:30 +0530 Subject: [PATCH 043/183] Added header to invoice preference page --- src/languages/en.ts | 10 ++++++++-- src/languages/es.ts | 10 ++++++++-- .../export/NetSuiteExportConfigurationPage.tsx | 2 +- .../NetSuiteInvoiceItemPreferenceSelectPage.tsx | 16 ++++++++++++++-- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 17a0d2c897c1..781bc5f3eeac 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2209,8 +2209,14 @@ export default { invoiceItem: { label: 'Invoice item', values: { - [CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.CREATE]: 'Create one for me', - [CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.SELECT]: 'Select existing', + [CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.CREATE]: { + label: 'Create one for me', + description: 'We\'ll create an "Expensify invoice line item" for you upon export (if one doesn’t exist already).', + }, + [CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.SELECT]: { + label: 'Select existing', + description: "We'll tie invoices from Expensify to the item selected below.", + }, }, }, exportDate: { diff --git a/src/languages/es.ts b/src/languages/es.ts index e5c38b4d825f..d5ab91817f05 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2245,8 +2245,14 @@ export default { invoiceItem: { label: 'Invoice item', values: { - [CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.CREATE]: 'Create one for me', - [CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.SELECT]: 'Select existing', + [CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.CREATE]: { + label: 'Create one for me', + description: 'We\'ll create an "Expensify invoice line item" for you upon export (if one doesn’t exist already).', + }, + [CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.SELECT]: { + label: 'Select existing', + description: "We'll tie invoices from Expensify to the item selected below.", + }, }, }, exportDate: { diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx index b623e59e3553..05cf8dd73d2d 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx @@ -143,7 +143,7 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) { description: translate('workspace.netsuite.invoiceItem.label'), onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_INVOICE_ITEM_PREFERENCE_SELECT.getRoute(policyID)), brickRoadIndicator: config?.errorFields?.invoiceItemPreference ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, - title: config?.invoiceItemPreference ? translate(`workspace.netsuite.invoiceItem.values.${config.invoiceItemPreference}`) : undefined, + title: config?.invoiceItemPreference ? translate(`workspace.netsuite.invoiceItem.values.${config.invoiceItemPreference}.label`) : undefined, pendingAction: config?.pendingFields?.invoiceItemPreference, errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.INVOICE_ITEM_PREFERENCE), onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.INVOICE_ITEM_PREFERENCE), diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx index 1b424d89a9cb..e7cc9c085835 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx @@ -1,4 +1,5 @@ -import React, {useCallback} from 'react'; +import React, {useCallback, useMemo} from 'react'; +import {View} from 'react-native'; import type {ValueOf} from 'type-fest'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -6,6 +7,7 @@ import RadioListItem from '@components/SelectionList/RadioListItem'; import type {ListItem} from '@components/SelectionList/types'; import SelectionScreen from '@components/SelectionScreen'; import type {SelectorType} from '@components/SelectionScreen'; +import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections/NetSuiteCommands'; @@ -29,7 +31,7 @@ function NetSuiteInvoiceItemPreferenceSelectPage({policy}: WithPolicyConnections const data: MenuListItem[] = Object.values(CONST.NETSUITE_INVOICE_ITEM_PREFERENCE).map((postingPreference) => ({ value: postingPreference, - text: translate(`workspace.netsuite.invoiceItem.values.${postingPreference}`), + text: translate(`workspace.netsuite.invoiceItem.values.${postingPreference}.label`), keyForList: postingPreference, isSelected: config?.invoiceItemPreference === postingPreference, })); @@ -46,12 +48,22 @@ function NetSuiteInvoiceItemPreferenceSelectPage({policy}: WithPolicyConnections [config?.invoiceItemPreference, policyID], ); + const headerContent = useMemo( + () => ( + + {translate(`workspace.netsuite.invoiceItem.values.${config?.invoiceItemPreference ?? 'create'}.description`)} + + ), + [styles.pb2, styles.ph5, styles.textNormal, translate, config?.invoiceItemPreference], + ); + return ( selectInvoicePreference(selection as MenuListItem)} initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} policyID={policyID} From e0af06e416eb38c5cfd37e30a74fcb5427d5e54f Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 24 Jun 2024 22:44:04 +0530 Subject: [PATCH 044/183] Fixing screen layout --- src/components/SelectionScreen.tsx | 5 +++-- .../export/NetSuiteExportConfigurationPage.tsx | 2 +- .../netsuite/export/NetSuiteExportExpensesPage.tsx | 2 +- .../export/NetSuiteInvoiceItemPreferenceSelectPage.tsx | 10 +++++++++- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/components/SelectionScreen.tsx b/src/components/SelectionScreen.tsx index 3dcc9887c275..df84cd100105 100644 --- a/src/components/SelectionScreen.tsx +++ b/src/components/SelectionScreen.tsx @@ -111,8 +111,9 @@ function SelectionScreen({ shouldShowTooltips={false} initiallyFocusedOptionKey={initiallyFocusedOptionKey} listEmptyContent={listEmptyContent} - /> - {children} + > + {children} + ); diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx index 05cf8dd73d2d..2724f11b058c 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx @@ -230,7 +230,7 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) { } return ( !item.shouldHide) .map((item) => ( { + const selectedRec = (items ?? []).find((item) => item.id === config?.invoiceItem); + return selectedRec; + }, [items, config?.invoiceItem]); + const data: MenuListItem[] = Object.values(CONST.NETSUITE_INVOICE_ITEM_PREFERENCE).map((postingPreference) => ({ value: postingPreference, text: translate(`workspace.netsuite.invoiceItem.values.${postingPreference}.label`), @@ -81,7 +88,8 @@ function NetSuiteInvoiceItemPreferenceSelectPage({policy}: WithPolicyConnections onClose={() => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.INVOICE_ITEM)} > Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_INVOICE_ITEM_SELECT.getRoute(policyID))} From 84ee1c8eb1cda18c3e2f9f4fc334b7e57b7c8603 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 24 Jun 2024 22:48:38 +0530 Subject: [PATCH 045/183] Fixing success response --- src/libs/actions/connections/NetSuiteCommands.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/connections/NetSuiteCommands.ts b/src/libs/actions/connections/NetSuiteCommands.ts index 7d1ab3c326b0..064af1760793 100644 --- a/src/libs/actions/connections/NetSuiteCommands.ts +++ b/src/libs/actions/connections/NetSuiteCommands.ts @@ -71,6 +71,7 @@ function updateNetSuiteOnyxData Date: Tue, 25 Jun 2024 03:31:13 +0530 Subject: [PATCH 046/183] fixes scroll issue in composer --- src/components/Composer/index.tsx | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 3a8a4e724948..684e1b2e68ac 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -5,7 +5,7 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {flushSync} from 'react-dom'; // eslint-disable-next-line no-restricted-imports import type {DimensionValue, NativeSyntheticEvent, Text as RNText, TextInput, TextInputKeyPressEventData, TextInputSelectionChangeEventData, TextStyle} from 'react-native'; -import {StyleSheet, View} from 'react-native'; +import {StyleSheet, View, DeviceEventEmitter} from 'react-native'; import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput'; import RNMarkdownTextInput from '@components/RNMarkdownTextInput'; import Text from '@components/Text'; @@ -74,7 +74,7 @@ function Composer( }, isReportActionCompose = false, isComposerFullSize = false, - shouldContainScroll = false, + shouldContainScroll = true, isGroupPolicyReport = false, ...props }: ComposerProps, @@ -105,6 +105,7 @@ function Composer( const [isRendered, setIsRendered] = useState(false); const isScrollBarVisible = useIsScrollBarVisible(textInput, value ?? ''); const [prevScroll, setPrevScroll] = useState(); + const isReportFlatListScrolling = useRef(false); useEffect(() => { if (!shouldClear) { @@ -249,6 +250,29 @@ function Composer( }; }, []); + useEffect(() => { + const scrollingListener = DeviceEventEmitter.addListener(CONST.EVENTS.SCROLLING, (scrolling) => { + isReportFlatListScrolling.current = scrolling; + }); + + return () => scrollingListener.remove(); + }, []); + + useEffect(() => { + const handleWheel = (e: MouseEvent) => { + if (isReportFlatListScrolling.current) { + e.preventDefault(); + return; + } + e.stopPropagation(); + }; + textInput.current?.addEventListener('wheel', handleWheel, {passive: false}); + + return () => { + textInput.current?.removeEventListener('wheel', handleWheel); + }; + }, []); + useEffect(() => { if (!textInput.current || prevScroll === undefined) { return; From 6517189f80b6bb4c7787699e6e2ea6ca89fa00a4 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 25 Jun 2024 11:37:00 +0530 Subject: [PATCH 047/183] Fixing screen layout --- src/components/SelectionScreen.tsx | 13 +++--- ...etSuiteInvoiceItemPreferenceSelectPage.tsx | 41 ++++++++++--------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/components/SelectionScreen.tsx b/src/components/SelectionScreen.tsx index df84cd100105..8dc0fca79622 100644 --- a/src/components/SelectionScreen.tsx +++ b/src/components/SelectionScreen.tsx @@ -19,9 +19,6 @@ type SelectorType = ListItem & { }; type SelectionScreenProps = { - /** React nodes that will be shown */ - children?: React.ReactNode; - /** Used to set the testID for tests */ displayName: string; @@ -34,6 +31,9 @@ type SelectionScreenProps = { /** Content to display if the list is empty */ listEmptyContent?: React.JSX.Element | null; + /** Custom content to display in the footer of list component. */ + listFooterContent?: React.JSX.Element | null; + /** Sections for the section list */ sections: Array>; @@ -66,11 +66,11 @@ type SelectionScreenProps = { }; function SelectionScreen({ - children, displayName, title, headerContent, listEmptyContent, + listFooterContent, sections, listItem, initiallyFocusedOptionKey, @@ -111,9 +111,8 @@ function SelectionScreen({ shouldShowTooltips={false} initiallyFocusedOptionKey={initiallyFocusedOptionKey} listEmptyContent={listEmptyContent} - > - {children} - + listFooterContent={listFooterContent} + /> ); diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx index 7741e688e0dc..034d0b641edd 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx @@ -78,26 +78,27 @@ function NetSuiteInvoiceItemPreferenceSelectPage({policy}: WithPolicyConnections featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID))} connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} - > - {config?.invoiceItemPreference === CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.SELECT && ( - Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.INVOICE_ITEM)} - > - Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_INVOICE_ITEM_SELECT.getRoute(policyID))} - brickRoadIndicator={config?.errorFields?.invoiceItem ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - /> - - )} - + listFooterContent={ + config?.invoiceItemPreference === CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.SELECT ? ( + Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.INVOICE_ITEM)} + > + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_INVOICE_ITEM_SELECT.getRoute(policyID))} + brickRoadIndicator={config?.errorFields?.invoiceItem ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + /> + + ) : null + } + /> ); } From aa747aaf3f674a8228452641fde3afa46a9e1ca3 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 25 Jun 2024 11:49:00 +0530 Subject: [PATCH 048/183] Remove a TODO --- src/components/MenuItem.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 8270e35856f1..c42e6a41e4ac 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -456,10 +456,6 @@ function MenuItem( const processedHelperText = useMemo(() => { let textToWrap = ''; - // TODO: Discuss this. - // if (shouldRenderAsHTML) { - // textToWrap = helperText ? convertToLTR(helperText) : ''; - // } if (shouldParseHelperText) { textToWrap = helperHtml; From fa048d0c6eef042db718d59e702ae61c0adb1b9a Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Tue, 25 Jun 2024 09:52:54 +0100 Subject: [PATCH 049/183] refactor: remove duplicate sizing --- src/styles/utils/sizing.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/styles/utils/sizing.ts b/src/styles/utils/sizing.ts index 1214a060fbdb..d0855b47f2bd 100644 --- a/src/styles/utils/sizing.ts +++ b/src/styles/utils/sizing.ts @@ -34,10 +34,6 @@ export default { maxHeight: '100%', }, - mnh0: { - minHeight: 0, - }, - mnh100: { minHeight: '100%', }, From 1a327da345fe7199c33cfb7df6c5042883976647 Mon Sep 17 00:00:00 2001 From: Riya Shete <156463907+eucool@users.noreply.github.com> Date: Tue, 25 Jun 2024 15:18:16 +0530 Subject: [PATCH 050/183] Update DisplayNamePage.tsx --- src/pages/settings/Profile/DisplayNamePage.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/settings/Profile/DisplayNamePage.tsx b/src/pages/settings/Profile/DisplayNamePage.tsx index e338fc16b0ee..90f7ca3abbd6 100644 --- a/src/pages/settings/Profile/DisplayNamePage.tsx +++ b/src/pages/settings/Profile/DisplayNamePage.tsx @@ -50,6 +50,8 @@ function DisplayNamePage({isLoadingApp = true, currentUserPersonalDetails}: Disp ErrorUtils.addErrorMessage(errors, 'firstName', translate('personalDetails.error.hasInvalidCharacter')); } else if (values.firstName.length > CONST.TITLE_CHARACTER_LIMIT) { ErrorUtils.addErrorMessage(errors, 'firstName', translate('common.error.characterLimitExceedCounter', {length: values.firstName.length, limit: CONST.TITLE_CHARACTER_LIMIT})); + } else if (values.firstName.length === 0) { + ErrorUtils.addErrorMessage(errors, 'firstName', translate('personalDetails.error.requiredFirstName')); } if (ValidationUtils.doesContainReservedWord(values.firstName, CONST.DISPLAY_NAME.RESERVED_NAMES)) { ErrorUtils.addErrorMessage(errors, 'firstName', translate('personalDetails.error.containsReservedWord')); From cc60db3effa82cd32d764125008db38e5ece2b6e Mon Sep 17 00:00:00 2001 From: Riya Shete <156463907+eucool@users.noreply.github.com> Date: Tue, 25 Jun 2024 15:18:55 +0530 Subject: [PATCH 051/183] Update en.ts --- src/languages/en.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index 68a8a8a6a029..be0734090888 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1429,6 +1429,7 @@ export default { error: { containsReservedWord: 'Name cannot contain the words Expensify or Concierge.', hasInvalidCharacter: 'Name cannot contain a comma or semicolon.', + requiredFirstName: 'First name cannot be empty.', }, }, privatePersonalDetails: { From 11c999fa68ad8a140ff9adf95791255e716c5521 Mon Sep 17 00:00:00 2001 From: Riya Shete <156463907+eucool@users.noreply.github.com> Date: Tue, 25 Jun 2024 15:19:12 +0530 Subject: [PATCH 052/183] Update es.ts --- src/languages/es.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/languages/es.ts b/src/languages/es.ts index cc3ae848de6b..8572a21d6bd5 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1429,6 +1429,7 @@ export default { error: { containsReservedWord: 'El nombre no puede contener las palabras Expensify o Concierge.', hasInvalidCharacter: 'El nombre no puede contener una coma o un punto y coma.', + requiredFirstName: 'El nombre no puede estar vacío.', }, }, privatePersonalDetails: { From 696fbcaa41924b695d3fa5a1a66b04095ac862fc Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Tue, 25 Jun 2024 16:08:06 +0530 Subject: [PATCH 053/183] prettier diffs --- src/components/Composer/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 684e1b2e68ac..f4a5174c2602 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -5,7 +5,7 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {flushSync} from 'react-dom'; // eslint-disable-next-line no-restricted-imports import type {DimensionValue, NativeSyntheticEvent, Text as RNText, TextInput, TextInputKeyPressEventData, TextInputSelectionChangeEventData, TextStyle} from 'react-native'; -import {StyleSheet, View, DeviceEventEmitter} from 'react-native'; +import {DeviceEventEmitter, StyleSheet, View} from 'react-native'; import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput'; import RNMarkdownTextInput from '@components/RNMarkdownTextInput'; import Text from '@components/Text'; From ad0ab1ddfbeb760a918a7eb1e554f08f9493619f Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 25 Jun 2024 18:39:41 +0800 Subject: [PATCH 054/183] fix send invoice recipient shown as hidden --- src/pages/iou/request/step/IOURequestStepConfirmation.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index ea03c9ae3b06..65c5756b507e 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -140,12 +140,10 @@ function IOURequestStepConfirmation({ const participants = useMemo( () => transaction?.participants?.map((participant) => { - const participantAccountID = participant.accountID ?? -1; - if (participant.isSender && iouType === CONST.IOU.TYPE.INVOICE) { return participant; } - return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); + return participant.accountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); }) ?? [], [transaction?.participants, personalDetails, iouType], ); From 5edc56d7fcabee3e8c4dc41a2fd814562b5d0662 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 25 Jun 2024 16:59:07 +0530 Subject: [PATCH 055/183] Fixing translations --- src/languages/en.ts | 25 +++------- src/languages/es.ts | 49 +++++++------------ .../NetSuiteExportConfigurationPage.tsx | 4 +- ...iteExportExpensesDestinationSelectPage.tsx | 2 +- .../export/NetSuiteExportExpensesPage.tsx | 4 +- ...NetSuiteExportExpensesVendorSelectPage.tsx | 2 +- .../NetSuitePreferredExporterSelectPage.tsx | 6 +-- ...uickbooksCompanyCardExpenseAccountPage.tsx | 2 +- .../QuickbooksExportConfigurationPage.tsx | 4 +- ...NonReimbursableDefaultVendorSelectPage.tsx | 2 +- ...oksOutOfPocketExpenseConfigurationPage.tsx | 2 +- ...ooksOutOfPocketExpenseEntitySelectPage.tsx | 2 +- ...ooksPreferredExporterConfigurationPage.tsx | 6 +-- .../export/XeroExportConfigurationPage.tsx | 4 +- .../XeroPreferredExporterSelectPage.tsx | 6 +-- 15 files changed, 47 insertions(+), 73 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 413df6f06fdd..70e14dafbe61 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2005,10 +2005,7 @@ export default { outOfPocketLocationEnabledDescription: 'QuickBooks Online doesn’t support locations on vendor bills or checks. As you have locations enabled on your workspace, these export options are unavailable.', taxesJournalEntrySwitchNote: "QuickBooks Online doesn't support taxes on journal entries. Please change your export option to vendor bill or check.", - export: 'Export', - exportAs: 'Export as', exportDescription: 'Configure how Expensify data exports to QuickBooks Online.', - preferredExporter: 'Preferred exporter', date: 'Export date', exportExpenses: 'Export out-of-pocket expenses as', exportInvoices: 'Export invoices to', @@ -2039,11 +2036,7 @@ export default { exportInvoicesDescription: 'Invoices will export to this account in QuickBooks Online.', exportCompanyCardsDescription: 'Set how company card purchases export to QuickBooks Online.', vendor: 'Vendor', - defaultVendor: 'Default vendor', defaultVendorDescription: 'Set a default vendor that will apply to all credit card transactions upon export.', - exportPreferredExporterNote: - 'The preferred exporter can be any workspace admin, but must also be a Domain Admin if you set different export accounts for individual company cards in Domain Settings.', - exportPreferredExporterSubNote: 'Once set, the preferred exporter will see reports for export in their account.', exportOutOfPocketExpensesDescription: 'Set how out-of-pocket expenses export to QuickBooks Online.', exportCheckDescription: "We'll create an itemized check for each Expensify report and send it from the bank account below.", exportJournalEntryDescription: "We'll create an itemized journal entry for each Expensify report and post it to the account below.", @@ -2121,7 +2114,6 @@ export default { default: 'Xero contact default', tag: 'Tags', }, - export: 'Export', exportDescription: 'Configure how Expensify data exports to Xero.', exportCompanyCard: 'Export company card expenses as', purchaseBill: 'Purchase bill', @@ -2129,7 +2121,6 @@ export default { bankTransactions: 'Bank transactions', xeroBankAccount: 'Xero bank account', xeroBankAccountDescription: 'Choose where expenses will post as bank transactions.', - preferredExporter: 'Preferred exporter', exportExpenses: 'Export out-of-pocket expenses as', exportExpensesDescription: 'Reports will export as a purchase bill with the date and status selected below.', purchaseBillDate: 'Purchase bill date', @@ -2175,28 +2166,18 @@ export default { [CONST.XERO_CONFIG.INVOICE_STATUS.AWAITING_PAYMENT]: 'Awaiting payment', }, }, - exportPreferredExporterNote: - 'The preferred exporter can be any workspace admin, but must be a domain admin if you set different export accounts for individual company cards in domain settings.', - exportPreferredExporterSubNote: 'Once set, the preferred exporter will see reports for export in their account.', noAccountsFound: 'No accounts found', noAccountsFoundDescription: 'Add the account in Xero and sync the connection again.', }, netsuite: { - export: 'Export', exportDescription: 'Configure how Expensify data exports to NetSuite.', exportReimbursable: 'Export reimbursable expenses as', exportNonReimbursable: 'Export non-reimbursable expenses as', - preferredExporter: 'Preferred exporter', exportInvoices: 'Export invoices to', journalEntriesTaxPostingAccount: 'Journal entries tax posting account', journalEntriesProvTaxPostingAccount: 'Journal entries provincial tax posting account', foreignCurrencyAmount: 'Export foreign currency amount', exportToNextOpenPeriod: 'Export to next open period', - exportPreferredExporterNote: - 'The preferred exporter can be any workspace admin, but must be a domain admin if you set different export accounts for individual company cards in domain settings.', - exportPreferredExporterSubNote: 'Once set, the preferred exporter will see reports for export in their account.', - exportAs: 'Export as', - defaultVendor: 'Default vendor', nonReimbursableJournalPostingAccount: 'Non-reimbursable journal posting account', reimbursableJournalPostingAccount: 'Reimbursable journal posting account', journalPostingPreference: { @@ -2663,6 +2644,12 @@ export default { } }, }, + preferredExporter: 'Preferred exporter', + exportPreferredExporterNote: + 'The preferred exporter can be any workspace admin, but must also be a Domain Admin if you set different export accounts for individual company cards in Domain Settings.', + exportPreferredExporterSubNote: 'Once set, the preferred exporter will see reports for export in their account.', + exportAs: 'Export as', + defaultVendor: 'Default vendor', }, bills: { manageYourBills: 'Manage your bills', diff --git a/src/languages/es.ts b/src/languages/es.ts index 0839b08e90ea..e1aa8020f2e6 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2028,13 +2028,10 @@ export default { taxesJournalEntrySwitchNote: 'QuickBooks Online no permite impuestos en los asientos contables. Por favor, cambia la opción de exportación a factura de proveedor o cheque.', locationsAdditionalDescription: 'QuickBooks Online no permite lugares en facturas de proveedores o cheques. Como tienes activadas los lugares en tu espacio de trabajo, estas opciones de exportación no están disponibles.', - export: 'Exportar', - exportAs: 'Exportar cómo', exportExpenses: 'Exportar gastos de bolsillo como', exportInvoices: 'Exportar facturas a', exportCompany: 'Exportar tarjetas de empresa como', exportDescription: 'Configura cómo se exportan los datos de Expensify a QuickBooks Online.', - preferredExporter: 'Exportador preferido', date: 'Fecha de exportación', deepDiveExpensifyCard: 'Las transacciones de la Tarjeta Expensify se exportan automáticamente a una "Cuenta de Responsabilidad de la Tarjeta Expensify" creada con', deepDiveExpensifyCardIntegration: 'nuestra integración.', @@ -2064,7 +2061,6 @@ export default { account: 'Cuenta', accountDescription: 'Elige dónde contabilizar las compensaciones de entradas a los asientos contables.', vendor: 'Proveedor', - defaultVendor: 'Proveedor predeterminado', defaultVendorDescription: 'Establece un proveedor predeterminado que se aplicará a todas las transacciones con tarjeta de crédito al momento de exportarlas.', accountsPayable: 'Cuentas por pagar', accountsPayableDescription: 'Elige dónde crear las facturas de proveedores.', @@ -2073,9 +2069,6 @@ export default { optionBelow: 'Elija una opción a continuación:', companyCardsLocationEnabledDescription: 'QuickBooks Online no permite lugares en las exportaciones de facturas de proveedores. Como tienes activadas los lugares en tu espacio de trabajo, esta opción de exportación no está disponible.', - exportPreferredExporterNote: - 'Puede ser cualquier administrador del espacio de trabajo, pero debe ser un administrador de dominio si configura diferentes cuentas de exportación para tarjetas de empresa individuales en la configuración del dominio.', - exportPreferredExporterSubNote: 'Una vez configurado, el exportador preferido verá los informes para exportar en tu cuenta.', exportOutOfPocketExpensesDescription: 'Establezca cómo se exportan los gastos de bolsillo a QuickBooks Online.', exportCheckDescription: 'Crearemos un cheque desglosado para cada informe de Expensify y lo enviaremos desde la cuenta bancaria a continuación.', exportJournalEntryDescription: 'Crearemos una entrada contable desglosada para cada informe de Expensify y lo contabilizaremos en la cuenta a continuación.', @@ -2155,7 +2148,6 @@ export default { default: 'Contacto de Xero por defecto', tag: 'Etiquetas', }, - export: 'Exportar', exportDescription: 'Configura cómo se exportan los datos de Expensify a Xero.', exportCompanyCard: 'Exportar gastos de la tarjeta de empresa como', purchaseBill: 'Factura de compra', @@ -2164,7 +2156,6 @@ export default { bankTransactions: 'Transacciones bancarias', xeroBankAccount: 'Cuenta bancaria de Xero', xeroBankAccountDescription: 'Elige dónde se contabilizarán los gastos como transacciones bancarias.', - preferredExporter: 'Exportador preferido', exportExpenses: 'Exportar gastos por cuenta propia como', exportExpensesDescription: 'Los informes se exportarán como una factura de compra utilizando la fecha y el estado que seleccione a continuación', purchaseBillDate: 'Fecha de la factura de compra', @@ -2211,28 +2202,18 @@ export default { [CONST.XERO_CONFIG.INVOICE_STATUS.AWAITING_PAYMENT]: 'Pendiente de pago', }, }, - exportPreferredExporterNote: - 'Puede ser cualquier administrador del espacio de trabajo, pero debe ser un administrador de dominio si configura diferentes cuentas de exportación para tarjetas de empresa individuales en la configuración del dominio.', - exportPreferredExporterSubNote: 'Una vez configurado, el exportador preferido verá los informes para exportar en su cuenta.', noAccountsFound: 'No se ha encontrado ninguna cuenta', noAccountsFoundDescription: 'Añade la cuenta en Xero y sincroniza de nuevo la conexión.', }, netsuite: { - export: 'Export', - exportDescription: 'Configure how Expensify data exports to NetSuite.', + exportDescription: 'Configura cómo se exportan los datos de Expensify a NetSuite.', exportReimbursable: 'Export reimbursable expenses as', exportNonReimbursable: 'Export non-reimbursable expenses as', - preferredExporter: 'Preferred exporter', - exportInvoices: 'Export invoices to', + exportInvoices: 'Exportar facturas a', journalEntriesTaxPostingAccount: 'Journal entries tax posting account', journalEntriesProvTaxPostingAccount: 'Journal entries provincial tax posting account', foreignCurrencyAmount: 'Export foreign currency amount', exportToNextOpenPeriod: 'Export to next open period', - exportPreferredExporterNote: - 'The preferred exporter can be any workspace admin, but must be a domain admin if you set different export accounts for individual company cards in domain settings.', - exportPreferredExporterSubNote: 'Once set, the preferred exporter will see reports for export in their account.', - exportAs: 'Export as', - defaultVendor: 'Default vendor', nonReimbursableJournalPostingAccount: 'Non-reimbursable journal posting account', reimbursableJournalPostingAccount: 'Reimbursable journal posting account', journalPostingPreference: { @@ -2256,20 +2237,20 @@ export default { }, }, exportDate: { - label: 'Export date', - description: 'Use this date when exporting reports to NetSuite.', + label: 'Fecha de exportación', + description: 'Usa esta fecha al exportar informe a NetSuite.', values: { [CONST.NETSUITE_EXPORT_DATE.LAST_EXPENSE]: { - label: 'Date of last expense', - description: 'Date of the most recent expense on the report.', + label: 'Fecha del último gasto', + description: 'Fecha del gasto mas reciente en el informe', }, [CONST.NETSUITE_EXPORT_DATE.EXPORTED]: { - label: 'Export date', - description: 'Date the report was exported to NetSuite.', + label: 'Fecha de exportación', + description: 'Fecha de exportación del informe a NetSuite', }, [CONST.NETSUITE_EXPORT_DATE.SUBMITTED]: { - label: 'Submitted date', - description: 'Date the report was submitted for approval.', + label: 'Fecha de envío', + description: 'Fecha en la que el informe se envió para su aprobación', }, }, }, @@ -2304,8 +2285,8 @@ export default { }, }, }, - noAccountsFound: 'No accounts found', - noAccountsFoundDescription: 'Add the account in NetSuite and sync the connection again.', + noAccountsFound: 'No se ha encontrado ninguna cuenta', + noAccountsFoundDescription: 'Añade la cuenta en NetSuite y sincroniza de nuevo la conexión.', noVendorsFound: 'No vendors found', noVendorsFoundDescription: 'Add vendors in NetSuite and sync the connection again.', noItemsFound: 'No invoice items found', @@ -2667,6 +2648,12 @@ export default { } }, }, + preferredExporter: 'Exportador preferido', + exportPreferredExporterNote: + 'Puede ser cualquier administrador del espacio de trabajo, pero debe ser un administrador de dominio si configura diferentes cuentas de exportación para tarjetas de empresa individuales en la configuración del dominio.', + exportPreferredExporterSubNote: 'Una vez configurado, el exportador preferido verá los informes para exportar en tu cuenta.', + exportAs: 'Exportar cómo', + defaultVendor: 'Proveedor predeterminado', }, card: { header: 'Desbloquea Tarjetas Expensify gratis', diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx index 2724f11b058c..95f9a58216e2 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx @@ -80,7 +80,7 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) { const menuItems: Array = [ { type: 'menuitem', - description: translate('workspace.netsuite.preferredExporter'), + description: translate('workspace.accounting.preferredExporter'), onPress: () => { Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_PREFERRED_EXPORTER_SELECT.getRoute(policyID)); }, @@ -201,7 +201,7 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) { return ( selectDestination(selection as MenuListItem)} diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx index e0405b34e774..1b243cb41628 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx @@ -73,7 +73,7 @@ function NetSuiteExportExpensesPage({policy}: WithPolicyConnectionsProps) { const menuItems: MenuItem[] = [ { - description: translate('workspace.netsuite.exportAs'), + description: translate('workspace.accounting.exportAs'), onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_DESTINATION_SELECT.getRoute(policyID, params.expenseType)), brickRoadIndicator: exportDestinationError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, title: exportDestination ? translate(`workspace.netsuite.exportDestination.values.${exportDestination}.label`) : undefined, @@ -84,7 +84,7 @@ function NetSuiteExportExpensesPage({policy}: WithPolicyConnectionsProps) { shouldParseHelperText: true, }, { - description: translate('workspace.netsuite.defaultVendor'), + description: translate('workspace.accounting.defaultVendor'), onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_VENDOR_SELECT.getRoute(policyID, params.expenseType)), brickRoadIndicator: config?.errorFields?.defaultVendor ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, title: defaultVendor ? defaultVendor.name : undefined, diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx index a356313fd7d2..f4b5586ec664 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx @@ -71,7 +71,7 @@ function NetSuiteExportExpensesVendorSelectPage({policy}: WithPolicyConnectionsP onSelectRow={updateDefaultVendor} initiallyFocusedOptionKey={initiallyFocusedOptionKey} onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES.getRoute(policyID, params.expenseType))} - title="workspace.netsuite.defaultVendor" + title="workspace.accounting.defaultVendor" listEmptyContent={listEmptyContent} connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} shouldBeBlocked={isReimbursable || config?.nonreimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.VENDOR_BILL} diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuitePreferredExporterSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuitePreferredExporterSelectPage.tsx index d22635e71940..2a7fdcdf0915 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuitePreferredExporterSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuitePreferredExporterSelectPage.tsx @@ -74,8 +74,8 @@ function NetSuitePreferredExporterSelectPage({policy}: WithPolicyConnectionsProp const headerContent = useMemo( () => ( - {translate('workspace.netsuite.exportPreferredExporterNote')} - {translate('workspace.netsuite.exportPreferredExporterSubNote')} + {translate('workspace.accounting.exportPreferredExporterNote')} + {translate('workspace.accounting.exportPreferredExporterSubNote')} ), [translate, styles.pb2, styles.ph5, styles.pb5, styles.textNormal], @@ -93,7 +93,7 @@ function NetSuitePreferredExporterSelectPage({policy}: WithPolicyConnectionsProp onSelectRow={selectExporter} initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID))} - title="workspace.netsuite.preferredExporter" + title="workspace.accounting.preferredExporter" connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} /> ); diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPage.tsx index d18edd0489b8..f23ab5db779a 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPage.tsx @@ -69,7 +69,7 @@ function QuickbooksCompanyCardExpenseAccountPage({policy}: WithPolicyConnections subtitle={translate('workspace.qbo.defaultVendorDescription')} switchAccessibilityLabel={translate('workspace.qbo.defaultVendorDescription')} errors={errorFields?.autoCreateVendor ?? undefined} - title={translate('workspace.qbo.defaultVendor')} + title={translate('workspace.accounting.defaultVendor')} wrapperStyle={[styles.ph5, styles.mb3, styles.mt1]} isActive={!!autoCreateVendor} onToggle={(isOn) => diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksExportConfigurationPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksExportConfigurationPage.tsx index 9f3709ae3b28..cb3718c1b2c5 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksExportConfigurationPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksExportConfigurationPage.tsx @@ -36,7 +36,7 @@ function QuickbooksExportConfigurationPage({policy}: WithPolicyConnectionsProps) } = policy?.connections?.quickbooksOnline?.config ?? {}; const menuItems: MenuItem[] = [ { - description: translate('workspace.qbo.preferredExporter'), + description: translate('workspace.accounting.preferredExporter'), onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_PREFERRED_EXPORTER.getRoute(policyID)), brickRoadIndicator: errorFields?.exporter ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, title: exportConfiguration?.exporter ?? policyOwner, @@ -93,7 +93,7 @@ function QuickbooksExportConfigurationPage({policy}: WithPolicyConnectionsProps) includeSafeAreaPaddingBottom={false} testID={QuickbooksExportConfigurationPage.displayName} > - + {translate('workspace.qbo.exportDescription')} {menuItems.map((menuItem) => ( diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableDefaultVendorSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableDefaultVendorSelectPage.tsx index 55c5cf7e9898..af9d3e4dbc6b 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableDefaultVendorSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableDefaultVendorSelectPage.tsx @@ -71,7 +71,7 @@ function QuickbooksNonReimbursableDefaultVendorSelectPage({policy}: WithPolicyCo featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > - + {translate('workspace.qbo.defaultVendorDescription')}} sections={sections} diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseConfigurationPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseConfigurationPage.tsx index 32449b89a7a6..42dfe13bf60d 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseConfigurationPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseConfigurationPage.tsx @@ -63,7 +63,7 @@ function QuickbooksOutOfPocketExpenseConfigurationPage({policy}: WithPolicyConne Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT_OUT_OF_POCKET_EXPENSES_SELECT.getRoute(policyID))} brickRoadIndicator={hasErrors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseEntitySelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseEntitySelectPage.tsx index 39eb6b1845c8..8983ea7e9731 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseEntitySelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseEntitySelectPage.tsx @@ -113,7 +113,7 @@ function QuickbooksOutOfPocketExpenseEntitySelectPage({policy}: WithPolicyConnec includeSafeAreaPaddingBottom={false} testID={QuickbooksOutOfPocketExpenseEntitySelectPage.displayName} > - + - + - {translate('workspace.qbo.exportPreferredExporterNote')} - {translate('workspace.qbo.exportPreferredExporterSubNote')} + {translate('workspace.accounting.exportPreferredExporterNote')} + {translate('workspace.accounting.exportPreferredExporterSubNote')} } sections={[{data}]} diff --git a/src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx b/src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx index 40e9c0310f74..00ce63b1dccc 100644 --- a/src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/export/XeroExportConfigurationPage.tsx @@ -33,7 +33,7 @@ function XeroExportConfigurationPage({policy}: WithPolicyConnectionsProps) { const menuItems: MenuItem[] = [ { - description: translate('workspace.xero.preferredExporter'), + description: translate('workspace.accounting.preferredExporter'), onPress: () => { Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_PREFERRED_EXPORTER_SELECT.getRoute(policyID)); }, @@ -91,7 +91,7 @@ function XeroExportConfigurationPage({policy}: WithPolicyConnectionsProps) { return ( ( - {translate('workspace.xero.exportPreferredExporterNote')} - {translate('workspace.xero.exportPreferredExporterSubNote')} + {translate('workspace.accounting.exportPreferredExporterNote')} + {translate('workspace.accounting.exportPreferredExporterSubNote')} ), [translate, styles.pb2, styles.ph5, styles.pb5, styles.textNormal], @@ -93,7 +93,7 @@ function XeroPreferredExporterSelectPage({policy}: WithPolicyConnectionsProps) { onSelectRow={selectExporter} initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_XERO_EXPORT.getRoute(policyID))} - title="workspace.xero.preferredExporter" + title="workspace.accounting.preferredExporter" connectionName={CONST.POLICY.CONNECTIONS.NAME.XERO} /> ); From 764af81f018bfd0304ac835d46617ce9134c7e8f Mon Sep 17 00:00:00 2001 From: Shubham Agrawal <58412969+shubham1206agra@users.noreply.github.com> Date: Tue, 25 Jun 2024 17:05:59 +0530 Subject: [PATCH 056/183] Apply suggestions from code review --- src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 3ac605e92052..5908a8a8eaa6 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -317,7 +317,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/xero/export/XeroPreferredExporterSelectPage').default, [SCREENS.WORKSPACE.ACCOUNTING.XERO_BILL_PAYMENT_ACCOUNT_SELECTOR]: () => - require('../../../../pages/workspace/accounting/xero/advanced/XeroBillPaymentAccountSelectorPage').default as React.ComponentType, + require('../../../../pages/workspace/accounting/xero/advanced/XeroBillPaymentAccountSelectorPage').default, [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT]: () => require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_PREFERRED_EXPORTER_SELECT]: () => From d5350558e61ec09fbedb77b9e8cc9978b3352e94 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 25 Jun 2024 17:09:27 +0530 Subject: [PATCH 057/183] Fixing types after merge --- .../ModalStackNavigators/index.tsx | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 5908a8a8eaa6..80127a353550 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -319,30 +319,29 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/xero/advanced/XeroBillPaymentAccountSelectorPage').default, - [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT]: () => require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage').default as React.ComponentType, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT]: () => require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage').default, [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_PREFERRED_EXPORTER_SELECT]: () => - require('../../../../pages/workspace/accounting/netsuite/export/NetSuitePreferredExporterSelectPage').default as React.ComponentType, - [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_DATE_SELECT]: () => require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteDateSelectPage').default as React.ComponentType, - [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES]: () => - require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage').default as React.ComponentType, + require('../../../../pages/workspace/accounting/netsuite/export/NetSuitePreferredExporterSelectPage').default, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_DATE_SELECT]: () => require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteDateSelectPage').default, + [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES]: () => require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage').default, [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES_DESTINATION_SELECT]: () => - require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesDestinationSelectPage').default as React.ComponentType, + require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesDestinationSelectPage').default, [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES_VENDOR_SELECT]: () => - require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage').default as React.ComponentType, + require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage').default, [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES_PAYABLE_ACCOUNT_SELECT]: () => - require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPayableAccountSelectPage').default as React.ComponentType, + require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPayableAccountSelectPage').default, [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_EXPORT_EXPENSES_JOURNAL_POSTING_PREFERENCE_SELECT]: () => - require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage').default as React.ComponentType, + require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage').default, [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_RECEIVABLE_ACCOUNT_SELECT]: () => - require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteReceivableAccountSelectPage').default as React.ComponentType, + require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteReceivableAccountSelectPage').default, [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_INVOICE_ITEM_PREFERENCE_SELECT]: () => - require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage').default as React.ComponentType, + require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage').default, [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_INVOICE_ITEM_SELECT]: () => - require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemSelectPage').default as React.ComponentType, + require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemSelectPage').default, [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_TAX_POSTING_ACCOUNT_SELECT]: () => - require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteTaxPostingAccountSelectPage').default as React.ComponentType, + require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteTaxPostingAccountSelectPage').default, [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_PROVINCIAL_TAX_POSTING_ACCOUNT_SELECT]: () => - require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage').default as React.ComponentType, + require('../../../../pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage').default, [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage').default, [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage').default, From e6fbf2b100690429c99dd8c271be2278ea130415 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 25 Jun 2024 15:53:47 +0200 Subject: [PATCH 058/183] apply suggested changes --- src/languages/en.ts | 2 +- src/libs/SubscriptionUtils.ts | 12 +- .../SubscriptionBillingBanner.tsx | 13 +- .../Subscription/CardSection/CardSection.tsx | 15 +- .../Subscription/CardSection/utils.ts | 14 +- tests/unit/CardsSectionUtilsTest.ts | 180 ++++++++++++++++++ tests/unit/SubscriptionUtilsTest.ts | 50 +++++ 7 files changed, 256 insertions(+), 30 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 9d6d5ab44285..6a161b479d6c 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3259,7 +3259,7 @@ export default { outdatedInfo: 'Your payment info is outdated', updatePaymentInformation: 'Please update your payment information.', updateCardDataByDate: ({date}) => `Update your payment card by ${date} to continue using all of your favorite features.`, - paymentPastDuePayByDate: ({date}) => `Your payment is past due. Please pay your invoice by ${date} to avoid service interruption`, + paymentPastDuePayByDate: ({date}) => `Your payment is past due. Please pay your invoice by ${date} to avoid service interruption.`, paymentPastDue: 'Your payment is past due. Please pay your invoice.', cardCouldNotBeCharged: 'Your card couldn’t be charged', retryMessage: 'Before retrying, please call your bank directly to authorize Expensify charges and remove any holds. Otherwise, try adding a different payment card.', diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 29b4bfa92227..d07640fa7b72 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -192,13 +192,17 @@ function getCardForSubscriptionBilling(): Fund | undefined { * @returns Whether the card is due to expire soon. */ function hasCardExpiringSoon(): boolean { + if (billingStatus) { + return false; + } + const card = getCardForSubscriptionBilling(); if (!card) { return false; } - return !billingStatus && card?.accountData?.cardMonth === new Date().getMonth() + 1; + return card?.accountData?.cardYear === new Date().getFullYear() && card?.accountData?.cardMonth === new Date().getMonth() + 1; } /** @@ -216,14 +220,14 @@ function isRetryBillingSuccessful(): boolean { } type SubscriptionStatus = { - status?: string; + status: string; isError?: boolean; }; /** * @returns The subscription status. */ -function getSubscriptionStatus(): SubscriptionStatus { +function getSubscriptionStatus(): SubscriptionStatus | undefined { if (hasOverdueGracePeriod()) { if (amountOwed) { // 1. Policy owner with amount owed, within grace period @@ -308,7 +312,7 @@ function getSubscriptionStatus(): SubscriptionStatus { }; } - return {}; + return undefined; } /** diff --git a/src/pages/settings/Subscription/CardSection/BillingBanner/SubscriptionBillingBanner.tsx b/src/pages/settings/Subscription/CardSection/BillingBanner/SubscriptionBillingBanner.tsx index fcad66c9b268..dce215e7dbbc 100644 --- a/src/pages/settings/Subscription/CardSection/BillingBanner/SubscriptionBillingBanner.tsx +++ b/src/pages/settings/Subscription/CardSection/BillingBanner/SubscriptionBillingBanner.tsx @@ -7,9 +7,6 @@ import BillingBanner from './BillingBanner'; import type {BillingBannerProps} from './BillingBanner'; type SubscriptionBillingBannerProps = Omit & { - /** Indicates whether there is an active trial */ - isTrialActive?: boolean; - /** Indicates whether there is an error */ isError?: boolean; @@ -17,13 +14,9 @@ type SubscriptionBillingBannerProps = Omit ); diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 4d79f165169c..d93b43fddff9 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -27,7 +27,7 @@ function CardSection() { const cardMonth = useMemo(() => DateUtils.getMonthNames(preferredLocale)[(defaultCard?.accountData?.cardMonth ?? 1) - 1], [defaultCard?.accountData?.cardMonth, preferredLocale]); - const {title, subtitle, isError, icon, rightIcon} = CardSectionUtils.getBillingStatus(translate, defaultCard?.accountData?.cardNumber ?? ''); + const billingStatus = CardSectionUtils.getBillingStatus(translate, defaultCard?.accountData?.cardNumber ?? ''); const nextPaymentDate = !isEmptyObject(privateSubscription) ? CardSectionUtils.getNextBillingDate() : undefined; @@ -36,15 +36,14 @@ function CardSection() { let BillingBanner: React.ReactNode | undefined; if (!CardSectionUtils.shouldShowPreTrialBillingBanner()) { BillingBanner = ; - } else if (title && subtitle) { + } else if (billingStatus) { BillingBanner = ( ); } diff --git a/src/pages/settings/Subscription/CardSection/utils.ts b/src/pages/settings/Subscription/CardSection/utils.ts index d7cf2972d18c..126f5c03b3f7 100644 --- a/src/pages/settings/Subscription/CardSection/utils.ts +++ b/src/pages/settings/Subscription/CardSection/utils.ts @@ -10,10 +10,9 @@ import type {Fund} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; type BillingStatusResult = { - title?: string; - subtitle?: string; - isError?: boolean; - isTrialActive?: boolean; + title: string; + subtitle: string; + isError: boolean; isRetryAvailable?: boolean; isAddButtonDark?: boolean; isAuthenticatingRequired?: boolean; @@ -24,7 +23,7 @@ type BillingStatusResult = { function getBillingStatus( translate: (phraseKey: TKey, ...phraseParameters: PhraseParameters>) => string, cardEnding: string, -): BillingStatusResult { +): BillingStatusResult | undefined { const amountOwed = SubscriptionUtils.getAmountOwed(); const status = SubscriptionUtils.getSubscriptionStatus(); @@ -33,7 +32,7 @@ function getBillingStatus( const endDateFormatted = endDate ? DateUtils.formatWithUTCTimeZone(fromUnixTime(endDate).toUTCString(), CONST.DATE.MONTH_DAY_YEAR_FORMAT) : null; - switch (status.status) { + switch (status?.status) { case SubscriptionUtils.PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED: return { title: translate('subscription.billingBanner.outdatedInfo'), @@ -121,7 +120,7 @@ function getBillingStatus( }; default: - return {}; + return undefined; } } @@ -152,3 +151,4 @@ export default { shouldShowPreTrialBillingBanner, getNextBillingDate, }; +export type {BillingStatusResult}; diff --git a/tests/unit/CardsSectionUtilsTest.ts b/tests/unit/CardsSectionUtilsTest.ts index 93d288943f4b..f0e66edcded8 100644 --- a/tests/unit/CardsSectionUtilsTest.ts +++ b/tests/unit/CardsSectionUtilsTest.ts @@ -1,5 +1,30 @@ +import * as Expensicons from '@components/Icon/Expensicons'; +import * as Illustrations from '@components/Icon/Illustrations'; +import type {Phrase, PhraseParameters} from '@libs/Localize'; +import type * as SubscriptionUtils from '@libs/SubscriptionUtils'; +import {PAYMENT_STATUS} from '@libs/SubscriptionUtils'; +import type {TranslationPaths} from '@src/languages/types'; +import type {BillingStatusResult} from '@src/pages/settings/Subscription/CardSection/utils'; import CardSectionUtils from '@src/pages/settings/Subscription/CardSection/utils'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars -- this param is required for the mock +function translateMock(key: TKey, ...phraseParameters: PhraseParameters>): string { + return key; +} + +const CARD_ENDING = '1234'; +const AMOUNT_OWED = 100; +const GRACE_PERIOD_DATE = 1750819200; + +const mockGetSubscriptionStatus = jest.fn(); + +jest.mock('@libs/SubscriptionUtils', () => ({ + ...jest.requireActual('@libs/SubscriptionUtils'), + getAmountOwed: () => AMOUNT_OWED, + getOverdueGracePeriodDate: () => GRACE_PERIOD_DATE, + getSubscriptionStatus: () => mockGetSubscriptionStatus() as BillingStatusResult, +})); + describe('getNextBillingDate', () => { beforeAll(() => { jest.useFakeTimers(); @@ -35,3 +60,158 @@ describe('getNextBillingDate', () => { expect(CardSectionUtils.getNextBillingDate()).toEqual(expectedNextBillingDate); }); }); + +describe('CardSectionUtils', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + beforeAll(() => { + mockGetSubscriptionStatus.mockReturnValue(''); + }); + + it('should return undefined by default', () => { + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toBeUndefined(); + }); + + it('should return POLICY_OWNER_WITH_AMOUNT_OWED variant', () => { + mockGetSubscriptionStatus.mockReturnValue({ + status: PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED, + }); + + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ + title: 'subscription.billingBanner.outdatedInfo', + subtitle: 'subscription.billingBanner.updateCardDataByDate', + isError: true, + isRetryAvailable: true, + }); + }); + + it('should return POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE variant', () => { + mockGetSubscriptionStatus.mockReturnValue({ + status: PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE, + }); + + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ + title: 'subscription.billingBanner.outdatedInfo', + subtitle: 'subscription.billingBanner.updatePaymentInformation', + isError: true, + }); + }); + + it('should return OWNER_OF_POLICY_UNDER_INVOICING variant', () => { + mockGetSubscriptionStatus.mockReturnValue({ + status: PAYMENT_STATUS.OWNER_OF_POLICY_UNDER_INVOICING, + }); + + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ + title: 'subscription.billingBanner.outdatedInfo', + subtitle: 'subscription.billingBanner.paymentPastDuePayByDate', + isError: true, + isAddButtonDark: true, + }); + }); + + it('should return OWNER_OF_POLICY_UNDER_INVOICING_OVERDUE variant', () => { + mockGetSubscriptionStatus.mockReturnValue({ + status: PAYMENT_STATUS.OWNER_OF_POLICY_UNDER_INVOICING_OVERDUE, + }); + + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ + title: 'subscription.billingBanner.outdatedInfo', + subtitle: 'subscription.billingBanner.paymentPastDue', + isError: true, + isAddButtonDark: true, + }); + }); + + it('should return BILLING_DISPUTE_PENDING variant', () => { + mockGetSubscriptionStatus.mockReturnValue({ + status: PAYMENT_STATUS.BILLING_DISPUTE_PENDING, + }); + + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ + title: 'subscription.billingBanner.cardCouldNotBeCharged', + subtitle: 'subscription.billingBanner.cardOnDispute', + isError: true, + isRetryAvailable: false, + }); + }); + + it('should return CARD_AUTHENTICATION_REQUIRED variant', () => { + mockGetSubscriptionStatus.mockReturnValue({ + status: PAYMENT_STATUS.CARD_AUTHENTICATION_REQUIRED, + }); + + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ + title: 'subscription.billingBanner.cardCouldNotBeCharged', + subtitle: 'subscription.billingBanner.cardNotFullyAuthenticated', + isError: true, + isAuthenticatingRequired: true, + }); + }); + + it('should return INSUFFICIENT_FUNDS variant', () => { + mockGetSubscriptionStatus.mockReturnValue({ + status: PAYMENT_STATUS.INSUFFICIENT_FUNDS, + }); + + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ + title: 'subscription.billingBanner.cardCouldNotBeCharged', + subtitle: 'subscription.billingBanner.cardDeclinedDueToInsufficientFunds', + isError: true, + isRetryAvailable: true, + }); + }); + + it('should return CARD_EXPIRED variant', () => { + mockGetSubscriptionStatus.mockReturnValue({ + status: PAYMENT_STATUS.CARD_EXPIRED, + }); + + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ + title: 'subscription.billingBanner.cardCouldNotBeCharged', + subtitle: 'subscription.billingBanner.cardExpired', + isError: true, + isRetryAvailable: true, + }); + }); + + it('should return CARD_EXPIRE_SOON variant', () => { + mockGetSubscriptionStatus.mockReturnValue({ + status: PAYMENT_STATUS.CARD_EXPIRE_SOON, + }); + + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ + title: 'subscription.billingBanner.cardExpiringSoon', + subtitle: 'subscription.billingBanner.cardWillExpireAtTheEndOfMonth', + isError: false, + icon: Illustrations.CreditCardEyes, + }); + }); + + it('should return RETRY_BILLING_SUCCESS variant', () => { + mockGetSubscriptionStatus.mockReturnValue({ + status: PAYMENT_STATUS.RETRY_BILLING_SUCCESS, + }); + + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ + title: 'subscription.billingBanner.succeeded', + subtitle: 'subscription.billingBanner.billedSuccessfully', + isError: false, + rightIcon: Expensicons.Close, + }); + }); + + it('should return RETRY_BILLING_ERROR variant', () => { + mockGetSubscriptionStatus.mockReturnValue({ + status: PAYMENT_STATUS.RETRY_BILLING_ERROR, + }); + + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ + title: 'subscription.billingBanner.cardCouldNotBeCharged', + subtitle: 'subscription.billingBanner.retryMessage', + isError: true, + }); + }); +}); diff --git a/tests/unit/SubscriptionUtilsTest.ts b/tests/unit/SubscriptionUtilsTest.ts index 498cbef2dc57..3d309a84547d 100644 --- a/tests/unit/SubscriptionUtilsTest.ts +++ b/tests/unit/SubscriptionUtilsTest.ts @@ -1,6 +1,7 @@ import {addDays, addMinutes, format as formatDate, getUnixTime, subDays} from 'date-fns'; import Onyx from 'react-native-onyx'; import * as SubscriptionUtils from '@libs/SubscriptionUtils'; +import {PAYMENT_STATUS} from '@libs/SubscriptionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {BillingGraceEndPeriod} from '@src/types/onyx'; @@ -12,6 +13,9 @@ const billingGraceEndPeriod: BillingGraceEndPeriod = { value: 0, }; +const GRACE_PERIOD_DATE = 1750819200100; +const AMOUNT_OWED = 100; + Onyx.init({keys: ONYXKEYS}); describe('SubscriptionUtils', () => { @@ -245,4 +249,50 @@ describe('SubscriptionUtils', () => { expect(SubscriptionUtils.shouldRestrictUserBillableActions(policyID)).toBeTruthy(); }); }); + + describe('getSubscriptionStatus', () => { + beforeAll(() => { + Onyx.init({ + keys: { + NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END: ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END, + NVP_PRIVATE_AMOUNT_OWED: ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED, + }, + initialKeyStates: { + [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: undefined, + [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: undefined, + }, + }); + }); + + afterEach(() => { + Onyx.clear(); + }); + + it('should return undefined by default', () => { + expect(SubscriptionUtils.getSubscriptionStatus()).toBeUndefined(); + }); + + it('should return POLICY_OWNER_WITH_AMOUNT_OWED status', async () => { + await Onyx.multiSet({ + [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: 1, + [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: AMOUNT_OWED, + }); + + expect(SubscriptionUtils.getSubscriptionStatus()).toEqual({ + status: PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED, + isError: true, + }); + }); + + it('should return POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE status', async () => { + await Onyx.multiSet({ + [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: GRACE_PERIOD_DATE, + [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: AMOUNT_OWED, + }); + + expect(SubscriptionUtils.getSubscriptionStatus()).toEqual({ + status: PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE, + }); + }); + }); }); From a9907d067742f97b29cf4f81390cf10436fe9a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 25 Jun 2024 15:00:22 +0100 Subject: [PATCH 059/183] Restrict actions throughout the App --- src/components/SettlementButton.tsx | 7 ++++++ src/libs/actions/IOU.ts | 11 ++++++++++ .../AttachmentPickerWithMenuItems.tsx | 22 ++++++++++++++----- .../MoneyRequestParticipantsSelector.tsx | 19 +++++++++++++++- 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx index 2b1cd0729c0b..da98b2faf329 100644 --- a/src/components/SettlementButton.tsx +++ b/src/components/SettlementButton.tsx @@ -4,8 +4,10 @@ import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx, withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; +import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import * as BankAccounts from '@userActions/BankAccounts'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; @@ -228,6 +230,11 @@ function SettlementButton({ }, [currency, formattedAmount, iouReport, policyID, translate, shouldHidePaymentOptions, shouldShowApproveButton, shouldDisableApproveButton]); const selectPaymentType = (event: KYCFlowEvent, iouPaymentType: PaymentMethodType, triggerKYCFlow: TriggerKYCFlow) => { + if (policy && SubscriptionUtils.shouldRestrictUserBillableActions(policy.id)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id)); + return; + } + if (iouPaymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY || iouPaymentType === CONST.IOU.PAYMENT_TYPE.VBBA) { triggerKYCFlow(event, iouPaymentType); BankAccounts.setPersonalBankAccountContinueKYCOnSuccess(ROUTES.ENABLE_PAYMENTS); diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index fd95947c5153..8bcfa58552ea 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -44,6 +44,7 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import type {OptimisticChatReport, OptimisticCreatedReportAction, OptimisticIOUReportAction, TransactionDetails} from '@libs/ReportUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import type {IOUAction, IOUType} from '@src/CONST'; @@ -6205,6 +6206,11 @@ function hasIOUToApproveOrPay(chatReport: OnyxEntry | EmptyObj } function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject, full?: boolean) { + if (expenseReport.policyID && SubscriptionUtils.shouldRestrictUserBillableActions(expenseReport.policyID)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(expenseReport.policyID)); + return; + } + const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`] ?? null; let total = expenseReport.total ?? 0; const hasHeldExpenses = ReportUtils.hasHeldExpenses(expenseReport.reportID); @@ -6568,6 +6574,11 @@ function cancelPayment(expenseReport: OnyxTypes.Report, chatReport: OnyxTypes.Re } function payMoneyRequest(paymentType: PaymentMethodType, chatReport: OnyxTypes.Report, iouReport: OnyxTypes.Report, full = true) { + if (chatReport.policyID && SubscriptionUtils.shouldRestrictUserBillableActions(chatReport.policyID)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(chatReport.policyID)); + return; + } + const recipient = {accountID: iouReport.ownerAccountID}; const {params, optimisticData, successData, failureData} = getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentType, full); diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index fd9a244f210b..d1e26681eeae 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -19,13 +19,16 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; import getIconForAction from '@libs/getIconForAction'; +import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; +import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import * as IOU from '@userActions/IOU'; import * as Report from '@userActions/Report'; import * as Task from '@userActions/Task'; import type {IOUType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; type MoneyRequestOptions = Record, PopoverMenuItem>; @@ -125,31 +128,40 @@ function AttachmentPickerWithMenuItems({ * Returns the list of IOU Options */ const moneyRequestOptions = useMemo(() => { + const selectOption = (onSelected: () => void) => { + if (policy && SubscriptionUtils.shouldRestrictUserBillableActions(policy.id)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id)); + return; + } + + onSelected(); + }; + const options: MoneyRequestOptions = { [CONST.IOU.TYPE.SPLIT]: { icon: Expensicons.Transfer, text: translate('iou.splitExpense'), - onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SPLIT, report?.reportID ?? '-1'), + onSelected: () => selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SPLIT, report?.reportID ?? '-1')), }, [CONST.IOU.TYPE.SUBMIT]: { icon: getIconForAction(CONST.IOU.TYPE.REQUEST), text: translate('iou.submitExpense'), - onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SUBMIT, report?.reportID ?? '-1'), + onSelected: () => selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SUBMIT, report?.reportID ?? '-1')), }, [CONST.IOU.TYPE.PAY]: { icon: getIconForAction(CONST.IOU.TYPE.SEND), text: translate('iou.paySomeone', {name: ReportUtils.getPayeeName(report)}), - onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.PAY, report?.reportID ?? '-1'), + onSelected: () => selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.PAY, report?.reportID ?? '-1')), }, [CONST.IOU.TYPE.TRACK]: { icon: getIconForAction(CONST.IOU.TYPE.TRACK), text: translate('iou.trackExpense'), - onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK, report?.reportID ?? '-1'), + onSelected: () => selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK, report?.reportID ?? '-1')), }, [CONST.IOU.TYPE.INVOICE]: { icon: Expensicons.InvoiceGeneric, text: translate('workspace.invoices.sendInvoice'), - onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.INVOICE, report?.reportID ?? '-1'), + onSelected: () => selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.INVOICE, report?.reportID ?? '-1')), }, }; diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 46c0d10e08ac..8a095b1a0d63 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -19,13 +19,16 @@ import usePermissions from '@hooks/usePermissions'; import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionStatus'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import * as Policy from '@userActions/Policy/Policy'; import * as Report from '@userActions/Report'; import type {IOUAction, IOURequestType, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type {Participant} from '@src/types/onyx/IOU'; type MoneyRequestParticipantsSelectorProps = { @@ -378,6 +381,20 @@ function MoneyRequestParticipantsSelector({participants = [], onFinish, onPartic onFinish, ]); + const onSelectRow = (option: Participant) => { + if (option.isPolicyExpenseChat && option.policyID && SubscriptionUtils.shouldRestrictUserBillableActions(option.policyID)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(option.policyID)); + return; + } + + if (isIOUSplit) { + addParticipantToSelection(option); + return; + } + + addSingleParticipant(option); + }; + return ( (isIOUSplit ? addParticipantToSelection(item) : addSingleParticipant(item))} + onSelectRow={onSelectRow} shouldDebounceRowSelect footerContent={footerContent} headerMessage={header} From 69f6c26aa4331f126cd86b6f6df0ff5b2d6f3199 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Tue, 25 Jun 2024 15:25:39 +0100 Subject: [PATCH 060/183] refactor: apply suggestions --- src/components/LHNOptionsList/OptionRowLHN.tsx | 2 +- src/languages/en.ts | 4 ++-- src/languages/es.ts | 4 ++-- src/pages/home/HeaderView.tsx | 2 +- src/pages/settings/InitialSettingsPage.tsx | 2 +- ...BillingBanner.tsx => TrialStartedBillingBanner.tsx} | 10 +++++----- .../settings/Subscription/CardSection/CardSection.tsx | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) rename src/pages/settings/Subscription/CardSection/BillingBanner/{TrialBillingBanner.tsx => TrialStartedBillingBanner.tsx} (59%) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 8a6e3b8cef2c..7703b804611a 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -232,7 +232,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti {ReportUtils.isChatUsedForOnboarding(report) && SubscriptionUtils.isUserOnFreeTrial() && ( )} diff --git a/src/languages/en.ts b/src/languages/en.ts index dc636ce3cbcc..2636472a2bc0 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3275,7 +3275,7 @@ export default { subscription: { mobileReducedFunctionalityMessage: 'You can’t make changes to your subscription in the mobile app.', badge: { - trial: ({numOfDays}) => `Free trial: ${numOfDays} ${numOfDays === 1 ? 'day' : 'days'} left`, + freeTrial: ({numOfDays}) => `Free trial: ${numOfDays} ${numOfDays === 1 ? 'day' : 'days'} left`, }, billingBanner: { preTrial: { @@ -3283,7 +3283,7 @@ export default { subtitle: 'To get started, ', subtitleLink: 'complete your setup checklist here', }, - trial: { + trialStarted: { title: ({numOfDays}) => `Free trial: ${numOfDays} ${numOfDays === 1 ? 'day' : 'days'} left!`, subtitle: 'Add a payment card below to continue using all of your favorite features.', }, diff --git a/src/languages/es.ts b/src/languages/es.ts index 5c2c606ad313..86392b9f448f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3779,7 +3779,7 @@ export default { subscription: { mobileReducedFunctionalityMessage: 'No puedes hacer cambios en tu suscripción en la aplicación móvil.', badge: { - trial: ({numOfDays}) => `Prueba gratuita: ${numOfDays === 1 ? `queda 1 día` : `quedan ${numOfDays} días`}`, + freeTrial: ({numOfDays}) => `Prueba gratuita: ${numOfDays === 1 ? `queda 1 día` : `quedan ${numOfDays} días`}`, }, billingBanner: { preTrial: { @@ -3787,7 +3787,7 @@ export default { subtitle: 'Para empezar, ', subtitleLink: 'completa la lista de configuración aquí', }, - trial: { + trialStarted: { title: ({numOfDays}) => `Prueba gratuita: ¡${numOfDays === 1 ? `queda 1 día` : `quedan ${numOfDays} días`}!`, subtitle: 'Añade una tarjeta de pago para seguir utilizando tus funciones favoritas.', }, diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx index 08bbfe303c70..a5520d9fbaf5 100644 --- a/src/pages/home/HeaderView.tsx +++ b/src/pages/home/HeaderView.tsx @@ -351,7 +351,7 @@ function HeaderView({ {ReportUtils.isChatUsedForOnboarding(report) && SubscriptionUtils.isUserOnFreeTrial() && ( )} {isTaskReport && !shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && } diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index 16f3e5d24cf3..88997ba8540b 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -212,7 +212,7 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa shouldShowRightIcon: true, iconRight: Expensicons.NewWindow, link: () => Link.buildOldDotURL(CONST.OLDDOT_URLS.ADMIN_POLICIES_URL), - badgeText: SubscriptionUtils.isUserOnFreeTrial() ? translate('subscription.badge.trial', {numOfDays: SubscriptionUtils.calculateRemainingFreeTrialDays()}) : '', + badgeText: SubscriptionUtils.isUserOnFreeTrial() ? translate('subscription.badge.freeTrial', {numOfDays: SubscriptionUtils.calculateRemainingFreeTrialDays()}) : '', }); } diff --git a/src/pages/settings/Subscription/CardSection/BillingBanner/TrialBillingBanner.tsx b/src/pages/settings/Subscription/CardSection/BillingBanner/TrialStartedBillingBanner.tsx similarity index 59% rename from src/pages/settings/Subscription/CardSection/BillingBanner/TrialBillingBanner.tsx rename to src/pages/settings/Subscription/CardSection/BillingBanner/TrialStartedBillingBanner.tsx index cc8079859f83..4b698722771c 100644 --- a/src/pages/settings/Subscription/CardSection/BillingBanner/TrialBillingBanner.tsx +++ b/src/pages/settings/Subscription/CardSection/BillingBanner/TrialStartedBillingBanner.tsx @@ -5,18 +5,18 @@ import useLocalize from '@hooks/useLocalize'; import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import BillingBanner from './BillingBanner'; -function TrialBillingBanner() { +function TrialStartedBillingBanner() { const {translate} = useLocalize(); return ( {translate('subscription.billingBanner.trial.subtitle')}} + title={translate('subscription.billingBanner.trialStarted.title', {numOfDays: SubscriptionUtils.calculateRemainingFreeTrialDays()})} + subtitle={{translate('subscription.billingBanner.trialStarted.subtitle')}} icon={Illustrations.TreasureChest} /> ); } -TrialBillingBanner.displayName = 'PreTrialBillingBanner'; +TrialStartedBillingBanner.displayName = 'TrialStartedBillingBanner'; -export default TrialBillingBanner; +export default TrialStartedBillingBanner; diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index b8a011a754d7..74ecc5ac25ac 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -13,7 +13,7 @@ import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import PreTrialBillingBanner from './BillingBanner/PreTrialBillingBanner'; -import TrialBillingBanner from './BillingBanner/TrialBillingBanner'; +import TrialStartedBillingBanner from './BillingBanner/TrialStartedBillingBanner'; import CardSectionActions from './CardSectionActions'; import CardSectionDataEmpty from './CardSectionDataEmpty'; import CardSectionUtils from './utils'; @@ -32,7 +32,7 @@ function CardSection() { const nextPaymentDate = !isEmptyObject(privateSubscription) ? CardSectionUtils.getNextBillingDate() : undefined; const sectionSubtitle = defaultCard && !!nextPaymentDate ? translate('subscription.cardSection.cardNextPayment', {nextPaymentDate}) : translate('subscription.cardSection.subtitle'); - const BillingBanner = SubscriptionUtils.isUserOnFreeTrial() ? : ; + const BillingBanner = SubscriptionUtils.isUserOnFreeTrial() ? : ; return (
Date: Tue, 25 Jun 2024 15:31:16 +0100 Subject: [PATCH 061/183] refactor: apply suggestion from https://github.com/Expensify/App/pull/43855#discussion_r1651583350 --- src/libs/PolicyUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index e8e640ba49eb..14887fb90513 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -154,7 +154,7 @@ const isPolicyAdmin = (policy: OnyxInputOrEntry | EmptyObject, currentUs (policy?.role ?? (currentUserLogin && policy?.employeeList?.[currentUserLogin]?.role)) === CONST.POLICY.ROLE.ADMIN; /** - * Checks if the current user is an user of the policy. + * Checks if the current user is of the role "user" on the policy. */ const isPolicyUser = (policy: OnyxInputOrEntry | EmptyObject, currentUserLogin?: string): boolean => (policy?.role ?? (currentUserLogin && policy?.employeeList?.[currentUserLogin]?.role)) === CONST.POLICY.ROLE.USER; From 1110e86197b2404ae9886836a2e9585869865975 Mon Sep 17 00:00:00 2001 From: Rayane Djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Wed, 26 Jun 2024 01:42:00 +0100 Subject: [PATCH 062/183] make the condition consistent --- src/pages/workspace/categories/WorkspaceCategoriesPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index ce978e38a1f3..5f018dc47343 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -309,7 +309,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { danger /> {shouldUseNarrowLayout && {getHeaderButtons()}} - {(!shouldUseNarrowLayout || (!hasVisibleCategories && !isLoading) || isLoading) && getHeaderText()} + {(!shouldUseNarrowLayout || categoryList.length === 0 || isLoading) && getHeaderText()} {isLoading && ( Date: Wed, 26 Jun 2024 10:08:19 +0530 Subject: [PATCH 063/183] Fix CONST after merge --- src/CONST.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 4e84ac3e8fc8..7d766ceebe30 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1334,10 +1334,6 @@ const CONST = { }, }, - NETSUITE_CONFIG: { - SUBSIDIARY: 'subsidiary', - }, - QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE: { VENDOR_BILL: 'bill', CHECK: 'check', @@ -1351,6 +1347,7 @@ const CONST = { }, NETSUITE_CONFIG: { + SUBSIDIARY: 'subsidiary', EXPORTER: 'exporter', EXPORT_DATE: 'exportDate', REIMBURSABLE_EXPENSES_EXPORT_DESTINATION: 'reimbursableExpensesExportDestination', From 8ffcb20199d6e057c27122006540aeab42b1e5a0 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 26 Jun 2024 10:11:05 +0530 Subject: [PATCH 064/183] Connecting export page with policy page --- src/pages/workspace/accounting/PolicyAccountingPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index 3bb63dadaadc..b1634cdad07b 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -113,7 +113,7 @@ function accountingIntegrationData( /> ), onImportPagePress: () => {}, - onExportPagePress: () => {}, + onExportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID)), onAdvancedPagePress: () => {}, }; default: From 17601b54dbe101380d73aee5e5a124197dba45e6 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 26 Jun 2024 10:12:30 +0530 Subject: [PATCH 065/183] Fix bad export --- src/libs/actions/Policy/Policy.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 44b5b870aa41..22b010c90f15 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -3007,7 +3007,6 @@ export { createDraftWorkspace, buildPolicyData, createPolicyExpenseChats, - clearNetSuiteErrorField, }; export type {NewCustomUnit}; From 3a1c32904963e56a0f16e1c3abaa79f3a34ef503 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 25 Jun 2024 23:25:39 -0700 Subject: [PATCH 066/183] Bump react-native-screens --- package-lock.json | 8 ++++---- package.json | 2 +- ...t-native-screens+3.30.1+001+fix-screen-type.patch | 12 ------------ 3 files changed, 5 insertions(+), 17 deletions(-) delete mode 100644 patches/react-native-screens+3.30.1+001+fix-screen-type.patch diff --git a/package-lock.json b/package-lock.json index fb15d51d1389..ae060c611450 100644 --- a/package-lock.json +++ b/package-lock.json @@ -115,7 +115,7 @@ "react-native-release-profiler": "^0.1.6", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.8.2", - "react-native-screens": "3.30.1", + "react-native-screens": "3.32.0", "react-native-share": "^10.0.2", "react-native-sound": "^0.11.2", "react-native-svg": "14.1.0", @@ -37355,9 +37355,9 @@ } }, "node_modules/react-native-screens": { - "version": "3.30.1", - "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.30.1.tgz", - "integrity": "sha512-/muEvjocCtFb+j5J3YmLvB25+f4rIU8hnnxgGTkXcAf2omPBY8uhPjJaaFUlvj64VEoEzJcRpugbXWsjfPPIFg==", + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.32.0.tgz", + "integrity": "sha512-wybqZAHX7v8ipOXhh90CqGLkBHw5JYqKNRBX7R/b0c2WQisTOgu0M0yGwBMM6LyXRBT+4k3NTGHdDbpJVpq0yQ==", "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" diff --git a/package.json b/package.json index d4be691e2fc2..de7e891a6d19 100644 --- a/package.json +++ b/package.json @@ -168,7 +168,7 @@ "react-native-release-profiler": "^0.1.6", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.8.2", - "react-native-screens": "3.30.1", + "react-native-screens": "3.32.0", "react-native-share": "^10.0.2", "react-native-sound": "^0.11.2", "react-native-svg": "14.1.0", diff --git a/patches/react-native-screens+3.30.1+001+fix-screen-type.patch b/patches/react-native-screens+3.30.1+001+fix-screen-type.patch deleted file mode 100644 index f282ec58b07b..000000000000 --- a/patches/react-native-screens+3.30.1+001+fix-screen-type.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/node_modules/react-native-screens/src/components/Screen.tsx b/node_modules/react-native-screens/src/components/Screen.tsx -index 3f9a1cb..45767f7 100644 ---- a/node_modules/react-native-screens/src/components/Screen.tsx -+++ b/node_modules/react-native-screens/src/components/Screen.tsx -@@ -79,6 +79,7 @@ export class InnerScreen extends React.Component { - // Due to how Yoga resolves layout, we need to have different components for modal nad non-modal screens - const AnimatedScreen = - Platform.OS === 'android' || -+ stackPresentation === undefined || - stackPresentation === 'push' || - stackPresentation === 'containedModal' || - stackPresentation === 'containedTransparentModal' From 9574937a4606c4358ebd513c595f9bfef12b834d Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 26 Jun 2024 12:03:13 +0530 Subject: [PATCH 067/183] Applying suggestions --- src/libs/PolicyUtils.ts | 4 ++-- .../netsuite/export/NetSuiteExportConfigurationPage.tsx | 4 ++-- .../export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index a92934e1fd79..66c29c4d204d 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -526,7 +526,7 @@ function canUseTaxNetSuite(canUseNetSuiteUSATax?: boolean, subsidiaryCountry?: s return !!canUseNetSuiteUSATax || CONST.NETSUITE_TAX_COUNTRIES.includes(subsidiaryCountry ?? ''); } -function canUseProvTaxNetSuite(subsidiaryCountry?: string) { +function canUseProvincialTaxNetSuite(subsidiaryCountry?: string) { return subsidiaryCountry === '_canada'; } @@ -618,7 +618,7 @@ export { getXeroBankAccountsWithDefaultSelect, getNetSuiteVendorOptions, canUseTaxNetSuite, - canUseProvTaxNetSuite, + canUseProvincialTaxNetSuite, getNetSuitePayableAccountOptions, getNetSuiteReceivableAccountOptions, getNetSuiteInvoiceItemOptions, diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx index 95f9a58216e2..fd5af04ee4b0 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx @@ -11,7 +11,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections/NetSuiteCommands'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {canUseProvTaxNetSuite, canUseTaxNetSuite} from '@libs/PolicyUtils'; +import {canUseProvincialTaxNetSuite, canUseTaxNetSuite} from '@libs/PolicyUtils'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {ToggleSettingOptionRowProps} from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -160,7 +160,7 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) { pendingAction: config?.pendingFields?.provincialTaxPostingAccount, errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.PROVINCIAL_TAX_POSTING_ACCOUNT), onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.PROVINCIAL_TAX_POSTING_ACCOUNT), - shouldHide: !!config?.suiteTaxEnabled || !config?.syncOptions.syncTax || !canUseProvTaxNetSuite(selectedSubsidiary?.country), + shouldHide: !!config?.suiteTaxEnabled || !config?.syncOptions.syncTax || !canUseProvincialTaxNetSuite(selectedSubsidiary?.country), }, { type: 'menuitem', diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx index d2d1134babad..08a1d513543e 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx @@ -8,7 +8,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections/NetSuiteCommands'; import Navigation from '@libs/Navigation/Navigation'; -import {canUseProvTaxNetSuite, getNetSuiteTaxAccountOptions} from '@libs/PolicyUtils'; +import {canUseProvincialTaxNetSuite, getNetSuiteTaxAccountOptions} from '@libs/PolicyUtils'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import variables from '@styles/variables'; @@ -73,7 +73,7 @@ function NetSuiteProvincialTaxPostingAccountSelectPage({policy}: WithPolicyConne title="workspace.netsuite.journalEntriesProvTaxPostingAccount" listEmptyContent={listEmptyContent} connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} - shouldBeBlocked={!!config?.suiteTaxEnabled || !config?.syncOptions.syncTax || !canUseProvTaxNetSuite(selectedSubsidiary?.country)} + shouldBeBlocked={!!config?.suiteTaxEnabled || !config?.syncOptions.syncTax || !canUseProvincialTaxNetSuite(selectedSubsidiary?.country)} /> ); } From 0c34d78c59e1f4b3c51eae769c728c1af016ede3 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 26 Jun 2024 12:20:56 +0530 Subject: [PATCH 068/183] Applying translations --- src/languages/es.ts | 76 ++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 0428f7441b80..a6307b1e2d72 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2210,32 +2210,32 @@ export default { subsidiary: 'Subsidiaria', subsidiarySelectDescription: 'Elige la subsidiaria de NetSuite de la que deseas importar datos.', exportDescription: 'Configura cómo se exportan los datos de Expensify a NetSuite.', - exportReimbursable: 'Export reimbursable expenses as', - exportNonReimbursable: 'Export non-reimbursable expenses as', + exportReimbursable: 'Exportar gastos reembolsables como', + exportNonReimbursable: 'Exportar gastos no reembolsables como', exportInvoices: 'Exportar facturas a', - journalEntriesTaxPostingAccount: 'Journal entries tax posting account', - journalEntriesProvTaxPostingAccount: 'Journal entries provincial tax posting account', - foreignCurrencyAmount: 'Export foreign currency amount', - exportToNextOpenPeriod: 'Export to next open period', - nonReimbursableJournalPostingAccount: 'Non-reimbursable journal posting account', - reimbursableJournalPostingAccount: 'Reimbursable journal posting account', + journalEntriesTaxPostingAccount: 'Cuenta de registro de impuestos de asientos contables', + journalEntriesProvTaxPostingAccount: 'Cuenta de registro de impuestos provinciales de asientos contables', + foreignCurrencyAmount: 'Exportar importe en moneda extranjera', + exportToNextOpenPeriod: 'Exportar al siguiente período abierto', + nonReimbursableJournalPostingAccount: 'Cuenta de registro de diario no reembolsable', + reimbursableJournalPostingAccount: 'Cuenta de registro de diario reembolsable', journalPostingPreference: { - label: 'Journal entries posting preference', + label: 'Preferencia de registro de asientos contables', values: { - [CONST.NETSUITE_JOURNAL_POSTING_PREFERENCE.JOURNALS_POSTING_INDIVIDUAL_LINE]: 'Single, itemized entry for each report', - [CONST.NETSUITE_JOURNAL_POSTING_PREFERENCE.JOURNALS_POSTING_TOTAL_LINE]: 'Single entry for each individual expense', + [CONST.NETSUITE_JOURNAL_POSTING_PREFERENCE.JOURNALS_POSTING_INDIVIDUAL_LINE]: 'Entrada única y detallada para cada informe', + [CONST.NETSUITE_JOURNAL_POSTING_PREFERENCE.JOURNALS_POSTING_TOTAL_LINE]: 'Entrada única para cada gasto individual', }, }, invoiceItem: { - label: 'Invoice item', + label: 'Artículo de la factura', values: { [CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.CREATE]: { - label: 'Create one for me', - description: 'We\'ll create an "Expensify invoice line item" for you upon export (if one doesn’t exist already).', + label: 'Crear uno para mí', + description: "Crearemos un 'Artículo de línea de factura de Expensify' para ti al exportar (si aún no existe).", }, [CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.SELECT]: { - label: 'Select existing', - description: "We'll tie invoices from Expensify to the item selected below.", + label: 'Seleccionar existente', + description: "Asociaremos las facturas de Expensify al artículo seleccionado a continuación.", }, }, }, @@ -2245,55 +2245,55 @@ export default { values: { [CONST.NETSUITE_EXPORT_DATE.LAST_EXPENSE]: { label: 'Fecha del último gasto', - description: 'Fecha del gasto mas reciente en el informe', + description: 'Fecha del gasto mas reciente en el informe.', }, [CONST.NETSUITE_EXPORT_DATE.EXPORTED]: { label: 'Fecha de exportación', - description: 'Fecha de exportación del informe a NetSuite', + description: 'Fecha de exportación del informe a NetSuite.', }, [CONST.NETSUITE_EXPORT_DATE.SUBMITTED]: { label: 'Fecha de envío', - description: 'Fecha en la que el informe se envió para su aprobación', + description: 'Fecha en la que el informe se envió para su aprobación.', }, }, }, exportDestination: { values: { [CONST.NETSUITE_EXPORT_DESTINATION.EXPENSE_REPORT]: { - label: 'Expense reports', - reimbursableDescription: 'Reimbursable expenses will export as expense reports to NetSuite.', - nonReimbursableDescription: 'Non-reimbursable expenses will export as expense reports to NetSuite.', + label: 'Informes de gastos', + reimbursableDescription: 'Los gastos reembolsables se exportarán como informes de gastos a NetSuite.', + nonReimbursableDescription: 'Los gastos no reembolsables se exportarán como informes de gastos a NetSuite.', }, [CONST.NETSUITE_EXPORT_DESTINATION.VENDOR_BILL]: { - label: 'Vendor bills', + label: 'Facturas de proveedores', reimbursableDescription: - 'Reimbursable expenses will export as bills payable to the NetSuite vendor specified below.\n' + + 'Los gastos reembolsables se exportarán como facturas pagaderas al proveedor especificado en NetSuite.\n' + '\n' + - 'If you’d like to set a specific vendor for each card, go to *Settings > Domains > Company Cards*.', + 'Si deseas establecer un proveedor específico para cada tarjeta, ve a *Configuraciones > Dominios > Tarjetas de Empresa*.', nonReimbursableDescription: - 'Non-reimbursable expenses will export as bills payable to the NetSuite vendor specified below.\n' + + 'Los gastos no reembolsables se exportarán como facturas pagaderas al proveedor especificado en NetSuite.\n' + '\n' + - 'If you’d like to set a specific vendor for each card, go to *Settings > Domains > Company Cards*.', + 'Si deseas establecer un proveedor específico para cada tarjeta, ve a *Configuraciones > Dominios > Tarjetas de Empresa*.', }, [CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY]: { - label: 'Journal Entries', + label: 'Asientos contables', reimbursableDescription: - 'Reimbursable expenses will export as journal entries to the NetSuite account specified below.\n' + + 'Los gastos reembolsables se exportarán como asientos contables a la cuenta especificada en NetSuite.\n' + '\n' + - 'If you’d like to set a specific vendor for each card, go to *Settings > Domains > Company Cards*.', + 'Si deseas establecer un proveedor específico para cada tarjeta, ve a *Configuraciones > Dominios > Tarjetas de Empresa*.', nonReimbursableDescription: - 'Non-reimbursable expenses will export as journal entries to the NetSuite account specified below.\n' + + 'Los gastos no reembolsables se exportarán como asientos contables a la cuenta especificada en NetSuite.\n' + '\n' + - 'If you’d like to set a specific vendor for each card, go to *Settings > Domains > Company Cards*.', + 'Si deseas establecer un proveedor específico para cada tarjeta, ve a *Configuraciones > Dominios > Tarjetas de Empresa*.', }, }, }, - noAccountsFound: 'No se ha encontrado ninguna cuenta', - noAccountsFoundDescription: 'Añade la cuenta en NetSuite y sincroniza de nuevo la conexión.', - noVendorsFound: 'No vendors found', - noVendorsFoundDescription: 'Add vendors in NetSuite and sync the connection again.', - noItemsFound: 'No invoice items found', - noItemsFoundDescription: 'Add invoice items in NetSuite and sync the connection again.', + noAccountsFound: 'No se han encontrado cuentas', + noAccountsFoundDescription: 'Añade la cuenta en NetSuite y sincroniza la conexión de nuevo.', + noVendorsFound: 'No se han encontrado proveedores', + noVendorsFoundDescription: 'Añade proveedores en NetSuite y sincroniza la conexión de nuevo.', + noItemsFound: 'No se han encontrado artículos de factura', + noItemsFoundDescription: 'Añade artículos de factura en NetSuite y sincroniza la conexión de nuevo.', noSubsidiariesFound: 'No se ha encontrado subsidiarias', noSubsidiariesFoundDescription: 'Añade la subsidiaria en NetSuite y sincroniza de nuevo la conexión.', }, From b69f74a9aac0f46d978e06b0e000522bcb0ab234 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 26 Jun 2024 12:28:06 +0530 Subject: [PATCH 069/183] Simplifying style --- .../HTMLEngineProvider/BaseHTMLEngineProvider.tsx | 4 ++-- src/styles/index.ts | 8 +------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx index 403c1caea0eb..bab66dfab911 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx @@ -44,7 +44,7 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim }), 'muted-text-label': HTMLElementModel.fromCustomModel({ tagName: 'muted-text-label', - mixedUAStyles: {...styles.colorMuted, ...styles.mb0, ...styles.labelNormal}, + mixedUAStyles: {...styles.mutedNormalTextLabel, ...styles.mb0}, contentModel: HTMLContentModel.block, }), comment: HTMLElementModel.fromCustomModel({ @@ -88,7 +88,7 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim contentModel: HTMLContentModel.block, }), }), - [styles.formError, styles.mb0, styles.colorMuted, styles.textLabelSupporting, styles.lh16, styles.textSupporting, styles.textLineThrough, styles.mt4, styles.labelNormal], + [styles.formError, styles.mb0, styles.colorMuted, styles.textLabelSupporting, styles.lh16, styles.textSupporting, styles.textLineThrough, styles.mt4, styles.mutedNormalTextLabel], ); /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/src/styles/index.ts b/src/styles/index.ts index 4cf9af4e5125..98e5776b4508 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -369,11 +369,6 @@ const styles = (theme: ThemeColors) => lineHeight: variables.lineHeightLarge, }, - labelNormal: { - fontSize: variables.fontSizeLabel, - lineHeight: variables.lineHeightNormal, - }, - textLabel: { color: theme.text, fontSize: variables.fontSizeLabel, @@ -2921,8 +2916,7 @@ const styles = (theme: ThemeColors) => height: 1, backgroundColor: theme.border, flexGrow: 1, - marginLeft: 20, - marginRight: 20, + marginHorizontal: 20, marginVertical: 12, }, From 4fb315499e4a494b4120f73fb71079c1cec33faa Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 26 Jun 2024 12:30:34 +0530 Subject: [PATCH 070/183] Simplifying style --- src/components/MenuItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index c42e6a41e4ac..30ee31d30514 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -791,7 +791,7 @@ function MenuItem( {!!helperText && (shouldParseHelperText ? ( - + ) : ( From 3666889880a33eedab50f721ab57b155dd49805a Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 26 Jun 2024 12:38:26 +0530 Subject: [PATCH 071/183] Realignment of options --- src/CONST.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 7d766ceebe30..36cd678e07fd 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1366,9 +1366,9 @@ const CONST = { }, NETSUITE_EXPORT_DATE: { - SUBMITTED: 'SUBMITTED', - EXPORTED: 'EXPORTED', LAST_EXPENSE: 'LAST_EXPENSE', + EXPORTED: 'EXPORTED', + SUBMITTED: 'SUBMITTED', }, NETSUITE_EXPORT_DESTINATION: { @@ -1383,8 +1383,8 @@ const CONST = { }, NETSUITE_JOURNAL_POSTING_PREFERENCE: { - JOURNALS_POSTING_TOTAL_LINE: 'JOURNALS_POSTING_TOTAL_LINE', JOURNALS_POSTING_INDIVIDUAL_LINE: 'JOURNALS_POSTING_INDIVIDUAL_LINE', + JOURNALS_POSTING_TOTAL_LINE: 'JOURNALS_POSTING_TOTAL_LINE', }, NETSUITE_EXPENSE_TYPE: { From 7f4d613a21e558957ed0c82e3ea69e85039de3a9 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 26 Jun 2024 12:57:02 +0530 Subject: [PATCH 072/183] Fixing customFormIDOptions type --- src/types/onyx/Policy.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index a2aaa294120a..2a6f89c56977 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1,4 +1,4 @@ -import type {RequireExactlyOne, ValueOf} from 'type-fest'; +import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; import type {Country} from '@src/CONST'; import type * as OnyxTypes from '.'; @@ -709,13 +709,13 @@ type NetSuiteJournalPostingPreferences = 'JOURNALS_POSTING_TOTAL_LINE' | 'JOURNA /** The custom form selection options for transactions (any one will be used at most) */ type NetSuiteCustomFormIDOptions = { /** If the option is expense report */ - expenseReport: string; + expenseReport?: string; /** If the option is vendor bill */ - vendorBill: string; + vendorBill?: string; /** If the option is journal entry */ - journalEntry: string; + journalEntry?: string; }; /** User configuration for the NetSuite accounting integration. */ @@ -882,10 +882,10 @@ type NetSuiteConnectionConfig = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Configurations for customer to set custom forms for which reimbursable and non-reimbursable transactions will export to in NetSuite */ customFormIDOptions?: { /** The custom form selections for reimbursable transactions */ - reimbursable: RequireExactlyOne; + reimbursable: NetSuiteCustomFormIDOptions; /** The custom form selections for non-reimbursable transactions */ - nonReimbursable: RequireExactlyOne; + nonReimbursable: NetSuiteCustomFormIDOptions; /** Whether we'll use the custom form selections upon export to NetSuite */ enabled: boolean; From 08f08d0c39dd56627f14d96fb88c4921a37f318e Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 26 Jun 2024 13:40:15 +0530 Subject: [PATCH 073/183] Fix lint --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index a6307b1e2d72..00930b767aa1 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2235,7 +2235,7 @@ export default { }, [CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.SELECT]: { label: 'Seleccionar existente', - description: "Asociaremos las facturas de Expensify al artículo seleccionado a continuación.", + description: 'Asociaremos las facturas de Expensify al artículo seleccionado a continuación.', }, }, }, From a204781d44b1257e303935d66fa41cf4df1f517d Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 26 Jun 2024 10:38:09 +0200 Subject: [PATCH 074/183] fix: resolve comments --- src/hooks/useIsEligibleForRefund.ts | 13 ++++----- .../API/parameters/RequestRefundParams.ts | 3 -- src/libs/API/parameters/index.ts | 1 - src/libs/API/types.ts | 2 +- src/libs/actions/User.ts | 2 +- src/pages/settings/InitialSettingsPage.tsx | 9 ++++++ .../Subscription/CardSection/CardSection.tsx | 28 ++++++++----------- 7 files changed, 28 insertions(+), 30 deletions(-) delete mode 100644 src/libs/API/parameters/RequestRefundParams.ts diff --git a/src/hooks/useIsEligibleForRefund.ts b/src/hooks/useIsEligibleForRefund.ts index f07865592798..7262d92bf6d8 100644 --- a/src/hooks/useIsEligibleForRefund.ts +++ b/src/hooks/useIsEligibleForRefund.ts @@ -1,17 +1,14 @@ -import {useMemo} from 'react'; import {useOnyx} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; -function useIsEligibleForRefund() { +function useIsEligibleForRefund(): boolean | undefined { const [account] = useOnyx(ONYXKEYS.ACCOUNT); - return useMemo(() => { - if (!account) { - return false; - } + if (!account) { + return false; + } - return account.isEligibleForRefund; - }, [account]); + return account.isEligibleForRefund; } export default useIsEligibleForRefund; diff --git a/src/libs/API/parameters/RequestRefundParams.ts b/src/libs/API/parameters/RequestRefundParams.ts deleted file mode 100644 index 697bc7a1263e..000000000000 --- a/src/libs/API/parameters/RequestRefundParams.ts +++ /dev/null @@ -1,3 +0,0 @@ -type RequestRefundParams = Record; - -export default RequestRefundParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index f68abb1fd711..c43ab514b251 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -233,5 +233,4 @@ export type {default as UpdateSubscriptionAutoRenewParams} from './UpdateSubscri export type {default as UpdateSubscriptionAddNewUsersAutomaticallyParams} from './UpdateSubscriptionAddNewUsersAutomaticallyParams'; export type {default as GenerateSpotnanaTokenParams} from './GenerateSpotnanaTokenParams'; export type {default as UpdateSubscriptionSizeParams} from './UpdateSubscriptionSizeParams'; -export type {default as RequestRefundParams} from './RequestRefundParams'; export type {default as UpdateNetSuiteSubsidiaryParams} from './UpdateNetSuiteSubsidiaryParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 41fb2b1ce9de..f16971735594 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -457,7 +457,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_AUTO_RENEW]: Parameters.UpdateSubscriptionAutoRenewParams; [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_ADD_NEW_USERS_AUTOMATICALLY]: Parameters.UpdateSubscriptionAddNewUsersAutomaticallyParams; [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_SIZE]: Parameters.UpdateSubscriptionSizeParams; - [WRITE_COMMANDS.REQUEST_REFUND]: Parameters.RequestRefundParams; + [WRITE_COMMANDS.REQUEST_REFUND]: null; // Netsuite parameters [WRITE_COMMANDS.UPDATE_NETSUITE_SUBSIDIARY]: Parameters.UpdateNetSuiteSubsidiaryParams; diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index b0b74bddf407..0f1960cd1c42 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -1024,7 +1024,7 @@ function dismissTrackTrainingModal() { } function requestRefund() { - API.write(WRITE_COMMANDS.REQUEST_REFUND, {}); + API.write(WRITE_COMMANDS.REQUEST_REFUND, null); } export { diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index 58d179033736..79e401758359 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -5,6 +5,7 @@ import type {GestureResponderEvent, ScrollView as RNScrollView, ScrollViewProps, import {View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; import ConfirmModal from '@components/ConfirmModal'; @@ -107,6 +108,14 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa const [shouldShowSignoutConfirmModal, setShouldShowSignoutConfirmModal] = useState(false); + useEffect(() => { + Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION); + }, []); + + useEffect(() => { + Onyx.merge(ONYXKEYS.ACCOUNT, {isEligibleForRefund: true, hasPurchases: true}); + }, []); + useEffect(() => { Wallet.openInitialSettingsPage(); }, []); diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 6c3ae1fa525d..3862f2eaecd8 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -30,7 +30,7 @@ function CardSection() { const styles = useThemeStyles(); const theme = useTheme(); const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); - const plan = useSubscriptionPlan(); + const subscriptionPlan = useSubscriptionPlan(); const isEligibleForRefund = useIsEligibleForRefund(); const [account] = useOnyx(ONYXKEYS.ACCOUNT); const [privateSubscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION); @@ -82,30 +82,28 @@ function CardSection() { )} + {isEmptyObject(defaultCard?.accountData) && } - {!isEmptyObject(defaultCard?.accountData && plan && isEligibleForRefund) && ( + {!!account?.hasPurchases && ( setIsRequestRefundModalVisible(true)} + onPress={() => Navigation.navigate(ROUTES.SEARCH.getRoute(CONST.SEARCH.TAB.ALL))} + hoverAndPressStyle={styles.hoveredComponentBG} /> )} - {!!account?.hasPurchases && ( + {!!(subscriptionPlan && isEligibleForRefund) && ( Navigation.navigate(ROUTES.SEARCH.getRoute(CONST.SEARCH.TAB.ALL))} - hoverAndPressStyle={styles.hoveredComponentBG} + onPress={() => setIsRequestRefundModalVisible(true)} /> )}
@@ -115,9 +113,7 @@ function CardSection() { title={translate('subscription.cardSection.requestRefund')} isVisible={isRequestRefundModalVisible} onConfirm={requestRefund} - onCancel={() => { - setIsRequestRefundModalVisible(false); - }} + onCancel={() => setIsRequestRefundModalVisible(false)} prompt={ <> {translate('subscription.cardSection.requestRefundModal.phrase1')} From c32b43b3f9dd305215b5bb2e8215c70eda8cdb9e Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 26 Jun 2024 11:28:48 +0200 Subject: [PATCH 075/183] fix: remove unnecesarry code --- src/pages/settings/InitialSettingsPage.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index 79e401758359..58d179033736 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -5,7 +5,6 @@ import type {GestureResponderEvent, ScrollView as RNScrollView, ScrollViewProps, import {View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; import ConfirmModal from '@components/ConfirmModal'; @@ -108,14 +107,6 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa const [shouldShowSignoutConfirmModal, setShouldShowSignoutConfirmModal] = useState(false); - useEffect(() => { - Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION); - }, []); - - useEffect(() => { - Onyx.merge(ONYXKEYS.ACCOUNT, {isEligibleForRefund: true, hasPurchases: true}); - }, []); - useEffect(() => { Wallet.openInitialSettingsPage(); }, []); From e7f3c92feafed7af9168504a92bc0491a627a358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 26 Jun 2024 11:12:46 +0100 Subject: [PATCH 076/183] Fix billing banner pre-trial subtitle link text --- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 2a5b32be5038..fbbb17bdf3b9 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3286,7 +3286,7 @@ export default { preTrial: { title: 'Start a free trial', subtitle: 'To get started, ', - subtitleLink: 'complete your setup checklist here', + subtitleLink: 'complete your setup checklist here.', }, }, cardSection: { diff --git a/src/languages/es.ts b/src/languages/es.ts index da228096eaf1..165f5f82ad82 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3790,7 +3790,7 @@ export default { preTrial: { title: 'Iniciar una prueba gratuita', subtitle: 'Para empezar, ', - subtitleLink: 'completa la lista de configuración aquí', + subtitleLink: 'completa la lista de configuración aquí.', }, }, cardSection: { From f453e7468b9af9177d9def90cdee1e5168b81781 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 26 Jun 2024 12:51:16 +0200 Subject: [PATCH 077/183] add unit tests for getSubscriptionStatus method --- src/libs/SubscriptionUtils.ts | 30 +++--- tests/unit/SubscriptionUtilsTest.ts | 143 ++++++++++++++++++++++++---- 2 files changed, 138 insertions(+), 35 deletions(-) diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index d07640fa7b72..9e244eaf06f8 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -5,6 +5,7 @@ import CONST from '@src/CONST'; import type {OnyxValues} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; import type {BillingGraceEndPeriod, BillingStatus, Fund, FundList, Policy, StripeCustomerID} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; const PAYMENT_STATUS = { POLICY_OWNER_WITH_AMOUNT_OWED: 'policy_owner_with_amount_owed', @@ -42,25 +43,13 @@ Onyx.connect({ let billingDisputePending: OnyxEntry; Onyx.connect({ key: ONYXKEYS.NVP_PRIVATE_BILLING_DISPUTE_PENDING, - callback: (value) => { - if (!value) { - return; - } - - billingDisputePending = value; - }, + callback: (value) => (billingDisputePending = value), }); let billingStatus: OnyxEntry; Onyx.connect({ key: ONYXKEYS.NVP_PRIVATE_BILLING_STATUS, - callback: (value) => { - if (!value) { - return; - } - - billingStatus = value; - }, + callback: (value) => (billingStatus = value), }); let ownerBillingGraceEndPeriod: OnyxEntry; @@ -153,6 +142,10 @@ function getAmountOwed(): number { return amountOwed ?? 0; } +function hasAmountOwed(): boolean { + return !!amountOwed; +} + /** * @returns Whether there is a card authentication error. */ @@ -192,7 +185,7 @@ function getCardForSubscriptionBilling(): Fund | undefined { * @returns Whether the card is due to expire soon. */ function hasCardExpiringSoon(): boolean { - if (billingStatus) { + if (!isEmptyObject(billingStatus ?? {})) { return false; } @@ -229,7 +222,7 @@ type SubscriptionStatus = { */ function getSubscriptionStatus(): SubscriptionStatus | undefined { if (hasOverdueGracePeriod()) { - if (amountOwed) { + if (hasAmountOwed()) { // 1. Policy owner with amount owed, within grace period if (hasGracePeriodOverdue() === false) { return { @@ -246,21 +239,20 @@ function getSubscriptionStatus(): SubscriptionStatus | undefined { } } else { // 3. Owner of policy under invoicing, within grace period - if (hasGracePeriodOverdue() && !amountOwed) { + if (hasGracePeriodOverdue()) { return { status: PAYMENT_STATUS.OWNER_OF_POLICY_UNDER_INVOICING, }; } // 4. Owner of policy under invoicing, overdue (past grace period) - if (hasGracePeriodOverdue() === false && amountOwed) { + if (hasGracePeriodOverdue() === false) { return { status: PAYMENT_STATUS.OWNER_OF_POLICY_UNDER_INVOICING_OVERDUE, }; } } } - // 5. Billing disputed by cardholder if (hasBillingDisputePending()) { return { diff --git a/tests/unit/SubscriptionUtilsTest.ts b/tests/unit/SubscriptionUtilsTest.ts index 3d309a84547d..8303d8918e26 100644 --- a/tests/unit/SubscriptionUtilsTest.ts +++ b/tests/unit/SubscriptionUtilsTest.ts @@ -4,7 +4,7 @@ import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import {PAYMENT_STATUS} from '@libs/SubscriptionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {BillingGraceEndPeriod} from '@src/types/onyx'; +import type {BillingGraceEndPeriod, BillingStatus, FundList, StripeCustomerID} from '@src/types/onyx'; import createRandomPolicy from '../utils/collections/policies'; const billingGraceEndPeriod: BillingGraceEndPeriod = { @@ -15,6 +15,31 @@ const billingGraceEndPeriod: BillingGraceEndPeriod = { const GRACE_PERIOD_DATE = 1750819200100; const AMOUNT_OWED = 100; +const STRIPE_CUSTOMER_ID: StripeCustomerID = { + paymentMethodID: '1', + intentsID: '2', + currency: 'USD', + status: 'authentication_required', +}; +const BILLING_STATUS_INSUFFICIENT_FUNDS: BillingStatus = { + action: 'action', + periodMonth: 'periodMonth', + periodYear: 'periodYear', + declineReason: 'insufficient_funds', +}; +const BILLING_STATUS_EXPIRED_CARD: BillingStatus = { + ...BILLING_STATUS_INSUFFICIENT_FUNDS, + declineReason: 'expired_card', +}; +const FUND_LIST: FundList = { + defaultCard: { + isDefault: true, + accountData: { + cardYear: new Date().getFullYear(), + cardMonth: new Date().getMonth() + 1, + }, + }, +}; Onyx.init({keys: ONYXKEYS}); @@ -251,21 +276,8 @@ describe('SubscriptionUtils', () => { }); describe('getSubscriptionStatus', () => { - beforeAll(() => { - Onyx.init({ - keys: { - NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END: ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END, - NVP_PRIVATE_AMOUNT_OWED: ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED, - }, - initialKeyStates: { - [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: undefined, - [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: undefined, - }, - }); - }); - afterEach(() => { - Onyx.clear(); + // Onyx.clear(); }); it('should return undefined by default', () => { @@ -287,12 +299,111 @@ describe('SubscriptionUtils', () => { it('should return POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE status', async () => { await Onyx.multiSet({ [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: GRACE_PERIOD_DATE, - [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: AMOUNT_OWED, }); expect(SubscriptionUtils.getSubscriptionStatus()).toEqual({ status: PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE, }); }); + + it('should return OWNER_OF_POLICY_UNDER_INVOICING status', async () => { + await Onyx.multiSet({ + [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: 0, + }); + + expect(SubscriptionUtils.getSubscriptionStatus()).toEqual({ + status: PAYMENT_STATUS.OWNER_OF_POLICY_UNDER_INVOICING, + }); + }); + + it('should return OWNER_OF_POLICY_UNDER_INVOICING_OVERDUE status', async () => { + await Onyx.multiSet({ + [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: 1, + }); + + expect(SubscriptionUtils.getSubscriptionStatus()).toEqual({ + status: PAYMENT_STATUS.OWNER_OF_POLICY_UNDER_INVOICING_OVERDUE, + }); + }); + + it('should return BILLING_DISPUTE_PENDING status', async () => { + await Onyx.multiSet({ + [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: 0, + [ONYXKEYS.NVP_PRIVATE_BILLING_DISPUTE_PENDING]: 1, + }); + + expect(SubscriptionUtils.getSubscriptionStatus()).toEqual({ + status: PAYMENT_STATUS.BILLING_DISPUTE_PENDING, + }); + }); + + it('should return CARD_AUTHENTICATION_REQUIRED status', async () => { + await Onyx.multiSet({ + [ONYXKEYS.NVP_PRIVATE_BILLING_DISPUTE_PENDING]: 0, + [ONYXKEYS.NVP_PRIVATE_STRIPE_CUSTOMER_ID]: STRIPE_CUSTOMER_ID, + }); + + expect(SubscriptionUtils.getSubscriptionStatus()).toEqual({ + status: PAYMENT_STATUS.CARD_AUTHENTICATION_REQUIRED, + }); + }); + + it('should return INSUFFICIENT_FUNDS status', async () => { + await Onyx.multiSet({ + [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: AMOUNT_OWED, + [ONYXKEYS.NVP_PRIVATE_STRIPE_CUSTOMER_ID]: {}, + [ONYXKEYS.NVP_PRIVATE_BILLING_STATUS]: BILLING_STATUS_INSUFFICIENT_FUNDS, + }); + + expect(SubscriptionUtils.getSubscriptionStatus()).toEqual({ + status: PAYMENT_STATUS.INSUFFICIENT_FUNDS, + }); + }); + + it('should return CARD_EXPIRED status', async () => { + await Onyx.multiSet({ + [ONYXKEYS.NVP_PRIVATE_BILLING_STATUS]: BILLING_STATUS_EXPIRED_CARD, + }); + + expect(SubscriptionUtils.getSubscriptionStatus()).toEqual({ + status: PAYMENT_STATUS.CARD_EXPIRED, + }); + }); + + it('should return CARD_EXPIRE_SOON status', async () => { + await Onyx.multiSet({ + [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: 0, + [ONYXKEYS.NVP_PRIVATE_BILLING_STATUS]: {}, + [ONYXKEYS.FUND_LIST]: FUND_LIST, + }); + + expect(SubscriptionUtils.getSubscriptionStatus()).toEqual({ + status: PAYMENT_STATUS.CARD_EXPIRE_SOON, + }); + }); + + it('should return RETRY_BILLING_SUCCESS status', async () => { + await Onyx.multiSet({ + [ONYXKEYS.FUND_LIST]: {}, + [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS]: CONST.SUBSCRIPTION_RETRY_BILLING_STATUS.SUCCESS, + }); + + expect(SubscriptionUtils.getSubscriptionStatus()).toEqual({ + status: PAYMENT_STATUS.RETRY_BILLING_SUCCESS, + isError: false, + }); + }); + + it('should return RETRY_BILLING_ERROR status', async () => { + await Onyx.multiSet({ + [ONYXKEYS.FUND_LIST]: {}, + [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS]: CONST.SUBSCRIPTION_RETRY_BILLING_STATUS.FAILED, + }); + + expect(SubscriptionUtils.getSubscriptionStatus()).toEqual({ + status: PAYMENT_STATUS.RETRY_BILLING_ERROR, + isError: true, + }); + }); }); }); From 38bbd5adadfed0d2ea11af9a570cb48cd9fa98fd Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 26 Jun 2024 16:54:59 +0530 Subject: [PATCH 078/183] Fixed sync time in netsuite --- .../workspace/accounting/PolicyAccountingPage.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index b1634cdad07b..266c9180981d 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -143,7 +143,15 @@ function PolicyAccountingPage({policy, connectionSyncProgress}: PolicyAccounting const accountingIntegrations = Object.values(CONST.POLICY.CONNECTIONS.NAME).filter((name) => !(name === CONST.POLICY.CONNECTIONS.NAME.NETSUITE && !canUseNetSuiteIntegration)); const connectedIntegration = accountingIntegrations.find((integration) => !!policy?.connections?.[integration]) ?? connectionSyncProgress?.connectionName; const policyID = policy?.id ?? '-1'; - const successfulDate = policy?.connections?.quickbooksOnline?.lastSync?.successfulDate; + const successfulDate = useMemo(() => { + if (!connectedIntegration) { + return undefined; + } + if (connectedIntegration === CONST.POLICY.CONNECTIONS.NAME.NETSUITE) { + return policy?.connections?.netsuite.lastSyncDate; + } + return policy?.connections?.[connectedIntegration]?.lastSync?.successfulDate; + }, [connectedIntegration, policy?.connections]); const formattedDate = useMemo(() => (successfulDate ? new Date(successfulDate) : new Date()), [successfulDate]); const policyConnectedToXero = connectedIntegration === CONST.POLICY.CONNECTIONS.NAME.XERO; From 49d6f5610458cc0f853cba4f33255f162fb6559a Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 26 Jun 2024 18:02:49 +0530 Subject: [PATCH 079/183] Fixed style in helper text --- src/components/MenuItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 30ee31d30514..9fd18524158d 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -791,7 +791,7 @@ function MenuItem( {!!helperText && (shouldParseHelperText ? ( - + ) : ( From 77ac09c602a4845ab89c7f4c6d0db58fd49cb6b5 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 26 Jun 2024 15:18:22 +0200 Subject: [PATCH 080/183] refactor billing banner translations --- src/languages/en.ts | 64 +++++++++++++----- src/languages/es.ts | 67 ++++++++++++++----- .../Subscription/CardSection/CardSection.tsx | 3 +- .../Subscription/CardSection/utils.ts | 58 +++++++--------- tests/unit/CardsSectionUtilsTest.ts | 46 ++++++------- 5 files changed, 147 insertions(+), 91 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 6a161b479d6c..c0263f4bfe7c 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3256,25 +3256,55 @@ export default { subscription: { mobileReducedFunctionalityMessage: 'You can’t make changes to your subscription in the mobile app.', billingBanner: { - outdatedInfo: 'Your payment info is outdated', - updatePaymentInformation: 'Please update your payment information.', - updateCardDataByDate: ({date}) => `Update your payment card by ${date} to continue using all of your favorite features.`, - paymentPastDuePayByDate: ({date}) => `Your payment is past due. Please pay your invoice by ${date} to avoid service interruption.`, - paymentPastDue: 'Your payment is past due. Please pay your invoice.', - cardCouldNotBeCharged: 'Your card couldn’t be charged', - retryMessage: 'Before retrying, please call your bank directly to authorize Expensify charges and remove any holds. Otherwise, try adding a different payment card.', - cardNotFullyAuthenticated: ({cardEnding}) => - `Your payment card hasn’t been fully authenticated. Please complete the authentication process to activate your payment card ending in ${cardEnding}.`, - cardDeclinedDueToInsufficientFunds: ({amountOwed}) => - `Your payment card was declined due to insufficient funds. Please retry or add a new payment card to clear your ${amountOwed} outstanding balance.`, - cardExpired: ({amountOwed}) => `Your payment card expired. Please add a new payment card to clear your ${amountOwed} outstanding balance.`, - cardExpiringSoon: 'Your card is expiring soon', - cardWillExpireAtTheEndOfMonth: - 'Your payment card will expire at the end of this month. Click the three-dot menu below to update it and continue using all your favorite features.', + policyOwnerAmountOwed: { + title: 'Your payment info is outdated', + subtitle: ({date}) => `Update your payment card by ${date} to continue using all of your favorite features.`, + }, + policyOwnerAmountOwedOverdue: { + title: 'Your payment info is outdated', + subtitle: 'Please update your payment information.', + }, + policyOwnerUnderInvoicing: { + title: 'Your payment info is outdated', + subtitle: ({date}) => `Your payment is past due. Please pay your invoice by ${date} to avoid service interruption.`, + }, + policyOwnerUnderInvoicingOverdue: { + title: 'Your payment info is outdated', + subtitle: 'Your payment is past due. Please pay your invoice.', + }, + billingDisputePending: { + title: 'Your card couldn’t be charged', + subtitle: ({amountOwed, cardEnding}) => + `You disputed the ${amountOwed} charge on the card ending in ${cardEnding}. Your account will be locked until the dispute is resolved with your bank.`, + }, + cardAuthenticationRequired: { + title: 'Your card couldn’t be charged', + subtitle: ({cardEnding}) => + `Your payment card hasn’t been fully authenticated. Please complete the authentication process to activate your payment card ending in ${cardEnding}.`, + }, + insufficientFunds: { + title: 'Your card couldn’t be charged', + subtitle: ({amountOwed}) => + `Your payment card was declined due to insufficient funds. Please retry or add a new payment card to clear your ${amountOwed} outstanding balance.`, + }, + cardExpired: { + title: 'Your card couldn’t be charged', + subtitle: ({amountOwed}) => `Your payment card expired. Please add a new payment card to clear your ${amountOwed} outstanding balance.`, + }, + cardExpireSoon: { + title: 'Your card is expiring soon', + subtitle: 'Your payment card will expire at the end of this month. Click the three-dot menu below to update it and continue using all your favorite features.', + }, + retryBillingSuccess: { + title: 'Success!', + subtitle: 'Your card has been billed successfully.', + }, + retryBillingError: { + title: 'Your card couldn’t be charged', + subtitle: 'Before retrying, please call your bank directly to authorize Expensify charges and remove any holds. Otherwise, try adding a different payment card.', + }, cardOnDispute: ({amountOwed, cardEnding}) => `You disputed the ${amountOwed} charge on the card ending in ${cardEnding}. Your account will be locked until the dispute is resolved with your bank.`, - succeeded: 'Success!', - billedSuccessfully: 'Your card has been billed successfully.', preTrial: { title: 'Start a free trial', subtitle: 'To get started, ', diff --git a/src/languages/es.ts b/src/languages/es.ts index 124972c84127..944511f302a0 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3760,26 +3760,57 @@ export default { subscription: { mobileReducedFunctionalityMessage: 'No puedes hacer cambios en tu suscripción en la aplicación móvil.', billingBanner: { - outdatedInfo: 'Tu información de pago está desactualizada', - updatePaymentInformation: 'Por favor, actualiza tu información de pago.', - updateCardDataByDate: ({date}) => `Actualiza tu tarjeta de pago antes del ${date} para continuar utilizando todas tus herramientas favoritas`, - paymentPastDuePayByDate: ({date}) => `Tu pago está vencido. Por favor, paga tu factura antes del ${date} para evitar la interrupción del servicio.`, - paymentPastDue: 'Tu pago está vencido. Por favor, paga tu factura.', - cardCouldNotBeCharged: 'No se ha podido realizar el cobro a tu tarjeta', - retryMessage: - 'Antes de volver a intentarlo, llama directamente a tu banco para que autorice los cargos de Expensify y elimine las retenciones. De lo contrario, añade una tarjeta de pago diferente.', - cardNotFullyAuthenticated: ({cardEnding}) => - `Tu tarjeta de pago no ha sido autenticada completamente. Por favor, completa el proceso de autenticación para activar tu tarjeta de pago que termina en ${cardEnding}.`, - cardDeclinedDueToInsufficientFunds: ({amountOwed}) => - `Tu tarjeta de pago fue rechazada por falta de fondos. Vuelve a intentarlo o añade una nueva tarjeta de pago para liquidar tu saldo pendiente de ${amountOwed}.`, - cardExpired: ({amountOwed}) => `Tu tarjeta de pago ha expirado. Por favor, añade una nueva tarjeta de pago para liquidar tu saldo pendiente de ${amountOwed}.`, - cardExpiringSoon: 'Tu tarjeta caducará pronto', - cardWillExpireAtTheEndOfMonth: - 'Tu tarjeta de pago caducará a finales de este mes. Haz clic en el menú de tres puntos que aparece a continuación para actualizarla y continuar utilizando todas tus herramientas favoritas.', + policyOwnerAmountOwed: { + title: 'Tu información de pago está desactualizada', + subtitle: ({date}) => `Actualiza tu tarjeta de pago antes del ${date} para continuar utilizando todas tus herramientas favoritas`, + }, + policyOwnerAmountOwedOverdue: { + title: 'Tu información de pago está desactualizada', + subtitle: 'Por favor, actualiza tu información de pago.', + }, + policyOwnerUnderInvoicing: { + title: 'Tu información de pago está desactualizada', + subtitle: ({date}) => `Tu pago está vencido. Por favor, paga tu factura antes del ${date} para evitar la interrupción del servicio.`, + }, + policyOwnerUnderInvoicingOverdue: { + title: 'Tu información de pago está desactualizada', + subtitle: 'Tu pago está vencido. Por favor, paga tu factura.', + }, + billingDisputePending: { + title: 'No se ha podido realizar el cobro a tu tarjeta', + subtitle: ({amountOwed, cardEnding}) => + `Has impugnado el cargo ${amountOwed} en la tarjeta terminada en ${cardEnding}. Tu cuenta estará bloqueada hasta que se resuelva la disputa con tu banco.`, + }, + cardAuthenticationRequired: { + title: 'No se ha podido realizar el cobro a tu tarjeta', + subtitle: ({cardEnding}) => + `Tu tarjeta de pago no ha sido autenticada completamente. Por favor, completa el proceso de autenticación para activar tu tarjeta de pago que termina en ${cardEnding}.`, + }, + insufficientFunds: { + title: 'No se ha podido realizar el cobro a tu tarjeta', + subtitle: ({amountOwed}) => + `Tu tarjeta de pago fue rechazada por falta de fondos. Vuelve a intentarlo o añade una nueva tarjeta de pago para liquidar tu saldo pendiente de ${amountOwed}.`, + }, + cardExpired: { + title: 'No se ha podido realizar el cobro a tu tarjeta', + subtitle: ({amountOwed}) => `Tu tarjeta de pago ha expirado. Por favor, añade una nueva tarjeta de pago para liquidar tu saldo pendiente de ${amountOwed}.`, + }, + cardExpireSoon: { + title: 'Tu tarjeta caducará pronto', + subtitle: + 'Tu tarjeta de pago caducará a finales de este mes. Haz clic en el menú de tres puntos que aparece a continuación para actualizarla y continuar utilizando todas tus herramientas favoritas.', + }, + retryBillingSuccess: { + title: 'Éxito!', + subtitle: 'Tu tarjeta fue facturada correctamente.', + }, + retryBillingError: { + title: 'No se ha podido realizar el cobro a tu tarjeta', + subtitle: + 'Antes de volver a intentarlo, llama directamente a tu banco para que autorice los cargos de Expensify y elimine las retenciones. De lo contrario, añade una tarjeta de pago diferente.', + }, cardOnDispute: ({amountOwed, cardEnding}) => `Has impugnado el cargo ${amountOwed} en la tarjeta terminada en ${cardEnding}. Tu cuenta estará bloqueada hasta que se resuelva la disputa con tu banco.`, - succeeded: 'Éxito!', - billedSuccessfully: 'Tu tarjeta fue facturada correctamente.', preTrial: { title: 'Iniciar una prueba gratuita', subtitle: 'Para empezar, ', diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index d93b43fddff9..1e83977e0ccd 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -9,6 +9,7 @@ import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; +import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import PreTrialBillingBanner from './BillingBanner/PreTrialBillingBanner'; @@ -23,7 +24,7 @@ function CardSection() { const theme = useTheme(); const [privateSubscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION); - const defaultCard = CardSectionUtils.getCardForSubscriptionBilling(); + const defaultCard = SubscriptionUtils.getCardForSubscriptionBilling(); const cardMonth = useMemo(() => DateUtils.getMonthNames(preferredLocale)[(defaultCard?.accountData?.cardMonth ?? 1) - 1], [defaultCard?.accountData?.cardMonth, preferredLocale]); diff --git a/src/pages/settings/Subscription/CardSection/utils.ts b/src/pages/settings/Subscription/CardSection/utils.ts index 126f5c03b3f7..38e9713d5db0 100644 --- a/src/pages/settings/Subscription/CardSection/utils.ts +++ b/src/pages/settings/Subscription/CardSection/utils.ts @@ -6,7 +6,6 @@ import type {Phrase, PhraseParameters} from '@libs/Localize'; import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; -import type {Fund} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; type BillingStatusResult = { @@ -15,7 +14,7 @@ type BillingStatusResult = { isError: boolean; isRetryAvailable?: boolean; isAddButtonDark?: boolean; - isAuthenticatingRequired?: boolean; + isAuthenticationRequired?: boolean; icon?: IconAsset; rightIcon?: IconAsset; }; @@ -26,96 +25,96 @@ function getBillingStatus( ): BillingStatusResult | undefined { const amountOwed = SubscriptionUtils.getAmountOwed(); - const status = SubscriptionUtils.getSubscriptionStatus(); + const subscriptionStatus = SubscriptionUtils.getSubscriptionStatus(); const endDate = SubscriptionUtils.getOverdueGracePeriodDate(); const endDateFormatted = endDate ? DateUtils.formatWithUTCTimeZone(fromUnixTime(endDate).toUTCString(), CONST.DATE.MONTH_DAY_YEAR_FORMAT) : null; - switch (status?.status) { + switch (subscriptionStatus?.status) { case SubscriptionUtils.PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED: return { - title: translate('subscription.billingBanner.outdatedInfo'), - subtitle: translate('subscription.billingBanner.updateCardDataByDate', {date: endDateFormatted}), + title: translate('subscription.billingBanner.policyOwnerAmountOwed.title'), + subtitle: translate('subscription.billingBanner.policyOwnerAmountOwed.subtitle', {date: endDateFormatted}), isError: true, isRetryAvailable: true, }; case SubscriptionUtils.PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE: return { - title: translate('subscription.billingBanner.outdatedInfo'), - subtitle: translate('subscription.billingBanner.updatePaymentInformation'), + title: translate('subscription.billingBanner.policyOwnerAmountOwedOverdue.title'), + subtitle: translate('subscription.billingBanner.policyOwnerAmountOwedOverdue.subtitle'), isError: true, }; case SubscriptionUtils.PAYMENT_STATUS.OWNER_OF_POLICY_UNDER_INVOICING: return { - title: translate('subscription.billingBanner.outdatedInfo'), - subtitle: translate('subscription.billingBanner.paymentPastDuePayByDate', {date: endDateFormatted}), + title: translate('subscription.billingBanner.policyOwnerUnderInvoicing.title'), + subtitle: translate('subscription.billingBanner.policyOwnerUnderInvoicing.subtitle', {date: endDateFormatted}), isError: true, isAddButtonDark: true, }; case SubscriptionUtils.PAYMENT_STATUS.OWNER_OF_POLICY_UNDER_INVOICING_OVERDUE: return { - title: translate('subscription.billingBanner.outdatedInfo'), - subtitle: translate('subscription.billingBanner.paymentPastDue'), + title: translate('subscription.billingBanner.policyOwnerUnderInvoicingOverdue.title'), + subtitle: translate('subscription.billingBanner.policyOwnerUnderInvoicingOverdue.subtitle'), isError: true, isAddButtonDark: true, }; case SubscriptionUtils.PAYMENT_STATUS.BILLING_DISPUTE_PENDING: return { - title: translate('subscription.billingBanner.cardCouldNotBeCharged'), - subtitle: translate('subscription.billingBanner.cardOnDispute', {amountOwed, cardEnding}), + title: translate('subscription.billingBanner.billingDisputePending.title'), + subtitle: translate('subscription.billingBanner.billingDisputePending.subtitle', {amountOwed, cardEnding}), isError: true, isRetryAvailable: false, }; case SubscriptionUtils.PAYMENT_STATUS.CARD_AUTHENTICATION_REQUIRED: return { - title: translate('subscription.billingBanner.cardCouldNotBeCharged'), - subtitle: translate('subscription.billingBanner.cardNotFullyAuthenticated', {cardEnding}), + title: translate('subscription.billingBanner.cardAuthenticationRequired.title'), + subtitle: translate('subscription.billingBanner.cardAuthenticationRequired.subtitle', {cardEnding}), isError: true, - isAuthenticatingRequired: true, + isAuthenticationRequired: true, }; case SubscriptionUtils.PAYMENT_STATUS.INSUFFICIENT_FUNDS: return { - title: translate('subscription.billingBanner.cardCouldNotBeCharged'), - subtitle: translate('subscription.billingBanner.cardDeclinedDueToInsufficientFunds', {amountOwed}), + title: translate('subscription.billingBanner.insufficientFunds.title'), + subtitle: translate('subscription.billingBanner.insufficientFunds.subtitle', {amountOwed}), isError: true, isRetryAvailable: true, }; case SubscriptionUtils.PAYMENT_STATUS.CARD_EXPIRED: return { - title: translate('subscription.billingBanner.cardCouldNotBeCharged'), - subtitle: translate('subscription.billingBanner.cardExpired', {amountOwed}), + title: translate('subscription.billingBanner.cardExpired.title'), + subtitle: translate('subscription.billingBanner.cardExpired.subtitle', {amountOwed}), isError: true, isRetryAvailable: true, }; case SubscriptionUtils.PAYMENT_STATUS.CARD_EXPIRE_SOON: return { - title: translate('subscription.billingBanner.cardExpiringSoon'), - subtitle: translate('subscription.billingBanner.cardWillExpireAtTheEndOfMonth'), + title: translate('subscription.billingBanner.cardExpireSoon.title'), + subtitle: translate('subscription.billingBanner.cardExpireSoon.subtitle'), isError: false, icon: Illustrations.CreditCardEyes, }; case SubscriptionUtils.PAYMENT_STATUS.RETRY_BILLING_SUCCESS: return { - title: translate('subscription.billingBanner.succeeded'), - subtitle: translate('subscription.billingBanner.billedSuccessfully'), + title: translate('subscription.billingBanner.retryBillingSuccess.title'), + subtitle: translate('subscription.billingBanner.retryBillingSuccess.subtitle'), isError: false, rightIcon: Expensicons.Close, }; case SubscriptionUtils.PAYMENT_STATUS.RETRY_BILLING_ERROR: return { - title: translate('subscription.billingBanner.cardCouldNotBeCharged'), - subtitle: translate('subscription.billingBanner.retryMessage'), + title: translate('subscription.billingBanner.retryBillingError.title'), + subtitle: translate('subscription.billingBanner.retryBillingError.subtitle'), isError: true, }; @@ -124,10 +123,6 @@ function getBillingStatus( } } -function getCardForSubscriptionBilling(): Fund | undefined { - return SubscriptionUtils.getCardForSubscriptionBilling(); -} - /** * Get the next billing date. * @@ -147,7 +142,6 @@ function shouldShowPreTrialBillingBanner(): boolean { export default { getBillingStatus, - getCardForSubscriptionBilling, shouldShowPreTrialBillingBanner, getNextBillingDate, }; diff --git a/tests/unit/CardsSectionUtilsTest.ts b/tests/unit/CardsSectionUtilsTest.ts index f0e66edcded8..eb4103419aaa 100644 --- a/tests/unit/CardsSectionUtilsTest.ts +++ b/tests/unit/CardsSectionUtilsTest.ts @@ -80,8 +80,8 @@ describe('CardSectionUtils', () => { }); expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ - title: 'subscription.billingBanner.outdatedInfo', - subtitle: 'subscription.billingBanner.updateCardDataByDate', + title: 'subscription.billingBanner.policyOwnerAmountOwed.title', + subtitle: 'subscription.billingBanner.policyOwnerAmountOwed.subtitle', isError: true, isRetryAvailable: true, }); @@ -93,8 +93,8 @@ describe('CardSectionUtils', () => { }); expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ - title: 'subscription.billingBanner.outdatedInfo', - subtitle: 'subscription.billingBanner.updatePaymentInformation', + title: 'subscription.billingBanner.policyOwnerAmountOwedOverdue.title', + subtitle: 'subscription.billingBanner.policyOwnerAmountOwedOverdue.subtitle', isError: true, }); }); @@ -105,8 +105,8 @@ describe('CardSectionUtils', () => { }); expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ - title: 'subscription.billingBanner.outdatedInfo', - subtitle: 'subscription.billingBanner.paymentPastDuePayByDate', + title: 'subscription.billingBanner.policyOwnerUnderInvoicing.title', + subtitle: 'subscription.billingBanner.policyOwnerUnderInvoicing.subtitle', isError: true, isAddButtonDark: true, }); @@ -118,8 +118,8 @@ describe('CardSectionUtils', () => { }); expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ - title: 'subscription.billingBanner.outdatedInfo', - subtitle: 'subscription.billingBanner.paymentPastDue', + title: 'subscription.billingBanner.policyOwnerUnderInvoicingOverdue.title', + subtitle: 'subscription.billingBanner.policyOwnerUnderInvoicingOverdue.subtitle', isError: true, isAddButtonDark: true, }); @@ -131,8 +131,8 @@ describe('CardSectionUtils', () => { }); expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ - title: 'subscription.billingBanner.cardCouldNotBeCharged', - subtitle: 'subscription.billingBanner.cardOnDispute', + title: 'subscription.billingBanner.billingDisputePending.title', + subtitle: 'subscription.billingBanner.billingDisputePending.subtitle', isError: true, isRetryAvailable: false, }); @@ -144,10 +144,10 @@ describe('CardSectionUtils', () => { }); expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ - title: 'subscription.billingBanner.cardCouldNotBeCharged', - subtitle: 'subscription.billingBanner.cardNotFullyAuthenticated', + title: 'subscription.billingBanner.cardAuthenticationRequired.title', + subtitle: 'subscription.billingBanner.cardAuthenticationRequired.subtitle', isError: true, - isAuthenticatingRequired: true, + isAuthenticationRequired: true, }); }); @@ -157,8 +157,8 @@ describe('CardSectionUtils', () => { }); expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ - title: 'subscription.billingBanner.cardCouldNotBeCharged', - subtitle: 'subscription.billingBanner.cardDeclinedDueToInsufficientFunds', + title: 'subscription.billingBanner.insufficientFunds.title', + subtitle: 'subscription.billingBanner.insufficientFunds.subtitle', isError: true, isRetryAvailable: true, }); @@ -170,8 +170,8 @@ describe('CardSectionUtils', () => { }); expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ - title: 'subscription.billingBanner.cardCouldNotBeCharged', - subtitle: 'subscription.billingBanner.cardExpired', + title: 'subscription.billingBanner.cardExpired.title', + subtitle: 'subscription.billingBanner.cardExpired.subtitle', isError: true, isRetryAvailable: true, }); @@ -183,8 +183,8 @@ describe('CardSectionUtils', () => { }); expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ - title: 'subscription.billingBanner.cardExpiringSoon', - subtitle: 'subscription.billingBanner.cardWillExpireAtTheEndOfMonth', + title: 'subscription.billingBanner.cardExpireSoon.title', + subtitle: 'subscription.billingBanner.cardExpireSoon.subtitle', isError: false, icon: Illustrations.CreditCardEyes, }); @@ -196,8 +196,8 @@ describe('CardSectionUtils', () => { }); expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ - title: 'subscription.billingBanner.succeeded', - subtitle: 'subscription.billingBanner.billedSuccessfully', + title: 'subscription.billingBanner.retryBillingSuccess.title', + subtitle: 'subscription.billingBanner.retryBillingSuccess.subtitle', isError: false, rightIcon: Expensicons.Close, }); @@ -209,8 +209,8 @@ describe('CardSectionUtils', () => { }); expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ - title: 'subscription.billingBanner.cardCouldNotBeCharged', - subtitle: 'subscription.billingBanner.retryMessage', + title: 'subscription.billingBanner.retryBillingError.title', + subtitle: 'subscription.billingBanner.retryBillingError.subtitle', isError: true, }); }); From 003d0ecc7b9ca6a2d94c5ce23ad15208dc28ee05 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 26 Jun 2024 16:13:07 +0200 Subject: [PATCH 081/183] bring back SUBSCRIPTION_RETRY_BILLING_STATUS logic --- src/CONST.ts | 5 ----- src/ONYXKEYS.ts | 10 +++++++--- src/libs/SubscriptionUtils.ts | 23 ++++++++++++++++++----- src/types/onyx/RetryBillingStatus.ts | 7 ------- src/types/onyx/index.ts | 2 -- tests/unit/SubscriptionUtilsTest.ts | 5 +++-- 6 files changed, 28 insertions(+), 24 deletions(-) delete mode 100644 src/types/onyx/RetryBillingStatus.ts diff --git a/src/CONST.ts b/src/CONST.ts index 221b9df62866..e71ad55a452c 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4913,11 +4913,6 @@ const CONST = { }, EXCLUDE_FROM_LAST_VISITED_PATH: [SCREENS.NOT_FOUND, SCREENS.SAML_SIGN_IN, SCREENS.VALIDATE_LOGIN] as string[], - - SUBSCRIPTION_RETRY_BILLING_STATUS: { - SUCCESS: 'success', - FAILED: 'failed', - }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index d8261b962db9..0b6107e7f2fd 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -351,8 +351,11 @@ const ONYXKEYS = { /** Holds the checks used while transferring the ownership of the workspace */ POLICY_OWNERSHIP_CHANGE_CHECKS: 'policyOwnershipChangeChecks', - /** Indicates the result of ClearOutstandingBalance, it can either be success or failed */ - SUBSCRIPTION_RETRY_BILLING_STATUS: 'subscriptionRetryBillingStatus', + /** Indicates whether ClearOutstandingBalance failed */ + SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED: 'subscriptionRetryBillingStatusFailed', + + /** Indicates whether ClearOutstandingBalance was successful */ + SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL: 'subscriptionRetryBillingStatusSuccessful', /** Stores info during review duplicates flow */ REVIEW_DUPLICATES: 'reviewDuplicates', @@ -740,7 +743,8 @@ type OnyxValuesMapping = { [ONYXKEYS.CACHED_PDF_PATHS]: Record; [ONYXKEYS.POLICY_OWNERSHIP_CHANGE_CHECKS]: Record; [ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE]: OnyxTypes.QuickAction; - [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS]: OnyxTypes.RetryBillingStatus; + [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED]: boolean; + [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL]: boolean; [ONYXKEYS.NVP_TRAVEL_SETTINGS]: OnyxTypes.TravelSettings; [ONYXKEYS.REVIEW_DUPLICATES]: OnyxTypes.ReviewDuplicates; [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: string; diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 9e244eaf06f8..de0996c1b1e0 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -70,18 +70,31 @@ Onyx.connect({ }, }); -let retryBillingStatus: OnyxValues[typeof ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS]; +let retryBillingSuccessful: OnyxEntry; Onyx.connect({ - key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS, + key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL, callback: (value) => { if (value === undefined) { return; } - retryBillingStatus = value; + retryBillingSuccessful = value; }, }); +let retryBillingFailed: OnyxEntry; +Onyx.connect({ + key: ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED, + callback: (value) => { + if (value === undefined) { + return; + } + + retryBillingFailed = value; + }, + initWithStoredValues: false, +}); + let firstDayFreeTrial: OnyxEntry; Onyx.connect({ key: ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL, @@ -202,14 +215,14 @@ function hasCardExpiringSoon(): boolean { * @returns Whether there is a retry billing error. */ function hasRetryBillingError(): boolean { - return retryBillingStatus === CONST.SUBSCRIPTION_RETRY_BILLING_STATUS.FAILED; + return !!retryBillingFailed ?? false; } /** * @returns Whether the retry billing was successful. */ function isRetryBillingSuccessful(): boolean { - return retryBillingStatus === CONST.SUBSCRIPTION_RETRY_BILLING_STATUS.SUCCESS; + return !!retryBillingSuccessful ?? false; } type SubscriptionStatus = { diff --git a/src/types/onyx/RetryBillingStatus.ts b/src/types/onyx/RetryBillingStatus.ts deleted file mode 100644 index 59a88a258516..000000000000 --- a/src/types/onyx/RetryBillingStatus.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type {ValueOf} from 'type-fest'; -import type CONST from '@src/CONST'; - -/** Indicates the status of ClearOutstandingBalance's response */ -type RetryBillingStatus = ValueOf; - -export default RetryBillingStatus; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 67d9121121c6..0d61ce86138b 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -67,7 +67,6 @@ import type ReportNextStep from './ReportNextStep'; import type ReportUserIsTyping from './ReportUserIsTyping'; import type Request from './Request'; import type Response from './Response'; -import type RetryBillingStatus from './RetryBillingStatus'; import type ReviewDuplicates from './ReviewDuplicates'; import type ScreenShareRequest from './ScreenShareRequest'; import type SearchResults from './SearchResults'; @@ -195,7 +194,6 @@ export type { ReviewDuplicates, PrivateSubscription, BillingGraceEndPeriod, - RetryBillingStatus, StripeCustomerID, BillingStatus, }; diff --git a/tests/unit/SubscriptionUtilsTest.ts b/tests/unit/SubscriptionUtilsTest.ts index 8303d8918e26..c234c1e55658 100644 --- a/tests/unit/SubscriptionUtilsTest.ts +++ b/tests/unit/SubscriptionUtilsTest.ts @@ -385,7 +385,7 @@ describe('SubscriptionUtils', () => { it('should return RETRY_BILLING_SUCCESS status', async () => { await Onyx.multiSet({ [ONYXKEYS.FUND_LIST]: {}, - [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS]: CONST.SUBSCRIPTION_RETRY_BILLING_STATUS.SUCCESS, + [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL]: true, }); expect(SubscriptionUtils.getSubscriptionStatus()).toEqual({ @@ -397,7 +397,8 @@ describe('SubscriptionUtils', () => { it('should return RETRY_BILLING_ERROR status', async () => { await Onyx.multiSet({ [ONYXKEYS.FUND_LIST]: {}, - [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS]: CONST.SUBSCRIPTION_RETRY_BILLING_STATUS.FAILED, + [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL]: false, + [ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED]: true, }); expect(SubscriptionUtils.getSubscriptionStatus()).toEqual({ From 74a1400217de9e60c7b2fbf19cabf07830269a15 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Wed, 26 Jun 2024 22:29:26 +0700 Subject: [PATCH 082/183] handle conflict --- src/libs/actions/Task.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index dcb4c19b8ed8..a42813fc185b 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -914,7 +914,7 @@ function getParentReport(report: OnyxEntry): OnyxEntry | EmptyObject { +function getReport(reportID: string): OnyxEntry { return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? {}; } From 65c260ef8b7a295092097a90fd93487a1fa5afbc Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Wed, 26 Jun 2024 22:36:52 +0700 Subject: [PATCH 083/183] handle conflict --- src/libs/actions/Task.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index a42813fc185b..81c9e98a5284 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -915,7 +915,7 @@ function getParentReport(report: OnyxEntry): OnyxEntry { - return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? {}; + return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; } /** From 1d406904cbdb8e77518097f92d142482d8d15993 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Wed, 26 Jun 2024 17:23:58 +0100 Subject: [PATCH 084/183] refactor: apply suggestions --- src/pages/settings/InitialSettingsPage.tsx | 4 +--- .../CardSection/BillingBanner/TrialStartedBillingBanner.tsx | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index 88997ba8540b..874783326728 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -92,7 +92,6 @@ type MenuData = { shouldShowRightIcon?: boolean; iconRight?: IconAsset; badgeText?: string; - badgeStyle?: ViewStyle; }; type Menu = {sectionStyle: StyleProp; sectionTranslationKey: TranslationPaths; items: MenuData[]}; @@ -212,7 +211,7 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa shouldShowRightIcon: true, iconRight: Expensicons.NewWindow, link: () => Link.buildOldDotURL(CONST.OLDDOT_URLS.ADMIN_POLICIES_URL), - badgeText: SubscriptionUtils.isUserOnFreeTrial() ? translate('subscription.badge.freeTrial', {numOfDays: SubscriptionUtils.calculateRemainingFreeTrialDays()}) : '', + badgeText: SubscriptionUtils.isUserOnFreeTrial() ? translate('subscription.badge.freeTrial', {numOfDays: SubscriptionUtils.calculateRemainingFreeTrialDays()}) : undefined, }); } @@ -320,7 +319,6 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa })} iconStyles={item.iconStyles} badgeText={item.badgeText ?? getWalletBalance(isPaymentItem)} - badgeStyle={item.badgeStyle} fallbackIcon={item.fallbackIcon} brickRoadIndicator={item.brickRoadIndicator} floatRightAvatars={item.floatRightAvatars} diff --git a/src/pages/settings/Subscription/CardSection/BillingBanner/TrialStartedBillingBanner.tsx b/src/pages/settings/Subscription/CardSection/BillingBanner/TrialStartedBillingBanner.tsx index 4b698722771c..7f4dce39d274 100644 --- a/src/pages/settings/Subscription/CardSection/BillingBanner/TrialStartedBillingBanner.tsx +++ b/src/pages/settings/Subscription/CardSection/BillingBanner/TrialStartedBillingBanner.tsx @@ -1,6 +1,5 @@ import React from 'react'; import * as Illustrations from '@components/Icon/Illustrations'; -import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import BillingBanner from './BillingBanner'; @@ -11,7 +10,7 @@ function TrialStartedBillingBanner() { return ( {translate('subscription.billingBanner.trialStarted.subtitle')}} + subtitle={translate('subscription.billingBanner.trialStarted.subtitle')} icon={Illustrations.TreasureChest} /> ); From a626bb36b8eab3ac7980ddedd8fe5f50ac068028 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 26 Jun 2024 09:26:03 -0700 Subject: [PATCH 085/183] Update Podfile.lock --- ios/Podfile.lock | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 35dccc2de393..a5ffdcb4b63c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1243,7 +1243,13 @@ PODS: - react-native-config (1.5.0): - react-native-config/App (= 1.5.0) - react-native-config/App (1.5.0): - - React-Core + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React + - React-Codegen + - React-RCTFabric + - ReactCommon/turbomodule/core - react-native-document-picker (9.1.1): - RCT-Folly - RCTRequired @@ -1974,7 +1980,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNScreens (3.30.1): + - RNScreens (3.32.0): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1988,13 +1994,14 @@ PODS: - React-ImageManager - React-NativeModulesApple - React-RCTFabric + - React-RCTImage - React-rendererdebug - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNScreens/common (= 3.30.1) + - RNScreens/common (= 3.32.0) - Yoga - - RNScreens/common (3.30.1): + - RNScreens/common (3.32.0): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2008,6 +2015,7 @@ PODS: - React-ImageManager - React-NativeModulesApple - React-RCTFabric + - React-RCTImage - React-rendererdebug - React-utils - ReactCommon/turbomodule/bridging @@ -2552,7 +2560,7 @@ SPEC CHECKSUMS: react-native-airship: 38e2596999242b68c933959d6145512e77937ac0 react-native-blob-util: 1ddace5234c62e3e6e4e154d305ad07ef686599b react-native-cameraroll: f373bebbe9f6b7c3fd2a6f97c5171cda574cf957 - react-native-config: 5330c8258265c1e5fdb8c009d2cabd6badd96727 + react-native-config: 5ce986133b07fc258828b20b9506de0e683efc1c react-native-document-picker: 8532b8af7c2c930f9e202aac484ac785b0f4f809 react-native-geolocation: f9e92eb774cb30ac1e099f34b3a94f03b4db7eb3 react-native-image-picker: f8a13ff106bcc7eb00c71ce11fdc36aac2a44440 @@ -2612,7 +2620,7 @@ SPEC CHECKSUMS: RNPermissions: 0b61d30d21acbeafe25baaa47d9bae40a0c65216 RNReactNativeHapticFeedback: 616c35bdec7d20d4c524a7949ca9829c09e35f37 RNReanimated: 323436b1a5364dca3b5f8b1a13458455e0de9efe - RNScreens: 9ec969a95987a6caae170ef09313138abf3331e1 + RNScreens: abd354e98519ed267600b7ee64fdcb8e060b1218 RNShare: 2a4cdfc0626ad56b0ef583d424f2038f772afe58 RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 RNSVG: 18f1381e046be2f1c30b4724db8d0c966238089f From 101925efa6a9f6195dde7e658b3afa31a6b7027a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 26 Jun 2024 17:56:14 +0100 Subject: [PATCH 086/183] Fix AttachmentPicker logic to dont include Invoice option to restrict --- .../AttachmentPickerWithMenuItems.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index d1e26681eeae..dc8091b2f793 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -128,8 +128,8 @@ function AttachmentPickerWithMenuItems({ * Returns the list of IOU Options */ const moneyRequestOptions = useMemo(() => { - const selectOption = (onSelected: () => void) => { - if (policy && SubscriptionUtils.shouldRestrictUserBillableActions(policy.id)) { + const selectOption = (onSelected: () => void, shouldRestrictAction: boolean) => { + if (shouldRestrictAction && policy && SubscriptionUtils.shouldRestrictUserBillableActions(policy.id)) { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id)); return; } @@ -141,27 +141,27 @@ function AttachmentPickerWithMenuItems({ [CONST.IOU.TYPE.SPLIT]: { icon: Expensicons.Transfer, text: translate('iou.splitExpense'), - onSelected: () => selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SPLIT, report?.reportID ?? '-1')), + onSelected: () => selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SPLIT, report?.reportID ?? '-1'), true), }, [CONST.IOU.TYPE.SUBMIT]: { icon: getIconForAction(CONST.IOU.TYPE.REQUEST), text: translate('iou.submitExpense'), - onSelected: () => selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SUBMIT, report?.reportID ?? '-1')), + onSelected: () => selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SUBMIT, report?.reportID ?? '-1'), true), }, [CONST.IOU.TYPE.PAY]: { icon: getIconForAction(CONST.IOU.TYPE.SEND), text: translate('iou.paySomeone', {name: ReportUtils.getPayeeName(report)}), - onSelected: () => selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.PAY, report?.reportID ?? '-1')), + onSelected: () => selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.PAY, report?.reportID ?? '-1'), true), }, [CONST.IOU.TYPE.TRACK]: { icon: getIconForAction(CONST.IOU.TYPE.TRACK), text: translate('iou.trackExpense'), - onSelected: () => selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK, report?.reportID ?? '-1')), + onSelected: () => selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK, report?.reportID ?? '-1'), true), }, [CONST.IOU.TYPE.INVOICE]: { icon: Expensicons.InvoiceGeneric, text: translate('workspace.invoices.sendInvoice'), - onSelected: () => selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.INVOICE, report?.reportID ?? '-1')), + onSelected: () => selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.INVOICE, report?.reportID ?? '-1'), false), }, }; From 05d214f45a0332422737f9d347bb81fedc094b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 26 Jun 2024 17:56:21 +0100 Subject: [PATCH 087/183] Fix shouldRestrictUserBillableActions() logic when user is past due but not the workspace owner --- src/libs/SubscriptionUtils.ts | 24 ++++++++++++++--- tests/unit/SubscriptionUtilsTest.ts | 42 ++++++++++++++++++++++++++--- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 50dc4f99eec0..67ebfe0f047e 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -4,6 +4,15 @@ import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {BillingGraceEndPeriod, Policy} from '@src/types/onyx'; +import * as PolicyUtils from './PolicyUtils'; + +let currentUserAccountID: number = -1; +Onyx.connect({ + key: ONYXKEYS.SESSION, + callback: (value) => { + currentUserAccountID = value?.accountID ?? -1; + }, +}); let firstDayFreeTrial: OnyxEntry; Onyx.connect({ @@ -106,6 +115,8 @@ function doesUserHavePaymentCardAdded(): boolean { function shouldRestrictUserBillableActions(policyID: string): boolean { const currentDate = new Date(); + const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; + // This logic will be executed if the user is a workspace's non-owner (normal user or admin). // We should restrict the workspace's non-owner actions if it's member of a workspace where the owner is // past due and is past its grace period end. @@ -114,10 +125,9 @@ function shouldRestrictUserBillableActions(policyID: string): boolean { if (userBillingGracePeriodEnd && isAfter(currentDate, fromUnixTime(userBillingGracePeriodEnd.value))) { // Extracts the owner account ID from the collection member key. - const ownerAccountID = entryKey.slice(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END.length); + const ownerAccountID = Number(entryKey.slice(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END.length)); - const ownerPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; - if (String(ownerPolicy?.ownerAccountID ?? -1) === ownerAccountID) { + if (PolicyUtils.isPolicyOwner(policy, ownerAccountID)) { return true; } } @@ -125,7 +135,13 @@ function shouldRestrictUserBillableActions(policyID: string): boolean { // If it reached here it means that the user is actually the workspace's owner. // We should restrict the workspace's owner actions if it's past its grace period end date and it's owing some amount. - if (ownerBillingGraceEndPeriod && amountOwed !== undefined && amountOwed > 0 && isAfter(currentDate, fromUnixTime(ownerBillingGraceEndPeriod))) { + if ( + PolicyUtils.isPolicyOwner(policy, currentUserAccountID) && + ownerBillingGraceEndPeriod && + amountOwed !== undefined && + amountOwed > 0 && + isAfter(currentDate, fromUnixTime(ownerBillingGraceEndPeriod)) + ) { return true; } diff --git a/tests/unit/SubscriptionUtilsTest.ts b/tests/unit/SubscriptionUtilsTest.ts index 7767ae9f387b..05d236c21240 100644 --- a/tests/unit/SubscriptionUtilsTest.ts +++ b/tests/unit/SubscriptionUtilsTest.ts @@ -148,6 +148,7 @@ describe('SubscriptionUtils', () => { afterEach(async () => { await Onyx.clear(); await Onyx.multiSet({ + [ONYXKEYS.SESSION]: null, [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END]: null, [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: null, [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWNED]: null, @@ -213,36 +214,71 @@ describe('SubscriptionUtils', () => { expect(SubscriptionUtils.shouldRestrictUserBillableActions(policyID)).toBeTruthy(); }); - it('should return false if the user is a workspace owner but is not past due billing', async () => { + it("should return false if the user is the workspace's owner but is not past due billing", async () => { + const accountID = 1; const policyID = '1001'; await Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: '', accountID}, [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: getUnixTime(addDays(new Date(), 3)), // not past due + [`${ONYXKEYS.COLLECTION.POLICY}${policyID}` as const]: { + ...createRandomPolicy(Number(policyID)), + ownerAccountID: accountID, + }, }); expect(SubscriptionUtils.shouldRestrictUserBillableActions(policyID)).toBeFalsy(); }); - it("should return false if the user is a workspace owner but is past due billing but isn't owning any amount", async () => { + it("should return false if the user is the workspace's owner that is past due billing but isn't owning any amount", async () => { + const accountID = 1; const policyID = '1001'; await Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: '', accountID}, [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: getUnixTime(subDays(new Date(), 3)), // past due [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWNED]: 0, + [`${ONYXKEYS.COLLECTION.POLICY}${policyID}` as const]: { + ...createRandomPolicy(Number(policyID)), + ownerAccountID: accountID, + }, }); expect(SubscriptionUtils.shouldRestrictUserBillableActions(policyID)).toBeFalsy(); }); - it('should return true if the user is a workspace owner but is past due billing and is owning some amount', async () => { + it("should return true if the user is the workspace's owner that is past due billing and is owning some amount", async () => { + const accountID = 1; const policyID = '1001'; await Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: '', accountID}, [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: getUnixTime(subDays(new Date(), 3)), // past due [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWNED]: 8010, + [`${ONYXKEYS.COLLECTION.POLICY}${policyID}` as const]: { + ...createRandomPolicy(Number(policyID)), + ownerAccountID: accountID, + }, }); expect(SubscriptionUtils.shouldRestrictUserBillableActions(policyID)).toBeTruthy(); }); + + it("should return false if the user is past due billing but is not the workspace's owner", async () => { + const accountID = 1; + const policyID = '1001'; + + await Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: '', accountID}, + [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: getUnixTime(subDays(new Date(), 3)), // past due + [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWNED]: 8010, + [`${ONYXKEYS.COLLECTION.POLICY}${policyID}` as const]: { + ...createRandomPolicy(Number(policyID)), + ownerAccountID: 2, // not the user + }, + }); + + expect(SubscriptionUtils.shouldRestrictUserBillableActions(policyID)).toBeFalsy(); + }); }); }); From 3c13487c0559f2588ae289d8574548c8e305e7ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 26 Jun 2024 18:20:18 +0100 Subject: [PATCH 088/183] Improve Restricted Action screen navigation --- .../Workspace/WorkspaceAdminRestrictedAction.tsx | 4 ++-- .../Workspace/WorkspaceOwnerRestrictedAction.tsx | 3 ++- .../Workspace/WorkspaceUserRestrictedAction.tsx | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/pages/RestrictedAction/Workspace/WorkspaceAdminRestrictedAction.tsx b/src/pages/RestrictedAction/Workspace/WorkspaceAdminRestrictedAction.tsx index efc9c36de51a..63c5613ec8e7 100644 --- a/src/pages/RestrictedAction/Workspace/WorkspaceAdminRestrictedAction.tsx +++ b/src/pages/RestrictedAction/Workspace/WorkspaceAdminRestrictedAction.tsx @@ -28,7 +28,7 @@ function WorkspaceAdminRestrictedAction({policyID}: WorkspaceAdminRestrictedActi const openAdminsReport = useCallback(() => { const reportID = `${PolicyUtils.getPolicy(policyID)?.chatReportIDAdmins}` ?? '-1'; - Report.openReport(reportID); + Navigation.resetToHome(); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); }, [policyID]); @@ -39,7 +39,7 @@ function WorkspaceAdminRestrictedAction({policyID}: WorkspaceAdminRestrictedActi > { + Navigation.resetToHome(); Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION); }; @@ -30,7 +31,7 @@ function WorkspaceOwnerRestrictedAction() { > diff --git a/src/pages/RestrictedAction/Workspace/WorkspaceUserRestrictedAction.tsx b/src/pages/RestrictedAction/Workspace/WorkspaceUserRestrictedAction.tsx index 33c159446398..21c1996ae762 100644 --- a/src/pages/RestrictedAction/Workspace/WorkspaceUserRestrictedAction.tsx +++ b/src/pages/RestrictedAction/Workspace/WorkspaceUserRestrictedAction.tsx @@ -28,7 +28,7 @@ function WorkspaceUserRestrictedAction({policyID}: WorkspaceUserRestrictedAction const openPolicyExpenseReport = useCallback(() => { const reportID = ReportUtils.findPolicyExpenseChatByPolicyID(policyID)?.reportID ?? '-1'; - Report.openReport(reportID); + Navigation.resetToHome(); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); }, [policyID]); @@ -39,7 +39,7 @@ function WorkspaceUserRestrictedAction({policyID}: WorkspaceUserRestrictedAction > Date: Wed, 26 Jun 2024 18:26:42 +0100 Subject: [PATCH 089/183] Restrict actions on FAB quick actions --- .../FloatingActionButtonAndPopover.tsx | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index ff5cfa05b57b..9c10fc1de6e9 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -23,6 +23,7 @@ import Navigation from '@libs/Navigation/Navigation'; import type {CentralPaneName, NavigationPartialRoute, RootStackParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import * as App from '@userActions/App'; import * as IOU from '@userActions/IOU'; import * as Policy from '@userActions/Policy/Policy'; @@ -228,41 +229,51 @@ function FloatingActionButtonAndPopover( }, [personalDetails, quickActionReport, quickAction?.action, quickActionAvatars]); const navigateToQuickAction = () => { + const selectOption = (onSelected: () => void, shouldRestrictAction: boolean) => { + if (shouldRestrictAction && quickActionReport?.policyID && SubscriptionUtils.shouldRestrictUserBillableActions(quickActionReport.policyID)) { + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(quickActionReport.policyID)); + return; + } + + onSelected(); + }; + const isValidReport = !(isEmptyObject(quickActionReport) || ReportUtils.isArchivedRoom(quickActionReport)); const quickActionReportID = isValidReport ? quickActionReport?.reportID ?? '-1' : ReportUtils.generateReportID(); + switch (quickAction?.action) { case CONST.QUICK_ACTIONS.REQUEST_MANUAL: - IOU.startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.MANUAL, true); + selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.MANUAL, true), true); return; case CONST.QUICK_ACTIONS.REQUEST_SCAN: - IOU.startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, true); + selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, true), true); return; case CONST.QUICK_ACTIONS.REQUEST_DISTANCE: - IOU.startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.DISTANCE, true); + selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.DISTANCE, true), true); return; case CONST.QUICK_ACTIONS.SPLIT_MANUAL: - IOU.startMoneyRequest(CONST.IOU.TYPE.SPLIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.MANUAL, true); + selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SPLIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.MANUAL, true), true); return; case CONST.QUICK_ACTIONS.SPLIT_SCAN: - IOU.startMoneyRequest(CONST.IOU.TYPE.SPLIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, true); + selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SPLIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, true), true); return; case CONST.QUICK_ACTIONS.SPLIT_DISTANCE: - IOU.startMoneyRequest(CONST.IOU.TYPE.SPLIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.DISTANCE, true); + selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SPLIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.DISTANCE, true), true); return; case CONST.QUICK_ACTIONS.SEND_MONEY: - IOU.startMoneyRequest(CONST.IOU.TYPE.PAY, quickActionReportID, CONST.IOU.REQUEST_TYPE.MANUAL, true); + selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.PAY, quickActionReportID, CONST.IOU.REQUEST_TYPE.MANUAL, true), true); return; case CONST.QUICK_ACTIONS.ASSIGN_TASK: - Task.clearOutTaskInfoAndNavigate(isValidReport ? quickActionReportID : '', isValidReport ? quickActionReport : undefined, quickAction.targetAccountID ?? -1, true); + selectOption(() => Task.clearOutTaskInfoAndNavigate(isValidReport ? quickActionReportID : '', isValidReport ? quickActionReport : undefined, quickAction.targetAccountID ?? -1, true), false); break; case CONST.QUICK_ACTIONS.TRACK_MANUAL: - IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK, quickActionReportID, CONST.IOU.REQUEST_TYPE.MANUAL, true); + selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK, quickActionReportID, CONST.IOU.REQUEST_TYPE.MANUAL, true), true); break; case CONST.QUICK_ACTIONS.TRACK_SCAN: - IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, true); + selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, true), true); break; case CONST.QUICK_ACTIONS.TRACK_DISTANCE: - IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK, quickActionReportID, CONST.IOU.REQUEST_TYPE.DISTANCE, true); + selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK, quickActionReportID, CONST.IOU.REQUEST_TYPE.DISTANCE, true), true); break; default: } From ca58a723c067d7d6bd8f81158eb8b4c2b428a721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 26 Jun 2024 21:11:16 +0100 Subject: [PATCH 090/183] Fix lint and prettier --- src/libs/SubscriptionUtils.ts | 2 +- .../Workspace/WorkspaceAdminRestrictedAction.tsx | 1 - .../Workspace/WorkspaceUserRestrictedAction.tsx | 1 - .../sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx | 5 ++++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 67ebfe0f047e..ed705397e5ec 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -6,7 +6,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {BillingGraceEndPeriod, Policy} from '@src/types/onyx'; import * as PolicyUtils from './PolicyUtils'; -let currentUserAccountID: number = -1; +let currentUserAccountID = -1; Onyx.connect({ key: ONYXKEYS.SESSION, callback: (value) => { diff --git a/src/pages/RestrictedAction/Workspace/WorkspaceAdminRestrictedAction.tsx b/src/pages/RestrictedAction/Workspace/WorkspaceAdminRestrictedAction.tsx index 63c5613ec8e7..a513351f8cb4 100644 --- a/src/pages/RestrictedAction/Workspace/WorkspaceAdminRestrictedAction.tsx +++ b/src/pages/RestrictedAction/Workspace/WorkspaceAdminRestrictedAction.tsx @@ -10,7 +10,6 @@ import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Report from '@libs/actions/Report'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import variables from '@styles/variables'; diff --git a/src/pages/RestrictedAction/Workspace/WorkspaceUserRestrictedAction.tsx b/src/pages/RestrictedAction/Workspace/WorkspaceUserRestrictedAction.tsx index 21c1996ae762..3db1304a2137 100644 --- a/src/pages/RestrictedAction/Workspace/WorkspaceUserRestrictedAction.tsx +++ b/src/pages/RestrictedAction/Workspace/WorkspaceUserRestrictedAction.tsx @@ -10,7 +10,6 @@ import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Report from '@libs/actions/Report'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import variables from '@styles/variables'; diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index 9c10fc1de6e9..6717cda9724e 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -264,7 +264,10 @@ function FloatingActionButtonAndPopover( selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.PAY, quickActionReportID, CONST.IOU.REQUEST_TYPE.MANUAL, true), true); return; case CONST.QUICK_ACTIONS.ASSIGN_TASK: - selectOption(() => Task.clearOutTaskInfoAndNavigate(isValidReport ? quickActionReportID : '', isValidReport ? quickActionReport : undefined, quickAction.targetAccountID ?? -1, true), false); + selectOption( + () => Task.clearOutTaskInfoAndNavigate(isValidReport ? quickActionReportID : '', isValidReport ? quickActionReport : undefined, quickAction.targetAccountID ?? -1, true), + false, + ); break; case CONST.QUICK_ACTIONS.TRACK_MANUAL: selectOption(() => IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK, quickActionReportID, CONST.IOU.REQUEST_TYPE.MANUAL, true), true); From 757ac0ad1b5c0176fa3c053b4276cc4cbf21eb7f Mon Sep 17 00:00:00 2001 From: Shubham Agrawal <58412969+shubham1206agra@users.noreply.github.com> Date: Thu, 27 Jun 2024 09:19:40 +0530 Subject: [PATCH 091/183] Apply suggestions from code review Co-authored-by: Manan --- .../accounting/netsuite/export/NetSuiteExportExpensesPage.tsx | 2 +- src/styles/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx index 1b243cb41628..87e4e3c635dc 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx @@ -137,7 +137,7 @@ function NetSuiteExportExpensesPage({policy}: WithPolicyConnectionsProps) { testID={NetSuiteExportExpensesPage.displayName} > Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID))} /> {menuItems diff --git a/src/styles/index.ts b/src/styles/index.ts index 98e5776b4508..cae0a86e20de 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -2916,8 +2916,8 @@ const styles = (theme: ThemeColors) => height: 1, backgroundColor: theme.border, flexGrow: 1, - marginHorizontal: 20, - marginVertical: 12, + ...spacing.mh5, + ...spacing.mv3, }, unreadIndicatorText: { From 2be29fd8775e86c46a6e88205ee6c0552aa20a53 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 27 Jun 2024 12:21:46 +0800 Subject: [PATCH 092/183] fix non-empty report detected as an empty report --- 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 cca52ac37c9d..6f2f65db918e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5218,7 +5218,7 @@ function isEmptyReport(report: OnyxEntry): boolean { if (!report) { return true; } - const lastVisibleMessage = ReportActionsUtils.getLastVisibleMessage(report.reportID); + const lastVisibleMessage = getLastVisibleMessage(report.reportID); return !report.lastMessageText && !report.lastMessageTranslationKey && !lastVisibleMessage.lastMessageText && !lastVisibleMessage.lastMessageTranslationKey; } From 6e7c717262cf34055fc9f04babf63b400e3a8d4c Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Thu, 27 Jun 2024 09:53:36 +0530 Subject: [PATCH 093/183] Applied suggestions --- .../NetSuiteExportConfigurationPage.tsx | 109 ++++++------------ ...iteExportExpensesDestinationSelectPage.tsx | 19 ++- ...nsesJournalPostingPreferenceSelectPage.tsx | 7 +- .../export/NetSuiteExportExpensesPage.tsx | 39 ++----- ...ExportExpensesPayableAccountSelectPage.tsx | 20 ++-- ...NetSuiteExportExpensesVendorSelectPage.tsx | 8 +- ...etSuiteInvoiceItemPreferenceSelectPage.tsx | 4 +- .../workspace/accounting/netsuite/types.ts | 44 +++++++ 8 files changed, 114 insertions(+), 136 deletions(-) create mode 100644 src/pages/workspace/accounting/netsuite/types.ts diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx index fd5af04ee4b0..a39614d5b5ea 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx @@ -1,10 +1,8 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; import ConnectionLayout from '@components/ConnectionLayout'; -import type {MenuItemProps} from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import type {OfflineWithFeedbackProps} from '@components/OfflineWithFeedback'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -14,38 +12,11 @@ import Navigation from '@libs/Navigation/Navigation'; import {canUseProvincialTaxNetSuite, canUseTaxNetSuite} from '@libs/PolicyUtils'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; -import type {ToggleSettingOptionRowProps} from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import type {Errors} from '@src/types/onyx/OnyxCommon'; - -type MenuItem = MenuItemProps & { - pendingAction?: OfflineWithFeedbackProps['pendingAction']; - - shouldHide?: boolean; - - /** Any error message to show */ - errors?: Errors; - - /** Callback to close the error messages */ - onCloseError?: () => void; - - type: 'menuitem'; -}; - -type DividerLineItem = { - type: 'divider'; - - shouldHide?: boolean; -}; - -type ToggleItem = ToggleSettingOptionRowProps & { - type: 'toggle'; - - shouldHide?: boolean; -}; +import type {DividerLineItem, MenuItem, ToggleItem} from '../types'; function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); @@ -58,23 +29,19 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) { const {subsidiaryList, receivableList, taxAccountsList} = policy?.connections?.netsuite?.options?.data ?? {}; const selectedSubsidiary = useMemo(() => { - const selectedSub = (subsidiaryList ?? []).find((subsidiary) => subsidiary.internalID === config?.subsidiaryID); - return selectedSub; + return (subsidiaryList ?? []).find((subsidiary) => subsidiary.internalID === config?.subsidiaryID); }, [subsidiaryList, config?.subsidiaryID]); const selectedReceivable = useMemo(() => { - const selectedRec = (receivableList ?? []).find((receivable) => receivable.id === config?.receivableAccount); - return selectedRec; + return (receivableList ?? []).find((receivable) => receivable.id === config?.receivableAccount); }, [receivableList, config?.receivableAccount]); const selectedTaxPostingAccount = useMemo(() => { - const selectedTaxAcc = (taxAccountsList ?? []).find((taxAccount) => taxAccount.externalID === config?.taxPostingAccount); - return selectedTaxAcc; + return (taxAccountsList ?? []).find((taxAccount) => taxAccount.externalID === config?.taxPostingAccount); }, [taxAccountsList, config?.taxPostingAccount]); const selectedProvTaxPostingAccount = useMemo(() => { - const selectedTaxAcc = (taxAccountsList ?? []).find((taxAccount) => taxAccount.externalID === config?.provincialTaxPostingAccount); - return selectedTaxAcc; + return (taxAccountsList ?? []).find((taxAccount) => taxAccount.externalID === config?.provincialTaxPostingAccount); }, [taxAccountsList, config?.provincialTaxPostingAccount]); const menuItems: Array = [ @@ -214,40 +181,40 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) { {menuItems .filter((item) => !item.shouldHide) .map((item) => { - if (item.type === 'divider') { - return ; - } - if (item.type === 'toggle') { - const {type, shouldHide, ...rest} = item; - return ( - - ); + switch (item.type) { + case 'divider': + return ; + case 'toggle': + const {type, shouldHide, ...rest} = item; + return ( + + ); + default: + return ( + + + + ); } - return ( - - - - ); })}
); diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesDestinationSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesDestinationSelectPage.tsx index 885d8ead7a7d..0e70c17908e0 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesDestinationSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesDestinationSelectPage.tsx @@ -12,45 +12,42 @@ import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnec import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; +import {ExpenseRouteParams} from '../types'; type MenuListItem = ListItem & { value: ValueOf; }; -type RouteParams = { - expenseType: ValueOf; -}; - function NetSuiteExportExpensesDestinationSelectPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const policyID = policy?.id ?? '-1'; const config = policy?.connections?.netsuite.options.config; const route = useRoute(); - const params = route.params as RouteParams; + const params = route.params as ExpenseRouteParams; const isReimbursable = params.expenseType === CONST.NETSUITE_EXPENSE_TYPE.REIMBURSABLE; - const currentValue = isReimbursable ? config?.reimbursableExpensesExportDestination : config?.nonreimbursableExpensesExportDestination; + const currentDestination = isReimbursable ? config?.reimbursableExpensesExportDestination : config?.nonreimbursableExpensesExportDestination; const data: MenuListItem[] = Object.values(CONST.NETSUITE_EXPORT_DESTINATION).map((dateType) => ({ value: dateType, text: translate(`workspace.netsuite.exportDestination.values.${dateType}.label`), keyForList: dateType, - isSelected: currentValue === dateType, + isSelected: currentDestination === dateType, })); const selectDestination = useCallback( (row: MenuListItem) => { - if (row.value !== currentValue) { + if (row.value !== currentDestination) { if (isReimbursable) { - Connections.updateNetSuiteReimbursableExpensesExportDestination(policyID, row.value, currentValue ?? 'EXPENSE_REPORT'); + Connections.updateNetSuiteReimbursableExpensesExportDestination(policyID, row.value, currentDestination ?? CONST.NETSUITE_EXPORT_DESTINATION.EXPENSE_REPORT); } else { - Connections.updateNetSuiteNonReimbursableExpensesExportDestination(policyID, row.value, currentValue ?? 'VENDOR_BILL'); + Connections.updateNetSuiteNonReimbursableExpensesExportDestination(policyID, row.value, currentDestination ?? CONST.NETSUITE_EXPORT_DESTINATION.VENDOR_BILL); } } Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES.getRoute(policyID, params.expenseType)); }, - [currentValue, isReimbursable, params.expenseType, policyID], + [currentDestination, isReimbursable, params.expenseType, policyID], ); return ( diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx index b58da40b9547..0a0b53ab16bd 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx @@ -12,22 +12,19 @@ import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnec import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; +import {ExpenseRouteParams} from '../types'; type MenuListItem = ListItem & { value: ValueOf; }; -type RouteParams = { - expenseType: ValueOf; -}; - function NetSuiteExportExpensesJournalPostingPreferenceSelectPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const policyID = policy?.id ?? '-1'; const config = policy?.connections?.netsuite.options.config; const route = useRoute(); - const params = route.params as RouteParams; + const params = route.params as ExpenseRouteParams; const isReimbursable = params.expenseType === CONST.NETSUITE_EXPENSE_TYPE.REIMBURSABLE; const data: MenuListItem[] = Object.values(CONST.NETSUITE_JOURNAL_POSTING_PREFERENCE).map((postingPreference) => ({ diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx index 87e4e3c635dc..afedf086c1b4 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx @@ -1,12 +1,9 @@ import {useRoute} from '@react-navigation/native'; import {isEmpty} from 'lodash'; import React, {useMemo} from 'react'; -import type {ValueOf} from 'type-fest'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import type {MenuItemProps} from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import type {OfflineWithFeedbackProps} from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -18,30 +15,16 @@ import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import type {Errors} from '@src/types/onyx/OnyxCommon'; +import type {ExpenseRouteParams, MenuItem} from '../types'; -type MenuItem = MenuItemProps & { - pendingAction?: OfflineWithFeedbackProps['pendingAction']; - - shouldHide?: boolean; - - /** Any error message to show */ - errors?: Errors; - - /** Callback to close the error messages */ - onCloseError?: () => void; -}; - -type RouteParams = { - expenseType: ValueOf; -}; +type MenuItemWithoutType = Omit; function NetSuiteExportExpensesPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id ?? '-1'; const route = useRoute(); - const params = route.params as RouteParams; + const params = route.params as ExpenseRouteParams; const isReimbursable = params.expenseType === CONST.NETSUITE_EXPENSE_TYPE.REIMBURSABLE; const config = policy?.connections?.netsuite?.options.config; @@ -55,23 +38,20 @@ function NetSuiteExportExpensesPage({policy}: WithPolicyConnectionsProps) { const {vendors, payableList} = policy?.connections?.netsuite?.options?.data ?? {}; const defaultVendor = useMemo(() => { - const selectedVendor = (vendors ?? []).find((vendor) => vendor.id === config?.defaultVendor); - return selectedVendor; + return (vendors ?? []).find((vendor) => vendor.id === config?.defaultVendor); }, [vendors, config?.defaultVendor]); const selectedPayableAccount = useMemo(() => { - const selectedPayableAcc = (payableList ?? []).find((payableAccount) => payableAccount.id === config?.payableAcct); - return selectedPayableAcc; + return (payableList ?? []).find((payableAccount) => payableAccount.id === config?.payableAcct); }, [payableList, config?.payableAcct]); const selectedReimbursablePayableAccount = useMemo(() => { - const selectedPayableAcc = (payableList ?? []).find((payableAccount) => payableAccount.id === config?.reimbursablePayableAccount); - return selectedPayableAcc; + return (payableList ?? []).find((payableAccount) => payableAccount.id === config?.reimbursablePayableAccount); }, [payableList, config?.reimbursablePayableAccount]); const isConnectionEmpty = isEmpty(policy?.connections?.netsuite); - const menuItems: MenuItem[] = [ + const menuItems: MenuItemWithoutType[] = [ { description: translate('workspace.accounting.exportAs'), onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_DESTINATION_SELECT.getRoute(policyID, params.expenseType)), @@ -137,7 +117,7 @@ function NetSuiteExportExpensesPage({policy}: WithPolicyConnectionsProps) { testID={NetSuiteExportExpensesPage.displayName} > Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID))} /> {menuItems @@ -152,9 +132,8 @@ function NetSuiteExportExpensesPage({policy}: WithPolicyConnectionsProps) { > ; -}; +import {ExpenseRouteParams} from '../types'; function NetSuiteExportExpensesPayableAccountSelectPage({policy}: WithPolicyConnectionsProps) { const styles = useThemeStyles(); @@ -28,27 +24,27 @@ function NetSuiteExportExpensesPayableAccountSelectPage({policy}: WithPolicyConn const policyID = policy?.id ?? '-1'; const route = useRoute(); - const params = route.params as RouteParams; + const params = route.params as ExpenseRouteParams; const isReimbursable = params.expenseType === CONST.NETSUITE_EXPENSE_TYPE.REIMBURSABLE; const config = policy?.connections?.netsuite.options.config; - const currentValue = isReimbursable ? config?.reimbursablePayableAccount : config?.payableAcct; - const netsuitePayableAccountOptions = useMemo(() => getNetSuitePayableAccountOptions(policy ?? undefined, currentValue), [currentValue, policy]); + const currentPayableAccountID = isReimbursable ? config?.reimbursablePayableAccount : config?.payableAcct; + const netsuitePayableAccountOptions = useMemo(() => getNetSuitePayableAccountOptions(policy ?? undefined, currentPayableAccountID), [currentPayableAccountID, policy]); const initiallyFocusedOptionKey = useMemo(() => netsuitePayableAccountOptions?.find((mode) => mode.isSelected)?.keyForList, [netsuitePayableAccountOptions]); const updatePayableAccount = useCallback( ({value}: SelectorType) => { - if (currentValue !== value) { + if (currentPayableAccountID !== value) { if (isReimbursable) { - Connections.updateNetSuiteReimbursablePayableAccount(policyID, value, currentValue ?? ''); + Connections.updateNetSuiteReimbursablePayableAccount(policyID, value, currentPayableAccountID ?? ''); } else { - Connections.updateNetSuitePayableAcct(policyID, value, currentValue ?? ''); + Connections.updateNetSuitePayableAcct(policyID, value, currentPayableAccountID ?? ''); } } Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES.getRoute(policyID, params.expenseType)); }, - [currentValue, policyID, params.expenseType, isReimbursable], + [currentPayableAccountID, policyID, params.expenseType, isReimbursable], ); const listEmptyContent = useMemo( diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx index f4b5586ec664..988246f125b7 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx @@ -1,6 +1,5 @@ import {useRoute} from '@react-navigation/native'; import React, {useCallback, useMemo} from 'react'; -import type {ValueOf} from 'type-fest'; import BlockingView from '@components/BlockingViews/BlockingView'; import * as Illustrations from '@components/Icon/Illustrations'; import RadioListItem from '@components/SelectionList/RadioListItem'; @@ -16,10 +15,7 @@ import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; - -type RouteParams = { - expenseType: ValueOf; -}; +import {ExpenseRouteParams} from '../types'; function NetSuiteExportExpensesVendorSelectPage({policy}: WithPolicyConnectionsProps) { const styles = useThemeStyles(); @@ -28,7 +24,7 @@ function NetSuiteExportExpensesVendorSelectPage({policy}: WithPolicyConnectionsP const policyID = policy?.id ?? '-1'; const route = useRoute(); - const params = route.params as RouteParams; + const params = route.params as ExpenseRouteParams; const isReimbursable = params.expenseType === CONST.NETSUITE_EXPENSE_TYPE.REIMBURSABLE; const config = policy?.connections?.netsuite.options.config; diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx index 034d0b641edd..6dcb438e3233 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx @@ -58,7 +58,9 @@ function NetSuiteInvoiceItemPreferenceSelectPage({policy}: WithPolicyConnections const headerContent = useMemo( () => ( - {translate(`workspace.netsuite.invoiceItem.values.${config?.invoiceItemPreference ?? 'create'}.description`)} + + {translate(`workspace.netsuite.invoiceItem.values.${config?.invoiceItemPreference ?? CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.CREATE}.description`)} + ), [styles.pb2, styles.ph5, styles.textNormal, translate, config?.invoiceItemPreference], diff --git a/src/pages/workspace/accounting/netsuite/types.ts b/src/pages/workspace/accounting/netsuite/types.ts new file mode 100644 index 000000000000..c1728f6d7854 --- /dev/null +++ b/src/pages/workspace/accounting/netsuite/types.ts @@ -0,0 +1,44 @@ +import {ValueOf} from 'type-fest'; +import type {MenuItemProps} from '@components/MenuItem'; +import type {OfflineWithFeedbackProps} from '@components/OfflineWithFeedback'; +import type {ToggleSettingOptionRowProps} from '@pages/workspace/workflows/ToggleSettingsOptionRow'; +import CONST from '@src/CONST'; + +type MenuItem = MenuItemProps & { + /** Type of the item */ + type: 'menuitem'; + + /** The type of action that's pending */ + pendingAction: OfflineWithFeedbackProps['pendingAction']; + + /** Whether the item should be hidden */ + shouldHide?: boolean; + + /** Any error message to show */ + errors: OfflineWithFeedbackProps['errors']; + + /** Callback to close the error messages */ + onCloseError: OfflineWithFeedbackProps['onClose']; +}; + +type DividerLineItem = { + /** Type of the item */ + type: 'divider'; + + /** Whether the item should be hidden */ + shouldHide?: boolean; +}; + +type ToggleItem = ToggleSettingOptionRowProps & { + /** Type of the item */ + type: 'toggle'; + + /** Whether the item should be hidden */ + shouldHide?: boolean; +}; + +type ExpenseRouteParams = { + expenseType: ValueOf; +}; + +export type {MenuItem, DividerLineItem, ToggleItem, ExpenseRouteParams}; From 469279d59f9a5066d23ef2c3c01f39cd4e4c64d6 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Thu, 27 Jun 2024 09:56:14 +0530 Subject: [PATCH 094/183] Applied suggestions --- .../accounting/netsuite/export/NetSuiteExportExpensesPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx index afedf086c1b4..264422dcbc72 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx @@ -49,7 +49,7 @@ function NetSuiteExportExpensesPage({policy}: WithPolicyConnectionsProps) { return (payableList ?? []).find((payableAccount) => payableAccount.id === config?.reimbursablePayableAccount); }, [payableList, config?.reimbursablePayableAccount]); - const isConnectionEmpty = isEmpty(policy?.connections?.netsuite); + const isConnectedToNetSuite = isEmpty(policy?.connections?.netsuite); const menuItems: MenuItemWithoutType[] = [ { @@ -110,7 +110,7 @@ function NetSuiteExportExpensesPage({policy}: WithPolicyConnectionsProps) { policyID={policyID} accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} - shouldBeBlocked={isConnectionEmpty} + shouldBeBlocked={isConnectedToNetSuite} > Date: Thu, 27 Jun 2024 10:15:25 +0530 Subject: [PATCH 095/183] Lint fix --- .../NetSuiteExportConfigurationPage.tsx | 24 +++++++++---------- ...iteExportExpensesDestinationSelectPage.tsx | 2 +- ...nsesJournalPostingPreferenceSelectPage.tsx | 2 +- .../export/NetSuiteExportExpensesPage.tsx | 17 ++++++------- ...ExportExpensesPayableAccountSelectPage.tsx | 2 +- ...NetSuiteExportExpensesVendorSelectPage.tsx | 2 +- .../workspace/accounting/netsuite/types.ts | 4 ++-- 7 files changed, 24 insertions(+), 29 deletions(-) diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx index a39614d5b5ea..d46e6bc42c0b 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx @@ -10,13 +10,13 @@ import * as Connections from '@libs/actions/connections/NetSuiteCommands'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {canUseProvincialTaxNetSuite, canUseTaxNetSuite} from '@libs/PolicyUtils'; +import type {DividerLineItem, MenuItem, ToggleItem} from '@pages/workspace/accounting/netsuite/types'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import type {DividerLineItem, MenuItem, ToggleItem} from '../types'; function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); @@ -28,21 +28,19 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) { const config = policy?.connections?.netsuite?.options.config; const {subsidiaryList, receivableList, taxAccountsList} = policy?.connections?.netsuite?.options?.data ?? {}; - const selectedSubsidiary = useMemo(() => { - return (subsidiaryList ?? []).find((subsidiary) => subsidiary.internalID === config?.subsidiaryID); - }, [subsidiaryList, config?.subsidiaryID]); + const selectedSubsidiary = useMemo(() => (subsidiaryList ?? []).find((subsidiary) => subsidiary.internalID === config?.subsidiaryID), [subsidiaryList, config?.subsidiaryID]); - const selectedReceivable = useMemo(() => { - return (receivableList ?? []).find((receivable) => receivable.id === config?.receivableAccount); - }, [receivableList, config?.receivableAccount]); + const selectedReceivable = useMemo(() => (receivableList ?? []).find((receivable) => receivable.id === config?.receivableAccount), [receivableList, config?.receivableAccount]); - const selectedTaxPostingAccount = useMemo(() => { - return (taxAccountsList ?? []).find((taxAccount) => taxAccount.externalID === config?.taxPostingAccount); - }, [taxAccountsList, config?.taxPostingAccount]); + const selectedTaxPostingAccount = useMemo( + () => (taxAccountsList ?? []).find((taxAccount) => taxAccount.externalID === config?.taxPostingAccount), + [taxAccountsList, config?.taxPostingAccount], + ); - const selectedProvTaxPostingAccount = useMemo(() => { - return (taxAccountsList ?? []).find((taxAccount) => taxAccount.externalID === config?.provincialTaxPostingAccount); - }, [taxAccountsList, config?.provincialTaxPostingAccount]); + const selectedProvTaxPostingAccount = useMemo( + () => (taxAccountsList ?? []).find((taxAccount) => taxAccount.externalID === config?.provincialTaxPostingAccount), + [taxAccountsList, config?.provincialTaxPostingAccount], + ); const menuItems: Array = [ { diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesDestinationSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesDestinationSelectPage.tsx index 0e70c17908e0..535951a51f87 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesDestinationSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesDestinationSelectPage.tsx @@ -8,11 +8,11 @@ import type {SelectorType} from '@components/SelectionScreen'; import useLocalize from '@hooks/useLocalize'; import * as Connections from '@libs/actions/connections/NetSuiteCommands'; import Navigation from '@navigation/Navigation'; +import type {ExpenseRouteParams} from '@pages/workspace/accounting/netsuite/types'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import {ExpenseRouteParams} from '../types'; type MenuListItem = ListItem & { value: ValueOf; diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx index 0a0b53ab16bd..3d01f96f9a64 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx @@ -8,11 +8,11 @@ import type {SelectorType} from '@components/SelectionScreen'; import useLocalize from '@hooks/useLocalize'; import * as Connections from '@libs/actions/connections/NetSuiteCommands'; import Navigation from '@navigation/Navigation'; +import type {ExpenseRouteParams} from '@pages/workspace/accounting/netsuite/types'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import {ExpenseRouteParams} from '../types'; type MenuListItem = ListItem & { value: ValueOf; diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx index 264422dcbc72..4d2bcc664e6f 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx @@ -10,12 +10,12 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import type {ExpenseRouteParams, MenuItem} from '@pages/workspace/accounting/netsuite/types'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import type {ExpenseRouteParams, MenuItem} from '../types'; type MenuItemWithoutType = Omit; @@ -37,17 +37,14 @@ function NetSuiteExportExpensesPage({policy}: WithPolicyConnectionsProps) { const {vendors, payableList} = policy?.connections?.netsuite?.options?.data ?? {}; - const defaultVendor = useMemo(() => { - return (vendors ?? []).find((vendor) => vendor.id === config?.defaultVendor); - }, [vendors, config?.defaultVendor]); + const defaultVendor = useMemo(() => (vendors ?? []).find((vendor) => vendor.id === config?.defaultVendor), [vendors, config?.defaultVendor]); - const selectedPayableAccount = useMemo(() => { - return (payableList ?? []).find((payableAccount) => payableAccount.id === config?.payableAcct); - }, [payableList, config?.payableAcct]); + const selectedPayableAccount = useMemo(() => (payableList ?? []).find((payableAccount) => payableAccount.id === config?.payableAcct), [payableList, config?.payableAcct]); - const selectedReimbursablePayableAccount = useMemo(() => { - return (payableList ?? []).find((payableAccount) => payableAccount.id === config?.reimbursablePayableAccount); - }, [payableList, config?.reimbursablePayableAccount]); + const selectedReimbursablePayableAccount = useMemo( + () => (payableList ?? []).find((payableAccount) => payableAccount.id === config?.reimbursablePayableAccount), + [payableList, config?.reimbursablePayableAccount], + ); const isConnectedToNetSuite = isEmpty(policy?.connections?.netsuite); diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPayableAccountSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPayableAccountSelectPage.tsx index 1486e7e861b0..c640e183bc99 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPayableAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPayableAccountSelectPage.tsx @@ -10,12 +10,12 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections/NetSuiteCommands'; import Navigation from '@libs/Navigation/Navigation'; import {getNetSuitePayableAccountOptions} from '@libs/PolicyUtils'; +import type {ExpenseRouteParams} from '@pages/workspace/accounting/netsuite/types'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import {ExpenseRouteParams} from '../types'; function NetSuiteExportExpensesPayableAccountSelectPage({policy}: WithPolicyConnectionsProps) { const styles = useThemeStyles(); diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx index 988246f125b7..9d4951c12bf0 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx @@ -10,12 +10,12 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections/NetSuiteCommands'; import Navigation from '@libs/Navigation/Navigation'; import {getNetSuiteVendorOptions} from '@libs/PolicyUtils'; +import type {ExpenseRouteParams} from '@pages/workspace/accounting/netsuite/types'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import {ExpenseRouteParams} from '../types'; function NetSuiteExportExpensesVendorSelectPage({policy}: WithPolicyConnectionsProps) { const styles = useThemeStyles(); diff --git a/src/pages/workspace/accounting/netsuite/types.ts b/src/pages/workspace/accounting/netsuite/types.ts index c1728f6d7854..b20b64ac3eb1 100644 --- a/src/pages/workspace/accounting/netsuite/types.ts +++ b/src/pages/workspace/accounting/netsuite/types.ts @@ -1,8 +1,8 @@ -import {ValueOf} from 'type-fest'; +import type {ValueOf} from 'type-fest'; import type {MenuItemProps} from '@components/MenuItem'; import type {OfflineWithFeedbackProps} from '@components/OfflineWithFeedback'; import type {ToggleSettingOptionRowProps} from '@pages/workspace/workflows/ToggleSettingsOptionRow'; -import CONST from '@src/CONST'; +import type CONST from '@src/CONST'; type MenuItem = MenuItemProps & { /** Type of the item */ From 4a0b60a2328f45a9f910d5eee329691bdf43f10e Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Thu, 27 Jun 2024 10:23:37 +0530 Subject: [PATCH 096/183] Lint fix --- .../netsuite/export/NetSuiteExportConfigurationPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx index d46e6bc42c0b..1ff610664e84 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx @@ -183,6 +183,7 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) { case 'divider': return ; case 'toggle': + // eslint-disable-next-line no-case-declarations const {type, shouldHide, ...rest} = item; return ( Date: Thu, 27 Jun 2024 10:35:11 +0530 Subject: [PATCH 097/183] Fix unique key issue --- .../export/NetSuiteExportConfigurationPage.tsx | 10 +++++++++- src/pages/workspace/accounting/netsuite/types.ts | 3 +++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx index 1ff610664e84..11cc2f54cce5 100644 --- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx @@ -57,6 +57,7 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) { }, { type: 'divider', + key: 'divider1', }, { type: 'menuitem', @@ -92,6 +93,7 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) { }, { type: 'divider', + key: 'divider2', }, { type: 'menuitem', @@ -115,6 +117,7 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) { }, { type: 'divider', + key: 'divider3', }, { type: 'menuitem', @@ -181,7 +184,12 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) { .map((item) => { switch (item.type) { case 'divider': - return ; + return ( + + ); case 'toggle': // eslint-disable-next-line no-case-declarations const {type, shouldHide, ...rest} = item; diff --git a/src/pages/workspace/accounting/netsuite/types.ts b/src/pages/workspace/accounting/netsuite/types.ts index b20b64ac3eb1..3d01d3a25efd 100644 --- a/src/pages/workspace/accounting/netsuite/types.ts +++ b/src/pages/workspace/accounting/netsuite/types.ts @@ -25,6 +25,9 @@ type DividerLineItem = { /** Type of the item */ type: 'divider'; + /** Unique key for the item */ + key: string; + /** Whether the item should be hidden */ shouldHide?: boolean; }; From 11f12fb2f077a749f61e6048235d75bf71cf5278 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 27 Jun 2024 10:47:15 +0200 Subject: [PATCH 098/183] fix: resolve comments --- src/hooks/useIsEligibleForRefund.ts | 14 -------------- .../Subscription/CardSection/CardSection.tsx | 10 +++++----- 2 files changed, 5 insertions(+), 19 deletions(-) delete mode 100644 src/hooks/useIsEligibleForRefund.ts diff --git a/src/hooks/useIsEligibleForRefund.ts b/src/hooks/useIsEligibleForRefund.ts deleted file mode 100644 index 7262d92bf6d8..000000000000 --- a/src/hooks/useIsEligibleForRefund.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {useOnyx} from 'react-native-onyx'; -import ONYXKEYS from '@src/ONYXKEYS'; - -function useIsEligibleForRefund(): boolean | undefined { - const [account] = useOnyx(ONYXKEYS.ACCOUNT); - - if (!account) { - return false; - } - - return account.isEligibleForRefund; -} - -export default useIsEligibleForRefund; diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 3862f2eaecd8..476c5ad8552e 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -7,7 +7,6 @@ import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import Section from '@components/Section'; import Text from '@components/Text'; -import useIsEligibleForRefund from '@hooks/useIsEligibleForRefund'; import useLocalize from '@hooks/useLocalize'; import useSubscriptionPlan from '@hooks/useSubscriptionPlan'; import useTheme from '@hooks/useTheme'; @@ -31,9 +30,9 @@ function CardSection() { const theme = useTheme(); const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); const subscriptionPlan = useSubscriptionPlan(); - const isEligibleForRefund = useIsEligibleForRefund(); const [account] = useOnyx(ONYXKEYS.ACCOUNT); const [privateSubscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION); + const [network] = useOnyx(ONYXKEYS.NETWORK); const defaultCard = useMemo(() => Object.values(fundList ?? {}).find((card) => card.isDefault), [fundList]); @@ -91,24 +90,25 @@ function CardSection() { wrapperStyle={styles.sectionMenuItemTopDescription} title={translate('subscription.cardSection.viewPaymentHistory')} titleStyle={styles.textStrong} + style={styles.mt5} onPress={() => Navigation.navigate(ROUTES.SEARCH.getRoute(CONST.SEARCH.TAB.ALL))} hoverAndPressStyle={styles.hoveredComponentBG} /> )} - {!!(subscriptionPlan && isEligibleForRefund) && ( + {!!(subscriptionPlan && account?.isEligibleForRefund) && ( setIsRequestRefundModalVisible(true)} /> )}
- {isEligibleForRefund && ( + {account?.isEligibleForRefund && ( Date: Thu, 27 Jun 2024 16:47:42 +0700 Subject: [PATCH 099/183] Fix: The entire page blinks when clicking Inbox --- .../createCustomBottomTabNavigator/BottomTabBar/index.tsx | 8 +++++++- .../BottomTabBar/index.website.tsx | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.tsx index 472d2c7d6d29..d1a4ebde0c84 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.tsx @@ -71,9 +71,12 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps const chatTabBrickRoad = getChatTabBrickRoad(activeWorkspaceID); const navigateToChats = useCallback(() => { + if (currentTabName === SCREENS.HOME) { + return; + } const route = activeWorkspaceID ? (`/w/${activeWorkspaceID}/home` as Route) : ROUTES.HOME; Navigation.navigate(route); - }, [activeWorkspaceID]); + }, [activeWorkspaceID, currentTabName]); return ( @@ -101,6 +104,9 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps { + if (currentTabName === SCREENS.SEARCH.BOTTOM_TAB || currentTabName === SCREENS.SEARCH.CENTRAL_PANE) { + return; + } interceptAnonymousUser(() => Navigation.navigate(ROUTES.SEARCH.getRoute(CONST.SEARCH.TAB.ALL))); }} role={CONST.ROLE.BUTTON} diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx index 9fe78273bdb0..ecacedde16dd 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx @@ -72,9 +72,12 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps const chatTabBrickRoad = getChatTabBrickRoad(activeWorkspaceID); const navigateToChats = useCallback(() => { + if (currentTabName === SCREENS.HOME) { + return; + } const route = activeWorkspaceID ? (`/w/${activeWorkspaceID}/home` as Route) : ROUTES.HOME; Navigation.navigate(route); - }, [activeWorkspaceID]); + }, [activeWorkspaceID, currentTabName]); return ( @@ -102,6 +105,9 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps { + if (currentTabName === SCREENS.SEARCH.BOTTOM_TAB || currentTabName === SCREENS.SEARCH.CENTRAL_PANE) { + return; + } interceptAnonymousUser(() => Navigation.navigate(ROUTES.SEARCH.getRoute(CONST.SEARCH.TAB.ALL))); }} role={CONST.ROLE.BUTTON} From 0900cb2fc14584c9e55617b5b6d09382951f6299 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Thu, 27 Jun 2024 12:51:50 +0200 Subject: [PATCH 100/183] apply suggested changes --- .github/libs/sanitizeStringForJSONParse.ts | 2 +- src/libs/SubscriptionUtils.ts | 12 ++++++++++-- src/types/onyx/BillingStatus.ts | 4 ++-- src/types/onyx/StripeCustomerID.ts | 2 +- tests/unit/SubscriptionUtilsTest.ts | 4 ---- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/.github/libs/sanitizeStringForJSONParse.ts b/.github/libs/sanitizeStringForJSONParse.ts index d646a566c517..ddb7549b0186 100644 --- a/.github/libs/sanitizeStringForJSONParse.ts +++ b/.github/libs/sanitizeStringForJSONParse.ts @@ -8,7 +8,7 @@ const replacer = (str: string): string => '\r': '\\r', '\f': '\\f', '"': '\\"', - })[str] ?? ''; + }[str] ?? ''); /** * Replace any characters in the string that will break JSON.parse for our Git Log output diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 3df2fae93598..525092ad2a0d 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -197,7 +197,7 @@ function getCardForSubscriptionBilling(): Fund | undefined { * @returns Whether the card is due to expire soon. */ function hasCardExpiringSoon(): boolean { - if (!isEmptyObject(billingStatus ?? {})) { + if (!isEmptyObject(billingStatus)) { return false; } @@ -207,7 +207,15 @@ function hasCardExpiringSoon(): boolean { return false; } - return card?.accountData?.cardYear === new Date().getFullYear() && card?.accountData?.cardMonth === new Date().getMonth() + 1; + const cardYear = card?.accountData?.cardYear; + const cardMonth = card?.accountData?.cardMonth; + const currentYear = new Date().getFullYear(); + const currentMonth = new Date().getMonth(); + + const isExpiringThisMonth = cardYear === currentYear && cardMonth === currentMonth; + const isExpiringNextMonth = cardYear === (currentMonth === 12 ? currentYear + 1 : currentYear) && cardMonth === (currentMonth === 12 ? 1 : currentMonth + 1); + + return isExpiringThisMonth || isExpiringNextMonth; } /** diff --git a/src/types/onyx/BillingStatus.ts b/src/types/onyx/BillingStatus.ts index 620a5eb3e2df..a28c5b5f770a 100644 --- a/src/types/onyx/BillingStatus.ts +++ b/src/types/onyx/BillingStatus.ts @@ -3,10 +3,10 @@ type BillingStatus = { /** Status action */ action: string; - /** Billing's period month */ + /** Billing period month */ periodMonth: string; - /** Billing's period year */ + /** Billing period year */ periodYear: string; /** Decline reason */ diff --git a/src/types/onyx/StripeCustomerID.ts b/src/types/onyx/StripeCustomerID.ts index 0790b12c5c69..292cb517e2a6 100644 --- a/src/types/onyx/StripeCustomerID.ts +++ b/src/types/onyx/StripeCustomerID.ts @@ -1,4 +1,4 @@ -/** Model of stripe customer */ +/** Model of Stripe customer */ type StripeCustomerID = { /** Payment method's ID */ paymentMethodID: string; diff --git a/tests/unit/SubscriptionUtilsTest.ts b/tests/unit/SubscriptionUtilsTest.ts index c234c1e55658..00cc793f9444 100644 --- a/tests/unit/SubscriptionUtilsTest.ts +++ b/tests/unit/SubscriptionUtilsTest.ts @@ -276,10 +276,6 @@ describe('SubscriptionUtils', () => { }); describe('getSubscriptionStatus', () => { - afterEach(() => { - // Onyx.clear(); - }); - it('should return undefined by default', () => { expect(SubscriptionUtils.getSubscriptionStatus()).toBeUndefined(); }); From ae82d068ce27e5d0485b2eb22b5472affb11764c Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Thu, 27 Jun 2024 13:25:50 +0200 Subject: [PATCH 101/183] feat: subscription localize currency --- .../useSubscriptionPossibleCostSavings.ts | 38 +++++++++++++++++++ src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- .../SubscriptionSettings/index.tsx | 11 +++--- 4 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 src/hooks/useSubscriptionPossibleCostSavings.ts diff --git a/src/hooks/useSubscriptionPossibleCostSavings.ts b/src/hooks/useSubscriptionPossibleCostSavings.ts new file mode 100644 index 000000000000..ef92009549fe --- /dev/null +++ b/src/hooks/useSubscriptionPossibleCostSavings.ts @@ -0,0 +1,38 @@ +import {useOnyx} from 'react-native-onyx'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import usePreferredCurrency from './usePreferredCurrency'; +import useSubscriptionPlan from './useSubscriptionPlan'; + +const POSSIBLE_COST_SAVINGS = { + [CONST.PAYMENT_CARD_CURRENCY.USD]: { + [CONST.POLICY.TYPE.TEAM]: 1000, + [CONST.POLICY.TYPE.CORPORATE]: 1800, + }, + [CONST.PAYMENT_CARD_CURRENCY.AUD]: { + [CONST.POLICY.TYPE.TEAM]: 1400, + [CONST.POLICY.TYPE.CORPORATE]: 3000, + }, + [CONST.PAYMENT_CARD_CURRENCY.GBP]: { + [CONST.POLICY.TYPE.TEAM]: 800, + [CONST.POLICY.TYPE.CORPORATE]: 1400, + }, + [CONST.PAYMENT_CARD_CURRENCY.NZD]: { + [CONST.POLICY.TYPE.TEAM]: 1600, + [CONST.POLICY.TYPE.CORPORATE]: 3200, + }, +} as const; + +function useSubscriptionPossibleCostSavings(): number { + const preferredCurrency = usePreferredCurrency(); + const subscriptionPlan = useSubscriptionPlan(); + const [privateSubscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION); + + if (!subscriptionPlan || !privateSubscription?.type) { + return 0; + } + + return POSSIBLE_COST_SAVINGS[preferredCurrency][subscriptionPlan]; +} + +export default useSubscriptionPossibleCostSavings; diff --git a/src/languages/en.ts b/src/languages/en.ts index a864e70b6189..035a06c5784c 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3376,7 +3376,7 @@ export default { title: 'Subscription settings', autoRenew: 'Auto-renew', autoIncrease: 'Auto-increase annual seats', - saveUpTo: ({amountSaved}) => `Save up to $${amountSaved}/month per active member`, + saveUpTo: ({amountWithCurrency}) => `Save up to ${amountWithCurrency}/month per active member`, automaticallyIncrease: 'Automatically increase your annual seats to accommodate for active members that exceed your subscription size. Note: This will extend your annual subscription end date.', disableAutoRenew: 'Disable auto-renew', diff --git a/src/languages/es.ts b/src/languages/es.ts index 2a11549e7355..ee83fe1ea880 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3879,7 +3879,7 @@ export default { title: 'Configuración de suscripción', autoRenew: 'Auto-renovación', autoIncrease: 'Auto-incremento', - saveUpTo: ({amountSaved}) => `Ahorre hasta $${amountSaved} al mes por miembro activo`, + saveUpTo: ({amountWithCurrency}) => `Ahorre hasta ${amountWithCurrency} al mes por miembro activo`, automaticallyIncrease: 'Aumenta automáticamente tus plazas anuales para dar lugar a los miembros activos que superen el tamaño de tu suscripción. Nota: Esto ampliará la fecha de finalización de tu suscripción anual.', disableAutoRenew: 'Desactivar auto-renovación', diff --git a/src/pages/settings/Subscription/SubscriptionSettings/index.tsx b/src/pages/settings/Subscription/SubscriptionSettings/index.tsx index 1b8bc13b0a76..247d82ef8ca3 100644 --- a/src/pages/settings/Subscription/SubscriptionSettings/index.tsx +++ b/src/pages/settings/Subscription/SubscriptionSettings/index.tsx @@ -6,9 +6,11 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Section from '@components/Section'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; -import useSubscriptionPlan from '@hooks/useSubscriptionPlan'; +import usePreferredCurrency from '@hooks/usePreferredCurrency'; +import useSubscriptionPossibleCostSavings from '@hooks/useSubscriptionPossibleCostSavings'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import {convertToShortDisplayString} from '@libs/CurrencyUtils'; import Navigation from '@navigation/Navigation'; import {formatSubscriptionEndDate} from '@pages/settings/Subscription/utils'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -21,10 +23,9 @@ function SubscriptionSettings() { const {translate} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); - const subscriptionPlan = useSubscriptionPlan(); const [privateSubscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION); - - const isCollect = subscriptionPlan === CONST.POLICY.TYPE.TEAM; + const preferredCurrency = usePreferredCurrency(); + const possibleCostSavings = useSubscriptionPossibleCostSavings(); const autoRenewalDate = formatSubscriptionEndDate(privateSubscription?.endDate); @@ -50,7 +51,7 @@ function SubscriptionSettings() { {translate('subscription.subscriptionSettings.autoIncrease')} {translate('subscription.subscriptionSettings.saveUpTo', { - amountSaved: isCollect ? CONST.SUBSCRIPTION_POSSIBLE_COST_SAVINGS.COLLECT_PLAN : CONST.SUBSCRIPTION_POSSIBLE_COST_SAVINGS.CONTROL_PLAN, + amountWithCurrency: convertToShortDisplayString(possibleCostSavings, preferredCurrency), })} From 74bd4de32bdcfea44dd792829dac85f10eea0488 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Thu, 27 Jun 2024 13:29:30 +0200 Subject: [PATCH 102/183] fix code linting --- src/hooks/useStepFormSubmit.ts | 11 +-- src/languages/types.ts | 20 ++--- src/libs/Localize/index.ts | 11 +-- src/libs/actions/Policy/Policy.ts | 28 +++---- src/libs/migrations/RenameCardIsVirtual.ts | 27 +++---- src/libs/migrations/RenameReceiptFilename.ts | 23 +++--- .../EnablePayments/utils/getSubstepValues.ts | 11 +-- .../BeneficialOwnersStep.tsx | 11 +-- .../report/ReportActionsListItemRenderer.tsx | 2 +- .../Subscription/CardSection/CardSection.tsx | 2 +- src/styles/index.ts | 76 +++++++++---------- src/types/onyx/SearchResults.ts | 8 +- .../ReportActionCompose.perf-test.tsx | 2 +- tests/unit/DateUtilsTest.ts | 4 +- tests/utils/ReportTestUtils.ts | 2 +- tests/utils/TestHelper.ts | 11 +-- 16 files changed, 111 insertions(+), 138 deletions(-) diff --git a/src/hooks/useStepFormSubmit.ts b/src/hooks/useStepFormSubmit.ts index cd422071c725..883fe980a338 100644 --- a/src/hooks/useStepFormSubmit.ts +++ b/src/hooks/useStepFormSubmit.ts @@ -24,13 +24,10 @@ export default function useStepFormSubmit return useCallback( (values: FormOnyxValues) => { if (shouldSaveDraft) { - const stepValues = fieldIds.reduce( - (acc, key) => { - acc[key] = values[key]; - return acc; - }, - {} as Record, OnyxValues[T][Exclude]>, - ); + const stepValues = fieldIds.reduce((acc, key) => { + acc[key] = values[key]; + return acc; + }, {} as Record, OnyxValues[T][Exclude]>); FormActions.setDraftValues(formId, stepValues); } diff --git a/src/languages/types.ts b/src/languages/types.ts index e96b3f6bd4d3..de9b1d2dadeb 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -261,22 +261,22 @@ type FlattenObject = { [TKey in keyof TObject]: TObject[TKey] extends (...args: any[]) => any ? `${TPrefix}${TKey & string}` : // eslint-disable-next-line @typescript-eslint/no-explicit-any - TObject[TKey] extends any[] - ? `${TPrefix}${TKey & string}` - : // eslint-disable-next-line @typescript-eslint/ban-types - TObject[TKey] extends object - ? FlattenObject - : `${TPrefix}${TKey & string}`; + TObject[TKey] extends any[] + ? `${TPrefix}${TKey & string}` + : // eslint-disable-next-line @typescript-eslint/ban-types + TObject[TKey] extends object + ? FlattenObject + : `${TPrefix}${TKey & string}`; }[keyof TObject]; // Retrieves a type for a given key path (calculated from the type above) type TranslateType = TPath extends keyof TObject ? TObject[TPath] : TPath extends `${infer TKey}.${infer TRest}` - ? TKey extends keyof TObject - ? TranslateType - : never - : never; + ? TKey extends keyof TObject + ? TranslateType + : never + : never; type EnglishTranslation = typeof en; diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index dd34175e6ad1..c9eef3170245 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -64,13 +64,10 @@ type Phrase = TranslationFlatObject[TKey] extends * in our cache. */ const translationCache = new Map, Map>( - Object.values(CONST.LOCALES).reduce( - (cache, locale) => { - cache.push([locale, new Map()]); - return cache; - }, - [] as Array<[ValueOf, Map]>, - ), + Object.values(CONST.LOCALES).reduce((cache, locale) => { + cache.push([locale, new Map()]); + return cache; + }, [] as Array<[ValueOf, Map]>), ); /** diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index ed601408f7b6..270dc8d50759 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -2518,16 +2518,13 @@ function enablePolicyTaxes(policyID: string, enabled: boolean) { taxRates: { ...defaultTaxRates, taxes: { - ...Object.keys(defaultTaxRates.taxes).reduce( - (acc, taxKey) => { - acc[taxKey] = { - ...defaultTaxRates.taxes[taxKey], - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }; - return acc; - }, - {} as Record, - ), + ...Object.keys(defaultTaxRates.taxes).reduce((acc, taxKey) => { + acc[taxKey] = { + ...defaultTaxRates.taxes[taxKey], + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }; + return acc; + }, {} as Record), }, }, }, @@ -2540,13 +2537,10 @@ function enablePolicyTaxes(policyID: string, enabled: boolean) { value: { taxRates: { taxes: { - ...Object.keys(defaultTaxRates.taxes).reduce( - (acc, taxKey) => { - acc[taxKey] = {pendingAction: null}; - return acc; - }, - {} as Record, - ), + ...Object.keys(defaultTaxRates.taxes).reduce((acc, taxKey) => { + acc[taxKey] = {pendingAction: null}; + return acc; + }, {} as Record), }, }, }, diff --git a/src/libs/migrations/RenameCardIsVirtual.ts b/src/libs/migrations/RenameCardIsVirtual.ts index acd99e5bdf1c..751c11d70eac 100644 --- a/src/libs/migrations/RenameCardIsVirtual.ts +++ b/src/libs/migrations/RenameCardIsVirtual.ts @@ -26,23 +26,20 @@ export default function () { } Log.info('[Migrate Onyx] Running RenameCardIsVirtual migration'); - const dataToSave = cardsWithIsVirtualProp.reduce( - (acc, card) => { - if (!card) { - return acc; - } + const dataToSave = cardsWithIsVirtualProp.reduce((acc, card) => { + if (!card) { + return acc; + } - acc[card.cardID] = { - nameValuePairs: { - isVirtual: card?.nameValuePairs?.isVirtual, - }, - isVirtual: undefined, - }; + acc[card.cardID] = { + nameValuePairs: { + isVirtual: card?.nameValuePairs?.isVirtual, + }, + isVirtual: undefined, + }; - return acc; - }, - {} as Record>, - ); + return acc; + }, {} as Record>); // eslint-disable-next-line rulesdir/prefer-actions-set-data Onyx.merge(ONYXKEYS.CARD_LIST, dataToSave).then(() => { diff --git a/src/libs/migrations/RenameReceiptFilename.ts b/src/libs/migrations/RenameReceiptFilename.ts index 2e27802e72d4..f01676595dd7 100644 --- a/src/libs/migrations/RenameReceiptFilename.ts +++ b/src/libs/migrations/RenameReceiptFilename.ts @@ -30,20 +30,17 @@ export default function () { return resolve(); } Log.info('[Migrate Onyx] Running RenameReceiptFilename migration'); - const dataToSave = transactionsWithReceipt?.reduce( - (acc, transaction) => { - if (!transaction) { - return acc; - } - Log.info(`[Migrate Onyx] Renaming receiptFilename ${transaction.receiptFilename} to filename`); - acc[`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`] = { - filename: transaction.receiptFilename, - receiptFilename: null, - }; + const dataToSave = transactionsWithReceipt?.reduce((acc, transaction) => { + if (!transaction) { return acc; - }, - {} as Record>, - ); + } + Log.info(`[Migrate Onyx] Renaming receiptFilename ${transaction.receiptFilename} to filename`); + acc[`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`] = { + filename: transaction.receiptFilename, + receiptFilename: null, + }; + return acc; + }, {} as Record>); // eslint-disable-next-line rulesdir/prefer-actions-set-data Onyx.mergeCollection(ONYXKEYS.COLLECTION.TRANSACTION, dataToSave).then(() => { diff --git a/src/pages/EnablePayments/utils/getSubstepValues.ts b/src/pages/EnablePayments/utils/getSubstepValues.ts index 4e444b1b53b1..2eb80ed1b2ee 100644 --- a/src/pages/EnablePayments/utils/getSubstepValues.ts +++ b/src/pages/EnablePayments/utils/getSubstepValues.ts @@ -8,13 +8,10 @@ function getSubstepValues( walletAdditionalDetailsDraft: OnyxEntry, walletAdditionalDetails: OnyxEntry, ): {[K in T]: WalletAdditionalDetailsForm[K] | string} { - return Object.entries(inputKeys).reduce( - (acc, [, value]) => { - acc[value] = walletAdditionalDetailsDraft?.[value] ?? walletAdditionalDetails?.[value as keyof PersonalInfoStepProps] ?? ''; - return acc; - }, - {} as {[K in T]: WalletAdditionalDetailsForm[K] | string}, - ); + return Object.entries(inputKeys).reduce((acc, [, value]) => { + acc[value] = walletAdditionalDetailsDraft?.[value] ?? walletAdditionalDetails?.[value as keyof PersonalInfoStepProps] ?? ''; + return acc; + }, {} as {[K in T]: WalletAdditionalDetailsForm[K] | string}); } export default getSubstepValues; diff --git a/src/pages/ReimbursementAccount/BeneficialOwnersStep.tsx b/src/pages/ReimbursementAccount/BeneficialOwnersStep.tsx index c745431290fb..ae0fded74347 100644 --- a/src/pages/ReimbursementAccount/BeneficialOwnersStep.tsx +++ b/src/pages/ReimbursementAccount/BeneficialOwnersStep.tsx @@ -68,13 +68,10 @@ function BeneficialOwnersStep({reimbursementAccount, reimbursementAccountDraft, const submit = () => { const beneficialOwnerFields = ['firstName', 'lastName', 'dob', 'ssnLast4', 'street', 'city', 'state', 'zipCode']; const beneficialOwners = beneficialOwnerKeys.map((ownerKey) => - beneficialOwnerFields.reduce( - (acc, fieldName) => { - acc[fieldName] = reimbursementAccountDraft ? reimbursementAccountDraft[`beneficialOwner_${ownerKey}_${fieldName}`] : undefined; - return acc; - }, - {} as Record, - ), + beneficialOwnerFields.reduce((acc, fieldName) => { + acc[fieldName] = reimbursementAccountDraft ? reimbursementAccountDraft[`beneficialOwner_${ownerKey}_${fieldName}`] : undefined; + return acc; + }, {} as Record), ); BankAccounts.updateBeneficialOwnersForBankAccount( diff --git a/src/pages/home/report/ReportActionsListItemRenderer.tsx b/src/pages/home/report/ReportActionsListItemRenderer.tsx index 103859e0bffc..e433ff00e1a1 100644 --- a/src/pages/home/report/ReportActionsListItemRenderer.tsx +++ b/src/pages/home/report/ReportActionsListItemRenderer.tsx @@ -109,7 +109,7 @@ function ReportActionsListItemRenderer({ childManagerAccountID: reportAction.childManagerAccountID, childMoneyRequestCount: reportAction.childMoneyRequestCount, childOwnerAccountID: reportAction.childOwnerAccountID, - }) as ReportAction, + } as ReportAction), [ reportAction.reportActionID, reportAction.message, diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 583a953db7b6..d494312fc928 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -10,8 +10,8 @@ import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; -import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import Navigation from '@libs/Navigation/Navigation'; +import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; diff --git a/src/styles/index.ts b/src/styles/index.ts index f746d89edb15..7aacb77965c7 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -108,14 +108,14 @@ const picker = (theme: ThemeColors) => height: variables.inputHeight, borderWidth: 0, textAlign: 'left', - }) satisfies TextStyle; + } satisfies TextStyle); const link = (theme: ThemeColors) => ({ color: theme.link, textDecorationColor: theme.link, fontFamily: FontUtils.fontFamily.platform.EXP_NEUE, - }) satisfies ViewStyle & MixedStyleDeclaration; + } satisfies ViewStyle & MixedStyleDeclaration); const baseCodeTagStyles = (theme: ThemeColors) => ({ @@ -123,7 +123,7 @@ const baseCodeTagStyles = (theme: ThemeColors) => borderRadius: 5, borderColor: theme.border, backgroundColor: theme.textBackground, - }) satisfies ViewStyle & MixedStyleDeclaration; + } satisfies ViewStyle & MixedStyleDeclaration); const headlineFont = { fontFamily: FontUtils.fontFamily.platform.EXP_NEW_KANSAS_MEDIUM, @@ -135,7 +135,7 @@ const modalNavigatorContainer = (isSmallScreenWidth: boolean) => position: 'absolute', width: isSmallScreenWidth ? '100%' : variables.sideBarWidth, height: '100%', - }) satisfies ViewStyle; + } satisfies ViewStyle); const webViewStyles = (theme: ThemeColors) => ({ @@ -237,7 +237,7 @@ const webViewStyles = (theme: ThemeColors) => lineHeight: variables.fontSizeNormalHeight, ...writingDirection.ltr, }, - }) satisfies WebViewStyle; + } satisfies WebViewStyle); const styles = (theme: ThemeColors) => ({ @@ -859,7 +859,7 @@ const styles = (theme: ThemeColors) => pointerEvents: 'none', opacity: 0, }, - }) satisfies CustomPickerStyle, + } satisfies CustomPickerStyle), badge: { backgroundColor: theme.border, @@ -995,7 +995,7 @@ const styles = (theme: ThemeColors) => paddingVertical: 40, gap: 4, flex: 1, - }) satisfies ViewStyle, + } satisfies ViewStyle), receiptViewTextContainer: { paddingHorizontal: 40, @@ -1136,14 +1136,14 @@ const styles = (theme: ThemeColors) => ({ height: lodashClamp(textInputHeight, minHeight, maxHeight), minHeight, - }) satisfies ViewStyle, + } satisfies ViewStyle), autoGrowHeightHiddenInput: (maxWidth: number, maxHeight?: number) => ({ maxWidth, maxHeight: maxHeight && maxHeight + 1, overflow: 'hidden', - }) satisfies TextStyle, + } satisfies TextStyle), textInputContainer: { flex: 1, @@ -1185,7 +1185,7 @@ const styles = (theme: ThemeColors) => textInputLabelTransformation: (translateY: AnimatableNumericValue, translateX: AnimatableNumericValue, scale: AnimatableNumericValue) => ({ transform: [{translateY}, {translateX}, {scale}], - }) satisfies TextStyle, + } satisfies TextStyle), baseTextInput: { fontFamily: FontUtils.fontFamily.platform.EXP_NEUE, @@ -1324,7 +1324,7 @@ const styles = (theme: ThemeColors) => inputAndroid: { ...picker(theme), }, - }) satisfies CustomPickerStyle, + } satisfies CustomPickerStyle), disabledText: { color: theme.icon, @@ -1563,7 +1563,7 @@ const styles = (theme: ThemeColors) => height: 12, width: 12, zIndex: 10, - }) satisfies ViewStyle, + } satisfies ViewStyle), bottomTabStatusIndicator: (backgroundColor = theme.danger) => ({ borderColor: theme.sidebar, @@ -1643,13 +1643,13 @@ const styles = (theme: ThemeColors) => ({ ...modalNavigatorContainer(isSmallScreenWidth), left: 0, - }) satisfies ViewStyle, + } satisfies ViewStyle), RHPNavigatorContainer: (isSmallScreenWidth: boolean) => ({ ...modalNavigatorContainer(isSmallScreenWidth), right: 0, - }) satisfies ViewStyle, + } satisfies ViewStyle), onboardingNavigatorOuterView: { flex: 1, @@ -1664,7 +1664,7 @@ const styles = (theme: ThemeColors) => maxHeight: '100%', borderRadius: shouldUseNarrowLayout ? 16 : 0, overflow: 'hidden', - }) satisfies ViewStyle, + } satisfies ViewStyle), welcomeVideoNarrowLayout: { width: variables.onboardingModalWidth, @@ -1685,20 +1685,20 @@ const styles = (theme: ThemeColors) => // Menu should be displayed 12px above the floating action button. // To achieve that sidebar must be moved by: distance from the bottom of the sidebar to the fab (variables.fabBottom) + fab height (variables.componentSizeLarge) + distance above the fab (12px) vertical: windowHeight - (variables.fabBottom + variables.componentSizeLarge + 12), - }) satisfies AnchorPosition, + } satisfies AnchorPosition), createAccountMenuPositionProfile: () => ({ horizontal: 18, ...getPopOverVerticalOffset(202 + 40), - }) satisfies AnchorPosition, + } satisfies AnchorPosition), createMenuPositionReportActionCompose: (shouldUseNarrowLayout: boolean, windowHeight: number, windowWidth: number) => ({ // On a narrow layout the menu is displayed in ReportScreen in RHP, so it must be moved from the right side of the screen horizontal: (shouldUseNarrowLayout ? windowWidth - variables.sideBarWidth : variables.sideBarWidth) + 18, vertical: windowHeight - CONST.MENU_POSITION_REPORT_ACTION_COMPOSE_BOTTOM, - }) satisfies AnchorPosition, + } satisfies AnchorPosition), createMenuPositionRightSidepane: { right: 18, @@ -1908,7 +1908,7 @@ const styles = (theme: ThemeColors) => outputRange: [0, variables.overlayOpacity], extrapolate: 'clamp', }), - }) satisfies ViewStyle, + } satisfies ViewStyle), nativeOverlayStyles: (current: OverlayStylesParams) => ({ @@ -1921,7 +1921,7 @@ const styles = (theme: ThemeColors) => outputRange: [0, variables.overlayOpacity], extrapolate: 'clamp', }), - }) satisfies ViewStyle, + } satisfies ViewStyle), appContent: { backgroundColor: theme.appBG, @@ -2508,13 +2508,13 @@ const styles = (theme: ThemeColors) => flexBasis: isSmallScreenWidth ? '100%' : 350, flexGrow: 0, alignSelf: 'flex-start', - }) satisfies ViewStyle, + } satisfies ViewStyle), centeredModalStyles: (isSmallScreenWidth: boolean, isFullScreenWhenSmall: boolean) => ({ borderWidth: isSmallScreenWidth && !isFullScreenWhenSmall ? 1 : 0, marginHorizontal: isSmallScreenWidth ? 0 : 20, - }) satisfies ViewStyle, + } satisfies ViewStyle), imageModalImageCenterContainer: { alignItems: 'center', @@ -2678,7 +2678,7 @@ const styles = (theme: ThemeColors) => backgroundColor: theme.cardBG, borderRadius: variables.componentBorderRadiusLarge, overflow: 'hidden', - }) satisfies ViewStyle & TextStyle, + } satisfies ViewStyle & TextStyle), anonymousRoomFooterWordmarkAndLogoContainer: (isSmallSizeLayout: boolean) => ({ flexDirection: 'row', @@ -2687,7 +2687,7 @@ const styles = (theme: ThemeColors) => justifyContent: 'space-between', marginTop: 16, }), - }) satisfies ViewStyle, + } satisfies ViewStyle), anonymousRoomFooterLogo: { width: 88, marginLeft: 0, @@ -2729,8 +2729,8 @@ const styles = (theme: ThemeColors) => borderRadius: 88, }, - rootNavigatorContainerStyles: (isSmallScreenWidth: boolean) => ({marginLeft: isSmallScreenWidth ? 0 : variables.sideBarWidth, flex: 1}) satisfies ViewStyle, - RHPNavigatorContainerNavigatorContainerStyles: (isSmallScreenWidth: boolean) => ({marginLeft: isSmallScreenWidth ? 0 : variables.sideBarWidth, flex: 1}) satisfies ViewStyle, + rootNavigatorContainerStyles: (isSmallScreenWidth: boolean) => ({marginLeft: isSmallScreenWidth ? 0 : variables.sideBarWidth, flex: 1} satisfies ViewStyle), + RHPNavigatorContainerNavigatorContainerStyles: (isSmallScreenWidth: boolean) => ({marginLeft: isSmallScreenWidth ? 0 : variables.sideBarWidth, flex: 1} satisfies ViewStyle), avatarInnerTextChat: { color: theme.text, @@ -3012,7 +3012,7 @@ const styles = (theme: ThemeColors) => switchThumbTransformation: (translateX: AnimatableNumericValue) => ({ transform: [{translateX}], - }) satisfies ViewStyle, + } satisfies ViewStyle), radioButtonContainer: { backgroundColor: theme.componentBG, @@ -3239,7 +3239,7 @@ const styles = (theme: ThemeColors) => growlNotificationTranslateY: (translateY: AnimatableNumericValue) => ({ transform: [{translateY}], - }) satisfies ViewStyle, + } satisfies ViewStyle), makeSlideInTranslation: (translationType: Translation, fromValue: number) => ({ @@ -3249,7 +3249,7 @@ const styles = (theme: ThemeColors) => to: { [translationType]: 0, }, - }) satisfies CustomAnimation, + } satisfies CustomAnimation), growlNotificationBox: { backgroundColor: theme.inverse, @@ -3558,25 +3558,25 @@ const styles = (theme: ThemeColors) => ({ ...getPopOverVerticalOffset(60), horizontal: windowWidth - 60, - }) satisfies AnchorPosition, + } satisfies AnchorPosition), threeDotsPopoverOffsetNoCloseButton: (windowWidth: number) => ({ ...getPopOverVerticalOffset(60), horizontal: windowWidth - 10, - }) satisfies AnchorPosition, + } satisfies AnchorPosition), threeDotsPopoverOffsetAttachmentModal: (windowWidth: number) => ({ ...getPopOverVerticalOffset(80), horizontal: windowWidth - 140, - }) satisfies AnchorPosition, + } satisfies AnchorPosition), popoverMenuOffset: (windowWidth: number) => ({ ...getPopOverVerticalOffset(180), horizontal: windowWidth - 355, - }) satisfies AnchorPosition, + } satisfies AnchorPosition), iPhoneXSafeArea: { backgroundColor: theme.inverse, @@ -3680,7 +3680,7 @@ const styles = (theme: ThemeColors) => ({ position: 'absolute', top: receiptImageTopPosition, - }) satisfies ViewStyle, + } satisfies ViewStyle), cardSectionContainer: { backgroundColor: theme.cardBG, @@ -3807,7 +3807,7 @@ const styles = (theme: ThemeColors) => position: 'absolute', width: isSmallScreenWidth ? windowWidth - 32 : CONST.EMOJI_PICKER_SIZE.WIDTH - 32, ...spacing.mh4, - }) satisfies ViewStyle, + } satisfies ViewStyle), reactionCounterText: { fontSize: 13, @@ -4209,7 +4209,7 @@ const styles = (theme: ThemeColors) => color: isSelected ? theme.text : theme.textSupporting, lineHeight: variables.lineHeightNormal, fontSize: variables.fontSizeNormal, - }) satisfies TextStyle, + } satisfies TextStyle), tabBackground: (hovered: boolean, isFocused: boolean, background: string | Animated.AnimatedInterpolation) => ({ backgroundColor: hovered && !isFocused ? theme.highlightBG : background, @@ -4233,7 +4233,7 @@ const styles = (theme: ThemeColors) => top: -height, left: 0, right: 0, - }) satisfies ViewStyle, + } satisfies ViewStyle), dualColorOverscrollSpacer: { position: 'absolute', @@ -5032,7 +5032,7 @@ const styles = (theme: ThemeColors) => fontSize: variables.fontSizeNormal, fontWeight: FontUtils.fontWeight.bold, }, - }) satisfies Styles; + } satisfies Styles); type ThemeStyles = ReturnType; diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index 75323b3f30f0..3932752f4325 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -12,15 +12,15 @@ type SearchDataTypes = ValueOf; type ListItemType = T extends typeof CONST.SEARCH.DATA_TYPES.TRANSACTION ? typeof TransactionListItem : T extends typeof CONST.SEARCH.DATA_TYPES.REPORT - ? typeof ReportListItem - : never; + ? typeof ReportListItem + : never; /** Model of search result section */ type SectionsType = T extends typeof CONST.SEARCH.DATA_TYPES.TRANSACTION ? TransactionListItemType[] : T extends typeof CONST.SEARCH.DATA_TYPES.REPORT - ? ReportListItemType[] - : never; + ? ReportListItemType[] + : never; /** Mapping of search results to list item */ type SearchTypeToItemMap = { diff --git a/tests/perf-test/ReportActionCompose.perf-test.tsx b/tests/perf-test/ReportActionCompose.perf-test.tsx index 1387ddf1366a..9c6cfdae5a5c 100644 --- a/tests/perf-test/ReportActionCompose.perf-test.tsx +++ b/tests/perf-test/ReportActionCompose.perf-test.tsx @@ -27,7 +27,7 @@ jest.mock( ({ ...jest.requireActual('react-native-reanimated/mock'), useAnimatedRef: jest.fn(), - }) as typeof Animated, + } as typeof Animated), ); jest.mock('@react-navigation/native', () => { diff --git a/tests/unit/DateUtilsTest.ts b/tests/unit/DateUtilsTest.ts index 2c68d5285770..9df0113168e4 100644 --- a/tests/unit/DateUtilsTest.ts +++ b/tests/unit/DateUtilsTest.ts @@ -94,7 +94,7 @@ describe('DateUtils', () => { () => ({ resolvedOptions: () => ({timeZone: 'America/Chicago'}), - }) as Intl.DateTimeFormat, + } as Intl.DateTimeFormat), ); Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {'999': {accountID: 999, timezone: {selected: 'Europe/London', automatic: true}}}).then(() => { const result = DateUtils.getCurrentTimezone(); @@ -110,7 +110,7 @@ describe('DateUtils', () => { () => ({ resolvedOptions: () => ({timeZone: UTC}), - }) as Intl.DateTimeFormat, + } as Intl.DateTimeFormat), ); Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {'999': {accountID: 999, timezone: {selected: 'Europe/London', automatic: true}}}).then(() => { const result = DateUtils.getCurrentTimezone(); diff --git a/tests/utils/ReportTestUtils.ts b/tests/utils/ReportTestUtils.ts index d9262cea249f..61c778ab5576 100644 --- a/tests/utils/ReportTestUtils.ts +++ b/tests/utils/ReportTestUtils.ts @@ -45,7 +45,7 @@ const getFakeReportAction = (index: number, actionName?: ReportActionName): Repo previousReportActionID: (index === 0 ? 0 : index - 1).toString(), sequenceNumber: 0, shouldShow: true, - }) as ReportAction; + } as ReportAction); const getMockedSortedReportActions = (length = 100): ReportAction[] => Array.from({length}, (element, index): ReportAction => { diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index 8d71a88157ee..9ca0969abc6a 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -215,13 +215,10 @@ function buildTestReportComment(created: string, actorAccountID: number, actionI function assertFormDataMatchesObject(formData: FormData, obj: Report) { expect( - Array.from(formData.entries()).reduce( - (acc, [key, val]) => { - acc[key] = val; - return acc; - }, - {} as Record, - ), + Array.from(formData.entries()).reduce((acc, [key, val]) => { + acc[key] = val; + return acc; + }, {} as Record), ).toEqual(expect.objectContaining(obj)); } From f2f2ee8e3779f139f9f8493354c7e14574c26094 Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Thu, 27 Jun 2024 13:38:08 +0200 Subject: [PATCH 103/183] fix: remove unused const --- src/CONST.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 0297f7bc0d5a..f2ebff78dca9 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4890,10 +4890,6 @@ const CONST = { }, SUBSCRIPTION_PRICE_FACTOR: 2, - SUBSCRIPTION_POSSIBLE_COST_SAVINGS: { - COLLECT_PLAN: 10, - CONTROL_PLAN: 18, - }, FEEDBACK_SURVEY_OPTIONS: { TOO_LIMITED: { ID: 'tooLimited', From 1ae18d2a66a0abe8a1a93d86fadfdc29990a944d Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Thu, 27 Jun 2024 17:22:23 +0530 Subject: [PATCH 104/183] fix: Category - No hover effect when hovering over selected parent category in the list. Signed-off-by: Krishna Gupta --- src/libs/OptionsListUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 24471b7f0140..b37ecc82fa41 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1100,7 +1100,7 @@ function getCategoryOptionTree(options: Record | Category[], i keyForList: searchText, searchText, tooltipText: optionName, - isDisabled: isChild ? !option.enabled || option.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE : true, + isDisabled: isChild ? !option.enabled || option.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE : !selectedOptionsName.includes(searchText), isSelected: isChild ? !!option.isSelected : selectedOptionsName.includes(searchText), pendingAction: option.pendingAction, }); From d14c6881e157b102341032f0e60798120401c5f3 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Thu, 27 Jun 2024 14:21:46 +0200 Subject: [PATCH 105/183] add description to hasAmountOwed util --- src/libs/SubscriptionUtils.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 525092ad2a0d..dc5f089f4738 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -154,6 +154,9 @@ function getAmountOwed(): number { return amountOwed ?? 0; } +/** + * @returns Whether there is an amount owed by the workspace owner. + */ function hasAmountOwed(): boolean { return !!amountOwed; } @@ -244,7 +247,7 @@ function getSubscriptionStatus(): SubscriptionStatus | undefined { if (hasOverdueGracePeriod()) { if (hasAmountOwed()) { // 1. Policy owner with amount owed, within grace period - if (hasGracePeriodOverdue() === false) { + if (!hasGracePeriodOverdue()) { return { status: PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED, isError: true, @@ -266,7 +269,7 @@ function getSubscriptionStatus(): SubscriptionStatus | undefined { } // 4. Owner of policy under invoicing, overdue (past grace period) - if (hasGracePeriodOverdue() === false) { + if (!hasGracePeriodOverdue()) { return { status: PAYMENT_STATUS.OWNER_OF_POLICY_UNDER_INVOICING_OVERDUE, }; From c5fe9292e431ca19e4507f5a78cac52c9aaa4360 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 27 Jun 2024 20:29:30 +0700 Subject: [PATCH 106/183] fix: redundant padding below money report header --- src/components/MoneyReportHeader.tsx | 96 ++++++++++++++-------------- 1 file changed, 49 insertions(+), 47 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index fa3454ed9e61..76be659b35b4 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -376,53 +376,55 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea )} - - {shouldShowSettlementButton && shouldUseNarrowLayout && ( - - )} - {shouldShowSubmitButton && shouldUseNarrowLayout && ( -