From a0c792b85d8765659d96504f03e659bf6c97368c Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Fri, 25 Sep 2026 11:51:14 +0200 Subject: [PATCH 01/11] bump reanimated and worklets --- ios/Podfile.lock | 40 ++--- package-lock.json | 46 +++--- package.json | 8 +- ...wn+0.1.336+001+worklets-0.12-runSync.patch | 29 ++++ .../react-native-live-markdown/details.md | 16 ++ patches/expo-modules-core/details.md | 2 +- ...re+57.0.15+001+disableViewRecycling.patch} | 4 +- patches/react-native-reanimated/details.md | 9 -- ...5.5+001+catch-all-exceptions-on-stoi.patch | 13 -- patches/whatwg-fetch/details.md | 2 +- ...+use-microtask-queue-for-xhr-events.patch} | 14 +- .../DatePicker/CalendarPicker/index.tsx | 143 ++++++++++-------- 12 files changed, 181 insertions(+), 145 deletions(-) create mode 100644 patches/@expensify/react-native-live-markdown/@expensify+react-native-live-markdown+0.1.336+001+worklets-0.12-runSync.patch create mode 100644 patches/@expensify/react-native-live-markdown/details.md rename patches/expo-modules-core/{expo-modules-core+57.0.7+001+disableViewRecycling.patch => expo-modules-core+57.0.15+001+disableViewRecycling.patch} (83%) delete mode 100644 patches/react-native-reanimated/details.md delete mode 100644 patches/react-native-reanimated/react-native-reanimated+4.5.5+001+catch-all-exceptions-on-stoi.patch rename patches/whatwg-fetch/{whatwg-fetch+3.6.2+001+use-microtask-queue-for-xhr-events.patch => whatwg-fetch+3.6.20+001+use-microtask-queue-for-xhr-events.patch} (88%) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 307217ee3162..c4981b96a354 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -146,7 +146,7 @@ PODS: - ExpoModulesCore - ExpoLogBox (57.0.1): - React-Core - - ExpoModulesCore (57.0.7): + - ExpoModulesCore (57.0.15): - ExpoModulesJSI - hermes-engine - RCTRequired @@ -170,10 +170,10 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - ExpoModulesJSI (57.0.4): + - ExpoModulesJSI (57.0.8): - React-Core - ReactCommon - - ExpoModulesWorklets (57.0.7): + - ExpoModulesWorklets (57.0.15): - ExpoModulesCore - ExpoModulesJSI - ExpoStoreReview (57.0.1): @@ -3095,7 +3095,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - RNReanimated (4.5.5): + - RNReanimated (4.7.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -3117,12 +3117,12 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - ReactNativeDependencies - - RNReanimated/apple (= 4.5.5) - - RNReanimated/common (= 4.5.5) - - RNReanimated/view (= 4.5.5) + - RNReanimated/apple (= 4.7.0) + - RNReanimated/common (= 4.7.0) + - RNReanimated/view (= 4.7.0) - RNWorklets - Yoga - - RNReanimated/apple (4.5.5): + - RNReanimated/apple (4.7.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -3146,7 +3146,7 @@ PODS: - ReactNativeDependencies - RNWorklets - Yoga - - RNReanimated/common (4.5.5): + - RNReanimated/common (4.7.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -3170,7 +3170,7 @@ PODS: - ReactNativeDependencies - RNWorklets - Yoga - - RNReanimated/view (4.5.5): + - RNReanimated/view (4.7.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -3331,7 +3331,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - RNWorklets (0.11.1): + - RNWorklets (0.13.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -3353,10 +3353,10 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - ReactNativeDependencies - - RNWorklets/apple (= 0.11.1) - - RNWorklets/common (= 0.11.1) + - RNWorklets/apple (= 0.13.0) + - RNWorklets/common (= 0.13.0) - Yoga - - RNWorklets/apple (0.11.1): + - RNWorklets/apple (0.13.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -3379,7 +3379,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - RNWorklets/common (0.11.1): + - RNWorklets/common (0.13.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -3955,9 +3955,9 @@ SPEC CHECKSUMS: ExpoImageManipulator: d3c962dabd86b331980f8ec6794c6073b3cb541a ExpoLocation: 187949911b868f72e34a8a6f8f487207ba86388a ExpoLogBox: 8a3cfa2897e088083cd49fc1ff7fdb2de4385547 - ExpoModulesCore: 5b2895ff83c42d320aaaa713c44051d84b0de698 - ExpoModulesJSI: 3accd404d715d63e7b04015d20c857e903e3bc73 - ExpoModulesWorklets: b21b8f233b5fd71ca74327217601fa4ead9ddaaf + ExpoModulesCore: 9c02efc618119023146b9b8db9c0930e43ac977d + ExpoModulesJSI: 4f849c58e99b57c2438b21ed6bc2fcb6f984a0d1 + ExpoModulesWorklets: 5e70b46d570c5b1b702c18bc8ac8d1af0171450e ExpoStoreReview: 2a0f6b8e04112aea2f6e3e1cb84109d78f4d041f ExpoTaskManager: f68326227852a30148700965c37c058c1473c6a2 ExpoVideo: b1d9a325ddcd4f3ed8ccb374c01b688e98bc6a04 @@ -4109,12 +4109,12 @@ SPEC CHECKSUMS: RNNitroSQLite: e354c4a31133050e6bbde1c136b8ac4857830236 RNPermissions: 32bd3b977f28442af0900b102f4458f0f9f9b77c RNReactNativeHapticFeedback: 6c8677db48f62a635e6a2065f5d2bd02498d7a73 - RNReanimated: f093c30aa11e45a8044dcc19417f44cffa1c4e1d + RNReanimated: e75c988a6527ec651ea2957b93891ddb64d530d4 RNScreens: 85c533985720c563272c6f6dd19e1278dfd0f5d4 RNSentry: c878a3c462c1097f63a81615c784d44641ae92e4 RNShare: eb378c57de30a83b0253eed74762ee99249f47d7 RNSVG: 394cfd0518613b144c65ffe69045f827b78a7a7c - RNWorklets: f0734080219ea54d479c179da498c9b24611faed + RNWorklets: 3da8f468c848547646f4e6b12fb46ad93a2cdee2 SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a SDWebImageAVIFCoder: afe194a084e851f70228e4be35ef651df0fc5c57 SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c diff --git a/package-lock.json b/package-lock.json index db894b3f9e03..bc12ae26bfc5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,7 +75,7 @@ "expo-image": "57.0.1", "expo-image-manipulator": "57.0.6", "expo-location": "57.0.6", - "expo-modules-core": "57.0.7", + "expo-modules-core": "57.0.15", "expo-store-review": "57.0.1", "expo-task-manager": "57.0.6", "expo-video": "57.0.2", @@ -135,7 +135,7 @@ "react-native-qrcode-svg": "6.3.21", "react-native-quick-base64": "^3.0.1", "react-native-quick-crypto": "^1.1.6", - "react-native-reanimated": "4.5.5", + "react-native-reanimated": "4.7.0", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "5.9.1", "react-native-screens": "4.25.0", @@ -147,7 +147,7 @@ "react-native-vision-camera": "^4.7.2", "react-native-web": "0.21.2", "react-native-webview": "16.0.0", - "react-native-worklets": "0.11.1", + "react-native-worklets": "0.13.0", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1", @@ -27310,13 +27310,13 @@ } }, "node_modules/expo-modules-core": { - "version": "57.0.7", - "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-57.0.7.tgz", - "integrity": "sha512-5HrbCfgYmLs0a5dzfM4GmRGelVTPIg+eYp0vmSbWnKRTOPj5DyVOfg1rHGEsuNKp07QwDxfLJfQVp8CNbuvxMQ==", + "version": "57.0.15", + "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-57.0.15.tgz", + "integrity": "sha512-HBxPXsx3eVLLRjAY57/8ghKDWIxjaEbluKKYFIq1OHPJC85ZfHsawwc0SOU4Z/qu+mdLz7Pwt5IkkWimeNSH5A==", "license": "MIT", "dependencies": { "@expo/expo-modules-macros-plugin": "0.6.1", - "expo-modules-jsi": "~57.0.4", + "expo-modules-jsi": "~57.0.7", "invariant": "^2.2.4" }, "peerDependencies": { @@ -27331,9 +27331,9 @@ } }, "node_modules/expo-modules-jsi": { - "version": "57.0.4", - "resolved": "https://registry.npmjs.org/expo-modules-jsi/-/expo-modules-jsi-57.0.4.tgz", - "integrity": "sha512-vt7FyqUqqFXiRVnBqYD7y+GSPTgeua5Ocoy0+SYt+RSHkZEA2Fyop7If3g1TYDzQObYybPRo7TG2Rle1XLaWFw==", + "version": "57.0.8", + "resolved": "https://registry.npmjs.org/expo-modules-jsi/-/expo-modules-jsi-57.0.8.tgz", + "integrity": "sha512-MFwvKP0FXLkEET27KsKvOh/L7Dpu2d00UgzAlmKozJgtI23WTKSyKPxu+EOSs9peSgkHzKI0vQOhWjOseGBMkQ==", "license": "MIT", "peerDependencies": { "react-native": "*" @@ -38075,9 +38075,9 @@ } }, "node_modules/react-native-reanimated": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.5.5.tgz", - "integrity": "sha512-xtJXZRZ1vkec1AIUVG02St2sbyZS5jc7227TlDu/HonqUYPeZAfiC4RI2jqL1dVMNBu1+7oBx/c9QkjF2kg6Lg==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.7.0.tgz", + "integrity": "sha512-8TyTrPisqz7WzmbhCz8F60Uw7NI2QeYWrY5Hk58/kurCA5L5KuHmhyQjxeq5k2YalpXUAKbdkn/mAMZWm/fU0g==", "license": "MIT", "dependencies": { "react-native-is-edge-to-edge": "^1.3.1", @@ -38085,8 +38085,8 @@ }, "peerDependencies": { "react": "*", - "react-native": "0.83 - 0.86", - "react-native-worklets": "0.10.x - 0.11.x" + "react-native": "0.86 - 0.88", + "react-native-worklets": "0.13.x" } }, "node_modules/react-native-reanimated/node_modules/semver": { @@ -38371,9 +38371,9 @@ } }, "node_modules/react-native-worklets": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.11.1.tgz", - "integrity": "sha512-e+deeFdC45RrQcRh0bYr0fbaaXSB6+mGTX4xlEzT76PCvNJEOmu/5aDbT7gpd9AMFAGhkO2b0eoDBErWuEvZ8Q==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.13.0.tgz", + "integrity": "sha512-1OrhAeAKaZF4XrBa4YbPQbKzNOpjh52HDlId8e6ete1OCHNdAlrQHzG4gznJ225EAj3dgQXHViJvK51M9B52jQ==", "license": "MIT", "dependencies": { "@babel/generator": "^7.27.1", @@ -38388,14 +38388,16 @@ "@babel/preset-typescript": "^7.28.5", "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", - "semver": "^7.7.4" + "semver": "^7.7.4", + "whatwg-fetch": "^3.6.20" }, "peerDependencies": { "@babel/core": "*", "@react-native/metro-config": "*", "react": "*", - "react-native": "0.83 - 0.86" + "react-native": "0.86 - 0.88" } }, "node_modules/react-native-worklets/node_modules/semver": { @@ -43047,7 +43049,9 @@ } }, "node_modules/whatwg-fetch": { - "version": "3.6.2", + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", "license": "MIT" }, "node_modules/whatwg-url": { diff --git a/package.json b/package.json index bf28ef6c1938..4822ebdce4ad 100644 --- a/package.json +++ b/package.json @@ -151,7 +151,7 @@ "expo-image": "57.0.1", "expo-image-manipulator": "57.0.6", "expo-location": "57.0.6", - "expo-modules-core": "57.0.7", + "expo-modules-core": "57.0.15", "expo-store-review": "57.0.1", "expo-task-manager": "57.0.6", "expo-video": "57.0.2", @@ -211,7 +211,7 @@ "react-native-qrcode-svg": "6.3.21", "react-native-quick-base64": "^3.0.1", "react-native-quick-crypto": "^1.1.6", - "react-native-reanimated": "4.5.5", + "react-native-reanimated": "4.7.0", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "5.9.1", "react-native-screens": "4.25.0", @@ -223,7 +223,7 @@ "react-native-vision-camera": "^4.7.2", "react-native-web": "0.21.2", "react-native-webview": "16.0.0", - "react-native-worklets": "0.11.1", + "react-native-worklets": "0.13.0", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1", @@ -434,7 +434,7 @@ "expo": "57.0.8" }, "expo-modules-core": { - "react-native-worklets": "0.11.1" + "react-native-worklets": "0.13.0" } }, "expo": { diff --git a/patches/@expensify/react-native-live-markdown/@expensify+react-native-live-markdown+0.1.336+001+worklets-0.12-runSync.patch b/patches/@expensify/react-native-live-markdown/@expensify+react-native-live-markdown+0.1.336+001+worklets-0.12-runSync.patch new file mode 100644 index 000000000000..7ef9b44d23ec --- /dev/null +++ b/patches/@expensify/react-native-live-markdown/@expensify+react-native-live-markdown+0.1.336+001+worklets-0.12-runSync.patch @@ -0,0 +1,29 @@ +diff --git a/node_modules/@expensify/react-native-live-markdown/android/src/main/cpp/MarkdownParser.cpp b/node_modules/@expensify/react-native-live-markdown/android/src/main/cpp/MarkdownParser.cpp +index 4edd28e..534e20c 100644 +--- a/node_modules/@expensify/react-native-live-markdown/android/src/main/cpp/MarkdownParser.cpp ++++ b/node_modules/@expensify/react-native-live-markdown/android/src/main/cpp/MarkdownParser.cpp +@@ -17,7 +17,7 @@ namespace livemarkdown { + const auto markdownWorklet = expensify::livemarkdown::getMarkdownWorklet(parserId); + + const auto input = jsi::String::createFromUtf8(rt, text->toStdString()); +- const auto output = markdownRuntime->runGuarded(markdownWorklet, input); ++ const auto output = markdownRuntime->runSync(markdownWorklet, input); + + const auto json = rt.global().getPropertyAsObject(rt, "JSON").getPropertyAsFunction(rt, "stringify").call(rt, output).asString(rt).utf8(rt); + return jni::make_jstring(json); +diff --git a/node_modules/@expensify/react-native-live-markdown/apple/MarkdownParser.mm b/node_modules/@expensify/react-native-live-markdown/apple/MarkdownParser.mm +index 89bb209..780034f 100644 +--- a/node_modules/@expensify/react-native-live-markdown/apple/MarkdownParser.mm ++++ b/node_modules/@expensify/react-native-live-markdown/apple/MarkdownParser.mm +@@ -223,9 +223,9 @@ - (void)drainPendingWarmups + + jsi::Value output; + try { +- output = markdownRuntime->runGuarded(markdownWorklet, input); ++ output = markdownRuntime->runSync(markdownWorklet, input); + } catch (const jsi::JSError &error) { +- // Skip formatting, runGuarded will show the error in LogBox ++ // Skip formatting, runSync will show the error in LogBox + return @[]; + } + diff --git a/patches/@expensify/react-native-live-markdown/details.md b/patches/@expensify/react-native-live-markdown/details.md new file mode 100644 index 000000000000..8f564fe46b9d --- /dev/null +++ b/patches/@expensify/react-native-live-markdown/details.md @@ -0,0 +1,16 @@ +# `@expensify/react-native-live-markdown` patches + +### [@expensify+react-native-live-markdown+0.1.336+001+worklets-0.12-runSync.patch](@expensify+react-native-live-markdown+0.1.336+001+worklets-0.12-runSync.patch) + +- Reason: + + ``` + react-native-worklets 0.12.0 removed the deprecated `WorkletRuntime::runGuarded` C++ API, so the native + Markdown parser no longer compiles against worklets 0.13 (Android `MarkdownParser.cpp` and iOS + `MarkdownParser.mm`). In worklets 0.11 `runGuarded` was only a deprecated alias of `runSync`, so this patch + calls `runSync` directly, which works on both worklets 0.11 and 0.13. + ``` + +- Upstream PR/issue: 🛑 +- E/App issue: https://github.com/Expensify/App/issues/101440 +- PR introducing patch: 🛑 diff --git a/patches/expo-modules-core/details.md b/patches/expo-modules-core/details.md index 50bed85a837c..0ad1cb38f52e 100644 --- a/patches/expo-modules-core/details.md +++ b/patches/expo-modules-core/details.md @@ -1,6 +1,6 @@ # `expo-modules-core` patches -### [expo-modules-core+57.0.7+001+disableViewRecycling.patch](expo-modules-core+57.0.7+001+disableViewRecycling.patch) +### [expo-modules-core+57.0.15+001+disableViewRecycling.patch](expo-modules-core+57.0.15+001+disableViewRecycling.patch) - Reason: Opt-out from RN Fabric view recycling - Upstream PR/issue: https://expo.canny.io/feature-requests/p/opt-out-of-react-native-fabric-view-recycling-for-expofabricviewobjc diff --git a/patches/expo-modules-core/expo-modules-core+57.0.7+001+disableViewRecycling.patch b/patches/expo-modules-core/expo-modules-core+57.0.15+001+disableViewRecycling.patch similarity index 83% rename from patches/expo-modules-core/expo-modules-core+57.0.7+001+disableViewRecycling.patch rename to patches/expo-modules-core/expo-modules-core+57.0.15+001+disableViewRecycling.patch index 88bc73dcde81..2a6f018796d9 100644 --- a/patches/expo-modules-core/expo-modules-core+57.0.7+001+disableViewRecycling.patch +++ b/patches/expo-modules-core/expo-modules-core+57.0.15+001+disableViewRecycling.patch @@ -1,8 +1,8 @@ diff --git a/node_modules/expo-modules-core/ios/Fabric/ExpoFabricViewObjC.mm b/node_modules/expo-modules-core/ios/Fabric/ExpoFabricViewObjC.mm -index 0645540..9243248 100644 +index 370015f..1eb91cb 100644 --- a/node_modules/expo-modules-core/ios/Fabric/ExpoFabricViewObjC.mm +++ b/node_modules/expo-modules-core/ios/Fabric/ExpoFabricViewObjC.mm -@@ -137,6 +137,11 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask +@@ -218,6 +218,11 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask } } diff --git a/patches/react-native-reanimated/details.md b/patches/react-native-reanimated/details.md deleted file mode 100644 index 638154d65bb8..000000000000 --- a/patches/react-native-reanimated/details.md +++ /dev/null @@ -1,9 +0,0 @@ - -# `react-native-reanimated` patches - -### [react-native-reanimated+4.5.5+001+catch-all-exceptions-on-stoi.patch](react-native-reanimated+4.5.5+001+catch-all-exceptions-on-stoi.patch) - -- Reason: Reanimated wasn't able to catch an exception here, so the catch clause was broadened. -- Upstream PR/issue: 🛑 -- E/App issue: 🛑 -- PR Introducing Patch: [Upgrade to React Native 0.76](https://github.com/Expensify/App/pull/51475) diff --git a/patches/react-native-reanimated/react-native-reanimated+4.5.5+001+catch-all-exceptions-on-stoi.patch b/patches/react-native-reanimated/react-native-reanimated+4.5.5+001+catch-all-exceptions-on-stoi.patch deleted file mode 100644 index 73ced70d9f6e..000000000000 --- a/patches/react-native-reanimated/react-native-reanimated+4.5.5+001+catch-all-exceptions-on-stoi.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/node_modules/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp b/node_modules/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp -index 9402e3e..ddc29cd 100644 ---- a/node_modules/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp -+++ b/node_modules/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp -@@ -986,7 +986,7 @@ void LayoutAnimationsProxy_Legacy::transferConfigFromNativeID(const std::string - auto nativeId = stoi(nativeIdString); - layoutAnimationsManager_->transferConfigFromNativeID(nativeId, tag); - } catch (std::invalid_argument) { -- } catch (std::out_of_range) {} -+ } catch (...) {} - } - - // When entering animations start, we temporarily set opacity to 0 diff --git a/patches/whatwg-fetch/details.md b/patches/whatwg-fetch/details.md index 26f043b4e76a..e9105a3ee656 100644 --- a/patches/whatwg-fetch/details.md +++ b/patches/whatwg-fetch/details.md @@ -1,6 +1,6 @@ # `whatwg-patches` patches -### [whatwg-fetch+3.6.2+001+use-microtask-queue-for-xhr-events.patch](whatwg-fetch+3.6.2+001+use-microtask-queue-for-xhr-events.patch) +### [whatwg-fetch+3.6.20+001+use-microtask-queue-for-xhr-events.patch](whatwg-fetch+3.6.20+001+use-microtask-queue-for-xhr-events.patch) - Reason: diff --git a/patches/whatwg-fetch/whatwg-fetch+3.6.2+001+use-microtask-queue-for-xhr-events.patch b/patches/whatwg-fetch/whatwg-fetch+3.6.20+001+use-microtask-queue-for-xhr-events.patch similarity index 88% rename from patches/whatwg-fetch/whatwg-fetch+3.6.2+001+use-microtask-queue-for-xhr-events.patch rename to patches/whatwg-fetch/whatwg-fetch+3.6.20+001+use-microtask-queue-for-xhr-events.patch index 3785dcfe07e0..bffd80bcb292 100644 --- a/patches/whatwg-fetch/whatwg-fetch+3.6.2+001+use-microtask-queue-for-xhr-events.patch +++ b/patches/whatwg-fetch/whatwg-fetch+3.6.20+001+use-microtask-queue-for-xhr-events.patch @@ -1,9 +1,9 @@ diff --git a/node_modules/whatwg-fetch/dist/fetch.umd.js b/node_modules/whatwg-fetch/dist/fetch.umd.js -index 8c3269e..b3bad8f 100644 +index 7a0d852..825500d 100644 --- a/node_modules/whatwg-fetch/dist/fetch.umd.js +++ b/node_modules/whatwg-fetch/dist/fetch.umd.js -@@ -525,27 +525,27 @@ - }; +@@ -557,27 +557,27 @@ + } options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL'); var body = 'response' in xhr ? xhr.response : xhr.responseText; - setTimeout(function() { @@ -24,7 +24,7 @@ index 8c3269e..b3bad8f 100644 xhr.ontimeout = function() { - setTimeout(function() { + queueMicrotask(function() { - reject(new TypeError('Network request failed')); + reject(new TypeError('Network request timed out')); - }, 0); + }); }; @@ -39,10 +39,10 @@ index 8c3269e..b3bad8f 100644 function fixUrl(url) { diff --git a/node_modules/whatwg-fetch/fetch.js b/node_modules/whatwg-fetch/fetch.js -index 5050a24..f70dbe6 100644 +index f39a983..3581bc0 100644 --- a/node_modules/whatwg-fetch/fetch.js +++ b/node_modules/whatwg-fetch/fetch.js -@@ -519,27 +519,27 @@ export function fetch(input, init) { +@@ -551,27 +551,27 @@ export function fetch(input, init) { } options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL') var body = 'response' in xhr ? xhr.response : xhr.responseText @@ -64,7 +64,7 @@ index 5050a24..f70dbe6 100644 xhr.ontimeout = function() { - setTimeout(function() { + queueMicrotask(function() { - reject(new TypeError('Network request failed')) + reject(new TypeError('Network request timed out')) - }, 0) + }) } diff --git a/src/components/DatePicker/CalendarPicker/index.tsx b/src/components/DatePicker/CalendarPicker/index.tsx index a93e1724cc1a..89aaa22f5c15 100644 --- a/src/components/DatePicker/CalendarPicker/index.tsx +++ b/src/components/DatePicker/CalendarPicker/index.tsx @@ -239,16 +239,67 @@ function CalendarPicker({ }); const webOnlyMarginStyle = isSmallScreenWidth ? {} : styles.mh1; - const calendarContainerStyle = isSmallScreenWidth ? [webOnlyMarginStyle, themeStyles.calendarBodyContainer] : [webOnlyMarginStyle, animatedStyle]; const headerPaddingStyle = headerContainerStyle ?? themeStyles.ph3; - // On mobile (isSmallScreenWidth is always true on native), the height animation is skipped - // so using Animated.View is unnecessary. Using a plain View with collapsable={false} avoids - // activating Reanimated's Fabric commit hook, which on Android can interfere with React's - // reconciliation of child view styles and prevent day-selection background changes from painting. - const CalendarBody = isSmallScreenWidth ? View : Animated.View; const getAccessibilityState = useCallback((isSelected: boolean) => ({selected: isSelected}), []); + const calendarWeeks = calendarDaysMatrix?.map((week) => ( + + {week.map((day, index) => { + const currentDate = new Date(currentYearView, currentMonthView, day); + const isBeforeMinDate = currentDate < startOfDay(new Date(minDate)); + const isAfterMaxDate = currentDate > startOfDay(new Date(maxDate)); + const isSelectable = selectableDates ? selectableDates?.some((date) => isSameDay(parseISO(date), currentDate)) : true; + const isDisabled = !day || isBeforeMinDate || isAfterMaxDate || !isSelectable; + const isSelected = !!day && isSameDay(parseISO(value.toString()), new Date(currentYearView, currentMonthView, day)); + const handleOnPress = () => { + if (!day || isDisabled) { + return; + } + + onDayPressed(day); + }; + const key = `${index}_day-${day}`; + const fullDate = day ? new Date(currentYearView, currentMonthView, day) : null; + const accessibilityDateLabel = fullDate ? DateUtils.formatToLongDateWithWeekday(fullDate, dateFnsLocale) : ''; + return ( + + {({hovered, pressed}) => ( + + {day} + + )} + + ); + })} + + )); + return ( ))} - - {calendarDaysMatrix?.map((week) => ( - - {week.map((day, index) => { - const currentDate = new Date(currentYearView, currentMonthView, day); - const isBeforeMinDate = currentDate < startOfDay(new Date(minDate)); - const isAfterMaxDate = currentDate > startOfDay(new Date(maxDate)); - const isSelectable = selectableDates ? selectableDates?.some((date) => isSameDay(parseISO(date), currentDate)) : true; - const isDisabled = !day || isBeforeMinDate || isAfterMaxDate || !isSelectable; - const isSelected = !!day && isSameDay(parseISO(value.toString()), new Date(currentYearView, currentMonthView, day)); - const handleOnPress = () => { - if (!day || isDisabled) { - return; - } - - onDayPressed(day); - }; - const key = `${index}_day-${day}`; - const fullDate = day ? new Date(currentYearView, currentMonthView, day) : null; - const accessibilityDateLabel = fullDate ? DateUtils.formatToLongDateWithWeekday(fullDate, dateFnsLocale) : ''; - return ( - - {({hovered, pressed}) => ( - - {day} - - )} - - ); - })} - - ))} - + {/* On mobile (isSmallScreenWidth is always true on native), the height animation is skipped + so using Animated.View is unnecessary. Using a plain View with collapsable={false} avoids + activating Reanimated's Fabric commit hook, which on Android can interfere with React's + reconciliation of child view styles and prevent day-selection background changes from painting. */} + {isSmallScreenWidth ? ( + + {calendarWeeks} + + ) : ( + + {calendarWeeks} + + )} Date: Fri, 2 Oct 2026 14:53:12 +0200 Subject: [PATCH 02/11] add resolver --- jest.config.js | 1 + jest/resolver.js | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 jest/resolver.js diff --git a/jest.config.js b/jest.config.js index 58b96c5f43b7..1f458a3d7367 100644 --- a/jest.config.js +++ b/jest.config.js @@ -16,6 +16,7 @@ const emitsUntestedFiles = !shardMatch || shardMatch[1] === '1'; module.exports = { preset: 'jest-expo', + resolver: '/jest/resolver.js', collectCoverageFrom: emitsUntestedFiles ? ['/src/**/*.{ts,tsx,js,jsx}', '!/src/**/__mocks__/**', '!/src/**/tests/**', '!**/*.d.ts'] : undefined, // Keeps the instrumented set identical to `collectCoverageFrom` above on the shards that skip it. coveragePathIgnorePatterns: ['/node_modules/', '/(?!src/)', '/__mocks__/', '/src/.*/tests/', '\\.d\\.ts$'], diff --git a/jest/resolver.js b/jest/resolver.js new file mode 100644 index 000000000000..c2fa384d9ecc --- /dev/null +++ b/jest/resolver.js @@ -0,0 +1,13 @@ +// Since Reanimated 4.6 its shared files are split into `.native`/`.web` variants and Jest resolves the `.native` ones, +// which call native-only APIs at import time (e.g. `setCSSEventHandler`) and crash every suite that imports Reanimated. +// Reanimated's resolver maps those files to their web variants (and chains the Worklets resolver), but replacing the +// jest-expo preset's React Native resolver would break mocking `react-native/*` subpaths, so this chains both. +const reactNativeResolver = require('@react-native/jest-preset/jest/resolver'); +const reanimatedResolver = require('react-native-reanimated/jest/resolver'); + +/** @type {import('jest-resolve').SyncResolver} */ +module.exports = (request, options) => + reanimatedResolver(request, { + ...options, + defaultResolver: (path, nextOptions) => reactNativeResolver(path, {...nextOptions, defaultResolver: options.defaultResolver}), + }); From 38f42860402460b9cb0c6178fd02eb406bc6ee06 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Fri, 2 Oct 2026 15:17:53 +0200 Subject: [PATCH 03/11] fix lint and knip --- jest.config.js | 2 +- jest/resolver.js | 13 ------------- jest/resolver.ts | 28 ++++++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 14 deletions(-) delete mode 100644 jest/resolver.js create mode 100644 jest/resolver.ts diff --git a/jest.config.js b/jest.config.js index 1f458a3d7367..61fe030d49f2 100644 --- a/jest.config.js +++ b/jest.config.js @@ -16,7 +16,7 @@ const emitsUntestedFiles = !shardMatch || shardMatch[1] === '1'; module.exports = { preset: 'jest-expo', - resolver: '/jest/resolver.js', + resolver: '/jest/resolver.ts', collectCoverageFrom: emitsUntestedFiles ? ['/src/**/*.{ts,tsx,js,jsx}', '!/src/**/__mocks__/**', '!/src/**/tests/**', '!**/*.d.ts'] : undefined, // Keeps the instrumented set identical to `collectCoverageFrom` above on the shards that skip it. coveragePathIgnorePatterns: ['/node_modules/', '/(?!src/)', '/__mocks__/', '/src/.*/tests/', '\\.d\\.ts$'], diff --git a/jest/resolver.js b/jest/resolver.js deleted file mode 100644 index c2fa384d9ecc..000000000000 --- a/jest/resolver.js +++ /dev/null @@ -1,13 +0,0 @@ -// Since Reanimated 4.6 its shared files are split into `.native`/`.web` variants and Jest resolves the `.native` ones, -// which call native-only APIs at import time (e.g. `setCSSEventHandler`) and crash every suite that imports Reanimated. -// Reanimated's resolver maps those files to their web variants (and chains the Worklets resolver), but replacing the -// jest-expo preset's React Native resolver would break mocking `react-native/*` subpaths, so this chains both. -const reactNativeResolver = require('@react-native/jest-preset/jest/resolver'); -const reanimatedResolver = require('react-native-reanimated/jest/resolver'); - -/** @type {import('jest-resolve').SyncResolver} */ -module.exports = (request, options) => - reanimatedResolver(request, { - ...options, - defaultResolver: (path, nextOptions) => reactNativeResolver(path, {...nextOptions, defaultResolver: options.defaultResolver}), - }); diff --git a/jest/resolver.ts b/jest/resolver.ts new file mode 100644 index 000000000000..1e4603e0e3f5 --- /dev/null +++ b/jest/resolver.ts @@ -0,0 +1,28 @@ +// Since Reanimated 4.6 its shared files are split into `.native`/`.web` variants and Jest resolves the `.native` ones, +// which call native-only APIs at import time (e.g. `setCSSEventHandler`) and crash every suite that imports Reanimated. +// Reanimated's resolver maps those files to their web variants (and chains the Worklets resolver), but replacing the +// jest-expo preset's React Native resolver would break mocking `react-native/*` subpaths, so this chains both. +// +// Jest `require`s this file untransformed, so Node strips the types and loads it as an ES module: Jest reads the `sync` +// export, and both resolvers are untyped CommonJS loaded through `createRequire`. +import {createRequire} from 'node:module'; + +type ResolverOptions = Record & { + defaultResolver: SyncResolver; +}; + +/** The subset of Jest's `SyncResolver` (from `jest-resolve`) that this file relies on. */ +type SyncResolver = (path: string, options: ResolverOptions) => string; + +const requireResolver: (id: string) => SyncResolver = createRequire(import.meta.url); +const reactNativeResolver = requireResolver('@react-native/jest-preset/jest/resolver'); +const reanimatedResolver = requireResolver('react-native-reanimated/jest/resolver'); + +const resolver: SyncResolver = (request, options) => + reanimatedResolver(request, { + ...options, + defaultResolver: (path, nextOptions) => reactNativeResolver(path, {...nextOptions, defaultResolver: options.defaultResolver}), + }); + +export default resolver; +export {resolver as sync}; From b9b21bc245bdf10e505506407016ca703c0560dc Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Fri, 2 Oct 2026 15:41:07 +0200 Subject: [PATCH 04/11] update live markdown patch details --- patches/@expensify/react-native-live-markdown/details.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patches/@expensify/react-native-live-markdown/details.md b/patches/@expensify/react-native-live-markdown/details.md index 674a9253cbf2..76f56b01a8fb 100644 --- a/patches/@expensify/react-native-live-markdown/details.md +++ b/patches/@expensify/react-native-live-markdown/details.md @@ -11,6 +11,6 @@ calls `runSync` directly, which works on both worklets 0.11 and 0.13. ``` -- Upstream PR/issue: 🛑 +- Upstream PR/issue: https://github.com/Expensify/react-native-live-markdown/pull/780 - E/App issue: https://github.com/Expensify/App/issues/101440 -- PR introducing patch: 🛑 +- PR introducing patch: https://github.com/Expensify/App/pull/102266 From 6a816f575cbb505fb6525dbf344712e018cc07a5 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Mon, 5 Oct 2026 17:34:18 +0200 Subject: [PATCH 05/11] add patch to expensify-common due to changing behavior in worklets --- patches/expensify-common/details.md | 24 + ...7+001+worklets-0.13-str-without-this.patch | 590 ++++++++++++++++++ 2 files changed, 614 insertions(+) create mode 100644 patches/expensify-common/details.md create mode 100644 patches/expensify-common/expensify-common+2.0.207+001+worklets-0.13-str-without-this.patch diff --git a/patches/expensify-common/details.md b/patches/expensify-common/details.md new file mode 100644 index 000000000000..6c9147300cba --- /dev/null +++ b/patches/expensify-common/details.md @@ -0,0 +1,24 @@ +# `expensify-common` patches + +### [expensify-common+2.0.207+001+worklets-0.13-str-without-this.patch](expensify-common+2.0.207+001+worklets-0.13-str-without-this.patch) + +- Reason: + + ``` + `str.js` is a `'worklet'` file, and `ExpensiMark` calls `Str` methods on the LiveMarkdownRuntime. Several `Str` + methods call other `Str` methods through `this` (e.g. `sanitizeURL` calls `this.cutBefore`). When the Worklets + Babel plugin runs without Bundle Mode (our Re.Pack builds), react-native-worklets 0.13 turns every method of the + `Str` object literal into a separate worklet, because it removed Worklet Context Objects + (software-mansion/react-native-reanimated#10411). `this` inside those methods no longer points to `Str` on the + Worklet Runtime, so typing a link in the composer crashed with "undefined is not a function". Referencing `Str` + from the methods instead does not work either: the plugin builds each method's closure while the object literal is + still being created, so `Str` is captured as `undefined`, which breaks `Str` on the JS thread too. + + The patch moves the 20 `Str` methods that other `Str` methods call (`cutBefore`, `isString`, `isTypeOf`, ...) to + module-level functions declared before `Str`, in dependency order, keeps them on `Str` as shorthand properties, + and replaces every `this.` with a direct call. The public `Str` API and its behavior are unchanged. + ``` + +- Upstream PR/issue: 🛑 +- E/App issue: https://github.com/Expensify/App/issues/101440 +- PR introducing patch: https://github.com/Expensify/App/pull/102266 diff --git a/patches/expensify-common/expensify-common+2.0.207+001+worklets-0.13-str-without-this.patch b/patches/expensify-common/expensify-common+2.0.207+001+worklets-0.13-str-without-this.patch new file mode 100644 index 000000000000..0596cb49de16 --- /dev/null +++ b/patches/expensify-common/expensify-common+2.0.207+001+worklets-0.13-str-without-this.patch @@ -0,0 +1,590 @@ +diff --git a/node_modules/expensify-common/dist/str.js b/node_modules/expensify-common/dist/str.js +index 124dd32..8d20430 100644 +--- a/node_modules/expensify-common/dist/str.js ++++ b/node_modules/expensify-common/dist/str.js +@@ -53,6 +53,126 @@ function resultFn(parameter, ...args) { + } + return parameter; + } ++// Str methods that other Str methods call are plain module-level functions instead of being reached through ++// `this`: react-native-worklets 0.13 no longer keeps an object literal together on a Worklet Runtime, so `this` ++// inside a workletized method does not point to Str there. ++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 undefined; ++ } ++ 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 we are grabbing the byte size, we need to temporarily diable no-bitwise for linting ++ /* eslint-disable no-bitwise */ ++ 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; ++ } ++ /* eslint-enable no-bitwise */ ++ 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) { ++ // eslint-disable-next-line no-void ++ 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 str is empty, str or mask aren't strings, ++ // or n is not a number, do nothing ++ 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; ++} + const Str = { + /** + * Return true if the string is ending with the provided suffix +@@ -110,18 +230,14 @@ const Str = { + * @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. + * +@@ -165,7 +281,7 @@ const Str = { + * @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. +@@ -251,32 +367,7 @@ const Str = { + * one. + * @returns Byte size of the character + */ +- getRawByteSize(inputChar) { +- const onlyChar = String(inputChar); +- const c = onlyChar.charCodeAt(0); +- // If we are grabbing the byte size, we need to temporarily diable no-bitwise for linting +- /* eslint-disable no-bitwise */ +- 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; +- } +- /* eslint-enable no-bitwise */ +- return Number.NaN; +- }, ++ getRawByteSize, + /** + * Gets the length of a string in bytes, including non-ASCII characters + * @returns The number of bytes used by string +@@ -284,7 +375,7 @@ const Str = { + getByteLength(input) { + // Force string type + 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; + }, + /** +@@ -296,7 +387,7 @@ const Str = { + 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) { + // If the next character exceeds the limit, stop and return the truncated string. + return `${stringInput.substr(0, i - 3)}...`; +@@ -312,9 +403,7 @@ const Str = { + * @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. + * +@@ -322,7 +411,7 @@ const Str = { + * @returns The text from within the HTML string. + */ + stripHTML(str) { +- if (!this.isString(str)) { ++ if (!isString(str)) { + return ''; + } + return str.replaceAll(/<[^>]*>?/gm, ''); +@@ -345,13 +434,7 @@ const Str = { + * @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. +@@ -361,13 +444,7 @@ const Str = { + * @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) + * +@@ -394,16 +471,7 @@ const Str = { + * + * @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. + * +@@ -421,9 +489,7 @@ const Str = { + * + * @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. + * +@@ -432,12 +498,12 @@ const Str = { + * @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; + }, + /** +@@ -454,9 +520,7 @@ const Str = { + * + * @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"). +@@ -466,7 +530,7 @@ const Str = { + * @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; + } +@@ -485,7 +549,7 @@ const Str = { + * @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. +@@ -543,13 +607,13 @@ const Str = { + * @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; +@@ -603,9 +667,7 @@ const Str = { + * + * @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 + * +@@ -615,7 +677,7 @@ const Str = { + * @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 +@@ -627,11 +689,7 @@ const Str = { + * 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 + * +@@ -640,7 +698,7 @@ const Str = { + * @returns true when first == second except for case + */ + caseInsensitiveEquals(strA, strB) { +- return this.caseInsensitiveCompare(strA, strB) === 0; ++ return caseInsensitiveCompare(strA, strB) === 0; + }, + /** + * Compare function +@@ -652,15 +710,7 @@ const Str = { + * 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 + */ +@@ -684,48 +734,39 @@ const Str = { + // Hide these numbers completely + // We should not be getting account numbers this small or large + if (len < 6 || len > 20) { +- return this.maskFirstNCharacters(accountNumber, len, 'X'); ++ return maskFirstNCharacters(accountNumber, len, 'X'); + } + // Can show last 4 + if (len < 14) { +- return this.maskFirstNCharacters(accountNumber, len - 4, 'X'); ++ return maskFirstNCharacters(accountNumber, len - 4, 'X'); + } + // Can show first 6 and last 4 + 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) { +- // eslint-disable-next-line no-void +- return obj === void 0; +- }, ++ isUndefined, + /** + * Replace first N characters of the string with maskChar + * eg: maskFirstNCharacters( '1234567890', 6, 'X' ) yields XXXXXX7890 +@@ -734,14 +775,7 @@ const Str = { + * @param mask String we want replace the first N chars with + * @returns Masked string + */ +- maskFirstNCharacters(str, num, mask) { +- // if str is empty, str or mask aren't strings, +- // or n is not a number, do nothing +- 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 + */ +@@ -753,7 +787,7 @@ const Str = { + * @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 +@@ -799,7 +833,7 @@ const Str = { + * Converts a value to boolean, case-insensitive. + */ + toBool(value) { +- if (this.isString(value)) { ++ if (isString(value)) { + return value.toLowerCase() === 'true'; + } + return !!value; +@@ -849,9 +883,7 @@ const Str = { + /** + * 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 +@@ -880,14 +912,12 @@ const Str = { + /** + * 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 +@@ -945,7 +975,7 @@ const Str = { + 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 +@@ -957,14 +987,7 @@ const Str = { + * 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 undefined; +- } +- 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 + * +@@ -972,7 +995,7 @@ const Str = { + * @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 +@@ -983,7 +1006,7 @@ const Str = { + * https://reactnative.dev/docs/image#source + */ + isImage(url) { +- const extension = this.getExtension(url); ++ const extension = getExtension(url); + if (!extension) { + return false; + } +@@ -999,7 +1022,7 @@ const Str = { + * 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; + } +@@ -1012,7 +1035,7 @@ const Str = { + * @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, From 3a021e7bae9bacfc2034c4f3355229bf80895de6 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Tue, 6 Oct 2026 13:58:18 +0200 Subject: [PATCH 06/11] Rebuild GitHub Actions with the expensify-common patch --- .../isDeployChecklistLocked/index.js | 288 ++++++++++-------- .../javascript/proposalPoliceComment/index.js | 288 ++++++++++-------- 2 files changed, 308 insertions(+), 268 deletions(-) diff --git a/.github/actions/javascript/isDeployChecklistLocked/index.js b/.github/actions/javascript/isDeployChecklistLocked/index.js index ebdf2bf6961c..13396e90aa98 100644 --- a/.github/actions/javascript/isDeployChecklistLocked/index.js +++ b/.github/actions/javascript/isDeployChecklistLocked/index.js @@ -22075,6 +22075,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 +22243,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 +22292,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 +22375,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 +22394,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 +22409,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 +22417,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 +22440,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 +22450,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 +22477,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 +22495,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 +22504,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 +22526,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 +22536,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 +22555,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 +22613,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 +22672,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 +22682,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 +22694,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 +22703,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 +22715,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 +22734,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 +22773,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 +22785,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 +22829,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 +22879,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 +22905,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 +22963,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 +22975,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 +22983,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 +22994,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 +23010,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 +23023,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, diff --git a/.github/actions/javascript/proposalPoliceComment/index.js b/.github/actions/javascript/proposalPoliceComment/index.js index 126b34462df4..e296ae687ef6 100644 --- a/.github/actions/javascript/proposalPoliceComment/index.js +++ b/.github/actions/javascript/proposalPoliceComment/index.js @@ -42889,6 +42889,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 @@ -42946,18 +43057,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. * @@ -42999,7 +43106,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. @@ -43082,36 +43189,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; }, /** @@ -43123,7 +43208,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)}...`; } @@ -43138,9 +43223,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. * @@ -43148,7 +43231,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, ""); @@ -43171,13 +43254,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. @@ -43187,13 +43264,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) * @@ -43220,16 +43291,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. * @@ -43247,9 +43309,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. * @@ -43258,12 +43318,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; }, /** @@ -43280,9 +43340,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"). @@ -43292,7 +43350,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; } @@ -43311,7 +43369,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. @@ -43369,13 +43427,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; @@ -43428,9 +43486,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 * @@ -43440,7 +43496,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 @@ -43452,11 +43508,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 * @@ -43465,7 +43517,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 @@ -43477,15 +43529,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 */ @@ -43504,45 +43548,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 @@ -43551,12 +43587,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 */ @@ -43568,7 +43599,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 @@ -43612,7 +43643,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; @@ -43662,9 +43693,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 @@ -43690,14 +43719,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 @@ -43750,7 +43777,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 @@ -43762,14 +43789,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 * @@ -43777,7 +43797,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 @@ -43788,7 +43808,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; } @@ -43804,7 +43824,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; } @@ -43817,7 +43837,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, From 306e4796acfe2d30040f356e44ce7406bc8c30d9 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Tue, 6 Oct 2026 14:16:05 +0200 Subject: [PATCH 07/11] remove unknown type --- jest/resolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest/resolver.ts b/jest/resolver.ts index 1e4603e0e3f5..b0093552708c 100644 --- a/jest/resolver.ts +++ b/jest/resolver.ts @@ -7,7 +7,7 @@ // export, and both resolvers are untyped CommonJS loaded through `createRequire`. import {createRequire} from 'node:module'; -type ResolverOptions = Record & { +type ResolverOptions = { defaultResolver: SyncResolver; }; From 3d9891565cf6b17fdd5ab737a25dbda4a9a31a91 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Tue, 6 Oct 2026 15:12:26 +0200 Subject: [PATCH 08/11] remove whatwg patch, since we don't use it --- patches/whatwg-fetch/details.md | 13 --- ...1+use-microtask-queue-for-xhr-events.patch | 80 ------------------- 2 files changed, 93 deletions(-) delete mode 100644 patches/whatwg-fetch/details.md delete mode 100644 patches/whatwg-fetch/whatwg-fetch+3.6.20+001+use-microtask-queue-for-xhr-events.patch diff --git a/patches/whatwg-fetch/details.md b/patches/whatwg-fetch/details.md deleted file mode 100644 index e9105a3ee656..000000000000 --- a/patches/whatwg-fetch/details.md +++ /dev/null @@ -1,13 +0,0 @@ -# `whatwg-patches` patches - -### [whatwg-fetch+3.6.20+001+use-microtask-queue-for-xhr-events.patch](whatwg-fetch+3.6.20+001+use-microtask-queue-for-xhr-events.patch) - -- Reason: - - ``` - This patch replaces setTimeout(0) with queueMicrotask for XHR event handlers within the whatwg-fetch polyfill. - ``` - -- Upstream PR/issue: https://github.com/JakeChampion/fetch/issues/1484 -- E/App issue: https://github.com/Expensify/App/issues/90309 -- PR introducing patch: https://github.com/Expensify/App/pull/94399 diff --git a/patches/whatwg-fetch/whatwg-fetch+3.6.20+001+use-microtask-queue-for-xhr-events.patch b/patches/whatwg-fetch/whatwg-fetch+3.6.20+001+use-microtask-queue-for-xhr-events.patch deleted file mode 100644 index bffd80bcb292..000000000000 --- a/patches/whatwg-fetch/whatwg-fetch+3.6.20+001+use-microtask-queue-for-xhr-events.patch +++ /dev/null @@ -1,80 +0,0 @@ -diff --git a/node_modules/whatwg-fetch/dist/fetch.umd.js b/node_modules/whatwg-fetch/dist/fetch.umd.js -index 7a0d852..825500d 100644 ---- a/node_modules/whatwg-fetch/dist/fetch.umd.js -+++ b/node_modules/whatwg-fetch/dist/fetch.umd.js -@@ -557,27 +557,27 @@ - } - options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL'); - var body = 'response' in xhr ? xhr.response : xhr.responseText; -- setTimeout(function() { -+ queueMicrotask(function() { - resolve(new Response(body, options)); -- }, 0); -+ }); - }; - - xhr.onerror = function() { -- setTimeout(function() { -+ queueMicrotask(function() { - reject(new TypeError('Network request failed')); -- }, 0); -+ }); - }; - - xhr.ontimeout = function() { -- setTimeout(function() { -+ queueMicrotask(function() { - reject(new TypeError('Network request timed out')); -- }, 0); -+ }); - }; - - xhr.onabort = function() { -- setTimeout(function() { -+ queueMicrotask(function() { - reject(new exports.DOMException('Aborted', 'AbortError')); -- }, 0); -+ }); - }; - - function fixUrl(url) { -diff --git a/node_modules/whatwg-fetch/fetch.js b/node_modules/whatwg-fetch/fetch.js -index f39a983..3581bc0 100644 ---- a/node_modules/whatwg-fetch/fetch.js -+++ b/node_modules/whatwg-fetch/fetch.js -@@ -551,27 +551,27 @@ export function fetch(input, init) { - } - options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL') - var body = 'response' in xhr ? xhr.response : xhr.responseText -- setTimeout(function() { -+ queueMicrotask(function() { - resolve(new Response(body, options)) -- }, 0) -+ }) - } - - xhr.onerror = function() { -- setTimeout(function() { -+ queueMicrotask(function() { - reject(new TypeError('Network request failed')) -- }, 0) -+ }) - } - - xhr.ontimeout = function() { -- setTimeout(function() { -+ queueMicrotask(function() { - reject(new TypeError('Network request timed out')) -- }, 0) -+ }) - } - - xhr.onabort = function() { -- setTimeout(function() { -+ queueMicrotask(function() { - reject(new DOMException('Aborted', 'AbortError')) -- }, 0) -+ }) - } - - function fixUrl(url) { From 15ce36278c544c7e9eaecb2d421e3223ced26e23 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Tue, 6 Oct 2026 16:02:53 +0200 Subject: [PATCH 09/11] remove our mocks and use official mock instead --- jest/setup.ts | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/jest/setup.ts b/jest/setup.ts index 9afaa8f219a1..58e92bbef3f4 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -146,23 +146,7 @@ jest.mock('react-native-share', () => ({ default: jest.fn(), })); -jest.mock('react-native-reanimated', () => ({ - ...jest.requireActual('react-native-reanimated/mock'), - createAnimatedPropAdapter: jest.fn, - // react-native-reanimated/mock leaves dispatchCommand out (see its own "ADD ME IF NEEDED" comment). forceClearInput - // (src/libs/ComponentUtils) dispatches it from a UI-thread worklet, so any test exercising that path needs it mocked. - dispatchCommand: jest.fn(), - // react-native-reanimated/mock also leaves out useComposedEventHandler, useHandler and isSharedValue, which Gesture Handler 3 - // calls from its detectors (including the ones behind its ScrollView / FlatList wrappers). - useComposedEventHandler: jest.fn(() => () => {}), - useHandler: jest.fn(() => ({context: {}, doDependenciesDiffer: false})), - isSharedValue: jest.fn((value: unknown) => typeof value === 'object' && value !== null && 'value' in value && 'get' in value && typeof value.get === 'function'), - useReducedMotion: jest.fn, - useScrollViewOffset: jest.fn(() => 0), - useAnimatedRef: jest.fn(() => jest.fn()), - LayoutAnimationConfig: ({children}: {children: React.ReactNode}) => children, - makeShareableCloneRecursive: jest.fn, -})); +jest.mock('react-native-reanimated', () => jest.requireActual('react-native-reanimated/mock')); // eslint-disable-next-line @typescript-eslint/no-unsafe-return jest.mock('react-native-worklets', () => require('react-native-worklets/src/mock')); From b32a91df63bee2450756741122a6643e38bfa21e Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Tue, 6 Oct 2026 16:08:37 +0200 Subject: [PATCH 10/11] revert some removed mocks --- jest/setup.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/jest/setup.ts b/jest/setup.ts index 58e92bbef3f4..d7e3eec8321e 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -146,7 +146,18 @@ jest.mock('react-native-share', () => ({ default: jest.fn(), })); -jest.mock('react-native-reanimated', () => jest.requireActual('react-native-reanimated/mock')); +jest.mock('react-native-reanimated', () => ({ + ...jest.requireActual('react-native-reanimated/mock'), + createAnimatedPropAdapter: jest.fn, + // react-native-reanimated/mock leaves dispatchCommand out (see its own "ADD ME IF NEEDED" comment). forceClearInput + // (src/libs/ComponentUtils) dispatches it from a UI-thread worklet, so any test exercising that path needs it mocked. + dispatchCommand: jest.fn(), + useReducedMotion: jest.fn, + useScrollViewOffset: jest.fn(() => 0), + useAnimatedRef: jest.fn(() => jest.fn()), + LayoutAnimationConfig: ({children}: {children: React.ReactNode}) => children, + makeShareableCloneRecursive: jest.fn, +})); // eslint-disable-next-line @typescript-eslint/no-unsafe-return jest.mock('react-native-worklets', () => require('react-native-worklets/src/mock')); From f42529d976e6a6159bff1ce5cc6a57b26e5a4cec Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Wed, 7 Oct 2026 10:01:26 +0200 Subject: [PATCH 11/11] bump versions and remove patches --- .../isDeployChecklistLocked/index.js | 17 +- .../javascript/proposalPoliceComment/index.js | 17 +- ios/Podfile.lock | 4 +- package-lock.json | 16 +- package.json | 4 +- ...wn+0.1.342+001+worklets-0.12-runSync.patch | 29 - .../react-native-live-markdown/details.md | 16 - patches/expensify-common/details.md | 24 - ...7+001+worklets-0.13-str-without-this.patch | 590 ------------------ 9 files changed, 34 insertions(+), 683 deletions(-) delete mode 100644 patches/@expensify/react-native-live-markdown/@expensify+react-native-live-markdown+0.1.342+001+worklets-0.12-runSync.patch delete mode 100644 patches/@expensify/react-native-live-markdown/details.md delete mode 100644 patches/expensify-common/details.md delete mode 100644 patches/expensify-common/expensify-common+2.0.207+001+worklets-0.13-str-without-this.patch diff --git a/.github/actions/javascript/isDeployChecklistLocked/index.js b/.github/actions/javascript/isDeployChecklistLocked/index.js index e62ecde2f903..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", @@ -24161,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}`; } - return `[${g4}](${email || g3})`; + if (incompleteLinkStart && (g4 === g5 || g4 === `https://${g5}`)) { + return `${incompleteLinkStart}${g5}`; + } + return `${incompleteLinkStart}[${g5}](${email || g4})`; } }, { diff --git a/.github/actions/javascript/proposalPoliceComment/index.js b/.github/actions/javascript/proposalPoliceComment/index.js index 884862585a9f..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", @@ -45420,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}`; } - return `[${g4}](${email || g3})`; + if (incompleteLinkStart && (g4 === g5 || g4 === `https://${g5}`)) { + return `${incompleteLinkStart}${g5}`; + } + return `${incompleteLinkStart}[${g5}](${email || g4})`; } }, { diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 55c08a02edae..b003bee00d33 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2955,7 +2955,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.342): + - RNLiveMarkdown (0.1.343): - hermes-engine - RCTRequired - RCTTypeSafety @@ -4109,7 +4109,7 @@ SPEC CHECKSUMS: RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8 RNGestureHandler: 85e32defcf23dd3b023356355c5c1dd1d9d51bde RNGoogleSignin: 89877c73f0fbf6af2038fbdb7b73b5a25b8330cc - RNLiveMarkdown: f4c383d846d5d756f42bdf6006e85d5baf0ebfc8 + RNLiveMarkdown: b6894bb864d517f51ea80c98facd26571f2cd0fe RNLocalize: 82e7aa2857ce5dcd9e2a0c04f8c34c2ed50821df rnmapbox-maps: 54eb40428a4fd89f484a492fc7186bc1c1eea9ce RNNitroSQLite: c986393b7cb0e5e4587d0c9f7108e56ee8db9627 diff --git a/package-lock.json b/package-lock.json index fd7151248d8a..f6274d9d60e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "@expensify/nitro-utils": "file:./modules/ExpensifyNitroUtils", "@expensify/react-native-background-task": "file:./modules/background-task", "@expensify/react-native-hybrid-app": "file:./modules/hybrid-app", - "@expensify/react-native-live-markdown": "0.1.342", + "@expensify/react-native-live-markdown": "0.1.343", "@expensify/react-native-wallet": "0.1.22", "@expo/metro-config": "57.0.7", "@expo/metro-runtime": "57.0.7", @@ -68,7 +68,7 @@ "date-fns-tz": "^3.2.0", "dom-serializer": "^0.2.2", "domhandler": "^5.0.3", - "expensify-common": "2.0.207", + "expensify-common": "2.0.211", "expo": "57.0.8", "expo-asset": "57.0.7", "expo-audio": "57.0.3", @@ -5936,9 +5936,9 @@ "link": true }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.342", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.342.tgz", - "integrity": "sha512-xt8t0pUTHHPfY9G25wZ0rQUP44eFdVQp44Mo2Ji2H66zr/qjUnDqDOQPJxi5azucUwI/uORmmMJOvXMdKsEDNg==", + "version": "0.1.343", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.343.tgz", + "integrity": "sha512-3M1FQ6YgMy9dv0LiMORI1rwKwpJ0z3Ce94NkuxqGvObHPuee1vHX2Ozi7th97c+MzBXkydDJgnq20yH20jic9Q==", "license": "MIT", "workspaces": [ "./example", @@ -26896,9 +26896,9 @@ } }, "node_modules/expensify-common": { - "version": "2.0.207", - "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.207.tgz", - "integrity": "sha512-AHrgp9LZkS9z/37G5ZbKbIwa9GqSCPVwfwilS1yhSQmFyL6Jb/eDxlopMfthaTbXO0NHTPRH2UmImlUtdlNYvw==", + "version": "2.0.211", + "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.211.tgz", + "integrity": "sha512-BMFDPN4UJrvfESDViMMt5p/JA8JXWfBpCYshqy+v6hhxgZ5D5oGcko3mLPi4eoNVD1M+etkIY4pQJIKb68D3hw==", "license": "MIT", "dependencies": { "awesome-phonenumber": "^5.4.0", diff --git a/package.json b/package.json index 5b359d18eaee..bb9dd896eaf8 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "@expensify/nitro-utils": "file:./modules/ExpensifyNitroUtils", "@expensify/react-native-background-task": "file:./modules/background-task", "@expensify/react-native-hybrid-app": "file:./modules/hybrid-app", - "@expensify/react-native-live-markdown": "0.1.342", + "@expensify/react-native-live-markdown": "0.1.343", "@expensify/react-native-wallet": "0.1.22", "@expo/metro-config": "57.0.7", "@expo/metro-runtime": "57.0.7", @@ -144,7 +144,7 @@ "date-fns-tz": "^3.2.0", "dom-serializer": "^0.2.2", "domhandler": "^5.0.3", - "expensify-common": "2.0.207", + "expensify-common": "2.0.211", "expo": "57.0.8", "expo-asset": "57.0.7", "expo-audio": "57.0.3", diff --git a/patches/@expensify/react-native-live-markdown/@expensify+react-native-live-markdown+0.1.342+001+worklets-0.12-runSync.patch b/patches/@expensify/react-native-live-markdown/@expensify+react-native-live-markdown+0.1.342+001+worklets-0.12-runSync.patch deleted file mode 100644 index 7ef9b44d23ec..000000000000 --- a/patches/@expensify/react-native-live-markdown/@expensify+react-native-live-markdown+0.1.342+001+worklets-0.12-runSync.patch +++ /dev/null @@ -1,29 +0,0 @@ -diff --git a/node_modules/@expensify/react-native-live-markdown/android/src/main/cpp/MarkdownParser.cpp b/node_modules/@expensify/react-native-live-markdown/android/src/main/cpp/MarkdownParser.cpp -index 4edd28e..534e20c 100644 ---- a/node_modules/@expensify/react-native-live-markdown/android/src/main/cpp/MarkdownParser.cpp -+++ b/node_modules/@expensify/react-native-live-markdown/android/src/main/cpp/MarkdownParser.cpp -@@ -17,7 +17,7 @@ namespace livemarkdown { - const auto markdownWorklet = expensify::livemarkdown::getMarkdownWorklet(parserId); - - const auto input = jsi::String::createFromUtf8(rt, text->toStdString()); -- const auto output = markdownRuntime->runGuarded(markdownWorklet, input); -+ const auto output = markdownRuntime->runSync(markdownWorklet, input); - - const auto json = rt.global().getPropertyAsObject(rt, "JSON").getPropertyAsFunction(rt, "stringify").call(rt, output).asString(rt).utf8(rt); - return jni::make_jstring(json); -diff --git a/node_modules/@expensify/react-native-live-markdown/apple/MarkdownParser.mm b/node_modules/@expensify/react-native-live-markdown/apple/MarkdownParser.mm -index 89bb209..780034f 100644 ---- a/node_modules/@expensify/react-native-live-markdown/apple/MarkdownParser.mm -+++ b/node_modules/@expensify/react-native-live-markdown/apple/MarkdownParser.mm -@@ -223,9 +223,9 @@ - (void)drainPendingWarmups - - jsi::Value output; - try { -- output = markdownRuntime->runGuarded(markdownWorklet, input); -+ output = markdownRuntime->runSync(markdownWorklet, input); - } catch (const jsi::JSError &error) { -- // Skip formatting, runGuarded will show the error in LogBox -+ // Skip formatting, runSync will show the error in LogBox - return @[]; - } - diff --git a/patches/@expensify/react-native-live-markdown/details.md b/patches/@expensify/react-native-live-markdown/details.md deleted file mode 100644 index 76f56b01a8fb..000000000000 --- a/patches/@expensify/react-native-live-markdown/details.md +++ /dev/null @@ -1,16 +0,0 @@ -# `@expensify/react-native-live-markdown` patches - -### [@expensify+react-native-live-markdown+0.1.342+001+worklets-0.12-runSync.patch](@expensify+react-native-live-markdown+0.1.342+001+worklets-0.12-runSync.patch) - -- Reason: - - ``` - react-native-worklets 0.12.0 removed the deprecated `WorkletRuntime::runGuarded` C++ API, so the native - Markdown parser no longer compiles against worklets 0.13 (Android `MarkdownParser.cpp` and iOS - `MarkdownParser.mm`). In worklets 0.11 `runGuarded` was only a deprecated alias of `runSync`, so this patch - calls `runSync` directly, which works on both worklets 0.11 and 0.13. - ``` - -- Upstream PR/issue: https://github.com/Expensify/react-native-live-markdown/pull/780 -- E/App issue: https://github.com/Expensify/App/issues/101440 -- PR introducing patch: https://github.com/Expensify/App/pull/102266 diff --git a/patches/expensify-common/details.md b/patches/expensify-common/details.md deleted file mode 100644 index 6c9147300cba..000000000000 --- a/patches/expensify-common/details.md +++ /dev/null @@ -1,24 +0,0 @@ -# `expensify-common` patches - -### [expensify-common+2.0.207+001+worklets-0.13-str-without-this.patch](expensify-common+2.0.207+001+worklets-0.13-str-without-this.patch) - -- Reason: - - ``` - `str.js` is a `'worklet'` file, and `ExpensiMark` calls `Str` methods on the LiveMarkdownRuntime. Several `Str` - methods call other `Str` methods through `this` (e.g. `sanitizeURL` calls `this.cutBefore`). When the Worklets - Babel plugin runs without Bundle Mode (our Re.Pack builds), react-native-worklets 0.13 turns every method of the - `Str` object literal into a separate worklet, because it removed Worklet Context Objects - (software-mansion/react-native-reanimated#10411). `this` inside those methods no longer points to `Str` on the - Worklet Runtime, so typing a link in the composer crashed with "undefined is not a function". Referencing `Str` - from the methods instead does not work either: the plugin builds each method's closure while the object literal is - still being created, so `Str` is captured as `undefined`, which breaks `Str` on the JS thread too. - - The patch moves the 20 `Str` methods that other `Str` methods call (`cutBefore`, `isString`, `isTypeOf`, ...) to - module-level functions declared before `Str`, in dependency order, keeps them on `Str` as shorthand properties, - and replaces every `this.` with a direct call. The public `Str` API and its behavior are unchanged. - ``` - -- Upstream PR/issue: 🛑 -- E/App issue: https://github.com/Expensify/App/issues/101440 -- PR introducing patch: https://github.com/Expensify/App/pull/102266 diff --git a/patches/expensify-common/expensify-common+2.0.207+001+worklets-0.13-str-without-this.patch b/patches/expensify-common/expensify-common+2.0.207+001+worklets-0.13-str-without-this.patch deleted file mode 100644 index 0596cb49de16..000000000000 --- a/patches/expensify-common/expensify-common+2.0.207+001+worklets-0.13-str-without-this.patch +++ /dev/null @@ -1,590 +0,0 @@ -diff --git a/node_modules/expensify-common/dist/str.js b/node_modules/expensify-common/dist/str.js -index 124dd32..8d20430 100644 ---- a/node_modules/expensify-common/dist/str.js -+++ b/node_modules/expensify-common/dist/str.js -@@ -53,6 +53,126 @@ function resultFn(parameter, ...args) { - } - return parameter; - } -+// Str methods that other Str methods call are plain module-level functions instead of being reached through -+// `this`: react-native-worklets 0.13 no longer keeps an object literal together on a Worklet Runtime, so `this` -+// inside a workletized method does not point to Str there. -+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 undefined; -+ } -+ 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 we are grabbing the byte size, we need to temporarily diable no-bitwise for linting -+ /* eslint-disable no-bitwise */ -+ 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; -+ } -+ /* eslint-enable no-bitwise */ -+ 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) { -+ // eslint-disable-next-line no-void -+ 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 str is empty, str or mask aren't strings, -+ // or n is not a number, do nothing -+ 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; -+} - const Str = { - /** - * Return true if the string is ending with the provided suffix -@@ -110,18 +230,14 @@ const Str = { - * @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. - * -@@ -165,7 +281,7 @@ const Str = { - * @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. -@@ -251,32 +367,7 @@ const Str = { - * one. - * @returns Byte size of the character - */ -- getRawByteSize(inputChar) { -- const onlyChar = String(inputChar); -- const c = onlyChar.charCodeAt(0); -- // If we are grabbing the byte size, we need to temporarily diable no-bitwise for linting -- /* eslint-disable no-bitwise */ -- 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; -- } -- /* eslint-enable no-bitwise */ -- return Number.NaN; -- }, -+ getRawByteSize, - /** - * Gets the length of a string in bytes, including non-ASCII characters - * @returns The number of bytes used by string -@@ -284,7 +375,7 @@ const Str = { - getByteLength(input) { - // Force string type - 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; - }, - /** -@@ -296,7 +387,7 @@ const Str = { - 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) { - // If the next character exceeds the limit, stop and return the truncated string. - return `${stringInput.substr(0, i - 3)}...`; -@@ -312,9 +403,7 @@ const Str = { - * @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. - * -@@ -322,7 +411,7 @@ const Str = { - * @returns The text from within the HTML string. - */ - stripHTML(str) { -- if (!this.isString(str)) { -+ if (!isString(str)) { - return ''; - } - return str.replaceAll(/<[^>]*>?/gm, ''); -@@ -345,13 +434,7 @@ const Str = { - * @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. -@@ -361,13 +444,7 @@ const Str = { - * @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) - * -@@ -394,16 +471,7 @@ const Str = { - * - * @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. - * -@@ -421,9 +489,7 @@ const Str = { - * - * @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. - * -@@ -432,12 +498,12 @@ const Str = { - * @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; - }, - /** -@@ -454,9 +520,7 @@ const Str = { - * - * @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"). -@@ -466,7 +530,7 @@ const Str = { - * @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; - } -@@ -485,7 +549,7 @@ const Str = { - * @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. -@@ -543,13 +607,13 @@ const Str = { - * @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; -@@ -603,9 +667,7 @@ const Str = { - * - * @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 - * -@@ -615,7 +677,7 @@ const Str = { - * @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 -@@ -627,11 +689,7 @@ const Str = { - * 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 - * -@@ -640,7 +698,7 @@ const Str = { - * @returns true when first == second except for case - */ - caseInsensitiveEquals(strA, strB) { -- return this.caseInsensitiveCompare(strA, strB) === 0; -+ return caseInsensitiveCompare(strA, strB) === 0; - }, - /** - * Compare function -@@ -652,15 +710,7 @@ const Str = { - * 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 - */ -@@ -684,48 +734,39 @@ const Str = { - // Hide these numbers completely - // We should not be getting account numbers this small or large - if (len < 6 || len > 20) { -- return this.maskFirstNCharacters(accountNumber, len, 'X'); -+ return maskFirstNCharacters(accountNumber, len, 'X'); - } - // Can show last 4 - if (len < 14) { -- return this.maskFirstNCharacters(accountNumber, len - 4, 'X'); -+ return maskFirstNCharacters(accountNumber, len - 4, 'X'); - } - // Can show first 6 and last 4 - 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) { -- // eslint-disable-next-line no-void -- return obj === void 0; -- }, -+ isUndefined, - /** - * Replace first N characters of the string with maskChar - * eg: maskFirstNCharacters( '1234567890', 6, 'X' ) yields XXXXXX7890 -@@ -734,14 +775,7 @@ const Str = { - * @param mask String we want replace the first N chars with - * @returns Masked string - */ -- maskFirstNCharacters(str, num, mask) { -- // if str is empty, str or mask aren't strings, -- // or n is not a number, do nothing -- 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 - */ -@@ -753,7 +787,7 @@ const Str = { - * @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 -@@ -799,7 +833,7 @@ const Str = { - * Converts a value to boolean, case-insensitive. - */ - toBool(value) { -- if (this.isString(value)) { -+ if (isString(value)) { - return value.toLowerCase() === 'true'; - } - return !!value; -@@ -849,9 +883,7 @@ const Str = { - /** - * 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 -@@ -880,14 +912,12 @@ const Str = { - /** - * 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 -@@ -945,7 +975,7 @@ const Str = { - 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 -@@ -957,14 +987,7 @@ const Str = { - * 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 undefined; -- } -- 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 - * -@@ -972,7 +995,7 @@ const Str = { - * @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 -@@ -983,7 +1006,7 @@ const Str = { - * https://reactnative.dev/docs/image#source - */ - isImage(url) { -- const extension = this.getExtension(url); -+ const extension = getExtension(url); - if (!extension) { - return false; - } -@@ -999,7 +1022,7 @@ const Str = { - * 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; - } -@@ -1012,7 +1035,7 @@ const Str = { - * @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,