From e13ded3468e4a1471a48c5388c789c550a185c64 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:18:55 +0530 Subject: [PATCH 01/12] Fetch policyVendors on demand for stale sessions --- .../parameters/OpenPolicyVendorsPageParams.ts | 5 +++++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 ++ src/libs/actions/Policy/Vendor.ts | 16 +++++++++++--- .../iou/request/step/IOURequestStepVendor.tsx | 12 +++++++++- .../vendors/WorkspaceVendorsPage.tsx | 22 ++++++++++++++++--- 6 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 src/libs/API/parameters/OpenPolicyVendorsPageParams.ts diff --git a/src/libs/API/parameters/OpenPolicyVendorsPageParams.ts b/src/libs/API/parameters/OpenPolicyVendorsPageParams.ts new file mode 100644 index 000000000000..7402b14c9ee2 --- /dev/null +++ b/src/libs/API/parameters/OpenPolicyVendorsPageParams.ts @@ -0,0 +1,5 @@ +type OpenPolicyVendorsPageParams = { + policyID: string; +}; + +export default OpenPolicyVendorsPageParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 598c5a1195e2..004d84611272 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -294,6 +294,7 @@ export type {default as OpenWorkspaceMembersPageParams} from './OpenWorkspaceMem export type {default as OpenPolicyRoomsPageParams} from './OpenPolicyRoomsPageParams'; export type {default as OpenPolicyCategoriesPageParams} from './OpenPolicyCategoriesPageParams'; export type {default as OpenPolicyTagsPageParams} from './OpenPolicyTagsPageParams'; +export type {default as OpenPolicyVendorsPageParams} from './OpenPolicyVendorsPageParams'; export type {default as VerifyAddSecondaryLoginCodeParams} from './VerifyAddSecondaryLoginCodeParams'; export type {default as OpenDraftWorkspaceRequestParams} from './OpenDraftWorkspaceRequestParams'; export type {default as OpenDraftPerDiemExpenseParams} from './OpenDraftPerDiemExpenseParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 989456affaf8..5bf4bd888756 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -1655,6 +1655,7 @@ const READ_COMMANDS = { OPEN_POLICY_CATEGORIES_PAGE: 'OpenPolicyCategoriesPage', OPEN_POLICY_ROOMS_PAGE: 'OpenPolicyRoomsPage', OPEN_POLICY_TAGS_PAGE: 'OpenPolicyTagsPage', + OPEN_POLICY_VENDORS_PAGE: 'OpenPolicyVendorsPage', OPEN_POLICY_TAXES_PAGE: 'OpenPolicyTaxesPage', OPEN_POLICY_REPORT_FIELDS_PAGE: 'OpenPolicyReportFieldsPage', OPEN_POLICY_INVOICES_PAGE: 'OpenPolicyInvoicesPage', @@ -1776,6 +1777,7 @@ type ReadCommandParameters = { [READ_COMMANDS.OPEN_POLICY_CATEGORIES_PAGE]: Parameters.OpenPolicyCategoriesPageParams; [READ_COMMANDS.OPEN_POLICY_ROOMS_PAGE]: Parameters.OpenPolicyRoomsPageParams; [READ_COMMANDS.OPEN_POLICY_TAGS_PAGE]: Parameters.OpenPolicyTagsPageParams; + [READ_COMMANDS.OPEN_POLICY_VENDORS_PAGE]: Parameters.OpenPolicyVendorsPageParams; [READ_COMMANDS.OPEN_POLICY_TAXES_PAGE]: Parameters.OpenPolicyTaxesPageParams; [READ_COMMANDS.OPEN_POLICY_REPORT_FIELDS_PAGE]: Parameters.OpenPolicyReportFieldsPageParams; [READ_COMMANDS.OPEN_POLICY_INVOICES_PAGE]: Parameters.OpenPolicyReportFieldsPageParams; diff --git a/src/libs/actions/Policy/Vendor.ts b/src/libs/actions/Policy/Vendor.ts index 972bc219efdc..50adfabcde1b 100644 --- a/src/libs/actions/Policy/Vendor.ts +++ b/src/libs/actions/Policy/Vendor.ts @@ -1,10 +1,11 @@ /** * Actions for enabling/disabling policy vendors and clearing their offline errors. */ -import {write} from '@libs/API'; +import {write, read} from '@libs/API'; import type {SetPolicyVendorsEnabledParams} from '@libs/API/parameters'; -import {WRITE_COMMANDS} from '@libs/API/types'; +import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; +import Log from '@libs/Log'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -110,4 +111,13 @@ function clearVendorErrors(policyID: string, vendorID: string) { }); } -export {setPolicyVendorsEnabled, clearVendorErrors}; +function openPolicyVendorsPage(policyID: string) { + if (!policyID) { + Log.warn('openPolicyVendorsPage invalid params', {policyID}); + return; + } + + read(READ_COMMANDS.OPEN_POLICY_VENDORS_PAGE, {policyID}); +} + +export {setPolicyVendorsEnabled, clearVendorErrors, openPolicyVendorsPage}; diff --git a/src/pages/iou/request/step/IOURequestStepVendor.tsx b/src/pages/iou/request/step/IOURequestStepVendor.tsx index 08b637143124..2de2437caacb 100644 --- a/src/pages/iou/request/step/IOURequestStepVendor.tsx +++ b/src/pages/iou/request/step/IOURequestStepVendor.tsx @@ -13,6 +13,7 @@ import useShowNotFoundPageInIOUStep from '@hooks/useShowNotFoundPageInIOUStep'; import useThemeStyles from '@hooks/useThemeStyles'; import {updateMoneyRequestVendor} from '@libs/actions/IOU/UpdateMoneyRequest'; +import {openPolicyVendorsPage} from '@libs/actions/Policy/Vendor'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import Navigation from '@libs/Navigation/Navigation'; import {getVendorEmptyState, hasVendorFeature, isXeroActiveMatchingSource, sortVendors} from '@libs/PolicyUtils'; @@ -24,7 +25,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; -import React, {useState} from 'react'; +import React, {useEffect, useState} from 'react'; import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; @@ -66,6 +67,15 @@ function IOURequestStepVendor({ const isVendorMatchingBetaEnabled = isBetaEnabled(CONST.BETAS.VENDOR_MATCHING); const isFeatureAvailable = hasVendorFeature(policy, isVendorMatchingBetaEnabled); + + // Sessions loaded before the policyVendors_ collection shipped never get it backfilled + // by the Onyx update stream, so the vendor list shows empty. Fetch it on demand. + useEffect(() => { + if (!isFeatureAvailable || !policy?.id || policyVendors !== undefined) { + return; + } + openPolicyVendorsPage(policy.id); + }, [isFeatureAvailable, policy?.id, policyVendors]); const isOnXero = isXeroActiveMatchingSource(policy); const emptyState = getVendorEmptyState(policy, translate); diff --git a/src/pages/workspace/vendors/WorkspaceVendorsPage.tsx b/src/pages/workspace/vendors/WorkspaceVendorsPage.tsx index d0bc837cbd1b..5a67e65fb645 100644 --- a/src/pages/workspace/vendors/WorkspaceVendorsPage.tsx +++ b/src/pages/workspace/vendors/WorkspaceVendorsPage.tsx @@ -23,7 +23,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; -import {clearVendorErrors, setPolicyVendorsEnabled} from '@libs/actions/Policy/Vendor'; +import {clearVendorErrors, openPolicyVendorsPage, setPolicyVendorsEnabled} from '@libs/actions/Policy/Vendor'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; @@ -41,7 +41,7 @@ import type DeepValueOf from '@src/types/utils/DeepValueOf'; import type {OnyxEntry} from 'react-native-onyx'; -import React from 'react'; +import React, {useCallback, useEffect} from 'react'; import {View} from 'react-native'; type VendorBulkActionsProps = { @@ -126,7 +126,6 @@ function WorkspaceVendorsPage({policy, route}: WorkspaceVendorsPageProps) { const {translate, localeCompare} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const {isBetaEnabled} = usePermissions(); - const {isOffline} = useNetwork(); const shouldDisplayButtonsInSeparateLine = useShouldDisplayButtonsInSeparateLine(); const isMobileSelectionModeEnabled = useMobileSelectionMode(); const {canWrite: canWriteVendors, showReadOnlyModal} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.VENDORS); @@ -138,6 +137,23 @@ function WorkspaceVendorsPage({policy, route}: WorkspaceVendorsPageProps) { const isVendorMatchingBetaEnabled = isBetaEnabled(CONST.BETAS.VENDOR_MATCHING); const isFeatureAvailable = hasVendorFeature(policy, isVendorMatchingBetaEnabled); + + // Sessions loaded before the policyVendors_ collection shipped never get it backfilled + // by the Onyx update stream, so the list shows empty. Fetch it like the Categories page does. + const fetchVendors = useCallback(() => { + if (!isFeatureAvailable) { + return; + } + openPolicyVendorsPage(policyID); + }, [isFeatureAvailable, policyID]); + + const {isOffline} = useNetwork({onReconnect: fetchVendors}); + + useEffect(() => { + fetchVendors(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const connectedIntegration = getActiveVendorMatchingIntegration(policy); const currentConnectionName = connectedIntegration ? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectedIntegration] : undefined; const defaultVendorID = getDefaultVendorID(policy, connectedIntegration); From 81778700415887423c56f49c3c38179b5c4dd4e3 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:20:11 +0530 Subject: [PATCH 02/12] Simplify vendor fetch comments --- src/pages/iou/request/step/IOURequestStepVendor.tsx | 3 +-- src/pages/workspace/vendors/WorkspaceVendorsPage.tsx | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepVendor.tsx b/src/pages/iou/request/step/IOURequestStepVendor.tsx index 2de2437caacb..c818328405d2 100644 --- a/src/pages/iou/request/step/IOURequestStepVendor.tsx +++ b/src/pages/iou/request/step/IOURequestStepVendor.tsx @@ -68,8 +68,7 @@ function IOURequestStepVendor({ const isVendorMatchingBetaEnabled = isBetaEnabled(CONST.BETAS.VENDOR_MATCHING); const isFeatureAvailable = hasVendorFeature(policy, isVendorMatchingBetaEnabled); - // Sessions loaded before the policyVendors_ collection shipped never get it backfilled - // by the Onyx update stream, so the vendor list shows empty. Fetch it on demand. + // Stale sessions may never have received the policyVendors_ collection, so fetch it if missing. useEffect(() => { if (!isFeatureAvailable || !policy?.id || policyVendors !== undefined) { return; diff --git a/src/pages/workspace/vendors/WorkspaceVendorsPage.tsx b/src/pages/workspace/vendors/WorkspaceVendorsPage.tsx index 5a67e65fb645..ac567b532250 100644 --- a/src/pages/workspace/vendors/WorkspaceVendorsPage.tsx +++ b/src/pages/workspace/vendors/WorkspaceVendorsPage.tsx @@ -138,8 +138,7 @@ function WorkspaceVendorsPage({policy, route}: WorkspaceVendorsPageProps) { const isVendorMatchingBetaEnabled = isBetaEnabled(CONST.BETAS.VENDOR_MATCHING); const isFeatureAvailable = hasVendorFeature(policy, isVendorMatchingBetaEnabled); - // Sessions loaded before the policyVendors_ collection shipped never get it backfilled - // by the Onyx update stream, so the list shows empty. Fetch it like the Categories page does. + // Stale sessions may never have received the policyVendors_ collection, so fetch it if missing. const fetchVendors = useCallback(() => { if (!isFeatureAvailable) { return; From 2cb028b190c280e18914de4b389e1c7c83c6376d Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:33:44 +0530 Subject: [PATCH 03/12] Drop useCallback from fetchVendors --- src/pages/workspace/vendors/WorkspaceVendorsPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/vendors/WorkspaceVendorsPage.tsx b/src/pages/workspace/vendors/WorkspaceVendorsPage.tsx index ac567b532250..0eeebaeb1d20 100644 --- a/src/pages/workspace/vendors/WorkspaceVendorsPage.tsx +++ b/src/pages/workspace/vendors/WorkspaceVendorsPage.tsx @@ -41,7 +41,7 @@ import type DeepValueOf from '@src/types/utils/DeepValueOf'; import type {OnyxEntry} from 'react-native-onyx'; -import React, {useCallback, useEffect} from 'react'; +import React, {useEffect} from 'react'; import {View} from 'react-native'; type VendorBulkActionsProps = { @@ -139,12 +139,12 @@ function WorkspaceVendorsPage({policy, route}: WorkspaceVendorsPageProps) { const isFeatureAvailable = hasVendorFeature(policy, isVendorMatchingBetaEnabled); // Stale sessions may never have received the policyVendors_ collection, so fetch it if missing. - const fetchVendors = useCallback(() => { + const fetchVendors = () => { if (!isFeatureAvailable) { return; } openPolicyVendorsPage(policyID); - }, [isFeatureAvailable, policyID]); + }; const {isOffline} = useNetwork({onReconnect: fetchVendors}); From 51739a2ed52a64b499c9c10c4e215d663b9a7581 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 16 Oct 2026 22:53:48 +0530 Subject: [PATCH 04/12] Restore github action build files to match main --- .../isDeployChecklistLocked/index.js | 305 ++++++++++-------- .../javascript/proposalPoliceComment/index.js | 305 ++++++++++-------- 2 files changed, 330 insertions(+), 280 deletions(-) diff --git a/.github/actions/javascript/isDeployChecklistLocked/index.js b/.github/actions/javascript/isDeployChecklistLocked/index.js index 9321db05e6d7..cfe1ac778241 100644 --- a/.github/actions/javascript/isDeployChecklistLocked/index.js +++ b/.github/actions/javascript/isDeployChecklistLocked/index.js @@ -3432,6 +3432,7 @@ var require_CONST = __commonJS({ REPLACE_CARD: "replace_card", SHIP_CARD: "ship_card", REPORT_CARD_FRAUD: "report_card_fraud", + APPROVE_DIGITAL_WALLET: "approve_digital_wallet", ISSUE_CARD: "issue_card", UPDATE_CARD: "update_card", UPDATE_PERSONAL_DETAILS: "update_personal_details", @@ -22075,6 +22076,117 @@ var require_str = __commonJS({ } return parameter; } + function compare(strA, strB) { + if (strA < strB) { + return -1; + } + if (strA > strB) { + return 1; + } + return 0; + } + function caseInsensitiveCompare(strA, strB) { + const lowerCaseStrA = strA.toLocaleLowerCase(); + const lowerCaseStrB = strB.toLocaleLowerCase(); + return compare(lowerCaseStrA, lowerCaseStrB); + } + function contains(haystack, needle) { + return haystack.indexOf(needle) !== -1; + } + function cutAfter(str, substr) { + const index = str.indexOf(substr); + if (index !== -1) { + return str.substring(0, index); + } + return str; + } + function cutBefore(str, substr) { + const index = str.indexOf(substr); + if (index !== -1) { + return str.substring(index + substr.length); + } + return str; + } + function extractEmailDomain(email) { + return cutBefore(email, "@"); + } + function getExtension(url) { + var _a, _b; + if (typeof url !== "string") { + Log_1.default.warn("Str.getExtension: url is not a string", { url }); + return void 0; + } + return (_b = (_a = url.split(".").pop()) === null || _a === void 0 ? void 0 : _a.split("?")[0]) === null || _b === void 0 ? void 0 : _b.toLowerCase(); + } + function getRawByteSize(inputChar) { + const onlyChar = String(inputChar); + const c = onlyChar.charCodeAt(0); + if (c < 1 << 7) { + return 1; + } + if (c < 1 << 11) { + return 2; + } + if (c < 1 << 16) { + return 3; + } + if (c < 1 << 21) { + return 4; + } + if (c < 1 << 26) { + return 5; + } + if (c < 1 << 31) { + return 6; + } + return Number.NaN; + } + function htmlDecode(s) { + return HtmlEntities.decode(s); + } + function htmlEncode(s) { + return HtmlEntities.encode(s); + } + function isTypeOf(obj, type) { + return Object.prototype.toString.call(obj) === `[object ${type}]`; + } + function isNumber(obj) { + return isTypeOf(obj, "Number"); + } + function isString(obj) { + return isTypeOf(obj, "String"); + } + function isUndefined(obj) { + return obj === void 0; + } + function isValidE164Phone(phone) { + return Constants.CONST.SMS.E164_REGEX.test(phone); + } + function isValidEmail(str) { + if (!str || typeof str !== "string") { + return false; + } + const unicodeVersion = punycode_1.default.toUnicode(str); + if (String(unicodeVersion).match(Constants.CONST.REG_EXP.EMOJI_RULE)) { + return false; + } + return !!String(str).match(Constants.CONST.REG_EXP.EMAIL); + } + function maskFirstNCharacters(str, num, mask) { + if (!isString(str) || !isString(mask) || str.length === 0 || !isNumber(num)) { + return str; + } + return str.substring(0, num).replaceAll(/./g, mask) + str.substring(num); + } + function removeSMSDomain(text) { + return text.replaceAll(REMOVE_SMS_DOMAIN_PATTERN, ""); + } + function removeTrailingComma(str) { + return str.trim().replaceAll(/(,$)/g, ""); + } + function startsWith(haystack, needle) { + return isString(haystack) && isString(needle) && haystack.substring(0, needle.length) === needle; + } var Str2 = { /** * Return true if the string is ending with the provided suffix @@ -22132,18 +22244,14 @@ var require_str = __commonJS({ * @param s The string to decode. * @returns The decoded string. */ - htmlDecode(s) { - return HtmlEntities.decode(s); - }, + htmlDecode, /** * HTML encodes the given string. * * @param s The string to encode. * @return string @p s HTML encoded. */ - htmlEncode(s) { - return HtmlEntities.encode(s); - }, + htmlEncode, /** * Decodes the given HTML encoded string. * @@ -22185,7 +22293,7 @@ var require_str = __commonJS({ * @returns True when first === second, ignoring HTML encoding */ htmlEncodingInsensitiveEquals(first, second) { - return first === second || this.htmlDecode(first) === second || this.htmlEncode(first) === second; + return first === second || htmlDecode(first) === second || htmlEncode(first) === second; }, /** * Creates an ID that can be used as an HTML attribute from @p str. @@ -22268,36 +22376,14 @@ var require_str = __commonJS({ * one. * @returns Byte size of the character */ - getRawByteSize(inputChar) { - const onlyChar = String(inputChar); - const c = onlyChar.charCodeAt(0); - if (c < 1 << 7) { - return 1; - } - if (c < 1 << 11) { - return 2; - } - if (c < 1 << 16) { - return 3; - } - if (c < 1 << 21) { - return 4; - } - if (c < 1 << 26) { - return 5; - } - if (c < 1 << 31) { - return 6; - } - return Number.NaN; - }, + getRawByteSize, /** * Gets the length of a string in bytes, including non-ASCII characters * @returns The number of bytes used by string */ getByteLength(input) { const stringInput = String(input); - const byteLength = Array.from(stringInput).reduce((acc, char) => acc + this.getRawByteSize(char), 0); + const byteLength = Array.from(stringInput).reduce((acc, char) => acc + getRawByteSize(char), 0); return byteLength; }, /** @@ -22309,7 +22395,7 @@ var require_str = __commonJS({ const stringInput = String(input); let totalByteLength = 0; for (let i = 0; i < stringInput.length; i++) { - const charByteSize = this.getRawByteSize(stringInput[i]); + const charByteSize = getRawByteSize(stringInput[i]); if (charByteSize + totalByteLength > maxSize) { return `${stringInput.substr(0, i - 3)}...`; } @@ -22324,9 +22410,7 @@ var require_str = __commonJS({ * @param needle The case-sensitive string to search for * @returns True if the haystack starts with the needle. */ - startsWith(haystack, needle) { - return this.isString(haystack) && this.isString(needle) && haystack.substring(0, needle.length) === needle; - }, + startsWith, /** * Gets the textual value of the given string. * @@ -22334,7 +22418,7 @@ var require_str = __commonJS({ * @returns The text from within the HTML string. */ stripHTML(str) { - if (!this.isString(str)) { + if (!isString(str)) { return ""; } return str.replaceAll(/<[^>]*>?/gm, ""); @@ -22357,13 +22441,7 @@ var require_str = __commonJS({ * @param substr The substring to search for. * @returns The cut/trimmed string. */ - cutAfter(str, substr) { - const index = str.indexOf(substr); - if (index !== -1) { - return str.substring(0, index); - } - return str; - }, + cutAfter, /** * Returns a string containing all the characters str from after the first * occurrence of substr to the end of the string. @@ -22373,13 +22451,7 @@ var require_str = __commonJS({ * @param substr The substring to search for. * @returns The cut/trimmed string. */ - cutBefore(str, substr) { - const index = str.indexOf(substr); - if (index !== -1) { - return str.substring(index + substr.length); - } - return str; - }, + cutBefore, /** * Checks that the string is a domain name (e.g. example.com) * @@ -22406,16 +22478,7 @@ var require_str = __commonJS({ * * @returns True if the string is an email */ - isValidEmail(str) { - if (!str || typeof str !== "string") { - return false; - } - const unicodeVersion = punycode_1.default.toUnicode(str); - if (String(unicodeVersion).match(Constants.CONST.REG_EXP.EMOJI_RULE)) { - return false; - } - return !!String(str).match(Constants.CONST.REG_EXP.EMAIL); - }, + isValidEmail, /** * Checks if the string is an valid email address formed during comment markdown formation. * @@ -22433,9 +22496,7 @@ var require_str = __commonJS({ * * @returns string with the trailing comma removed */ - removeTrailingComma(str) { - return str.trim().replaceAll(/(,$)/g, ""); - }, + removeTrailingComma, /** * Checks that the string is a list of coma separated email addresss. * @@ -22444,12 +22505,12 @@ var require_str = __commonJS({ * @returns True if all emails are valid or if input is empty */ areValidEmails(str) { - const string = this.removeTrailingComma(str); + const string = removeTrailingComma(str); if (string === "") { return true; } const emails = string.split(","); - const result = emails.every((email) => this.isValidEmail(email.trim())); + const result = emails.every((email) => isValidEmail(email.trim())); return result; }, /** @@ -22466,9 +22527,7 @@ var require_str = __commonJS({ * * @returns The domain name in the email address. */ - extractEmailDomain(email) { - return this.cutBefore(email, "@"); - }, + extractEmailDomain, /** * Tries to extract the company name from the given email address * (e.g. "yelp" for "joe@yelp.co.uk"). @@ -22478,7 +22537,7 @@ var require_str = __commonJS({ * @returns The company name in the email address or null. */ extractCompanyNameFromEmailDomain(email) { - const domain = this.extractEmailDomain(email); + const domain = extractEmailDomain(email); if (!domain) { return null; } @@ -22497,7 +22556,7 @@ var require_str = __commonJS({ * @returns The local part in the email address. */ extractEmailLocalPart(email) { - return this.cutAfter(email, "@"); + return cutAfter(email, "@"); }, /** * Sanitize phone number to return only numbers. Return null if non valid phone number. @@ -22555,13 +22614,13 @@ var require_str = __commonJS({ * @returns true if the length is in the range, false otherwise */ isOfLength(str, minimumLength, maximumLength) { - if (!this.isString(str)) { + if (!isString(str)) { return false; } if (str.length < minimumLength) { return false; } - if (!this.isUndefined(maximumLength) && str.length > maximumLength) { + if (!isUndefined(maximumLength) && str.length > maximumLength) { return false; } return true; @@ -22614,9 +22673,7 @@ var require_str = __commonJS({ * * @returns Returns true if the haystack contains the needle */ - contains(haystack, needle) { - return haystack.indexOf(needle) !== -1; - }, + contains, /** * Returns true if the haystack contains the needle, ignoring case * @@ -22626,7 +22683,7 @@ var require_str = __commonJS({ * @returns Returns true if the haystack contains the needle, ignoring case */ caseInsensitiveContains(haystack, needle) { - return this.contains(haystack.toLowerCase(), needle.toLowerCase()); + return contains(haystack.toLowerCase(), needle.toLowerCase()); }, /** * Case insensitive compare function @@ -22638,11 +22695,7 @@ var require_str = __commonJS({ * 1 if first string > second string * 0 if first string = second string */ - caseInsensitiveCompare(strA, strB) { - const lowerCaseStrA = strA.toLocaleLowerCase(); - const lowerCaseStrB = strB.toLocaleLowerCase(); - return this.compare(lowerCaseStrA, lowerCaseStrB); - }, + caseInsensitiveCompare, /** * Case insensitive equals * @@ -22651,7 +22704,7 @@ var require_str = __commonJS({ * @returns true when first == second except for case */ caseInsensitiveEquals(strA, strB) { - return this.caseInsensitiveCompare(strA, strB) === 0; + return caseInsensitiveCompare(strA, strB) === 0; }, /** * Compare function @@ -22663,15 +22716,7 @@ var require_str = __commonJS({ * 1 if first string > second string * 0 if first string = second string */ - compare(strA, strB) { - if (strA < strB) { - return -1; - } - if (strA > strB) { - return 1; - } - return 0; - }, + compare, /** * Check if a file extension is supported by SmartReports */ @@ -22690,45 +22735,37 @@ var require_str = __commonJS({ const accountNumber = String(num); const len = accountNumber.length; if (len < 6 || len > 20) { - return this.maskFirstNCharacters(accountNumber, len, "X"); + return maskFirstNCharacters(accountNumber, len, "X"); } if (len < 14) { - return this.maskFirstNCharacters(accountNumber, len - 4, "X"); + return maskFirstNCharacters(accountNumber, len - 4, "X"); } const first = accountNumber.substr(0, 6); const last = accountNumber.substr(7); - const masked = this.maskFirstNCharacters(last, len - 11, "X"); + const masked = maskFirstNCharacters(last, len - 11, "X"); return `${first}${masked}`; }, /** * Checks if something is a string * Stolen from underscore */ - isString(obj) { - return this.isTypeOf(obj, "String"); - }, + isString, /** * Checks if something is a number * Stolen from underscore * @param obj */ - isNumber(obj) { - return this.isTypeOf(obj, "Number"); - }, + isNumber, /** * Checks if something is a certain type * Stolen from underscore */ - isTypeOf(obj, type) { - return Object.prototype.toString.call(obj) === `[object ${type}]`; - }, + isTypeOf, /** * Checks to see if something is undefined * Stolen from underscore */ - isUndefined(obj) { - return obj === void 0; - }, + isUndefined, /** * Replace first N characters of the string with maskChar * eg: maskFirstNCharacters( '1234567890', 6, 'X' ) yields XXXXXX7890 @@ -22737,12 +22774,7 @@ var require_str = __commonJS({ * @param mask String we want replace the first N chars with * @returns Masked string */ - maskFirstNCharacters(str, num, mask) { - if (!this.isString(str) || !this.isString(mask) || str.length === 0 || !this.isNumber(num)) { - return str; - } - return str.substring(0, num).replaceAll(/./g, mask) + str.substring(num); - }, + maskFirstNCharacters, /** * Trim a string */ @@ -22754,7 +22786,7 @@ var require_str = __commonJS({ * @param percentageString The percentage as a string */ percentageStringToNumber(percentageString) { - return Number(this.cutAfter(percentageString, "%")); + return Number(cutAfter(percentageString, "%")); }, /** * Remove all the spaces from a string @@ -22798,7 +22830,7 @@ var require_str = __commonJS({ * Converts a value to boolean, case-insensitive. */ toBool(value) { - if (this.isString(value)) { + if (isString(value)) { return value.toLowerCase() === "true"; } return !!value; @@ -22848,9 +22880,7 @@ var require_str = __commonJS({ /** * Check for whether a phone number is valid according to E.164 standard. */ - isValidE164Phone(phone) { - return Constants.CONST.SMS.E164_REGEX.test(phone); - }, + isValidE164Phone, /** * Check for whether a phone number is valid in different formats/standards. For example: * significant: 4404589784 @@ -22876,14 +22906,12 @@ var require_str = __commonJS({ /** * Returns text without our SMS domain */ - removeSMSDomain(text) { - return text.replaceAll(REMOVE_SMS_DOMAIN_PATTERN, ""); - }, + removeSMSDomain, /** * Returns true if the text is a valid E.164 phone number with our SMS domain removed */ isSMSLogin(text) { - return this.isValidE164Phone(this.removeSMSDomain(text)); + return isValidE164Phone(removeSMSDomain(text)); }, /** * This method will return all matches of a single regex like preg_match_all() in PHP. This is not a common part of @@ -22936,7 +22964,7 @@ var require_str = __commonJS({ return url; } const website = match[3] ? match[2] : `${defaultScheme}://${match[2]}`; - return website.toLowerCase() + this.cutBefore(match[1], match[2]); + return website.toLowerCase() + cutBefore(match[1], match[2]); }, /** * Checks if parameter is a string or function @@ -22948,14 +22976,7 @@ var require_str = __commonJS({ * Get file extension for a given url with or * without query parameters */ - getExtension(url) { - var _a, _b; - if (typeof url !== "string") { - Log_1.default.warn("Str.getExtension: url is not a string", { url }); - return void 0; - } - return (_b = (_a = url.split(".").pop()) === null || _a === void 0 ? void 0 : _a.split("?")[0]) === null || _b === void 0 ? void 0 : _b.toLowerCase(); - }, + getExtension, /** * Takes in a URL and checks if the file extension is PDF * @@ -22963,7 +22984,7 @@ var require_str = __commonJS({ * @returns Whether file path is PDF or not */ isPDF(url) { - return this.getExtension(url) === "pdf"; + return getExtension(url) === "pdf"; }, /** * Takes in a URL and checks if the file extension is an image @@ -22974,7 +22995,7 @@ var require_str = __commonJS({ * https://reactnative.dev/docs/image#source */ isImage(url) { - const extension = this.getExtension(url); + const extension = getExtension(url); if (!extension) { return false; } @@ -22990,7 +23011,7 @@ var require_str = __commonJS({ * https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Video_codecs */ isVideo(url) { - const extension = this.getExtension(url); + const extension = getExtension(url); if (!extension) { return false; } @@ -23003,7 +23024,7 @@ var require_str = __commonJS({ * @returns True if is a domain account email, otherwise false. */ isDomainEmail(email) { - return this.startsWith(email, "+@"); + return startsWith(email, "+@"); }, /** * Find the minimum indentation of any line in the string, @@ -24141,13 +24162,17 @@ ${g2} }, { name: "anchor", - regex: /<(a)[^><]*href\s*=\s*(['"])(.*?)\2(?:".*?"|'.*?'|[^'"><])*>([\s\S]*?)<\/\1>(?![^<]*(<\/pre>|<\/code>))/gi, - replacement: (_extras, _match, _g1, _g2, g3, g4) => { - const email = g3.startsWith("mailto:") ? g3.slice(7) : ""; - if (email === g4) { - return email; + regex: /(\]\()?<(a)[^><]*href\s*=\s*(['"])(.*?)\3(?:".*?"|'.*?'|[^'"><])*>([\s\S]*?)<\/\2>(?![^<]*(<\/pre>|<\/code>))/gi, + replacement: (_extras, _match, g1, _g2, _g3, g4, g5) => { + const incompleteLinkStart = g1 !== null && g1 !== void 0 ? g1 : ""; + const email = g4.startsWith("mailto:") ? g4.slice(7) : ""; + if (email === g5) { + return `${incompleteLinkStart}${email}`; + } + if (incompleteLinkStart && (g4 === g5 || g4 === `https://${g5}`)) { + return `${incompleteLinkStart}${g5}`; } - return `[${g4}](${email || g3})`; + return `${incompleteLinkStart}[${g5}](${email || g4})`; } }, { diff --git a/.github/actions/javascript/proposalPoliceComment/index.js b/.github/actions/javascript/proposalPoliceComment/index.js index beb367dd3ebd..58a12c5fb27a 100644 --- a/.github/actions/javascript/proposalPoliceComment/index.js +++ b/.github/actions/javascript/proposalPoliceComment/index.js @@ -24691,6 +24691,7 @@ var require_CONST = __commonJS({ REPLACE_CARD: "replace_card", SHIP_CARD: "ship_card", REPORT_CARD_FRAUD: "report_card_fraud", + APPROVE_DIGITAL_WALLET: "approve_digital_wallet", ISSUE_CARD: "issue_card", UPDATE_CARD: "update_card", UPDATE_PERSONAL_DETAILS: "update_personal_details", @@ -43334,6 +43335,117 @@ var require_str = __commonJS({ } return parameter; } + function compare(strA, strB) { + if (strA < strB) { + return -1; + } + if (strA > strB) { + return 1; + } + return 0; + } + function caseInsensitiveCompare(strA, strB) { + const lowerCaseStrA = strA.toLocaleLowerCase(); + const lowerCaseStrB = strB.toLocaleLowerCase(); + return compare(lowerCaseStrA, lowerCaseStrB); + } + function contains(haystack, needle) { + return haystack.indexOf(needle) !== -1; + } + function cutAfter(str2, substr) { + const index = str2.indexOf(substr); + if (index !== -1) { + return str2.substring(0, index); + } + return str2; + } + function cutBefore(str2, substr) { + const index = str2.indexOf(substr); + if (index !== -1) { + return str2.substring(index + substr.length); + } + return str2; + } + function extractEmailDomain(email) { + return cutBefore(email, "@"); + } + function getExtension(url) { + var _a3, _b; + if (typeof url !== "string") { + Log_1.default.warn("Str.getExtension: url is not a string", { url }); + return void 0; + } + return (_b = (_a3 = url.split(".").pop()) === null || _a3 === void 0 ? void 0 : _a3.split("?")[0]) === null || _b === void 0 ? void 0 : _b.toLowerCase(); + } + function getRawByteSize(inputChar) { + const onlyChar = String(inputChar); + const c = onlyChar.charCodeAt(0); + if (c < 1 << 7) { + return 1; + } + if (c < 1 << 11) { + return 2; + } + if (c < 1 << 16) { + return 3; + } + if (c < 1 << 21) { + return 4; + } + if (c < 1 << 26) { + return 5; + } + if (c < 1 << 31) { + return 6; + } + return Number.NaN; + } + function htmlDecode(s) { + return HtmlEntities.decode(s); + } + function htmlEncode(s) { + return HtmlEntities.encode(s); + } + function isTypeOf(obj, type) { + return Object.prototype.toString.call(obj) === `[object ${type}]`; + } + function isNumber(obj) { + return isTypeOf(obj, "Number"); + } + function isString(obj) { + return isTypeOf(obj, "String"); + } + function isUndefined(obj) { + return obj === void 0; + } + function isValidE164Phone(phone) { + return Constants.CONST.SMS.E164_REGEX.test(phone); + } + function isValidEmail(str2) { + if (!str2 || typeof str2 !== "string") { + return false; + } + const unicodeVersion = punycode_1.default.toUnicode(str2); + if (String(unicodeVersion).match(Constants.CONST.REG_EXP.EMOJI_RULE)) { + return false; + } + return !!String(str2).match(Constants.CONST.REG_EXP.EMAIL); + } + function maskFirstNCharacters(str2, num, mask) { + if (!isString(str2) || !isString(mask) || str2.length === 0 || !isNumber(num)) { + return str2; + } + return str2.substring(0, num).replaceAll(/./g, mask) + str2.substring(num); + } + function removeSMSDomain(text) { + return text.replaceAll(REMOVE_SMS_DOMAIN_PATTERN, ""); + } + function removeTrailingComma(str2) { + return str2.trim().replaceAll(/(,$)/g, ""); + } + function startsWith(haystack, needle) { + return isString(haystack) && isString(needle) && haystack.substring(0, needle.length) === needle; + } var Str6 = { /** * Return true if the string is ending with the provided suffix @@ -43391,18 +43503,14 @@ var require_str = __commonJS({ * @param s The string to decode. * @returns The decoded string. */ - htmlDecode(s) { - return HtmlEntities.decode(s); - }, + htmlDecode, /** * HTML encodes the given string. * * @param s The string to encode. * @return string @p s HTML encoded. */ - htmlEncode(s) { - return HtmlEntities.encode(s); - }, + htmlEncode, /** * Decodes the given HTML encoded string. * @@ -43444,7 +43552,7 @@ var require_str = __commonJS({ * @returns True when first === second, ignoring HTML encoding */ htmlEncodingInsensitiveEquals(first, second) { - return first === second || this.htmlDecode(first) === second || this.htmlEncode(first) === second; + return first === second || htmlDecode(first) === second || htmlEncode(first) === second; }, /** * Creates an ID that can be used as an HTML attribute from @p str. @@ -43527,36 +43635,14 @@ var require_str = __commonJS({ * one. * @returns Byte size of the character */ - getRawByteSize(inputChar) { - const onlyChar = String(inputChar); - const c = onlyChar.charCodeAt(0); - if (c < 1 << 7) { - return 1; - } - if (c < 1 << 11) { - return 2; - } - if (c < 1 << 16) { - return 3; - } - if (c < 1 << 21) { - return 4; - } - if (c < 1 << 26) { - return 5; - } - if (c < 1 << 31) { - return 6; - } - return Number.NaN; - }, + getRawByteSize, /** * Gets the length of a string in bytes, including non-ASCII characters * @returns The number of bytes used by string */ getByteLength(input) { const stringInput = String(input); - const byteLength = Array.from(stringInput).reduce((acc, char) => acc + this.getRawByteSize(char), 0); + const byteLength = Array.from(stringInput).reduce((acc, char) => acc + getRawByteSize(char), 0); return byteLength; }, /** @@ -43568,7 +43654,7 @@ var require_str = __commonJS({ const stringInput = String(input); let totalByteLength = 0; for (let i = 0; i < stringInput.length; i++) { - const charByteSize = this.getRawByteSize(stringInput[i]); + const charByteSize = getRawByteSize(stringInput[i]); if (charByteSize + totalByteLength > maxSize) { return `${stringInput.substr(0, i - 3)}...`; } @@ -43583,9 +43669,7 @@ var require_str = __commonJS({ * @param needle The case-sensitive string to search for * @returns True if the haystack starts with the needle. */ - startsWith(haystack, needle) { - return this.isString(haystack) && this.isString(needle) && haystack.substring(0, needle.length) === needle; - }, + startsWith, /** * Gets the textual value of the given string. * @@ -43593,7 +43677,7 @@ var require_str = __commonJS({ * @returns The text from within the HTML string. */ stripHTML(str2) { - if (!this.isString(str2)) { + if (!isString(str2)) { return ""; } return str2.replaceAll(/<[^>]*>?/gm, ""); @@ -43616,13 +43700,7 @@ var require_str = __commonJS({ * @param substr The substring to search for. * @returns The cut/trimmed string. */ - cutAfter(str2, substr) { - const index = str2.indexOf(substr); - if (index !== -1) { - return str2.substring(0, index); - } - return str2; - }, + cutAfter, /** * Returns a string containing all the characters str from after the first * occurrence of substr to the end of the string. @@ -43632,13 +43710,7 @@ var require_str = __commonJS({ * @param substr The substring to search for. * @returns The cut/trimmed string. */ - cutBefore(str2, substr) { - const index = str2.indexOf(substr); - if (index !== -1) { - return str2.substring(index + substr.length); - } - return str2; - }, + cutBefore, /** * Checks that the string is a domain name (e.g. example.com) * @@ -43665,16 +43737,7 @@ var require_str = __commonJS({ * * @returns True if the string is an email */ - isValidEmail(str2) { - if (!str2 || typeof str2 !== "string") { - return false; - } - const unicodeVersion = punycode_1.default.toUnicode(str2); - if (String(unicodeVersion).match(Constants.CONST.REG_EXP.EMOJI_RULE)) { - return false; - } - return !!String(str2).match(Constants.CONST.REG_EXP.EMAIL); - }, + isValidEmail, /** * Checks if the string is an valid email address formed during comment markdown formation. * @@ -43692,9 +43755,7 @@ var require_str = __commonJS({ * * @returns string with the trailing comma removed */ - removeTrailingComma(str2) { - return str2.trim().replaceAll(/(,$)/g, ""); - }, + removeTrailingComma, /** * Checks that the string is a list of coma separated email addresss. * @@ -43703,12 +43764,12 @@ var require_str = __commonJS({ * @returns True if all emails are valid or if input is empty */ areValidEmails(str2) { - const string = this.removeTrailingComma(str2); + const string = removeTrailingComma(str2); if (string === "") { return true; } const emails = string.split(","); - const result = emails.every((email) => this.isValidEmail(email.trim())); + const result = emails.every((email) => isValidEmail(email.trim())); return result; }, /** @@ -43725,9 +43786,7 @@ var require_str = __commonJS({ * * @returns The domain name in the email address. */ - extractEmailDomain(email) { - return this.cutBefore(email, "@"); - }, + extractEmailDomain, /** * Tries to extract the company name from the given email address * (e.g. "yelp" for "joe@yelp.co.uk"). @@ -43737,7 +43796,7 @@ var require_str = __commonJS({ * @returns The company name in the email address or null. */ extractCompanyNameFromEmailDomain(email) { - const domain = this.extractEmailDomain(email); + const domain = extractEmailDomain(email); if (!domain) { return null; } @@ -43756,7 +43815,7 @@ var require_str = __commonJS({ * @returns The local part in the email address. */ extractEmailLocalPart(email) { - return this.cutAfter(email, "@"); + return cutAfter(email, "@"); }, /** * Sanitize phone number to return only numbers. Return null if non valid phone number. @@ -43814,13 +43873,13 @@ var require_str = __commonJS({ * @returns true if the length is in the range, false otherwise */ isOfLength(str2, minimumLength, maximumLength) { - if (!this.isString(str2)) { + if (!isString(str2)) { return false; } if (str2.length < minimumLength) { return false; } - if (!this.isUndefined(maximumLength) && str2.length > maximumLength) { + if (!isUndefined(maximumLength) && str2.length > maximumLength) { return false; } return true; @@ -43873,9 +43932,7 @@ var require_str = __commonJS({ * * @returns Returns true if the haystack contains the needle */ - contains(haystack, needle) { - return haystack.indexOf(needle) !== -1; - }, + contains, /** * Returns true if the haystack contains the needle, ignoring case * @@ -43885,7 +43942,7 @@ var require_str = __commonJS({ * @returns Returns true if the haystack contains the needle, ignoring case */ caseInsensitiveContains(haystack, needle) { - return this.contains(haystack.toLowerCase(), needle.toLowerCase()); + return contains(haystack.toLowerCase(), needle.toLowerCase()); }, /** * Case insensitive compare function @@ -43897,11 +43954,7 @@ var require_str = __commonJS({ * 1 if first string > second string * 0 if first string = second string */ - caseInsensitiveCompare(strA, strB) { - const lowerCaseStrA = strA.toLocaleLowerCase(); - const lowerCaseStrB = strB.toLocaleLowerCase(); - return this.compare(lowerCaseStrA, lowerCaseStrB); - }, + caseInsensitiveCompare, /** * Case insensitive equals * @@ -43910,7 +43963,7 @@ var require_str = __commonJS({ * @returns true when first == second except for case */ caseInsensitiveEquals(strA, strB) { - return this.caseInsensitiveCompare(strA, strB) === 0; + return caseInsensitiveCompare(strA, strB) === 0; }, /** * Compare function @@ -43922,15 +43975,7 @@ var require_str = __commonJS({ * 1 if first string > second string * 0 if first string = second string */ - compare(strA, strB) { - if (strA < strB) { - return -1; - } - if (strA > strB) { - return 1; - } - return 0; - }, + compare, /** * Check if a file extension is supported by SmartReports */ @@ -43949,45 +43994,37 @@ var require_str = __commonJS({ const accountNumber = String(num); const len = accountNumber.length; if (len < 6 || len > 20) { - return this.maskFirstNCharacters(accountNumber, len, "X"); + return maskFirstNCharacters(accountNumber, len, "X"); } if (len < 14) { - return this.maskFirstNCharacters(accountNumber, len - 4, "X"); + return maskFirstNCharacters(accountNumber, len - 4, "X"); } const first = accountNumber.substr(0, 6); const last = accountNumber.substr(7); - const masked = this.maskFirstNCharacters(last, len - 11, "X"); + const masked = maskFirstNCharacters(last, len - 11, "X"); return `${first}${masked}`; }, /** * Checks if something is a string * Stolen from underscore */ - isString(obj) { - return this.isTypeOf(obj, "String"); - }, + isString, /** * Checks if something is a number * Stolen from underscore * @param obj */ - isNumber(obj) { - return this.isTypeOf(obj, "Number"); - }, + isNumber, /** * Checks if something is a certain type * Stolen from underscore */ - isTypeOf(obj, type) { - return Object.prototype.toString.call(obj) === `[object ${type}]`; - }, + isTypeOf, /** * Checks to see if something is undefined * Stolen from underscore */ - isUndefined(obj) { - return obj === void 0; - }, + isUndefined, /** * Replace first N characters of the string with maskChar * eg: maskFirstNCharacters( '1234567890', 6, 'X' ) yields XXXXXX7890 @@ -43996,12 +44033,7 @@ var require_str = __commonJS({ * @param mask String we want replace the first N chars with * @returns Masked string */ - maskFirstNCharacters(str2, num, mask) { - if (!this.isString(str2) || !this.isString(mask) || str2.length === 0 || !this.isNumber(num)) { - return str2; - } - return str2.substring(0, num).replaceAll(/./g, mask) + str2.substring(num); - }, + maskFirstNCharacters, /** * Trim a string */ @@ -44013,7 +44045,7 @@ var require_str = __commonJS({ * @param percentageString The percentage as a string */ percentageStringToNumber(percentageString) { - return Number(this.cutAfter(percentageString, "%")); + return Number(cutAfter(percentageString, "%")); }, /** * Remove all the spaces from a string @@ -44057,7 +44089,7 @@ var require_str = __commonJS({ * Converts a value to boolean, case-insensitive. */ toBool(value) { - if (this.isString(value)) { + if (isString(value)) { return value.toLowerCase() === "true"; } return !!value; @@ -44107,9 +44139,7 @@ var require_str = __commonJS({ /** * Check for whether a phone number is valid according to E.164 standard. */ - isValidE164Phone(phone) { - return Constants.CONST.SMS.E164_REGEX.test(phone); - }, + isValidE164Phone, /** * Check for whether a phone number is valid in different formats/standards. For example: * significant: 4404589784 @@ -44135,14 +44165,12 @@ var require_str = __commonJS({ /** * Returns text without our SMS domain */ - removeSMSDomain(text) { - return text.replaceAll(REMOVE_SMS_DOMAIN_PATTERN, ""); - }, + removeSMSDomain, /** * Returns true if the text is a valid E.164 phone number with our SMS domain removed */ isSMSLogin(text) { - return this.isValidE164Phone(this.removeSMSDomain(text)); + return isValidE164Phone(removeSMSDomain(text)); }, /** * This method will return all matches of a single regex like preg_match_all() in PHP. This is not a common part of @@ -44195,7 +44223,7 @@ var require_str = __commonJS({ return url; } const website = match2[3] ? match2[2] : `${defaultScheme}://${match2[2]}`; - return website.toLowerCase() + this.cutBefore(match2[1], match2[2]); + return website.toLowerCase() + cutBefore(match2[1], match2[2]); }, /** * Checks if parameter is a string or function @@ -44207,14 +44235,7 @@ var require_str = __commonJS({ * Get file extension for a given url with or * without query parameters */ - getExtension(url) { - var _a3, _b; - if (typeof url !== "string") { - Log_1.default.warn("Str.getExtension: url is not a string", { url }); - return void 0; - } - return (_b = (_a3 = url.split(".").pop()) === null || _a3 === void 0 ? void 0 : _a3.split("?")[0]) === null || _b === void 0 ? void 0 : _b.toLowerCase(); - }, + getExtension, /** * Takes in a URL and checks if the file extension is PDF * @@ -44222,7 +44243,7 @@ var require_str = __commonJS({ * @returns Whether file path is PDF or not */ isPDF(url) { - return this.getExtension(url) === "pdf"; + return getExtension(url) === "pdf"; }, /** * Takes in a URL and checks if the file extension is an image @@ -44233,7 +44254,7 @@ var require_str = __commonJS({ * https://reactnative.dev/docs/image#source */ isImage(url) { - const extension = this.getExtension(url); + const extension = getExtension(url); if (!extension) { return false; } @@ -44249,7 +44270,7 @@ var require_str = __commonJS({ * https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Video_codecs */ isVideo(url) { - const extension = this.getExtension(url); + const extension = getExtension(url); if (!extension) { return false; } @@ -44262,7 +44283,7 @@ var require_str = __commonJS({ * @returns True if is a domain account email, otherwise false. */ isDomainEmail(email) { - return this.startsWith(email, "+@"); + return startsWith(email, "+@"); }, /** * Find the minimum indentation of any line in the string, @@ -45400,13 +45421,17 @@ ${g2} }, { name: "anchor", - regex: /<(a)[^><]*href\s*=\s*(['"])(.*?)\2(?:".*?"|'.*?'|[^'"><])*>([\s\S]*?)<\/\1>(?![^<]*(<\/pre>|<\/code>))/gi, - replacement: (_extras, _match, _g1, _g2, g3, g4) => { - const email = g3.startsWith("mailto:") ? g3.slice(7) : ""; - if (email === g4) { - return email; + regex: /(\]\()?<(a)[^><]*href\s*=\s*(['"])(.*?)\3(?:".*?"|'.*?'|[^'"><])*>([\s\S]*?)<\/\2>(?![^<]*(<\/pre>|<\/code>))/gi, + replacement: (_extras, _match, g1, _g2, _g3, g4, g5) => { + const incompleteLinkStart = g1 !== null && g1 !== void 0 ? g1 : ""; + const email = g4.startsWith("mailto:") ? g4.slice(7) : ""; + if (email === g5) { + return `${incompleteLinkStart}${email}`; + } + if (incompleteLinkStart && (g4 === g5 || g4 === `https://${g5}`)) { + return `${incompleteLinkStart}${g5}`; } - return `[${g4}](${email || g3})`; + return `${incompleteLinkStart}[${g5}](${email || g4})`; } }, { From 3225d7718b211cbecda51854c58b6229951893e9 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 17 Oct 2026 00:09:28 +0530 Subject: [PATCH 05/12] Address review comments on vendor fetch and violation retention --- src/libs/Violations/ViolationsUtils.ts | 5 +-- .../step/DynamicIOURequestStepDescription.tsx | 2 ++ .../iou/request/step/IOURequestStepVendor.tsx | 10 ++++++ .../vendors/WorkspaceVendorsPage.tsx | 1 + tests/unit/ViolationUtilsTest.ts | 31 +++++++++++++++++++ 5 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index e6be6cadf952..ff09be21f3d0 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -816,6 +816,7 @@ const ViolationsUtils = { const matchedVendor = getMatchingVendorByID(policy, transactionVendorID); const isVendorDisabled = policyVendorsParam?.[transactionVendorID]?.enabled === false; const isVendorInactive = !matchedVendor || isVendorDisabled; + const canConfirmVendorEnabled = policyVendorsParam !== undefined && !isVendorDisabled; // Stamp Xero-specific copy on the violation so the render site can use the // "Supplier" wording the rest of the Xero UI uses; QBO/Intacct keep the default @@ -828,9 +829,9 @@ const ViolationsUtils = { showInReview: true, ...(isSupplierViolation ? {data: {isSupplierViolation: true}} : {}), }); - } else if (!isVendorInactive && hasInactiveVendorViolation) { + } else if (!isVendorInactive && canConfirmVendorEnabled && hasInactiveVendorViolation) { newTransactionViolations = reject(newTransactionViolations, {name: CONST.VIOLATIONS.INACTIVE_VENDOR}); - } else if (isVendorInactive && hasInactiveVendorViolation) { + } else if (hasInactiveVendorViolation) { // Reconcile data.isSupplierViolation with the current active matching source. // Backfills the flag when Xero is now active (server-fired violation, or // persisted from before this code path existed). Strips a stale flag when the diff --git a/src/pages/iou/request/step/DynamicIOURequestStepDescription.tsx b/src/pages/iou/request/step/DynamicIOURequestStepDescription.tsx index 1966103dbaf9..bf627403c9b7 100644 --- a/src/pages/iou/request/step/DynamicIOURequestStepDescription.tsx +++ b/src/pages/iou/request/step/DynamicIOURequestStepDescription.tsx @@ -75,6 +75,7 @@ function DynamicIOURequestStepDescription({ const [splitDraftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`); const allTransactionViolations = useAllTransactionViolations(transaction?.transactionID); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report?.policyID}`); + const [policyVendors] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_VENDORS}${report?.policyID}`); const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(report?.parentReportID)}`); const [iouReportOwnerLogin] = usePersonalDetail(parentReport?.ownerAccountID, loginSelector); const [reportPolicyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${getNonEmptyStringOnyxID(parentReport?.policyID)}`); @@ -167,6 +168,7 @@ function DynamicIOURequestStepDescription({ policy, policyTagList: policyTags, policyCategories, + policyVendors, currentUserAccountIDParam, currentUserEmailParam, isASAPSubmitBetaEnabled, diff --git a/src/pages/iou/request/step/IOURequestStepVendor.tsx b/src/pages/iou/request/step/IOURequestStepVendor.tsx index c818328405d2..6199c1a780bf 100644 --- a/src/pages/iou/request/step/IOURequestStepVendor.tsx +++ b/src/pages/iou/request/step/IOURequestStepVendor.tsx @@ -6,6 +6,7 @@ import type {ListItem} from '@components/SelectionList/types'; import useDelegateAccountID from '@hooks/useDelegateAccountID'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; import usePolicyForTransaction from '@hooks/usePolicyForTransaction'; @@ -69,6 +70,15 @@ function IOURequestStepVendor({ const isFeatureAvailable = hasVendorFeature(policy, isVendorMatchingBetaEnabled); // Stale sessions may never have received the policyVendors_ collection, so fetch it if missing. + const fetchVendorsIfMissing = () => { + if (!isFeatureAvailable || !policy?.id || policyVendors !== undefined) { + return; + } + openPolicyVendorsPage(policy.id); + }; + + useNetwork({onReconnect: fetchVendorsIfMissing}); + useEffect(() => { if (!isFeatureAvailable || !policy?.id || policyVendors !== undefined) { return; diff --git a/src/pages/workspace/vendors/WorkspaceVendorsPage.tsx b/src/pages/workspace/vendors/WorkspaceVendorsPage.tsx index 4b155b06e10f..ca20e992cab7 100644 --- a/src/pages/workspace/vendors/WorkspaceVendorsPage.tsx +++ b/src/pages/workspace/vendors/WorkspaceVendorsPage.tsx @@ -150,6 +150,7 @@ function WorkspaceVendorsPage({policy, route}: WorkspaceVendorsPageProps) { useEffect(() => { fetchVendors(); + // Run only on mount because reconnect refetches are handled by useNetwork above. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts index c0ab4807b7d5..e27b52984a0c 100644 --- a/tests/unit/ViolationUtilsTest.ts +++ b/tests/unit/ViolationUtilsTest.ts @@ -3262,10 +3262,34 @@ describe('getViolationsOnyxData', () => { policyCategories, hasDependentTags: false, isInvoiceTransaction: false, + policyVendors: { + ['v-active']: { + externalID: 'v-active', + name: 'Acme Co', + enabled: true, + }, + }, }); expect(result.value).not.toContainEqual(inactiveVendorViolation); }); + it('preserves an existing violation when policyVendors is not provided even if the vendor is in the raw integration list', () => { + policy = policyWithQBOVendorFeature(); + transaction.comment = {...transaction.comment, vendor: {externalID: 'v-active', wasManuallySet: true}}; + const result = ViolationsUtils.getViolationsOnyxData({ + isVendorMatchingBetaEnabled: true, + ownerLogin: undefined, + updatedTransaction: transaction, + transactionViolations: [inactiveVendorViolation], + policy, + policyTagList: policyTags, + policyCategories, + hasDependentTags: false, + isInvoiceTransaction: false, + }); + expect(result.value).toContainEqual(inactiveVendorViolation); + }); + it('adds the violation when the vendor remains in the raw integration list but is disabled in policyVendors', () => { const activeVendorID = 'v-active'; policy = policyWithQBOVendorFeature(); @@ -3696,6 +3720,13 @@ describe('getViolationsOnyxData', () => { policyCategories, hasDependentTags: false, isInvoiceTransaction: false, + policyVendors: { + xcActive: { + externalID: 'xcActive', + name: 'Acme Xero', + enabled: true, + }, + }, }); expect(result.value).not.toContainEqual(inactiveVendorViolation); }); From a1053a4ea71227d7db6e0c682f85789648212edc Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 17 Oct 2026 00:11:55 +0530 Subject: [PATCH 06/12] Update WorkspaceVendorsPage.tsx --- src/pages/workspace/vendors/WorkspaceVendorsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/vendors/WorkspaceVendorsPage.tsx b/src/pages/workspace/vendors/WorkspaceVendorsPage.tsx index ca20e992cab7..233ede4e6d77 100644 --- a/src/pages/workspace/vendors/WorkspaceVendorsPage.tsx +++ b/src/pages/workspace/vendors/WorkspaceVendorsPage.tsx @@ -150,7 +150,7 @@ function WorkspaceVendorsPage({policy, route}: WorkspaceVendorsPageProps) { useEffect(() => { fetchVendors(); - // Run only on mount because reconnect refetches are handled by useNetwork above. + // Run only on mount because reconnect re-fetches are handled by useNetwork above. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); From 7951f82fd06c73a644fd2b8115277fb3fe47e4b8 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 17 Oct 2026 01:54:55 +0530 Subject: [PATCH 07/12] Remove unnecessary computed key in ViolationUtilsTest --- tests/unit/ViolationUtilsTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts index e27b52984a0c..43cfcdf812f7 100644 --- a/tests/unit/ViolationUtilsTest.ts +++ b/tests/unit/ViolationUtilsTest.ts @@ -3263,7 +3263,7 @@ describe('getViolationsOnyxData', () => { hasDependentTags: false, isInvoiceTransaction: false, policyVendors: { - ['v-active']: { + 'v-active': { externalID: 'v-active', name: 'Acme Co', enabled: true, From c37f226f1b296a332a74f1d0f0dc71626a502071 Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Mon, 19 Oct 2026 23:28:58 +0530 Subject: [PATCH 08/12] Update ViolationUtilsTest.ts --- tests/unit/ViolationUtilsTest.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts index 43cfcdf812f7..f179f089bf9b 100644 --- a/tests/unit/ViolationUtilsTest.ts +++ b/tests/unit/ViolationUtilsTest.ts @@ -3184,7 +3184,7 @@ describe('getViolationsOnyxData', () => { // Pass a `vendors` array to control the synced list, or `null` to simulate the list still // hydrating (`data.vendors` absent, so `isMatchingVendorListLoaded` returns false). - const policyWithQBOVendorFeature = (vendors: Array<{id: string; name: string; currency: string}> | null = [{id: 'v-active', name: 'Acme Co', currency: 'USD'}]) => + const policyWithQBOVendorFeature = (vendors: Array<{id: string; name: string; currency: string}> | null = [{id: 'v_active', name: 'Acme Co', currency: 'USD'}]) => createMock({ requiresTag: false, requiresCategory: false, @@ -3251,7 +3251,7 @@ describe('getViolationsOnyxData', () => { it('removes an existing violation when the vendor is restored in the policy list', () => { policy = policyWithQBOVendorFeature(); - transaction.comment = {...transaction.comment, vendor: {externalID: 'v-active', wasManuallySet: true}}; + transaction.comment = {...transaction.comment, vendor: {externalID: 'v_active', wasManuallySet: true}}; const result = ViolationsUtils.getViolationsOnyxData({ isVendorMatchingBetaEnabled: true, ownerLogin: undefined, @@ -3263,8 +3263,8 @@ describe('getViolationsOnyxData', () => { hasDependentTags: false, isInvoiceTransaction: false, policyVendors: { - 'v-active': { - externalID: 'v-active', + 'v_active': { + externalID: 'v_active', name: 'Acme Co', enabled: true, }, @@ -3275,7 +3275,7 @@ describe('getViolationsOnyxData', () => { it('preserves an existing violation when policyVendors is not provided even if the vendor is in the raw integration list', () => { policy = policyWithQBOVendorFeature(); - transaction.comment = {...transaction.comment, vendor: {externalID: 'v-active', wasManuallySet: true}}; + transaction.comment = {...transaction.comment, vendor: {externalID: 'v_active', wasManuallySet: true}}; const result = ViolationsUtils.getViolationsOnyxData({ isVendorMatchingBetaEnabled: true, ownerLogin: undefined, @@ -3291,7 +3291,7 @@ describe('getViolationsOnyxData', () => { }); it('adds the violation when the vendor remains in the raw integration list but is disabled in policyVendors', () => { - const activeVendorID = 'v-active'; + const activeVendorID = 'v_active'; policy = policyWithQBOVendorFeature(); transaction.comment = {...transaction.comment, vendor: {externalID: activeVendorID, wasManuallySet: true}}; const policyVendors: PolicyVendors = { @@ -3317,7 +3317,7 @@ describe('getViolationsOnyxData', () => { }); it('retains an existing violation through an optimistic update while the vendor remains disabled in policyVendors', () => { - const activeVendorID = 'v-active'; + const activeVendorID = 'v_active'; policy = policyWithQBOVendorFeature(); transaction.comment = {...transaction.comment, vendor: {externalID: activeVendorID, wasManuallySet: true}}; const policyVendors: PolicyVendors = { @@ -3343,7 +3343,7 @@ describe('getViolationsOnyxData', () => { }); it('removes an existing violation when the vendor is re-enabled in policyVendors', () => { - const activeVendorID = 'v-active'; + const activeVendorID = 'v_active'; policy = policyWithQBOVendorFeature(); transaction.comment = {...transaction.comment, vendor: {externalID: activeVendorID, wasManuallySet: true}}; const policyVendors: PolicyVendors = { @@ -3369,7 +3369,7 @@ describe('getViolationsOnyxData', () => { }); it('leaves the violation untouched when the active integration vendor list is not loaded yet even if policyVendors is present', () => { - const activeVendorID = 'v-active'; + const activeVendorID = 'v_active'; policy = policyWithQBOVendorFeature(null); transaction.comment = {...transaction.comment, vendor: {externalID: activeVendorID, wasManuallySet: true}}; const policyVendors: PolicyVendors = { @@ -3418,11 +3418,11 @@ describe('getViolationsOnyxData', () => { connections: { [CONST.POLICY.CONNECTIONS.NAME.QBO]: { config: {nonReimbursableExpensesExportDestination: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.VENDOR_BILL}, - data: {vendors: [{id: 'v-active', name: 'Acme Co', currency: 'USD'}]}, + data: {vendors: [{id: 'v_active', name: 'Acme Co', currency: 'USD'}]}, }, }, }); - transaction.comment = {...transaction.comment, vendor: {externalID: 'v-active', wasManuallySet: true}}; + transaction.comment = {...transaction.comment, vendor: {externalID: 'v_active', wasManuallySet: true}}; const result = ViolationsUtils.getViolationsOnyxData({ isVendorMatchingBetaEnabled: true, ownerLogin: undefined, @@ -3496,7 +3496,7 @@ describe('getViolationsOnyxData', () => { it('preserves an existing violation while the QBO vendor list is still hydrating', () => { // Given the vendor list is still hydrating but an inactive-vendor violation already exists from a prior real check policy = policyWithQBOVendorFeature(null); - transaction.comment = {...transaction.comment, vendor: {externalID: 'v-active', wasManuallySet: true}}; + transaction.comment = {...transaction.comment, vendor: {externalID: 'v_active', wasManuallySet: true}}; // When violations are recomputed const result = ViolationsUtils.getViolationsOnyxData({ @@ -3520,7 +3520,7 @@ describe('getViolationsOnyxData', () => { // hydrating. Intacct vendors carry the display name in `value`. const policyWithIntacctVendorFeature = ( nonReimbursable: SageIntacctExportConfig['nonReimbursable'] = CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE, - vendors: Array<{id: string; name: string; value: string}> | null = [{id: 'iv-active', name: 'V001', value: 'Acme Intacct'}], + vendors: Array<{id: string; name: string; value: string}> | null = [{id: 'iv_active', name: 'V001', value: 'Acme Intacct'}], ) => createMock({ requiresTag: false, @@ -3555,7 +3555,7 @@ describe('getViolationsOnyxData', () => { it('does not add the violation when the Intacct vendor is in the synced list', () => { isBetaEnabledSpy.mockImplementation(() => false); policy = policyWithIntacctVendorFeature(); - transaction.comment = {...transaction.comment, vendor: {externalID: 'iv-active', wasManuallySet: true}}; + transaction.comment = {...transaction.comment, vendor: {externalID: 'iv_active', wasManuallySet: true}}; const result = ViolationsUtils.getViolationsOnyxData({ ownerLogin: undefined, isVendorMatchingBetaEnabled: false, @@ -3683,7 +3683,7 @@ describe('getViolationsOnyxData', () => { connections: { [CONST.POLICY.CONNECTIONS.NAME.QBO]: { config: {nonReimbursableExpensesExportDestination: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}, - data: {vendors: [{id: 'v-active', name: 'Acme QBO', currency: 'USD'}]}, + data: {vendors: [{id: 'v_active', name: 'Acme QBO', currency: 'USD'}]}, }, [CONST.POLICY.CONNECTIONS.NAME.XERO]: { config: {isConfigured: true}, @@ -3839,7 +3839,7 @@ describe('getViolationsOnyxData', () => { connections: { [CONST.POLICY.CONNECTIONS.NAME.QBO]: { config: {nonReimbursableExpensesExportDestination: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}, - data: {vendors: [{id: 'v-active', name: 'Acme QBO', currency: 'USD'}]}, + data: {vendors: [{id: 'v_active', name: 'Acme QBO', currency: 'USD'}]}, }, [CONST.POLICY.CONNECTIONS.NAME.XERO]: { config: {}, From ec36de7c88748f21f532e464fce0ded7f8bb2ae4 Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:29:39 +0530 Subject: [PATCH 09/12] Update ViolationUtilsTest.ts --- tests/unit/ViolationUtilsTest.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts index f179f089bf9b..a8ae4d18fb83 100644 --- a/tests/unit/ViolationUtilsTest.ts +++ b/tests/unit/ViolationUtilsTest.ts @@ -3184,7 +3184,7 @@ describe('getViolationsOnyxData', () => { // Pass a `vendors` array to control the synced list, or `null` to simulate the list still // hydrating (`data.vendors` absent, so `isMatchingVendorListLoaded` returns false). - const policyWithQBOVendorFeature = (vendors: Array<{id: string; name: string; currency: string}> | null = [{id: 'v_active', name: 'Acme Co', currency: 'USD'}]) => + const policyWithQBOVendorFeature = (vendors: Array<{id: string; name: string; currency: string}> | null = [{id: 'vActive', name: 'Acme Co', currency: 'USD'}]) => createMock({ requiresTag: false, requiresCategory: false, @@ -3251,7 +3251,7 @@ describe('getViolationsOnyxData', () => { it('removes an existing violation when the vendor is restored in the policy list', () => { policy = policyWithQBOVendorFeature(); - transaction.comment = {...transaction.comment, vendor: {externalID: 'v_active', wasManuallySet: true}}; + transaction.comment = {...transaction.comment, vendor: {externalID: 'vActive', wasManuallySet: true}}; const result = ViolationsUtils.getViolationsOnyxData({ isVendorMatchingBetaEnabled: true, ownerLogin: undefined, @@ -3263,8 +3263,8 @@ describe('getViolationsOnyxData', () => { hasDependentTags: false, isInvoiceTransaction: false, policyVendors: { - 'v_active': { - externalID: 'v_active', + 'vActive': { + externalID: 'vActive', name: 'Acme Co', enabled: true, }, @@ -3275,7 +3275,7 @@ describe('getViolationsOnyxData', () => { it('preserves an existing violation when policyVendors is not provided even if the vendor is in the raw integration list', () => { policy = policyWithQBOVendorFeature(); - transaction.comment = {...transaction.comment, vendor: {externalID: 'v_active', wasManuallySet: true}}; + transaction.comment = {...transaction.comment, vendor: {externalID: 'vActive', wasManuallySet: true}}; const result = ViolationsUtils.getViolationsOnyxData({ isVendorMatchingBetaEnabled: true, ownerLogin: undefined, @@ -3291,7 +3291,7 @@ describe('getViolationsOnyxData', () => { }); it('adds the violation when the vendor remains in the raw integration list but is disabled in policyVendors', () => { - const activeVendorID = 'v_active'; + const activeVendorID = 'vActive'; policy = policyWithQBOVendorFeature(); transaction.comment = {...transaction.comment, vendor: {externalID: activeVendorID, wasManuallySet: true}}; const policyVendors: PolicyVendors = { @@ -3317,7 +3317,7 @@ describe('getViolationsOnyxData', () => { }); it('retains an existing violation through an optimistic update while the vendor remains disabled in policyVendors', () => { - const activeVendorID = 'v_active'; + const activeVendorID = 'vActive'; policy = policyWithQBOVendorFeature(); transaction.comment = {...transaction.comment, vendor: {externalID: activeVendorID, wasManuallySet: true}}; const policyVendors: PolicyVendors = { @@ -3343,7 +3343,7 @@ describe('getViolationsOnyxData', () => { }); it('removes an existing violation when the vendor is re-enabled in policyVendors', () => { - const activeVendorID = 'v_active'; + const activeVendorID = 'vActive'; policy = policyWithQBOVendorFeature(); transaction.comment = {...transaction.comment, vendor: {externalID: activeVendorID, wasManuallySet: true}}; const policyVendors: PolicyVendors = { @@ -3369,7 +3369,7 @@ describe('getViolationsOnyxData', () => { }); it('leaves the violation untouched when the active integration vendor list is not loaded yet even if policyVendors is present', () => { - const activeVendorID = 'v_active'; + const activeVendorID = 'vActive'; policy = policyWithQBOVendorFeature(null); transaction.comment = {...transaction.comment, vendor: {externalID: activeVendorID, wasManuallySet: true}}; const policyVendors: PolicyVendors = { @@ -3418,11 +3418,11 @@ describe('getViolationsOnyxData', () => { connections: { [CONST.POLICY.CONNECTIONS.NAME.QBO]: { config: {nonReimbursableExpensesExportDestination: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.VENDOR_BILL}, - data: {vendors: [{id: 'v_active', name: 'Acme Co', currency: 'USD'}]}, + data: {vendors: [{id: 'vActive', name: 'Acme Co', currency: 'USD'}]}, }, }, }); - transaction.comment = {...transaction.comment, vendor: {externalID: 'v_active', wasManuallySet: true}}; + transaction.comment = {...transaction.comment, vendor: {externalID: 'vActive', wasManuallySet: true}}; const result = ViolationsUtils.getViolationsOnyxData({ isVendorMatchingBetaEnabled: true, ownerLogin: undefined, @@ -3496,7 +3496,7 @@ describe('getViolationsOnyxData', () => { it('preserves an existing violation while the QBO vendor list is still hydrating', () => { // Given the vendor list is still hydrating but an inactive-vendor violation already exists from a prior real check policy = policyWithQBOVendorFeature(null); - transaction.comment = {...transaction.comment, vendor: {externalID: 'v_active', wasManuallySet: true}}; + transaction.comment = {...transaction.comment, vendor: {externalID: 'vActive', wasManuallySet: true}}; // When violations are recomputed const result = ViolationsUtils.getViolationsOnyxData({ @@ -3520,7 +3520,7 @@ describe('getViolationsOnyxData', () => { // hydrating. Intacct vendors carry the display name in `value`. const policyWithIntacctVendorFeature = ( nonReimbursable: SageIntacctExportConfig['nonReimbursable'] = CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE, - vendors: Array<{id: string; name: string; value: string}> | null = [{id: 'iv_active', name: 'V001', value: 'Acme Intacct'}], + vendors: Array<{id: string; name: string; value: string}> | null = [{id: 'ivActive', name: 'V001', value: 'Acme Intacct'}], ) => createMock({ requiresTag: false, @@ -3555,7 +3555,7 @@ describe('getViolationsOnyxData', () => { it('does not add the violation when the Intacct vendor is in the synced list', () => { isBetaEnabledSpy.mockImplementation(() => false); policy = policyWithIntacctVendorFeature(); - transaction.comment = {...transaction.comment, vendor: {externalID: 'iv_active', wasManuallySet: true}}; + transaction.comment = {...transaction.comment, vendor: {externalID: 'ivActive', wasManuallySet: true}}; const result = ViolationsUtils.getViolationsOnyxData({ ownerLogin: undefined, isVendorMatchingBetaEnabled: false, @@ -3683,7 +3683,7 @@ describe('getViolationsOnyxData', () => { connections: { [CONST.POLICY.CONNECTIONS.NAME.QBO]: { config: {nonReimbursableExpensesExportDestination: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}, - data: {vendors: [{id: 'v_active', name: 'Acme QBO', currency: 'USD'}]}, + data: {vendors: [{id: 'vActive', name: 'Acme QBO', currency: 'USD'}]}, }, [CONST.POLICY.CONNECTIONS.NAME.XERO]: { config: {isConfigured: true}, @@ -3839,7 +3839,7 @@ describe('getViolationsOnyxData', () => { connections: { [CONST.POLICY.CONNECTIONS.NAME.QBO]: { config: {nonReimbursableExpensesExportDestination: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}, - data: {vendors: [{id: 'v_active', name: 'Acme QBO', currency: 'USD'}]}, + data: {vendors: [{id: 'vActive', name: 'Acme QBO', currency: 'USD'}]}, }, [CONST.POLICY.CONNECTIONS.NAME.XERO]: { config: {}, From 708967ad55d9f2d83c17c62c663b95c61a003d69 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:56:40 +0530 Subject: [PATCH 10/12] Fix fmt --- tests/unit/ViolationUtilsTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts index a8ae4d18fb83..c02627c66be2 100644 --- a/tests/unit/ViolationUtilsTest.ts +++ b/tests/unit/ViolationUtilsTest.ts @@ -3263,7 +3263,7 @@ describe('getViolationsOnyxData', () => { hasDependentTags: false, isInvoiceTransaction: false, policyVendors: { - 'vActive': { + vActive: { externalID: 'vActive', name: 'Acme Co', enabled: true, From 3e2c3c1d41ca4c691c56434561d00f20c2854262 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Thu, 22 Oct 2026 02:48:05 +0530 Subject: [PATCH 11/12] Fix read-only vendor toggle and vendor picker loading states --- .../Tables/WorkspaceVendorsTable/index.tsx | 3 +- src/languages/zh-hans.ts | 2 +- src/libs/actions/IOU/UpdateMoneyRequest.ts | 2 +- .../iou/request/step/IOURequestStepVendor.tsx | 28 ++++++++++++------- .../vendors/WorkspaceVendorsPage.tsx | 1 - 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/components/Tables/WorkspaceVendorsTable/index.tsx b/src/components/Tables/WorkspaceVendorsTable/index.tsx index a43c313cbcb0..7b3e7e981e2e 100644 --- a/src/components/Tables/WorkspaceVendorsTable/index.tsx +++ b/src/components/Tables/WorkspaceVendorsTable/index.tsx @@ -23,7 +23,6 @@ type WorkspaceVendorTableColumnKey = 'name' | 'enabled'; type WorkspaceVendorTableRowData = TableData & { name: string; enabled: boolean; - disabled: boolean; isLocked: boolean; errors?: OnyxCommon.Errors; pendingAction?: OnyxCommon.PendingAction; @@ -111,7 +110,7 @@ function WorkspaceVendorsTable({vendors, selectionEnabled = false, selectedKeys diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 9b916dd48478..0c72571c0562 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6764,7 +6764,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM enableVendor: '启用供应商', enableVendors: '启用供应商', disableVendor: '停用供应商', - disableVendors: '禁用供应商', + disableVendors: '停用供应商', updateFailureMessage: '更新供应商时出错,请重试', }, tags: { diff --git a/src/libs/actions/IOU/UpdateMoneyRequest.ts b/src/libs/actions/IOU/UpdateMoneyRequest.ts index f2a19294be74..f04261c5efbb 100644 --- a/src/libs/actions/IOU/UpdateMoneyRequest.ts +++ b/src/libs/actions/IOU/UpdateMoneyRequest.ts @@ -864,7 +864,7 @@ function updateMoneyRequestVendor({ } // Optimistically clear any existing inactive-vendor violation. This is the user-driven write - // path: the vendor selector RHP only offers vendors from `getMatchingVendors(policy)`, so a user + // path: the vendor selector RHP only offers vendors that are enabled on the policy, so a user // pick is always a valid vendor (resolving the violation); clearing the vendor likewise // resolves it (no vendor → no inactive-vendor). Without this, the stale violation persists // in Onyx until some unrelated recalculation fires, keeping the expense incorrectly flagged. diff --git a/src/pages/iou/request/step/IOURequestStepVendor.tsx b/src/pages/iou/request/step/IOURequestStepVendor.tsx index 37f5b48fd1c9..5284d2bb6068 100644 --- a/src/pages/iou/request/step/IOURequestStepVendor.tsx +++ b/src/pages/iou/request/step/IOURequestStepVendor.tsx @@ -1,3 +1,4 @@ +import ActivityIndicator from '@components/ActivityIndicator'; import BlockingView from '@components/BlockingViews/BlockingView'; import SelectionList from '@components/SelectionList'; import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem'; @@ -26,7 +27,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; -import React, {useEffect, useState} from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; @@ -69,21 +70,18 @@ function IOURequestStepVendor({ const isVendorMatchingBetaEnabled = isBetaEnabled(CONST.BETAS.VENDOR_MATCHING); const isFeatureAvailable = hasVendorFeature(policy, isVendorMatchingBetaEnabled); // Stale sessions may never have received the policyVendors_ collection, so fetch it if missing. - const fetchVendorsIfMissing = () => { + const fetchVendorsIfMissing = useCallback(() => { if (!isFeatureAvailable || !policy?.id || policyVendors !== undefined) { return; } openPolicyVendorsPage(policy.id); - }; + }, [isFeatureAvailable, policy?.id, policyVendors]); - useNetwork({onReconnect: fetchVendorsIfMissing}); + const {isOffline} = useNetwork({onReconnect: fetchVendorsIfMissing}); useEffect(() => { - if (!isFeatureAvailable || !policy?.id || policyVendors !== undefined) { - return; - } - openPolicyVendorsPage(policy.id); - }, [isFeatureAvailable, policy?.id, policyVendors]); + fetchVendorsIfMissing(); + }, [fetchVendorsIfMissing]); const emptyState = getVendorEmptyState(policy, translate); // Vendor is scoped to non-reimbursable expenses on a policy expense chat; block deep-link / stale-open access if the transaction is reimbursable or is an invoice (invoices are non-reimbursable but don't route through the vendor-matching flow). @@ -92,7 +90,9 @@ function IOURequestStepVendor({ const currentVendorID = transaction?.comment?.vendor?.externalID; const currentVendor = currentVendorID ? policyVendors?.[currentVendorID] : undefined; - const isCurrentVendorDisabled = !!currentVendorID && !currentVendor?.enabled; + const isCurrentVendorDisabled = !!currentVendorID && policyVendors !== undefined && !currentVendor?.enabled; + + const isLoading = !isOffline && policyVendors === undefined; const enabledVendors = Object.values(policyVendors ?? {}).filter((vendor) => vendor.enabled); const sortedEnabledVendors = sortVendors(enabledVendors, localeCompare); @@ -193,6 +193,14 @@ function IOURequestStepVendor({ if (!didScreenTransitionEnd) { return null; } + if (isLoading) { + return ( + + ); + } return ( Date: Thu, 22 Oct 2026 02:54:28 +0530 Subject: [PATCH 12/12] Remove useCallback from vendor fetch for React Compiler compliance --- src/pages/iou/request/step/IOURequestStepVendor.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepVendor.tsx b/src/pages/iou/request/step/IOURequestStepVendor.tsx index 5284d2bb6068..2dca9fd0201d 100644 --- a/src/pages/iou/request/step/IOURequestStepVendor.tsx +++ b/src/pages/iou/request/step/IOURequestStepVendor.tsx @@ -27,7 +27,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; -import React, {useCallback, useEffect, useState} from 'react'; +import React, {useEffect, useState} from 'react'; import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; @@ -70,18 +70,20 @@ function IOURequestStepVendor({ const isVendorMatchingBetaEnabled = isBetaEnabled(CONST.BETAS.VENDOR_MATCHING); const isFeatureAvailable = hasVendorFeature(policy, isVendorMatchingBetaEnabled); // Stale sessions may never have received the policyVendors_ collection, so fetch it if missing. - const fetchVendorsIfMissing = useCallback(() => { + const fetchVendorsIfMissing = () => { if (!isFeatureAvailable || !policy?.id || policyVendors !== undefined) { return; } openPolicyVendorsPage(policy.id); - }, [isFeatureAvailable, policy?.id, policyVendors]); + }; const {isOffline} = useNetwork({onReconnect: fetchVendorsIfMissing}); useEffect(() => { fetchVendorsIfMissing(); - }, [fetchVendorsIfMissing]); + // Re-run when the policy loads or the collection is cleared, so a missing fetch retries. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isFeatureAvailable, policy?.id, policyVendors]); const emptyState = getVendorEmptyState(policy, translate); // Vendor is scoped to non-reimbursable expenses on a policy expense chat; block deep-link / stale-open access if the transaction is reimbursable or is an invoice (invoices are non-reimbursable but don't route through the vendor-matching flow).