From dc45902530aac6ca32d8a2745da95a86a2735a08 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sun, 8 Mar 2026 23:09:50 +0000 Subject: [PATCH 01/14] chore: bumpt NitroModules and NitroSQLite --- package-lock.json | 815 ++++++++++++++-------------------------------- package.json | 6 +- 2 files changed, 239 insertions(+), 582 deletions(-) diff --git a/package-lock.json b/package-lock.json index ba179b10d301..69267611b6ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -115,9 +115,9 @@ "react-native-keyboard-controller": "1.21.0-beta.1", "react-native-launch-arguments": "^4.1.0", "react-native-localize": "^3.5.4", - "react-native-nitro-modules": "0.29.4", - "react-native-nitro-sqlite": "9.2.0", "react-native-onyx": "3.0.42", + "react-native-nitro-modules": "0.35.0", + "react-native-nitro-sqlite": "9.6.0", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", @@ -262,7 +262,7 @@ "link": "^2.1.1", "memfs": "^4.6.0", "mini-css-extract-plugin": "^2.9.4", - "nitrogen": "0.29.4", + "nitrogen": "0.35.0", "onchange": "^7.1.0", "openai": "^6.16.0", "patch-package": "^8.1.0-canary.1", @@ -470,27 +470,6 @@ "node": ">=6.0.0" } }, - "node_modules/@asamuzakjp/css-color": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", - "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^2.1.3", - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "lru-cache": "^10.4.3" - } - }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", @@ -4783,121 +4762,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@csstools/color-helpers": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", - "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - } - }, - "node_modules/@csstools/css-calc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", - "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", - "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^5.1.0", - "@csstools/css-calc": "^2.1.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", - "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", - "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "dev": true, @@ -14774,6 +14638,8 @@ }, "node_modules/@sqltools/formatter": { "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", "license": "MIT" }, "node_modules/@storybook/addon-a11y": { @@ -15857,32 +15723,51 @@ } }, "node_modules/@ts-morph/common": { - "version": "0.26.1", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.28.1.tgz", + "integrity": "sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==", "dev": true, "license": "MIT", "dependencies": { - "fast-glob": "^3.3.2", - "minimatch": "^9.0.4", - "path-browserify": "^1.0.1" + "minimatch": "^10.0.1", + "path-browserify": "^1.0.1", + "tinyglobby": "^0.2.14" + } + }, + "node_modules/@ts-morph/common/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/@ts-morph/common/node_modules/brace-expansion": { - "version": "2.0.1", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/@ts-morph/common/node_modules/minimatch": { - "version": "9.0.5", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -17873,6 +17758,15 @@ "node": ">=4" } }, + "node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "node_modules/any-promise": { "version": "1.3.0", "license": "MIT" @@ -17890,6 +17784,8 @@ }, "node_modules/app-root-path": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", + "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", "license": "MIT", "engines": { "node": ">= 6.0.0" @@ -18120,10 +18016,6 @@ "version": "2.0.6", "license": "MIT" }, - "node_modules/ascii-table": { - "version": "0.0.9", - "license": "MIT" - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -19596,83 +19488,6 @@ "node": ">=8" } }, - "node_modules/cli-highlight": { - "version": "2.1.11", - "license": "ISC", - "dependencies": { - "chalk": "^4.0.0", - "highlight.js": "^10.7.1", - "mz": "^2.4.0", - "parse5": "^5.1.1", - "parse5-htmlparser2-tree-adapter": "^6.0.0", - "yargs": "^16.0.0" - }, - "bin": { - "highlight": "bin/highlight" - }, - "engines": { - "node": ">=8.0.0", - "npm": ">=5.0.0" - } - }, - "node_modules/cli-highlight/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cli-highlight/node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cli-highlight/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/cli-highlight/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/cli-highlight/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-highlight/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cli-spinners": { "version": "2.9.2", "license": "MIT", @@ -19757,6 +19572,8 @@ }, "node_modules/code-block-writer": { "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", "dev": true, "license": "MIT" }, @@ -20976,20 +20793,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cssstyle": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", - "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^3.2.0", - "rrweb-cssom": "^0.8.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/csstype": { "version": "3.1.1", "license": "MIT" @@ -21198,47 +21001,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/data-urls": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/data-urls/node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/data-urls/node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -26117,13 +25879,6 @@ "hermes-estree": "0.32.0" } }, - "node_modules/highlight.js": { - "version": "10.7.3", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "license": "BSD-3-Clause", @@ -26366,30 +26121,6 @@ "node": ">=8.0.0" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/http-proxy-agent/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/http-proxy-middleware": { "version": "2.0.7", "dev": true, @@ -28220,6 +27951,21 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/jest-environment-jsdom/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/jest-environment-jsdom/node_modules/jsdom": { "version": "20.0.3", "dev": true, @@ -28275,6 +28021,32 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/jest-environment-jsdom/node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-environment-jsdom/node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/jest-environment-jsdom/node_modules/tr46": { "version": "3.0.0", "dev": true, @@ -28325,47 +28097,6 @@ "node": ">=12" } }, - "node_modules/jest-environment-jsdom/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/jest-environment-jsdom/node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jest-environment-jsdom/node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/jest-environment-node": { "version": "29.7.0", "license": "MIT", @@ -30705,18 +30436,10 @@ "version": "4.17.21", "license": "MIT" }, - "node_modules/lodash.bindall": { - "version": "4.4.0", - "license": "MIT" - }, "node_modules/lodash.camelcase": { "version": "4.3.0", "license": "MIT" }, - "node_modules/lodash.clone": { - "version": "4.5.0", - "license": "MIT" - }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -30743,18 +30466,10 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.pick": { - "version": "4.4.0", - "license": "MIT" - }, "node_modules/lodash.throttle": { "version": "4.1.1", "license": "MIT" }, - "node_modules/lodash.transform": { - "version": "4.6.0", - "license": "MIT" - }, "node_modules/log-symbols": { "version": "4.1.0", "devOptional": true, @@ -31914,17 +31629,16 @@ "license": "MIT" }, "node_modules/nitrogen": { - "version": "0.29.4", - "resolved": "https://registry.npmjs.org/nitrogen/-/nitrogen-0.29.4.tgz", - "integrity": "sha512-XtQyaWw12S8LpKD3muf+BXNXP2UAPONd4wC7oWK2+JoYVMSlYnMTnRxd6RRT7qH2j5KwFQeuRpwRPWSsxtCAXQ==", + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/nitrogen/-/nitrogen-0.35.0.tgz", + "integrity": "sha512-K8/4h9bCQahi3qEheWZx5joLFsAW3QjK0dVSC3gNLlQhlSJN42UFmffAouOZXYjg9rBDpVlrVo+Hsja45swsJQ==", "dev": true, - "hasInstallScript": true, "license": "MIT", "dependencies": { "chalk": "^5.3.0", - "react-native-nitro-modules": "^0.29.4", - "ts-morph": "^25.0.0", - "yargs": "^17.7.2", + "react-native-nitro-modules": "^0.35.0", + "ts-morph": "^27.0.0", + "yargs": "^18.0.0", "zod": "^4.0.5" }, "bin": { @@ -32680,21 +32394,6 @@ "dev": true, "license": "MIT" }, - "node_modules/parse5": { - "version": "5.1.1", - "license": "MIT" - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "parse5": "^6.0.1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { - "version": "6.0.1", - "license": "MIT" - }, "node_modules/parseurl": { "version": "1.3.3", "license": "MIT", @@ -32848,6 +32547,8 @@ }, "node_modules/path-browserify": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", "dev": true, "license": "MIT" }, @@ -34360,10 +34061,9 @@ } }, "node_modules/react-native-nitro-modules": { - "version": "0.29.4", - "resolved": "https://registry.npmjs.org/react-native-nitro-modules/-/react-native-nitro-modules-0.29.4.tgz", - "integrity": "sha512-AfUMcwFtj9FuEDwDLN5eIVo0lBYTQqDaV7meiFzuoZyRmc8ywykFTKfyZwRN2t8Z/WtTlfCj9Y9yaET33IImsg==", - "hasInstallScript": true, + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/react-native-nitro-modules/-/react-native-nitro-modules-0.35.0.tgz", + "integrity": "sha512-Eho1yEcLbsteGpBFn2XZOp5FIptnEciWzuYBW49S0jo41Un2LeyesIO/MqYLY/c5o7D9Fw9th4pxGtV7OAb0+g==", "license": "MIT", "peerDependencies": { "react": "*", @@ -34371,34 +34071,23 @@ } }, "node_modules/react-native-nitro-sqlite": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/react-native-nitro-sqlite/-/react-native-nitro-sqlite-9.2.0.tgz", - "integrity": "sha512-ROWf5ZDiRoU0kF3uaQTY7UNA6gpPNaiNGiOZAMOdY5enAIxX2WpsGeHzO9IuPHdTXqr6QuAmFe7FpoC0+qSRyA==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/react-native-nitro-sqlite/-/react-native-nitro-sqlite-9.6.0.tgz", + "integrity": "sha512-a/N1yGhM8RvCCnaYhEHhh35YS+HDOAcGKeKFsp2ExCzIjP8vPXuzQtHylgLQLeAh7rUaism5q0QQFfogXm1SXA==", "license": "MIT", "dependencies": { - "typeorm": "0.3.20" + "typeorm": "0.3.27" }, "peerDependencies": { "react": ">=17.0.0", "react-native": ">=0.75.0", - "react-native-nitro-modules": ">=0.27.2" - } - }, - "node_modules/react-native-nitro-sqlite/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "react-native-nitro-modules": ">=0.35.0" } }, "node_modules/react-native-nitro-sqlite/node_modules/buffer": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "funding": [ { "type": "github", @@ -34419,83 +34108,52 @@ "ieee754": "^1.2.1" } }, - "node_modules/react-native-nitro-sqlite/node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-native-nitro-sqlite/node_modules/color-convert": { - "version": "2.0.1", + "node_modules/react-native-nitro-sqlite/node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/react-native-nitro-sqlite/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/react-native-nitro-sqlite/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } } }, - "node_modules/react-native-nitro-sqlite/node_modules/mkdirp": { - "version": "2.1.6", - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, + "node_modules/react-native-nitro-sqlite/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/react-native-nitro-sqlite/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" + "url": "https://dotenvx.com" } }, "node_modules/react-native-nitro-sqlite/node_modules/typeorm": { - "version": "0.3.20", + "version": "0.3.27", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.27.tgz", + "integrity": "sha512-pNV1bn+1n8qEe8tUNsNdD8ejuPcMAg47u2lUGnbsajiNUr3p2Js1XLKQjBMH0yMRMDfdX8T+fIRejFmIwy9x4A==", "license": "MIT", "dependencies": { "@sqltools/formatter": "^1.2.5", + "ansis": "^3.17.0", "app-root-path": "^3.1.0", "buffer": "^6.0.3", - "chalk": "^4.1.2", - "cli-highlight": "^2.1.11", - "dayjs": "^1.11.9", - "debug": "^4.3.4", - "dotenv": "^16.0.3", - "glob": "^10.3.10", - "mkdirp": "^2.1.3", - "reflect-metadata": "^0.2.1", - "sha.js": "^2.4.11", - "tslib": "^2.5.0", - "uuid": "^9.0.0", - "yargs": "^17.6.2" + "dayjs": "^1.11.13", + "debug": "^4.4.0", + "dedent": "^1.6.0", + "dotenv": "^16.4.7", + "glob": "^10.4.5", + "sha.js": "^2.4.12", + "sql-highlight": "^6.0.0", + "tslib": "^2.8.1", + "uuid": "^11.1.0", + "yargs": "^17.7.2" }, "bin": { "typeorm": "cli.js", @@ -34509,23 +34167,23 @@ "url": "https://opencollective.com/typeorm" }, "peerDependencies": { - "@google-cloud/spanner": "^5.18.0", - "@sap/hana-client": "^2.12.25", - "better-sqlite3": "^7.1.2 || ^8.0.0 || ^9.0.0", - "hdb-pool": "^0.1.6", + "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0", + "@sap/hana-client": "^2.14.22", + "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", "ioredis": "^5.0.4", - "mongodb": "^5.8.0", - "mssql": "^9.1.1 || ^10.0.1", + "mongodb": "^5.8.0 || ^6.0.0", + "mssql": "^9.1.1 || ^10.0.1 || ^11.0.1", "mysql2": "^2.2.5 || ^3.0.1", "oracledb": "^6.3.0", "pg": "^8.5.1", "pg-native": "^3.0.0", "pg-query-stream": "^4.0.0", - "redis": "^3.1.1 || ^4.0.0", + "redis": "^3.1.1 || ^4.0.0 || ^5.0.14", + "reflect-metadata": "^0.1.14 || ^0.2.0", "sql.js": "^1.4.0", "sqlite3": "^5.0.3", "ts-node": "^10.7.0", - "typeorm-aurora-data-api-driver": "^2.0.0" + "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0" }, "peerDependenciesMeta": { "@google-cloud/spanner": { @@ -34537,9 +34195,6 @@ "better-sqlite3": { "optional": true }, - "hdb-pool": { - "optional": true - }, "ioredis": { "optional": true }, @@ -34582,14 +34237,16 @@ } }, "node_modules/react-native-nitro-sqlite/node_modules/uuid": { - "version": "9.0.1", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/react-native-onyx": { @@ -34666,17 +34323,6 @@ "react-native-blob-util": ">=0.13.7" } }, - "node_modules/react-native-performance": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/react-native-performance/-/react-native-performance-6.0.0.tgz", - "integrity": "sha512-Sca75O8jhqXAnNbqvINnrw248Kv9cIwoGxToD8u2uX+BrkAxxXS+YhClEV5L3JdiOpdNCO1MJ5R9bgs2VkNpFg==", - "license": "MIT", - "optional": true, - "peer": true, - "peerDependencies": { - "react-native": "*" - } - }, "node_modules/react-native-permissions": { "version": "5.4.0", "license": "MIT", @@ -35306,7 +34952,10 @@ }, "node_modules/reflect-metadata": { "version": "0.2.2", - "license": "Apache-2.0" + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0", + "peer": true }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -35708,13 +35357,6 @@ "rock": "dist/src/bin.js" } }, - "node_modules/rrweb-cssom": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", - "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", - "dev": true, - "license": "MIT" - }, "node_modules/run-applescript": { "version": "7.0.0", "dev": true, @@ -36197,16 +35839,45 @@ "license": "ISC" }, "node_modules/sha.js": { - "version": "2.4.11", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", "license": "(MIT AND BSD-3-Clause)", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sha.js/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/shallow-clone": { "version": "3.0.1", "dev": true, @@ -36685,6 +36356,22 @@ "version": "1.0.3", "license": "BSD-3-Clause" }, + "node_modules/sql-highlight": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz", + "integrity": "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==", + "funding": [ + "https://github.com/scriptcoded/sql-highlight?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/scriptcoded" + } + ], + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/ssf": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", @@ -37849,30 +37536,44 @@ "node": ">=14.0.0" } }, - "node_modules/tldts": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", - "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", - "dev": true, + "node_modules/tmpl": { + "version": "1.0.5", + "license": "BSD-3-Clause" + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", "license": "MIT", "dependencies": { - "tldts-core": "^6.1.86" + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" }, - "bin": { - "tldts": "bin/cli.js" + "engines": { + "node": ">= 0.4" } }, - "node_modules/tldts-core": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", - "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", - "dev": true, + "node_modules/to-buffer/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT" }, - "node_modules/tmpl": { - "version": "1.0.5", - "license": "BSD-3-Clause" - }, "node_modules/to-regex-range": { "version": "5.0.1", "license": "MIT", @@ -37898,19 +37599,6 @@ "node": ">=6" } }, - "node_modules/tough-cookie": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", - "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^6.1.32" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/tr46": { "version": "0.0.3", "license": "MIT" @@ -38025,11 +37713,13 @@ } }, "node_modules/ts-morph": { - "version": "25.0.1", + "version": "27.0.2", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-27.0.2.tgz", + "integrity": "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==", "dev": true, "license": "MIT", "dependencies": { - "@ts-morph/common": "~0.26.0", + "@ts-morph/common": "~0.28.1", "code-block-writer": "^13.0.3" } }, @@ -38700,19 +38390,6 @@ "pbf": "^3.2.1" } }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/wait-port": { "version": "0.2.14", "dev": true, @@ -39270,16 +38947,6 @@ "version": "3.6.2", "license": "MIT" }, - "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/whatwg-url": { "version": "5.0.0", "license": "MIT", @@ -39661,16 +39328,6 @@ "node": ">=0.8" } }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, "node_modules/xml2js": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.0.tgz", diff --git a/package.json b/package.json index 962251ccd38a..5bdb4bf83348 100644 --- a/package.json +++ b/package.json @@ -179,9 +179,9 @@ "react-native-keyboard-controller": "1.21.0-beta.1", "react-native-launch-arguments": "^4.1.0", "react-native-localize": "^3.5.4", - "react-native-nitro-modules": "0.29.4", - "react-native-nitro-sqlite": "9.2.0", "react-native-onyx": "3.0.42", + "react-native-nitro-modules": "0.35.0", + "react-native-nitro-sqlite": "9.6.0", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", @@ -326,7 +326,7 @@ "link": "^2.1.1", "memfs": "^4.6.0", "mini-css-extract-plugin": "^2.9.4", - "nitrogen": "0.29.4", + "nitrogen": "0.35.0", "onchange": "^7.1.0", "openai": "^6.16.0", "patch-package": "^8.1.0-canary.1", From 614fab7df791355b20ab52aba2ca8d89486ad671 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sun, 8 Mar 2026 23:18:43 +0000 Subject: [PATCH 02/14] chore: bump nitrogen generated specs in ExpensifyNitroUtils --- .../ExpensifyNitroUtils+autolinking.cmake | 13 +- .../ExpensifyNitroUtils+autolinking.gradle | 2 +- .../android/ExpensifyNitroUtilsOnLoad.cpp | 68 +++++--- .../android/ExpensifyNitroUtilsOnLoad.hpp | 19 +- .../generated/android/c++/JContact.hpp | 14 +- .../generated/android/c++/JContactFields.hpp | 13 +- .../c++/JHybridAppStartTimeModuleSpec.cpp | 40 +++-- .../c++/JHybridAppStartTimeModuleSpec.hpp | 43 +++-- .../android/c++/JHybridContactsModuleSpec.cpp | 38 ++-- .../android/c++/JHybridContactsModuleSpec.hpp | 41 +++-- .../generated/android/c++/JStringHolder.hpp | 8 +- .../kotlin/com/margelo/nitro/utils/Contact.kt | 47 +++-- .../com/margelo/nitro/utils/ContactFields.kt | 4 +- .../nitro/utils/ExpensifyNitroUtilsOnLoad.kt | 2 +- .../utils/HybridAppStartTimeModuleSpec.kt | 36 ++-- .../nitro/utils/HybridContactsModuleSpec.kt | 35 ++-- .../com/margelo/nitro/utils/StringHolder.kt | 23 ++- .../ios/ExpensifyNitroUtils+autolinking.rb | 4 +- .../ExpensifyNitroUtils-Swift-Cxx-Bridge.cpp | 15 +- .../ExpensifyNitroUtils-Swift-Cxx-Bridge.hpp | 27 +-- ...ExpensifyNitroUtils-Swift-Cxx-Umbrella.hpp | 2 +- .../ios/ExpensifyNitroUtilsAutolinking.mm | 2 +- .../ios/ExpensifyNitroUtilsAutolinking.swift | 28 ++- .../c++/HybridAppStartTimeModuleSpecSwift.cpp | 2 +- .../c++/HybridAppStartTimeModuleSpecSwift.hpp | 18 +- .../ios/c++/HybridContactsModuleSpecSwift.cpp | 2 +- .../ios/c++/HybridContactsModuleSpecSwift.hpp | 11 +- .../generated/ios/swift/Contact.swift | 164 +++++------------- .../generated/ios/swift/ContactFields.swift | 2 +- .../swift/Func_void_std__exception_ptr.swift | 3 +- .../Func_void_std__vector_Contact_.swift | 3 +- .../swift/HybridAppStartTimeModuleSpec.swift | 16 +- .../HybridAppStartTimeModuleSpec_cxx.swift | 32 ++-- .../ios/swift/HybridContactsModuleSpec.swift | 14 +- .../swift/HybridContactsModuleSpec_cxx.swift | 21 ++- .../generated/ios/swift/StringHolder.swift | 12 +- .../nitrogen/generated/shared/c++/Contact.hpp | 50 ++++-- .../generated/shared/c++/ContactFields.hpp | 2 +- .../c++/HybridAppStartTimeModuleSpec.cpp | 3 +- .../c++/HybridAppStartTimeModuleSpec.hpp | 4 +- .../shared/c++/HybridContactsModuleSpec.cpp | 2 +- .../shared/c++/HybridContactsModuleSpec.hpp | 2 +- .../generated/shared/c++/StringHolder.hpp | 26 ++- 43 files changed, 485 insertions(+), 428 deletions(-) diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtils+autolinking.cmake b/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtils+autolinking.cmake index 90c9d6cab0e4..b09226ad9a7f 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtils+autolinking.cmake +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtils+autolinking.cmake @@ -2,7 +2,7 @@ # ExpensifyNitroUtils+autolinking.cmake # This file was generated by nitrogen. DO NOT MODIFY THIS FILE. # https://github.com/mrousavy/nitro -# Copyright © 2026 Marc Rousavy @ Margelo +# Copyright © Marc Rousavy @ Margelo # # This is a CMake file that adds all files generated by Nitrogen @@ -13,6 +13,12 @@ # include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/ExpensifyNitroUtils+autolinking.cmake) # ``` +# Define a flag to check if we are building properly +add_definitions(-DBUILDING_EXPENSIFYNITROUTILS_WITH_GENERATED_CMAKE_PROJECT) + +# Enable Raw Props parsing in react-native (for Nitro Views) +add_definitions(-DRN_SERIALIZABLE_STATE) + # Add all headers that were generated by Nitrogen include_directories( "../nitrogen/generated/shared/c++" @@ -34,12 +40,9 @@ target_sources( ../nitrogen/generated/android/c++/JHybridContactsModuleSpec.cpp ) -# Define a flag to check if we are building properly -add_definitions(-DBUILDING_EXPENSIFYNITROUTILS_WITH_GENERATED_CMAKE_PROJECT) - # From node_modules/react-native/ReactAndroid/cmake-utils/folly-flags.cmake # Used in node_modules/react-native/ReactAndroid/cmake-utils/ReactNative-application.cmake - target_compile_definitions( +target_compile_definitions( ExpensifyNitroUtils PRIVATE -DFOLLY_NO_CONFIG=1 -DFOLLY_HAVE_CLOCK_GETTIME=1 diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtils+autolinking.gradle b/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtils+autolinking.gradle index f199d0712989..f65bb1cd4a6b 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtils+autolinking.gradle +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtils+autolinking.gradle @@ -2,7 +2,7 @@ /// ExpensifyNitroUtils+autolinking.gradle /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// /// This is a Gradle file that adds all files generated by Nitrogen diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtilsOnLoad.cpp b/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtilsOnLoad.cpp index 00c1e6a59825..f328c82635bd 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtilsOnLoad.cpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtilsOnLoad.cpp @@ -2,7 +2,7 @@ /// ExpensifyNitroUtilsOnLoad.cpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #ifndef BUILDING_EXPENSIFYNITROUTILS_WITH_GENERATED_CMAKE_PROJECT @@ -22,33 +22,49 @@ namespace margelo::nitro::utils { int initialize(JavaVM* vm) { + return facebook::jni::initialize(vm, []() { + ::margelo::nitro::utils::registerAllNatives(); + }); +} + +struct JHybridContactsModuleSpecImpl: public jni::JavaClass { + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/utils/HybridContactsModule;"; + static std::shared_ptr create() { + static auto constructorFn = javaClassStatic()->getConstructor(); + jni::local_ref javaPart = javaClassStatic()->newObject(constructorFn); + return javaPart->getJHybridContactsModuleSpec(); + } +}; +struct JHybridAppStartTimeModuleSpecImpl: public jni::JavaClass { + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/utils/HybridAppStartTimeModule;"; + static std::shared_ptr create() { + static auto constructorFn = javaClassStatic()->getConstructor(); + jni::local_ref javaPart = javaClassStatic()->newObject(constructorFn); + return javaPart->getJHybridAppStartTimeModuleSpec(); + } +}; + +void registerAllNatives() { using namespace margelo::nitro; using namespace margelo::nitro::utils; - using namespace facebook; - - return facebook::jni::initialize(vm, [] { - // Register native JNI methods - margelo::nitro::utils::JHybridAppStartTimeModuleSpec::registerNatives(); - margelo::nitro::utils::JHybridContactsModuleSpec::registerNatives(); - - // Register Nitro Hybrid Objects - HybridObjectRegistry::registerHybridObjectConstructor( - "ContactsModule", - []() -> std::shared_ptr { - static DefaultConstructableObject object("com/margelo/nitro/utils/HybridContactsModule"); - auto instance = object.create(); - return instance->cthis()->shared(); - } - ); - HybridObjectRegistry::registerHybridObjectConstructor( - "AppStartTimeModule", - []() -> std::shared_ptr { - static DefaultConstructableObject object("com/margelo/nitro/utils/HybridAppStartTimeModule"); - auto instance = object.create(); - return instance->cthis()->shared(); - } - ); - }); + + // Register native JNI methods + margelo::nitro::utils::JHybridAppStartTimeModuleSpec::CxxPart::registerNatives(); + margelo::nitro::utils::JHybridContactsModuleSpec::CxxPart::registerNatives(); + + // Register Nitro Hybrid Objects + HybridObjectRegistry::registerHybridObjectConstructor( + "ContactsModule", + []() -> std::shared_ptr { + return JHybridContactsModuleSpecImpl::create(); + } + ); + HybridObjectRegistry::registerHybridObjectConstructor( + "AppStartTimeModule", + []() -> std::shared_ptr { + return JHybridAppStartTimeModuleSpecImpl::create(); + } + ); } } // namespace margelo::nitro::utils diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtilsOnLoad.hpp b/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtilsOnLoad.hpp index f6235b868cbb..28c454ec157e 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtilsOnLoad.hpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtilsOnLoad.hpp @@ -2,24 +2,33 @@ /// ExpensifyNitroUtilsOnLoad.hpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #include +#include #include namespace margelo::nitro::utils { + [[deprecated("Use registerNatives() instead.")]] + int initialize(JavaVM* vm); + /** - * Initializes the native (C++) part of ExpensifyNitroUtils, and autolinks all Hybrid Objects. - * Call this in your `JNI_OnLoad` function (probably inside `cpp-adapter.cpp`). + * Register the native (C++) part of ExpensifyNitroUtils, and autolinks all Hybrid Objects. + * Call this in your `JNI_OnLoad` function (probably inside `cpp-adapter.cpp`), + * inside a `facebook::jni::initialize(vm, ...)` call. * Example: * ```cpp (cpp-adapter.cpp) * JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { - * return margelo::nitro::utils::initialize(vm); + * return facebook::jni::initialize(vm, []() { + * // register all ExpensifyNitroUtils HybridObjects + * margelo::nitro::utils::registerNatives(); + * // any other custom registrations go here. + * }); * } * ``` */ - int initialize(JavaVM* vm); + void registerAllNatives(); } // namespace margelo::nitro::utils diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JContact.hpp b/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JContact.hpp index 2789f76a1836..2dccb42fa544 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JContact.hpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JContact.hpp @@ -2,7 +2,7 @@ /// JContact.hpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #pragma once @@ -78,7 +78,11 @@ namespace margelo::nitro::utils { */ [[maybe_unused]] static jni::local_ref fromCpp(const Contact& value) { - return newInstance( + using JSignature = JContact(jni::alias_ref, jni::alias_ref, jni::alias_ref>, jni::alias_ref>, jni::alias_ref); + static const auto clazz = javaClassStatic(); + static const auto create = clazz->getStaticMethod("fromCpp"); + return create( + clazz, value.firstName.has_value() ? jni::make_jstring(value.firstName.value()) : nullptr, value.lastName.has_value() ? jni::make_jstring(value.lastName.value()) : nullptr, value.phoneNumbers.has_value() ? [&]() { @@ -86,7 +90,8 @@ namespace margelo::nitro::utils { jni::local_ref> __array = jni::JArrayClass::newArray(__size); for (size_t __i = 0; __i < __size; __i++) { const auto& __element = value.phoneNumbers.value()[__i]; - __array->setElement(__i, *JStringHolder::fromCpp(__element)); + auto __elementJni = JStringHolder::fromCpp(__element); + __array->setElement(__i, *__elementJni); } return __array; }() : nullptr, @@ -95,7 +100,8 @@ namespace margelo::nitro::utils { jni::local_ref> __array = jni::JArrayClass::newArray(__size); for (size_t __i = 0; __i < __size; __i++) { const auto& __element = value.emailAddresses.value()[__i]; - __array->setElement(__i, *JStringHolder::fromCpp(__element)); + auto __elementJni = JStringHolder::fromCpp(__element); + __array->setElement(__i, *__elementJni); } return __array; }() : nullptr, diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JContactFields.hpp b/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JContactFields.hpp index 2cf3d29547bf..1ed3380932ce 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JContactFields.hpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JContactFields.hpp @@ -2,7 +2,7 @@ /// JContactFields.hpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #pragma once @@ -41,22 +41,21 @@ namespace margelo::nitro::utils { [[maybe_unused]] static jni::alias_ref fromCpp(ContactFields value) { static const auto clazz = javaClassStatic(); - static const auto fieldFIRST_NAME = clazz->getStaticField("FIRST_NAME"); - static const auto fieldLAST_NAME = clazz->getStaticField("LAST_NAME"); - static const auto fieldPHONE_NUMBERS = clazz->getStaticField("PHONE_NUMBERS"); - static const auto fieldEMAIL_ADDRESSES = clazz->getStaticField("EMAIL_ADDRESSES"); - static const auto fieldIMAGE_DATA = clazz->getStaticField("IMAGE_DATA"); - switch (value) { case ContactFields::FIRST_NAME: + static const auto fieldFIRST_NAME = clazz->getStaticField("FIRST_NAME"); return clazz->getStaticFieldValue(fieldFIRST_NAME); case ContactFields::LAST_NAME: + static const auto fieldLAST_NAME = clazz->getStaticField("LAST_NAME"); return clazz->getStaticFieldValue(fieldLAST_NAME); case ContactFields::PHONE_NUMBERS: + static const auto fieldPHONE_NUMBERS = clazz->getStaticField("PHONE_NUMBERS"); return clazz->getStaticFieldValue(fieldPHONE_NUMBERS); case ContactFields::EMAIL_ADDRESSES: + static const auto fieldEMAIL_ADDRESSES = clazz->getStaticField("EMAIL_ADDRESSES"); return clazz->getStaticFieldValue(fieldEMAIL_ADDRESSES); case ContactFields::IMAGE_DATA: + static const auto fieldIMAGE_DATA = clazz->getStaticField("IMAGE_DATA"); return clazz->getStaticFieldValue(fieldIMAGE_DATA); default: std::string stringValue = std::to_string(static_cast(value)); diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridAppStartTimeModuleSpec.cpp b/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridAppStartTimeModuleSpec.cpp index b4eb2b05e7d7..3556e2cc22ef 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridAppStartTimeModuleSpec.cpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridAppStartTimeModuleSpec.cpp @@ -2,7 +2,7 @@ /// JHybridAppStartTimeModuleSpec.cpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #include "JHybridAppStartTimeModuleSpec.hpp" @@ -13,37 +13,41 @@ namespace margelo::nitro::utils { - jni::local_ref JHybridAppStartTimeModuleSpec::initHybrid(jni::alias_ref jThis) { - return makeCxxInstance(jThis); + std::shared_ptr JHybridAppStartTimeModuleSpec::JavaPart::getJHybridAppStartTimeModuleSpec() { + auto hybridObject = JHybridObject::JavaPart::getJHybridObject(); + auto castHybridObject = std::dynamic_pointer_cast(hybridObject); + if (castHybridObject == nullptr) [[unlikely]] { + throw std::runtime_error("Failed to downcast JHybridObject to JHybridAppStartTimeModuleSpec!"); + } + return castHybridObject; } - void JHybridAppStartTimeModuleSpec::registerNatives() { - registerHybrid({ - makeNativeMethod("initHybrid", JHybridAppStartTimeModuleSpec::initHybrid), - }); + jni::local_ref JHybridAppStartTimeModuleSpec::CxxPart::initHybrid(jni::alias_ref jThis) { + return makeCxxInstance(jThis); } - size_t JHybridAppStartTimeModuleSpec::getExternalMemorySize() noexcept { - static const auto method = javaClassStatic()->getMethod("getMemorySize"); - return method(_javaPart); + std::shared_ptr JHybridAppStartTimeModuleSpec::CxxPart::createHybridObject(const jni::local_ref& javaPart) { + auto castJavaPart = jni::dynamic_ref_cast(javaPart); + if (castJavaPart == nullptr) [[unlikely]] { + throw std::runtime_error("Failed to cast JHybridObject::JavaPart to JHybridAppStartTimeModuleSpec::JavaPart!"); + } + return std::make_shared(castJavaPart); } - void JHybridAppStartTimeModuleSpec::dispose() noexcept { - static const auto method = javaClassStatic()->getMethod("dispose"); - method(_javaPart); + void JHybridAppStartTimeModuleSpec::CxxPart::registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", JHybridAppStartTimeModuleSpec::CxxPart::initHybrid), + }); } // Properties double JHybridAppStartTimeModuleSpec::getAppStartTime() { - static const auto method = javaClassStatic()->getMethod("getAppStartTime"); + static const auto method = _javaPart->javaClassStatic()->getMethod("getAppStartTime"); auto __result = method(_javaPart); return __result; } // Methods - void JHybridAppStartTimeModuleSpec::recordAppStartTime() { - static const auto method = javaClassStatic()->getMethod("recordAppStartTime"); - method(_javaPart); - } + } // namespace margelo::nitro::utils diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridAppStartTimeModuleSpec.hpp b/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridAppStartTimeModuleSpec.hpp index 135fb2db00d7..91fc411edbd0 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridAppStartTimeModuleSpec.hpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridAppStartTimeModuleSpec.hpp @@ -2,7 +2,7 @@ /// HybridAppStartTimeModuleSpec.hpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #pragma once @@ -18,32 +18,33 @@ namespace margelo::nitro::utils { using namespace facebook; - class JHybridAppStartTimeModuleSpec: public jni::HybridClass, - public virtual HybridAppStartTimeModuleSpec { + class JHybridAppStartTimeModuleSpec: public virtual HybridAppStartTimeModuleSpec, public virtual JHybridObject { public: - static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/utils/HybridAppStartTimeModuleSpec;"; - static jni::local_ref initHybrid(jni::alias_ref jThis); - static void registerNatives(); - - protected: - // C++ constructor (called from Java via `initHybrid()`) - explicit JHybridAppStartTimeModuleSpec(jni::alias_ref jThis) : - HybridObject(HybridAppStartTimeModuleSpec::TAG), - HybridBase(jThis), - _javaPart(jni::make_global(jThis)) {} + struct JavaPart: public jni::JavaClass { + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/utils/HybridAppStartTimeModuleSpec;"; + std::shared_ptr getJHybridAppStartTimeModuleSpec(); + }; + struct CxxPart: public jni::HybridClass { + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/utils/HybridAppStartTimeModuleSpec$CxxPart;"; + static jni::local_ref initHybrid(jni::alias_ref jThis); + static void registerNatives(); + using HybridBase::HybridBase; + protected: + std::shared_ptr createHybridObject(const jni::local_ref& javaPart) override; + }; public: + explicit JHybridAppStartTimeModuleSpec(const jni::local_ref& javaPart): + HybridObject(HybridAppStartTimeModuleSpec::TAG), + JHybridObject(javaPart), + _javaPart(jni::make_global(javaPart)) {} ~JHybridAppStartTimeModuleSpec() override { // Hermes GC can destroy JS objects on a non-JNI Thread. jni::ThreadScope::WithClassLoader([&] { _javaPart.reset(); }); } public: - size_t getExternalMemorySize() noexcept override; - void dispose() noexcept override; - - public: - inline const jni::global_ref& getJavaPart() const noexcept { + inline const jni::global_ref& getJavaPart() const noexcept { return _javaPart; } @@ -53,12 +54,10 @@ namespace margelo::nitro::utils { public: // Methods - void recordAppStartTime() override; + private: - friend HybridBase; - using HybridBase::HybridBase; - jni::global_ref _javaPart; + jni::global_ref _javaPart; }; } // namespace margelo::nitro::utils diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridContactsModuleSpec.cpp b/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridContactsModuleSpec.cpp index 17499137b22a..812c363b0d36 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridContactsModuleSpec.cpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridContactsModuleSpec.cpp @@ -2,7 +2,7 @@ /// JHybridContactsModuleSpec.cpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #include "JHybridContactsModuleSpec.hpp" @@ -28,24 +28,31 @@ namespace margelo::nitro::utils { enum class ContactFields; } namespace margelo::nitro::utils { - jni::local_ref JHybridContactsModuleSpec::initHybrid(jni::alias_ref jThis) { - return makeCxxInstance(jThis); + std::shared_ptr JHybridContactsModuleSpec::JavaPart::getJHybridContactsModuleSpec() { + auto hybridObject = JHybridObject::JavaPart::getJHybridObject(); + auto castHybridObject = std::dynamic_pointer_cast(hybridObject); + if (castHybridObject == nullptr) [[unlikely]] { + throw std::runtime_error("Failed to downcast JHybridObject to JHybridContactsModuleSpec!"); + } + return castHybridObject; } - void JHybridContactsModuleSpec::registerNatives() { - registerHybrid({ - makeNativeMethod("initHybrid", JHybridContactsModuleSpec::initHybrid), - }); + jni::local_ref JHybridContactsModuleSpec::CxxPart::initHybrid(jni::alias_ref jThis) { + return makeCxxInstance(jThis); } - size_t JHybridContactsModuleSpec::getExternalMemorySize() noexcept { - static const auto method = javaClassStatic()->getMethod("getMemorySize"); - return method(_javaPart); + std::shared_ptr JHybridContactsModuleSpec::CxxPart::createHybridObject(const jni::local_ref& javaPart) { + auto castJavaPart = jni::dynamic_ref_cast(javaPart); + if (castJavaPart == nullptr) [[unlikely]] { + throw std::runtime_error("Failed to cast JHybridObject::JavaPart to JHybridContactsModuleSpec::JavaPart!"); + } + return std::make_shared(castJavaPart); } - void JHybridContactsModuleSpec::dispose() noexcept { - static const auto method = javaClassStatic()->getMethod("dispose"); - method(_javaPart); + void JHybridContactsModuleSpec::CxxPart::registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", JHybridContactsModuleSpec::CxxPart::initHybrid), + }); } // Properties @@ -53,13 +60,14 @@ namespace margelo::nitro::utils { // Methods std::shared_ptr>> JHybridContactsModuleSpec::getAll(const std::vector& keys) { - static const auto method = javaClassStatic()->getMethod(jni::alias_ref> /* keys */)>("getAll"); + static const auto method = _javaPart->javaClassStatic()->getMethod(jni::alias_ref> /* keys */)>("getAll"); auto __result = method(_javaPart, [&]() { size_t __size = keys.size(); jni::local_ref> __array = jni::JArrayClass::newArray(__size); for (size_t __i = 0; __i < __size; __i++) { const auto& __element = keys[__i]; - __array->setElement(__i, *JContactFields::fromCpp(__element)); + auto __elementJni = JContactFields::fromCpp(__element); + __array->setElement(__i, *__elementJni); } return __array; }()); diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridContactsModuleSpec.hpp b/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridContactsModuleSpec.hpp index c4977a2937c1..b140e3c9f376 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridContactsModuleSpec.hpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridContactsModuleSpec.hpp @@ -2,7 +2,7 @@ /// HybridContactsModuleSpec.hpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #pragma once @@ -18,32 +18,33 @@ namespace margelo::nitro::utils { using namespace facebook; - class JHybridContactsModuleSpec: public jni::HybridClass, - public virtual HybridContactsModuleSpec { + class JHybridContactsModuleSpec: public virtual HybridContactsModuleSpec, public virtual JHybridObject { public: - static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/utils/HybridContactsModuleSpec;"; - static jni::local_ref initHybrid(jni::alias_ref jThis); - static void registerNatives(); - - protected: - // C++ constructor (called from Java via `initHybrid()`) - explicit JHybridContactsModuleSpec(jni::alias_ref jThis) : - HybridObject(HybridContactsModuleSpec::TAG), - HybridBase(jThis), - _javaPart(jni::make_global(jThis)) {} + struct JavaPart: public jni::JavaClass { + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/utils/HybridContactsModuleSpec;"; + std::shared_ptr getJHybridContactsModuleSpec(); + }; + struct CxxPart: public jni::HybridClass { + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/utils/HybridContactsModuleSpec$CxxPart;"; + static jni::local_ref initHybrid(jni::alias_ref jThis); + static void registerNatives(); + using HybridBase::HybridBase; + protected: + std::shared_ptr createHybridObject(const jni::local_ref& javaPart) override; + }; public: + explicit JHybridContactsModuleSpec(const jni::local_ref& javaPart): + HybridObject(HybridContactsModuleSpec::TAG), + JHybridObject(javaPart), + _javaPart(jni::make_global(javaPart)) {} ~JHybridContactsModuleSpec() override { // Hermes GC can destroy JS objects on a non-JNI Thread. jni::ThreadScope::WithClassLoader([&] { _javaPart.reset(); }); } public: - size_t getExternalMemorySize() noexcept override; - void dispose() noexcept override; - - public: - inline const jni::global_ref& getJavaPart() const noexcept { + inline const jni::global_ref& getJavaPart() const noexcept { return _javaPart; } @@ -56,9 +57,7 @@ namespace margelo::nitro::utils { std::shared_ptr>> getAll(const std::vector& keys) override; private: - friend HybridBase; - using HybridBase::HybridBase; - jni::global_ref _javaPart; + jni::global_ref _javaPart; }; } // namespace margelo::nitro::utils diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JStringHolder.hpp b/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JStringHolder.hpp index babbbf5289c0..11b563d2b695 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JStringHolder.hpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JStringHolder.hpp @@ -2,7 +2,7 @@ /// JStringHolder.hpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #pragma once @@ -44,7 +44,11 @@ namespace margelo::nitro::utils { */ [[maybe_unused]] static jni::local_ref fromCpp(const StringHolder& value) { - return newInstance( + using JSignature = JStringHolder(jni::alias_ref); + static const auto clazz = javaClassStatic(); + static const auto create = clazz->getStaticMethod("fromCpp"); + return create( + clazz, jni::make_jstring(value.value) ); } diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/Contact.kt b/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/Contact.kt index 327fe876dbbb..ca037de30643 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/Contact.kt +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/Contact.kt @@ -2,14 +2,13 @@ /// Contact.kt /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// package com.margelo.nitro.utils import androidx.annotation.Keep import com.facebook.proguard.annotations.DoNotStrip -import com.margelo.nitro.core.* /** @@ -17,25 +16,35 @@ import com.margelo.nitro.core.* */ @DoNotStrip @Keep -data class Contact +data class Contact( @DoNotStrip @Keep - constructor( - @DoNotStrip - @Keep - val firstName: String?, - @DoNotStrip - @Keep - val lastName: String?, - @DoNotStrip - @Keep - val phoneNumbers: Array?, - @DoNotStrip - @Keep - val emailAddresses: Array?, + val firstName: String?, + @DoNotStrip + @Keep + val lastName: String?, + @DoNotStrip + @Keep + val phoneNumbers: Array?, + @DoNotStrip + @Keep + val emailAddresses: Array?, + @DoNotStrip + @Keep + val imageData: String? +) { + /* primary constructor */ + + companion object { + /** + * Constructor called from C++ + */ @DoNotStrip @Keep - val imageData: String? - ) { - /* main constructor */ + @Suppress("unused") + @JvmStatic + private fun fromCpp(firstName: String?, lastName: String?, phoneNumbers: Array?, emailAddresses: Array?, imageData: String?): Contact { + return Contact(firstName, lastName, phoneNumbers, emailAddresses, imageData) + } + } } diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/ContactFields.kt b/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/ContactFields.kt index 619aed2f224f..8c2c337ac373 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/ContactFields.kt +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/ContactFields.kt @@ -2,7 +2,7 @@ /// ContactFields.kt /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// package com.margelo.nitro.utils @@ -21,4 +21,6 @@ enum class ContactFields(@DoNotStrip @Keep val value: Int) { PHONE_NUMBERS(2), EMAIL_ADDRESSES(3), IMAGE_DATA(4); + + companion object } diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/ExpensifyNitroUtilsOnLoad.kt b/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/ExpensifyNitroUtilsOnLoad.kt index f454ec43e2ff..3b6c58ae83cb 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/ExpensifyNitroUtilsOnLoad.kt +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/ExpensifyNitroUtilsOnLoad.kt @@ -2,7 +2,7 @@ /// ExpensifyNitroUtilsOnLoad.kt /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// package com.margelo.nitro.utils diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/HybridAppStartTimeModuleSpec.kt b/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/HybridAppStartTimeModuleSpec.kt index a03637b06ae7..f925221dd42b 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/HybridAppStartTimeModuleSpec.kt +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/HybridAppStartTimeModuleSpec.kt @@ -2,7 +2,7 @@ /// HybridAppStartTimeModuleSpec.kt /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// package com.margelo.nitro.utils @@ -10,7 +10,7 @@ package com.margelo.nitro.utils import androidx.annotation.Keep import com.facebook.jni.HybridData import com.facebook.proguard.annotations.DoNotStrip -import com.margelo.nitro.core.* +import com.margelo.nitro.core.HybridObject /** * A Kotlin class representing the AppStartTimeModule HybridObject. @@ -24,31 +24,31 @@ import com.margelo.nitro.core.* "LocalVariableName", "PropertyName", "PrivatePropertyName", "FunctionName" ) abstract class HybridAppStartTimeModuleSpec: HybridObject() { - @DoNotStrip - private var mHybridData: HybridData = initHybrid() - - init { - super.updateNative(mHybridData) - } - - override fun updateNative(hybridData: HybridData) { - mHybridData = hybridData - super.updateNative(hybridData) - } - // Properties @get:DoNotStrip @get:Keep abstract val appStartTime: Double // Methods + + + // Default implementation of `HybridObject.toString()` + override fun toString(): String { + return "[HybridObject AppStartTimeModule]" + } + + // C++ backing class @DoNotStrip @Keep - abstract fun recordAppStartTime(): Unit - - private external fun initHybrid(): HybridData + protected open class CxxPart(javaPart: HybridAppStartTimeModuleSpec): HybridObject.CxxPart(javaPart) { + // C++ JHybridAppStartTimeModuleSpec::CxxPart::initHybrid(...) + external override fun initHybrid(): HybridData + } + override fun createCxxPart(): CxxPart { + return CxxPart(this) + } companion object { - private const val TAG = "HybridAppStartTimeModuleSpec" + protected const val TAG = "HybridAppStartTimeModuleSpec" } } diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/HybridContactsModuleSpec.kt b/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/HybridContactsModuleSpec.kt index eb72823ea494..8f68b49898ea 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/HybridContactsModuleSpec.kt +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/HybridContactsModuleSpec.kt @@ -2,7 +2,7 @@ /// HybridContactsModuleSpec.kt /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// package com.margelo.nitro.utils @@ -10,7 +10,8 @@ package com.margelo.nitro.utils import androidx.annotation.Keep import com.facebook.jni.HybridData import com.facebook.proguard.annotations.DoNotStrip -import com.margelo.nitro.core.* +import com.margelo.nitro.core.Promise +import com.margelo.nitro.core.HybridObject /** * A Kotlin class representing the ContactsModule HybridObject. @@ -24,18 +25,6 @@ import com.margelo.nitro.core.* "LocalVariableName", "PropertyName", "PrivatePropertyName", "FunctionName" ) abstract class HybridContactsModuleSpec: HybridObject() { - @DoNotStrip - private var mHybridData: HybridData = initHybrid() - - init { - super.updateNative(mHybridData) - } - - override fun updateNative(hybridData: HybridData) { - mHybridData = hybridData - super.updateNative(hybridData) - } - // Properties @@ -44,9 +33,23 @@ abstract class HybridContactsModuleSpec: HybridObject() { @Keep abstract fun getAll(keys: Array): Promise> - private external fun initHybrid(): HybridData + // Default implementation of `HybridObject.toString()` + override fun toString(): String { + return "[HybridObject ContactsModule]" + } + + // C++ backing class + @DoNotStrip + @Keep + protected open class CxxPart(javaPart: HybridContactsModuleSpec): HybridObject.CxxPart(javaPart) { + // C++ JHybridContactsModuleSpec::CxxPart::initHybrid(...) + external override fun initHybrid(): HybridData + } + override fun createCxxPart(): CxxPart { + return CxxPart(this) + } companion object { - private const val TAG = "HybridContactsModuleSpec" + protected const val TAG = "HybridContactsModuleSpec" } } diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/StringHolder.kt b/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/StringHolder.kt index 2b5330c8c4e2..accaebee2968 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/StringHolder.kt +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/StringHolder.kt @@ -2,14 +2,13 @@ /// StringHolder.kt /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// package com.margelo.nitro.utils import androidx.annotation.Keep import com.facebook.proguard.annotations.DoNotStrip -import com.margelo.nitro.core.* /** @@ -17,13 +16,23 @@ import com.margelo.nitro.core.* */ @DoNotStrip @Keep -data class StringHolder +data class StringHolder( @DoNotStrip @Keep - constructor( + val value: String +) { + /* primary constructor */ + + companion object { + /** + * Constructor called from C++ + */ @DoNotStrip @Keep - val value: String - ) { - /* main constructor */ + @Suppress("unused") + @JvmStatic + private fun fromCpp(value: String): StringHolder { + return StringHolder(value) + } + } } diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils+autolinking.rb b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils+autolinking.rb index 997952fe1fc7..78c2c451cd92 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils+autolinking.rb +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils+autolinking.rb @@ -2,7 +2,7 @@ # ExpensifyNitroUtils+autolinking.rb # This file was generated by nitrogen. DO NOT MODIFY THIS FILE. # https://github.com/mrousavy/nitro -# Copyright © 2026 Marc Rousavy @ Margelo +# Copyright © Marc Rousavy @ Margelo # # This is a Ruby script that adds all files generated by Nitrogen @@ -52,7 +52,7 @@ def add_nitrogen_files(spec) spec.pod_target_xcconfig = current_pod_target_xcconfig.merge({ # Use C++ 20 "CLANG_CXX_LANGUAGE_STANDARD" => "c++20", - # Enables C++ <-> Swift interop (by default it's only C) + # Enables C++ <-> Swift interop (by default it's only ObjC) "SWIFT_OBJC_INTEROP_MODE" => "objcxx", # Enables stricter modular headers "DEFINES_MODULE" => "YES", diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Bridge.cpp b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Bridge.cpp index b4ba285734f0..bad2bd0d8e7a 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Bridge.cpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Bridge.cpp @@ -2,7 +2,7 @@ /// ExpensifyNitroUtils-Swift-Cxx-Bridge.cpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #include "ExpensifyNitroUtils-Swift-Cxx-Bridge.hpp" @@ -11,15 +11,16 @@ #include "ExpensifyNitroUtils-Swift-Cxx-Umbrella.hpp" #include "HybridAppStartTimeModuleSpecSwift.hpp" #include "HybridContactsModuleSpecSwift.hpp" +#include namespace margelo::nitro::utils::bridge::swift { // pragma MARK: std::shared_ptr - std::shared_ptr create_std__shared_ptr_HybridAppStartTimeModuleSpec_(void* _Nonnull swiftUnsafePointer) noexcept { + std::shared_ptr create_std__shared_ptr_HybridAppStartTimeModuleSpec_(void* NON_NULL swiftUnsafePointer) noexcept { ExpensifyNitroUtils::HybridAppStartTimeModuleSpec_cxx swiftPart = ExpensifyNitroUtils::HybridAppStartTimeModuleSpec_cxx::fromUnsafe(swiftUnsafePointer); return std::make_shared(swiftPart); } - void* _Nonnull get_std__shared_ptr_HybridAppStartTimeModuleSpec_(std__shared_ptr_HybridAppStartTimeModuleSpec_ cppType) noexcept { + void* NON_NULL get_std__shared_ptr_HybridAppStartTimeModuleSpec_(std__shared_ptr_HybridAppStartTimeModuleSpec_ cppType) { std::shared_ptr swiftWrapper = std::dynamic_pointer_cast(cppType); #ifdef NITRO_DEBUG if (swiftWrapper == nullptr) [[unlikely]] { @@ -31,7 +32,7 @@ namespace margelo::nitro::utils::bridge::swift { } // pragma MARK: std::function& /* result */)> - Func_void_std__vector_Contact_ create_Func_void_std__vector_Contact_(void* _Nonnull swiftClosureWrapper) noexcept { + Func_void_std__vector_Contact_ create_Func_void_std__vector_Contact_(void* NON_NULL swiftClosureWrapper) noexcept { auto swiftClosure = ExpensifyNitroUtils::Func_void_std__vector_Contact_::fromUnsafe(swiftClosureWrapper); return [swiftClosure = std::move(swiftClosure)](const std::vector& result) mutable -> void { swiftClosure.call(result); @@ -39,7 +40,7 @@ namespace margelo::nitro::utils::bridge::swift { } // pragma MARK: std::function - Func_void_std__exception_ptr create_Func_void_std__exception_ptr(void* _Nonnull swiftClosureWrapper) noexcept { + Func_void_std__exception_ptr create_Func_void_std__exception_ptr(void* NON_NULL swiftClosureWrapper) noexcept { auto swiftClosure = ExpensifyNitroUtils::Func_void_std__exception_ptr::fromUnsafe(swiftClosureWrapper); return [swiftClosure = std::move(swiftClosure)](const std::exception_ptr& error) mutable -> void { swiftClosure.call(error); @@ -47,11 +48,11 @@ namespace margelo::nitro::utils::bridge::swift { } // pragma MARK: std::shared_ptr - std::shared_ptr create_std__shared_ptr_HybridContactsModuleSpec_(void* _Nonnull swiftUnsafePointer) noexcept { + std::shared_ptr create_std__shared_ptr_HybridContactsModuleSpec_(void* NON_NULL swiftUnsafePointer) noexcept { ExpensifyNitroUtils::HybridContactsModuleSpec_cxx swiftPart = ExpensifyNitroUtils::HybridContactsModuleSpec_cxx::fromUnsafe(swiftUnsafePointer); return std::make_shared(swiftPart); } - void* _Nonnull get_std__shared_ptr_HybridContactsModuleSpec_(std__shared_ptr_HybridContactsModuleSpec_ cppType) noexcept { + void* NON_NULL get_std__shared_ptr_HybridContactsModuleSpec_(std__shared_ptr_HybridContactsModuleSpec_ cppType) { std::shared_ptr swiftWrapper = std::dynamic_pointer_cast(cppType); #ifdef NITRO_DEBUG if (swiftWrapper == nullptr) [[unlikely]] { diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Bridge.hpp b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Bridge.hpp index ebcb69c0ee02..1e38d4cb4153 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Bridge.hpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Bridge.hpp @@ -2,7 +2,7 @@ /// ExpensifyNitroUtils-Swift-Cxx-Bridge.hpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #pragma once @@ -52,22 +52,13 @@ namespace margelo::nitro::utils::bridge::swift { * Specialized version of `std::shared_ptr`. */ using std__shared_ptr_HybridAppStartTimeModuleSpec_ = std::shared_ptr; - std::shared_ptr create_std__shared_ptr_HybridAppStartTimeModuleSpec_(void* _Nonnull swiftUnsafePointer) noexcept; - void* _Nonnull get_std__shared_ptr_HybridAppStartTimeModuleSpec_(std__shared_ptr_HybridAppStartTimeModuleSpec_ cppType) noexcept; + std::shared_ptr create_std__shared_ptr_HybridAppStartTimeModuleSpec_(void* NON_NULL swiftUnsafePointer) noexcept; + void* NON_NULL get_std__shared_ptr_HybridAppStartTimeModuleSpec_(std__shared_ptr_HybridAppStartTimeModuleSpec_ cppType); // pragma MARK: std::weak_ptr using std__weak_ptr_HybridAppStartTimeModuleSpec_ = std::weak_ptr; inline std__weak_ptr_HybridAppStartTimeModuleSpec_ weakify_std__shared_ptr_HybridAppStartTimeModuleSpec_(const std::shared_ptr& strong) noexcept { return strong; } - // pragma MARK: Result - using Result_void_ = Result; - inline Result_void_ create_Result_void_() noexcept { - return Result::withValue(); - } - inline Result_void_ create_Result_void_(const std::exception_ptr& error) noexcept { - return Result::withError(error); - } - // pragma MARK: std::optional /** * Specialized version of `std::optional`. @@ -80,7 +71,7 @@ namespace margelo::nitro::utils::bridge::swift { return optional.has_value(); } inline std::string get_std__optional_std__string_(const std::optional& optional) noexcept { - return *optional; + return optional.value(); } // pragma MARK: std::vector @@ -106,7 +97,7 @@ namespace margelo::nitro::utils::bridge::swift { return optional.has_value(); } inline std::vector get_std__optional_std__vector_StringHolder__(const std::optional>& optional) noexcept { - return *optional; + return optional.value(); } // pragma MARK: std::vector @@ -149,7 +140,7 @@ namespace margelo::nitro::utils::bridge::swift { private: std::unique_ptr& /* result */)>> _function; } SWIFT_NONCOPYABLE; - Func_void_std__vector_Contact_ create_Func_void_std__vector_Contact_(void* _Nonnull swiftClosureWrapper) noexcept; + Func_void_std__vector_Contact_ create_Func_void_std__vector_Contact_(void* NON_NULL swiftClosureWrapper) noexcept; inline Func_void_std__vector_Contact__Wrapper wrap_Func_void_std__vector_Contact_(Func_void_std__vector_Contact_ value) noexcept { return Func_void_std__vector_Contact__Wrapper(std::move(value)); } @@ -171,7 +162,7 @@ namespace margelo::nitro::utils::bridge::swift { private: std::unique_ptr> _function; } SWIFT_NONCOPYABLE; - Func_void_std__exception_ptr create_Func_void_std__exception_ptr(void* _Nonnull swiftClosureWrapper) noexcept; + Func_void_std__exception_ptr create_Func_void_std__exception_ptr(void* NON_NULL swiftClosureWrapper) noexcept; inline Func_void_std__exception_ptr_Wrapper wrap_Func_void_std__exception_ptr(Func_void_std__exception_ptr value) noexcept { return Func_void_std__exception_ptr_Wrapper(std::move(value)); } @@ -192,8 +183,8 @@ namespace margelo::nitro::utils::bridge::swift { * Specialized version of `std::shared_ptr`. */ using std__shared_ptr_HybridContactsModuleSpec_ = std::shared_ptr; - std::shared_ptr create_std__shared_ptr_HybridContactsModuleSpec_(void* _Nonnull swiftUnsafePointer) noexcept; - void* _Nonnull get_std__shared_ptr_HybridContactsModuleSpec_(std__shared_ptr_HybridContactsModuleSpec_ cppType) noexcept; + std::shared_ptr create_std__shared_ptr_HybridContactsModuleSpec_(void* NON_NULL swiftUnsafePointer) noexcept; + void* NON_NULL get_std__shared_ptr_HybridContactsModuleSpec_(std__shared_ptr_HybridContactsModuleSpec_ cppType); // pragma MARK: std::weak_ptr using std__weak_ptr_HybridContactsModuleSpec_ = std::weak_ptr; diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Umbrella.hpp b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Umbrella.hpp index 52ea5f386373..f61ac3b6b942 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Umbrella.hpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Umbrella.hpp @@ -2,7 +2,7 @@ /// ExpensifyNitroUtils-Swift-Cxx-Umbrella.hpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #pragma once diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtilsAutolinking.mm b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtilsAutolinking.mm index b3101413be34..dcf15dd6aba0 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtilsAutolinking.mm +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtilsAutolinking.mm @@ -2,7 +2,7 @@ /// ExpensifyNitroUtilsAutolinking.mm /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #import diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtilsAutolinking.swift b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtilsAutolinking.swift index b402dc07c98f..5794c7097255 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtilsAutolinking.swift +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtilsAutolinking.swift @@ -2,19 +2,16 @@ /// ExpensifyNitroUtilsAutolinking.swift /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// +import NitroModules + +// TODO: Use empty enums once Swift supports exporting them as namespaces +// See: https://github.com/swiftlang/swift/pull/83616 public final class ExpensifyNitroUtilsAutolinking { public typealias bridge = margelo.nitro.utils.bridge.swift - /** - * Creates an instance of a Swift class that implements `HybridContactsModuleSpec`, - * and wraps it in a Swift class that can directly interop with C++ (`HybridContactsModuleSpec_cxx`) - * - * This is generated by Nitrogen and will initialize the class specified - * in the `"autolinking"` property of `nitro.json` (in this case, `HybridContactsModule`). - */ public static func createContactsModule() -> bridge.std__shared_ptr_HybridContactsModuleSpec_ { let hybridObject = HybridContactsModule() return { () -> bridge.std__shared_ptr_HybridContactsModuleSpec_ in @@ -23,13 +20,10 @@ public final class ExpensifyNitroUtilsAutolinking { }() } - /** - * Creates an instance of a Swift class that implements `HybridAppStartTimeModuleSpec`, - * and wraps it in a Swift class that can directly interop with C++ (`HybridAppStartTimeModuleSpec_cxx`) - * - * This is generated by Nitrogen and will initialize the class specified - * in the `"autolinking"` property of `nitro.json` (in this case, `HybridAppStartTimeModule`). - */ + public static func isContactsModuleRecyclable() -> Bool { + return HybridContactsModule.self is any RecyclableView.Type + } + public static func createAppStartTimeModule() -> bridge.std__shared_ptr_HybridAppStartTimeModuleSpec_ { let hybridObject = HybridAppStartTimeModule() return { () -> bridge.std__shared_ptr_HybridAppStartTimeModuleSpec_ in @@ -37,4 +31,8 @@ public final class ExpensifyNitroUtilsAutolinking { return __cxxWrapped.getCxxPart() }() } + + public static func isAppStartTimeModuleRecyclable() -> Bool { + return HybridAppStartTimeModule.self is any RecyclableView.Type + } } diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridAppStartTimeModuleSpecSwift.cpp b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridAppStartTimeModuleSpecSwift.cpp index e9cb5771aaa3..c5c0f33208a5 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridAppStartTimeModuleSpecSwift.cpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridAppStartTimeModuleSpecSwift.cpp @@ -2,7 +2,7 @@ /// HybridAppStartTimeModuleSpecSwift.cpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #include "HybridAppStartTimeModuleSpecSwift.hpp" diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridAppStartTimeModuleSpecSwift.hpp b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridAppStartTimeModuleSpecSwift.hpp index 8a0da8729b56..2f7cf1df7128 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridAppStartTimeModuleSpecSwift.hpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridAppStartTimeModuleSpecSwift.hpp @@ -2,7 +2,7 @@ /// HybridAppStartTimeModuleSpecSwift.hpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #pragma once @@ -47,9 +47,18 @@ namespace margelo::nitro::utils { inline size_t getExternalMemorySize() noexcept override { return _swiftPart.getMemorySize(); } + bool equals(const std::shared_ptr& other) override { + if (auto otherCast = std::dynamic_pointer_cast(other)) { + return _swiftPart.equals(otherCast->_swiftPart); + } + return false; + } void dispose() noexcept override { _swiftPart.dispose(); } + std::string toString() override { + return _swiftPart.toString(); + } public: // Properties @@ -59,12 +68,7 @@ namespace margelo::nitro::utils { public: // Methods - inline void recordAppStartTime() override { - auto __result = _swiftPart.recordAppStartTime(); - if (__result.hasError()) [[unlikely]] { - std::rethrow_exception(__result.error()); - } - } + private: ExpensifyNitroUtils::HybridAppStartTimeModuleSpec_cxx _swiftPart; diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.cpp b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.cpp index 4ed87fe30a92..c112a079efb9 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.cpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.cpp @@ -2,7 +2,7 @@ /// HybridContactsModuleSpecSwift.cpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #include "HybridContactsModuleSpecSwift.hpp" diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.hpp b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.hpp index 0ec5342eccec..17fb6c17149a 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.hpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.hpp @@ -2,7 +2,7 @@ /// HybridContactsModuleSpecSwift.hpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #pragma once @@ -58,9 +58,18 @@ namespace margelo::nitro::utils { inline size_t getExternalMemorySize() noexcept override { return _swiftPart.getMemorySize(); } + bool equals(const std::shared_ptr& other) override { + if (auto otherCast = std::dynamic_pointer_cast(other)) { + return _swiftPart.equals(otherCast->_swiftPart); + } + return false; + } void dispose() noexcept override { _swiftPart.dispose(); } + std::string toString() override { + return _swiftPart.toString(); + } public: // Properties diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/Contact.swift b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/Contact.swift index bc66c3e46d2b..2e6df64a0eaa 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/Contact.swift +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/Contact.swift @@ -2,7 +2,7 @@ /// Contact.swift /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// import NitroModules @@ -64,135 +64,63 @@ public extension Contact { }()) } + @inline(__always) var firstName: String? { - @inline(__always) - get { - return { () -> String? in - if bridge.has_value_std__optional_std__string_(self.__firstName) { - let __unwrapped = bridge.get_std__optional_std__string_(self.__firstName) - return String(__unwrapped) - } else { - return nil - } - }() - } - @inline(__always) - set { - self.__firstName = { () -> bridge.std__optional_std__string_ in - if let __unwrappedValue = newValue { - return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) - } else { - return .init() - } - }() - } + return { () -> String? in + if bridge.has_value_std__optional_std__string_(self.__firstName) { + let __unwrapped = bridge.get_std__optional_std__string_(self.__firstName) + return String(__unwrapped) + } else { + return nil + } + }() } + @inline(__always) var lastName: String? { - @inline(__always) - get { - return { () -> String? in - if bridge.has_value_std__optional_std__string_(self.__lastName) { - let __unwrapped = bridge.get_std__optional_std__string_(self.__lastName) - return String(__unwrapped) - } else { - return nil - } - }() - } - @inline(__always) - set { - self.__lastName = { () -> bridge.std__optional_std__string_ in - if let __unwrappedValue = newValue { - return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) - } else { - return .init() - } - }() - } + return { () -> String? in + if bridge.has_value_std__optional_std__string_(self.__lastName) { + let __unwrapped = bridge.get_std__optional_std__string_(self.__lastName) + return String(__unwrapped) + } else { + return nil + } + }() } + @inline(__always) var phoneNumbers: [StringHolder]? { - @inline(__always) - get { - return { () -> [StringHolder]? in - if bridge.has_value_std__optional_std__vector_StringHolder__(self.__phoneNumbers) { - let __unwrapped = bridge.get_std__optional_std__vector_StringHolder__(self.__phoneNumbers) - return __unwrapped.map({ __item in __item }) - } else { - return nil - } - }() - } - @inline(__always) - set { - self.__phoneNumbers = { () -> bridge.std__optional_std__vector_StringHolder__ in - if let __unwrappedValue = newValue { - return bridge.create_std__optional_std__vector_StringHolder__({ () -> bridge.std__vector_StringHolder_ in - var __vector = bridge.create_std__vector_StringHolder_(__unwrappedValue.count) - for __item in __unwrappedValue { - __vector.push_back(__item) - } - return __vector - }()) - } else { - return .init() - } - }() - } + return { () -> [StringHolder]? in + if bridge.has_value_std__optional_std__vector_StringHolder__(self.__phoneNumbers) { + let __unwrapped = bridge.get_std__optional_std__vector_StringHolder__(self.__phoneNumbers) + return __unwrapped.map({ __item in __item }) + } else { + return nil + } + }() } + @inline(__always) var emailAddresses: [StringHolder]? { - @inline(__always) - get { - return { () -> [StringHolder]? in - if bridge.has_value_std__optional_std__vector_StringHolder__(self.__emailAddresses) { - let __unwrapped = bridge.get_std__optional_std__vector_StringHolder__(self.__emailAddresses) - return __unwrapped.map({ __item in __item }) - } else { - return nil - } - }() - } - @inline(__always) - set { - self.__emailAddresses = { () -> bridge.std__optional_std__vector_StringHolder__ in - if let __unwrappedValue = newValue { - return bridge.create_std__optional_std__vector_StringHolder__({ () -> bridge.std__vector_StringHolder_ in - var __vector = bridge.create_std__vector_StringHolder_(__unwrappedValue.count) - for __item in __unwrappedValue { - __vector.push_back(__item) - } - return __vector - }()) - } else { - return .init() - } - }() - } + return { () -> [StringHolder]? in + if bridge.has_value_std__optional_std__vector_StringHolder__(self.__emailAddresses) { + let __unwrapped = bridge.get_std__optional_std__vector_StringHolder__(self.__emailAddresses) + return __unwrapped.map({ __item in __item }) + } else { + return nil + } + }() } + @inline(__always) var imageData: String? { - @inline(__always) - get { - return { () -> String? in - if bridge.has_value_std__optional_std__string_(self.__imageData) { - let __unwrapped = bridge.get_std__optional_std__string_(self.__imageData) - return String(__unwrapped) - } else { - return nil - } - }() - } - @inline(__always) - set { - self.__imageData = { () -> bridge.std__optional_std__string_ in - if let __unwrappedValue = newValue { - return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) - } else { - return .init() - } - }() - } + return { () -> String? in + if bridge.has_value_std__optional_std__string_(self.__imageData) { + let __unwrapped = bridge.get_std__optional_std__string_(self.__imageData) + return String(__unwrapped) + } else { + return nil + } + }() } } diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/ContactFields.swift b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/ContactFields.swift index 9f464ce24e0a..1f7f694816da 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/ContactFields.swift +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/ContactFields.swift @@ -2,7 +2,7 @@ /// ContactFields.swift /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// /** diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift index a2ddc405ac6d..17206995f1f6 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift @@ -2,12 +2,11 @@ /// Func_void_std__exception_ptr.swift /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// import NitroModules - /** * Wraps a Swift `(_ error: Error) -> Void` as a class. * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/Func_void_std__vector_Contact_.swift b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/Func_void_std__vector_Contact_.swift index 1dd498ba1629..48f3bb192548 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/Func_void_std__vector_Contact_.swift +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/Func_void_std__vector_Contact_.swift @@ -2,12 +2,11 @@ /// Func_void_std__vector_Contact_.swift /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// import NitroModules - /** * Wraps a Swift `(_ value: [Contact]) -> Void` as a class. * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridAppStartTimeModuleSpec.swift b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridAppStartTimeModuleSpec.swift index e3b9e50858fb..97370c141ba0 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridAppStartTimeModuleSpec.swift +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridAppStartTimeModuleSpec.swift @@ -2,10 +2,9 @@ /// HybridAppStartTimeModuleSpec.swift /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// -import Foundation import NitroModules /// See ``HybridAppStartTimeModuleSpec`` @@ -14,7 +13,14 @@ public protocol HybridAppStartTimeModuleSpec_protocol: HybridObject { var appStartTime: Double { get } // Methods - func recordAppStartTime() throws -> Void + +} + +public extension HybridAppStartTimeModuleSpec_protocol { + /// Default implementation of ``HybridObject.toString`` + func toString() -> String { + return "[HybridObject AppStartTimeModule]" + } } /// See ``HybridAppStartTimeModuleSpec`` @@ -23,14 +29,14 @@ open class HybridAppStartTimeModuleSpec_base { public init() { } public func getCxxWrapper() -> HybridAppStartTimeModuleSpec_cxx { #if DEBUG - guard self is HybridAppStartTimeModuleSpec else { + guard self is any HybridAppStartTimeModuleSpec else { fatalError("`self` is not a `HybridAppStartTimeModuleSpec`! Did you accidentally inherit from `HybridAppStartTimeModuleSpec_base` instead of `HybridAppStartTimeModuleSpec`?") } #endif if let cxxWrapper = self.cxxWrapper { return cxxWrapper } else { - let cxxWrapper = HybridAppStartTimeModuleSpec_cxx(self as! HybridAppStartTimeModuleSpec) + let cxxWrapper = HybridAppStartTimeModuleSpec_cxx(self as! any HybridAppStartTimeModuleSpec) self.cxxWrapper = cxxWrapper return cxxWrapper } diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridAppStartTimeModuleSpec_cxx.swift b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridAppStartTimeModuleSpec_cxx.swift index 3d54beba719d..933ed8c89bee 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridAppStartTimeModuleSpec_cxx.swift +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridAppStartTimeModuleSpec_cxx.swift @@ -2,10 +2,9 @@ /// HybridAppStartTimeModuleSpec_cxx.swift /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// -import Foundation import NitroModules /** @@ -76,7 +75,7 @@ open class HybridAppStartTimeModuleSpec_cxx { */ public func getCxxPart() -> bridge.std__shared_ptr_HybridAppStartTimeModuleSpec_ { let cachedCxxPart = self.__cxxPart.lock() - if cachedCxxPart.__convertToBool() { + if Bool(fromCxx: cachedCxxPart) { return cachedCxxPart } else { let newCxxPart = bridge.create_std__shared_ptr_HybridAppStartTimeModuleSpec_(self.toUnsafe()) @@ -96,6 +95,14 @@ open class HybridAppStartTimeModuleSpec_cxx { return MemoryHelper.getSizeOf(self.__implementation) + self.__implementation.memorySize } + /** + * Compares this object with the given [other] object for reference equality. + */ + @inline(__always) + public func equals(other: HybridAppStartTimeModuleSpec_cxx) -> Bool { + return self.__implementation === other.__implementation + } + /** * Call dispose() on the Swift class. * This _may_ be called manually from JS. @@ -105,6 +112,14 @@ open class HybridAppStartTimeModuleSpec_cxx { self.__implementation.dispose() } + /** + * Call toString() on the Swift class. + */ + @inline(__always) + public func toString() -> String { + return self.__implementation.toString() + } + // Properties public final var appStartTime: Double { @inline(__always) @@ -114,14 +129,5 @@ open class HybridAppStartTimeModuleSpec_cxx { } // Methods - @inline(__always) - public final func recordAppStartTime() -> bridge.Result_void_ { - do { - try self.__implementation.recordAppStartTime() - return bridge.create_Result_void_() - } catch (let __error) { - let __exceptionPtr = __error.toCpp() - return bridge.create_Result_void_(__exceptionPtr) - } - } + } diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridContactsModuleSpec.swift b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridContactsModuleSpec.swift index 3456cb65982b..cd6b32d83873 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridContactsModuleSpec.swift +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridContactsModuleSpec.swift @@ -2,10 +2,9 @@ /// HybridContactsModuleSpec.swift /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// -import Foundation import NitroModules /// See ``HybridContactsModuleSpec`` @@ -17,20 +16,27 @@ public protocol HybridContactsModuleSpec_protocol: HybridObject { func getAll(keys: [ContactFields]) throws -> Promise<[Contact]> } +public extension HybridContactsModuleSpec_protocol { + /// Default implementation of ``HybridObject.toString`` + func toString() -> String { + return "[HybridObject ContactsModule]" + } +} + /// See ``HybridContactsModuleSpec`` open class HybridContactsModuleSpec_base { private weak var cxxWrapper: HybridContactsModuleSpec_cxx? = nil public init() { } public func getCxxWrapper() -> HybridContactsModuleSpec_cxx { #if DEBUG - guard self is HybridContactsModuleSpec else { + guard self is any HybridContactsModuleSpec else { fatalError("`self` is not a `HybridContactsModuleSpec`! Did you accidentally inherit from `HybridContactsModuleSpec_base` instead of `HybridContactsModuleSpec`?") } #endif if let cxxWrapper = self.cxxWrapper { return cxxWrapper } else { - let cxxWrapper = HybridContactsModuleSpec_cxx(self as! HybridContactsModuleSpec) + let cxxWrapper = HybridContactsModuleSpec_cxx(self as! any HybridContactsModuleSpec) self.cxxWrapper = cxxWrapper return cxxWrapper } diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridContactsModuleSpec_cxx.swift b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridContactsModuleSpec_cxx.swift index 80081c8acb78..e8c768395282 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridContactsModuleSpec_cxx.swift +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridContactsModuleSpec_cxx.swift @@ -2,10 +2,9 @@ /// HybridContactsModuleSpec_cxx.swift /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// -import Foundation import NitroModules /** @@ -76,7 +75,7 @@ open class HybridContactsModuleSpec_cxx { */ public func getCxxPart() -> bridge.std__shared_ptr_HybridContactsModuleSpec_ { let cachedCxxPart = self.__cxxPart.lock() - if cachedCxxPart.__convertToBool() { + if Bool(fromCxx: cachedCxxPart) { return cachedCxxPart } else { let newCxxPart = bridge.create_std__shared_ptr_HybridContactsModuleSpec_(self.toUnsafe()) @@ -96,6 +95,14 @@ open class HybridContactsModuleSpec_cxx { return MemoryHelper.getSizeOf(self.__implementation) + self.__implementation.memorySize } + /** + * Compares this object with the given [other] object for reference equality. + */ + @inline(__always) + public func equals(other: HybridContactsModuleSpec_cxx) -> Bool { + return self.__implementation === other.__implementation + } + /** * Call dispose() on the Swift class. * This _may_ be called manually from JS. @@ -105,6 +112,14 @@ open class HybridContactsModuleSpec_cxx { self.__implementation.dispose() } + /** + * Call toString() on the Swift class. + */ + @inline(__always) + public func toString() -> String { + return self.__implementation.toString() + } + // Properties diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/StringHolder.swift b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/StringHolder.swift index 5f9c0bd1b5b1..f1ccaabdc507 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/StringHolder.swift +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/StringHolder.swift @@ -2,7 +2,7 @@ /// StringHolder.swift /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// import NitroModules @@ -22,14 +22,8 @@ public extension StringHolder { self.init(std.string(value)) } + @inline(__always) var value: String { - @inline(__always) - get { - return String(self.__value) - } - @inline(__always) - set { - self.__value = std.string(newValue) - } + return String(self.__value) } } diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/Contact.hpp b/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/Contact.hpp index c30eb56e1820..cda9afb3f5f3 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/Contact.hpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/Contact.hpp @@ -2,7 +2,7 @@ /// Contact.hpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #pragma once @@ -17,6 +17,16 @@ #else #error NitroModules cannot be found! Are you sure you installed NitroModules properly? #endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif // Forward declaration of `StringHolder` to properly resolve imports. namespace margelo::nitro::utils { struct StringHolder; } @@ -31,7 +41,7 @@ namespace margelo::nitro::utils { /** * A struct which can be represented as a JavaScript object (Contact). */ - struct Contact { + struct Contact final { public: std::optional firstName SWIFT_PRIVATE; std::optional lastName SWIFT_PRIVATE; @@ -42,6 +52,9 @@ namespace margelo::nitro::utils { public: Contact() = default; explicit Contact(std::optional firstName, std::optional lastName, std::optional> phoneNumbers, std::optional> emailAddresses, std::optional imageData): firstName(firstName), lastName(lastName), phoneNumbers(phoneNumbers), emailAddresses(emailAddresses), imageData(imageData) {} + + public: + friend bool operator==(const Contact& lhs, const Contact& rhs) = default; }; } // namespace margelo::nitro::utils @@ -54,20 +67,20 @@ namespace margelo::nitro { static inline margelo::nitro::utils::Contact fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { jsi::Object obj = arg.asObject(runtime); return margelo::nitro::utils::Contact( - JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "firstName")), - JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "lastName")), - JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, "phoneNumbers")), - JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, "emailAddresses")), - JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "imageData")) + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "firstName"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "lastName"))), + JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "phoneNumbers"))), + JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "emailAddresses"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "imageData"))) ); } static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::utils::Contact& arg) { jsi::Object obj(runtime); - obj.setProperty(runtime, "firstName", JSIConverter>::toJSI(runtime, arg.firstName)); - obj.setProperty(runtime, "lastName", JSIConverter>::toJSI(runtime, arg.lastName)); - obj.setProperty(runtime, "phoneNumbers", JSIConverter>>::toJSI(runtime, arg.phoneNumbers)); - obj.setProperty(runtime, "emailAddresses", JSIConverter>>::toJSI(runtime, arg.emailAddresses)); - obj.setProperty(runtime, "imageData", JSIConverter>::toJSI(runtime, arg.imageData)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "firstName"), JSIConverter>::toJSI(runtime, arg.firstName)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "lastName"), JSIConverter>::toJSI(runtime, arg.lastName)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "phoneNumbers"), JSIConverter>>::toJSI(runtime, arg.phoneNumbers)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "emailAddresses"), JSIConverter>>::toJSI(runtime, arg.emailAddresses)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "imageData"), JSIConverter>::toJSI(runtime, arg.imageData)); return obj; } static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { @@ -75,11 +88,14 @@ namespace margelo::nitro { return false; } jsi::Object obj = value.getObject(runtime); - if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "firstName"))) return false; - if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "lastName"))) return false; - if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, "phoneNumbers"))) return false; - if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, "emailAddresses"))) return false; - if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "imageData"))) return false; + if (!nitro::isPlainObject(runtime, obj)) { + return false; + } + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "firstName")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "lastName")))) return false; + if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "phoneNumbers")))) return false; + if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "emailAddresses")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "imageData")))) return false; return true; } }; diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/ContactFields.hpp b/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/ContactFields.hpp index df13271b6919..562bed803574 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/ContactFields.hpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/ContactFields.hpp @@ -2,7 +2,7 @@ /// ContactFields.hpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #pragma once diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridAppStartTimeModuleSpec.cpp b/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridAppStartTimeModuleSpec.cpp index 90c53a80a53d..a3a70baab6b5 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridAppStartTimeModuleSpec.cpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridAppStartTimeModuleSpec.cpp @@ -2,7 +2,7 @@ /// HybridAppStartTimeModuleSpec.cpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #include "HybridAppStartTimeModuleSpec.hpp" @@ -15,7 +15,6 @@ namespace margelo::nitro::utils { // load custom methods/properties registerHybrids(this, [](Prototype& prototype) { prototype.registerHybridGetter("appStartTime", &HybridAppStartTimeModuleSpec::getAppStartTime); - prototype.registerHybridMethod("recordAppStartTime", &HybridAppStartTimeModuleSpec::recordAppStartTime); }); } diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridAppStartTimeModuleSpec.hpp b/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridAppStartTimeModuleSpec.hpp index 75b174de7a6b..21188ee4dbdf 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridAppStartTimeModuleSpec.hpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridAppStartTimeModuleSpec.hpp @@ -2,7 +2,7 @@ /// HybridAppStartTimeModuleSpec.hpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #pragma once @@ -48,7 +48,7 @@ namespace margelo::nitro::utils { public: // Methods - virtual void recordAppStartTime() = 0; + protected: // Hybrid Setup diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridContactsModuleSpec.cpp b/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridContactsModuleSpec.cpp index 3f609e5bbcb2..dec0a2adc457 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridContactsModuleSpec.cpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridContactsModuleSpec.cpp @@ -2,7 +2,7 @@ /// HybridContactsModuleSpec.cpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #include "HybridContactsModuleSpec.hpp" diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridContactsModuleSpec.hpp b/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridContactsModuleSpec.hpp index c9ffa5ed3cca..cbb6831c685c 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridContactsModuleSpec.hpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridContactsModuleSpec.hpp @@ -2,7 +2,7 @@ /// HybridContactsModuleSpec.hpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #pragma once diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/StringHolder.hpp b/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/StringHolder.hpp index 1f400597c645..506185c82184 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/StringHolder.hpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/StringHolder.hpp @@ -2,7 +2,7 @@ /// StringHolder.hpp /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro -/// Copyright © 2026 Marc Rousavy @ Margelo +/// Copyright © Marc Rousavy @ Margelo /// #pragma once @@ -17,6 +17,16 @@ #else #error NitroModules cannot be found! Are you sure you installed NitroModules properly? #endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif @@ -27,13 +37,16 @@ namespace margelo::nitro::utils { /** * A struct which can be represented as a JavaScript object (StringHolder). */ - struct StringHolder { + struct StringHolder final { public: std::string value SWIFT_PRIVATE; public: StringHolder() = default; explicit StringHolder(std::string value): value(value) {} + + public: + friend bool operator==(const StringHolder& lhs, const StringHolder& rhs) = default; }; } // namespace margelo::nitro::utils @@ -46,12 +59,12 @@ namespace margelo::nitro { static inline margelo::nitro::utils::StringHolder fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { jsi::Object obj = arg.asObject(runtime); return margelo::nitro::utils::StringHolder( - JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "value")) + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "value"))) ); } static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::utils::StringHolder& arg) { jsi::Object obj(runtime); - obj.setProperty(runtime, "value", JSIConverter::toJSI(runtime, arg.value)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "value"), JSIConverter::toJSI(runtime, arg.value)); return obj; } static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { @@ -59,7 +72,10 @@ namespace margelo::nitro { return false; } jsi::Object obj = value.getObject(runtime); - if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "value"))) return false; + if (!nitro::isPlainObject(runtime, obj)) { + return false; + } + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "value")))) return false; return true; } }; From f78b3392055525d5d53ff2fe3d964e11f4ce6a92 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sun, 8 Mar 2026 23:28:45 +0000 Subject: [PATCH 03/14] chore: update `react-native-onyx` to latest commit in Onyx PR Onyx PR: https://github.com/Expensify/react-native-onyx/pull/749 --- package-lock.json | 46 ++++++++++++++++++++++++++++++++++++++++------ package.json | 2 +- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 69267611b6ba..b16758c11c68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -115,9 +115,9 @@ "react-native-keyboard-controller": "1.21.0-beta.1", "react-native-launch-arguments": "^4.1.0", "react-native-localize": "^3.5.4", - "react-native-onyx": "3.0.42", "react-native-nitro-modules": "0.35.0", "react-native-nitro-sqlite": "9.6.0", + "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#0e1750d7e955257c076d59d9dd1babb08f624ecf", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", @@ -18016,6 +18016,12 @@ "version": "2.0.6", "license": "MIT" }, + "node_modules/ascii-table": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/ascii-table/-/ascii-table-0.0.9.tgz", + "integrity": "sha512-xpkr6sCDIYTPqzvjG8M3ncw1YOTaloWZOyrUmicoEifBEKzQzt+ooUpRpQ/AbOoJfO/p2ZKiyp79qHThzJDulQ==", + "license": "MIT" + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -30436,10 +30442,23 @@ "version": "4.17.21", "license": "MIT" }, + "node_modules/lodash.bindall": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.bindall/-/lodash.bindall-4.4.0.tgz", + "integrity": "sha512-NQ+QvFohS2gPbWpyLfyuiF0ZQA3TTaJ+n0XDID5jwtMZBKE32gN5vSyy7xBVsqvJkvT/UY9dvHXIk9tZmBVF3g==", + "license": "MIT" + }, "node_modules/lodash.camelcase": { "version": "4.3.0", "license": "MIT" }, + "node_modules/lodash.clone": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", + "integrity": "sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg==", + "deprecated": "This package is deprecated. Use structuredClone instead.", + "license": "MIT" + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -30466,10 +30485,23 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==", + "deprecated": "This package is deprecated. Use destructuring assignment syntax instead.", + "license": "MIT" + }, "node_modules/lodash.throttle": { "version": "4.1.1", "license": "MIT" }, + "node_modules/lodash.transform": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.transform/-/lodash.transform-4.6.0.tgz", + "integrity": "sha512-LO37ZnhmBVx0GvOU/caQuipEh4GN82TcWv3yHlebGDgOxbxiwwzW5Pcx2AcvpIv2WmvmSMoC492yQFNhy/l/UQ==", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "devOptional": true, @@ -34250,9 +34282,9 @@ } }, "node_modules/react-native-onyx": { - "version": "3.0.42", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-3.0.42.tgz", - "integrity": "sha512-+JM4+2s3JNd6PIPuM/sWBZgKKeS7/LXav90rNq7OwizpbOBRvqEcnYIouKOWLbsYm2996zOvXUGcSiuxSUbXYw==", + "version": "3.0.45", + "resolved": "git+ssh://git@github.com/Expensify/react-native-onyx.git#0e1750d7e955257c076d59d9dd1babb08f624ecf", + "integrity": "sha512-Uai2V90F59zIy8Hco83zCDARDLZhmhejjf6RxXryXnHVVcpswDpSVc83fc2plllXiFATfvWppYLfzjQcZczGow==", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", @@ -34272,8 +34304,8 @@ "react": ">=18.1.0", "react-native": ">=0.75.0", "react-native-device-info": "^10.3.0", - "react-native-nitro-modules": ">=0.27.2", - "react-native-nitro-sqlite": "^9.2.0", + "react-native-nitro-modules": ">=0.35.0", + "react-native-nitro-sqlite": "^9.6.0", "react-native-performance": ">=5.1.0" }, "peerDependenciesMeta": { @@ -34296,6 +34328,8 @@ }, "node_modules/react-native-onyx/node_modules/fast-equals": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", + "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==", "license": "MIT" }, "node_modules/react-native-pager-view": { diff --git a/package.json b/package.json index 5bdb4bf83348..9ce7bcdcbb11 100644 --- a/package.json +++ b/package.json @@ -179,9 +179,9 @@ "react-native-keyboard-controller": "1.21.0-beta.1", "react-native-launch-arguments": "^4.1.0", "react-native-localize": "^3.5.4", - "react-native-onyx": "3.0.42", "react-native-nitro-modules": "0.35.0", "react-native-nitro-sqlite": "9.6.0", + "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#0e1750d7e955257c076d59d9dd1babb08f624ecf", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", From 871360073bda42ef1286e908dbcacdf902263c23 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sun, 8 Mar 2026 23:46:39 +0000 Subject: [PATCH 04/14] fix: remove `override` keyword from `HybridAppStartTimeModule` --- .../java/com/margelo/nitro/utils/HybridAppStartTimeModule.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/ExpensifyNitroUtils/android/src/main/java/com/margelo/nitro/utils/HybridAppStartTimeModule.kt b/modules/ExpensifyNitroUtils/android/src/main/java/com/margelo/nitro/utils/HybridAppStartTimeModule.kt index ddde08906f00..5ba9df67cdd5 100644 --- a/modules/ExpensifyNitroUtils/android/src/main/java/com/margelo/nitro/utils/HybridAppStartTimeModule.kt +++ b/modules/ExpensifyNitroUtils/android/src/main/java/com/margelo/nitro/utils/HybridAppStartTimeModule.kt @@ -2,14 +2,15 @@ package com.margelo.nitro.utils import android.content.Context import com.margelo.nitro.NitroModules +import androidx.core.content.edit class HybridAppStartTimeModule : HybridAppStartTimeModuleSpec() { override val memorySize: Long = 16L - override fun recordAppStartTime() { + fun recordAppStartTime() { val context = NitroModules.applicationContext ?: return val sharedPreferences = context.getSharedPreferences("AppStartTime", Context.MODE_PRIVATE) - sharedPreferences.edit().putLong("AppStartTime", System.currentTimeMillis()).apply() + sharedPreferences.edit { putLong("AppStartTime", System.currentTimeMillis()) } } override val appStartTime: Double From 434118be32d9731cb82558308fb1bf70b7750dd3 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sun, 8 Mar 2026 23:57:06 +0000 Subject: [PATCH 05/14] Update Podfile.lock --- ios/Podfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 750a9147bc80..14378879602c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -379,7 +379,7 @@ PODS: - nanopb/encode (= 3.30910.0) - nanopb/decode (3.30910.0) - nanopb/encode (3.30910.0) - - NitroModules (0.29.4): + - NitroModules (0.35.0): - boost - DoubleConversion - fast_float @@ -3559,7 +3559,7 @@ PODS: - SocketRocket - Turf - Yoga - - RNNitroSQLite (9.2.0): + - RNNitroSQLite (9.6.0): - boost - DoubleConversion - fast_float @@ -4558,7 +4558,7 @@ SPEC CHECKSUMS: MapboxMaps: f87023cf0d72b180b40ea0b6fb4b2d7db6b73b71 MapboxMobileEvents: d044b9edbe0ec7df60f6c2c9634fe9a7f449266b nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - NitroModules: 8bffd214aa360baba0f2c5718fe0acff5ef94763 + NitroModules: f8c2cc3025e4550aee15ff77c525622bf98e774a NWWebSocket: b4741420f1976e1dff4da3edad00c401e4f1d769 Onfido: 65454f91d10758193c857fd149417f6efbea84c5 onfido-react-native-sdk: bb8cfd9198e2e97978461d969497d18b37e45ca7 @@ -4664,7 +4664,7 @@ SPEC CHECKSUMS: RNLiveMarkdown: 60621617bc0504ac39669e3a8d1e9cadf1ada34f RNLocalize: 05e367a873223683f0e268d0af9a8a8e6aed3b26 rnmapbox-maps: 392ac61c42a9ff01a51d4c2f6775d9131b5951fb - RNNitroSQLite: fb251387cfbee73b100cd484a3c886fda681b3b5 + RNNitroSQLite: a9b5965d511ed6e99ce903380e64934d043a0d2c RNPermissions: 518f0a0c439acc74e2b9937e0e7d29e5031ae949 RNReactNativeHapticFeedback: 5f1542065f0b24c9252bd8cf3e83bc9c548182e4 RNReanimated: e79d7f42b76ba026e7dc5fb3e3f81991c590d3af From f335703ac848e7721d40485b1d34eaa860f8bced Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 20 Mar 2026 12:30:31 +0100 Subject: [PATCH 06/14] chore: update onyx commit --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index bd76f4089bc1..4a6734837a21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -117,7 +117,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-modules": "0.35.0", "react-native-nitro-sqlite": "9.6.0", - "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#674b1f2db915059aac6c9e1f2d9babd19f42064a", + "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#5d957b684f41f616117ba7a64d8580a0ff5d3905", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", @@ -34754,7 +34754,7 @@ }, "node_modules/react-native-onyx": { "version": "3.0.48", - "resolved": "git+ssh://git@github.com/Expensify/react-native-onyx.git#674b1f2db915059aac6c9e1f2d9babd19f42064a", + "resolved": "git+ssh://git@github.com/Expensify/react-native-onyx.git#5d957b684f41f616117ba7a64d8580a0ff5d3905", "integrity": "sha512-D+brPV/dh9vUALImk2sQJBwOKk9G+oqTZlPg7dkpOBIFaekKfGVYQWKCmaVfdyh3wXRq8oKCQgNy9Z1ajpns6Q==", "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 05c9f59c3509..afdda195c855 100644 --- a/package.json +++ b/package.json @@ -181,7 +181,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-modules": "0.35.0", "react-native-nitro-sqlite": "9.6.0", - "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#674b1f2db915059aac6c9e1f2d9babd19f42064a", + "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#674b1f2db915059aac6c9e1f2d9babd19f42064agra", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", From 5d5edfff6291753ccec893a2dff2019de86a52c6 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 23 Mar 2026 13:34:47 +0100 Subject: [PATCH 07/14] Update package-lock.json --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f688464ebfd8..028f41235780 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34753,9 +34753,9 @@ } }, "node_modules/react-native-onyx": { - "version": "3.0.48", - "resolved": "git+ssh://git@github.com/Expensify/react-native-onyx.git#5d957b684f41f616117ba7a64d8580a0ff5d3905", - "integrity": "sha512-D+brPV/dh9vUALImk2sQJBwOKk9G+oqTZlPg7dkpOBIFaekKfGVYQWKCmaVfdyh3wXRq8oKCQgNy9Z1ajpns6Q==", + "version": "3.0.50", + "resolved": "git+ssh://git@github.com/Expensify/react-native-onyx.git#5e44d80f5fc5dbde4e9ae8f100e8fb05b09301d3", + "integrity": "sha512-Hj5JbZOmUCLAsHR3ROx0YXUfTXzEFhW1DRXfMU0399TWeiyYChHeBQWC0wAohcd5PRLWtA/zNDfKyA8FsyynLA==", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", From 7613a9f2426d530175c0390ee9793af5a5bd67e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 31 Mar 2026 14:52:51 +0100 Subject: [PATCH 08/14] Remove cache eviction references --- contributingGuides/STYLE.md | 4 +--- .../philosophies/ONYX-DATA-MANAGEMENT.md | 12 +----------- src/components/ArchivedReportFooter.tsx | 2 +- .../Attachments/AttachmentCarousel/index.tsx | 4 ++-- src/components/AvatarWithDisplayName.tsx | 2 +- src/components/MoneyRequestHeaderPrimaryAction.tsx | 2 +- .../MoneyRequestHeaderSecondaryActions.tsx | 2 +- .../ReportActionItem/MoneyRequestReceiptView.tsx | 4 +--- src/components/ReportActionItem/MoneyRequestView.tsx | 4 +--- src/components/Search/index.tsx | 1 - src/hooks/usePaginatedReportActions.ts | 1 - src/hooks/useParentReportAction.ts | 4 ---- src/hooks/useSearchSections.ts | 1 - src/hooks/useShowNotFoundPageInIOUStep.ts | 1 - src/pages/Debug/Report/DebugReportActions.tsx | 1 - .../Debug/ReportAction/DebugReportActionPage.tsx | 1 - .../ContextMenu/BaseReportActionContextMenu.tsx | 2 -- .../ReportActionCompose/ReportActionCompose.tsx | 4 +--- .../report/withReportAndReportActionOrNotFound.tsx | 2 +- .../report/ReportAddAttachmentModalContent/index.tsx | 4 +--- .../routes/report/ReportAttachmentModalContent.tsx | 4 +--- src/setup/index.ts | 2 -- 22 files changed, 14 insertions(+), 50 deletions(-) diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md index 98e59ab86a76..103da4c15d6a 100644 --- a/contributingGuides/STYLE.md +++ b/contributingGuides/STYLE.md @@ -571,9 +571,7 @@ When you change the function id argument type to allow `undefined`, check if it ```diff function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = false, updatedTransaction, isFromReviewDuplicates = false}: MoneyRequestViewProps) { - const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, { - canEvict: false, - }); + const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`); - const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '-1']; + const parentReportAction = parentReportActions?.[report?.parentReportActionID]; ``` diff --git a/contributingGuides/philosophies/ONYX-DATA-MANAGEMENT.md b/contributingGuides/philosophies/ONYX-DATA-MANAGEMENT.md index 24131557c591..6bc1f3819864 100644 --- a/contributingGuides/philosophies/ONYX-DATA-MANAGEMENT.md +++ b/contributingGuides/philosophies/ONYX-DATA-MANAGEMENT.md @@ -35,17 +35,7 @@ Different platforms come with varying storage capacities and Onyx has a way to g **To flag a key as safe for removal:** - Add the key to the `evictableKeys` option in `Onyx.init(options)` -- Implement `canEvict` in the Onyx config for each component subscribing to a key -- The key will only be deleted when all subscribers return `true` for `canEvict` - -Example: -```js -Onyx.init({ - evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS], -}); - -const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {canEvict: !isActiveReport}); -``` +- A least recently accessed key will only be deleted when an Onyx operation retries after failing. ## Onyx Derived Values diff --git a/src/components/ArchivedReportFooter.tsx b/src/components/ArchivedReportFooter.tsx index 6f7456ec004e..d931f2fe27a1 100644 --- a/src/components/ArchivedReportFooter.tsx +++ b/src/components/ArchivedReportFooter.tsx @@ -26,7 +26,7 @@ function ArchivedReportFooter({reportID}: ArchivedReportFooterProps) { const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); const [personalDetails = getEmptyObject()] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); - const [reportClosedAction] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {canEvict: false, selector: getLastClosedReportAction}); + const [reportClosedAction] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {selector: getLastClosedReportAction}); const originalMessage = isClosedAction(reportClosedAction) ? getOriginalMessage(reportClosedAction) : null; const archiveReason = originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT; const actorPersonalDetails = personalDetails?.[reportClosedAction?.actorAccountID ?? CONST.DEFAULT_NUMBER_ID]; diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index 8fb7d3e300b2..9a71f9b708fb 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -27,8 +27,8 @@ function AttachmentCarousel({ attachmentLink, onAttachmentError, }: AttachmentCarouselProps) { - const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, {canEvict: false}); - const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, {canEvict: false}); + const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, {}); + const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, {}); const canUseTouchScreen = canUseTouchScreenUtil(); const styles = useThemeStyles(); const isReportArchived = useReportIsArchived(report.reportID); diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index 1646f458b584..068da83c1487 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -186,7 +186,7 @@ function AvatarWithDisplayName({ }: AvatarWithDisplayNameProps) { const {localeCompare, formatPhoneNumber} = useLocalize(); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); - const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`, {canEvict: false}); + const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`, {}); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST) ?? CONST.EMPTY_OBJECT; const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const theme = useTheme(); diff --git a/src/components/MoneyRequestHeaderPrimaryAction.tsx b/src/components/MoneyRequestHeaderPrimaryAction.tsx index ad053abc99cd..381f1d0ad45c 100644 --- a/src/components/MoneyRequestHeaderPrimaryAction.tsx +++ b/src/components/MoneyRequestHeaderPrimaryAction.tsx @@ -56,7 +56,7 @@ function MoneyRequestHeaderPrimaryAction({reportID}: MoneyRequestHeaderPrimaryAc const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`); - const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`, {canEvict: false}); + const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`, {}); const parentReportAction = report?.parentReportActionID ? parentReportActions?.[report.parentReportActionID] : undefined; const transactionIDFromAction = isMoneyRequestAction(parentReportAction) ? (getOriginalMessage(parentReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) diff --git a/src/components/MoneyRequestHeaderSecondaryActions.tsx b/src/components/MoneyRequestHeaderSecondaryActions.tsx index 9e2b0e8cc3f0..29e6b8a6ac5a 100644 --- a/src/components/MoneyRequestHeaderSecondaryActions.tsx +++ b/src/components/MoneyRequestHeaderSecondaryActions.tsx @@ -115,7 +115,7 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`); - const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`, {canEvict: false}); + const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`, {}); const parentReportAction = report?.parentReportActionID ? parentReportActions?.[report.parentReportActionID] : undefined; const transactionIDFromAction = isMoneyRequestAction(parentReportAction) ? (getOriginalMessage(parentReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx index f2678c433a5b..e79e9b66c4c1 100644 --- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx +++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx @@ -128,9 +128,7 @@ function MoneyRequestReceiptView({ const parentReportID = report?.parentReportID; const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(parentReportID)}`); const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(parentReport?.parentReportID)}`); - const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, { - canEvict: false, - }); + const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, {}); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 9516387f0b94..0339a539dfbe 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -191,9 +191,7 @@ function MoneyRequestView({ parentReport = parentReport ?? currentSearchResults?.data[`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`]; const [parentReportNextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${getNonEmptyStringOnyxID(parentReport?.reportID)}`); - const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, { - canEvict: false, - }); + const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, {}); const parentReportAction = transactionThreadReport?.parentReportActionID ? parentReportActions?.[transactionThreadReport.parentReportActionID] : undefined; const isFromMergeTransaction = !!mergeTransactionID; diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index b86bf47b6fa0..c681b50cf48a 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -324,7 +324,6 @@ function Search({ const archivedReportsIdSet = useArchivedReportsIdSet(); const [exportReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, { - canEvict: false, selector: selectFilteredReportActions, }); diff --git a/src/hooks/usePaginatedReportActions.ts b/src/hooks/usePaginatedReportActions.ts index 5a3f78b2e8c6..6f5fa79cd144 100644 --- a/src/hooks/usePaginatedReportActions.ts +++ b/src/hooks/usePaginatedReportActions.ts @@ -35,7 +35,6 @@ function usePaginatedReportActions(reportID: string | undefined, reportActionID? const [sortedAllReportActions] = useOnyx( `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${nonEmptyStringReportID}`, { - canEvict: false, selector: getSortedAllReportActionsSelector, }, [getSortedAllReportActionsSelector], diff --git a/src/hooks/useParentReportAction.ts b/src/hooks/useParentReportAction.ts index 3f512c3219e8..716f5705f18a 100644 --- a/src/hooks/useParentReportAction.ts +++ b/src/hooks/useParentReportAction.ts @@ -17,10 +17,6 @@ function useParentReportAction(report: OnyxEntry) { const [parentReportAction] = useOnyx( `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, { - // Only set canEvict when we have a valid parentReportID — reportActions_ is an evictable key, - // but reportActions_undefined is not and will throw if canEvict is specified. - canEvict: parentReportID ? false : undefined, - selector: getParentReportAction, }, [getParentReportAction], diff --git a/src/hooks/useSearchSections.ts b/src/hooks/useSearchSections.ts index b9f5a5351674..b7fa54da3460 100644 --- a/src/hooks/useSearchSections.ts +++ b/src/hooks/useSearchSections.ts @@ -41,7 +41,6 @@ function useSearchSections(): UseSearchSectionsResult { const isActionLoadingSet = useActionLoadingReportIDs(); const [exportReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, { - canEvict: false, selector: selectFilteredReportActions, }); diff --git a/src/hooks/useShowNotFoundPageInIOUStep.ts b/src/hooks/useShowNotFoundPageInIOUStep.ts index 9f58a84a705c..192fb7c2889b 100644 --- a/src/hooks/useShowNotFoundPageInIOUStep.ts +++ b/src/hooks/useShowNotFoundPageInIOUStep.ts @@ -45,7 +45,6 @@ const useShowNotFoundPageInIOUStep = (action: IOUAction, iouType: IOUType, repor const [reportAction] = useOnyx( `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportActionsReportID}`, { - canEvict: false, selector: getReportActionSelector, }, [getReportActionSelector], diff --git a/src/pages/Debug/Report/DebugReportActions.tsx b/src/pages/Debug/Report/DebugReportActions.tsx index 45a0f70a06a2..9a573c411a5f 100644 --- a/src/pages/Debug/Report/DebugReportActions.tsx +++ b/src/pages/Debug/Report/DebugReportActions.tsx @@ -50,7 +50,6 @@ function DebugReportActions({reportID}: DebugReportActionsProps) { const [sortedAllReportActions] = useOnyx( `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { - canEvict: false, selector: getSortedAllReportActionsSelector, }, [getSortedAllReportActionsSelector], diff --git a/src/pages/Debug/ReportAction/DebugReportActionPage.tsx b/src/pages/Debug/ReportAction/DebugReportActionPage.tsx index d1be4c2669c9..c0cd99c5b57b 100644 --- a/src/pages/Debug/ReportAction/DebugReportActionPage.tsx +++ b/src/pages/Debug/ReportAction/DebugReportActionPage.tsx @@ -45,7 +45,6 @@ function DebugReportActionPage({ const [reportAction] = useOnyx( `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { - canEvict: false, selector: getReportActionSelector, }, [getReportActionSelector], diff --git a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx index 969ff12cdb6b..97035c99bfb9 100755 --- a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -164,11 +164,9 @@ function BaseReportActionContextMenu({ const threeDotRef = useRef(null); const [betas] = useOnyx(ONYXKEYS.BETAS); const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { - canEvict: false, selector: withDEWRoutedActionsObject, }); const [originalReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, { - canEvict: false, selector: withDEWRoutedActionsObject, }); diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx index 7f3600739379..f283435c37bb 100644 --- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx @@ -284,9 +284,7 @@ function ReportActionCompose({reportID}: ReportActionComposeProps) { const isTransactionThreadView = useMemo(() => isReportTransactionThread(report), [report]); const isExpensesReport = useMemo(() => reportTransactions && reportTransactions.length > 1, [reportTransactions]); - const [rawReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`, { - canEvict: false, - }); + const [rawReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`, {}); const iouAction = rawReportActions ? Object.values(rawReportActions).find((action) => isMoneyRequestAction(action)) : null; const linkedTransactionID = iouAction && !isExpensesReport ? getLinkedTransactionID(iouAction) : undefined; diff --git a/src/pages/inbox/report/withReportAndReportActionOrNotFound.tsx b/src/pages/inbox/report/withReportAndReportActionOrNotFound.tsx index 36bc7712ca5c..22bf092c0b95 100644 --- a/src/pages/inbox/report/withReportAndReportActionOrNotFound.tsx +++ b/src/pages/inbox/report/withReportAndReportActionOrNotFound.tsx @@ -45,7 +45,7 @@ export default function Date: Tue, 14 Apr 2026 21:37:04 +0100 Subject: [PATCH 09/14] chore: bump `react-native-onyx` to version 3.0.63 --- package-lock.json | 21 +++++---------------- package.json | 2 +- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 684b49ae489e..75fea15a1c9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -114,7 +114,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-modules": "0.35.0", "react-native-nitro-sqlite": "9.6.0", - "react-native-onyx": "3.0.62", + "react-native-onyx": "3.0.63", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", @@ -34680,9 +34680,9 @@ } }, "node_modules/react-native-onyx": { - "version": "3.0.62", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-3.0.62.tgz", - "integrity": "sha512-ILYKm0/4s5KChKCjbo5xY2arbdkzsCosHcPcGbdCfLNa7nDN2/MxlOA/1+WjnUqyIcFW2n6jgxReCpMaIkAS+Q==", + "version": "3.0.63", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-3.0.63.tgz", + "integrity": "sha512-6UUpjyUh9viXll9M2YomXM9PMg+791foSPA7E0fazgWfrUAmofNwo4xkXT4k3NDdToZFOYxaXL0uw5z00g/MOA==", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", @@ -34703,8 +34703,7 @@ "react-native": ">=0.75.0", "react-native-device-info": "^10.3.0", "react-native-nitro-modules": ">=0.35.0", - "react-native-nitro-sqlite": "^9.6.0", - "react-native-performance": ">=5.1.0" + "react-native-nitro-sqlite": "^9.6.0" }, "peerDependenciesMeta": { "idb-keyval": { @@ -34752,16 +34751,6 @@ "react-native-blob-util": ">=0.13.7" } }, - "node_modules/react-native-performance": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/react-native-performance/-/react-native-performance-6.0.0.tgz", - "integrity": "sha512-Sca75O8jhqXAnNbqvINnrw248Kv9cIwoGxToD8u2uX+BrkAxxXS+YhClEV5L3JdiOpdNCO1MJ5R9bgs2VkNpFg==", - "license": "MIT", - "peer": true, - "peerDependencies": { - "react-native": "*" - } - }, "node_modules/react-native-permissions": { "version": "5.4.0", "license": "MIT", diff --git a/package.json b/package.json index 439c0bc1cd2d..a0e3c9a15d0e 100644 --- a/package.json +++ b/package.json @@ -177,7 +177,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-modules": "0.35.0", "react-native-nitro-sqlite": "9.6.0", - "react-native-onyx": "3.0.62", + "react-native-onyx": "3.0.63", "react-native-pager-view": "8.0.0", "react-native-pdf": "7.0.2", "react-native-permissions": "^5.4.0", From b703a9279eb8b2da9f83a506517aa4730c0043bb Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 14 Apr 2026 21:39:51 +0100 Subject: [PATCH 10/14] chore: update `react-native-onyx` patch based on new version --- ...ative-onyx+3.0.59.patch => react-native-onyx+3.0.63.patch} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename patches/react-native-onyx/{react-native-onyx+3.0.59.patch => react-native-onyx+3.0.63.patch} (96%) diff --git a/patches/react-native-onyx/react-native-onyx+3.0.59.patch b/patches/react-native-onyx/react-native-onyx+3.0.63.patch similarity index 96% rename from patches/react-native-onyx/react-native-onyx+3.0.59.patch rename to patches/react-native-onyx/react-native-onyx+3.0.63.patch index 6c7f9e68c773..7b321b0f51de 100644 --- a/patches/react-native-onyx/react-native-onyx+3.0.59.patch +++ b/patches/react-native-onyx/react-native-onyx+3.0.63.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/react-native-onyx/dist/useOnyx.js b/node_modules/react-native-onyx/dist/useOnyx.js -index 8e0f03a..9e29ace 100644 +index b361d0d..4df16fa 100644 --- a/node_modules/react-native-onyx/dist/useOnyx.js +++ b/node_modules/react-native-onyx/dist/useOnyx.js @@ -97,6 +97,9 @@ function useOnyx(key, options, dependencies = []) { @@ -12,7 +12,7 @@ index 8e0f03a..9e29ace 100644 // Indicates if the hook is connecting to an Onyx key. const isConnectingRef = (0, react_1.useRef)(false); // Stores the `onStoreChange()` function, which can be used to trigger a `getSnapshot()` update when desired. -@@ -234,11 +237,15 @@ function useOnyx(key, options, dependencies = []) { +@@ -220,11 +223,15 @@ function useOnyx(key, options, dependencies = []) { const subscribe = (0, react_1.useCallback)((onStoreChange) => { // Reset internal state so the hook properly transitions through loading // for the new key instead of preserving stale state from the previous one. From da5862594cbae7dd4b09ae70112672359e508057 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 14 Apr 2026 21:47:48 +0100 Subject: [PATCH 11/14] chore: update `details.md` for `react-native-onyx` --- patches/react-native-onyx/details.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patches/react-native-onyx/details.md b/patches/react-native-onyx/details.md index 1e0e4b90c91a..67ad7c054e85 100644 --- a/patches/react-native-onyx/details.md +++ b/patches/react-native-onyx/details.md @@ -1,6 +1,6 @@ # `react-native-onyx` patches -### [react-native-onyx+3.0.59.patch](react-native-onyx+3.0.59.patch) +### [react-native-onyx+3.0.63.patch](react-native-onyx+3.0.63.patch) - Reason: Onyx v3.0.59 ([PR #756](https://github.com/Expensify/react-native-onyx/pull/756)) added a state reset inside the `subscribe` callback of `useOnyx` to fix stale data when keys change dynamically. However, this reset runs unconditionally — including on initial mount — which causes `useSyncExternalStore` to see a new snapshot reference after subscription, triggering one extra render per `useOnyx` hook. This patch guards the reset with a `hasMountedRef` flag so it only runs on key-change re-subscriptions, not on initial mount. - E/App issue: https://github.com/Expensify/App/issues/85416 From 23b90d7c028539608cc331583f61ddcb5490bb08 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 16 Apr 2026 13:56:06 +0100 Subject: [PATCH 12/14] Merge branch 'main' into @chrispader/bump-onyx-to-3.0.46 --- .../rules/clean-react-0-compiler.md | 2 +- .../javascript/authorChecklist/index.js | 4 +- .../javascript/awaitStagingDeploys/index.js | 4 +- .../javascript/checkAndroidStatus/index.js | 4 +- .../javascript/checkDeployBlockers/index.js | 4 +- .../javascript/checkSVGCompression/index.js | 4 +- .../javascript/formatCodeCovComment/index.js | 4 +- .../javascript/getArtifactInfo/index.js | 4 +- .../getDeployPullRequestList/index.js | 4 +- .../javascript/getPreviousVersion/index.js | 4 +- .../javascript/getPullRequestDetails/index.js | 4 +- .../getPullRequestIncrementalChanges/index.js | 69 +- .../isDeployChecklistLocked/index.js | 9 +- .../markPullRequestsAsDeployed/index.js | 4 +- .../javascript/postTestBuildComment/index.js | 4 +- .../javascript/proposalPoliceComment/index.js | 4 +- .../reopenIssueWithComment/index.js | 4 +- .../javascript/reviewerChecklist/index.js | 4 +- .../javascript/verifySignedCommits/index.js | 4 +- .../javascript/waitForPreviousRuns/index.js | 4 +- .github/libs/DeployChecklistUtils.ts | 6 +- .github/libs/GithubUtils.ts | 4 +- .../workflows/react-compiler-compliance.yml | 2 +- .github/workflows/typecheck.yml | 2 +- .storybook/preview.tsx | 3 - CLAUDE.md | 1 + android/app/build.gradle | 4 +- babel.config.js | 7 +- config/babel/reactCompilerConfig.js | 16 + contributingGuides/INTERACTION_MANAGER.md | 67 + contributingGuides/NETWORK_STATE_DETECTION.md | 214 + contributingGuides/REACT_COMPILER.md | 87 +- contributingGuides/philosophies/OFFLINE.md | 4 + cspell.json | 2 + .../Configure-Company-Card-Settings.md | 3 + .../Connect-Personal-Cards.md | 66 + ...al-Card-Transactions-From-a-Spreadsheet.md | 70 + .../Manage-Personal-Cards.md | 38 +- ...p-a-Direct-Company-Card-Feed-Connection.md | 11 + .../Expensify-Home-Overview.md | 8 +- .../reports-and-expenses/Approve-Expenses.md | 159 +- .../reports-and-expenses/Distance-Expenses.md | 4 +- .../Duplicate-a-Report.md | 78 + .../Expense-and-Report-Actions.md | 2 + .../How-to-Duplicate-an-Expense.md | 82 +- ...derstanding-Report-Statuses-and-Actions.md | 1 + .../settings/Account-Settings.md | 5 +- .../new-expensify/settings/Personal-Karma.md | 23 +- .../images/ExpensifyHelp-FixPersonalCards.png | Bin 0 -> 216723 bytes eslint-plugin-react-compiler-compat/index.mjs | 13 +- ios/NewExpensify/Info.plist | 4 +- ios/NotificationServiceExtension/Info.plist | 4 +- ios/Podfile.lock | 32 +- ios/ShareViewController/Info.plist | 4 +- jest/setup.ts | 2 +- jest/setupAfterEnv.ts | 29 + modules/group-ib-fp/group-ib-fp.podspec | 19 +- package-lock.json | 90 +- package.json | 5 +- ...+fix-horizontal-height-normalization.patch | 2 +- ...fix-inverted-scroll-direction-on-web.patch | 191 + ...0+004+fix-inverted-first-item-offset.patch | 38 + ...nding-children-blocking-measurements.patch | 68 + ...+2.3.0+006+fix-inverted-mvcp-android.patch | 114 + patches/@shopify/flash-list/details.md | 28 + patches/react-compiler-healthcheck/details.md | 38 - ...001+add-verbose-error-logging-option.patch | 90 - ...-20241020+002+enable-ref-identifiers.patch | 28 - ...9.0.0-beta-8a03594-20241020+003+json.patch | 72 - patches/react-native-screens/details.md | 8 + ...ix-lifecycle-events-in-fragment-host.patch | 13 + scripts/react-compiler-compliance-check.ts | 963 +---- scripts/utils/CLI.ts | 29 +- scripts/utils/FileUtils.ts | 44 + scripts/utils/Git.ts | 80 +- src/App.tsx | 24 - src/CONST/index.ts | 47 +- src/Expensify.tsx | 12 +- src/ONYXKEYS.ts | 21 +- src/ROUTES.ts | 69 +- src/SCREENS.ts | 19 +- src/components/BigNumberPad.tsx | 11 +- src/components/BlockingViews/BlockingView.tsx | 8 +- src/components/ChronosTimerHeaderButton.tsx | 2 +- .../CurrencyListContextProvider/index.tsx | 10 +- .../CurrencyListContextProvider/types.ts | 2 +- src/components/DistanceEReceipt.tsx | 3 +- src/components/EReceipt.tsx | 5 +- src/components/EmojiPicker/EmojiPicker.tsx | 2 +- .../EmojiPickerMenu/index.native.tsx | 5 +- .../CellRendererComponent.tsx | 2 +- .../getShowScrollIndicator/index.android.ts | 8 + .../getShowScrollIndicator/index.ts | 5 + .../getShowScrollIndicator/types.ts | 3 + .../FlashList/InvertedFlashList/index.tsx | 43 + src/components/FlashList/index.tsx | 26 + .../FlashList/useFlashListScrollKey.ts | 42 + .../FlatList/InvertedFlatList/index.tsx | 59 - .../index.native.ts | 6 - .../shouldRemoveClippedSubviews/index.ts | 1 - .../FlatList/InvertedFlatList/types.ts | 14 - .../RenderTaskQueue.tsx | 0 .../FlatList/hooks/useFlatListScrollKey.ts | 2 +- src/components/Form/FormProvider.tsx | 20 +- src/components/ImportSpreadsheet.tsx | 7 +- src/components/LocaleContextProvider.tsx | 2 +- src/components/Modal/BaseModal.tsx | 4 +- .../Modal/Global/HoldMenuModalWrapper.tsx | 5 - .../Modal/ReanimatedModal/index.tsx | 17 + src/components/MoneyReportHeader.tsx | 17 +- src/components/MoneyReportHeaderModals.tsx | 5 +- .../MarkAsResolvedPrimaryAction.tsx | 5 +- .../PayPrimaryAction.tsx | 26 +- .../MoneyRequestConfirmationList.tsx | 21 - .../sections/AmountField.tsx | 238 ++ .../sections/AttendeeField.tsx | 60 + .../sections/CategoryField.tsx | 111 + .../sections/DateField.tsx | 56 + .../sections/DescriptionField.tsx | 142 + .../sections/DistanceField.tsx | 79 + .../sections/InvoiceSenderField.tsx | 84 + .../sections/MerchantField.tsx | 132 + .../sections/PerDiemFields.tsx | 138 + .../sections/RateField.tsx | 106 + .../sections/ReportField.tsx | 137 + .../sections/TagFields.tsx | 79 + .../sections/TaxFields.tsx | 85 + .../sections/TimeFields.tsx | 73 + .../sections/ToggleFields.tsx | 63 + .../MoneyRequestConfirmationListFooter.tsx | 1326 ++---- .../MoneyRequestHeaderPrimaryAction.tsx | 2 +- .../MoneyRequestReportActionsList.tsx | 14 +- .../MoneyRequestReportTransactionItem.tsx | 10 +- .../MoneyRequestReportTransactionList.tsx | 20 +- .../MoneyRequestReportView.tsx | 5 +- .../SelectionToolbar.tsx | 1 - .../OutcomeScreen/OutcomeScreenBase.tsx | 45 +- .../QuickCreationActionsBar/index.tsx | 6 +- src/components/NumberWithSymbolForm.tsx | 68 + src/components/ProcessMoneyReportHoldMenu.tsx | 5 - .../MoneyRequestReportPreviewContent.tsx | 4 - .../PayActionButton.tsx | 21 +- .../ReportPreviewActionButton.tsx | 3 - .../ReportActionItem/MoneyRequestView.tsx | 2 +- .../TransactionPreviewContent.tsx | 3 +- .../TransactionPreview/index.tsx | 3 +- .../Search/FilterDropdowns/DisplayPopup.tsx | 160 +- .../Search/FilterDropdowns/DropdownButton.tsx | 71 +- .../Search/FilterDropdowns/InSelectPopup.tsx | 5 +- .../FilterDropdowns/MultiSelectPopup.tsx | 1 + .../FilterDropdowns/SingleSelectPopup.tsx | 2 +- .../Search/FilterDropdowns/SortByPopup.tsx | 50 +- .../Search/FilterDropdowns/SortOrderPopup.tsx | 53 + .../FilterDropdowns/UserSelectPopup.tsx | 13 +- .../FilterDropdowns/WorkspaceSelectPopup.tsx | 6 +- .../Search/SearchAutocompleteInput.tsx | 124 +- .../Search/SearchBulkActionsButton.tsx | 11 - src/components/Search/SearchContext.tsx | 14 +- .../SearchList/BaseSearchList/index.tsx | 7 +- .../Search/SearchList/BaseSearchList/types.ts | 9 +- .../ListItem/ActionCell/PayActionCell.tsx | 14 +- .../ActionCell/actionTranslationsMap.ts | 1 + .../SearchList/ListItem/ActionCell/index.tsx | 12 +- .../ListItem/ExpenseReportListItem.tsx | 3 + .../ListItem/ExpenseReportListItemRow.tsx | 4 +- .../ListItem/ReportListItemHeader.tsx | 4 +- .../Search/SearchList/ListItem/StatusCell.tsx | 11 +- .../Search/SearchList/ListItem/TotalCell.tsx | 3 +- .../ListItem/TransactionGroupListExpanded.tsx | 25 +- .../ListItem/TransactionGroupListItem.tsx | 4 + .../ListItem/TransactionListItem.tsx | 22 +- .../ListItem/UserInfoAndActionButtonRow.tsx | 2 +- .../Search/SearchList/ListItem/types.ts | 28 +- src/components/Search/SearchList/index.tsx | 28 +- .../SearchActionsBarCreateButton.tsx | 2 +- .../SearchActionsBarNarrow.tsx | 71 +- .../SearchPageHeader/SearchActionsBarWide.tsx | 100 +- .../SearchAdvancedFiltersButton.tsx | 35 +- .../SearchDisplayDropdownButton.tsx | 34 +- .../SearchPageHeader/SearchFilterBar.tsx | 105 + .../SearchPageHeader/SearchFiltersBar.tsx | 28 - .../SearchFiltersBarNarrow.tsx | 87 +- .../SearchPageHeader/SearchFiltersBarWide.tsx | 85 +- .../SearchPageHeader/SearchPageHeader.tsx | 53 - .../SearchPageHeaderInput.tsx | 386 -- .../SearchPageHeaderNarrow.tsx | 3 +- .../SearchPageHeader/SearchPageHeaderWide.tsx | 2 +- .../SearchPageInputNarrow.tsx | 16 +- .../SearchPageHeader/SearchPageInputWide.tsx | 17 +- .../SearchPageHeader/SearchSaveButton.tsx | 34 +- .../SearchPageHeader/useSearchActionsBar.tsx | 431 -- .../SearchPageHeader/useSearchFiltersBar.tsx | 828 ++-- .../Search/SearchRouter/SearchRouter.tsx | 2 +- .../Search/hooks/useFilterFeedValue.tsx | 31 - .../Search/hooks/useFilterFromValue.tsx | 20 - .../Search/hooks/useFilterUserValue.tsx | 20 + src/components/Search/index.tsx | 33 +- src/components/Search/types.ts | 4 +- ...Skeleton.tsx => SearchFiltersSkeleton.tsx} | 10 +- .../SearchInputSelectionSkeleton.tsx | 2 +- .../SupportalPermissionDeniedModal.tsx | 40 + ...SupportalPermissionDeniedModalProvider.tsx | 37 - src/components/TestToolMenu.tsx | 2 +- .../implementation/index.native.tsx | 5 +- .../TextInput/BaseTextInput/types.ts | 3 + src/components/TextInputWithSymbol/types.ts | 1 + .../TransactionItemRow/DataCells/TaxCell.tsx | 3 +- .../DataCells/TotalCell.tsx | 8 +- src/components/TransactionItemRow/index.tsx | 77 +- .../VideoPlayer/BaseVideoPlayer.tsx | 2 +- src/hooks/useAdvancedSearchFilters.ts | 15 - src/hooks/useEndSubmitNavigationSpans.ts | 59 + src/hooks/useHoldMenuSubmit.ts | 13 +- src/hooks/useMobileSelectionMode.ts | 2 +- src/hooks/useNetwork.ts | 22 +- src/hooks/useNonReimbursablePaymentModal.ts | 36 - src/hooks/useSearchBackPress/index.android.ts | 2 +- src/hooks/useSearchBulkActions.ts | 68 +- src/hooks/useSearchFilterSync.ts | 2 +- src/hooks/useSearchSelector.base.ts | 3 +- src/hooks/useSelectedTransactionsActions.ts | 2 +- src/hooks/useSelectionModeReportActions.ts | 19 +- src/hooks/useUndeleteTransactions.ts | 31 + src/languages/IntlStore.ts | 2 +- src/languages/de.ts | 22 +- src/languages/en.ts | 21 +- src/languages/es.ts | 21 +- src/languages/fr.ts | 22 +- src/languages/it.ts | 22 +- src/languages/ja.ts | 21 +- src/languages/nl.ts | 21 +- src/languages/pl.ts | 22 +- src/languages/pt-BR.ts | 21 +- src/languages/zh-hans.ts | 27 +- src/libs/API/index.ts | 22 +- .../API/parameters/MergeDuplicatesParams.ts | 1 + ...UpdateTravelInvoicingMonthlyLimitParams.ts | 6 + src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 4 + src/libs/AppState/index.ts | 5 +- src/libs/AppState/types.ts | 5 +- src/libs/AvatarUtils.ts | 6 +- src/libs/CardUtils.ts | 34 +- src/libs/FailureTracker.ts | 78 + src/libs/IOUUtils.ts | 9 +- src/libs/Middleware/FailureTracking.ts | 29 + src/libs/Middleware/Reauthentication.ts | 10 +- src/libs/Middleware/RecheckConnection.ts | 29 - src/libs/Middleware/index.ts | 4 +- src/libs/MoneyRequestReportUtils.ts | 3 +- .../Navigation/AppNavigator/AuthScreens.tsx | 30 +- .../AppNavigator/AuthScreensInitHandler.tsx | 19 - .../ModalStackNavigators/index.tsx | 32 +- .../Navigators/RightModalNavigator.tsx | 7 +- src/libs/Navigation/Navigation.ts | 119 +- .../PlatformStackNavigation/ScreenLayout.tsx | 46 + .../index.native.tsx | 2 + .../index.tsx | 2 + src/libs/Navigation/TransitionTracker.ts | 167 + src/libs/Navigation/guards/OnboardingGuard.ts | 8 + .../dismissModalAndOpenReportInInboxTab.ts | 15 +- .../dynamicRoutesUtils/createDynamicRoute.ts | 14 +- src/libs/Navigation/helpers/linkTo/types.ts | 6 +- .../linkingConfig/RELATIONS/SEARCH_TO_RHP.ts | 4 - .../RELATIONS/WORKSPACE_TO_RHP.ts | 15 +- src/libs/Navigation/linkingConfig/config.ts | 35 +- src/libs/Navigation/types.ts | 29 +- src/libs/Network/MainQueue.ts | 5 +- src/libs/Network/NetworkStore.ts | 40 +- src/libs/Network/SequentialQueue.ts | 74 +- src/libs/NetworkConnection.ts | 361 -- src/libs/NetworkState.ts | 398 ++ src/libs/OptionsListUtils/index.ts | 29 +- src/libs/OptionsListUtils/searchMatchUtils.ts | 66 + src/libs/PersonalDetailsUtils.ts | 2 +- src/libs/PolicyUtils.ts | 6 +- src/libs/PusherUtils.ts | 5 +- src/libs/ReportActionsUtils.ts | 56 +- src/libs/ReportNameUtils.ts | 3 +- src/libs/ReportUtils.ts | 81 +- src/libs/SearchUIUtils.ts | 633 ++- src/libs/SubscriptionUtils.ts | 8 +- src/libs/TransactionUtils/index.ts | 11 +- src/libs/Violations/ViolationsUtils.ts | 11 +- src/libs/actions/App.ts | 9 +- src/libs/actions/Card.ts | 81 +- src/libs/actions/Delegate.ts | 4 +- src/libs/actions/Help.ts | 9 + src/libs/actions/IOU/BulkEdit.ts | 584 +++ src/libs/actions/IOU/Duplicate.ts | 10 +- src/libs/actions/IOU/RejectMoneyRequest.ts | 4 +- src/libs/actions/IOU/ReportWorkflow.ts | 249 +- src/libs/actions/IOU/SendMoney.ts | 40 +- src/libs/actions/IOU/Split.ts | 40 +- src/libs/actions/IOU/UpdateMoneyRequest.ts | 826 +++- src/libs/actions/IOU/index.ts | 3655 +++++------------ src/libs/actions/Link.ts | 17 +- src/libs/actions/MapboxToken.ts | 39 +- src/libs/actions/MergeTransaction.ts | 5 +- src/libs/actions/MobileSelectionMode.ts | 4 +- src/libs/actions/Network.ts | 48 +- src/libs/actions/OnyxDerived/index.ts | 3 +- src/libs/actions/PersistedRequests.ts | 177 +- src/libs/actions/QueuedOnyxUpdates.ts | 4 +- src/libs/actions/Reconnect.ts | 84 + .../actions/Report/MarkAllMessageAsRead.tsx | 4 +- src/libs/actions/Report/index.ts | 32 +- src/libs/actions/Search.ts | 15 +- .../Session/AttachmentImageReauthenticator.ts | 19 +- src/libs/actions/Session/index.ts | 6 +- src/libs/actions/SignInRedirect.ts | 8 +- src/libs/actions/Task.ts | 4 +- src/libs/actions/Transaction.ts | 74 +- src/libs/actions/TravelInvoicing.ts | 82 + src/libs/actions/User.ts | 6 +- src/libs/actions/Wallet.ts | 22 +- src/libs/telemetry/submitFollowUpAction.ts | 166 +- src/pages/AddUnreportedExpense.tsx | 3 +- src/pages/EnablePayments/EnablePayments.tsx | 8 +- .../EnablePayments/EnablePaymentsPage.tsx | 36 +- src/pages/NewChatPage.tsx | 39 +- .../BaseOnboardingInterestedFeatures.tsx | 21 +- src/pages/ReportAddApproverPage.tsx | 2 +- src/pages/ReportChangeApproverPage.tsx | 2 +- src/pages/ReportChangeWorkspacePage.tsx | 4 +- src/pages/ReportParticipantsPage.tsx | 44 +- src/pages/Search/AdvancedSearchFilters.tsx | 65 - src/pages/Search/EmptySearchView.tsx | 23 +- src/pages/Search/SearchAddApproverPage.tsx | 2 +- .../SearchFiltersGroupByPage.tsx | 93 - .../SearchFiltersGroupCurrencyPage.tsx | 14 - .../SearchFiltersLimitPage.tsx | 16 - .../SearchFiltersViewPage.tsx | 91 - src/pages/Search/SearchChangeApproverPage.tsx | 2 +- .../SearchEditMultipleAmountPage.tsx | 2 +- .../SearchEditMultipleBooleanPage.tsx | 2 +- .../SearchEditMultipleCategoryPage.tsx | 2 +- .../SearchEditMultipleDatePage.tsx | 2 +- .../SearchEditMultipleDescriptionPage.tsx | 2 +- .../SearchEditMultipleMerchantPage.tsx | 2 +- .../SearchEditMultiplePage.tsx | 2 +- .../SearchEditMultipleTagPage.tsx | 2 +- .../SearchEditMultipleTaxPage.tsx | 2 +- src/pages/Search/SearchPage.tsx | 1 + .../SearchPageNarrow/StaticFiltersBar.tsx | 102 - .../StaticSearchActionsBar.tsx | 40 + .../StaticSearchPageHeader.tsx | 40 - .../StaticSearchPageInput.tsx | 37 + ...bSelector.tsx => StaticSearchTypeMenu.tsx} | 10 +- .../Search/SearchPageNarrow/Switches.tsx | 77 +- src/pages/Search/SearchPageNarrow/index.tsx | 108 +- src/pages/Search/SearchPageWide.tsx | 30 +- src/pages/Search/SearchSavePage.tsx | 61 +- ...bSelector.tsx => SearchTypeMenuNarrow.tsx} | 19 +- src/pages/Share/SubmitDetailsPage.tsx | 108 +- .../TransactionDuplicate/Confirmation.tsx | 16 +- .../TransactionDuplicate/ReviewTaxCode.tsx | 5 +- .../MergeTransactionsListContent.tsx | 3 +- src/pages/ValidateLoginPage/index.website.tsx | 19 +- src/pages/inbox/ReportActionsList.tsx | 4 +- .../report/ContextMenu/ContextMenuActions.tsx | 4 +- .../inbox/report/PureReportActionItem.tsx | 3 +- src/pages/inbox/report/ReportActionsList.tsx | 54 +- src/pages/inbox/report/ReportActionsView.tsx | 41 +- src/pages/inbox/report/ReportFooter.tsx | 8 +- .../actionContents/SimpleMessageContent.tsx | 4 +- src/pages/iou/request/IOURequestStartPage.tsx | 9 + .../MoneyRequestAccountantSelector.tsx | 236 +- .../request/MoneyRequestAttendeeSelector.tsx | 4 +- .../MoneyRequestParticipantsSelector.tsx | 5 +- .../request/step/IOURequestStepAccountant.tsx | 1 - .../step/IOURequestStepCompanyInfo.tsx | 21 +- .../step/IOURequestStepConfirmation.tsx | 265 +- .../SubmitExpenseOrchestrator.tsx | 186 + .../routes/TransactionReceiptModalContent.tsx | 9 +- .../report/ReportAttachmentModalContent.tsx | 5 +- src/pages/settings/HelpPage/HelpPage.tsx | 62 +- .../settings/Wallet/WalletPage/index.tsx | 33 +- ...ickbooksCompanyCardExpenseAccountPage.tsx} | 28 +- ...mpanyCardExpenseAccountSelectCardPage.tsx} | 20 +- ...ksCompanyCardExpenseAccountSelectPage.tsx} | 24 +- ...amicQuickbooksExportConfigurationPage.tsx} | 14 +- .../DynamicQuickbooksExportDateSelectPage.tsx | 6 +- ...ckbooksExportInvoiceAccountSelectPage.tsx} | 20 +- ...oksOutOfPocketExpenseConfigurationPage.tsx | 4 +- ...ooksPreferredExporterConfigurationPage.tsx | 6 +- src/pages/workspace/accounting/utils.tsx | 5 +- .../categories/ImportCategoriesPage.tsx | 16 +- .../categories/ImportedCategoriesPage.tsx | 20 +- .../categories/WorkspaceCategoriesPage.tsx | 5 +- .../WorkspaceCategoriesSettingsPage.tsx | 9 +- .../WorkspaceCompanyCardsTable/index.tsx | 6 +- src/pages/workspace/companyCards/utils.tsx | 5 +- .../expensifyCard/issueNew/AssigneeStep.tsx | 2 +- .../expensifyCard/issueNew/CardNameStep.tsx | 2 +- .../expensifyCard/issueNew/CardTypeStep.tsx | 2 +- .../issueNew/ConfirmationStep.tsx | 2 +- .../issueNew/InviteNewMemberStep.tsx | 2 +- .../IssueNewCardConfirmMagicCodePage.tsx | 2 +- .../issueNew/IssueNewCardPage.tsx | 2 +- .../expensifyCard/issueNew/LimitTypeStep.tsx | 2 +- .../issueNew/SetExpiryOptionsStep.tsx | 2 +- .../WorkspaceInviteMessageComponent.tsx | 6 +- .../rules/SpendRules/SpendRuleCardPage.tsx | 42 +- .../SpendRules/SpendRuleCategoryPage.tsx | 15 +- .../rules/SpendRules/SpendRulePageBase.tsx | 3 +- .../rules/SpendRules/SpendRulesSection.tsx | 102 +- src/pages/workspace/tags/ImportedTagsPage.tsx | 2 +- ...rkspaceTravelInvoicingMonthlyLimitPage.tsx | 120 + .../WorkspaceTravelInvoicingSection.tsx | 22 + src/selectors/Network.ts | 16 +- .../index.native.ts | 12 +- src/setup/index.ts | 7 +- src/stories/Form.stories.tsx | 7 - src/styles/index.ts | 87 +- src/styles/theme/themes/dark.ts | 4 + src/styles/theme/themes/light.ts | 4 + src/styles/theme/types.ts | 2 +- .../getHiddenChatContentStyle/index.native.ts | 7 + .../utils/getHiddenChatContentStyle/index.ts | 5 + .../utils/getHiddenChatContentStyle/types.ts | 6 + src/styles/utils/index.ts | 2 + src/styles/utils/spacing.ts | 4 + src/styles/variables.ts | 8 +- .../EditTravelInvoicingMonthlyLimitForm.ts | 13 + src/types/form/SearchAdvancedFiltersForm.ts | 28 + src/types/form/index.ts | 1 + src/types/onyx/Account.ts | 6 + src/types/onyx/ExpensifyCardSettings.ts | 7 +- src/types/onyx/Network.ts | 29 +- src/types/onyx/OriginalMessage.ts | 10 +- src/types/onyx/UserWallet.ts | 3 + src/utils/keyboard/index.android.ts | 12 +- src/utils/keyboard/index.ts | 12 +- src/utils/keyboard/index.website.ts | 21 +- src/utils/keyboard/types.ts | 7 + tests/actions/IOUTest.ts | 1280 +----- tests/actions/IOUTest/BulkEditTest.ts | 1291 ++++++ tests/actions/IOUTest/DuplicateTest.ts | 43 +- .../actions/IOUTest/RejectMoneyRequestTest.ts | 87 +- .../IOUTest/SplitDistanceMessageTest.ts | 2 +- tests/actions/QueuedOnyxUpdatesTest.ts | 8 +- tests/actions/ReportTest.ts | 105 +- tests/actions/SessionTest.ts | 48 +- tests/navigation/createDynamicRouteTests.ts | 29 + .../ReportActionCompose.perf-test.tsx | 3 +- .../perf-test/ReportActionsList.perf-test.tsx | 3 +- tests/perf-test/SearchRouter.perf-test.tsx | 3 +- tests/perf-test/SidebarLinks.perf-test.tsx | 3 +- .../useFilterFormValues.perf-test.tsx | 2 +- tests/ui/AssignCardFeed.tsx | 39 +- tests/ui/AuthScreensInitHandlerTest.tsx | 47 +- tests/ui/CategoryListItemHeaderTest.tsx | 7 +- tests/ui/ClearReportActionErrorsUITest.tsx | 3 +- tests/ui/LHNItemsPresence.tsx | 3 +- tests/ui/MerchantListItemHeaderTest.tsx | 7 +- ...equestReportActionsListRejectModalTest.tsx | 12 +- tests/ui/MonthListItemHeaderTest.tsx | 7 +- tests/ui/PaginationTest.tsx | 1 + tests/ui/PureReportActionItemTest.tsx | 31 +- tests/ui/ReportActionsViewTest.tsx | 12 +- tests/ui/ReportListItemHeaderTest.tsx | 4 +- tests/ui/TestToolMenuBiometricsTest.tsx | 3 + tests/ui/TimeExpenseConfirmationTest.tsx | 3 + tests/ui/UnreadIndicatorsTest.tsx | 3 +- tests/ui/WeekListItemHeaderTest.tsx | 7 +- tests/ui/WorkspacePageWithSectionsTest.tsx | 11 +- tests/ui/YearListItemHeaderTest.tsx | 7 +- .../components/FeatureTrainingModalTest.tsx | 5 - .../IOURequestStepConfirmationPageTest.tsx | 2 + tests/ui/components/LHNOptionsListTest.tsx | 5 +- .../ProductTrainingContextProvider.tsx | 3 +- tests/unit/APITest.ts | 149 +- tests/unit/CLIVariadicTest.ts | 113 + tests/unit/CardUtilsTest.ts | 25 +- tests/unit/FailureTrackerTest.ts | 175 + tests/unit/FailureTrackingTest.ts | 60 + tests/unit/GitTest.ts | 4 +- tests/unit/GoogleTagManagerTest.tsx | 61 +- tests/unit/IOUUtilsTest.ts | 12 + tests/unit/LocalizeTests.ts | 2 +- tests/unit/MiddlewareTest.ts | 2 +- .../MoneyRequestReportButtonUtils.test.ts | 8 +- .../unit/Navigation/TransitionTrackerTest.ts | 185 + .../Navigation/guards/OnboardingGuard.test.ts | 157 +- tests/unit/NetworkStateReachabilityTest.ts | 167 + tests/unit/NetworkStateTest.ts | 500 +++ tests/unit/NetworkTest.tsx | 202 +- tests/unit/OnyxDerivedTest.tsx | 2 +- tests/unit/OptionsListUtilsTest.tsx | 3 +- tests/unit/PersistedRequests.ts | 116 +- .../unit/ReactCompilerComplianceCheckTest.ts | 97 + tests/unit/ReconnectTest.ts | 174 + tests/unit/RenderTaskQueueTest.ts | 4 +- tests/unit/ReportActionItemSingleTest.ts | 6 +- tests/unit/ReportActionsUtilsTest.ts | 3 +- tests/unit/ReportUtilsTest.ts | 126 +- tests/unit/ResolveFilePathsTest.ts | 80 + .../unit/Search/SearchListRenderCountTest.tsx | 3 +- tests/unit/Search/SearchUIUtilsTest.ts | 24 +- .../Search/handleActionButtonPressTest.ts | 2 + tests/unit/SidebarFilterTest.ts | 3 +- tests/unit/SidebarOrderTest.ts | 4 +- tests/unit/SidebarTest.ts | 3 +- tests/unit/SubscriptionUtilsTest.ts | 24 +- tests/unit/ViolationUtilsTest.ts | 24 + ...dismissModalAndOpenReportInInboxTabTest.ts | 10 +- tests/unit/hooks/useFilterFormValues.test.ts | 2 +- .../hooks/useSearchFiltersBarHelpers.test.ts | 27 - .../useSelectionModeReportActions.test.ts | 9 - .../unit/keyboard/AndroidKeyboardUtilsTest.ts | 5 + tests/unit/keyboard/KeyboardUtilsTest.ts | 27 +- tests/unit/navigateAfterExpenseCreateTest.ts | 16 +- tests/unit/searchMatchUtilsTest.ts | 145 + tests/unit/selectors/NetworkTest.ts | 40 +- .../unit/showReportActionNotificationTest.ts | 20 - web/snippets/gib.js | 1451 ++++--- 517 files changed, 16669 insertions(+), 13224 deletions(-) create mode 100644 config/babel/reactCompilerConfig.js create mode 100644 contributingGuides/INTERACTION_MANAGER.md create mode 100644 contributingGuides/NETWORK_STATE_DETECTION.md create mode 100644 docs/articles/new-expensify/connect-credit-cards/Connect-Personal-Cards.md create mode 100644 docs/articles/new-expensify/connect-credit-cards/Import-Personal-Card-Transactions-From-a-Spreadsheet.md create mode 100644 docs/articles/new-expensify/reports-and-expenses/Duplicate-a-Report.md create mode 100644 docs/assets/images/ExpensifyHelp-FixPersonalCards.png create mode 100644 patches/@shopify/flash-list/@shopify+flash-list+2.3.0+003+fix-inverted-scroll-direction-on-web.patch create mode 100644 patches/@shopify/flash-list/@shopify+flash-list+2.3.0+004+fix-inverted-first-item-offset.patch create mode 100644 patches/@shopify/flash-list/@shopify+flash-list+2.3.0+005+fix-pending-children-blocking-measurements.patch create mode 100644 patches/@shopify/flash-list/@shopify+flash-list+2.3.0+006+fix-inverted-mvcp-android.patch delete mode 100644 patches/react-compiler-healthcheck/details.md delete mode 100644 patches/react-compiler-healthcheck/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+001+add-verbose-error-logging-option.patch delete mode 100644 patches/react-compiler-healthcheck/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+002+enable-ref-identifiers.patch delete mode 100644 patches/react-compiler-healthcheck/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+003+json.patch create mode 100644 patches/react-native-screens/details.md create mode 100644 patches/react-native-screens/react-native-screens+4.15.4+001+fix-lifecycle-events-in-fragment-host.patch rename src/components/{FlatList/InvertedFlatList => FlashList/InvertedFlashList}/CellRendererComponent.tsx (94%) create mode 100644 src/components/FlashList/InvertedFlashList/getShowScrollIndicator/index.android.ts create mode 100644 src/components/FlashList/InvertedFlashList/getShowScrollIndicator/index.ts create mode 100644 src/components/FlashList/InvertedFlashList/getShowScrollIndicator/types.ts create mode 100644 src/components/FlashList/InvertedFlashList/index.tsx create mode 100644 src/components/FlashList/index.tsx create mode 100644 src/components/FlashList/useFlashListScrollKey.ts delete mode 100644 src/components/FlatList/InvertedFlatList/index.tsx delete mode 100644 src/components/FlatList/InvertedFlatList/shouldRemoveClippedSubviews/index.native.ts delete mode 100644 src/components/FlatList/InvertedFlatList/shouldRemoveClippedSubviews/index.ts delete mode 100644 src/components/FlatList/InvertedFlatList/types.ts rename src/components/FlatList/{InvertedFlatList => }/RenderTaskQueue.tsx (100%) create mode 100644 src/components/MoneyRequestConfirmationList/sections/AmountField.tsx create mode 100644 src/components/MoneyRequestConfirmationList/sections/AttendeeField.tsx create mode 100644 src/components/MoneyRequestConfirmationList/sections/CategoryField.tsx create mode 100644 src/components/MoneyRequestConfirmationList/sections/DateField.tsx create mode 100644 src/components/MoneyRequestConfirmationList/sections/DescriptionField.tsx create mode 100644 src/components/MoneyRequestConfirmationList/sections/DistanceField.tsx create mode 100644 src/components/MoneyRequestConfirmationList/sections/InvoiceSenderField.tsx create mode 100644 src/components/MoneyRequestConfirmationList/sections/MerchantField.tsx create mode 100644 src/components/MoneyRequestConfirmationList/sections/PerDiemFields.tsx create mode 100644 src/components/MoneyRequestConfirmationList/sections/RateField.tsx create mode 100644 src/components/MoneyRequestConfirmationList/sections/ReportField.tsx create mode 100644 src/components/MoneyRequestConfirmationList/sections/TagFields.tsx create mode 100644 src/components/MoneyRequestConfirmationList/sections/TaxFields.tsx create mode 100644 src/components/MoneyRequestConfirmationList/sections/TimeFields.tsx create mode 100644 src/components/MoneyRequestConfirmationList/sections/ToggleFields.tsx create mode 100644 src/components/Search/FilterDropdowns/SortOrderPopup.tsx create mode 100644 src/components/Search/SearchPageHeader/SearchFilterBar.tsx delete mode 100644 src/components/Search/SearchPageHeader/SearchFiltersBar.tsx delete mode 100644 src/components/Search/SearchPageHeader/SearchPageHeader.tsx delete mode 100644 src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx delete mode 100644 src/components/Search/SearchPageHeader/useSearchActionsBar.tsx delete mode 100644 src/components/Search/hooks/useFilterFeedValue.tsx delete mode 100644 src/components/Search/hooks/useFilterFromValue.tsx create mode 100644 src/components/Search/hooks/useFilterUserValue.tsx rename src/components/Skeletons/{SearchActionsSkeleton.tsx => SearchFiltersSkeleton.tsx} (88%) create mode 100644 src/components/SupportalPermissionDeniedModal.tsx delete mode 100644 src/components/SupportalPermissionDeniedModalProvider.tsx create mode 100644 src/hooks/useEndSubmitNavigationSpans.ts delete mode 100644 src/hooks/useNonReimbursablePaymentModal.ts create mode 100644 src/hooks/useUndeleteTransactions.ts create mode 100644 src/libs/API/parameters/UpdateTravelInvoicingMonthlyLimitParams.ts create mode 100644 src/libs/FailureTracker.ts create mode 100644 src/libs/Middleware/FailureTracking.ts delete mode 100644 src/libs/Middleware/RecheckConnection.ts create mode 100644 src/libs/Navigation/PlatformStackNavigation/ScreenLayout.tsx create mode 100644 src/libs/Navigation/TransitionTracker.ts delete mode 100644 src/libs/NetworkConnection.ts create mode 100644 src/libs/NetworkState.ts create mode 100644 src/libs/OptionsListUtils/searchMatchUtils.ts create mode 100644 src/libs/actions/Help.ts create mode 100644 src/libs/actions/IOU/BulkEdit.ts create mode 100644 src/libs/actions/Reconnect.ts delete mode 100644 src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersGroupByPage.tsx delete mode 100644 src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersGroupCurrencyPage.tsx delete mode 100644 src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersLimitPage.tsx delete mode 100644 src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersViewPage.tsx delete mode 100644 src/pages/Search/SearchPageNarrow/StaticFiltersBar.tsx create mode 100644 src/pages/Search/SearchPageNarrow/StaticSearchActionsBar.tsx delete mode 100644 src/pages/Search/SearchPageNarrow/StaticSearchPageHeader.tsx create mode 100644 src/pages/Search/SearchPageNarrow/StaticSearchPageInput.tsx rename src/pages/Search/SearchPageNarrow/{StaticTabSelector.tsx => StaticSearchTypeMenu.tsx} (89%) rename src/pages/Search/{SearchPageTabSelector.tsx => SearchTypeMenuNarrow.tsx} (94%) create mode 100644 src/pages/iou/request/step/confirmation/SubmitExpenseOrchestrator.tsx rename src/pages/workspace/accounting/qbo/export/{QuickbooksCompanyCardExpenseAccountPage.tsx => DynamicQuickbooksCompanyCardExpenseAccountPage.tsx} (85%) rename src/pages/workspace/accounting/qbo/export/{QuickbooksCompanyCardExpenseAccountSelectCardPage.tsx => DynamicQuickbooksCompanyCardExpenseAccountSelectCardPage.tsx} (86%) rename src/pages/workspace/accounting/qbo/export/{QuickbooksCompanyCardExpenseAccountSelectPage.tsx => DynamicQuickbooksCompanyCardExpenseAccountSelectPage.tsx} (84%) rename src/pages/workspace/accounting/qbo/export/{QuickbooksExportConfigurationPage.tsx => DynamicQuickbooksExportConfigurationPage.tsx} (92%) rename src/pages/workspace/accounting/qbo/export/{QuickbooksExportInvoiceAccountSelectPage.tsx => DynamicQuickbooksExportInvoiceAccountSelectPage.tsx} (82%) create mode 100644 src/pages/workspace/travel/WorkspaceTravelInvoicingMonthlyLimitPage.tsx create mode 100644 src/styles/utils/getHiddenChatContentStyle/index.native.ts create mode 100644 src/styles/utils/getHiddenChatContentStyle/index.ts create mode 100644 src/styles/utils/getHiddenChatContentStyle/types.ts create mode 100644 src/types/form/EditTravelInvoicingMonthlyLimitForm.ts create mode 100644 src/utils/keyboard/types.ts create mode 100644 tests/actions/IOUTest/BulkEditTest.ts create mode 100644 tests/unit/CLIVariadicTest.ts create mode 100644 tests/unit/FailureTrackerTest.ts create mode 100644 tests/unit/FailureTrackingTest.ts create mode 100644 tests/unit/Navigation/TransitionTrackerTest.ts create mode 100644 tests/unit/NetworkStateReachabilityTest.ts create mode 100644 tests/unit/NetworkStateTest.ts create mode 100644 tests/unit/ReactCompilerComplianceCheckTest.ts create mode 100644 tests/unit/ReconnectTest.ts create mode 100644 tests/unit/ResolveFilePathsTest.ts delete mode 100644 tests/unit/hooks/useSearchFiltersBarHelpers.test.ts create mode 100644 tests/unit/searchMatchUtilsTest.ts diff --git a/.claude/skills/coding-standards/rules/clean-react-0-compiler.md b/.claude/skills/coding-standards/rules/clean-react-0-compiler.md index 67dfc4692b8d..58199a3aabd5 100644 --- a/.claude/skills/coding-standards/rules/clean-react-0-compiler.md +++ b/.claude/skills/coding-standards/rules/clean-react-0-compiler.md @@ -18,7 +18,7 @@ Manual memoization is therefore: The codebase enforces this via: - **Babel plugin**: `babel-plugin-react-compiler` in `babel.config.js` - **ESLint processor**: `eslint-plugin-react-compiler-compat` suppresses redundant lint rules when files compile successfully -- **CI compliance check**: `scripts/react-compiler-compliance-check.ts` blocks PRs with manual memoization in new files +- **CI compliance check**: `scripts/react-compiler-compliance-check.ts` enforces that new components/hooks compile and that existing compiled files don't regress Reference: [React Compiler documentation](https://react.dev/learn/react-compiler) diff --git a/.github/actions/javascript/authorChecklist/index.js b/.github/actions/javascript/authorChecklist/index.js index 6fb947077e24..87f0587ec36d 100644 --- a/.github/actions/javascript/authorChecklist/index.js +++ b/.github/actions/javascript/authorChecklist/index.js @@ -15767,11 +15767,11 @@ class GithubUtils { /** * Fetch all pull requests given a list of PR numbers. */ - static fetchAllPullRequests(pullRequestNumbers) { + static fetchAllPullRequests(pullRequestNumbers, repo = CONST_1.default.APP_REPO) { const oldestPR = pullRequestNumbers.sort((a, b) => a - b).at(0); return this.paginate(this.octokit.pulls.list, { owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, + repo, state: 'all', sort: 'created', direction: 'desc', diff --git a/.github/actions/javascript/awaitStagingDeploys/index.js b/.github/actions/javascript/awaitStagingDeploys/index.js index c655339397f8..92168e1c5c31 100644 --- a/.github/actions/javascript/awaitStagingDeploys/index.js +++ b/.github/actions/javascript/awaitStagingDeploys/index.js @@ -12545,11 +12545,11 @@ class GithubUtils { /** * Fetch all pull requests given a list of PR numbers. */ - static fetchAllPullRequests(pullRequestNumbers) { + static fetchAllPullRequests(pullRequestNumbers, repo = CONST_1.default.APP_REPO) { const oldestPR = pullRequestNumbers.sort((a, b) => a - b).at(0); return this.paginate(this.octokit.pulls.list, { owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, + repo, state: 'all', sort: 'created', direction: 'desc', diff --git a/.github/actions/javascript/checkAndroidStatus/index.js b/.github/actions/javascript/checkAndroidStatus/index.js index 43973e398085..cba463f67965 100644 --- a/.github/actions/javascript/checkAndroidStatus/index.js +++ b/.github/actions/javascript/checkAndroidStatus/index.js @@ -737288,11 +737288,11 @@ class GithubUtils { /** * Fetch all pull requests given a list of PR numbers. */ - static fetchAllPullRequests(pullRequestNumbers) { + static fetchAllPullRequests(pullRequestNumbers, repo = CONST_1.default.APP_REPO) { const oldestPR = pullRequestNumbers.sort((a, b) => a - b).at(0); return this.paginate(this.octokit.pulls.list, { owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, + repo, state: 'all', sort: 'created', direction: 'desc', diff --git a/.github/actions/javascript/checkDeployBlockers/index.js b/.github/actions/javascript/checkDeployBlockers/index.js index 4f9879646d9e..320a6a190cf0 100644 --- a/.github/actions/javascript/checkDeployBlockers/index.js +++ b/.github/actions/javascript/checkDeployBlockers/index.js @@ -11812,11 +11812,11 @@ class GithubUtils { /** * Fetch all pull requests given a list of PR numbers. */ - static fetchAllPullRequests(pullRequestNumbers) { + static fetchAllPullRequests(pullRequestNumbers, repo = CONST_1.default.APP_REPO) { const oldestPR = pullRequestNumbers.sort((a, b) => a - b).at(0); return this.paginate(this.octokit.pulls.list, { owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, + repo, state: 'all', sort: 'created', direction: 'desc', diff --git a/.github/actions/javascript/checkSVGCompression/index.js b/.github/actions/javascript/checkSVGCompression/index.js index 08035a5a2ea1..2a1bad543517 100644 --- a/.github/actions/javascript/checkSVGCompression/index.js +++ b/.github/actions/javascript/checkSVGCompression/index.js @@ -20337,11 +20337,11 @@ class GithubUtils { /** * Fetch all pull requests given a list of PR numbers. */ - static fetchAllPullRequests(pullRequestNumbers) { + static fetchAllPullRequests(pullRequestNumbers, repo = CONST_1.default.APP_REPO) { const oldestPR = pullRequestNumbers.sort((a, b) => a - b).at(0); return this.paginate(this.octokit.pulls.list, { owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, + repo, state: 'all', sort: 'created', direction: 'desc', diff --git a/.github/actions/javascript/formatCodeCovComment/index.js b/.github/actions/javascript/formatCodeCovComment/index.js index f2dee5c3d4b5..2478c7b47258 100644 --- a/.github/actions/javascript/formatCodeCovComment/index.js +++ b/.github/actions/javascript/formatCodeCovComment/index.js @@ -11999,11 +11999,11 @@ class GithubUtils { /** * Fetch all pull requests given a list of PR numbers. */ - static fetchAllPullRequests(pullRequestNumbers) { + static fetchAllPullRequests(pullRequestNumbers, repo = CONST_1.default.APP_REPO) { const oldestPR = pullRequestNumbers.sort((a, b) => a - b).at(0); return this.paginate(this.octokit.pulls.list, { owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, + repo, state: 'all', sort: 'created', direction: 'desc', diff --git a/.github/actions/javascript/getArtifactInfo/index.js b/.github/actions/javascript/getArtifactInfo/index.js index ba1dafc205bb..b031dfa3dbc4 100644 --- a/.github/actions/javascript/getArtifactInfo/index.js +++ b/.github/actions/javascript/getArtifactInfo/index.js @@ -11773,11 +11773,11 @@ class GithubUtils { /** * Fetch all pull requests given a list of PR numbers. */ - static fetchAllPullRequests(pullRequestNumbers) { + static fetchAllPullRequests(pullRequestNumbers, repo = CONST_1.default.APP_REPO) { const oldestPR = pullRequestNumbers.sort((a, b) => a - b).at(0); return this.paginate(this.octokit.pulls.list, { owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, + repo, state: 'all', sort: 'created', direction: 'desc', diff --git a/.github/actions/javascript/getDeployPullRequestList/index.js b/.github/actions/javascript/getDeployPullRequestList/index.js index a20dce0f925b..cc1b5935b5d4 100644 --- a/.github/actions/javascript/getDeployPullRequestList/index.js +++ b/.github/actions/javascript/getDeployPullRequestList/index.js @@ -12154,11 +12154,11 @@ class GithubUtils { /** * Fetch all pull requests given a list of PR numbers. */ - static fetchAllPullRequests(pullRequestNumbers) { + static fetchAllPullRequests(pullRequestNumbers, repo = CONST_1.default.APP_REPO) { const oldestPR = pullRequestNumbers.sort((a, b) => a - b).at(0); return this.paginate(this.octokit.pulls.list, { owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, + repo, state: 'all', sort: 'created', direction: 'desc', diff --git a/.github/actions/javascript/getPreviousVersion/index.js b/.github/actions/javascript/getPreviousVersion/index.js index b68d608b4e34..b164e5aebb24 100644 --- a/.github/actions/javascript/getPreviousVersion/index.js +++ b/.github/actions/javascript/getPreviousVersion/index.js @@ -11965,11 +11965,11 @@ class GithubUtils { /** * Fetch all pull requests given a list of PR numbers. */ - static fetchAllPullRequests(pullRequestNumbers) { + static fetchAllPullRequests(pullRequestNumbers, repo = CONST_1.default.APP_REPO) { const oldestPR = pullRequestNumbers.sort((a, b) => a - b).at(0); return this.paginate(this.octokit.pulls.list, { owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, + repo, state: 'all', sort: 'created', direction: 'desc', diff --git a/.github/actions/javascript/getPullRequestDetails/index.js b/.github/actions/javascript/getPullRequestDetails/index.js index ba050da3e984..70200845421f 100644 --- a/.github/actions/javascript/getPullRequestDetails/index.js +++ b/.github/actions/javascript/getPullRequestDetails/index.js @@ -11902,11 +11902,11 @@ class GithubUtils { /** * Fetch all pull requests given a list of PR numbers. */ - static fetchAllPullRequests(pullRequestNumbers) { + static fetchAllPullRequests(pullRequestNumbers, repo = CONST_1.default.APP_REPO) { const oldestPR = pullRequestNumbers.sort((a, b) => a - b).at(0); return this.paginate(this.octokit.pulls.list, { owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, + repo, state: 'all', sort: 'created', direction: 'desc', diff --git a/.github/actions/javascript/getPullRequestIncrementalChanges/index.js b/.github/actions/javascript/getPullRequestIncrementalChanges/index.js index 1a342b6645cc..da69280aaaf6 100644 --- a/.github/actions/javascript/getPullRequestIncrementalChanges/index.js +++ b/.github/actions/javascript/getPullRequestIncrementalChanges/index.js @@ -12023,11 +12023,11 @@ class GithubUtils { /** * Fetch all pull requests given a list of PR numbers. */ - static fetchAllPullRequests(pullRequestNumbers) { + static fetchAllPullRequests(pullRequestNumbers, repo = CONST_1.default.APP_REPO) { const oldestPR = pullRequestNumbers.sort((a, b) => a - b).at(0); return this.paginate(this.octokit.pulls.list, { owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, + repo, state: 'all', sort: 'created', direction: 'desc', @@ -12410,8 +12410,8 @@ class Git { * @throws Error when git command fails (invalid refs, not a git repo, file not found, etc.) */ static diff(fromRef, toRef, filePaths, shouldIncludeUntrackedFiles = false) { - // Build git diff command (with 0 context lines for easier parsing) - let command = `git diff -U0 ${fromRef}`; + // Build git diff command (with 0 context lines for easier parsing, -M for rename detection) + let command = `git diff -U0 -M ${fromRef}`; if (toRef) { command += ` ${toRef}`; } @@ -12453,12 +12453,12 @@ class Git { const files = []; let currentFile = null; let currentHunk = null; - let oldFilePath = null; // Track old file path to determine fileDiffType + let oldFilePath = null; + let renameFromPath = null; for (const line of lines) { // File header: diff --git a/file b/file if (line.startsWith('diff --git')) { if (currentFile) { - // Push the current hunk to the current file before processing the new file if (currentHunk) { currentFile.hunks.push(currentHunk); } @@ -12466,41 +12466,51 @@ class Git { } currentFile = null; currentHunk = null; - oldFilePath = null; // Reset for next file + oldFilePath = null; + renameFromPath = null; + continue; + } + // Rename detection: "rename from " appears before --- / +++ + if (line.startsWith('rename from ')) { + renameFromPath = line.slice('rename from '.length); + continue; + } + if (line.startsWith('rename to ') || line.startsWith('similarity index ')) { continue; } // Old file path: --- a/file or --- /dev/null (for new files) - // This comes before +++ in git diff output if (line.startsWith('--- ')) { - oldFilePath = line.slice(4); // Store the old file path (remove '--- ') + oldFilePath = line.slice(4); continue; } // New file path: +++ b/file or +++ /dev/null (for removed files) if (line.startsWith('+++ ')) { - const newFilePath = line.slice(4); // Remove '+++ ' - // Determine fileDiffType based on old and new file paths - // Note: oldFilePath should always be set by the time we see +++, but handle null for type safety + const newFilePath = line.slice(4); let fileDiffType = 'modified'; let diffFilePath; + let previousFilePath; const oldPath = oldFilePath ?? ''; if (oldPath === '/dev/null') { - // New file: use the new file path fileDiffType = 'added'; diffFilePath = newFilePath.startsWith('b/') ? newFilePath.slice(2) : newFilePath; } else if (newFilePath === '/dev/null') { - // Removed file: use the old file path fileDiffType = 'removed'; diffFilePath = oldPath.startsWith('a/') ? oldPath.slice(2) : oldPath; } + else if (renameFromPath) { + fileDiffType = 'renamed'; + diffFilePath = newFilePath.startsWith('b/') ? newFilePath.slice(2) : newFilePath; + previousFilePath = renameFromPath; + } else { - // Modified file: use the new file path fileDiffType = 'modified'; diffFilePath = newFilePath.startsWith('b/') ? newFilePath.slice(2) : newFilePath; } currentFile = { filePath: diffFilePath, diffType: fileDiffType, + previousFilePath, hunks: [], addedLines: new Set(), removedLines: new Set(), @@ -12723,20 +12733,37 @@ class Git { return false; } } - static async getChangedFileNames(fromRef, toRef, shouldIncludeUntrackedFiles = false) { + /** + * Get changed files with their status (added, modified, removed, renamed). + * In CI, uses the GitHub API with pagination for accuracy. + * Locally, uses git diff against the provided ref. + */ + static async getChangedFilesWithStatus(fromRef, toRef, shouldIncludeUntrackedFiles = false) { if (IS_CI) { - const { data: changedFiles } = await GithubUtils_1.default.octokit.pulls.listFiles({ + const files = await GithubUtils_1.default.paginate(GithubUtils_1.default.octokit.pulls.listFiles, { owner: CONST_1.default.GITHUB_OWNER, repo: CONST_1.default.APP_REPO, // eslint-disable-next-line @typescript-eslint/naming-convention pull_number: github_1.context.payload.pull_request?.number ?? 0, + // eslint-disable-next-line @typescript-eslint/naming-convention + per_page: 100, }); - return changedFiles.map((file) => file.filename); + return files.map((file) => ({ + filename: file.filename, + status: file.status, + previousFilename: file.previous_filename, + })); } - // Get the diff output and check status const diffResult = this.diff(fromRef, toRef, undefined, shouldIncludeUntrackedFiles); - const files = diffResult.files.map((file) => file.filePath); - return files; + return diffResult.files.map((file) => ({ + filename: file.filePath, + status: file.diffType, + previousFilename: file.previousFilePath, + })); + } + static async getChangedFileNames(fromRef, toRef, shouldIncludeUntrackedFiles = false) { + const files = await this.getChangedFilesWithStatus(fromRef, toRef, shouldIncludeUntrackedFiles); + return files.map((file) => file.filename); } /** * Get list of untracked files from git. diff --git a/.github/actions/javascript/isDeployChecklistLocked/index.js b/.github/actions/javascript/isDeployChecklistLocked/index.js index 6a954faf6be8..2d794efb63cd 100644 --- a/.github/actions/javascript/isDeployChecklistLocked/index.js +++ b/.github/actions/javascript/isDeployChecklistLocked/index.js @@ -11748,8 +11748,11 @@ async function generateDeployChecklistBodyAndAssignees({ tag, PRList, PRListMobi console.log('Found the following Internal QA PRs:', Object.fromEntries(internalQAPRMap)); const noQAPRNumbers = Array.isArray(data) ? data.filter((PR) => /\[No\s?QA]/i.test(PR.title)).map((item) => item.number) : []; console.log('Found the following NO QA PRs:', noQAPRNumbers); + const mobileExpensifyData = PRListMobileExpensify.length > 0 ? await GithubUtils_1.default.fetchAllPullRequests(PRListMobileExpensify, CONST_1.default.MOBILE_EXPENSIFY_REPO) : []; + const noQAMobileExpensifyPRNumbers = Array.isArray(mobileExpensifyData) ? mobileExpensifyData.filter((PR) => /\[No\s?QA]/i.test(PR.title)).map((item) => item.number) : []; + console.log('Found the following NO QA Mobile-Expensify PRs:', noQAMobileExpensifyPRNumbers); const verifiedAppPRs = new Set([...verifiedPRList, ...noQAPRNumbers]); - const verifiedMobileExpensifyPRs = new Set(verifiedPRListMobileExpensify); + const verifiedMobileExpensifyPRs = new Set([...verifiedPRListMobileExpensify, ...noQAMobileExpensifyPRNumbers]); const resolvedInternalQAPRSet = new Set(resolvedInternalQAPRs); const resolvedDeployBlockerSet = new Set(resolvedDeployBlockers); const internalQAPRNumbers = new Set(internalQAPRMap.keys()); @@ -11956,11 +11959,11 @@ class GithubUtils { /** * Fetch all pull requests given a list of PR numbers. */ - static fetchAllPullRequests(pullRequestNumbers) { + static fetchAllPullRequests(pullRequestNumbers, repo = CONST_1.default.APP_REPO) { const oldestPR = pullRequestNumbers.sort((a, b) => a - b).at(0); return this.paginate(this.octokit.pulls.list, { owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, + repo, state: 'all', sort: 'created', direction: 'desc', diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/index.js b/.github/actions/javascript/markPullRequestsAsDeployed/index.js index 57d6fd19e092..3fbf1edac732 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/index.js +++ b/.github/actions/javascript/markPullRequestsAsDeployed/index.js @@ -13236,11 +13236,11 @@ class GithubUtils { /** * Fetch all pull requests given a list of PR numbers. */ - static fetchAllPullRequests(pullRequestNumbers) { + static fetchAllPullRequests(pullRequestNumbers, repo = CONST_1.default.APP_REPO) { const oldestPR = pullRequestNumbers.sort((a, b) => a - b).at(0); return this.paginate(this.octokit.pulls.list, { owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, + repo, state: 'all', sort: 'created', direction: 'desc', diff --git a/.github/actions/javascript/postTestBuildComment/index.js b/.github/actions/javascript/postTestBuildComment/index.js index cacbcdd353d2..ec3d7f41af67 100644 --- a/.github/actions/javascript/postTestBuildComment/index.js +++ b/.github/actions/javascript/postTestBuildComment/index.js @@ -11902,11 +11902,11 @@ class GithubUtils { /** * Fetch all pull requests given a list of PR numbers. */ - static fetchAllPullRequests(pullRequestNumbers) { + static fetchAllPullRequests(pullRequestNumbers, repo = CONST_1.default.APP_REPO) { const oldestPR = pullRequestNumbers.sort((a, b) => a - b).at(0); return this.paginate(this.octokit.pulls.list, { owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, + repo, state: 'all', sort: 'created', direction: 'desc', diff --git a/.github/actions/javascript/proposalPoliceComment/index.js b/.github/actions/javascript/proposalPoliceComment/index.js index 020a4ca6dab4..3b605b449d2d 100644 --- a/.github/actions/javascript/proposalPoliceComment/index.js +++ b/.github/actions/javascript/proposalPoliceComment/index.js @@ -12076,11 +12076,11 @@ class GithubUtils { /** * Fetch all pull requests given a list of PR numbers. */ - static fetchAllPullRequests(pullRequestNumbers) { + static fetchAllPullRequests(pullRequestNumbers, repo = CONST_1.default.APP_REPO) { const oldestPR = pullRequestNumbers.sort((a, b) => a - b).at(0); return this.paginate(this.octokit.pulls.list, { owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, + repo, state: 'all', sort: 'created', direction: 'desc', diff --git a/.github/actions/javascript/reopenIssueWithComment/index.js b/.github/actions/javascript/reopenIssueWithComment/index.js index 39520938469d..e594793e3ac5 100644 --- a/.github/actions/javascript/reopenIssueWithComment/index.js +++ b/.github/actions/javascript/reopenIssueWithComment/index.js @@ -11783,11 +11783,11 @@ class GithubUtils { /** * Fetch all pull requests given a list of PR numbers. */ - static fetchAllPullRequests(pullRequestNumbers) { + static fetchAllPullRequests(pullRequestNumbers, repo = CONST_1.default.APP_REPO) { const oldestPR = pullRequestNumbers.sort((a, b) => a - b).at(0); return this.paginate(this.octokit.pulls.list, { owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, + repo, state: 'all', sort: 'created', direction: 'desc', diff --git a/.github/actions/javascript/reviewerChecklist/index.js b/.github/actions/javascript/reviewerChecklist/index.js index 2dfb1fa8ef0f..ef2e0e4d5456 100644 --- a/.github/actions/javascript/reviewerChecklist/index.js +++ b/.github/actions/javascript/reviewerChecklist/index.js @@ -11875,11 +11875,11 @@ class GithubUtils { /** * Fetch all pull requests given a list of PR numbers. */ - static fetchAllPullRequests(pullRequestNumbers) { + static fetchAllPullRequests(pullRequestNumbers, repo = CONST_1.default.APP_REPO) { const oldestPR = pullRequestNumbers.sort((a, b) => a - b).at(0); return this.paginate(this.octokit.pulls.list, { owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, + repo, state: 'all', sort: 'created', direction: 'desc', diff --git a/.github/actions/javascript/verifySignedCommits/index.js b/.github/actions/javascript/verifySignedCommits/index.js index 8dde1a048b1c..6562860939cf 100644 --- a/.github/actions/javascript/verifySignedCommits/index.js +++ b/.github/actions/javascript/verifySignedCommits/index.js @@ -11815,11 +11815,11 @@ class GithubUtils { /** * Fetch all pull requests given a list of PR numbers. */ - static fetchAllPullRequests(pullRequestNumbers) { + static fetchAllPullRequests(pullRequestNumbers, repo = CONST_1.default.APP_REPO) { const oldestPR = pullRequestNumbers.sort((a, b) => a - b).at(0); return this.paginate(this.octokit.pulls.list, { owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, + repo, state: 'all', sort: 'created', direction: 'desc', diff --git a/.github/actions/javascript/waitForPreviousRuns/index.js b/.github/actions/javascript/waitForPreviousRuns/index.js index 5b333852e9ca..0ba892fca51f 100644 --- a/.github/actions/javascript/waitForPreviousRuns/index.js +++ b/.github/actions/javascript/waitForPreviousRuns/index.js @@ -11824,11 +11824,11 @@ class GithubUtils { /** * Fetch all pull requests given a list of PR numbers. */ - static fetchAllPullRequests(pullRequestNumbers) { + static fetchAllPullRequests(pullRequestNumbers, repo = CONST_1.default.APP_REPO) { const oldestPR = pullRequestNumbers.sort((a, b) => a - b).at(0); return this.paginate(this.octokit.pulls.list, { owner: CONST_1.default.GITHUB_OWNER, - repo: CONST_1.default.APP_REPO, + repo, state: 'all', sort: 'created', direction: 'desc', diff --git a/.github/libs/DeployChecklistUtils.ts b/.github/libs/DeployChecklistUtils.ts index 86c7353e9cea..c820cff0a051 100644 --- a/.github/libs/DeployChecklistUtils.ts +++ b/.github/libs/DeployChecklistUtils.ts @@ -172,8 +172,12 @@ async function generateDeployChecklistBodyAndAssignees({ const noQAPRNumbers = Array.isArray(data) ? data.filter((PR) => /\[No\s?QA]/i.test(PR.title)).map((item) => item.number) : []; console.log('Found the following NO QA PRs:', noQAPRNumbers); + const mobileExpensifyData = PRListMobileExpensify.length > 0 ? await GithubUtils.fetchAllPullRequests(PRListMobileExpensify, CONST.MOBILE_EXPENSIFY_REPO) : []; + const noQAMobileExpensifyPRNumbers = Array.isArray(mobileExpensifyData) ? mobileExpensifyData.filter((PR) => /\[No\s?QA]/i.test(PR.title)).map((item) => item.number) : []; + console.log('Found the following NO QA Mobile-Expensify PRs:', noQAMobileExpensifyPRNumbers); + const verifiedAppPRs = new Set([...verifiedPRList, ...noQAPRNumbers]); - const verifiedMobileExpensifyPRs = new Set(verifiedPRListMobileExpensify); + const verifiedMobileExpensifyPRs = new Set([...verifiedPRListMobileExpensify, ...noQAMobileExpensifyPRNumbers]); const resolvedInternalQAPRSet = new Set(resolvedInternalQAPRs); const resolvedDeployBlockerSet = new Set(resolvedDeployBlockers); diff --git a/.github/libs/GithubUtils.ts b/.github/libs/GithubUtils.ts index 37a5a6355e7e..541ef5f61740 100644 --- a/.github/libs/GithubUtils.ts +++ b/.github/libs/GithubUtils.ts @@ -133,13 +133,13 @@ class GithubUtils { /** * Fetch all pull requests given a list of PR numbers. */ - static fetchAllPullRequests(pullRequestNumbers: number[]): Promise { + static fetchAllPullRequests(pullRequestNumbers: number[], repo: string = CONST.APP_REPO): Promise { const oldestPR = pullRequestNumbers.sort((a, b) => a - b).at(0); return this.paginate( this.octokit.pulls.list, { owner: CONST.GITHUB_OWNER, - repo: CONST.APP_REPO, + repo, state: 'all', sort: 'created', direction: 'desc', diff --git a/.github/workflows/react-compiler-compliance.yml b/.github/workflows/react-compiler-compliance.yml index 06cbf075e871..13b38271e702 100644 --- a/.github/workflows/react-compiler-compliance.yml +++ b/.github/workflows/react-compiler-compliance.yml @@ -5,7 +5,7 @@ on: pull_request: types: [opened, synchronize] branches-ignore: [staging, production] - paths: ["**.tsx"] + paths: ["**.ts", "**.tsx"] concurrency: group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-react-compiler-compliance diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 9a079da783db..80fc2d0148bd 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -34,7 +34,7 @@ jobs: # - git diff is used to see the files that were added on this branch # - gh pr view is used to list files touched by this PR. Git diff may give false positives if the branch isn't up-to-date with main # - wc counts the words in the result of the intersection - count_new_js=$(comm -1 -2 <(git diff --name-only --diff-filter=A origin/main HEAD -- 'src/*.js' '__mocks__/*.js' '.storybook/*.js' 'assets/*.js' 'config/*.js' 'jest/*.js' 'scripts/*.js' 'tests/*.js' '.github/libs/*.js' '.github/scripts/*.js' ':!src/libs/SearchParser/*.js') <(gh pr view ${{ github.event.pull_request.number }} --json files | jq -r '.files | map(.path) | .[]') | wc -l) + count_new_js=$(comm -1 -2 <(git diff --name-only --diff-filter=A origin/main HEAD -- 'src/*.js' '__mocks__/*.js' '.storybook/*.js' 'assets/*.js' 'config/*.js' 'jest/*.js' 'scripts/*.js' 'tests/*.js' '.github/libs/*.js' '.github/scripts/*.js' ':!src/libs/SearchParser/*.js' ':!config/babel/*.js') <(gh pr view ${{ github.event.pull_request.number }} --json files | jq -r '.files | map(.path) | .[]') | wc -l) if [ "$count_new_js" -gt "0" ]; then echo "ERROR: Found new JavaScript files in the project; use TypeScript instead." exit 1 diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index ece74ca71aa1..19a350b23af6 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -17,9 +17,6 @@ import './fonts.css'; Onyx.init({ keys: ONYXKEYS, - initialKeyStates: { - [ONYXKEYS.NETWORK]: {isOffline: false}, - }, }); IntlStore.load(CONST.LOCALES.EN); diff --git a/CLAUDE.md b/CLAUDE.md index 56cbf058019e..c6856a4da3a4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -194,6 +194,7 @@ The skill provides guidance on: 1. **Prettier**: Run `npx prettier --write ` on every file you modified. This is mandatory - CI will reject unformatted code. 2. **ESLint**: Run `npx eslint --max-warnings=0` to catch lint errors early. 3. **TypeScript**: Run `npm run typecheck-tsgo` after changes that may affect typing (types, interfaces, or function signatures). It is ~10x faster and usually stricter than tsc. CI validates with `npm run typecheck` (tsc), which remains the required merge gate. +4. **React Compiler**: If you added new React components/hooks or modified existing ones, run `npm run react-compiler-compliance-check check-changed` to verify they compile with React Compiler. This applies the same rules as CI: new components/hooks must compile, and existing compiled files must not regress. See `contributingGuides/REACT_COMPILER.md` for details and common fixes. ### Testing - **Unit Tests**: Jest with React Native Testing Library diff --git a/android/app/build.gradle b/android/app/build.gradle index 056857951728..4df0a4688f56 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -111,8 +111,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009035903 - versionName "9.3.59-3" + versionCode 1009036001 + versionName "9.3.60-1" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/babel.config.js b/babel.config.js index 66094c956fac..a95bcc0f9742 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,10 +1,9 @@ require('dotenv').config(); +const BaseReactCompilerConfig = require('./config/babel/reactCompilerConfig'); + const ReactCompilerConfig = { - target: '19', - environment: { - enableTreatRefLikeIdentifiersAsRefs: true, - }, + ...BaseReactCompilerConfig, sources: (filename) => !filename.includes('tests/') && !filename.includes('node_modules/'), }; diff --git a/config/babel/reactCompilerConfig.js b/config/babel/reactCompilerConfig.js new file mode 100644 index 000000000000..7f084769d768 --- /dev/null +++ b/config/babel/reactCompilerConfig.js @@ -0,0 +1,16 @@ +/** + * Shared React Compiler configuration used across: + * - babel.config.js (build pipeline, extends with `sources` filter) + * - eslint-plugin-react-compiler-compat (lint-time analysis) + * - react-compiler-compliance-check (CI and local checking) + * + * Intentionally omits `sources` since that's only relevant for the Babel build pipeline. + */ +const ReactCompilerConfig = { + target: '19', + environment: { + enableTreatRefLikeIdentifiersAsRefs: true, + }, +}; + +module.exports = ReactCompilerConfig; diff --git a/contributingGuides/INTERACTION_MANAGER.md b/contributingGuides/INTERACTION_MANAGER.md new file mode 100644 index 000000000000..08008b0b42ad --- /dev/null +++ b/contributingGuides/INTERACTION_MANAGER.md @@ -0,0 +1,67 @@ +# InteractionManager Migration + +## Why + +`InteractionManager` is being removed from React Native. We currently maintain a patch to keep it working, but that's a temporary measure and upstream libraries will also drop support over time. + +Rather than keep patching, we're replacing `InteractionManager.runAfterInteractions` with purpose-built alternatives that are more precise. + +## Current state + +`runAfterInteractions` is used across the codebase for a wide range of reasons: waiting for navigation transitions, deferring work after modals close, managing input focus, delaying scroll operations, and many other cases that are hard to classify. + +## The problem + +`runAfterInteractions` is a global queue with no granularity. This made it a convenient catch-all, but the intent behind each call is often unclear. Many usages exist simply because it "just worked" as a timing workaround, not because it was the right tool for the job. + +This makes the migration non-trivial: you have to understand *what each call is actually waiting for* before you can pick the right replacement. + +## The approach + +**TransitionTracker** is the backbone. It tracks navigation transitions explicitly, so other APIs can hook into transition lifecycle without relying on a global queue. + +On top of TransitionTracker, existing APIs gain transition-aware callbacks: + +- Navigation methods accept `afterTransition` — a callback that runs after the triggered navigation transition completes +- Navigation methods accept `waitForTransition` — the call waits for all ongoing transitions to finish before navigating +- Keyboard methods accept `afterTransition` — a callback that runs after the keyboard transition completes +- `useConfirmModal` hook's `showConfirmModal` returns a Promise that resolves **after the modal close transition completes**, so any work awaited after it naturally runs post-transition — no explicit `afterTransition` callback needed + +This makes the code self-descriptive: instead of a generic `runAfterInteractions`, each call site says exactly what it's waiting for and why. + +> **Note:** `TransitionTracker.runAfterTransitions` is an internal primitive. Application code should use the higher-level APIs (`Navigation`, `useConfirmModal`, etc.) rather than importing TransitionTracker directly. + +## How +The migration is split into 9 issues. Current status of the migration can be found in the parent Github issue [here](https://github.com/Expensify/App/issues/71913). + +## Primitives comparison + +For reference, here's how the available timing primitives compare: + +### `requestAnimationFrame` (rAF) + +- Fires **before the next paint** (~16ms at 60fps) +- Guaranteed to run every frame if the thread isn't blocked +- Use for: UI updates that need to happen on the next frame (scroll, layout measurement, enabling a button after a state flush) + +### `requestIdleCallback` + +- Fires when the runtime has **idle time** — no pending frames, no urgent work +- May be delayed indefinitely if the main thread stays busy +- Accepts a `timeout` option to force execution after a deadline +- Use for: Non-urgent background work (Pusher subscriptions, search API calls, contact imports) + +### `InteractionManager.runAfterInteractions` (legacy — do not use) + +- React Native-specific. Fires after all **ongoing interactions** (animations, touches) complete +- Tracks interactions via `createInteractionHandle()` — anything that calls `handle.done()` unblocks the queue +- In practice, this means "run after the current navigation transition finishes" +- Problem: it's a global queue with no granularity — you can't say "after _this specific_ transition" + +### Summary + +| | Timing | Granularity | Platform | +| ---------------------- | ------------------------- | ------------------------- | --------------------- | +| `rAF` | Next frame (~16ms) | None — just "next paint" | Web + RN | +| `requestIdleCallback` | When idle (unpredictable) | None — "whenever free" | Web + RN (polyfilled) | +| `runAfterInteractions` | After animations finish | Global — all interactions | RN only | diff --git a/contributingGuides/NETWORK_STATE_DETECTION.md b/contributingGuides/NETWORK_STATE_DETECTION.md new file mode 100644 index 000000000000..0b91a1f69d82 --- /dev/null +++ b/contributingGuides/NETWORK_STATE_DETECTION.md @@ -0,0 +1,214 @@ +# Network State Detection + +## Overview + +The app uses a two-layer detection model to determine connectivity status. Each layer feeds into a central **hard stop** state machine that decides whether the app is offline. When a hard stop is active, the app pauses outgoing requests and shows the offline UI. Recovery relies on NetInfo's built-in reachability polling (`isInternetReachable`) and the FailureTracker detecting a successful request. Once connectivity is confirmed, the app clears the hard stop and reconnects. + +## Architecture Diagram + +``` +┌──────────────────────┐ +│ Layer 1: OS Radio │ NetInfo isConnected +│ (NetworkState) │──────────────────────────┐ +└──────────────────────┘ │ + ▼ + ┌──────────────────┐ +┌──────────────────────┐ │ │ +│ Layer 2: Sustained │ FailureTracker│ NetworkState │──── isOffline ──▶ listeners (UI, SQ) +│ Failures │───────────────▶│ (hard stop) │ +│ (FailureTracking MW) │ │ │ +└──────────────────────┘ └──────────────────┘ + ▲ + │ onReachabilityRestored + │ + ┌──────────────────┐ + │ NetInfo listener │──── isInternetReachable + │ (reachability │ transitions →true + │ polling) │ + └──────────────────┘ + │ + ▼ + ┌──────────────────┐ + │ Reconnect │──── openApp / reconnectApp + │ │──── flush SequentialQueue + └──────────────────┘ +``` + +## The Hard Stop Model + +A **hard stop** means the app considers itself offline. When at least one trigger is active, the hard stop is ON. When all triggers are cleared, the hard stop is OFF. + +Five triggers can activate a hard stop: + +| Trigger | Source | Meaning | +|---|---|---| +| `noRadioActive` | OS radio detection | Device has no network interface (airplane mode, WiFi off) | +| `internetUnreachable` | NetInfo reachability polling | `api/Ping` failed — server unreachable despite connected radio | +| `sustainedFailuresActive` | Failure tracker | Requests have been failing consistently | +| `shouldForceOffline` | Debug tool | Manually forced offline via TestToolMenu | +| `simulatedOffline` | Test tool | Poor connection simulator toggling offline randomly | + +When a hard stop activates: +1. `NetworkState` notifies all subscribers (via `subscribe()`) +2. `SequentialQueue` guards against processing while offline (reads `getIsOffline()` synchronously) +3. Components using `useNetwork()` re-render with `isOffline: true` + +When the hard stop clears: +1. `NetworkState` notifies all subscribers +2. `Reconnect.ts` detects the offline→online transition and calls `SequentialQueue.flush()` +3. Components using `useNetwork()` re-render with `isOffline: false` + +## Layer 1: OS Radio Detection + +**File:** `src/libs/NetworkState.ts` + +This layer uses `@react-native-community/netinfo` to detect whether the device has an active network interface. The NetInfo listener lives inside `NetworkState.ts` — there is no separate module. + +- A module-level Onyx connection to `SESSION` triggers `configureAndSubscribe()` whenever accountID changes +- The NetInfo listener reads `state.isConnected` and calls `setHasRadio()` internally +- When `isConnected` is `false` → `setHasRadio(false)` → activates the `noRadioActive` hard stop +- When `isConnected` returns to `true` → `setHasRadio(true)` → clears the `noRadioActive` hard stop +- Detects: airplane mode, WiFi disabled, no cellular signal +- Does **not** determine actual server reachability — a device can be connected to WiFi but have no internet + +### Reachability tracking + +The NetInfo listener tracks `isInternetReachable` transitions in both directions: + +- **any non-`false` → `false`** (`true→false`, `null→false`, `undefined→false`) — `api/Ping` failed. Sets the `internetUnreachable` hard stop. Only `false→false` is skipped (already offline). This covers cold start with no internet (`null→false` after the initial indeterminate event), post-recovery resets, and normal online→offline transitions. Without this, an idle user would never see the offline indicator because no API requests are failing. +- **`false` → `true`** and **`null` → `true`** — `api/Ping` succeeded after a previous failure. Triggers `onReachabilityRestored()` which clears all hard stops and fires reconnect listeners. +- **`undefined` → `true`** — the initial event on subscribe, delivering current state. This is **not** treated as a recovery to prevent duplicate `openApp()`/`reconnectApp()` calls on boot. + +**Platform behavior:** + +We configure `useNativeReachability: false` so that NetInfo uses JS fetch polling (`api/Ping`) on **all platforms** instead of trusting native OS reachability. This aligns behavior across web and mobile. NetInfo's default polling intervals apply (60s when reachable, 5s when unreachable). + +### Why `useNativeReachability: false` is required + +With `useNativeReachability: false`, NetInfo determines `isInternetReachable` by polling `api/Ping` via a real `fetch()` request — making it a genuine request outcome, consistent with the design principle that "request outcomes are the authority." + +If `useNativeReachability` were set to `true`, NetInfo would use native OS reachability heuristics instead of JS polling. The `isInternetReachable` value would no longer represent a real request outcome and should NOT be used as an offline trigger. The `internetUnreachable` hard stop depends on this configuration. + +How `isConnected` vs `isInternetReachable` are determined per platform (with `useNativeReachability: false`): + +| Platform | `isConnected` source | `isInternetReachable` source | +|---|---|---| +| Web | `navigator.onLine` | JS `fetch(api/Ping)` | +| iOS | `SCNetworkReachability` flags | JS `fetch(api/Ping)` | +| Android | `NetworkCapabilities` transport type | JS `fetch(api/Ping)` | + +Note: Android's native module computes its own `isInternetReachable` via `NET_CAPABILITY_VALIDATED`, but the JS `InternetReachability.update()` gate discards it when `useNativeReachability: false`. iOS doesn't send `isInternetReachable` from native at all. See the NetInfo source files `internetReachability.ts`, `nativeModule.web.ts`, `ConnectivityReceiver.java`, and `RNCNetInfo.mm` for implementation details. + +## Layer 2: Sustained Failure Detection + +**Files:** `src/libs/FailureTracker.ts`, `src/libs/Middleware/FailureTracking.ts` + +This layer detects connectivity loss through request outcomes, catching cases where the OS reports a connection but the server is unreachable (e.g., captive portal, DNS failure, server outage). + +### FailureTracking Middleware + +The middleware observes every API response: + +- Any resolved response (server responded at all) → calls `recordSuccess()` +- `FAILED_TO_FETCH` error → calls `recordFailure()` (DNS failure, no internet, network timeout) +- `EXPENSIFY_SERVICE_INTERRUPTED` error → calls `recordFailure()` (server down: 500/502/504/520) +- All other errors (4xx, throttling, etc.) → **not tracked** as connectivity failures + +### FailureTracker + +Counts consecutive failures and applies a dual threshold: + +1. **Count threshold:** at least `SUSTAINED_FAILURE_THRESHOLD_COUNT` failures (default: 3) +2. **Time threshold:** at least `SUSTAINED_FAILURE_WINDOW_MS` elapsed since the first failure (default: 10s) + +Both thresholds must be met simultaneously to trigger a sustained failure hard stop. This prevents brief transient errors from being misidentified as connectivity loss. + +One successful request resets everything — it proves the server is reachable and clears the `sustainedFailuresActive` flag. + +## Central State Machine + +**File:** `src/libs/NetworkState.ts` + +`NetworkState` is the single source of truth for offline status. It holds module-level boolean flags and uses a subscriber pattern — components use `useNetwork()` (backed by `useSyncExternalStore`) and `SequentialQueue` subscribes via `subscribe()`. Because the state is module-level (not persisted in Onyx), each browser tab detects connectivity independently. This is intentional — each tab has its own network conditions and should evaluate them on its own. + +``` +hasRadio — set by NetInfo listener (Layer 1), inverted: !hasRadio = noRadio hard stop +internetUnreachable — set by NetInfo listener when isInternetReachable transitions to false (any non-false→false) +sustainedFailuresActive — set by FailureTracker (Layer 2) +shouldForceOffline — set by debug tools (Onyx NETWORK key) +simulatedOffline — set by poor connection simulator +``` + +### Core logic + +`getIsOffline()` derives the offline status: + +```typescript +const offline = !hasRadio || internetUnreachable || sustainedFailuresActive || shouldForceOffline || simulatedOffline; +``` + +`updateState()` notifies all subscribers when the state changes. `Reconnect.ts` subscribes and calls `SequentialQueue.flush()` on offline→online transitions. `SequentialQueue` reads `getIsOffline()` synchronously for its guard checks but does not own the transition subscription. + +### Recovery flow + +`onReachabilityRestored()`: +1. Sets `hasRadio = true` and `sustainedFailuresActive = false`, resets FailureTracker counters +2. Calls `updateState()` (which notifies subscribers and clears the hard stop) +3. Calls `notifyReconnectListeners()` — `Reconnect.ts` subscribes to this and triggers app data sync + +### App foreground handling + +`Reconnect.ts` registers an `AppStateMonitor.addBecameActiveListener` callback: +- If in hard stop → calls `NetworkState.refresh()` which triggers `NetInfo.refresh()` (bypasses stale `isInternetReachable` cache, see NetInfo issue #326) +- Always → calls `reconnect()` to catch up on missed Pusher events + +## Recovery & Reconnect + +**File:** `src/libs/actions/Reconnect.ts` + +Subscribes to `NetworkState.onReachabilityConfirmed()` and `AppStateMonitor.addBecameActiveListener()`. Handles data synchronization: + +- Skips reconnection if no active session (`currentAccountID` is undefined) +- If `isLoadingApp` is true → calls `App.openApp()` (full initial load) +- Otherwise → calls `App.reconnectApp(lastUpdateIDAppliedToClient)` (incremental sync) +- Flushes `SequentialQueue` to send any pending write requests + +## Thundering Herd Protection + +When the server recovers after an outage, many clients detect reachability at roughly the same time. The queue does not add an artificial delay before flushing because the architecture has three layers of natural backoff: + +1. **Polling jitter** — each client's NetInfo polls `api/Ping` on its own 5-second cycle, so clients discover recovery at different times (up to 5 seconds of natural spread). +2. **Per-request exponential backoff** — if the server is still overloaded when a client flushes, `RequestThrottle` applies jittered exponential backoff (10–100 ms initial, doubling up to 30 s cap) on each failed request. +3. **Re-triggering hard stop** — if enough requests fail after recovery (3 failures over 10 seconds), `FailureTracker` puts the client back into a hard stop, preventing it from hammering the server further. + +## Configuration Constants + +All values are defined in `src/CONST/index.ts` under `CONST.NETWORK`: + +| Constant | Value | Description | +|---|---|---| +| `SUSTAINED_FAILURE_THRESHOLD_COUNT` | `3` | Minimum failures before triggering sustained failure hard stop | +| `SUSTAINED_FAILURE_WINDOW_MS` | `10000` (10s) | Minimum elapsed time from first failure to trigger hard stop | + +## Debug Tools + +Two debug options are available via the TestToolMenu (accessible in dev builds): + +- **`shouldForceOffline`**: Forces the app into a hard stop. Flows through Onyx → `NetworkState.setForceOffline()`. Useful for testing offline UX patterns. +- **`shouldSimulatePoorConnection`**: Randomly toggles the app between online and offline every 2–5 seconds. Handled in `NetworkState.ts`. Useful for testing flaky network behavior. + +## Key Files Reference + +| File | Role | +|---|---| +| `src/libs/NetworkState.ts` | Central hard stop state machine, NetInfo configuration/subscription, OS radio detection, and reachability tracking | +| `src/libs/FailureTracker.ts` | Counts failures, triggers sustained failure hard stop via listener pattern | +| `src/libs/Middleware/FailureTracking.ts` | Middleware that observes request outcomes and feeds FailureTracker | +| `src/libs/actions/Reconnect.ts` | Subscribes to reachability + foreground events, syncs app data after recovery | +| `src/libs/Network/SequentialQueue.ts` | Write request queue, reads `getIsOffline()` synchronously for guard checks | +| `src/libs/actions/Network.ts` | Onyx actions for debug flags (forceOffline, simulatePoorConnection) | +| `src/hooks/useNetwork.ts` | Hook for components — uses `useSyncExternalStore` with `NetworkState.subscribe()` | + +## Relationship to Offline UX Patterns + +This document covers **how** the app detects connectivity changes and determines offline status. For **how features respond** to being offline (optimistic updates, blocking forms, full-page blocking), see [Offline UX Patterns](philosophies/OFFLINE.md). diff --git a/contributingGuides/REACT_COMPILER.md b/contributingGuides/REACT_COMPILER.md index 997b07d04dae..0f479e840b86 100644 --- a/contributingGuides/REACT_COMPILER.md +++ b/contributingGuides/REACT_COMPILER.md @@ -4,86 +4,55 @@ [React Compiler](https://react.dev/learn/react-compiler) is a tool designed to enhance the performance of React applications by automatically memoizing components that lack optimizations. -At Expensify, we are early adopters of this tool and aim to fully leverage its capabilities. +React Compiler is enabled in both the webpack (web) and metro (mobile) build pipelines via `babel-plugin-react-compiler`. -## React Compiler compliance checker +## React Compiler CI check -We provide a script, `scripts/react-compiler-compliance-check.ts`, which checks for "Rules of React" compliance locally and enforces these in PRs adding or changing React code through a CI check. +A CI check runs on every PR that modifies `.ts` or `.tsx` files. It uses `@babel/core`'s `transformSync` with `babel-plugin-react-compiler` to check whether each changed file's components and hooks compile successfully. -### What it does +### What the CI check enforces -Runs `react-compiler-healthcheck` in verbose mode, parses output, and summarizes which files compiled and which failed, including file, line, column, and reason. It can: +The check enforces two rules: -- Check all files or a specific file/glob -- Check only files changed relative to a base branch -- Optionally generate a machine-readable report `react-compiler-report.json` -- Exit with non-zero code when failures are found (useful for CI) +1. **New files must compile.** If you add a new file containing React components or hooks, they must all compile with React Compiler. This prevents new Rules of React violations from entering the codebase. +2. **No regressions.** If a file already compiles on `main` and your PR breaks that, the check fails. This prevents introducing violations into files that were previously compliant. -### Usage +The check does **not** fail if: +- A file has no React components or hooks (utilities, types, constants are silently skipped) +- A file was already failing to compile on `main` and still fails on your branch (not a regression) -> [!NOTE] -> This script uses `origin` as the base remote by default. If your GH remote is named differently, use the `--remote ` flag. +### What to do when the check fails -#### Check entire codebase or a specific file/glob +The CI output shows which files failed, with the compiler error reason, file path, and line number for each failure. Look for lines like: -```bash -npm run react-compiler-compliance-check check # Check all files -npm run react-compiler-compliance-check check src/path/Component.tsx # Check specific file -npm run react-compiler-compliance-check check "src/**/*.tsx" # Check glob pattern ``` - -#### Check only changed files (against main) - -```bash -npm run react-compiler-compliance-check check-changed +FAILED src/components/MyComponent.tsx (new file must compile) + src/components/MyComponent.tsx:42:8: Hooks must always be called in a consistent order... ``` -#### Generate a detailed report (saved as `./react-compiler-report.json`) - -You can use the `--report` flag with both of the above commands: - -```bash -npm run react-compiler-compliance-check check --report -npm run react-compiler-compliance-check check-changed --report -``` - -#### Additional flags - -**Filter by diff changes (`--filterByDiff`)** +The error messages come directly from React Compiler and describe which [Rule of React](https://react.dev/reference/rules) was violated. See the ["How to fix a particular problem?"](#how-to-fix-a-particular-problem) section below for common fixes. -Only check files that have been modified in the current diff. This is useful when you want to focus on files that have actual changes: +### Local usage -```bash -npm run react-compiler-compliance-check check --filterByDiff -npm run react-compiler-compliance-check check-changed --filterByDiff -``` - -**Print successful compilations (`--printSuccesses`)** - -By default, the script only shows compilation failures. Use this flag to also display files that compiled successfully: +You can run the same check locally before pushing: ```bash -npm run react-compiler-compliance-check check --printSuccesses -npm run react-compiler-compliance-check check-changed --printSuccesses -``` - -**Custom report filename (`--reportFileName`)** +# Check specific files, directories, or glob patterns +npm run react-compiler-compliance-check check src/components/Foo.tsx +npm run react-compiler-compliance-check check src/components/ +npm run react-compiler-compliance-check check "src/hooks/**/*.ts" -Specify a custom filename for the generated report instead of the default `react-compiler-report.json`: +# Check only files changed relative to main (same as CI) +npm run react-compiler-compliance-check check-changed -```bash -npm run react-compiler-compliance-check check --report --reportFileName my-custom-report.json -npm run react-compiler-compliance-check check-changed --report --reportFileName my-custom-report.json +# Show detailed output including files that compiled or were skipped +npm run react-compiler-compliance-check check --verbose src/components/ ``` -**Custom remote name (`--remote`)** - -By default, the script uses `origin` as the base remote. If your GitHub remote is named differently, specify it with this flag: +#### Flags -```bash -npm run react-compiler-compliance-check check-changed --remote upstream -npm run react-compiler-compliance-check check --filterByDiff --remote my-remote -``` +- `--verbose` -- Show detailed output including skipped files and files that compiled successfully. +- `--remote ` -- Git remote name for the base branch (default: `origin`). ## How to fix a particular problem? diff --git a/contributingGuides/philosophies/OFFLINE.md b/contributingGuides/philosophies/OFFLINE.md index f984f85c016c..740e94a706cb 100644 --- a/contributingGuides/philosophies/OFFLINE.md +++ b/contributingGuides/philosophies/OFFLINE.md @@ -11,6 +11,10 @@ * [UX Pattern Flowchart](#ux-pattern-flow-chart) - [Answering Questions on the Flow Chart](#answering-questions-on-the-flow-chart) +## How Offline State Is Detected + +This document covers UX patterns for handling offline state. For the underlying architecture of how the app detects connectivity changes (hard stop model, failure tracking, recovery probes), see [Network State Detection](../NETWORK_STATE_DETECTION.md). + ## Motivation & Philosophy Understanding the offline behavior of our app is vital to becoming a productive contributor to the Expensify codebase. Our mission is to support our users in every possible environment, and often our app is used in places where a stable internet connection is not guaranteed. diff --git a/cspell.json b/cspell.json index ac25b59fc7cd..18cb700e9eb2 100644 --- a/cspell.json +++ b/cspell.json @@ -218,6 +218,7 @@ "endcapture", "enddate", "endfor", + "endgroup", "enroute", "entityid", "Entra", @@ -648,6 +649,7 @@ "RPID", "RRGGBB", "rstrip", + "recyclerview", "RTER", "s3uqn2oe4m85tufi6mqflbfbuajrm2i3", "SAASPASS", diff --git a/docs/articles/new-expensify/connect-credit-cards/Configure-Company-Card-Settings.md b/docs/articles/new-expensify/connect-credit-cards/Configure-Company-Card-Settings.md index 13b285bf7966..569ce2a49aef 100644 --- a/docs/articles/new-expensify/connect-credit-cards/Configure-Company-Card-Settings.md +++ b/docs/articles/new-expensify/connect-credit-cards/Configure-Company-Card-Settings.md @@ -31,6 +31,9 @@ You can manage the following options: **Note:** This setting only applies to transactions imported after the setting is enabled. +- **Assign new cards** (direct feeds only) + Reconnects to your bank to refresh the card list so newly issued cards appear in the assignment UI. + - **Remove card feed** Remove the card feed from the workspace and unassign all cards. If the feed is not connected to another workspace, it is permanently deleted. diff --git a/docs/articles/new-expensify/connect-credit-cards/Connect-Personal-Cards.md b/docs/articles/new-expensify/connect-credit-cards/Connect-Personal-Cards.md new file mode 100644 index 000000000000..bba95525d299 --- /dev/null +++ b/docs/articles/new-expensify/connect-credit-cards/Connect-Personal-Cards.md @@ -0,0 +1,66 @@ +--- +title: Connect a personal card +description: Learn how to connect a new personal credit card to your Expensify account to automatically import transactions. +keywords: [New Expensify, personal cards, connect card, Wallet, Plaid, bank connection, card import, credit card, New Expensify, oAuth, add card] +internalScope: Audience is members who want to connect a new personal credit card. Covers the card connection flow including country selection, bank selection, oAuth and Plaid connections, company card warnings, and free card limits. Does not cover managing existing cards or company card programs. +--- + +# Connect a personal card + +Connect your personal credit card to Expensify to automatically import transactions, eliminating the need to manually create expenses or scan receipts. Expensify supports over 10,000 banks across the United States, Canada, the United Kingdom, and the European Union. + +--- + +## Who can connect a personal card + +Anyone with an Expensify account can connect up to two personal cards for free. To connect more than two personal cards, you'll need a paid workspace (Collect plan or above). + +--- + +## How to connect a personal card + +1. Click the navigation tabs (on the left on web, on the bottom on mobile), then go to **Account > Wallet**. +2. Tap **Add personal card**. +3. Select your country from the country selector. +4. If you selected **United States**, choose your bank from the bank selector. + - If your bank is listed, you'll be prompted to connect via your bank's secure login (oAuth). + - If your bank is not listed, select **Other** to connect via Plaid. +5. Follow the prompts to log into your bank and authorize the connection. + +Once your card is connected, it will appear under **Assigned cards** in the **Wallet**, and transactions will begin importing automatically. + +--- + +## What to do if you see a company card warning + +If you're a member of a Workspace or domain that has a company card feed, Expensify will ask you to confirm that the card you're adding is a personal card. + +- **If you're an employee:** You'll see options to ask your admin if you should be assigned a company card, or continue adding a personal card. +- **If you're an admin:** You'll see options to confirm this is a company card (which redirects you to the company card connection flow) or proceed with adding it as a personal card. + +--- + +## What happens when you reach the free card limit + +If you're not on a paid workspace and try to add more than two personal cards, you'll see an upgrade page prompting you to create a Collect workspace. + +If you proceed with the upgrade: + +1. A Collect workspace is created for your account. +2. You'll see a success page with options to add company cards or additional personal cards. + +--- + +# FAQ + +## How can I check if my bank or card issuer is supported? + +Expensify supports most major banks and card issuers worldwide. [Learn how to check which banks are supported in Expensify](/articles/new-expensify/connect-credit-cards/Check-Supported-Banks) + +## How many personal cards can I connect for free? + +You can connect up to two personal cards for free. To add more, you'll need a paid workspace on the Collect plan or above. + +## How do I manage a personal card after connecting it? + +Once connected, your card appears in **Account > Wallet** under **Assigned cards**. [Learn how to manage your personal cards](/articles/new-expensify/connect-credit-cards/Personal-Cards). diff --git a/docs/articles/new-expensify/connect-credit-cards/Import-Personal-Card-Transactions-From-a-Spreadsheet.md b/docs/articles/new-expensify/connect-credit-cards/Import-Personal-Card-Transactions-From-a-Spreadsheet.md new file mode 100644 index 000000000000..6162db1c885c --- /dev/null +++ b/docs/articles/new-expensify/connect-credit-cards/Import-Personal-Card-Transactions-From-a-Spreadsheet.md @@ -0,0 +1,70 @@ +--- +title: Import personal card transactions from a spreadsheet +description: Learn how Members can manually import personal card transactions using a spreadsheet in Wallet. +keywords: [New Expensify, import personal card, upload file, import spreadsheet, CSV, TXT, XLS, XLSX, Wallet, card feed, reimbursable, bring your own card, BYOC] +internalScope: Audience is all members. Covers how to import, update, and delete personal card transactions via CSV. Does not cover company cards or Plaid connections. +--- + +# Import personal card transactions from a spreadsheet + +If your bank isn't supported by a direct connection, you can still import personal card transactions into Expensify using a spreadsheet file. This allows you to track and submit expenses without connecting your bank account. + +If your bank is supported, you can connect your account to automatically import transactions. [Learn how to manage personal cards](/articles/new-expensify/connect-credit-cards/Manage-Personal-Cards). + +--- + +## Who can import personal card transactions from a spreadsheet + +Anyone can import personal card transactions using a spreadsheet file in their account. + +--- + +## How to import personal card transactions from a spreadsheet + +1. In the navigation tabs (on the left on web, on the bottom on mobile) select **Wallet**. +2. In the **Assigned Cards** section, choose **Import transactions**. +3. Enter a display name and configure the currency, reimbursable state, and amount sign direction. +3. Enter a display name and configure the currency, reimbursable state, and amount sign direction. +5. Map your spreadsheet columns to the required fields (**Date**, **Merchant**, **Amount**). +6. Click **Import**. + +--- + +## What happens after you import personal card transactions from a spreadsheet + +- Imported transactions appear as **Unreported** expenses. +- You can edit, categorize, and submit these expenses on a report. +- Imported transactions are available on both web and mobile. + +[Learn how to create and submit a report](/articles/new-expensify/reports-and-expenses/Create-and-Submit-Reports). + +--- + +# FAQ + +## What spreadsheet file formats are accepted for personal card imports? + +You can import the following file types: +- .CSV +- .TXT +- .XLS +- .XLSX + +## What columns are required for personal card spreadsheet imports? + +Your file must include at least the following columns: +- Date +- Amount +- Merchant + +## What happens if I use the same column twice when mapping fields? + +You’ll see an error message and won’t be able to proceed until the issue is resolved. + +## Can I import additional transactions to the same card? + +Yes. You can simply repeat the import process. + +## Can I delete personal card spreadsheet imports? + +You cannot delete the imported file, but you can delete the individual expenses that were created. [Learn how to delete expenses](/articles/new-expensify/reports-and-expenses/How-to-Delete-Expenses). diff --git a/docs/articles/new-expensify/connect-credit-cards/Manage-Personal-Cards.md b/docs/articles/new-expensify/connect-credit-cards/Manage-Personal-Cards.md index 35a4ff4fd172..6ee204c53804 100644 --- a/docs/articles/new-expensify/connect-credit-cards/Manage-Personal-Cards.md +++ b/docs/articles/new-expensify/connect-credit-cards/Manage-Personal-Cards.md @@ -7,15 +7,15 @@ internalScope: Audience is members with personal credit cards already connected # Manage Personal Cards -Expensify lets you view and manage your personal credit cards in one place, making it easier to track spending, submit reimbursable expenses, and stay organized for tax time. +Expensify lets you manage your personal credit cards in one place using **Wallet**. From there, you can review card details, track spending, and use transactions for expense reporting. -If you previously connected a personal card in Expensify Classic, you can now manage that card directly in Expensify on both web and mobile. +If you don’t have a card connected yet, [learn how to connect a personal card](/articles/new-expensify/connect-credit-cards/Connect-Personal-Cards). --- ## Who can manage personal cards -You can manage personal cards if you have a personal credit card that was already connected to your account. You can manage these cards on both web and mobile, alongside any company cards on your account. +Anyone with a personal credit card connected to their Expensify account can manage it in **Wallet**. --- @@ -23,25 +23,27 @@ You can manage personal cards if you have a personal credit card that was alread 1. Click the navigation tabs (on the left on web, on the bottom on mobile), then select **Account > Wallet**. 2. Under **Assigned cards**: - - Personal cards imported from Expensify Classic will appear here. + - Personal cards will appear in the **Personal** section. - You'll see the card name, bank icon, and last 4 digits. -3. Tap the personal card you want to manage. If you have both company and personal cards, you'll see them separated into **Company cards** and **Personal cards** sections. -![Tap a personal card to open card details]({{site.url}}/assets/images/personal-card-01.png){:width="100%"} +![Tap a personal card to open card details]({{site.url}}/assets/images/ExpensifyHelp-FixPersonalCards.png){:width="100%"} --- -## What you can do with personal cards +## What you can manage on a personal card in Wallet -You can: -- View all personal cards imported from Expensify Classic -- Automatically import transactions from your linked personal cards -- Generate IRS-compliant eReceipts for eligible USD purchases -- Update personal card settings -- Fix broken card connections -- Filter expenses by card in the **Expenses** tab on the **Reports** page +You can manage the following settings for a personal card: + +- Rename the card with a custom nickname +- Turn on **Mark transactions as reimbursable** for imported transactions +- View the last successful transaction import +- Update the transaction start date for imports +- View imported transactions from the card +- Manually refresh the card to import recent transactions +- Fix connection issues if the card stops syncing +- Remove the card from your account --- @@ -53,10 +55,9 @@ You can: - Rename the card - Update the card to pull in new transactions (if not CSV-imported) - Unassign the card if it's no longer needed - - Toggle **Mark transactions as reimbursable** - - The reimbursable setting applies only to **new** transactions and is turned on by default. + - Toggle **Mark transactions as reimbursable** to mark future transactions as reimbursable. -**Note:** Unassigning a personal card permanently deletes any unreported expenses or expenses on draft reports from that card. +**Note:** Removing a personal card permanently deletes any unreported expenses or expenses on draft reports from that card. ![Update personal card settings from the card details page]({{site.url}}/assets/images/personal-card-02.png){:width="100%"} @@ -112,9 +113,6 @@ You can tap the hyperlinked text in the violation to go directly to the **Card d # FAQ -## Can I add another personal card? - -Not yet. Only cards connected to your account in Expensify Classic are available to manage in New Expensify. Support for adding new personal cards will be available in a future update. ## Can I change the reimbursable setting for past transactions? diff --git a/docs/articles/new-expensify/connect-credit-cards/Set-up-a-Direct-Company-Card-Feed-Connection.md b/docs/articles/new-expensify/connect-credit-cards/Set-up-a-Direct-Company-Card-Feed-Connection.md index e178dc196a16..2ff2b8877be2 100644 --- a/docs/articles/new-expensify/connect-credit-cards/Set-up-a-Direct-Company-Card-Feed-Connection.md +++ b/docs/articles/new-expensify/connect-credit-cards/Set-up-a-Direct-Company-Card-Feed-Connection.md @@ -61,6 +61,17 @@ Some banks have specific requirements for successful connections: # FAQ +## Why don't newly issued cards appear in the assignment list? + +Direct feeds cache the card list when first connected. If your bank issues new cards after the initial setup, they won't appear automatically. To refresh the card list: + +1. Go to **Workspaces > [Workspace Name] > Company cards**. +2. Click **Settings** in the top-right corner. +3. Click **Assign new cards**. +4. Complete the bank re-authentication to pull the latest cards from your bank. + +New cards will then be available for assignment. + ## How do I fix a broken company card feed connection? If your company card feed is broken, you can fix it from the **Time Sensitive** section on **Home**. Click **Fix** to restore the connection. [Learn how to fix a broken company card feed connection](/articles/new-expensify/connect-credit-cards/Fix-a-broken-Company-Card-Feed-Connection). diff --git a/docs/articles/new-expensify/getting-started/Expensify-Home-Overview.md b/docs/articles/new-expensify/getting-started/Expensify-Home-Overview.md index fded0c08ce86..797ad65da594 100644 --- a/docs/articles/new-expensify/getting-started/Expensify-Home-Overview.md +++ b/docs/articles/new-expensify/getting-started/Expensify-Home-Overview.md @@ -33,7 +33,7 @@ Home includes: - **Time-sensitive alerts** (when applicable) - **For you** - **Spend over time** (when applicable) -- **Discover** +- **Discover** (when applicable) - **Announcements** - **Assigned cards** (when applicable) @@ -85,6 +85,8 @@ The **Discover** section helps you get familiar with things you can do in Expens This section includes a short demo that introduces key areas of the app and shows how different parts fit together. This can be helpful when you’re getting started or returning after some time away. +Once you’ve watched the demo, the **Discover** section is automatically hidden from Home. + --- ## How the Announcements section works on Home @@ -129,6 +131,10 @@ The **Time-sensitive** section only appears when there is an urgent issue or lim The **Spend over time** section only appears if you are a Workspace Admin, Auditor, or approver on a paid workspace that has existing transactions. If you don't hold one of these roles, or your workspace has no transactions yet, this section won't be visible. +## Why don’t I see the Discover section? + +The **Discover** section only appears until you’ve watched the demo. Once you’ve completed it, the section is automatically hidden from Home. + ## Why don’t I see the Assigned cards section? The **Assigned cards** section only appears if you have an active **Expensify Card** assigned to you. diff --git a/docs/articles/new-expensify/reports-and-expenses/Approve-Expenses.md b/docs/articles/new-expensify/reports-and-expenses/Approve-Expenses.md index 499c39696978..d63e3ddc054e 100644 --- a/docs/articles/new-expensify/reports-and-expenses/Approve-Expenses.md +++ b/docs/articles/new-expensify/reports-and-expenses/Approve-Expenses.md @@ -1,127 +1,148 @@ --- title: Approve Expenses -description: Approve, hold, and unapprove submitted expenses -keywords: [New Expensify, approve expenses, hold expenses, unapprove report, workspace approval workflow, expense approval, reimburse expenses, pending expense, Expensify Card, expense status, expense settings] +description: Approve, hold, reject, and unapprove submitted expenses +keywords: [New Expensify, approve expenses, hold expenses, unapprove report, reject report, reject expense, workspace approval workflow, expense approval, reimburse expenses, pending expense, Expensify Card, expense status, expense settings] +internalScope: Audience is Workspace approvers and admins. Covers approving and managing submitted expenses and reports. Does not cover creating or submitting expenses. --- -Expenses can be created through manual entry, tracking distance, or scanning a receipt. They can be submitted to an individual or to a workspace. For steps on creating and submitting expenses, check out [Create an Expense](https://help.expensify.com/articles/new-expensify/expenses-and-payments/Create-an-expense). +# Approve Expenses ---- - -# Expenses Sent to an Individual (Not a Workspace) +When an expense report is submitted on a Workspace with an approval workflow configured, it must be approved before it can be paid. To set an approval workflow on your Workspace, [learn how to add approvals](/articles/new-expensify/workspaces/Add-Approvals). -When an expense is submitted to an individual (rather than on a workspace), it won't go through an approval process. It only needs to be paid. See [Pay an Expense](https://help.expensify.com/articles/new-expensify/expenses-and-payments/Pay-an-expense) for the payment steps. +When an expense report is submitted to an individual (rather than on a workspace), it won't go through an approval process. It only needs to be paid. [Learn how to pay an expense](/articles/new-expensify/expenses-and-payments/Pay-an-expense). --- -# Expenses Submitted on a Workspace +## What actions can be taken on expenses and reports submitted on a Workspace -When an expense is submitted on a workspace with an approval workflow configured, it must be approved before it can be paid. +On a submitted report, the approver can: -**For each expense report, you can:** - -- **Approve**: Confirm the expense is valid and ready to pay. +- **Approve**: Confirm the report is valid and ready to pay. - **Unapprove**: Return the report to its previous state for additional edits. +- **Reject**: Return the report to the submitter or a previous approver. -**For each expense on a report, you can:** -- **Hold**: Temporarily delay approval of the individual expense if more information is needed. -- **Reject**: Remove the expense from the report and send back to the submitter with a reason. The rejected expense can be marked as resolved and resubmitted by the submitter. +On any expense submitted on a report, the approver can: -**To set up an approval workflow:** +- **Hold**: Temporarily delay approval of the individual expense if more information is needed. +- **Reject**: Remove the expense from the report and send it back to the submitter with a reason. The rejected expense can be marked as resolved and resubmitted. -1. In the navigation tabs (on the left on web, and at the bottom on mobile), go to **Workspaces > [Workspace Name] > Workflows**. -2. Create a custom workflow to route expenses to the appropriate approvers. +**Note:** On reports with only one expense, you cannot reject a single expense. Instead, the entire report must be rejected. --- -# Approve an Expense +## How to review and approve a report -1. You’ll receive an in-app and email notification when an expense is submitted. -2. Click the notification to review the expense in Expensify, or you can find the expense in your **Inbox**. -3. Review details like the receipt, amount, and description. -4. Click **Approve**. +When a report is submitted to you for approval it will appear in the **For you** section on **Home**, and on **Reports** in the **Approve** section. -**Note:** If the transaction is still pending (e.g., an Expensify Card or SmartScan expense), you must wait until the transaction posts before approving. +To review and approve a report submitted to you for approval: ---- +1. Click the report to open it. +2. Review details like the receipt, amount, and description. +3. Click **Approve** at the top of the report. -# Change Approver +--- -**To add an approver:** +## How to add an approver to a report -1. From the Report, open the **More** dropdown at the top of the expense. +1. From the Report, choose **More**. 2. Select **Change approver**. 3. Select **Add approver**. -4. Select the approver and save. -5. The approver you selected is now the current approver, and the prescribed workflow will continue after their approval. +4. Choose an additional approver to add to the report. +5. Click **Save**. -**To bypass approvers:** +The approver you added is now the current approver, and the original workflow will continue after their approval. -1. From the Report, open the **More** dropdown at the top of the expense. +--- + +## How to bypass an approver on a report + +Workspace admins can bypass the approval workflow on a report to final approve it themselves. + +1. From the Report, choose **More**. 2. Select **Change approver**. 3. Select **Bypass approvers**. 4. You are now the final approver, and the prescribed workflow has been bypassed. +**Note:** Only Workspace admins can bypass the prescribed approval workflow. + --- -# Hold an Expense +## How to hold an expense -**To place an expense on hold:** +1. In the navigation tabs (on the left on web, and at the bottom on mobile), go to **Reports > Expenses**. +2. Locate the expense you want to hold. +3. Click the expense to open it. +4. Click **More** at the top of the expense. +5. Select **Hold** and enter a reason. -1. In the navigation tabs (on the left on web, and at the bottom on mobile), head to **Reports > Expenses**. -2. Locate the expense using the search bar or filters. -3. Click **View**. -4. Open the **More** dropdown at the top of the expense. -5. Select **Hold** and enter a reason (this will be added to the report). +To take an expense off hold, follow the same steps but select **Unhold**. -**To take an expense off hold to approve:** +--- -1. In the navigation tabs (on the left on web, and at the bottom on mobile), head to **Reports > Expenses**. -2. Locate the held expense using the search bar or filters. -3. Click **View**. -4. Open the **More** dropdown at the top of the expense. -5. Select **Unhold**. -6. Then follow the steps above to **Approve**. +## How to unapprove a report -**Note:** Held expenses cannot be paid until they are approved. +1. In the navigation tabs (on the left on web, and at the bottom on mobile), go to **Reports > Reports**. +2. Locate the Approved report you want to unapprove. +3. Click the report to open it. +4. Click **More** at the top of the report. +5. Select **Unapprove**. + +Unapproving a report returns it to the Outstanding state. The last approver will be notified and can then revise or reject expenses on the report. + +**Note:** Only Approved reports can be unapproved. Paid and Done reports cannot be unapproved. --- -# Unapprove a report +## How to reject a report -1. In the navigation tabs (on the left on web, and at the bottom on mobile), head to **Reports > Reports**. -2. Locate the report using the search bar or filters. -3. Click **View**. -4. Open the **More** dropdown at the top of the report. -5. Select **Unapprove**. +As the assigned approver, you can reject an entire expense report to return it to the submitter or a previous approver while keeping the report's expense grouping intact. + +1. In the navigation tabs (on the left on web, and at the bottom on mobile), go to **Reports > Approve**. +2. Locate the Outstanding report you want to reject. +3. Click the report to open it. +4. Click **More** at the top of the report. +5. Select **Reject**. +6. Enter a comment to explain why you will not approve the report. +7. If the report passed through previous approvers, choose who the report should be rejected back to for review. +8. Click **Reject report** to confirm. + +--- -Unapproving a report returns it to the Processing state. The last approver will be notified and can then revise or reject expenses on the report. +## What happens after a report is rejected -**Note:** Paid reports cannot be unapproved. If the expense was already exported to accounting software, unapproving it may cause reconciliation issues. Be sure to remove the exported data before approving the expense again. +- **Rejected to the submitter**: The report moves back to Draft. The submitter must fix any issues and manually resubmit — rejected reports are skipped during scheduled submit. +- **Rejected to a previous approver**: The report stays Outstanding and prior approvals are preserved, so it won't restart the entire approval workflow. --- -# Reject an expense +## How to reject an expense -1. In the navigation tabs (on the left on web, and at the bottom on mobile), head to **Reports > Expenses**. -2. Locate the expense using the search bar or filters. -3. Click **View**. -4. Open the **More** dropdown at the top of the expense. -5. Select **Reject** and enter a reason (this will be added to the report). +1. In the navigation tabs (on the left on web, and at the bottom on mobile), go to **Reports > Expenses**. +2. Locate the Outstanding expense you want to reject. +3. Click the expense to open it. +4. Click **More** at the top of the expense. +5. Select **Reject** and enter a reason. -The rejected expense will be removed from the report, and the submitter will be notified. The rejection reason will be added to the expense. The expense can later be marked as resolved and resubmitted for approval. +The rejected expense will be removed from the report, and the submitter will be notified. The rejection reason will be added to the expense, and it can later be marked as resolved and resubmitted for approval. --- - # FAQ -## Why is an employee expense showing as pending? -Expensify Card expenses show as pending until the merchant posts them. This can take 1–3 business days. Hotel or rental car holds may take longer (up to 31 days for hotels). +## Why can't I action a pending expense? +Expensify Card expenses show as pending until the merchant posts them. This can take 1–3 business days. Hotel or rental car holds may take longer (up to 31 days for hotels). Only posted expenses can be approved. + +## Why can’t I see Bypass approvers? +Only Workspace admins can bypass the prescribed approval workflow. If **Prevent Self-Approval** is enabled, an admin cannot bypass approvals to approve their own report. + +## What’s the difference between rejecting a report and rejecting an expense? + +Rejecting a report sends the entire report back while keeping all expenses grouped together. Rejecting an expense removes only that expense from the report and sends it back to the submitter. + +## Why can’t I unapprove a report? -## What are expense reports? -Expense reports group multiple expenses into one batch for review or payment. Draft reports collect new expenses automatically. You can check the status of an expense under **Reports > Expenses**. +Reports that are already paid cannot be unapproved. You also need to be an approver on the report to unapprove it. -## Why can’t I see “Bypass approvers”? -Only workspace admins can bypass the prescribed approval workflow. If “Prevent Self-Approval” is enabled, an admin cannot bypass approvals to approve their own report. +## What happens after I approve a report? +The report moves to the next approver in the workflow. If you are the final approver, the report becomes **Approved** and is ready for payment. diff --git a/docs/articles/new-expensify/reports-and-expenses/Distance-Expenses.md b/docs/articles/new-expensify/reports-and-expenses/Distance-Expenses.md index 8a69b68895b8..a10cb1ef4154 100644 --- a/docs/articles/new-expensify/reports-and-expenses/Distance-Expenses.md +++ b/docs/articles/new-expensify/reports-and-expenses/Distance-Expenses.md @@ -42,7 +42,7 @@ To create an expense using distance between the starting and ending locations of - Distance - Amount - Date - - (Optional) Add a description, category, or tag. + - Other optional fields 6. Select **Create expense**. --- @@ -59,7 +59,7 @@ To create an expense by inputting a distance manually: - Distance - Amount - Date - - (Optional) Add a description, category, tag or receipt. + - Other optional fields 6. Select **Create expense**. --- diff --git a/docs/articles/new-expensify/reports-and-expenses/Duplicate-a-Report.md b/docs/articles/new-expensify/reports-and-expenses/Duplicate-a-Report.md new file mode 100644 index 000000000000..fcdeee45b9c6 --- /dev/null +++ b/docs/articles/new-expensify/reports-and-expenses/Duplicate-a-Report.md @@ -0,0 +1,78 @@ +--- +title: Duplicate a Report +description: Learn how to create a copy of an existing report and its expenses in Expensify using Duplicate report. +keywords: [New Expensify, duplicate report, copy report, create copy of report, duplicate expenses] +internalScope: Audience is all members. Covers how to duplicate an entire report and its non-card expenses. Does not cover duplicating individual expenses or duplicate detection. +--- + +# Duplicate a Report + +You can create a copy of an existing report and all of its non-card expenses using **Duplicate report**. This creates a new report with copies of all non-card expenses from the original, including each expense's merchant, amount, category, tags, and tax. Expense dates are set to today and receipt images are not copied. + +This is useful when you need to recreate a similar set of expenses such as recurring monthly reports. + +If you only need to duplicate specific expenses, [learn how to duplicate an expense](/articles/new-expensify/reports-and-expenses/How-to-Duplicate-an-Expense). + +--- + +## Who can duplicate a report + +Any member who created a report can duplicate it. **Duplicate report** is available for reports in any status, including Draft, Processing, Approved, Done, and Paid. + +--- + +## How to duplicate a report + +1. In the navigation tabs (on the left on web, and on the bottom on mobile), go to **Reports > Reports**. +2. Locate the report you want to duplicate. +3. Click the report to open it. +4. Select **More**. +5. Choose **Duplicate report**. + +A new report is created and you are taken to it automatically. + +**Note:** You can only duplicate reports that you created. You can't duplicate other members' reports. + +--- + +## What happens after you duplicate a report + +A new report is created with the name **"Copy of [Original report name]"**. + +- If you still have access to the Workspace the original report is on, the new report is created on that workspace. +- If you no longer have access to that Workspace, the new report is created on your primary Workspace. + +Each non-card expense on the original report is copied to the new report with the following details: + +- Merchant +- Amount and currency +- Category +- Tags +- Tax + +The following details are **not** copied: + +- Date is set to today instead of the original date +- Receipt images are not included on the duplicate expenses +- Card expenses (company cards and Expensify Cards) are not copied +- Per diem and distance expenses are not copied when the new report is created in a different Workspace than the original + +--- + +# FAQ + +## Can I duplicate multiple reports at once? + +No. Reports must be duplicated one at a time. + +## Are card expenses included in the duplicate? + +No. Only non-card expenses are copied. Expenses from company cards or Expensify Cards are skipped. + +## Does the duplicate include receipt images? + +No. Receipt images are not copied to the duplicate expenses. You will need to attach new receipts if required. + +## Why is the Duplicate report option not showing in the More menu? + +**Duplicate report** only appears on reports that you created. If you are viewing someone else's report, you will not see this option. diff --git a/docs/articles/new-expensify/reports-and-expenses/Expense-and-Report-Actions.md b/docs/articles/new-expensify/reports-and-expenses/Expense-and-Report-Actions.md index f76bd63be9bf..cab15663f4cf 100644 --- a/docs/articles/new-expensify/reports-and-expenses/Expense-and-Report-Actions.md +++ b/docs/articles/new-expensify/reports-and-expenses/Expense-and-Report-Actions.md @@ -48,9 +48,11 @@ Report actions guide the submitter, approver, and payer regarding the report's s | **Mark as exported** | On approved/paid reports when integrated to an accounting system| Admin, Exporter | Marks the report as manually exported for tracking purposes | | **Export to [accounting system]** | When an accounting system is connected | Admin, Exporter | Sends the report to an external system such as Xero or QuickBooks Online | | **Duplicate expense** | On non-card expenses in any state | Submitter | Creates a copy of the expense on your primary workspace with the same details. Date is set to today and receipts are not copied | +| **Duplicate report** | On reports the submitter owns, in any state | Submitter | Creates a copy of the report and its non-card expenses. Date is set to today and receipts are not copied | | **Download** | Any report state | All roles | Downloads a copy of the report as a PDF | | **Print** | Any report state | All roles | Opens the report in a printable format and triggers the browser's print dialog | | **Submit** | For draft reports | Submitter, Admin (on behalf of submitter) | Kicks off report approval workflow | +| **Reject** | On Outstanding reports | Assigned approver | Returns the entire report to the submitter or a previous approver with a required reason. The report moves to Draft (if rejected to submitter) or stays Outstanding (if rejected to a previous approver) | | **Approve** | For outstanding reports | Admin | Skips current approver | | **View details** | Any report | All roles | Opens details view with options to share, pin, view members | | **Cancel payment** | After payment is initiated, but before the payment has been processed, or always for a manual payment | Payer | Cancels pending payment | diff --git a/docs/articles/new-expensify/reports-and-expenses/How-to-Duplicate-an-Expense.md b/docs/articles/new-expensify/reports-and-expenses/How-to-Duplicate-an-Expense.md index 34140158fce1..8e79e067e005 100644 --- a/docs/articles/new-expensify/reports-and-expenses/How-to-Duplicate-an-Expense.md +++ b/docs/articles/new-expensify/reports-and-expenses/How-to-Duplicate-an-Expense.md @@ -1,23 +1,28 @@ --- title: How to Duplicate an Expense -description: Learn how to create a copy of an existing expense in Expensify using the Duplicate option. -keywords: [New Expensify, duplicate expense, copy expense, duplicate cash expense, duplicate distance expense, duplicate per diem expense, create copy of expense] -internalScope: Audience is expense submitters. Covers how to duplicate a single cash, distance, or per diem expense. Does not cover duplicate detection, resolving flagged duplicates, or merging expenses. +description: Learn how to create a copy of one or more expenses in Expensify using Duplicate expense. +keywords: [New Expensify, duplicate expense, copy expense, duplicate cash expense, duplicate distance expense, duplicate per diem expense, create copy of expense, bulk duplicate, duplicate multiple expenses] +internalScope: Audience is all members. Covers how to duplicate a single expense and how to bulk-duplicate multiple expenses from the Expenses tab or a report. Does not cover duplicate detection, resolving flagged duplicates, or merging expenses. --- # How to duplicate an expense -You can create a copy of an existing expense using **Duplicate expense**. This generates a new expense on your primary workspace with the same details as the original, including the merchant, amount, category, tags, and tax. The date is set to today and receipt images are not copied. +You can create a copy of one or more expenses using **Duplicate expense**. This generates new expenses on your primary workspace with the same details as the originals, including the merchant, amount, category, tags, and tax. The date is set to today and receipt images are not copied. -This is useful when you need to create a similar expense quickly without re-entering all the details manually. +This is useful when you need to create similar expenses quickly without re-entering all the details manually. You can duplicate a single expense or select multiple expenses and duplicate them all at once. -**Note:** Card expenses cannot be duplicated. Only cash, distance, and per diem expenses support duplication. +**Note:** Card expenses cannot be duplicated. Only cash, distance, and per diem expenses are eligible to be duplicated. + + --- ## Who can duplicate an expense -Any expense submitter can duplicate their own expenses. **Duplicate expense** is available for cash, distance, and per diem expenses in any status, including Unreported, Draft, Processing, Approved, and Paid. +Anyone can duplicate their own expenses. **Duplicate expense** is available for cash, distance, and per diem expenses in any status, including Unreported, Draft, Processing, Approved, and Paid. Card expenses (company cards and Expensify Cards) cannot be duplicated. @@ -26,35 +31,44 @@ Card expenses (company cards and Expensify Cards) cannot be duplicated. ## How to duplicate a single expense 1. In the navigation tabs (on the left on web, on the bottom on mobile), select **Reports > Expenses** -1. In the navigation tabs (on the left on web, on the bottom on mobile), select **Reports > Expenses**. +2. Click to open the eligible expense you want to duplicate. 3. Select **More**. -4. Choose **Duplicate expense**.The **Duplicate expense** menu item will briefly change to **Duplicated** to confirm the action. +4. Choose **Duplicate expense**. -The duplicate expense is created on your primary workspace with the original expense's merchant, amount, category, tags, tax, and billable or reimbursable status. The date is set to today and receipt images are not included. +![Select an Expense > More > Duplicate expense]({{site.url}}/assets/images/ExpensifyHelp-DuplicateExpense_01.png){:width="100%"} + +--- + +## How to duplicate multiple expenses at once + +You can select multiple eligible expenses and duplicate them all in one action. + +**On web:** + +1. In the navigation tabs on the left, click **Reports > Expenses**. +2. Select two or more cash, distance, or per diem expenses that you created. +3. Click **Selected**, then **Duplicate expenses**. + +**On mobile** + +1. In the navigation tabs on the bottom, tap **Reports > Expenses**. +2. Long-press an expense, then tap **Select** to enter selection mode. +3. Select two or more cash, distance, or per diem expenses that you created. +4. Tap **Selected**, then **Duplicate expenses**. + +Each selected expense is duplicated individually. The same rules apply as for single duplication — card expenses, scanning expenses, per diem expenses without dates, and expenses you did not submit are excluded from selection. --- ## What happens after you duplicate an expense -A new expense is created on your primary workspace with the following details copied from the original: - -- Merchant -- Amount and currency -- Category -- Tags -- Tax -- Billable and reimbursable status -- Attendees -- Description - -The following details are not copied: - -- **Date** is set to today instead of the original date -- **Receipt images** are not included on the duplicate +The duplicate expense is created on your primary workspace with the original expense's merchant, amount, category, tags, tax, and billable or reimbursable status. The date is set to today and receipt images are not included. If the original expense's coding (such as categories or tags) is not available on your primary workspace, the duplicate expense will still be created. You will be prompted to correct the coding on the expense. @@ -70,27 +84,19 @@ When this restriction applies, Expensify will display a message explaining that # FAQ -## Can I duplicate multiple expenses at once? - -No, you can’t duplicate multiple expenses at once. Expenses must be duplicated one at a time. - ## Can I duplicate a card expense? No. Expenses from company cards or Expensify Cards cannot be duplicated. Only cash, distance, and per diem expenses support duplication. - - ## Does the duplicated expense include the receipt image? No. Receipt images are not copied to the duplicate expense. You will need to attach a new receipt if one is required. -## Why is the Duplicate option not showing in the More menu? +## Why is Duplicate not showing in the menu? -**Duplicate expense** only appears when the expense is a cash, distance, or per diem expense that you submitted. It will not appear if: +**Duplicate expense** only appears when the expense is a cash, distance, or per diem expense that you created. It will not appear if: -- The expense is from a company card or Expensify Card +- The expense was created by someone else. +- The expense is imported from a company card or Expensify Card - A receipt is currently being scanned - For per diem expenses, if start or end dates are missing diff --git a/docs/articles/new-expensify/reports-and-expenses/Understanding-Report-Statuses-and-Actions.md b/docs/articles/new-expensify/reports-and-expenses/Understanding-Report-Statuses-and-Actions.md index bae7dcafc8cc..0e3fe8fd55ba 100644 --- a/docs/articles/new-expensify/reports-and-expenses/Understanding-Report-Statuses-and-Actions.md +++ b/docs/articles/new-expensify/reports-and-expenses/Understanding-Report-Statuses-and-Actions.md @@ -33,6 +33,7 @@ The grey **More** button is always visible in the report header. Tap it to acces - Hold / Unhold - Unapprove +- Duplicate report - Download as CSV - **Print** - Change Workspace diff --git a/docs/articles/new-expensify/settings/Account-Settings.md b/docs/articles/new-expensify/settings/Account-Settings.md index 00fe74ac27bd..fc82f3cfdd24 100644 --- a/docs/articles/new-expensify/settings/Account-Settings.md +++ b/docs/articles/new-expensify/settings/Account-Settings.md @@ -128,9 +128,12 @@ Your photo helps teammates identify you easily in chats, reports, and notificati ## Will changing my language affect reports? No. It only changes the language of your user interface, not the content of your reports. -## Can I disable all Expensify notifications? +## Can I disable all Expensify app notifications? Yes. From the navigation tabs, go to **Account > Preferences** and toggle off both update and sound notifications. +## Can I disable email notifications for reports that I approve for reimbursement? +No. To comply with regulations in several US states, Expensify is required to send you an email whenever you send money to another individual. + ## Will theme preferences sync across devices? Yes! Your chosen theme applies across web, mobile, and desktop versions of Expensify. diff --git a/docs/articles/new-expensify/settings/Personal-Karma.md b/docs/articles/new-expensify/settings/Personal-Karma.md index 40d8ebd650a7..7201d18ff105 100644 --- a/docs/articles/new-expensify/settings/Personal-Karma.md +++ b/docs/articles/new-expensify/settings/Personal-Karma.md @@ -8,15 +8,28 @@ keywords: [New Expensify, personal karma, donations, expensify.org, save the wor Personal Karma lets you automatically donate a small percentage of your monthly added expenses to [Expensify.org](https://www.expensify.org/about). -For every $500 in expenses you add, $1 is donated to a related Expensify.org fund. Donations are calculated monthly and charged to the billing card on file. You’ll receive a donation receipt by email after each charge. +For every $500 in expenses you add, $1 is donated to a related Expensify.org fund. Donations are calculated monthly and charged to the billing card on file. You'll receive a donation receipt by email after each charge. --- -## How to enable or disable Personal Karma +## How to enable Personal Karma -1. In the navigation tabs (on the left on Web, at the bottom on Mobile), choose **Account**. -2. Select **Save the World** -3. Toggle **Personal Karma** to enable or disable it. +1. In the navigation tabs (on the left on web, at the bottom on mobile), choose **Account**. +2. Select **Save the World**. +3. Toggle **Enable Personal Karma** on. + +If you don't have a payment card on file, you'll be prompted to add one before Personal Karma can be enabled. If you dismiss the payment card prompt without adding a card, the toggle will revert to disabled. + +Once enabled, your billing card details are displayed below the toggle. + +--- + +## How to disable Personal Karma + +1. In the navigation tabs (on the left on web, at the bottom on mobile), choose **Account**. +2. Select **Save the World**. +3. Toggle **Enable Personal Karma** off. +4. A confirmation prompt will ask if you want to stop donating to Expensify.org. Select **Disable** to confirm, or **Cancel** to keep Personal Karma enabled. --- diff --git a/docs/assets/images/ExpensifyHelp-FixPersonalCards.png b/docs/assets/images/ExpensifyHelp-FixPersonalCards.png new file mode 100644 index 0000000000000000000000000000000000000000..b5c668925c9867876c8472073dbb2a579072b74b GIT binary patch literal 216723 zcmeFYXH-+&);0`wM8I3b0s>YL5KyVo5fPQ%l`0^e5PFB8G)0Pv^xiw6Ce%bhL3$^U z5CS3)2q8ccLI_D-&U^0j+|T)be?Q+nM#j$EbF97hT5IlEt~uui13fK{v%F_nSXelo zKYL=t!g6|yh2_M=nNvqsR$szT8~-kMy{jpeymeO<~@W0$u6;^O9+in{j4Z867BuzdS@ z(FI_pZ5-9OTQ@ZN=KsC|3rjhq>z>2_V-8@mT#p!mwq58YsR_T>)5q!TC@X-A=hTBwJwh!;&Uyd zVL!P+n1l+i@lxSCxLb0VYOZ;&j%o$1(TXGWLgF#`kGhjLhw#!>nY-^VdA`ze3=vd6 zd7zn=l~JNUzZa?EO;h}umvQfZp2=c<%JL;`GMQP;KFZvl(LNZ)jp7V+Q@wT!-bVZt zzBxl9#RRJ8ctxE0|2zc?&=Q>h0lWRIuARVM{{H#a^E!TILwTtqeHICO_aDvmjcOl7 zLctD_oBiQuI=fFgqFN*wAoy*if7IjHEz3_Ge=OmCb2laK^{T-mVdS^^8M%l2$NuLf zBaNSHzmTQ=7DmG{hl97eV~(bD(%urno%fHvm*>t0Ek;SW|AT4laHtn8 zGB0btB%PYpI(GJnhG)B#?H3>Vb6(!;O4l8_^bHBylZ-bU@1L80k$WmY6dbXZzi6fn z_~^ztQ{|@>vjtrFo(SkW4bUCYhwWI=5@i;N#k%>bc-^$|v7OeYCp=(^tkh`x1>C<8^0F&b>52R83XE(W!W|H5Ept%*u&h#ENEe&QF(?OXfAvw&x^M#TD4|2p z{jO%9Enrz#%N~doSQORBw$va?e>W3vd3kCk_s#)Tj`2JKuK%~_=;aTKp$M zcaVKv`FrS+*s)te1`XoxQj8|1)1wt0BVj@5jBRkT>(7+xX%ln_u}vMBbqv|%1D+Yh zlth0Ga-@uYQH?ENz5Mi7Cui|^MW+qeYvn_`8GhKUrrJl>bZErnwZbsSJP-GRYC zd!KQgRGjy@BUHZO;{bIp=+aOjDiDUr=u0jLNr7;60zDn*`l>{SOmXZFb!hJ_US8=c zHsH-EqDvAchIWdj3-14CЛ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 z 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

n5Lx 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 D6stmP(6vdVE{rthh}SjupEtd1k?+x zRT)9C4O=DmUF;Qgu51gE4Y^sO`UGvkHZ3qIxDP#Xz#@a5*Je%6`J+o9>}YZnXMb zA;c?Xg(%aCDAne{1YsNIm0JmM;6}Jg8GD=x=WG9(Be7@H#lS2p8E-4fz zdJXn;!`$_sf;zxv8mrqt;bk0JIbGaieUY_ z{&IOTP2G^S$Hs@)lCM(G1pQr55wUR6w>>;EW*D3;U}W?Y)wj5>roPBC@%2;@l>!J} z^PND17aN;#K$8r)I>CI@ z*zRn;*7;e|wU6U5?!V*3ptvig+xCP5$OuuWrBS8`eq>YN=JsMKD3V zDyV$8ljXJ}NK*w~O&rPlUfyHPg7x5gmX!*!6PRe4W|ZYHN2GDRWa*uWYV(hramctz z5F?@TqqLNW6vRzTRvPxoSV7~7R9#-&aG&V7g#s1vkyX=dbD9s*cww5(u7}F_3p+5& z#lK6qzDT1Dm8SX*Yz;Q*M!_6y;?~k(n*B5KYfYF!e>>i$(hh+C+3&}WVakw}609zo z%8qeC6k_!y^Ok$!!K`PLXaf%R3+U4+Ktpv+%=4(^seHA0NVSS6#&!wbA?EWv=o@YctRYLk^fk+V!+AR=QMIUdbUYvUs2uqVckj=1J9(}6&SOxa6>lT+K8|wDgZd4np07ac?SQK*bn*&u^ z@cdz+#M^D6mS}9Ph0Ge&&f<&1^qp06X-lS)N2x2H+u01*ZKYDB8tNQ}V!OWTWgIey?S&DMk_GrAX(1^eCdAT-$0$+ly0jr| zf>K+*l`-9R(7nBaKx+e|{bx>2@!$IBD39o!VAH|haXe~!I*!QZq^mhx_BxCCm!_`O zdq21A$+nu3Ryi!Z?zDhh8Pak?STma})y)!jdaHtN;WvFyXp=9uM=n_|lxZEi_3Gua z+`dYOTQ{vHef_se=|Uf2$(6TP2JnqhMHk?pp5vTM>Wd}Ky%E5smn0bj1#N6O#kISV z%jTj-uRJul68UjG6Iy}nNtP%D`oQfD{PHe>+)8SyYcrSw?U6b?l8MeL-fk?5ot>ba z2ryr>ZpiXL_lS#k^%1_pTjydH!G5{|SoaS$Xu6Ly^^<{-6DX{#H=V!zr*iu5Cn}jO zQ;^UYeK-Q@{oln&rpggY6Ye*5V#tS^x}@`6+3P=An&I6ysyQa#@c9N)f){#V%)Y+c zmkXN&)iXLGQP1D-p=s=h+ERkLZl?SGsf{Z#3U++NO!Guc>wx>Gg<}4Y`wUlecnqGO zXVknlzHH2NFOH#5UL$&Uy#L`W`LyInM4CriQuCey*{omaSmAfE{O)1J`b@TCI8fs3 zR>>anjiXqgQsZhnDg&p>Oqr5j7geb|_?teu@6NCBoxSSGBPT!9KJ3AVd+`6TuwyjC XEqHx$f&L|G4=Q literal 0 HcmV?d00001 diff --git a/eslint-plugin-react-compiler-compat/index.mjs b/eslint-plugin-react-compiler-compat/index.mjs index aca2fc6d1eff..2a1a60070583 100644 --- a/eslint-plugin-react-compiler-compat/index.mjs +++ b/eslint-plugin-react-compiler-compat/index.mjs @@ -13,6 +13,10 @@ */ import {transformSync} from '@babel/core'; import _ from 'lodash'; +import {createRequire} from 'module'; + +const require = createRequire(import.meta.url); +const ReactCompilerConfig = require('../config/babel/reactCompilerConfig'); // Rules that are entirely unnecessary when React Compiler successfully compiles // all functions in a file. Add more rules here as needed. @@ -24,15 +28,6 @@ const RULES_SUPPRESSED_BY_REACT_COMPILER = new Set(['react/jsx-no-constructed-co // about genuinely missing dependencies. const EXHAUSTIVE_DEPS_USECALLBACK_USEMEMO_PATTERN = /\buseCallback\(\) Hook\b|\buseMemo\(\) Hook\b/; -// Mirror the compiler config from babel.config.js (excluding `sources`, -// which is only relevant for the Babel build pipeline, not for our analysis). -const ReactCompilerConfig = { - target: '19', - environment: { - enableTreatRefLikeIdentifiersAsRefs: true, - }, -}; - // Per-file compilation results, populated in preprocess, consumed in postprocess. const compilationResults = new Map(); diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index f16fe44553c2..0f680253f979 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -23,7 +23,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.3.59 + 9.3.60 CFBundleSignature ???? CFBundleURLTypes @@ -44,7 +44,7 @@ CFBundleVersion - 9.3.59.3 + 9.3.60.1 FullStory OrgId diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 42b5108cd9cb..e00d190bd6d6 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.3.59 + 9.3.60 CFBundleVersion - 9.3.59.3 + 9.3.60.1 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 73a893eda6ec..13ee55073f75 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -308,12 +308,33 @@ PODS: - GoogleUtilities/Logger - GoogleUtilities/Privacy - group-ib-fp (2.6.0): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - - React-Codegen - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core + - SocketRocket + - Yoga - GTMAppAuth (4.1.1): - AppAuth/Core (~> 1.7) - GTMSessionFetcher/Core (< 4.0, >= 3.3) @@ -467,7 +488,6 @@ PODS: - React-RCTText (= 0.83.1) - React-RCTVibration (= 0.83.1) - React-callinvoker (0.83.1) - - React-Codegen (0.1.0) - React-Core (0.83.1): - boost - DoubleConversion @@ -4266,7 +4286,7 @@ DEPENDENCIES: - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) - - "FullStory (from `{http: \"https://ios-releases.fullstory.com/fullstory-1.68.3-xcframework.tar.gz\"}`)" + - "FullStory (from `{:http=>\"https://ios-releases.fullstory.com/fullstory-1.68.3-xcframework.tar.gz\"}`)" - "fullstory_react-native (from `../node_modules/@fullstory/react-native`)" - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - group-ib-fp (from `../node_modules/group-ib-fp`) @@ -4427,7 +4447,6 @@ SPEC REPOS: - Plaid - PromisesObjC - PusherSwift - - React-Codegen - SDWebImage - SDWebImageAVIFCoder - SDWebImageSVGCoder @@ -4784,11 +4803,11 @@ SPEC CHECKSUMS: GoogleAppMeasurement: 0dfca1a4b534d123de3945e28f77869d10d0d600 GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 - group-ib-fp: 03efaa63a9996535431452a213ed5c7642576c55 + group-ib-fp: 42bc98218d2b4b18eac9a2dd0156a3074e0df082 GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa - hermes-engine: 0711ccb14bd615969ef611bc6c2483ea2ed3b09e + hermes-engine: 5125891942d0cdeca799d3a5a1bb5e766a783790 libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7 libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 @@ -4815,7 +4834,6 @@ SPEC CHECKSUMS: RCTTypeSafety: 27927d0ca04e419ed9467578b3e6297e37210b5c React: 4bc1f928568ad4bcfd147260f907b4ea5873a03b React-callinvoker: 87f8728235a0dc62e9dc19b3851c829d9347d015 - React-Codegen: 4b8b4817cea7a54b83851d4c1f91f79aa73de30a React-Core: 76bed73b02821e5630e7f2cb2e82432ee964695d React-CoreModules: 752dbfdaeb096658aa0adc4a03ba6214815a08df React-cxxreact: b6798528aa601c6db66e6adc7e2da2b059c8be74 diff --git a/ios/ShareViewController/Info.plist b/ios/ShareViewController/Info.plist index 92c5a0d7eae8..ea534828f26f 100644 --- a/ios/ShareViewController/Info.plist +++ b/ios/ShareViewController/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.3.59 + 9.3.60 CFBundleVersion - 9.3.59.3 + 9.3.60.1 NSExtension NSExtensionAttributes diff --git a/jest/setup.ts b/jest/setup.ts index 9255404f5d68..aef83f583b3a 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -262,7 +262,7 @@ jest.mock('../src/components/Icon/IllustrationLoader.ts', () => ({ })); jest.mock( - '@components/FlatList/InvertedFlatList/RenderTaskQueue', + '@components/FlatList/RenderTaskQueue', () => class SyncRenderTaskQueue { private handler: (info: unknown) => void = () => {}; diff --git a/jest/setupAfterEnv.ts b/jest/setupAfterEnv.ts index c9ef9afc5d98..c074877a3f02 100644 --- a/jest/setupAfterEnv.ts +++ b/jest/setupAfterEnv.ts @@ -4,6 +4,35 @@ import ONYXKEYS from '@src/ONYXKEYS'; jest.useRealTimers(); +// This mock must live in setupAfterEnv (not setupFiles) because @shopify/flash-list/jestSetup, +// imported in setup.ts, registers its own measureLayout mock. Placing ours here ensures it +// runs after FlashList's setup and takes precedence. +jest.mock( + '@shopify/flash-list/dist/recyclerview/utils/measureLayout', + () => + ({ + ...jest.requireActual('@shopify/flash-list/dist/recyclerview/utils/measureLayout'), + measureParentSize: jest.fn().mockImplementation(() => ({ + x: 0, + y: 0, + width: 300, + height: 400, + })), + measureFirstChildLayout: jest.fn().mockImplementation(() => ({ + x: 0, + y: 0, + width: 300, + height: 400, + })), + measureItemLayout: jest.fn().mockImplementation(() => ({ + x: 0, + y: 0, + width: 300, + height: 75, + })), + }) as Record, +); + // Auto-initialize Onyx for tests. // Tests that already call Onyx.init() in their own beforeAll will safely re-configure Onyx — // the second init() just re-runs initStoreValues and re-resolves the already-resolved deferred task. diff --git a/modules/group-ib-fp/group-ib-fp.podspec b/modules/group-ib-fp/group-ib-fp.podspec index c4d6ead3f067..156ba7e656c3 100644 --- a/modules/group-ib-fp/group-ib-fp.podspec +++ b/modules/group-ib-fp/group-ib-fp.podspec @@ -1,7 +1,6 @@ require "json" package = JSON.parse(File.read(File.join(__dir__, "package.json"))) -folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' Pod::Spec.new do |s| s.name = "group-ib-fp" @@ -11,26 +10,12 @@ Pod::Spec.new do |s| s.license = package["license"] s.authors = package["author"] - s.platforms = { :ios => "11.0" } + s.platforms = { :ios => min_ios_version_supported } s.source = { :git => "https://group-ib.com.git", :tag => "#{s.version}" } s.source_files = "ios/*.{h,m,mm,swift}" - s.dependency "React-Core" s.vendored_frameworks = ['iOS/Frameworks/GIBMobileSdk.xcframework', 'iOS/Frameworks/FPAppsCapability.xcframework', 'iOS/Frameworks/FPCallCapability.xcframework'] - # Don't install the dependencies when we run `pod install` in the old architecture. - if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then - s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" - s.pod_target_xcconfig = { - "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", - "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", - "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" - } - s.dependency "React-Codegen" - s.dependency "RCT-Folly" - s.dependency "RCTRequired" - s.dependency "RCTTypeSafety" - s.dependency "ReactCommon/turbomodule/core" - end + install_modules_dependencies(s) end diff --git a/package-lock.json b/package-lock.json index 75fea15a1c9d..b373f4e340fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.3.59-3", + "version": "9.3.60-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.3.59-3", + "version": "9.3.60-1", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -267,7 +267,6 @@ "peggy": "^4.0.3", "portfinder": "^1.0.34", "prettier": "3.7.4", - "react-compiler-healthcheck": "^19.0.0-beta-8a03594-20241020", "react-native-clean-project": "^4.0.0-alpha4.0", "react-refresh": "^0.14.2", "react-test-renderer": "19.2.0", @@ -33950,91 +33949,6 @@ "react": ">=16.3.0" } }, - "node_modules/react-compiler-healthcheck": { - "version": "19.0.0-beta-8a03594-20241020", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "chalk": "4", - "fast-glob": "^3.3.2", - "ora": "5.4.1", - "yargs": "^17.7.2", - "zod": "^3.22.4", - "zod-validation-error": "^3.0.3" - }, - "bin": { - "react-compiler-healthcheck": "dist/index.js" - }, - "engines": { - "node": "^14.17.0 || ^16.0.0 || >= 18.0.0" - } - }, - "node_modules/react-compiler-healthcheck/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-compiler-healthcheck/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-compiler-healthcheck/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/react-compiler-healthcheck/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/react-compiler-healthcheck/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/react-compiler-healthcheck/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/react-content-loader": { "version": "7.0.0", "license": "MIT", diff --git a/package.json b/package.json index a0e3c9a15d0e..6ed1c438f107 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.3.59-3", + "version": "9.3.60-1", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -45,7 +45,7 @@ "perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure", "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc", "typecheck-tsgo": "tsgo --project tsconfig.tsgo.json", - "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=314 --cache --cache-location=node_modules/.cache/eslint --cache-strategy content --concurrency=auto", + "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=313 --cache --cache-location=node_modules/.cache/eslint --cache-strategy content --concurrency=auto", "lint-changed": "NODE_OPTIONS=--max_old_space_size=8192 ./scripts/lintChanged.sh", "lint-watch": "npx eslint-watch --watch --changed", "shellcheck": "./scripts/shellCheck.sh", @@ -330,7 +330,6 @@ "peggy": "^4.0.3", "portfinder": "^1.0.34", "prettier": "3.7.4", - "react-compiler-healthcheck": "^19.0.0-beta-8a03594-20241020", "react-native-clean-project": "^4.0.0-alpha4.0", "react-refresh": "^0.14.2", "react-test-renderer": "19.2.0", diff --git a/patches/@shopify/flash-list/@shopify+flash-list+2.3.0+001+fix-horizontal-height-normalization.patch b/patches/@shopify/flash-list/@shopify+flash-list+2.3.0+001+fix-horizontal-height-normalization.patch index 007d0682e1cc..e9d9e3dfd981 100644 --- a/patches/@shopify/flash-list/@shopify+flash-list+2.3.0+001+fix-horizontal-height-normalization.patch +++ b/patches/@shopify/flash-list/@shopify+flash-list+2.3.0+001+fix-horizontal-height-normalization.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@shopify/flash-list/dist/recyclerview/layout-managers/LinearLayoutManager.js b/node_modules/@shopify/flash-list/dist/recyclerview/layout-managers/LinearLayoutManager.js -index fb40ded..ea4eba2 100644 +index fb40ded..12375d9 100644 --- a/node_modules/@shopify/flash-list/dist/recyclerview/layout-managers/LinearLayoutManager.js +++ b/node_modules/@shopify/flash-list/dist/recyclerview/layout-managers/LinearLayoutManager.js @@ -92,6 +92,17 @@ export class RVLinearLayoutManagerImpl extends RVLayoutManager { diff --git a/patches/@shopify/flash-list/@shopify+flash-list+2.3.0+003+fix-inverted-scroll-direction-on-web.patch b/patches/@shopify/flash-list/@shopify+flash-list+2.3.0+003+fix-inverted-scroll-direction-on-web.patch new file mode 100644 index 000000000000..edb436a356b4 --- /dev/null +++ b/patches/@shopify/flash-list/@shopify+flash-list+2.3.0+003+fix-inverted-scroll-direction-on-web.patch @@ -0,0 +1,191 @@ +diff --git a/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerView.js b/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerView.js +index dd2d3bc..d7a3d84 100644 +--- a/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerView.js ++++ b/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerView.js +@@ -2,8 +2,8 @@ + * RecyclerView is a high-performance list component that efficiently renders and recycles list items. + * It's designed to handle large lists with optimal memory usage and smooth scrolling. + */ +-import React, { useCallback, useLayoutEffect, useMemo, useRef, forwardRef, useState, useId, } from "react"; +-import { Animated, I18nManager, } from "react-native"; ++import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, forwardRef, useState, useId, } from "react"; ++import { Animated, I18nManager, Platform, } from "react-native"; + import { ErrorMessages } from "../errors/ErrorMessages"; + import { WarningMessages } from "../errors/WarningMessages"; + import { areDimensionsNotEqual, measureFirstChildLayout, measureItemLayout, measureParentSize, } from "./utils/measureLayout"; +@@ -66,6 +66,66 @@ const RecyclerViewComponent = (props, ref) => { + // Hook to detect when scrolling reaches list bounds + const { checkBounds } = useBoundDetection(recyclerViewManager, scrollViewRef); + const isHorizontalRTL = I18nManager.isRTL && horizontal; ++ // Web-only: Fix inverted scroll direction. ++ useEffect(() => { ++ if (!inverted || Platform.OS !== "web") { ++ return; ++ } ++ const scrollRef = scrollViewRef.current; ++ if (!scrollRef || typeof scrollRef.getScrollableNode !== "function") { ++ return; ++ } ++ const node = scrollRef.getScrollableNode(); ++ if (!node) { ++ return; ++ } ++ const wheelHandler = (ev) => { ++ const target = ev.target; ++ const deltaX = ev.deltaX || ev.wheelDeltaX || 0; ++ const deltaY = ev.deltaY || ev.wheelDeltaY || 0; ++ // Compute scroll limits from the DOM node for overscroll recoil prevention. ++ const nodeScrollOffset = horizontal ? node.scrollLeft : node.scrollTop; ++ const nodeScrollLength = horizontal ? node.scrollWidth : node.scrollHeight; ++ const nodeClientLength = horizontal ? node.clientWidth : node.clientHeight; ++ const isOnScrollLimit = nodeScrollOffset <= 0 || Math.ceil(nodeScrollOffset) >= nodeScrollLength - nodeClientLength; ++ const scrollOffset = horizontal ? target.scrollLeft : target.scrollTop; ++ const scrollLength = horizontal ? target.scrollWidth : target.scrollHeight; ++ const clientLength = horizontal ? target.clientWidth : target.clientHeight; ++ const isEventTargetScrollable = scrollLength > clientLength; ++ const delta = horizontal ? deltaX : deltaY; ++ let leftoverDelta = delta; ++ if (isEventTargetScrollable) { ++ leftoverDelta = delta < 0 ++ ? Math.min(delta + scrollOffset, 0) ++ : Math.max(delta - (scrollLength - clientLength - scrollOffset), 0); ++ } ++ const targetDelta = delta - leftoverDelta; ++ if (horizontal) { ++ if (Math.abs(deltaX) > Math.abs(deltaY)) { ++ target.scrollLeft += targetDelta; ++ node.scrollLeft = node.scrollLeft - leftoverDelta; ++ ev.preventDefault(); ++ ev.stopPropagation(); ++ } ++ } ++ else { ++ // Prevent overscroll recoil/rubber band at scroll boundaries. ++ if (isOnScrollLimit && Math.abs(deltaY) > 0) { ++ ev.preventDefault(); ++ } ++ if (Math.abs(deltaY) > Math.abs(deltaX)) { ++ target.scrollTop += targetDelta; ++ node.scrollTop = node.scrollTop - leftoverDelta; ++ ev.preventDefault(); ++ ev.stopPropagation(); ++ } ++ } ++ }; ++ node.addEventListener("wheel", wheelHandler, { passive: false }); ++ return () => { ++ node.removeEventListener("wheel", wheelHandler); ++ }; ++ }, [inverted, horizontal]); + /** + * Initialize the RecyclerView by measuring and setting up the window size + * This effect runs when the component mounts or when layout changes +diff --git a/node_modules/@shopify/flash-list/src/recyclerview/RecyclerView.tsx b/node_modules/@shopify/flash-list/src/recyclerview/RecyclerView.tsx +index 34722d4..ea801d2 100644 +--- a/node_modules/@shopify/flash-list/src/recyclerview/RecyclerView.tsx ++++ b/node_modules/@shopify/flash-list/src/recyclerview/RecyclerView.tsx +@@ -5,6 +5,7 @@ + import React, { + RefObject, + useCallback, ++ useEffect, + useLayoutEffect, + useMemo, + useRef, +@@ -17,6 +18,7 @@ import { + I18nManager, + NativeScrollEvent, + NativeSyntheticEvent, ++ Platform, + } from "react-native"; + + import { FlashListRef } from "../FlashListRef"; +@@ -158,6 +160,88 @@ const RecyclerViewComponent = ( + + const isHorizontalRTL = I18nManager.isRTL && horizontal; + ++ /** ++ * Web-only: Fix inverted scroll direction. ++ * When a list is visually inverted via scaleY/scaleX: -1, the browser's native ++ * wheel scroll goes in the wrong visual direction. This effect attaches a wheel ++ * event listener that negates the delta to correct the scroll direction. ++ * Mirrors the fix in react-native-web's VirtualizedList. ++ */ ++ useEffect(() => { ++ if (!inverted || Platform.OS !== "web") { ++ return; ++ } ++ const scrollRef = scrollViewRef.current; ++ if (!scrollRef || typeof (scrollRef as any).getScrollableNode !== "function") { ++ return; ++ } ++ const node = (scrollRef as any).getScrollableNode() as HTMLElement; ++ if (!node) { ++ return; ++ } ++ ++ const wheelHandler = (ev: WheelEvent) => { ++ const target = ev.target as HTMLElement; ++ const deltaX = ev.deltaX || (ev as any).wheelDeltaX || 0; ++ const deltaY = ev.deltaY || (ev as any).wheelDeltaY || 0; ++ ++ // Compute scroll limits from the DOM node for overscroll recoil prevention. ++ const nodeScrollOffset = horizontal ? node.scrollLeft : node.scrollTop; ++ const nodeScrollLength = horizontal ? node.scrollWidth : node.scrollHeight; ++ const nodeClientLength = horizontal ? node.clientWidth : node.clientHeight; ++ const isOnScrollLimit = ++ nodeScrollOffset <= 0 || ++ Math.ceil(nodeScrollOffset) >= nodeScrollLength - nodeClientLength; ++ ++ const scrollOffset = horizontal ? target.scrollLeft : target.scrollTop; ++ const scrollLength = horizontal ? target.scrollWidth : target.scrollHeight; ++ const clientLength = horizontal ? target.clientWidth : target.clientHeight; ++ const isEventTargetScrollable = scrollLength > clientLength; ++ const delta = horizontal ? deltaX : deltaY; ++ ++ // Calculate how much delta the event target can consume vs leftover for parent ++ let leftoverDelta = delta; ++ if (isEventTargetScrollable) { ++ leftoverDelta = ++ delta < 0 ++ ? Math.min(delta + scrollOffset, 0) ++ : Math.max( ++ delta - (scrollLength - clientLength - scrollOffset), ++ 0 ++ ); ++ } ++ const targetDelta = delta - leftoverDelta; ++ ++ // Only adjust scroll and consume the event when the dominant axis ++ // matches the list orientation. stopPropagation prevents parent ++ // inverted lists from also handling this event. ++ if (horizontal) { ++ if (Math.abs(deltaX) > Math.abs(deltaY)) { ++ target.scrollLeft += targetDelta; ++ node.scrollLeft = node.scrollLeft - leftoverDelta; ++ ev.preventDefault(); ++ ev.stopPropagation(); ++ } ++ } else { ++ // Prevent overscroll recoil/rubber band at scroll boundaries. ++ if (isOnScrollLimit && Math.abs(deltaY) > 0) { ++ ev.preventDefault(); ++ } ++ if (Math.abs(deltaY) > Math.abs(deltaX)) { ++ target.scrollTop += targetDelta; ++ node.scrollTop = node.scrollTop - leftoverDelta; ++ ev.preventDefault(); ++ ev.stopPropagation(); ++ } ++ } ++ }; ++ ++ node.addEventListener("wheel", wheelHandler, { passive: false }); ++ return () => { ++ node.removeEventListener("wheel", wheelHandler); ++ }; ++ }, [inverted, horizontal]); ++ + /** + * Initialize the RecyclerView by measuring and setting up the window size + * This effect runs when the component mounts or when layout changes diff --git a/patches/@shopify/flash-list/@shopify+flash-list+2.3.0+004+fix-inverted-first-item-offset.patch b/patches/@shopify/flash-list/@shopify+flash-list+2.3.0+004+fix-inverted-first-item-offset.patch new file mode 100644 index 000000000000..98bac124be64 --- /dev/null +++ b/patches/@shopify/flash-list/@shopify+flash-list+2.3.0+004+fix-inverted-first-item-offset.patch @@ -0,0 +1,38 @@ +diff --git a/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerView.js b/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerView.js +index d7a3d84..ffcdad8 100644 +--- a/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerView.js ++++ b/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerView.js +@@ -142,9 +142,11 @@ const RecyclerViewComponent = (props, ref) => { + containerViewSizeRef.current = outerViewSize; + // firstChildViewLayout is already relative to the outer container, + // so its x/y directly gives the first item offset. +- const firstItemOffset = horizontal +- ? firstChildViewLayout.x +- : firstChildViewLayout.y; ++ const firstItemOffset = inverted ++ ? 0 ++ : horizontal ++ ? firstChildViewLayout.x ++ : firstChildViewLayout.y; + // Update the RecyclerView manager with window dimensions + recyclerViewManager.updateLayoutParams({ + width: horizontal ? outerViewSize.width : firstChildViewLayout.width, +diff --git a/node_modules/@shopify/flash-list/src/recyclerview/RecyclerView.tsx b/node_modules/@shopify/flash-list/src/recyclerview/RecyclerView.tsx +index ea801d2..8a7deff 100644 +--- a/node_modules/@shopify/flash-list/src/recyclerview/RecyclerView.tsx ++++ b/node_modules/@shopify/flash-list/src/recyclerview/RecyclerView.tsx +@@ -259,9 +259,11 @@ const RecyclerViewComponent = ( + + // firstChildViewLayout is already relative to the outer container, + // so its x/y directly gives the first item offset. +- const firstItemOffset = horizontal +- ? firstChildViewLayout.x +- : firstChildViewLayout.y; ++ const firstItemOffset = inverted ++ ? 0 ++ : horizontal ++ ? firstChildViewLayout.x ++ : firstChildViewLayout.y; + + // Update the RecyclerView manager with window dimensions + recyclerViewManager.updateLayoutParams( diff --git a/patches/@shopify/flash-list/@shopify+flash-list+2.3.0+005+fix-pending-children-blocking-measurements.patch b/patches/@shopify/flash-list/@shopify+flash-list+2.3.0+005+fix-pending-children-blocking-measurements.patch new file mode 100644 index 000000000000..6b96845c5875 --- /dev/null +++ b/patches/@shopify/flash-list/@shopify+flash-list+2.3.0+005+fix-pending-children-blocking-measurements.patch @@ -0,0 +1,68 @@ +diff --git a/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerView.js b/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerView.js +index ffcdad8..ee42f63 100644 +--- a/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerView.js ++++ b/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerView.js +@@ -166,9 +166,6 @@ const RecyclerViewComponent = (props, ref) => { + // eslint-disable-next-line react-hooks/exhaustive-deps + useLayoutEffect(() => { + var _a, _b; +- if (pendingChildIds.size > 0) { +- return; +- } + if (((_a = containerViewSizeRef.current) === null || _a === void 0 ? void 0 : _a.width) === 0 && + ((_b = containerViewSizeRef.current) === null || _b === void 0 ? void 0 : _b.height) === 0) { + return; +@@ -196,8 +193,17 @@ const RecyclerViewComponent = (props, ref) => { + } + if (recyclerViewManager.modifyChildrenLayout(layoutInfo, (_a = data === null || data === void 0 ? void 0 : data.length) !== null && _a !== void 0 ? _a : 0) && + !hasExceededMaxRendersWithoutCommit) { +- // Trigger re-render if layout modifications were made +- setRenderId((prev) => prev + 1); ++ if (pendingChildIds.size > 0) { ++ // When child FlashLists are still loading, avoid triggering a full ++ // RecyclerView re-render (setRenderId) to prevent cascading setState ++ // calls that could cause "Maximum update depth exceeded" errors. ++ // Instead, just commit the layout to update item positions in ++ // ViewHolderCollection without re-measuring. ++ (_b = viewHolderCollectionRef.current) === null || _b === void 0 ? void 0 : _b.commitLayout(); ++ } else { ++ // Trigger re-render if layout modifications were made ++ setRenderId((prev) => prev + 1); ++ } + } + else { + (_b = viewHolderCollectionRef.current) === null || _b === void 0 ? void 0 : _b.commitLayout(); +diff --git a/node_modules/@shopify/flash-list/src/recyclerview/RecyclerView.tsx b/node_modules/@shopify/flash-list/src/recyclerview/RecyclerView.tsx +index 8a7deff..b2bd67a 100644 +--- a/node_modules/@shopify/flash-list/src/recyclerview/RecyclerView.tsx ++++ b/node_modules/@shopify/flash-list/src/recyclerview/RecyclerView.tsx +@@ -287,9 +287,6 @@ const RecyclerViewComponent = ( + */ + // eslint-disable-next-line react-hooks/exhaustive-deps + useLayoutEffect(() => { +- if (pendingChildIds.size > 0) { +- return; +- } + const layoutInfo = Array.from(refHolder, ([index, viewHolderRef]) => { + const layout = measureItemLayout( + viewHolderRef.current!, +@@ -323,8 +320,17 @@ const RecyclerViewComponent = ( + recyclerViewManager.modifyChildrenLayout(layoutInfo, data?.length ?? 0) && + !hasExceededMaxRendersWithoutCommit + ) { +- // Trigger re-render if layout modifications were made +- setRenderId((prev) => prev + 1); ++ if (pendingChildIds.size > 0) { ++ // When child FlashLists are still loading, avoid triggering a full ++ // RecyclerView re-render (setRenderId) to prevent cascading setState ++ // calls that could cause "Maximum update depth exceeded" errors. ++ // Instead, just commit the layout to update item positions in ++ // ViewHolderCollection without re-measuring. ++ viewHolderCollectionRef.current?.commitLayout(); ++ } else { ++ // Trigger re-render if layout modifications were made ++ setRenderId((prev) => prev + 1); ++ } + } else { + viewHolderCollectionRef.current?.commitLayout(); + applyOffsetCorrection(); diff --git a/patches/@shopify/flash-list/@shopify+flash-list+2.3.0+006+fix-inverted-mvcp-android.patch b/patches/@shopify/flash-list/@shopify+flash-list+2.3.0+006+fix-inverted-mvcp-android.patch new file mode 100644 index 000000000000..814d54311362 --- /dev/null +++ b/patches/@shopify/flash-list/@shopify+flash-list+2.3.0+006+fix-inverted-mvcp-android.patch @@ -0,0 +1,114 @@ +diff --git a/node_modules/@shopify/flash-list/dist/recyclerview/hooks/useRecyclerViewController.js b/node_modules/@shopify/flash-list/dist/recyclerview/hooks/useRecyclerViewController.js +index 70f856a..52546f7 100644 +--- a/node_modules/@shopify/flash-list/dist/recyclerview/hooks/useRecyclerViewController.js ++++ b/node_modules/@shopify/flash-list/dist/recyclerview/hooks/useRecyclerViewController.js +@@ -1,5 +1,5 @@ + import { useCallback, useImperativeHandle, useMemo, useRef, useState, } from "react"; +-import { I18nManager } from "react-native"; ++import { I18nManager, Platform } from "react-native"; + import { adjustOffsetForRTL } from "../utils/adjustOffsetForRTL"; + import { PlatformConfig } from "../../native/config/PlatformHelper"; + import { WarningMessages } from "../../errors/WarningMessages"; +@@ -25,6 +25,8 @@ export function useRecyclerViewController(recyclerViewManager, ref, scrollViewRe + const isUnmounted = useUnmountFlag(); + const [_, setRenderId] = useState(0); + const pauseOffsetCorrection = useRef(false); ++ const pendingAndroidInvertedRafId = useRef(null); ++ const skipNextAndroidInvertedCorrection = useRef(false); + const lastDataLengthRef = useRef(recyclerViewManager.getDataLength()); + const { setTimeout } = useUnmountAwareTimeout(); + // Track the first visible item for maintaining scroll position +@@ -79,7 +81,7 @@ export function useRecyclerViewController(recyclerViewManager, ref, scrollViewRe + */ + const applyOffsetCorrection = useCallback(() => { + var _a, _b, _c; +- const { horizontal, data } = recyclerViewManager.props; ++ const { horizontal, data, inverted } = recyclerViewManager.props; + // Execute all pending callbacks from previous scroll offset updates + // This ensures any scroll operations that were waiting for render are completed + const callbacks = pendingScrollCallbacks.current; +@@ -91,6 +93,11 @@ export function useRecyclerViewController(recyclerViewManager, ref, scrollViewRe + currentDataLength > 0 && + recyclerViewManager.shouldMaintainVisibleContentPosition()) { + const hasDataChanged = currentDataLength !== lastDataLengthRef.current; ++ // Read and reset the skip flag so it never persists across multiple correction cycles ++ const shouldSkipAndroidInvertedCorrection = hasDataChanged && inverted && Platform.OS === 'android' && skipNextAndroidInvertedCorrection.current; ++ if (shouldSkipAndroidInvertedCorrection) { ++ skipNextAndroidInvertedCorrection.current = false; ++ } + // If we have a tracked first visible item, maintain its position + if (firstVisibleItemKey.current) { + const currentIndexOfFirstVisibleItem = (_a = recyclerViewManager +@@ -115,10 +122,31 @@ export function useRecyclerViewController(recyclerViewManager, ref, scrollViewRe + !pauseOffsetCorrection.current && + !recyclerViewManager.animationOptimizationsEnabled) { + // console.log("diff", diff, firstVisibleItemKey.current); +- if (PlatformConfig.supportsOffsetCorrection) { +- // console.log("scrollBy", diff); ++ const useAndroidInvertedFallback = hasDataChanged && inverted && Platform.OS === 'android'; ++ if (PlatformConfig.supportsOffsetCorrection && !useAndroidInvertedFallback) { + (_b = scrollAnchorRef.current) === null || _b === void 0 ? void 0 : _b.scrollBy(diff); + } ++ else if (useAndroidInvertedFallback) { ++ if (!shouldSkipAndroidInvertedCorrection) { ++ const scrollToParams = horizontal ++ ? { ++ x: recyclerViewManager.getAbsoluteLastScrollOffset() + diff, ++ animated: false, ++ } ++ : { ++ y: recyclerViewManager.getAbsoluteLastScrollOffset() + diff, ++ animated: false, ++ }; ++ if (pendingAndroidInvertedRafId.current !== null) { ++ cancelAnimationFrame(pendingAndroidInvertedRafId.current); ++ } ++ // rAF scrollTo to correct after native layout commits ++ pendingAndroidInvertedRafId.current = requestAnimationFrame(() => { ++ pendingAndroidInvertedRafId.current = null; ++ (_c = scrollViewRef.current) === null || _c === void 0 ? void 0 : _c.scrollTo(scrollToParams); ++ }); ++ } ++ } + else { + const scrollToParams = horizontal + ? { +@@ -162,6 +190,13 @@ export function useRecyclerViewController(recyclerViewManager, ref, scrollViewRe + * Handles RTL layouts and first item offset adjustments. + */ + scrollToOffset: ({ offset, animated, skipFirstItemOffset = true, }) => { ++ if (pendingAndroidInvertedRafId.current !== null) { ++ cancelAnimationFrame(pendingAndroidInvertedRafId.current); ++ pendingAndroidInvertedRafId.current = null; ++ } ++ if (recyclerViewManager.props.inverted && Platform.OS === 'android') { ++ skipNextAndroidInvertedCorrection.current = true; ++ } + const { horizontal } = recyclerViewManager.props; + if (scrollViewRef.current) { + // Adjust offset for RTL layouts in horizontal mode +@@ -205,6 +240,13 @@ export function useRecyclerViewController(recyclerViewManager, ref, scrollViewRe + * Scrolls to the end of the list. + */ + scrollToEnd: async ({ animated } = {}) => { ++ if (pendingAndroidInvertedRafId.current !== null) { ++ cancelAnimationFrame(pendingAndroidInvertedRafId.current); ++ pendingAndroidInvertedRafId.current = null; ++ } ++ if (recyclerViewManager.props.inverted && Platform.OS === 'android') { ++ skipNextAndroidInvertedCorrection.current = true; ++ } + const { data } = recyclerViewManager.props; + if (data && data.length > 0) { + const lastIndex = data.length - 1; +@@ -235,6 +277,10 @@ export function useRecyclerViewController(recyclerViewManager, ref, scrollViewRe + * Returns a Promise that resolves when the scroll is complete. + */ + scrollToIndex: ({ index, animated, viewPosition, viewOffset, }) => { ++ if (pendingAndroidInvertedRafId.current !== null) { ++ cancelAnimationFrame(pendingAndroidInvertedRafId.current); ++ pendingAndroidInvertedRafId.current = null; ++ } + return new Promise((resolve) => { + const { horizontal } = recyclerViewManager.props; + if (scrollViewRef.current && diff --git a/patches/@shopify/flash-list/details.md b/patches/@shopify/flash-list/details.md index 6012a1c00a87..45570d16761f 100644 --- a/patches/@shopify/flash-list/details.md +++ b/patches/@shopify/flash-list/details.md @@ -20,3 +20,31 @@ - Upstream PR/issue: TBD - E/App issue: https://github.com/Expensify/App/issues/83976 - PR introducing patch: https://github.com/Expensify/App/pull/84887 + +### [@shopify+flash-list+2.3.0+003+fix-inverted-scroll-direction-on-web.patch](@shopify+flash-list+2.3.0+003+fix-inverted-scroll-direction-on-web.patch) + +- Reason: Fixes inverted scroll direction on web. FlashList uses `scaleY: -1` / `scaleX: -1` CSS transform to visually invert the list, but the browser's native wheel scroll doesn't flip accordingly — scrolling down visually scrolls up and vice versa. This patch adds a `useEffect` in `RecyclerView` that attaches a `wheel` event listener on web when `inverted` is true, intercepting the event, negating the scroll delta, and manually adjusting `scrollTop`/`scrollLeft`. Mirrors the same fix applied in react-native-web's `VirtualizedList`. +- Upstream PR/issue: TBD +- E/App issue: https://github.com/Expensify/App/issues/33725 +- PR introducing patch: https://github.com/Expensify/App/pull/85114 + +### [@shopify+flash-list+2.3.0+004+fix-inverted-first-item-offset.patch](@shopify+flash-list+2.3.0+004+fix-inverted-first-item-offset.patch) + +- Reason: Fixes inverted lists rendering only a few items with white space on scroll. FlashList's `RecyclerView` measures `firstItemOffset` by calling `measureFirstChildLayout` relative to the outer container. When `inverted` is true, the outer container has `scaleY: -1`, which flips the coordinate system — causing the measured y-offset to equal the container height instead of 0. This makes all scroll offsets negative after adjustment (`adjustedOffset = scrollOffset - firstItemOffset`), so the viewport thinks it's in negative space where no items exist. Only items caught by the draw-distance buffer render. The fix forces `firstItemOffset` to 0 for inverted lists, since the transform already handles visual inversion. +- Upstream PR/issue: TBD +- E/App issue: https://github.com/Expensify/App/issues/33725 +- PR introducing patch: https://github.com/Expensify/App/pull/85114 + +### [@shopify+flash-list+2.3.0+005+fix-pending-children-blocking-measurements.patch](@shopify+flash-list+2.3.0+005+fix-pending-children-blocking-measurements.patch) + +- Reason: Fixes items overlapping on initial load when a list contains nested FlashLists (e.g. a horizontal list inside a chat message). The `RecyclerView` layout measurement `useLayoutEffect` had an early return when `pendingChildIds.size > 0` — while any nested FlashList was still doing its progressive first layout, the parent list skipped ALL measurement processing. This meant newly added items stayed at estimated positions (wrong heights/y-offsets) while being visible (`opacity: 1`), causing overlap. The fix moves the `pendingChildIds` check so that measurements are always collected and processed by the layout manager, but when children are pending, `commitLayout()` is called instead of `setRenderId()`. This updates item positions in `ViewHolderCollection` without triggering a full `RecyclerView` re-render, avoiding the cascading `setState` calls that the original guard was meant to prevent. +- Upstream PR/issue: TBD +- E/App issue: https://github.com/Expensify/App/issues/33725 +- PR introducing patch: https://github.com/Expensify/App/pull/85114 + +### [@shopify+flash-list+2.3.0+006+fix-inverted-mvcp-android.patch](@shopify+flash-list+2.3.0+006+fix-inverted-mvcp-android.patch) + +- Reason: Fixes `maintainVisibleContentPosition` not working on Android for inverted lists when items are prepended (e.g. new messages arriving, or `useFlashListScrollKey` switching from sliced to full data). FlashList's offset correction uses a `ScrollAnchor` component — an invisible absolutely-positioned element whose `top` changes to trigger the native `maintainVisibleContentPosition` on the ScrollView. On Android, where inversion uses `rotate: 180deg` (vs `scaleY: -1` on iOS), this mechanism silently fails: the anchor position changes but the native ScrollView does not adjust its scroll offset. The fix detects the specific case (`inverted && Platform.OS === 'android' && hasDataChanged`) and bypasses `ScrollAnchor` in favor of a deferred `scrollTo` via `requestAnimationFrame`, which fires after the native layout has committed the new content size. Non-inverted lists, iOS, web, and layout-only corrections (no data change) are unaffected and continue using the original code paths. +- Upstream PR/issue: TBD +- E/App issue: https://github.com/Expensify/App/issues/33725 +- PR introducing patch: https://github.com/Expensify/App/pull/85114 diff --git a/patches/react-compiler-healthcheck/details.md b/patches/react-compiler-healthcheck/details.md deleted file mode 100644 index aea119f68fe7..000000000000 --- a/patches/react-compiler-healthcheck/details.md +++ /dev/null @@ -1,38 +0,0 @@ -# `react-compiler-healthcheck` patches - -### [react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+001+add-verbose-error-logging-option.patch](react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+001+add-verbose-error-logging-option.patch) - -- Reason: - - ``` - This patch adds verbose error logging option. - ``` - -- Upstream PR/issue: https://github.com/facebook/react/pull/29080 and https://github.com/facebook/react/pull/29851 -- E/App issue: https://github.com/Expensify/App/issues/44384 -- PR introducing patch: https://github.com/Expensify/App/pull/44460 - -### [react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+002+enable-ref-identifiers.patch](react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+002+enable-ref-identifiers.patch) - -- Reason: - - ``` - This patch allows mutating refs in certain components. - ``` - -- Upstream PR/issue: https://github.com/facebook/react/pull/29916 -- E/App issue: Same as the PR. -- PR introducing patch: https://github.com/Expensify/App/pull/45464 - - -### [react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+003+json.patch](react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+003+json.patch) - -- Reason: - - ``` - This patch adds --json option to healthcheck CLI. - ``` - -- Upstream PR/issue: 🛑, commented in the App PR https://github.com/Expensify/App/pull/45915#issuecomment-3346345841 -- E/App issue: https://github.com/Expensify/App/pull/45464 -- PR introducing patch: https://github.com/Expensify/App/pull/45915 \ No newline at end of file diff --git a/patches/react-compiler-healthcheck/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+001+add-verbose-error-logging-option.patch b/patches/react-compiler-healthcheck/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+001+add-verbose-error-logging-option.patch deleted file mode 100644 index 03b386587338..000000000000 --- a/patches/react-compiler-healthcheck/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+001+add-verbose-error-logging-option.patch +++ /dev/null @@ -1,90 +0,0 @@ -diff --git a/node_modules/react-compiler-healthcheck/dist/index.js b/node_modules/react-compiler-healthcheck/dist/index.js -index 5a4060d..460339b 100755 ---- a/node_modules/react-compiler-healthcheck/dist/index.js -+++ b/node_modules/react-compiler-healthcheck/dist/index.js -@@ -56969,7 +56969,7 @@ var reactCompilerCheck = { - compile(source, path); - } - }, -- report() { -+ report(verbose) { - const totalComponents = - SucessfulCompilation.length + - countUniqueLocInEvents(OtherFailures) + -@@ -56979,6 +56979,50 @@ var reactCompilerCheck = { - `Successfully compiled ${SucessfulCompilation.length} out of ${totalComponents} components.` - ) - ); -+ -+ if (verbose) { -+ for (const compilation of [...SucessfulCompilation, ...ActionableFailures, ...OtherFailures]) { -+ const filename = compilation.fnLoc?.filename; -+ -+ if (compilation.kind === "CompileSuccess") { -+ const name = compilation.fnName; -+ const isHook = name?.startsWith('use'); -+ -+ if (name) { -+ console.log( -+ chalk.green( -+ `Successfully compiled ${isHook ? "hook" : "component" } [${name}](${filename})` -+ ) -+ ); -+ } else { -+ console.log(chalk.green(`Successfully compiled ${compilation.fnLoc?.filename}`)); -+ } -+ } -+ -+ if (compilation.kind === "CompileError") { -+ const { reason, severity, loc } = compilation.detail; -+ -+ const lnNo = loc.start?.line; -+ const colNo = loc.start?.column; -+ -+ const isTodo = severity === ErrorSeverity.Todo; -+ -+ console.log( -+ chalk[isTodo ? 'yellow' : 'red']( -+ `Failed to compile ${ -+ filename -+ }${ -+ lnNo !== undefined ? `:${lnNo}${ -+ colNo !== undefined ? `:${colNo}` : "" -+ }.` : "" -+ }` -+ ), -+ chalk[isTodo ? 'yellow' : 'red'](reason? `Reason: ${reason}` : "") -+ ); -+ console.log("\n"); -+ } -+ } -+ } - }, - }; - const JsFileExtensionRE = /(js|ts|jsx|tsx)$/; -@@ -57015,9 +57059,16 @@ function main() { - type: 'string', - default: '**/+(*.{js,mjs,jsx,ts,tsx}|package.json)', - }) -+ .option('verbose', { -+ description: 'run with verbose logging', -+ type: 'boolean', -+ default: false, -+ alias: 'v', -+ }) - .parseSync(); - const spinner = ora('Checking').start(); - let src = argv.src; -+ let verbose = argv.verbose; - const globOptions = { - onlyFiles: true, - ignore: [ -@@ -57037,7 +57088,7 @@ function main() { - libraryCompatCheck.run(source, path); - } - spinner.stop(); -- reactCompilerCheck.report(); -+ reactCompilerCheck.report(verbose); - strictModeCheck.report(); - libraryCompatCheck.report(); - }); diff --git a/patches/react-compiler-healthcheck/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+002+enable-ref-identifiers.patch b/patches/react-compiler-healthcheck/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+002+enable-ref-identifiers.patch deleted file mode 100644 index 8ae46e379619..000000000000 --- a/patches/react-compiler-healthcheck/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+002+enable-ref-identifiers.patch +++ /dev/null @@ -1,28 +0,0 @@ -diff --git a/node_modules/react-compiler-healthcheck/dist/index.js b/node_modules/react-compiler-healthcheck/dist/index.js -index 460339b..17b0f96 100755 ---- a/node_modules/react-compiler-healthcheck/dist/index.js -+++ b/node_modules/react-compiler-healthcheck/dist/index.js -@@ -56902,6 +56902,9 @@ const COMPILER_OPTIONS = { - noEmit: true, - compilationMode: 'infer', - panicThreshold: 'critical_errors', -+ environment: { -+ enableTreatRefLikeIdentifiersAsRefs: true, -+ }, - logger: logger, - }; - function isActionableDiagnostic(detail) { -diff --git a/node_modules/react-compiler-healthcheck/src/checks/reactCompiler.ts b/node_modules/react-compiler-healthcheck/src/checks/reactCompiler.ts -index 3094548..fd05b76 100644 ---- a/node_modules/react-compiler-healthcheck/src/checks/reactCompiler.ts -+++ b/node_modules/react-compiler-healthcheck/src/checks/reactCompiler.ts -@@ -50,6 +50,9 @@ const COMPILER_OPTIONS: Partial = { - noEmit: true, - compilationMode: 'infer', - panicThreshold: 'critical_errors', -+ environment: { -+ enableTreatRefLikeIdentifiersAsRefs: true, -+ }, - logger, - }; - diff --git a/patches/react-compiler-healthcheck/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+003+json.patch b/patches/react-compiler-healthcheck/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+003+json.patch deleted file mode 100644 index 246351351195..000000000000 --- a/patches/react-compiler-healthcheck/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+003+json.patch +++ /dev/null @@ -1,72 +0,0 @@ -diff --git a/node_modules/react-compiler-healthcheck/dist/index.js b/node_modules/react-compiler-healthcheck/dist/index.js -index 17b0f96..e386e34 100755 ---- a/node_modules/react-compiler-healthcheck/dist/index.js -+++ b/node_modules/react-compiler-healthcheck/dist/index.js -@@ -56972,16 +56972,28 @@ var reactCompilerCheck = { - compile(source, path); - } - }, -- report(verbose) { -+ report(verbose, json) { - const totalComponents = - SucessfulCompilation.length + - countUniqueLocInEvents(OtherFailures) + - countUniqueLocInEvents(ActionableFailures); -- console.log( -- chalk.green( -- `Successfully compiled ${SucessfulCompilation.length} out of ${totalComponents} components.` -- ) -- ); -+ if (!json) { -+ console.log( -+ chalk.green( -+ `Successfully compiled ${SucessfulCompilation.length} out of ${totalComponents} components.` -+ ) -+ ); -+ } -+ -+ if (json) { -+ const extractFileName = (output) => output.fnLoc.filename; -+ const successfulFiles = SucessfulCompilation.map(extractFileName); -+ const unsuccessfulFiles = [...new Set([...OtherFailures, ...ActionableFailures].map(extractFileName))]; -+ console.log(JSON.stringify({ -+ success: successfulFiles, -+ failure: unsuccessfulFiles, -+ })); -+ } - - if (verbose) { - for (const compilation of [...SucessfulCompilation, ...ActionableFailures, ...OtherFailures]) { -@@ -57068,10 +57080,17 @@ function main() { - default: false, - alias: 'v', - }) -+ .option('json', { -+ description: 'print a list of compiled/not-compiled files as JSON', -+ type: 'boolean', -+ default: false, -+ alias: 'j', -+ }) - .parseSync(); - const spinner = ora('Checking').start(); - let src = argv.src; - let verbose = argv.verbose; -+ let json = argv.json; - const globOptions = { - onlyFiles: true, - ignore: [ -@@ -57091,9 +57110,11 @@ function main() { - libraryCompatCheck.run(source, path); - } - spinner.stop(); -- reactCompilerCheck.report(verbose); -- strictModeCheck.report(); -- libraryCompatCheck.report(); -+ reactCompilerCheck.report(verbose, json); -+ if (!json) { -+ strictModeCheck.report(); -+ libraryCompatCheck.report(); -+ } - }); - } - main(); diff --git a/patches/react-native-screens/details.md b/patches/react-native-screens/details.md new file mode 100644 index 000000000000..e9986d919fec --- /dev/null +++ b/patches/react-native-screens/details.md @@ -0,0 +1,8 @@ +# `react-native-screens` patches + +### [react-native-screens+4.15.4+001+fix-lifecycle-events-in-fragment-host.patch](react-native-screens+4.15.4+001+fix-lifecycle-events-in-fragment-host.patch) + +- Reason: In HybridApp, React Native is hosted inside a `ReactNativeFragment`, which causes `ScreenFragment.dispatchViewAnimationEvent()` to silently dismiss lifecycle events for root screen fragments. This prevents `transitionStart`/`transitionEnd` from being emitted, which breaks `TransitionTracker` (`src/libs/Navigation/TransitionTracker.ts`). The fix allows event dispatch when the parent fragment is not a `ScreenFragment`. +- Upstream PR/issue: https://github.com/software-mansion/react-native-screens/pull/3854 — once merged and released, bump the version and remove this patch. +- E/App issue: 🛑 +- PR Introducing Patch: https://github.com/Expensify/App/pull/85759 diff --git a/patches/react-native-screens/react-native-screens+4.15.4+001+fix-lifecycle-events-in-fragment-host.patch b/patches/react-native-screens/react-native-screens+4.15.4+001+fix-lifecycle-events-in-fragment-host.patch new file mode 100644 index 000000000000..7060a0baaa35 --- /dev/null +++ b/patches/react-native-screens/react-native-screens+4.15.4+001+fix-lifecycle-events-in-fragment-host.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.kt b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.kt +index 65c6e30..3b9f2e2 100644 +--- a/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.kt ++++ b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.kt +@@ -293,7 +293,7 @@ open class ScreenFragment : + // check for `isTransitioning` should be enough since the child's animation should take only + // 20ms due to always being `StackAnimation.NONE` when nested stack being pushed + val parent = parentFragment +- if (parent == null || (parent is ScreenFragment && !parent.isTransitioning)) { ++ if (parent == null || parent !is ScreenFragment || (parent is ScreenFragment && !parent.isTransitioning)) { + // onViewAnimationStart/End is triggered from View#onAnimationStart/End method of the fragment's root + // view. We override an appropriate method of the StackFragment's + // root view in order to achieve this. diff --git a/scripts/react-compiler-compliance-check.ts b/scripts/react-compiler-compliance-check.ts index 716b466404e2..0acaf02b186b 100644 --- a/scripts/react-compiler-compliance-check.ts +++ b/scripts/react-compiler-compliance-check.ts @@ -1,859 +1,326 @@ #!/usr/bin/env ts-node -/* eslint-disable max-classes-per-file */ /** - * React Compiler Compliance Checker + * React Compiler Compliance Check * - * This script tracks which components can be compiled by React Compiler and which cannot. - * It provides both CI and local development tools to enforce Rules of React compliance. + * Checks whether React components and hooks compile with React Compiler. + * Two modes: + * - `check ` -- check specific files, report per-file status + * - `check-changed` -- check files changed in a PR, enforce two rules: + * 1. New files with components/hooks must compile + * 2. Modified files must not regress (compiled on main -> must compile on PR) */ -import {execSync} from 'child_process'; -import fs, {readFileSync} from 'fs'; +import {transformSync} from '@babel/core'; +import fs from 'fs'; import path from 'path'; -import type {TupleToUnion} from 'type-fest'; import CLI from './utils/CLI'; -import EslintUtils from './utils/EslintUtils'; import FileUtils from './utils/FileUtils'; import Git from './utils/Git'; -import type {DiffResult} from './utils/Git'; -import {log, bold as logBold, error as logError, info as logInfo, note as logNote, success as logSuccess, warn as logWarn} from './utils/Logger'; - -type CompilerResults = { - success: Set; - errors: Map; - errorsForAddedFiles: Map; - errorsForModifiedFiles: Map; - manualMemoErrors: Map; - suppressedErrors: Map; +import {log, error as logError, info as logInfo, success as logSuccess, warn as logWarn} from './utils/Logger'; + +// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment +const ReactCompilerConfig = require('../config/babel/reactCompilerConfig'); + +type SourceLocation = { + start: {line: number; column: number}; + end: {line: number; column: number}; }; type CompilerError = { - file: string; - line: number; - column: number; - reason?: string; + reason: string; + severity: string; + loc?: SourceLocation; + fnLoc?: SourceLocation; }; -type ManualMemoizationError = { - keyword: string; - line: number; - column: number; +type CompilationResult = { + status: 'compiled' | 'failed' | 'no-components'; + errors: CompilerError[]; }; -type CheckMode = 'static' | 'incremental'; - -type BaseCheckParameters = { - remote?: string; - verbose?: boolean; +type CompilerLogEvent = { + kind: string; + fnLoc?: SourceLocation; + fnName?: string; + detail?: { + severity?: string; + reason?: string; + loc?: SourceLocation; + }; }; -type CheckParameters = BaseCheckParameters & { - mode?: CheckMode; - files?: string[]; -}; +const FILE_EXTENSIONS = ['.ts', '.tsx']; -type CheckOptions = { - mode: CheckMode; - verbose: boolean; -}; +const IS_CI = process.env.CI === 'true'; /** - * Handles running react-compiler-healthcheck and parsing its output. + * Check if a source string compiles with React Compiler. + * Returns the compilation status and any errors with their details. */ -class ReactCompilerHealthcheck { - private static readonly SUPPRESSED_ERRORS = [ - // This error is caused by an internal limitation of React Compiler - // https://github.com/facebook/react/issues/29583 - '(BuildHIR::lowerExpression) Expected Identifier, got MemberExpression key in ObjectExpression', - ] as const; - - private static readonly OUTPUT_REGEXES = { - SUCCESS: /Successfully compiled (?:hook|component) \[([^\]]+)\]\(([^)]+)\)/, - ERROR_WITH_REASON: /Failed to compile ([^:]+):(\d+):(\d+)\. Reason: (.+)/, - ERROR_WITHOUT_REASON: /Failed to compile ([^:]+):(\d+):(\d+)\./, - REASON: /Reason: (.+)/, - } as const; - - /** - * Run the react-compiler-healthcheck CLI tool and parse its output. - */ - static run(src?: string): CompilerResults { - const srcArg = src ? `--src "${src}"` : ''; - const output = execSync(`npx react-compiler-healthcheck ${srcArg} --verbose`, { - encoding: 'utf8', - cwd: process.cwd(), - }); - - return this.parseOutput(output); - } - - /** - * Parse the output of the react-compiler-healthcheck command. - */ - private static parseOutput(output: string): CompilerResults { - const lines = output.split('\n'); - - const results: CompilerResults = { - success: new Set(), - errors: new Map(), - errorsForAddedFiles: new Map(), - errorsForModifiedFiles: new Map(), - manualMemoErrors: new Map(), - suppressedErrors: new Map(), - }; - - let currentErrorWithoutReason: CompilerError | null = null; - - for (const line of lines) { - const successMatch = line.match(this.OUTPUT_REGEXES.SUCCESS); - if (successMatch) { - const filePath = successMatch[2]; - results.success.add(filePath); - continue; - } - - const errorWithReasonMatch = line.match(this.OUTPUT_REGEXES.ERROR_WITH_REASON); - if (errorWithReasonMatch) { - const newError: CompilerError = { - file: errorWithReasonMatch[1], - line: parseInt(errorWithReasonMatch[2], 10), - column: parseInt(errorWithReasonMatch[3], 10), - reason: errorWithReasonMatch[4], - }; - - currentErrorWithoutReason = null; - - if (this.shouldSuppressError(newError.reason)) { - this.addOrUpdateError(results.suppressedErrors, newError); - continue; - } - - this.addOrUpdateError(results.errors, newError); - } - - const errorWithoutReasonMatch = line.match(this.OUTPUT_REGEXES.ERROR_WITHOUT_REASON); - if (errorWithoutReasonMatch) { - const newError: CompilerError = { - file: errorWithoutReasonMatch[1], - line: parseInt(errorWithoutReasonMatch[2], 10), - column: parseInt(errorWithoutReasonMatch[3], 10), - }; - - currentErrorWithoutReason = newError; - this.addOrUpdateError(results.errors, newError); - continue; - } - - const reasonMatch = line.match(this.OUTPUT_REGEXES.REASON); - if (reasonMatch && currentErrorWithoutReason) { - const reason = reasonMatch[1]; - - const currentError: CompilerError = { - file: currentErrorWithoutReason.file, - line: currentErrorWithoutReason.line, - column: currentErrorWithoutReason.column, - reason, - }; - - currentErrorWithoutReason = null; - - if (this.shouldSuppressError(reason)) { - this.addOrUpdateError(results.suppressedErrors, currentError); - continue; - } - - this.addOrUpdateError(results.errors, currentError); - } - } - - results.success = new Set(Array.from(results.success).sort((a, b) => a.localeCompare(b))); - results.errors = this.sortErrors(results.errors); - results.suppressedErrors = this.sortErrors(results.suppressedErrors); - - return results; - } +function checkReactCompilerCompliance(source: string, filename: string): CompilationResult { + let hasError = false; + let hasSuccess = false; + const errors: CompilerError[] = []; - private static addOrUpdateError(errorMap: Map, newError: CompilerError): boolean { - const key = this.getErrorKey(newError); - const existingError = errorMap.get(key); - - if (existingError) { - const isReasonSet = !!existingError.reason; - const isNewReasonSet = !!newError.reason; - if (!isReasonSet && isNewReasonSet) { - errorMap.set(key, newError); - return true; - } - - return false; - } - - errorMap.set(key, newError); - return true; - } - - private static sortErrors(errors: Map) { - const arr = Array.from(errors.entries()); - arr.sort(([, a], [, b]) => { - const keyA = this.getErrorKey(a); - const keyB = this.getErrorKey(b); - return keyA.localeCompare(keyB); + try { + transformSync(source, { + filename, + ast: false, + code: false, + configFile: false, + babelrc: false, + parserOpts: { + plugins: ['typescript', 'jsx'], + }, + plugins: [ + [ + 'babel-plugin-react-compiler', + { + ...ReactCompilerConfig, + panicThreshold: 'none', + noEmit: true, + logger: { + logEvent(_filename: string, event: CompilerLogEvent) { + if (event.kind === 'CompileError') { + hasError = true; + if (event.detail?.reason) { + errors.push({ + reason: event.detail.reason ?? 'Unknown compiler error', + severity: event.detail.severity ?? 'Error', + loc: event.detail.loc, + fnLoc: event.fnLoc, + }); + } + } + if (event.kind === 'CompileSuccess') { + hasSuccess = true; + } + }, + }, + }, + ], + ], + }); + } catch (e) { + hasError = true; + errors.push({ + reason: e instanceof Error ? e.message : String(e), + severity: 'Error', }); - return new Map(arr); } - private static shouldSuppressError(reason: string | undefined): boolean { - if (!reason) { - return false; - } - - return this.SUPPRESSED_ERRORS.some((suppressedError) => reason.includes(suppressedError)); + if (hasError) { + return {status: 'failed', errors}; } - - static getErrorKey({file, line, column}: CompilerError): string { - const isLineSet = line !== undefined; - const isLineAndColumnSet = isLineSet && column !== undefined; - - return file + (isLineSet ? `:${line}` : '') + (isLineAndColumnSet ? `:${column}` : ''); + if (hasSuccess) { + return {status: 'compiled', errors: []}; } + return {status: 'no-components', errors: []}; +} - static getErrorsByFile(errors: Map) { - const errorsByFile = new Map>(); - for (const [key, error] of errors.entries()) { - if (!errorsByFile.has(error.file)) { - errorsByFile.set(error.file, new Map()); - } - errorsByFile.get(error.file)?.set(key, error); - } - - const filesWithErrors = new Set(errorsByFile.keys()); - - return { - errorsByFile, - filesWithErrors, - }; +function formatErrorLocation(filename: string, error: CompilerError): string { + const loc = error.loc ?? error.fnLoc; + if (loc) { + return `${filename}:${loc.start.line}:${loc.start.column}`; } + return filename; } -/** - * Analyzes git diffs to filter compiler results to only changed lines. - */ -class DiffAnalyzer { - private static readonly ESLINT_LINT_RULES = ['react-hooks'] as const; - - /** - * Filter compiler results to only include errors for lines that were changed in the git diff. - */ - static async filterResultsByDiff(results: CompilerResults, mainBaseCommitHash: string, diffResult: DiffResult, {verbose}: CheckOptions): Promise { - logInfo(`Filtering results by diff between ${mainBaseCommitHash} and the working tree...`); - - if (!diffResult.hasChanges) { - return { - success: new Set(), - errors: new Map(), - errorsForAddedFiles: new Map(), - errorsForModifiedFiles: new Map(), - manualMemoErrors: new Map(), - suppressedErrors: new Map(), - }; - } - - const changedLinesMap = this.buildChangedLinesMap(diffResult); - const {filesWithEslintDisable, linesWithEslintDisableNextLine} = this.detectEslintDisables(diffResult); - - const filterErrorsByChangedLines = (errors: Map) => { - const filteredErrors = new Map(); - - for (const [key, error] of errors) { - const changedLines = changedLinesMap.get(error.file); - - if (!changedLines) { - continue; - } - - if (filesWithEslintDisable.has(error.file)) { - filteredErrors.set(key, error); - continue; - } - - if (error.line !== undefined) { - const isLineChanged = changedLines.has(error.line); - const isLineEslintDisabled = linesWithEslintDisableNextLine.get(error.file)?.has(error.line); - - if (isLineChanged || isLineEslintDisabled) { - filteredErrors.set(key, error); - } - continue; - } - - filteredErrors.set(key, error); - } - - return filteredErrors; - }; - - const filteredErrors = filterErrorsByChangedLines(results.errors); - const filteredSuppressedErrors = filterErrorsByChangedLines(results.suppressedErrors); - - const changedFiles = new Set(diffResult.files.map((file) => file.filePath)); - const filteredSuccesses = new Set(); - for (const file of results.success) { - if (!changedFiles.has(file)) { - continue; - } - filteredSuccesses.add(file); - } - - if (filteredErrors.size === 0) { - logInfo('No errors remain after filtering by diff.'); - } else { - logInfo(`${filteredErrors.size} out of ${results.errors.size} errors remain after filtering by diff.`); - } - - if (verbose) { - if (filteredSuppressedErrors.size === 0) { - logInfo('No suppressed errors remain after filtering by diff.'); - } else { - logInfo(`${filteredSuppressedErrors.size} out of ${results.suppressedErrors.size} successes remain after filtering by diff.`); - } - - if (filteredSuccesses.size === 0) { - logInfo('No successes remain after filtering by diff.'); - } else { - logInfo(`${filteredSuccesses.size} out of ${results.success.size} successes remain after filtering by diff.`); - } - } - - return { - success: filteredSuccesses, - errors: filteredErrors, - errorsForAddedFiles: new Map(), - errorsForModifiedFiles: new Map(), - manualMemoErrors: new Map(), - suppressedErrors: filteredSuppressedErrors, - }; +function printErrors(filename: string, errors: CompilerError[]): void { + if (IS_CI) { + console.log(`::group::${filename} (${errors.length} error${errors.length === 1 ? '' : 's'})`); } - - private static buildChangedLinesMap(diffResult: DiffResult): Map> { - const changedLinesMap = new Map>(); - - for (const file of diffResult.files) { - const changedLines = new Set([...file.addedLines, ...file.modifiedLines]); - changedLinesMap.set(file.filePath, changedLines); - } - - return changedLinesMap; + for (const error of errors) { + const location = formatErrorLocation(filename, error); + logError(` ${location}: ${error.reason}`); } - - private static detectEslintDisables(diffResult: DiffResult): { - filesWithEslintDisable: Set; - linesWithEslintDisableNextLine: Map>; - } { - const filesWithEslintDisable = new Set(); - const linesWithEslintDisableNextLine = new Map>(); - - for (const file of diffResult.files) { - for (const hunk of file.hunks) { - for (const line of hunk.lines) { - if (EslintUtils.hasEslintDisableComment(line.content, true, [...this.ESLINT_LINT_RULES])) { - filesWithEslintDisable.add(file.filePath); - } - - if (EslintUtils.hasEslintDisableComment(line.content, false, [...this.ESLINT_LINT_RULES])) { - if (!linesWithEslintDisableNextLine.has(file.filePath)) { - linesWithEslintDisableNextLine.set(file.filePath, new Set()); - } - - const disabledLines = linesWithEslintDisableNextLine.get(file.filePath); - if (!disabledLines) { - continue; - } - - const reactCompilerErrorLineNumber = line.type === 'removed' ? line.number + hunk.newCount : line.number + hunk.newCount + 1; - disabledLines.add(reactCompilerErrorLineNumber); - } - } - } - } - - return {filesWithEslintDisable, linesWithEslintDisableNextLine}; + if (IS_CI) { + console.log('::endgroup::'); } } /** - * Checks for manual memoization in files that should use automatic memoization. + * Check specific files and report per-file status. */ -class ManualMemoizationChecker { - static readonly PATTERNS = { - memo: /\b(?:React\.)?memo\s*\(/g, - useMemo: /\b(?:React\.)?useMemo\s*\(/g, - useCallback: /\b(?:React\.)?useCallback\s*\(/g, - } as const; - - private static readonly FILE_EXTENSIONS = ['.tsx', '.jsx'] as const; +function checkFiles(inputs: string[], verbose: boolean): boolean { + const files = FileUtils.resolveFilePaths(inputs, FILE_EXTENSIONS); - private static readonly NO_MEMO_DIRECTIVE_PATTERN = /["']use no memo["']\s*;?/; - - static getErrorMessage(keyword: keyof typeof this.PATTERNS): string { - return `Found a manual memoization usage of \`${keyword}\`. Newly added React component files must not contain any manual memoization and instead be auto-memoized by React Compiler. Remove \`${keyword}\` or disable automatic memoization by adding the \`"use no memo";\` directive at the beginning of the component and give a reason why automatic memoization is not applicable.`; + if (files.length === 0) { + logWarn(`No ${FILE_EXTENSIONS.join('/')} files found matching the provided paths.`); + return true; } - /** - * Split errors by file diff type and check for manual memoization violations. - */ - static splitErrorsBasedOnFileDiffType({success, errors: reactCompilerErrors}: CompilerResults, diffResult: DiffResult) { - const {filesWithErrors, errorsByFile} = ReactCompilerHealthcheck.getErrorsByFile(reactCompilerErrors); - - const {addedFiles, enforcedAutoMemoFiles} = this.categorizeFiles(diffResult, success, filesWithErrors); - - const reactCompilerErrorsForModifiedFiles = reactCompilerErrors; - const reactCompilerErrorsForAddedFiles = new Map(); - - for (const file of filesWithErrors) { - if (enforcedAutoMemoFiles.has(file)) { - continue; - } - - const errors = errorsByFile.get(file); - - if (addedFiles.has(file)) { - for (const [errorKey, error] of errors?.entries() ?? []) { - reactCompilerErrorsForAddedFiles.set(errorKey, error); - reactCompilerErrors.delete(errorKey); + let hasFailure = false; + + for (const file of files) { + const source = fs.readFileSync(file, 'utf8'); + const result = checkReactCompilerCompliance(source, file); + + switch (result.status) { + case 'compiled': + logSuccess(`COMPILED ${file}`); + break; + case 'failed': + logError(`FAILED ${file}`); + printErrors(file, result.errors); + hasFailure = true; + break; + case 'no-components': + default: + if (verbose) { + logInfo(`SKIPPED ${file} (no components or hooks)`); } - } - } - - const manualMemoErrors = this.findViolations(enforcedAutoMemoFiles); - - return { - manualMemoErrors, - reactCompilerErrorsForModifiedFiles, - reactCompilerErrorsForAddedFiles, - }; - } - - private static categorizeFiles( - diffResult: DiffResult, - successFiles: Set, - filesWithErrors: Set, - ): { - addedFiles: Set; - enforcedAutoMemoFiles: Set; - } { - const enforcedAutoMemoFiles = new Set(); - const addedFiles = new Set(); - - for (const file of diffResult.files) { - const filePath = file.filePath; - - filesWithErrors.add(filePath); - - const isAddedFile = file.diffType === 'added'; - if (isAddedFile) { - addedFiles.add(filePath); - } - - const isReactComponentSourceFile = this.FILE_EXTENSIONS.some((extension) => filePath.endsWith(extension)); - const isSuccessfullyCompiled = successFiles.has(filePath); - - if (isReactComponentSourceFile && isSuccessfullyCompiled && isAddedFile) { - enforcedAutoMemoFiles.add(filePath); - } + break; } - - return {addedFiles, enforcedAutoMemoFiles}; } - private static findViolations(files: Set): Map { - const manualMemoErrors = new Map(); - - for (const file of files) { - let source: string | null = null; - try { - const absolutePath = path.join(process.cwd(), file); - source = readFileSync(absolutePath, 'utf8'); - } catch (error) { - logWarn(`Unable to read ${file} while enforcing new component rules.`, error); - } - - if (!source || this.NO_MEMO_DIRECTIVE_PATTERN.test(source)) { - continue; - } - - const manualMemoizationMatches = this.findMatches(source); - - if (manualMemoizationMatches.length === 0) { - continue; - } - - manualMemoErrors.set(file, manualMemoizationMatches); - } - - return manualMemoErrors; - } - - private static findMatches(source: string): ManualMemoizationError[] { - const matches: ManualMemoizationError[] = []; - - for (const keyword of Object.keys(this.PATTERNS) as Array) { - const regex = this.PATTERNS[keyword]; - const regexMatches = source.matchAll(regex); - - for (const regexMatch of regexMatches) { - const matchIndex = regexMatch.index; - if (matchIndex === undefined) { - continue; - } - const {line, column} = FileUtils.getLineAndColumnFromIndex(source, matchIndex); - matches.push({keyword, line, column}); - } - } - - matches.sort((a, b) => { - if (a.line !== b.line) { - return a.line - b.line; - } - return a.column - b.column; - }); - - return matches; - } + return !hasFailure; } /** - * Handles printing compiler results to the console. + * Check files changed in a PR for React Compiler compliance. + * Rule 1: New files with components/hooks must compile. + * Rule 2: Modified files must not regress (compiled on main -> must compile on PR). */ -class ResultsPrinter { - private static readonly TAB = ' '; - - /** - * Print all results and determine pass/fail status. - */ - static printResults({success, errorsForAddedFiles, errorsForModifiedFiles, suppressedErrors, manualMemoErrors}: CompilerResults, {verbose}: CheckOptions): boolean { - this.printSuccesses(success, verbose); - this.printSuppressedErrors(suppressedErrors, verbose); - - const {hasModifiedFilesErrors, hasAddedFilesErrors} = this.printCompilerErrors(errorsForModifiedFiles, errorsForAddedFiles); - const hasManualMemoErrors = this.printManualMemoErrors(manualMemoErrors); - - if ((hasModifiedFilesErrors || hasAddedFilesErrors) && !hasManualMemoErrors) { - log(); - } - - const didCheckForAddedFilesPass = errorsForAddedFiles.size === 0; - const isPassed = didCheckForAddedFilesPass && !hasManualMemoErrors; - - if (isPassed) { - if (hasModifiedFilesErrors) { - logWarn(`React Compiler compliance check passed with warnings! The warnings must NOT be fixed and can get ignored.`); - } - - logSuccess('React Compiler compliance check passed!'); - return true; - } +async function checkChangedFiles(remote: string, verbose: boolean): Promise { + const mainBaseCommitHash = await Git.getMainBranchCommitHash(remote); + const changedFiles = await Git.getChangedFilesWithStatus(mainBaseCommitHash); - log(); - logError( - `The files above failed the React Compiler compliance check. Do not remove any manual memoization patterns, unless a file is already able to compile with React Compiler. You can use the "React Compiler Marker" VS Code extension to check whether a file is being compiled with React Compiler.`, - ); + const reactFiles = changedFiles.filter((f) => FILE_EXTENSIONS.some((ext) => f.filename.endsWith(ext)) && f.status !== 'removed'); - return false; + if (reactFiles.length === 0) { + logSuccess('No React files changed, skipping check.'); + return true; } - private static printSuccesses(success: Set, verbose: boolean): void { - if (!verbose || success.size === 0) { - return; - } + logInfo(`Checking ${reactFiles.length} changed React files...`); - log(); - logSuccess(`Successfully compiled ${success.size} files with React Compiler:`); - log(); + const failures: Array<{file: string; reason: string; errors: CompilerError[]}> = []; - for (const successFile of success) { - logSuccess(`${successFile}`); + for (const {filename, status, previousFilename} of reactFiles) { + const absolutePath = path.resolve(filename); + if (!fs.existsSync(absolutePath)) { + continue; } - log(); - } + const source = fs.readFileSync(absolutePath, 'utf8'); + const result = checkReactCompilerCompliance(source, absolutePath); - private static printSuppressedErrors(suppressedErrors: Map, verbose: boolean): void { - if (!verbose || suppressedErrors.size === 0) { - return; - } - - const suppressedErrorMap = new Map(); - for (const [, error] of suppressedErrors) { - if (!error.reason) { - continue; - } - - if (!suppressedErrorMap.has(error.reason)) { - suppressedErrorMap.set(error.reason, []); + if (status === 'added') { + if (result.status === 'failed') { + failures.push({file: filename, reason: 'New file contains components/hooks that fail to compile with React Compiler', errors: result.errors}); + logError(`FAILED ${filename} (new file must compile)`); + printErrors(filename, result.errors); + } else if (verbose) { + const label = result.status === 'compiled' ? 'COMPILED' : 'SKIPPED '; + logSuccess(`${label} ${filename}`); } - - suppressedErrorMap.get(error.reason)?.push(error); + continue; } - log(); - logWarn(`Suppressed the following errors in these files:`); - log(); - - for (const [error, suppressedErrorFiles] of suppressedErrorMap) { - logBold(error); - const filesLine = suppressedErrorFiles.map((suppressedError) => ReactCompilerHealthcheck.getErrorKey(suppressedError)).join(', '); - logNote(`${this.TAB} - ${filesLine}`); - } - - log(); - } - - private static printCompilerErrors( - errorsForModifiedFiles: Map, - errorsForAddedFiles: Map, - ): {hasModifiedFilesErrors: boolean; hasAddedFilesErrors: boolean} { - const hasModifiedFilesErrors = errorsForModifiedFiles.size > 0; - const hasAddedFilesErrors = errorsForAddedFiles.size > 0; - - if (hasModifiedFilesErrors) { - const {filesWithErrors} = ReactCompilerHealthcheck.getErrorsByFile(errorsForModifiedFiles); - - if (filesWithErrors.size > 0) { - log(); - logWarn(`Failed to compile ${filesWithErrors.size} modified files with React Compiler:`); - log(); - - this.printErrors(errorsForModifiedFiles); - } - } - - if (hasAddedFilesErrors) { - const {filesWithErrors} = ReactCompilerHealthcheck.getErrorsByFile(errorsForAddedFiles); - - if (filesWithErrors.size > 0) { - log(); - logError(`Failed to compile ${filesWithErrors.size} added files with React Compiler:`); - log(); - - this.printErrors(errorsForAddedFiles); + // Modified or renamed files: check for regression + if (result.status === 'failed') { + let mainStatus: CompilationResult['status'] = 'no-components'; + const mainPath = previousFilename ?? filename; + try { + const mainSource = Git.show('origin/main', mainPath); + mainStatus = checkReactCompilerCompliance(mainSource, mainPath).status; + } catch { + mainStatus = 'no-components'; } - } - - return {hasModifiedFilesErrors, hasAddedFilesErrors}; - } - private static printManualMemoErrors(manualMemoErrors: Map): boolean { - const hasManualMemoErrors = manualMemoErrors.size > 0; - - if (!hasManualMemoErrors) { - return false; - } - - log(); - logError(`The following newly added components should be auto memoized by the React Compiler (manual memoization is not allowed):`); - - for (const [filePath, manualMemoizationMatches] of manualMemoErrors) { - log(); - - for (const manualMemoizationMatch of manualMemoizationMatches) { - const location = manualMemoizationMatch.line && manualMemoizationMatch.column ? `:${manualMemoizationMatch.line}:${manualMemoizationMatch.column}` : ''; - logBold(`${filePath}${location}`); - logNote(`${this.TAB}${ManualMemoizationChecker.getErrorMessage(manualMemoizationMatch.keyword as keyof typeof ManualMemoizationChecker.PATTERNS)}`); + if (mainStatus === 'compiled') { + failures.push({file: filename, reason: 'File compiled on main but fails to compile on this branch (regression)', errors: result.errors}); + logError(`FAILED ${filename} (regression: compiled on main)`); + printErrors(filename, result.errors); + } else if (verbose) { + logWarn(`WARNING ${filename} (fails to compile, but also failed on main)`); } + } else if (verbose) { + const label = result.status === 'compiled' ? 'COMPILED' : 'SKIPPED '; + logSuccess(`${label} ${filename}`); } - - return true; } - private static printErrors(errorsToPrint: Map, level = 0) { - for (const error of errorsToPrint.values()) { - const location = error.line && error.column ? `:${error.line}:${error.column}` : ''; - logBold(`${this.TAB.repeat(level)}${error.file}${location}`); - logNote(`${this.TAB.repeat(level + 1)}${error.reason ?? 'No reason provided'}`); - } - } -} - -/** - * Generates JSON reports of compiler results. - */ -class ReportGenerator { - /** - * Generate a report and save it to /tmp. - */ - static generate({success, errors, suppressedErrors, manualMemoErrors, errorsForAddedFiles, errorsForModifiedFiles}: CompilerResults): void { - const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-'); - const reportFileName = `react-compiler-compliance-check-report-${timestamp}.json`; - const reportFile = path.join('/tmp', reportFileName); - - const resultsObject = { - success: Array.from(success), - errors: Object.fromEntries(errors.entries()), - errorsForAddedFiles: Object.fromEntries(errorsForAddedFiles.entries()), - errorsForModifiedFiles: Object.fromEntries(errorsForModifiedFiles.entries()), - manualMemoErrors: Object.fromEntries(manualMemoErrors.entries()), - suppressedErrors: Object.fromEntries(suppressedErrors.entries()), - } satisfies Record | string[]>; - - fs.writeFileSync( - reportFile, - JSON.stringify( - { - timestamp: new Date().toISOString(), - results: resultsObject, - }, - null, - 2, - ), - ); - + log(); + if (failures.length > 0) { + logError(`React Compiler compliance check failed with ${failures.length} error(s).`); log(); - logInfo(`Report saved to: ${reportFile}`); - } -} - -/** - * Main checker orchestrates the compliance check workflow. - */ -class Checker { - /** - * Check changed files for React Compiler compliance. - */ - static async checkChangedFiles({remote, ...restOptions}: BaseCheckParameters): Promise { - logInfo('Checking changed files for React Compiler compliance...'); - - const mainBaseCommitHash = await Git.getMainBranchCommitHash(remote); - const changedFiles = await Git.getChangedFileNames(mainBaseCommitHash, undefined, true); - - if (changedFiles.length === 0) { - logSuccess('No React files changed, skipping check.'); - return true; - } - - return this.check({mode: 'incremental', files: changedFiles, ...restOptions}); - } - - /** - * Check specific files or all files for React Compiler compliance. - */ - static async check({mode = 'static', files, remote, verbose = false}: CheckParameters): Promise { - const options: CheckOptions = {mode, verbose}; - - if (files) { - logInfo(`Running React Compiler check for ${files.length} files or glob patterns...`); - } else { - logInfo('Running React Compiler check for all files...'); - } - - let results = ReactCompilerHealthcheck.run(this.createFilesGlob(files)); - - const mainBaseCommitHash = await Git.getMainBranchCommitHash(remote); - const diffResult = Git.diff(mainBaseCommitHash, undefined, undefined, true); - - if (mode === 'incremental') { - results = await DiffAnalyzer.filterResultsByDiff(results, mainBaseCommitHash, diffResult, options); - } - - const {reactCompilerErrorsForModifiedFiles, reactCompilerErrorsForAddedFiles, manualMemoErrors} = ManualMemoizationChecker.splitErrorsBasedOnFileDiffType(results, diffResult); - - results.manualMemoErrors = manualMemoErrors; - results.errorsForAddedFiles = reactCompilerErrorsForAddedFiles; - results.errorsForModifiedFiles = reactCompilerErrorsForModifiedFiles; - - const isPassed = ResultsPrinter.printResults(results, options); - - ReportGenerator.generate(results); - - return isPassed; + logInfo('See contributingGuides/REACT_COMPILER.md for help fixing these errors.'); + return false; } - /** - * Create a glob pattern from an array of file paths. - */ - private static createFilesGlob(files?: string[]): string | undefined { - if (!files || files.length === 0) { - return undefined; - } - - if (files.length === 1) { - return files.at(0); - } - - return `**/+(${files.join('|')})`; - } + logSuccess('React Compiler compliance check passed!'); + return true; } const CLI_COMMANDS = ['check', 'check-changed'] as const; -type CliCommand = TupleToUnion; async function main() { const cli = new CLI({ positionalArgs: [ { name: 'command', - description: 'Command to run', - required: false, + description: 'Command to run (check or check-changed)', default: 'check', parse: (val) => { - if (!CLI_COMMANDS.includes(val as CliCommand)) { + if (!(CLI_COMMANDS as readonly string[]).includes(val)) { throw new Error(`Invalid command. Must be one of: ${CLI_COMMANDS.join(', ')}`); } return val; }, }, { - name: 'file', - description: 'File path or glob pattern to check', - required: false, - default: '', + name: 'files', + description: 'File paths, directories, or glob patterns to check (only for "check" command)', + variadic: true, + default: [], }, ], namedArgs: { remote: { - description: 'Git remote name to use for main branch (default: no remote locally and origin in CI)', + description: 'Git remote name (default: origin in CI, none locally)', required: false, - supersedes: ['check-changed'], }, }, flags: { verbose: { - description: 'Print logs of successes and suppressed errors', - required: false, - default: false, + description: 'Show detailed output including skipped files', }, }, }); - const {command, file} = cli.positionalArgs; + const {command} = cli.positionalArgs; + const files = cli.positionalArgs.files as string[]; const {remote} = cli.namedArgs; const {verbose} = cli.flags; - const commonOptions: BaseCheckParameters = { - verbose, - }; + let passed = false; - async function runCommand() { - switch (command) { - case 'check': - return Checker.check({files: file ? [file] : undefined, ...commonOptions}); - case 'check-changed': - return Checker.checkChangedFiles({remote, ...commonOptions}); - default: - logError(`Unknown command: ${String(command)}`); - return Promise.resolve(false); - } + switch (command) { + case 'check': + if (files.length === 0) { + logError('No paths specified. Usage: npm run react-compiler-compliance-check check '); + process.exit(1); + } + passed = checkFiles(files, verbose); + break; + case 'check-changed': + passed = await checkChangedFiles(remote ?? 'origin', verbose); + break; + default: + logError(`Unknown command: ${String(command)}`); + process.exit(1); } - try { - const isPassed = await runCommand(); - process.exit(isPassed ? 0 : 1); - } catch (error) { - logError('Error running react-compiler-compliance-check:', error); - process.exit(1); - } + process.exit(passed ? 0 : 1); } if (require.main === module) { - main(); + main().catch((error: unknown) => { + logError('Unexpected error:', error); + process.exit(1); + }); } -export default Checker; +export {checkReactCompilerCompliance}; +export type {CompilationResult, CompilerError}; diff --git a/scripts/utils/CLI.ts b/scripts/utils/CLI.ts index 49b844637ab5..de8bfc0dfd22 100644 --- a/scripts/utils/CLI.ts +++ b/scripts/utils/CLI.ts @@ -34,14 +34,16 @@ type StringArg = CLIArg & { /** * A positional argument is just a string arg, but also must be assigned a name which we will eventually expose the CLI consumer. + * If `variadic` is true, this must be the last positional arg and it collects all remaining positional args into a string[]. */ type PositionalArg = StringArg & { name: string; + variadic?: true; }; /** * This type represents the config for a CLI. - * Note: this utility does not yet support variadic args of any kind. + * The last positional arg can be marked `variadic: true` to collect all remaining positional args into a string[]. */ type CLIConfig = NonEmptyObject<{ /** @@ -87,9 +89,10 @@ type ParsedNamedArgs = { /** * Record of positional args after parsing. + * Variadic args are parsed as string[]; all others use InferStringArgParsedValue. */ type ParsedPositionalArgs = { - [K in NonNullable[number] as K['name']]: InferStringArgParsedValue; + [K in NonNullable[number] as K['name']]: K extends {variadic: true} ? string[] : InferStringArgParsedValue; }; /** @@ -217,6 +220,19 @@ class CLI { if (spec === undefined) { throw new Error(`Unexpected arg: ${rawArg}`); } + if (spec.variadic) { + // Variadic: collect this and all remaining non-flag args into an array + const collected: string[] = []; + for (let j = i; j < rawArgs.length; j++) { + const remaining = rawArgs.at(j); + if (remaining === undefined || remaining.startsWith('--')) { + break; + } + collected.push(remaining); + } + parsedPositionalArgs[spec.name as keyof typeof parsedPositionalArgs] = collected as ValueOf; + break; + } parsedPositionalArgs[spec.name as keyof typeof parsedPositionalArgs] = CLI.parseStringArg(rawArg, spec.name, spec) as ValueOf; positionalIndex++; } @@ -265,6 +281,8 @@ class CLI { if (!(spec.name in parsedPositionalArgs)) { if (spec.default !== undefined) { parsedPositionalArgs[spec.name as keyof typeof parsedPositionalArgs] = spec.default as ValueOf; + } else if (spec.variadic) { + parsedPositionalArgs[spec.name as keyof typeof parsedPositionalArgs] = [] as ValueOf; } else { throw new Error(`Missing required positional argument --${spec.name}`); } @@ -291,7 +309,12 @@ class CLI { private printHelp(): void { const {flags = {}, namedArgs = {}, positionalArgs = []} = this.config; const scriptName = process.argv.at(1) ?? 'script.ts'; - const positionalUsage = positionalArgs.map((arg) => (arg.default === undefined ? `<${arg.name}>` : `[${arg.name}]`)).join(' '); + const positionalUsage = positionalArgs + .map((arg) => { + const label = arg.variadic ? `${arg.name}...` : arg.name; + return arg.default === undefined ? `<${label}>` : `[${label}]`; + }) + .join(' '); const namedArgUsage = Object.keys(namedArgs) .map((key) => `[--${key} ]`) .join(' '); diff --git a/scripts/utils/FileUtils.ts b/scripts/utils/FileUtils.ts index 83e227c041da..a559a8aea817 100644 --- a/scripts/utils/FileUtils.ts +++ b/scripts/utils/FileUtils.ts @@ -1,3 +1,9 @@ +import fs from 'fs'; +import {globSync} from 'glob'; +import path from 'path'; + +const DEFAULT_EXTENSIONS = ['.ts', '.tsx']; + const ERROR_MESSAGES = { SOURCE_CANNOT_BE_EMPTY: 'Source cannot be empty', INDEX_CANNOT_BE_NEGATIVE: 'Index cannot be negative', @@ -31,6 +37,44 @@ const FileUtils = { const column = lastLineBreakIndex === -1 ? index + 1 : index - lastLineBreakIndex; return {line, column}; }, + + /** + * Resolve a list of inputs (file paths, directories, or glob patterns) to concrete file paths. + * Directories are expanded recursively. Results are deduplicated. + * + * @param inputs - File paths, directories, or glob patterns + * @param extensions - File extensions to include (default: .ts, .tsx) + */ + resolveFilePaths: (inputs: string[], extensions: string[] = DEFAULT_EXTENSIONS): string[] => { + const resolved = new Set(); + + for (const input of inputs) { + const absoluteInput = path.resolve(input); + const exists = fs.existsSync(absoluteInput); + const stat = exists ? fs.statSync(absoluteInput) : null; + + if (exists && stat?.isDirectory()) { + const pattern = path.join(absoluteInput, '**', `*{${extensions.join(',')}}`); + for (const file of globSync(pattern)) { + resolved.add(file); + } + continue; + } + + if (exists && stat?.isFile()) { + resolved.add(absoluteInput); + continue; + } + + for (const file of globSync(input, {absolute: true})) { + if (extensions.some((ext) => file.endsWith(ext))) { + resolved.add(file); + } + } + } + + return Array.from(resolved); + }, }; export default FileUtils; diff --git a/scripts/utils/Git.ts b/scripts/utils/Git.ts index 523ef31cf8c0..145237b73553 100644 --- a/scripts/utils/Git.ts +++ b/scripts/utils/Git.ts @@ -64,7 +64,8 @@ type DiffHunk = { */ type FileDiff = { filePath: string; - diffType: 'added' | 'removed' | 'modified'; + diffType: 'added' | 'removed' | 'modified' | 'renamed'; + previousFilePath?: string; hunks: DiffHunk[]; addedLines: Set; removedLines: Set; @@ -79,6 +80,12 @@ type DiffResult = { hasChanges: boolean; }; +type ChangedFile = { + filename: string; + status: 'added' | 'modified' | 'removed' | 'renamed'; + previousFilename?: string; +}; + /** * Utility class for git operations. */ @@ -110,8 +117,8 @@ class Git { * @throws Error when git command fails (invalid refs, not a git repo, file not found, etc.) */ static diff(fromRef: string, toRef?: string, filePaths?: string | string[], shouldIncludeUntrackedFiles = false): DiffResult { - // Build git diff command (with 0 context lines for easier parsing) - let command = `git diff -U0 ${fromRef}`; + // Build git diff command (with 0 context lines for easier parsing, -M for rename detection) + let command = `git diff -U0 -M ${fromRef}`; if (toRef) { command += ` ${toRef}`; } @@ -160,13 +167,13 @@ class Git { const files: FileDiff[] = []; let currentFile: FileDiff | null = null; let currentHunk: DiffHunk | null = null; - let oldFilePath: string | null = null; // Track old file path to determine fileDiffType + let oldFilePath: string | null = null; + let renameFromPath: string | null = null; for (const line of lines) { // File header: diff --git a/file b/file if (line.startsWith('diff --git')) { if (currentFile) { - // Push the current hunk to the current file before processing the new file if (currentHunk) { currentFile.hunks.push(currentHunk); } @@ -174,38 +181,48 @@ class Git { } currentFile = null; currentHunk = null; - oldFilePath = null; // Reset for next file + oldFilePath = null; + renameFromPath = null; + continue; + } + + // Rename detection: "rename from " appears before --- / +++ + if (line.startsWith('rename from ')) { + renameFromPath = line.slice('rename from '.length); + continue; + } + + if (line.startsWith('rename to ') || line.startsWith('similarity index ')) { continue; } // Old file path: --- a/file or --- /dev/null (for new files) - // This comes before +++ in git diff output if (line.startsWith('--- ')) { - oldFilePath = line.slice(4); // Store the old file path (remove '--- ') + oldFilePath = line.slice(4); continue; } // New file path: +++ b/file or +++ /dev/null (for removed files) if (line.startsWith('+++ ')) { - const newFilePath = line.slice(4); // Remove '+++ ' + const newFilePath = line.slice(4); - // Determine fileDiffType based on old and new file paths - // Note: oldFilePath should always be set by the time we see +++, but handle null for type safety - let fileDiffType: 'added' | 'removed' | 'modified' = 'modified'; + let fileDiffType: FileDiff['diffType'] = 'modified'; let diffFilePath: string; + let previousFilePath: string | undefined; const oldPath = oldFilePath ?? ''; if (oldPath === '/dev/null') { - // New file: use the new file path fileDiffType = 'added'; diffFilePath = newFilePath.startsWith('b/') ? newFilePath.slice(2) : newFilePath; } else if (newFilePath === '/dev/null') { - // Removed file: use the old file path fileDiffType = 'removed'; diffFilePath = oldPath.startsWith('a/') ? oldPath.slice(2) : oldPath; + } else if (renameFromPath) { + fileDiffType = 'renamed'; + diffFilePath = newFilePath.startsWith('b/') ? newFilePath.slice(2) : newFilePath; + previousFilePath = renameFromPath; } else { - // Modified file: use the new file path fileDiffType = 'modified'; diffFilePath = newFilePath.startsWith('b/') ? newFilePath.slice(2) : newFilePath; } @@ -213,6 +230,7 @@ class Git { currentFile = { filePath: diffFilePath, diffType: fileDiffType, + previousFilePath, hunks: [], addedLines: new Set(), removedLines: new Set(), @@ -458,22 +476,40 @@ class Git { } } - static async getChangedFileNames(fromRef: string, toRef?: string, shouldIncludeUntrackedFiles = false): Promise { + /** + * Get changed files with their status (added, modified, removed, renamed). + * In CI, uses the GitHub API with pagination for accuracy. + * Locally, uses git diff against the provided ref. + */ + static async getChangedFilesWithStatus(fromRef: string, toRef?: string, shouldIncludeUntrackedFiles = false): Promise { if (IS_CI) { - const {data: changedFiles} = await GitHubUtils.octokit.pulls.listFiles({ + const files = await GitHubUtils.paginate(GitHubUtils.octokit.pulls.listFiles, { owner: CONST.GITHUB_OWNER, repo: CONST.APP_REPO, // eslint-disable-next-line @typescript-eslint/naming-convention pull_number: context.payload.pull_request?.number ?? 0, + // eslint-disable-next-line @typescript-eslint/naming-convention + per_page: 100, }); - return changedFiles.map((file) => file.filename); + return files.map((file) => ({ + filename: file.filename, + status: file.status as 'added' | 'modified' | 'removed' | 'renamed', + previousFilename: file.previous_filename, + })); } - // Get the diff output and check status const diffResult = this.diff(fromRef, toRef, undefined, shouldIncludeUntrackedFiles); - const files = diffResult.files.map((file) => file.filePath); - return files; + return diffResult.files.map((file) => ({ + filename: file.filePath, + status: file.diffType, + previousFilename: file.previousFilePath, + })); + } + + static async getChangedFileNames(fromRef: string, toRef?: string, shouldIncludeUntrackedFiles = false): Promise { + const files = await this.getChangedFilesWithStatus(fromRef, toRef, shouldIncludeUntrackedFiles); + return files.map((file) => file.filename); } /** @@ -583,4 +619,4 @@ class Git { } export default Git; -export type {DiffResult, FileDiff, DiffHunk, DiffLine}; +export type {DiffResult, FileDiff, DiffHunk, DiffLine, ChangedFile}; diff --git a/src/App.tsx b/src/App.tsx index 8ef6d1a55f19..8298054849ad 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,28 +20,19 @@ import FullScreenLoaderContextProvider from './components/FullScreenLoaderContex import HTMLEngineProvider from './components/HTMLEngineProvider'; import InitialURLContextProvider from './components/InitialURLContextProvider'; import {InputBlurContextProvider} from './components/InputBlurContext'; -import {KeyboardDismissibleFlatListContextProvider} from './components/KeyboardDismissibleFlatList/KeyboardDismissibleFlatListContext'; import KeyboardProvider from './components/KeyboardProvider'; -import KYCWallContextProvider from './components/KYCWall/KYCWallContext'; import {LocaleContextProvider} from './components/LocaleContextProvider'; import {ModalProvider} from './components/Modal/Global/ModalContext'; import NavigationBar from './components/NavigationBar'; import OnyxListItemProvider from './components/OnyxListItemProvider'; import PopoverContextProvider from './components/PopoverProvider'; -import {ProductTrainingContextProvider} from './components/ProductTrainingContext'; import SafeArea from './components/SafeArea'; import ScrollOffsetContextProvider from './components/ScrollOffsetContextProvider'; -import {SearchRouterContextProvider} from './components/Search/SearchRouter/SearchRouterContext'; import SidePanelContextProvider from './components/SidePanel/SidePanelContextProvider'; import SVGDefinitionsProvider from './components/SVGDefinitionsProvider'; import ThemeIllustrationsProvider from './components/ThemeIllustrationsProvider'; import ThemeProvider from './components/ThemeProvider'; import ThemeStylesProvider from './components/ThemeStylesContextProvider'; -import FullScreenContextProvider from './components/VideoPlayerContexts/FullScreenContextProvider'; -import {PlaybackContextProvider} from './components/VideoPlayerContexts/PlaybackContext'; -import {VideoPopoverMenuContextProvider} from './components/VideoPlayerContexts/VideoPopoverMenuContext'; -import {VolumeContextProvider} from './components/VideoPlayerContexts/VolumeContext'; -import WideRHPContextProvider from './components/WideRHPContextProvider'; import {KeyboardStateProvider} from './components/withKeyboardState'; import CONFIG from './CONFIG'; import CONST from './CONST'; @@ -51,9 +42,6 @@ import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop'; import HybridAppHandler from './HybridAppHandler'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; import './libs/HybridApp'; -import {AttachmentModalContextProvider} from './pages/media/AttachmentModalScreen/AttachmentModalContext'; -import ExpensifyCardContextProvider from './pages/settings/Wallet/ExpensifyCardPage/ExpensifyCardContextProvider'; -import TravelCVVContextProvider from './pages/settings/Wallet/TravelCVVPage/TravelCVVContextProvider'; import './setup/backgroundLocationTrackingTask'; import './setup/backgroundTask'; import './setup/fraudProtection'; @@ -111,30 +99,18 @@ function App() { PopoverContextProvider, CurrentReportIDContextProvider, ScrollOffsetContextProvider, - AttachmentModalContextProvider, PickerStateProvider, EnvironmentProvider, CustomStatusBarAndBackgroundContextProvider, ActiveElementRoleProvider, ActionSheetAwareScrollViewProvider, - PlaybackContextProvider, - FullScreenContextProvider, - VolumeContextProvider, - VideoPopoverMenuContextProvider, KeyboardProvider, KeyboardStateProvider, - KeyboardDismissibleFlatListContextProvider, - SearchRouterContextProvider, - ProductTrainingContextProvider, InputBlurContextProvider, FullScreenBlockingViewContextProvider, FullScreenLoaderContextProvider, ModalProvider, SidePanelContextProvider, - ExpensifyCardContextProvider, - TravelCVVContextProvider, - KYCWallContextProvider, - WideRHPContextProvider, ]} > diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 22a56ed0053b..5c01e4b6c39d 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -234,6 +234,8 @@ const CONST = { }, ANIMATION_IN_TIMING: 100, COMPOSER_FOCUS_DELAY: 150, + MAX_TRANSITION_DURATION_MS: 1000, + MAX_TRANSITION_START_WAIT_MS: 1000, ANIMATION_DIRECTION: { IN: 'in', OUT: 'out', @@ -1309,6 +1311,7 @@ const CONST = { MAX_COUNT_BEFORE_FOCUS_UPDATE: 30, MIN_INITIAL_REPORT_ACTION_COUNT: 15, UNREPORTED_REPORT_ID: '0', + TRASH_REPORT_ID: '-1', SPLIT_REPORT_ID: '-2', SECONDARY_ACTIONS: { SUBMIT: 'submit', @@ -2019,6 +2022,7 @@ const CONST = { ATTRIBUTE_IS_FROM_GLOBAL_CREATE: 'is_from_global_create', /** Sentry span attribute: follow-up action taken after submit (e.g. dismiss_modal_and_open_report, navigate_to_search). */ ATTRIBUTE_SUBMIT_FOLLOW_UP_ACTION: 'submit_follow_up_action', + ATTRIBUTE_FAST_PATH_HANDLER: 'fast_path_handler', ATTRIBUTE_COMMAND: 'command', ATTRIBUTE_JSON_CODE: 'json_code', ATTRIBUTE_COLD_START: 'cold_start', @@ -2032,6 +2036,19 @@ const CONST = { NAVIGATE_TO_SEARCH: 'navigate_to_search', DISMISS_MODAL_ONLY: 'dismiss_modal_only', }, + FAST_PATH_HANDLER: { + SEARCH_PRE_INSERT: 'search_pre_insert', + REPORT_PRE_INSERT: 'report_pre_insert', + DISMISS_MODAL: 'dismiss_modal', + REPORT_IN_RHP_DISMISS: 'report_in_rhp_dismiss', + SEARCH_DISMISS: 'search_dismiss', + DEFAULT: 'default', + }, + SUBMIT_OPTIMIZATION: { + PRE_INSERT: 'pre_insert', + DISMISS_FIRST: 'dismiss_first', + DEFERRED_WRITE: 'deferred_write', + }, /** Trigger for useSubmitToDestinationVisible: end span on focus vs on layout. */ SUBMIT_TO_DESTINATION_VISIBLE_TRIGGER: { FOCUS: 'focus', @@ -2215,14 +2232,11 @@ const CONST = { MAX_RETRY_WAIT_TIME_MS: 10 * 1000, PROCESS_REQUEST_DELAY_MS: 1000, MAX_PENDING_TIME_MS: 10 * 1000, - RECHECK_INTERVAL_MS: 60 * 1000, MAX_REQUEST_RETRIES: 10, MAX_OPEN_APP_REQUEST_RETRIES: 2, - NETWORK_STATUS: { - ONLINE: 'online', - OFFLINE: 'offline', - UNKNOWN: 'unknown', - }, + SUSTAINED_FAILURE_THRESHOLD_COUNT: 3, + SUSTAINED_FAILURE_WINDOW_MS: 10 * 1000, + RECONNECT_STAMPEDE_JITTER_MS: 5000, }, // The number of milliseconds for an idle session to expire SESSION_EXPIRATION_TIME_MS: 2 * 3600 * 1000, // 2 hours @@ -2230,7 +2244,6 @@ const CONST = { DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'}, DEFAULT_ACCOUNT_DATA: {errors: null, success: '', isLoading: false}, DEFAULT_CLOSE_ACCOUNT_DATA: {errors: null, success: '', isLoading: false}, - DEFAULT_NETWORK_DATA: {isOffline: false}, FORMS: { LOGIN_FORM: 'LoginForm', VALIDATE_CODE_FORM: 'ValidateCodeForm', @@ -7486,6 +7499,7 @@ const CONST = { DONE: 'done', EXPORT_TO_ACCOUNTING: 'exportToAccounting', PAID: 'paid', + UNDELETE: 'undelete', }, HAS_VALUES: { RECEIPT: 'receipt', @@ -7509,6 +7523,7 @@ const CONST = { CHANGE_REPORT: 'changeReport', SPLIT: 'split', DUPLICATE: 'duplicate', + UNDELETE: 'undelete', }, TRANSACTION_TYPE: { CASH: 'cash', @@ -7689,8 +7704,6 @@ const CONST = { this.TABLE_COLUMNS.MERCHANT, this.TABLE_COLUMNS.FROM, this.TABLE_COLUMNS.CATEGORY, - this.TABLE_COLUMNS.ATTENDEES, - this.TABLE_COLUMNS.TOTAL_PER_ATTENDEE, this.TABLE_COLUMNS.TAG, this.TABLE_COLUMNS.TOTAL_AMOUNT, ], @@ -7747,6 +7760,7 @@ const CONST = { APPROVED: 'approved', DONE: 'done', PAID: 'paid', + DELETED: 'deleted', }, EXPENSE_REPORT: { ALL: '', @@ -8714,7 +8728,6 @@ const CONST = { }, MODAL_EVENTS: { - CLOSED: 'modalClosed', DISABLE_RHP_ANIMATION: 'disableRHPAnimation', RESTORE_RHP_ANIMATION: 'restoreRHPAnimation', }, @@ -8829,22 +8842,11 @@ const CONST = { SELECT_ALL_BUTTON: 'Search-SelectAllButton', TYPE_MENU_BUTTON: 'Search-TypeMenuButton', FILTER_DISPLAY: 'Search-FilterDisplay', - FILTER_TYPE: 'Search-FilterType', - FILTER_STATUS: 'Search-FilterStatus', - FILTER_DATE: 'Search-FilterDate', - FILTER_FROM: 'Search-FilterFrom', - FILTER_WORKSPACE: 'Search-FilterWorkspace', FILTER_GROUP_BY: 'Search-FilterGroupBy', FILTER_SORT_BY: 'Search-FilterSortBy', FILTER_GROUP_CURRENCY: 'Search-FilterGroupCurrency', FILTER_VIEW: 'Search-FilterView', FILTER_LIMIT: 'Search-FilterLimit', - FILTER_FEED: 'Search-FilterFeed', - FILTER_POSTED: 'Search-FilterPosted', - FILTER_WITHDRAWN: 'Search-FilterWithdrawn', - FILTER_WITHDRAWAL_TYPE: 'Search-FilterWithdrawalType', - FILTER_HAS: 'Search-FilterHas', - FILTER_IS: 'Search-FilterIs', ADVANCED_FILTERS_BUTTON: 'Search-AdvancedFiltersButton', COLUMNS_BUTTON: 'Search-ColumnsButton', SELECT_ALL_MATCHING_BUTTON: 'Search-SelectAllMatchingButton', @@ -9518,6 +9520,9 @@ const CONST = { SETTINGS_HELP: { CONCIERGE_CHAT: 'SettingsHelp-ConciergeChat', HELP_DOCS: 'SettingsHelp-HelpDocs', + ACCOUNT_MANAGER: 'SettingsHelp-AccountManager', + PARTNER_MANAGER: 'SettingsHelp-PartnerManager', + GUIDE: 'SettingsHelp-Guide', }, SETTINGS_ABOUT: { APP_DOWNLOAD_LINKS: 'SettingsAbout-AppDownloadLinks', diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 9c5f021b57bb..70f8a81666f6 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -24,7 +24,8 @@ import Log from './libs/Log'; import migrateOnyx from './libs/migrateOnyx'; import Navigation from './libs/Navigation/Navigation'; import NavigationRoot from './libs/Navigation/NavigationRoot'; -import NetworkConnection from './libs/NetworkConnection'; +// This lib needs to be imported for its module-level NetInfo and Onyx subscriptions +import './libs/NetworkState'; import PushNotification from './libs/Notification/PushNotification'; import {endSpan, getSpan, startSpan} from './libs/telemetry/activeSpans'; import type {BootsplashGateStatus} from './libs/telemetry/bootsplashTelemetry'; @@ -34,7 +35,6 @@ import Visibility from './libs/Visibility'; import ONYXKEYS from './ONYXKEYS'; import PriorityModeHandler from './PriorityModeHandler'; import type {Route} from './ROUTES'; -import {accountIDSelector} from './selectors/Session'; import {useSplashScreenActions, useSplashScreenState} from './SplashScreenStateContext'; Onyx.registerLogger(({level, message, parameters}) => { @@ -56,7 +56,6 @@ function Expensify() { const {setSplashScreenState} = useSplashScreenActions(); const [hasAttemptedToOpenPublicRoom, setAttemptedToOpenPublicRoom] = useState(false); const {preferredLocale} = useLocalize(); - const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector}); const [lastRoute] = useOnyx(ONYXKEYS.LAST_ROUTE); const [isCheckingPublicRoom = true] = useOnyx(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, {initWithStoredValues: false}); const [updateRequired] = useOnyx(ONYXKEYS.RAM_ONLY_UPDATE_REQUIRED); @@ -210,12 +209,7 @@ function Expensify() { useLayoutEffect(() => { // Initialize this client as being an active client ActiveClientManager.init(); - - // Used for the offline indicator appearing when someone is offline - const unsubscribeNetInfo = NetworkConnection.subscribeToNetInfo(accountID); - - return unsubscribeNetInfo; - }, [accountID]); + }, []); // Log the platform and config to debug .env issues useEffect(() => { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index f46b97c39a03..de76dc0b4160 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -31,7 +31,7 @@ const ONYXKEYS = { DEVICE_ID: 'deviceID', /** Boolean flag set whenever the sidebar has loaded */ - IS_SIDEBAR_LOADED: 'isSidebarLoaded', + RAM_ONLY_IS_SIDEBAR_LOADED: 'isSidebarLoaded', /** Boolean flag set whenever we are searching for reports in the server */ RAM_ONLY_IS_SEARCHING_FOR_REPORTS: 'isSearchingForReports', @@ -210,7 +210,7 @@ const ONYXKEYS = { NVP_PREFERRED_LOCALE: 'nvp_preferredLocale', /** Whether the app is currently loading a translation */ - ARE_TRANSLATIONS_LOADING: 'areTranslationsLoading', + RAM_ONLY_ARE_TRANSLATIONS_LOADING: 'areTranslationsLoading', /** Whether the user has tried focus mode yet */ NVP_TRY_FOCUS_MODE: 'nvp_tryFocusMode', @@ -547,7 +547,7 @@ const ONYXKEYS = { ASSIGN_CARD: 'assignCard', /** Stores the information if mobile selection mode is active */ - MOBILE_SELECTION_MODE: 'mobileSelectionMode', + RAM_ONLY_MOBILE_SELECTION_MODE: 'mobileSelectionMode', NVP_PRIVATE_CANCELLATION_DETAILS: 'nvp_private_cancellationDetails', @@ -802,7 +802,7 @@ const ONYXKEYS = { NVP_EXPENSIFY_REPORT_PDF_FILENAME: 'nvp_expensify_report_PDFFilename_', /** Stores the information about the state of issuing a new card */ - ISSUE_NEW_EXPENSIFY_CARD: 'issueNewExpensifyCard_', + RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD: 'issueNewExpensifyCard_', /** Used for identifying user as admin of a domain */ SHARED_NVP_PRIVATE_ADMIN_ACCESS: 'sharedNVP_private_admin_access_', @@ -1006,6 +1006,8 @@ const ONYXKEYS = { EDIT_EXPENSIFY_CARD_NAME_DRAFT_FORM: 'editExpensifyCardNameDraft', EDIT_EXPENSIFY_CARD_LIMIT_FORM: 'editExpensifyCardLimit', EDIT_EXPENSIFY_CARD_LIMIT_DRAFT_FORM: 'editExpensifyCardLimitDraft', + EDIT_TRAVEL_INVOICING_MONTHLY_LIMIT_FORM: 'editTravelInvoicingMonthlyLimit', + EDIT_TRAVEL_INVOICING_MONTHLY_LIMIT_DRAFT_FORM: 'editTravelInvoicingMonthlyLimitDraft', EDIT_EXPENSIFY_CARD_LIMIT_TYPE_FORM: 'editExpensifyCardLimitType', EDIT_EXPENSIFY_CARD_LIMIT_TYPE_DRAFT_FORM: 'editExpensifyCardLimitTypeDraft', SAGE_INTACCT_CREDENTIALS_FORM: 'sageIntacctCredentialsForm', @@ -1186,6 +1188,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.ASSIGN_CARD_FORM]: FormTypes.AssignCardForm; [ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_NAME_FORM]: FormTypes.EditExpensifyCardNameForm; [ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_LIMIT_FORM]: FormTypes.EditExpensifyCardLimitForm; + [ONYXKEYS.FORMS.EDIT_TRAVEL_INVOICING_MONTHLY_LIMIT_FORM]: FormTypes.EditTravelInvoicingMonthlyLimitForm; [ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_LIMIT_TYPE_FORM]: FormTypes.EditExpensifyCardLimitTypeForm; [ONYXKEYS.FORMS.SAGE_INTACCT_CREDENTIALS_FORM]: FormTypes.SageIntactCredentialsForm; [ONYXKEYS.FORMS.NETSUITE_CUSTOM_FIELD_FORM]: FormTypes.NetSuiteCustomFieldForm; @@ -1288,7 +1291,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.LAST_SELECTED_FEED]: OnyxTypes.CompanyCardFeedWithDomainID; [ONYXKEYS.COLLECTION.LAST_SELECTED_EXPENSIFY_CARD_FEED]: OnyxTypes.FundID; [ONYXKEYS.COLLECTION.NVP_EXPENSIFY_ON_CARD_WAITLIST]: OnyxTypes.CardOnWaitlist; - [ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD]: OnyxTypes.IssueNewCard; + [ONYXKEYS.COLLECTION.RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD]: OnyxTypes.IssueNewCard; [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS]: boolean; [ONYXKEYS.COLLECTION.SAML_METADATA]: OnyxTypes.SamlMetadata; [ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS]: OnyxTypes.DomainPendingActions; @@ -1312,7 +1315,7 @@ type OnyxValuesMapping = { [ONYXKEYS.RECENTLY_USED_CURRENCIES]: string[]; [ONYXKEYS.ACTIVE_CLIENTS]: string[]; [ONYXKEYS.DEVICE_ID]: string; - [ONYXKEYS.IS_SIDEBAR_LOADED]: boolean; + [ONYXKEYS.RAM_ONLY_IS_SIDEBAR_LOADED]: boolean; [ONYXKEYS.PERSISTED_REQUESTS]: OnyxTypes.AnyRequest[]; [ONYXKEYS.PERSISTED_ONGOING_REQUESTS]: OnyxTypes.AnyRequest; [ONYXKEYS.CURRENT_DATE]: string; @@ -1379,7 +1382,7 @@ type OnyxValuesMapping = { [ONYXKEYS.ONFIDO_TOKEN]: string; [ONYXKEYS.ONFIDO_APPLICANT_ID]: string; [ONYXKEYS.NVP_PREFERRED_LOCALE]: OnyxTypes.Locale; - [ONYXKEYS.ARE_TRANSLATIONS_LOADING]: boolean; + [ONYXKEYS.RAM_ONLY_ARE_TRANSLATIONS_LOADING]: boolean; [ONYXKEYS.NVP_ACTIVE_POLICY_ID]: string; [ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS]: OnyxTypes.DismissedReferralBanners; [ONYXKEYS.NVP_HAS_SEEN_TRACK_TRAINING]: boolean; @@ -1413,11 +1416,11 @@ type OnyxValuesMapping = { [ONYXKEYS.IS_LOADING_SHARE_BANK_ACCOUNTS]: boolean; [ONYXKEYS.IS_LOADING_BULK_CHANGE_APPROVER_PAGE]: boolean; [ONYXKEYS.IS_LOADING_POLICY_CODING_RULES_PREVIEW]: boolean; + [ONYXKEYS.IS_LOADING_REPORT_DATA]: boolean; [ONYXKEYS.IS_SEARCH_FILTERS_CARD_DATA_LOADED]: boolean; [ONYXKEYS.IS_LOADING_SUBSCRIPTION_DATA]: boolean; [ONYXKEYS.IS_PENDING_UPDATE_PERSONAL_KARMA]: boolean; [ONYXKEYS.IS_SEARCH_PAGE_DATA_LOADED]: boolean; - [ONYXKEYS.IS_LOADING_REPORT_DATA]: boolean; [ONYXKEYS.IS_TEST_TOOLS_MODAL_OPEN]: boolean; [ONYXKEYS.IS_LOADING_APP]: boolean; [ONYXKEYS.HAS_LOADED_APP]: boolean; @@ -1467,7 +1470,7 @@ type OnyxValuesMapping = { [ONYXKEYS.ADD_NEW_COMPANY_CARD]: OnyxTypes.AddNewCompanyCardFeed; [ONYXKEYS.ADD_NEW_PERSONAL_CARD]: OnyxTypes.AddNewPersonalCard; [ONYXKEYS.ASSIGN_CARD]: OnyxTypes.AssignCard; - [ONYXKEYS.MOBILE_SELECTION_MODE]: boolean; + [ONYXKEYS.RAM_ONLY_MOBILE_SELECTION_MODE]: boolean; [ONYXKEYS.DUPLICATE_WORKSPACE]: OnyxTypes.DuplicateWorkspace; [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: string; [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: string; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 299776537aac..9401d1d87be0 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -129,11 +129,11 @@ const DYNAMIC_ROUTES = { }, POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_PREFERRED_EXPORTER: { path: 'preferred-exporter', - entryScreens: [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_EXPORT], + entryScreens: [SCREENS.WORKSPACE.ACCOUNTING.DYNAMIC_QUICKBOOKS_ONLINE_EXPORT], }, POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT_OUT_OF_POCKET_EXPENSES: { path: 'out-of-pocket-expense', - entryScreens: [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_EXPORT], + entryScreens: [SCREENS.WORKSPACE.ACCOUNTING.DYNAMIC_QUICKBOOKS_ONLINE_EXPORT], }, POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT_OUT_OF_POCKET_EXPENSES_ACCOUNT_SELECT: { path: 'account-select', @@ -145,7 +145,27 @@ const DYNAMIC_ROUTES = { }, POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT_DATE_SELECT: { path: 'date-select', - entryScreens: [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_EXPORT], + entryScreens: [SCREENS.WORKSPACE.ACCOUNTING.DYNAMIC_QUICKBOOKS_ONLINE_EXPORT], + }, + POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT: { + path: 'quickbooks-online/export', + entryScreens: [SCREENS.WORKSPACE.ACCOUNTING.ROOT, SCREENS.WORKSPACE.COMPANY_CARD_EXPORT], + }, + POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT: { + path: 'company-card-expense-account', + entryScreens: [SCREENS.WORKSPACE.ACCOUNTING.DYNAMIC_QUICKBOOKS_ONLINE_EXPORT], + }, + POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT: { + path: 'company-card-expense-account-select', + entryScreens: [SCREENS.WORKSPACE.ACCOUNTING.DYNAMIC_QUICKBOOKS_ONLINE_EXPORT, SCREENS.WORKSPACE.ACCOUNTING.DYNAMIC_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT], + }, + POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_CARD_SELECT: { + path: 'card-select', + entryScreens: [SCREENS.WORKSPACE.ACCOUNTING.DYNAMIC_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT], + }, + POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECT: { + path: 'invoice-account-select', + entryScreens: [SCREENS.WORKSPACE.ACCOUNTING.DYNAMIC_QUICKBOOKS_ONLINE_EXPORT], }, POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_DESTINATION: { path: 'destination', @@ -216,6 +236,14 @@ const DYNAMIC_ROUTES = { path: 'workspace-address', entryScreens: [SCREENS.WORKSPACE.PROFILE], }, + WORKSPACE_CATEGORIES_IMPORT: { + path: 'import', + entryScreens: [SCREENS.WORKSPACE.CATEGORIES], + }, + WORKSPACE_CATEGORIES_IMPORTED: { + path: 'imported', + entryScreens: [SCREENS.WORKSPACE.CATEGORIES], + }, WORKSPACE_INVITE: { path: 'invite', entryScreens: [SCREENS.WORKSPACE.PROFILE, SCREENS.WORKSPACE.MEMBERS], @@ -1777,23 +1805,6 @@ const ROUTES = { return getUrlWithBackToParam(`workspaces/${policyID}/accounting/quickbooks-online/export` as const, backTo, false); }, }, - POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT: { - route: 'workspaces/:policyID/accounting/quickbooks-online/export/company-card-expense-account', - - // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`workspaces/${policyID}/accounting/quickbooks-online/export/company-card-expense-account` as const, backTo), - }, - POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT: { - route: 'workspaces/:policyID/accounting/quickbooks-online/export/company-card-expense-account/account-select', - getRoute: (policyID: string | undefined, backTo?: string) => { - if (!policyID) { - Log.warn('Invalid policyID is used to build the POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT route'); - } - - // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - return getUrlWithBackToParam(`workspaces/${policyID}/accounting/quickbooks-online/export/company-card-expense-account/account-select` as const, backTo); - }, - }, POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_NON_REIMBURSABLE_DEFAULT_VENDOR_SELECT: { route: 'workspaces/:policyID/accounting/quickbooks-online/export/company-card-expense-account/default-vendor-select', getRoute: (policyID: string | undefined) => { @@ -1814,12 +1825,6 @@ const ROUTES = { return getUrlWithBackToParam(`workspaces/${policyID}/accounting/quickbooks-online/export/company-card-expense-account/card-select` as const, backTo); }, }, - POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECT: { - route: 'workspaces/:policyID/accounting/quickbooks-online/export/invoice-account-select', - - // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`workspaces/${policyID}/accounting/quickbooks-online/export/invoice-account-select` as const, backTo), - }, POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_TRAVEL_INVOICING_CONFIGURATION: { route: 'workspaces/:policyID/accounting/quickbooks-online/export/travel-invoicing', getRoute: (policyID: string) => `workspaces/${policyID}/accounting/quickbooks-online/export/travel-invoicing` as const, @@ -2275,14 +2280,6 @@ const ROUTES = { route: 'workspaces/:policyID/categories/settings', getRoute: (policyID: string) => `workspaces/${policyID}/categories/settings` as const, }, - WORKSPACE_CATEGORIES_IMPORT: { - route: 'workspaces/:policyID/categories/import', - getRoute: (policyID: string) => `workspaces/${policyID}/categories/import` as const, - }, - WORKSPACE_CATEGORIES_IMPORTED: { - route: 'workspaces/:policyID/categories/imported', - getRoute: (policyID: string) => `workspaces/${policyID}/categories/imported` as const, - }, WORKSPACE_CATEGORY_CREATE: { route: 'workspaces/:policyID/categories/new', getRoute: (policyID: string) => `workspaces/${policyID}/categories/new` as const, @@ -2822,6 +2819,10 @@ const ROUTES = { route: 'workspaces/:policyID/travel/settings/frequency', getRoute: (policyID: string) => `workspaces/${policyID}/travel/settings/frequency` as const, }, + WORKSPACE_TRAVEL_SETTINGS_MONTHLY_LIMIT: { + route: 'workspaces/:policyID/travel/settings/monthly-limit', + getRoute: (policyID: string) => `workspaces/${policyID}/travel/settings/monthly-limit` as const, + }, WORKSPACE_TRAVEL_MISSING_PERSONAL_DETAILS: { route: 'workspaces/:policyID/travel/missing-personal-details', getRoute: (policyID: string) => `workspaces/${policyID}/travel/missing-personal-details` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 8216ca68090c..ab5c2af89d4f 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -53,8 +53,6 @@ const SCREENS = { REPORT_VERIFY_ACCOUNT: 'Search_Report_Verify_Account', ADVANCED_FILTERS_RHP: 'Search_Advanced_Filters_RHP', ADVANCED_FILTERS_TYPE_RHP: 'Search_Advanced_Filters_Type_RHP', - ADVANCED_FILTERS_GROUP_BY_RHP: 'Search_Advanced_Filters_GroupBy_RHP', - ADVANCED_FILTERS_VIEW_RHP: 'Search_Advanced_Filters_View_RHP', ADVANCED_FILTERS_STATUS_RHP: 'Search_Advanced_Filters_Status_RHP', ADVANCED_FILTERS_DATE_RHP: 'Search_Advanced_Filters_Date_RHP', ADVANCED_FILTERS_SUBMITTED_RHP: 'Search_Advanced_Filters_Submitted_RHP', @@ -65,7 +63,6 @@ const SCREENS = { ADVANCED_FILTERS_POSTED_RHP: 'Search_Advanced_Filters_Posted_RHP', ADVANCED_FILTERS_WITHDRAWN_RHP: 'Search_Advanced_Filters_Withdrawn_RHP', ADVANCED_FILTERS_CURRENCY_RHP: 'Search_Advanced_Filters_Currency_RHP', - ADVANCED_FILTERS_GROUP_CURRENCY_RHP: 'Search_Advanced_Filters_Group_Currency_RHP', ADVANCED_FILTERS_DESCRIPTION_RHP: 'Search_Advanced_Filters_Description_RHP', ADVANCED_FILTERS_MERCHANT_RHP: 'Search_Advanced_Filters_Merchant_RHP', ADVANCED_FILTERS_REPORT_ID_RHP: 'Search_Advanced_Filters_ReportID_RHP', @@ -81,7 +78,6 @@ const SCREENS = { ADVANCED_FILTERS_WITHDRAWAL_ID_RHP: 'Search_Advanced_Filters_Withdrawal_ID_RHP', ADVANCED_FILTERS_TAG_RHP: 'Search_Advanced_Filters_Tag_RHP', ADVANCED_FILTERS_HAS_RHP: 'Search_Advanced_Filters_Has_RHP', - ADVANCED_FILTERS_LIMIT_RHP: 'Search_Advanced_Filters_Limit_RHP', ADVANCED_FILTERS_FROM_RHP: 'Search_Advanced_Filters_From_RHP', ADVANCED_FILTERS_TO_RHP: 'Search_Advanced_Filters_To_RHP', ADVANCED_FILTERS_TITLE_RHP: 'Search_Advanced_Filters_Title_RHP', @@ -530,13 +526,13 @@ const SCREENS = { QUICKBOOKS_ONLINE_CUSTOMERS: 'Policy_Accounting_Quickbooks_Online_Import_Customers', QUICKBOOKS_ONLINE_LOCATIONS: 'Policy_Accounting_Quickbooks_Online_Import_Locations', QUICKBOOKS_ONLINE_TAXES: 'Policy_Accounting_Quickbooks_Online_Import_Taxes', - QUICKBOOKS_ONLINE_EXPORT: 'Workspace_Accounting_Quickbooks_Online_Export', + DYNAMIC_QUICKBOOKS_ONLINE_EXPORT: 'Dynamic_Workspace_Accounting_Quickbooks_Online_Export', DYNAMIC_QUICKBOOKS_ONLINE_EXPORT_DATE_SELECT: 'Dynamic_Workspace_Accounting_Quickbooks_Online_Export_Date_Select', - QUICKBOOKS_ONLINE_EXPORT_INVOICE_ACCOUNT_SELECT: 'Workspace_Accounting_Quickbooks_Online_Export_Invoice_Account_Select', - QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT: 'Workspace_Accounting_Quickbooks_Online_Export_Company_Card_Expense', - QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT: 'Workspace_Accounting_Quickbooks_Online_Export_Company_Card_Expense_Account_Select', + DYNAMIC_QUICKBOOKS_ONLINE_EXPORT_INVOICE_ACCOUNT_SELECT: 'Dynamic_Workspace_Accounting_Quickbooks_Online_Export_Invoice_Account_Select', + DYNAMIC_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT: 'Dynamic_Workspace_Accounting_Quickbooks_Online_Export_Company_Card_Expense', + DYNAMIC_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT: 'Dynamic_Workspace_Accounting_Quickbooks_Online_Export_Company_Card_Expense_Account_Select', QUICKBOOKS_ONLINE_NON_REIMBURSABLE_DEFAULT_VENDOR_SELECT: 'Workspace_Accounting_Quickbooks_Online_Export_Non_Reimbursable_Default_Vendor_Select', - QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_COMPANY_CARD_SELECT: 'Workspace_Accounting_Quickbooks_Online_Export_Company_Card_Expense_Select', + DYNAMIC_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_COMPANY_CARD_SELECT: 'Dynamic_Workspace_Accounting_Quickbooks_Online_Export_Company_Card_Expense_Select', DYNAMIC_QUICKBOOKS_ONLINE_EXPORT_PREFERRED_EXPORTER: 'Dynamic_Workspace_Accounting_Quickbooks_Online_Export_Preferred_Exporter', QUICKBOOKS_ONLINE_TRAVEL_INVOICING_CONFIGURATION: 'Workspace_Accounting_Quickbooks_Online_Travel_Invoicing_Configuration', QUICKBOOKS_ONLINE_TRAVEL_INVOICING_VENDOR_SELECT: 'Workspace_Accounting_Quickbooks_Online_Travel_Invoicing_Vendor_Select', @@ -782,8 +778,8 @@ const SCREENS = { CATEGORY_REQUIRE_ITEMIZED_RECEIPTS_OVER: 'Category_Require_Itemized_Receipts_Over', CATEGORY_REQUIRED_FIELDS: 'Category_Required_Fields', CATEGORIES_SETTINGS: 'Categories_Settings', - CATEGORIES_IMPORT: 'Categories_Import', - CATEGORIES_IMPORTED: 'Categories_Imported', + DYNAMIC_CATEGORIES_IMPORT: 'Dynamic_Categories_Import', + DYNAMIC_CATEGORIES_IMPORTED: 'Dynamic_Categories_Imported', MORE_FEATURES: 'Workspace_More_Features', MEMBER_DETAILS: 'Workspace_Member_Details', MEMBER_DETAILS_ROLE: 'Workspace_Member_Details_Role', @@ -796,6 +792,7 @@ const SCREENS = { TRAVEL: 'Travel', TRAVEL_SETTINGS_ACCOUNT: 'Workspace_Travel_Settings_Account', TRAVEL_SETTINGS_FREQUENCY: 'Workspace_Travel_Settings_Frequency', + TRAVEL_SETTINGS_MONTHLY_LIMIT: 'Workspace_Travel_Settings_Monthly_Limit', TRAVEL_EXPORT: 'Workspace_Travel_Invoicing_Export', TRAVEL_MISSING_PERSONAL_DETAILS: 'Travel_Missing_Personal_Details', CREATE_DISTANCE_RATE: 'Create_Distance_Rate', diff --git a/src/components/BigNumberPad.tsx b/src/components/BigNumberPad.tsx index 262f07f44f6b..efce3c12ae2a 100644 --- a/src/components/BigNumberPad.tsx +++ b/src/components/BigNumberPad.tsx @@ -34,7 +34,7 @@ function BigNumberPad({numberPressed, longPressHandlerStateChanged = () => {}, i const styles = useThemeStyles(); const [timer, setTimer] = useState(null); - const {isExtraSmallScreenHeight} = useResponsiveLayout(); + const {isExtraSmallScreenHeight, isInLandscapeMode} = useResponsiveLayout(); const numberPressedRef = useRef(numberPressed); useEffect(() => { @@ -64,10 +64,10 @@ function BigNumberPad({numberPressed, longPressHandlerStateChanged = () => {}, i style={[styles.flexColumn, styles.w100]} id={id} > - {padNumbers.map((row) => ( + {padNumbers.map((row, index) => ( {row.map((column, columnIndex) => { // Adding margin between buttons except first column to @@ -77,8 +77,9 @@ function BigNumberPad({numberPressed, longPressHandlerStateChanged = () => {}, i return ( + + {buttonText} + + + + )} {/* Dropdown overlay */} section.data).length || 1; return ( section.data).length || 1, windowHeight, shouldUseNarrowLayout, isInLandscapeMode, true)]} + style={[styles.getCommonSelectionListPopoverHeight(itemCount, variables.optionRowHeight, windowHeight, shouldUseNarrowLayout, isInLandscapeMode, true)]} > ({label, loading, value, items, close ListItem={MultiSelectListItem} onSelectRow={updateSelectedItems} textInputOptions={textInputOptions} + style={{contentContainerStyle: [styles.pb0]}} /> )} diff --git a/src/components/Search/FilterDropdowns/SingleSelectPopup.tsx b/src/components/Search/FilterDropdowns/SingleSelectPopup.tsx index 8f4e44ad0e7d..5d3ff7964fad 100644 --- a/src/components/Search/FilterDropdowns/SingleSelectPopup.tsx +++ b/src/components/Search/FilterDropdowns/SingleSelectPopup.tsx @@ -145,7 +145,7 @@ function SingleSelectPopup({ ListItem={SingleSelectListItem} onSelectRow={updateSelectedItem} textInputOptions={textInputOptions} - style={selectionListStyle} + style={{contentContainerStyle: [styles.pb0], ...selectionListStyle}} shouldUpdateFocusedIndex={isSearchable} initiallyFocusedItemKey={isSearchable ? value?.value : undefined} shouldShowLoadingPlaceholder={!noResultsFound} diff --git a/src/components/Search/FilterDropdowns/SortByPopup.tsx b/src/components/Search/FilterDropdowns/SortByPopup.tsx index 9b82d6426a6a..7979f4519b0b 100644 --- a/src/components/Search/FilterDropdowns/SortByPopup.tsx +++ b/src/components/Search/FilterDropdowns/SortByPopup.tsx @@ -1,11 +1,12 @@ import React from 'react'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import {useSearchActionsContext, useSearchStateContext} from '@components/Search/SearchContext'; import type {SearchColumnType, SearchGroupBy, SearchQueryJSON} from '@components/Search/types'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import {close} from '@libs/actions/Modal'; import Navigation from '@libs/Navigation/Navigation'; @@ -23,13 +24,13 @@ type SortByPopupProps = { queryJSON: SearchQueryJSON; groupBy: SingleSelectItem | null; onSort: () => void; + onSortOrderPress: () => void; closeOverlay: () => void; }; -function SortByPopup({searchResults, queryJSON, groupBy, onSort, closeOverlay}: SortByPopupProps) { +function SortByPopup({searchResults, queryJSON, groupBy, onSort, onSortOrderPress, closeOverlay}: SortByPopupProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); const {accountID} = useCurrentUserPersonalDetails(); const {shouldUseLiveData} = useSearchStateContext(); const {clearSelectedTransactions} = useSearchActionsContext(); @@ -40,14 +41,11 @@ function SortByPopup({searchResults, queryJSON, groupBy, onSort, closeOverlay}: : getColumnsToShow({currentAccountID: accountID, data: searchResults.data, visibleColumns, type: searchDataType, groupBy: groupBy?.value}); const sortableColumns = getSortByOptions(currentColumns, translate); const sortBy = {text: translate(getSearchColumnTranslationKey(queryJSON.sortBy)), value: queryJSON.sortBy}; + const sortOrder = queryJSON.sortOrder; const onSortChange = (column: SearchColumnType) => { clearSelectedTransactions(); - const newQuery = buildSearchQueryString({ - ...queryJSON, - sortBy: column, - sortOrder: CONST.SEARCH.SORT_ORDER.DESC, - }); + const newQuery = buildSearchQueryString({...queryJSON, sortBy: column}); onSort(); // We want to explicitly clear stale rawQuery since it's only used for manually typed-in queries. close(() => { @@ -56,20 +54,28 @@ function SortByPopup({searchResults, queryJSON, groupBy, onSort, closeOverlay}: }; return ( - { - if (!item) { - return; - } - onSortChange(item.value); - }} - /> + <> + + + { + if (!item) { + return; + } + onSortChange(item.value); + }} + /> + ); } diff --git a/src/components/Search/FilterDropdowns/SortOrderPopup.tsx b/src/components/Search/FilterDropdowns/SortOrderPopup.tsx new file mode 100644 index 000000000000..83734a263380 --- /dev/null +++ b/src/components/Search/FilterDropdowns/SortOrderPopup.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import {useSearchActionsContext} from '@components/Search/SearchContext'; +import type {SearchQueryJSON, SortOrder} from '@components/Search/types'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {close} from '@libs/actions/Modal'; +import Navigation from '@libs/Navigation/Navigation'; +import {buildSearchQueryString} from '@libs/SearchQueryUtils'; +import {getSortOrderOptions} from '@libs/SearchUIUtils'; +import SingleSelectPopup from './SingleSelectPopup'; + +type SortOrderPopupProps = { + queryJSON: SearchQueryJSON; + onSort: () => void; + closeOverlay: () => void; +}; + +function SortOrderPopup({queryJSON, onSort, closeOverlay}: SortOrderPopupProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const {clearSelectedTransactions} = useSearchActionsContext(); + + const onSortChange = (sortOrder: SortOrder) => { + clearSelectedTransactions(); + const newQuery = buildSearchQueryString({...queryJSON, sortOrder}); + onSort(); + // We want to explicitly clear stale rawQuery since it's only used for manually typed-in queries. + close(() => { + Navigation.setParams({q: newQuery, rawQuery: undefined}); + }); + }; + + const sortOrderOptions = getSortOrderOptions(translate); + const sortOrder = {text: translate(`search.filters.sortOrder.${queryJSON.sortOrder}`), value: queryJSON.sortOrder}; + + return ( + { + if (!item) { + return; + } + onSortChange(item.value); + }} + /> + ); +} + +export default SortOrderPopup; diff --git a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx index f220e9890b5a..e751d236cf06 100644 --- a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx +++ b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx @@ -14,6 +14,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; import {getParticipantsOption} from '@libs/OptionsListUtils'; import type {OptionData} from '@libs/ReportUtils'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import BasePopup from './BasePopup'; @@ -176,7 +177,16 @@ function UserSelectPopup({value, label, closeOverlay, onChange, isSearchable}: U onApply={applyChanges} resetSentryLabel={CONST.SENTRY_LABEL.SEARCH.FILTER_POPUP_RESET_USER} applySentryLabel={CONST.SENTRY_LABEL.SEARCH.FILTER_POPUP_APPLY_USER} - style={[styles.getUserSelectionListPopoverHeight(listData.length || 1, windowHeight, shouldUseNarrowLayout, isInLandscapeMode, shouldShowSearchInput)]} + style={[ + styles.getCommonSelectionListPopoverHeight( + listData.length || 1, + variables.optionRowHeightCompact, + windowHeight, + shouldUseNarrowLayout, + isInLandscapeMode, + shouldShowSearchInput, + ), + ]} > ); diff --git a/src/components/Search/FilterDropdowns/WorkspaceSelectPopup.tsx b/src/components/Search/FilterDropdowns/WorkspaceSelectPopup.tsx index 4f8be0b07f51..e2afd79b33a6 100644 --- a/src/components/Search/FilterDropdowns/WorkspaceSelectPopup.tsx +++ b/src/components/Search/FilterDropdowns/WorkspaceSelectPopup.tsx @@ -6,13 +6,13 @@ import useOnyx from '@hooks/useOnyx'; import ONYXKEYS from '@src/ONYXKEYS'; import type {SearchAdvancedFiltersForm} from '@src/types/form'; import type {Icon} from '@src/types/onyx/OnyxCommon'; -import type {PopoverComponentProps} from './DropdownButton'; import type {MultiSelectItem} from './MultiSelectPopup'; import MultiSelectPopup from './MultiSelectPopup'; -type WorkspaceSelectPopupProps = PopoverComponentProps & { - updateFilterForm: (values: Partial) => void; +type WorkspaceSelectPopupProps = { policyIDQuery: string[] | undefined; + updateFilterForm: (values: Partial) => void; + closeOverlay: () => void; }; function filterPolicyIDSelector(searchAdvancedFiltersForm: OnyxEntry) { diff --git a/src/components/Search/SearchAutocompleteInput.tsx b/src/components/Search/SearchAutocompleteInput.tsx index 9b097fb86a0f..df1785d31675 100644 --- a/src/components/Search/SearchAutocompleteInput.tsx +++ b/src/components/Search/SearchAutocompleteInput.tsx @@ -2,7 +2,7 @@ import passthroughPolicyTagListSelector from '@selectors/PolicyTagList'; import type {ForwardedRef} from 'react'; import React, {useEffect, useRef} from 'react'; -import type {StyleProp, TextInputProps, ViewStyle} from 'react-native'; +import type {StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; import {View} from 'react-native'; import Animated, {interpolateColor, useAnimatedStyle, useSharedValue} from 'react-native-reanimated'; import FormHelpMessage from '@components/FormHelpMessage'; @@ -60,6 +60,12 @@ type SearchAutocompleteInputProps = { /** Any additional styles to apply to text input along with FormHelperMessage */ outerWrapperStyle?: StyleProp; + inputStyle?: StyleProp; + + inputContainerStyle?: StyleProp; + + touchableInputWrapperStyle?: StyleProp; + /** Whether the search reports API call is running */ isSearchingForReports?: boolean; @@ -88,6 +94,9 @@ function SearchAutocompleteInput({ wrapperStyle, wrapperFocusedStyle = {}, outerWrapperStyle, + inputStyle, + inputContainerStyle, + touchableInputWrapperStyle, isSearchingForReports, selection, substitutionMap, @@ -193,63 +202,62 @@ function SearchAutocompleteInput({ return ( - - - { - onFocus?.(); - focusedSharedValue.set(true); - }} - onBlur={() => { - focusedSharedValue.set(false); - onBlur?.(); - }} - onKeyPress={onKeyPress} - isLoading={isSearchingForReports} - ref={(element) => { - if (!ref) { - return; - } - - inputRef.current = element as AnimatedTextInputRef; - - if (typeof ref === 'function') { - ref(element); - return; - } - - // eslint-disable-next-line no-param-reassign - ref.current = element; - }} - type="markdown" - multiline={false} - parser={parser} - selection={selection} - shouldShowClearButton={!!value && !isSearchingForReports} - shouldHideClearButton={false} - onClearInput={clearInput} - /> - + + { + onFocus?.(); + focusedSharedValue.set(true); + }} + onBlur={() => { + focusedSharedValue.set(false); + onBlur?.(); + }} + onKeyPress={onKeyPress} + isLoading={isSearchingForReports} + ref={(element) => { + if (!ref) { + return; + } + + inputRef.current = element as AnimatedTextInputRef; + + if (typeof ref === 'function') { + ref(element); + return; + } + + // eslint-disable-next-line no-param-reassign + ref.current = element; + }} + type="markdown" + multiline={false} + parser={parser} + selection={selection} + shouldShowClearButton={!!value && !isSearchingForReports} + shouldHideClearButton={false} + onClearInput={clearInput} + /> - 1)} - isSmallScreenWidth={isSmallScreenWidth} - onSecondOptionSubmit={handleNonReimbursablePaymentErrorModalClose} - secondOptionText={translate('common.buttonConfirm')} - isVisible={isNonReimbursablePaymentErrorModalVisible} - onClose={handleNonReimbursablePaymentErrorModalClose} - /> {!!rejectModalAction && ( {}, removeTransaction: () => {}, clearSelectedTransactions: () => {}, - setShouldShowActionsBarLoading: () => {}, + setShouldShowFiltersBarLoading: () => {}, setShouldShowSelectAllMatchingItems: () => {}, selectAllMatchingItems: () => {}, setShouldResetSearchQuery: () => {}, @@ -103,7 +103,7 @@ function SearchContextProvider({children}: SearchContextProps) { const areTransactionsEmpty = useRef(true); const [lastSearchType, setLastSearchType] = useState(); const [areAllMatchingItemsSelected, selectAllMatchingItems] = useState(false); - const [shouldShowActionsBarLoading, setShouldShowActionsBarLoading] = useState(false); + const [shouldShowFiltersBarLoading, setShouldShowFiltersBarLoading] = useState(false); const [shouldShowSelectAllMatchingItems, setShouldShowSelectAllMatchingItems] = useState(false); const [searchContextData, setSearchContextData] = useState({...defaultSearchContextData}); @@ -330,7 +330,7 @@ function SearchContextProvider({children}: SearchContextProps) { currentSimilarSearchHash, currentSearchResults, shouldUseLiveData, - shouldShowActionsBarLoading, + shouldShowFiltersBarLoading, lastSearchType, shouldShowSelectAllMatchingItems, areAllMatchingItemsSelected, @@ -344,7 +344,7 @@ function SearchContextProvider({children}: SearchContextProps) { currentSimilarSearchHash, currentSearchResults, shouldUseLiveData, - shouldShowActionsBarLoading, + shouldShowFiltersBarLoading, lastSearchType, shouldShowSelectAllMatchingItems, areAllMatchingItemsSelected, @@ -358,13 +358,13 @@ function SearchContextProvider({children}: SearchContextProps) { setSelectedTransactions, setCurrentSelectedTransactionReportID, clearSelectedTransactions, - setShouldShowActionsBarLoading, + setShouldShowFiltersBarLoading, setLastSearchType, setShouldShowSelectAllMatchingItems, selectAllMatchingItems, setShouldResetSearchQuery, }), - // setShouldShowActionsBarLoading, setLastSearchType, setShouldShowSelectAllMatchingItems, + // shouldShowFiltersBarLoading, setLastSearchType, setShouldShowSelectAllMatchingItems, // and selectAllMatchingItems are stable useState setters — excluded from deps intentionally. // setCurrentSelectedTransactionReportID only uses setSearchContextData (stable setter). [removeTransaction, setSelectedTransactions, clearSelectedTransactions, setShouldResetSearchQuery], diff --git a/src/components/Search/SearchList/BaseSearchList/index.tsx b/src/components/Search/SearchList/BaseSearchList/index.tsx index 13c04e34ab1e..54d17dc7672c 100644 --- a/src/components/Search/SearchList/BaseSearchList/index.tsx +++ b/src/components/Search/SearchList/BaseSearchList/index.tsx @@ -32,7 +32,8 @@ function BaseSearchList({ flattenedItemsLength, newTransactions, selectedTransactions, - customCardNames, + policyForMovingExpenses, + nonPersonalAndWorkspaceCards, }: BaseSearchListProps) { const hasKeyBeenPressed = useRef(false); const isFocused = useIsFocused(); @@ -106,8 +107,8 @@ function BaseSearchList({ }, [setHasKeyBeenPressed]); const extraData = useMemo( - () => [focusedIndex, columns, newTransactions, selectedTransactions, customCardNames], - [focusedIndex, columns, newTransactions, selectedTransactions, customCardNames], + () => [focusedIndex, columns, newTransactions, selectedTransactions, nonPersonalAndWorkspaceCards, policyForMovingExpenses], + [focusedIndex, columns, newTransactions, selectedTransactions, nonPersonalAndWorkspaceCards, policyForMovingExpenses], ); return ( diff --git a/src/components/Search/SearchList/BaseSearchList/types.ts b/src/components/Search/SearchList/BaseSearchList/types.ts index afd925474cea..b0c05aeb9e58 100644 --- a/src/components/Search/SearchList/BaseSearchList/types.ts +++ b/src/components/Search/SearchList/BaseSearchList/types.ts @@ -4,7 +4,7 @@ import type {NativeSyntheticEvent} from 'react-native'; import type {SearchListItem} from '@components/Search/SearchList/ListItem/types'; import type {SearchColumnType, SelectedTransactions} from '@components/Search/types'; import type {ExtendedTargetedEvent} from '@components/SelectionList/ListItem/types'; -import type {Transaction} from '@src/types/onyx'; +import type {CardList, Policy, Transaction} from '@src/types/onyx'; type BaseSearchListProps = Pick< FlashListProps, @@ -45,8 +45,11 @@ type BaseSearchListProps = Pick< /** Selected transactions for triggering re-render via extraData */ selectedTransactions?: SelectedTransactions; - /** Custom card names for triggering re-render via extraData */ - customCardNames?: Record; + /** Policy for moving expenses for triggering re-render via extraData */ + policyForMovingExpenses?: Policy; + + /** Non-personal and workspace cards for triggering re-render via extraData */ + nonPersonalAndWorkspaceCards?: CardList; }; export default BaseSearchListProps; diff --git a/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx b/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx index 97a410be7b6f..4b0b5f476175 100644 --- a/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx +++ b/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx @@ -5,7 +5,6 @@ import {SearchScopeProvider} from '@components/Search/SearchScopeProvider'; import SettlementButton from '@components/SettlementButton'; import type {PaymentActionParams} from '@components/SettlementButton/types'; import useNetwork from '@hooks/useNetwork'; -import useNonReimbursablePaymentModal from '@hooks/useNonReimbursablePaymentModal'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; import useReportWithTransactionsAndViolations from '@hooks/useReportWithTransactionsAndViolations'; @@ -13,7 +12,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {canIOUBePaid} from '@libs/actions/IOU/ReportWorkflow'; import {getPayMoneyOnSearchInvoiceParams, payMoneyRequestOnSearch} from '@libs/actions/Search'; import {convertToDisplayString} from '@libs/CurrencyUtils'; -import {hasOnlyNonReimbursableTransactions, isInvoiceReport} from '@libs/ReportUtils'; +import {isInvoiceReport} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -34,17 +33,13 @@ function PayActionCell({isLoading, policyID, reportID, hash, amount, extraSmall, const {isDelegateAccessRestricted} = useDelegateNoAccessState(); const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); const [iouReport, transactions] = useReportWithTransactionsAndViolations(reportID); - const {showNonReimbursablePaymentErrorModal, shouldBlockDirectPayment} = useNonReimbursablePaymentModal(iouReport, transactions); const policy = usePolicy(policyID); const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReport?.chatReportID}`); const invoiceReceiverPolicyID = chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : undefined; const invoiceReceiverPolicy = usePolicy(invoiceReceiverPolicyID); const canBePaid = canIOUBePaid(iouReport, chatReport, policy, bankAccountList, transactions, false, undefined, invoiceReceiverPolicy); - const shouldOnlyShowElsewhere = - !canBePaid && - canIOUBePaid(iouReport, chatReport, policy, bankAccountList, transactions, true, undefined, invoiceReceiverPolicy) && - !hasOnlyNonReimbursableTransactions(iouReport?.reportID, transactions); + const shouldOnlyShowElsewhere = !canBePaid && canIOUBePaid(iouReport, chatReport, policy, bankAccountList, transactions, true, undefined, invoiceReceiverPolicy); const {currency} = iouReport ?? {}; @@ -58,11 +53,6 @@ function PayActionCell({isLoading, policyID, reportID, hash, amount, extraSmall, return; } - if (shouldBlockDirectPayment(type)) { - showNonReimbursablePaymentErrorModal(); - return; - } - const invoiceParams = getPayMoneyOnSearchInvoiceParams(policyID, payAsBusiness, methodID, paymentMethod); payMoneyRequestOnSearch(hash, [ { diff --git a/src/components/Search/SearchList/ListItem/ActionCell/actionTranslationsMap.ts b/src/components/Search/SearchList/ListItem/ActionCell/actionTranslationsMap.ts index d1d96ce43458..c1da181721ef 100644 --- a/src/components/Search/SearchList/ListItem/ActionCell/actionTranslationsMap.ts +++ b/src/components/Search/SearchList/ListItem/ActionCell/actionTranslationsMap.ts @@ -11,6 +11,7 @@ const actionTranslationsMap: Record = [CONST.SEARCH.ACTION_TYPES.EXPORT_TO_ACCOUNTING]: 'common.export', [CONST.SEARCH.ACTION_TYPES.DONE]: 'common.done', [CONST.SEARCH.ACTION_TYPES.PAID]: 'iou.settledExpensify', + [CONST.SEARCH.ACTION_TYPES.UNDELETE]: 'search.bulkActions.undelete', }; export default actionTranslationsMap; diff --git a/src/components/Search/SearchList/ListItem/ActionCell/index.tsx b/src/components/Search/SearchList/ListItem/ActionCell/index.tsx index 1ab70eeac6ea..edf8e5814401 100644 --- a/src/components/Search/SearchList/ListItem/ActionCell/index.tsx +++ b/src/components/Search/SearchList/ListItem/ActionCell/index.tsx @@ -11,7 +11,7 @@ import PayActionCell from './PayActionCell'; type ActionCellProps = { action?: SearchTransactionAction; isSelected?: boolean; - goToItem: () => void; + onButtonPress: () => void; isChildListItem?: boolean; isLoading?: boolean; policyID?: string; @@ -25,7 +25,7 @@ type ActionCellProps = { function ActionCell({ action = CONST.SEARCH.ACTION_TYPES.VIEW, isSelected = false, - goToItem, + onButtonPress, isChildListItem = false, isLoading = false, policyID = '', @@ -41,7 +41,7 @@ function ActionCell({ const shouldUseViewAction = action === CONST.SEARCH.ACTION_TYPES.VIEW || action === CONST.SEARCH.ACTION_TYPES.PAID || action === CONST.SEARCH.ACTION_TYPES.DONE; - if (shouldUseViewAction || isChildListItem) { + if (shouldUseViewAction || (isChildListItem && action !== CONST.SEARCH.ACTION_TYPES.UNDELETE)) { const text = translate(actionTranslationsMap[CONST.SEARCH.ACTION_TYPES.VIEW]); const buttonInnerStyles = isSelected ? styles.buttonDefaultSelected : {}; @@ -49,7 +49,7 @@ function ActionCell({ - ); -} - -/** - * In the submit-and-navigate flow we only ever land on `type:expense` or `type:invoice` - * with default status and no extra filters, so the chips are mostly hardcoded. - * The only conditional chip is "Workspaces" (shown when the user has >1 workspace), - * resolved via a cheap boolean Onyx selector. - */ -function StaticFiltersBar({queryJSON}: {queryJSON: SearchQueryJSON}) { - const styles = useThemeStyles(); - const theme = useTheme(); - const {translate} = useLocalize(); - const expensifyIcons = useMemoizedLazyExpensifyIcons(['Filter'] as const); - const [policyInfo] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: staticPolicyInfoSelector}); - const hasMultipleWorkspaces = policyInfo?.hasMultipleWorkspaces ?? false; - - const typeLabel = queryJSON.type === CONST.SEARCH.DATA_TYPES.INVOICE ? translate('common.invoice') : translate('common.expense'); - - const chips = useMemo( - () => [ - {key: 'type', label: `${translate('common.type')}: ${typeLabel}`}, - {key: 'status', label: translate('common.status')}, - {key: 'date', label: translate('common.date')}, - {key: 'from', label: translate('common.from')}, - ...(hasMultipleWorkspaces ? [{key: 'workspace', label: translate('workspace.common.workspace')}] : []), - ], - [translate, typeLabel, hasMultipleWorkspaces], - ); - - return ( - - item.key} - renderItem={({item}) => } - ListFooterComponent={ -