diff --git a/.github/actions/composite/setupNode/action.yml b/.github/actions/composite/setupNode/action.yml index 0b32d8ee6dc1..c6a6029e06e0 100644 --- a/.github/actions/composite/setupNode/action.yml +++ b/.github/actions/composite/setupNode/action.yml @@ -18,13 +18,13 @@ runs: desktop/package-lock.json - id: cache-node-modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: node_modules key: ${{ runner.os }}-node-modules-${{ hashFiles('package-lock.json', 'patches/**') }} - id: cache-desktop-node-modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: desktop/node_modules key: ${{ runner.os }}-desktop-node-modules-${{ hashFiles('desktop/package-lock.json', 'desktop/patches/**') }} diff --git a/.github/actions/javascript/bumpVersion/index.js b/.github/actions/javascript/bumpVersion/index.js index d17760baa91f..8fe84446ba82 100644 --- a/.github/actions/javascript/bumpVersion/index.js +++ b/.github/actions/javascript/bumpVersion/index.js @@ -2657,12 +2657,17 @@ createToken('XRANGELOOSE', `^${src[t.GTLT]}\\s*${src[t.XRANGEPLAINLOOSE]}$`) // Coercion. // Extract anything that could conceivably be a part of a valid semver -createToken('COERCE', `${'(^|[^\\d])' + +createToken('COERCEPLAIN', `${'(^|[^\\d])' + '(\\d{1,'}${MAX_SAFE_COMPONENT_LENGTH}})` + `(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?` + - `(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?` + + `(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?`) +createToken('COERCE', `${src[t.COERCEPLAIN]}(?:$|[^\\d])`) +createToken('COERCEFULL', src[t.COERCEPLAIN] + + `(?:${src[t.PRERELEASE]})?` + + `(?:${src[t.BUILD]})?` + `(?:$|[^\\d])`) createToken('COERCERTL', src[t.COERCE], true) +createToken('COERCERTLFULL', src[t.COERCEFULL], true) // Tilde ranges. // Meaning is "reasonably at or greater than" diff --git a/.github/workflows/checkE2ETestCode.yml b/.github/workflows/checkE2ETestCode.yml new file mode 100644 index 000000000000..090b7a7f23e4 --- /dev/null +++ b/.github/workflows/checkE2ETestCode.yml @@ -0,0 +1,23 @@ +name: Check e2e test code builds correctly + +on: + workflow_call: + pull_request: + types: [opened, synchronize] + paths: + - 'tests/e2e/**' + - 'src/libs/E2E/**' + +jobs: + lint: + if: ${{ github.actor != 'OSBotify' || github.event_name == 'workflow_call' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: ./.github/actions/composite/setupNode + + - name: Verify e2e tests compile correctly + run: npm run e2e-test-runner-build \ No newline at end of file diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml index 338cb8313465..8a47ea4bb220 100644 --- a/.github/workflows/e2ePerformanceTests.yml +++ b/.github/workflows/e2ePerformanceTests.yml @@ -15,6 +15,10 @@ on: type: string required: true +concurrency: + group: "${{ github.ref }}-e2e" + cancel-in-progress: true + jobs: buildBaseline: runs-on: ubuntu-latest-xl @@ -175,23 +179,11 @@ jobs: - name: Rename delta APK run: mv "${{steps.downloadDeltaAPK.outputs.download-path}}/app-e2edelta-release.apk" "${{steps.downloadDeltaAPK.outputs.download-path}}/app-e2edeltaRelease.apk" - - name: Copy e2e code into zip folder - run: cp -r tests/e2e zip + - name: Compile test runner to be executable in a nodeJS environment + run: npm run e2e-test-runner-build - # Note: we can't reuse the apps tsconfig, as it depends on modules that aren't available in the AWS Device Farm environment - - name: Write tsconfig.json to zip folder - run: | - echo '{ - "compilerOptions": { - "target": "ESNext", - "module": "commonjs", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "allowJs": true, - } - }' > zip/tsconfig.json + - name: Copy e2e code into zip folder + run: cp tests/e2e/dist/index.js zip/testRunner.js - name: Zip everything in the zip directory up run: zip -qr App.zip ./zip diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 33c850823413..50e886942c98 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,8 +7,13 @@ on: branches-ignore: [staging, production] paths: ['**.js', '**.ts', '**.tsx', '**.json', '**.mjs', '**.cjs', 'config/.editorconfig', '.watchmanconfig', '.imgbotconfig'] +concurrency: + group: "${{ github.ref }}-lint" + cancel-in-progress: true + jobs: lint: + name: Run ESLint if: ${{ github.actor != 'OSBotify' || github.event_name == 'workflow_call' }} runs-on: ubuntu-latest steps: diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index 818441828bf0..4d6597334447 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -194,7 +194,7 @@ jobs: bundler-cache: true - name: Cache Pod dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 id: pods-cache with: path: ios/Pods diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bdc14950a337..71b4bc3d8fc3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,6 +7,10 @@ on: branches-ignore: [staging, production] paths: ['**.js', '**.ts', '**.tsx', '**.sh', 'package.json', 'package-lock.json'] +concurrency: + group: "${{ github.ref }}-jest" + cancel-in-progress: true + jobs: jest: if: ${{ github.actor != 'OSBotify' && github.actor != 'imgbot[bot]' || github.event_name == 'workflow_call' }} @@ -31,7 +35,7 @@ jobs: - name: Cache Jest cache id: cache-jest-cache - uses: actions/cache@ac25611caef967612169ab7e95533cf932c32270 + uses: actions/cache@v4 with: path: .jest-cache key: ${{ runner.os }}-jest diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index 9548c3a6e595..3f02430f3c1f 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -167,7 +167,7 @@ jobs: bundler-cache: true - name: Cache Pod dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 id: pods-cache with: path: ios/Pods diff --git a/__mocks__/react-native.js b/__mocks__/react-native.ts similarity index 63% rename from __mocks__/react-native.js rename to __mocks__/react-native.ts index 1eeea877ca0f..27b78b308446 100644 --- a/__mocks__/react-native.js +++ b/__mocks__/react-native.ts @@ -1,27 +1,47 @@ // eslint-disable-next-line no-restricted-imports import * as ReactNative from 'react-native'; -import _ from 'underscore'; +import type StartupTimer from '@libs/StartupTimer/types'; + +const {BootSplash} = ReactNative.NativeModules; jest.doMock('react-native', () => { let url = 'https://new.expensify.com/'; const getInitialURL = () => Promise.resolve(url); - let appState = 'active'; + let appState: ReactNative.AppStateStatus = 'active'; let count = 0; - const changeListeners = {}; + const changeListeners: Record void> = {}; // Tests will run with the app in a typical small screen size by default. We do this since the react-native test renderer // runs against index.native.js source and so anything that is testing a component reliant on withWindowDimensions() // would be most commonly assumed to be on a mobile phone vs. a tablet or desktop style view. This behavior can be // overridden by explicitly setting the dimensions inside a test via Dimensions.set() - let dimensions = { + let dimensions: Record = { width: 300, height: 700, scale: 1, fontScale: 1, }; - return Object.setPrototypeOf( + type ReactNativeMock = typeof ReactNative & { + NativeModules: typeof ReactNative.NativeModules & { + BootSplash: { + getVisibilityStatus: typeof BootSplash.getVisibilityStatus; + hide: typeof BootSplash.hide; + logoSizeRatio: number; + navigationBarHeight: number; + }; + StartupTimer: StartupTimer; + }; + Linking: typeof ReactNative.Linking & { + setInitialURL: (newUrl: string) => void; + }; + AppState: typeof ReactNative.AppState & { + emitCurrentTestState: (state: ReactNative.AppStateStatus) => void; + }; + }; + + const reactNativeMock: ReactNativeMock = Object.setPrototypeOf( { NativeModules: { ...ReactNative.NativeModules, @@ -36,7 +56,7 @@ jest.doMock('react-native', () => { Linking: { ...ReactNative.Linking, getInitialURL, - setInitialURL(newUrl) { + setInitialURL(newUrl: string) { url = newUrl; }, }, @@ -45,11 +65,11 @@ jest.doMock('react-native', () => { get currentState() { return appState; }, - emitCurrentTestState(state) { + emitCurrentTestState(state: ReactNative.AppStateStatus) { appState = state; - _.each(changeListeners, (listener) => listener(appState)); + Object.entries(changeListeners).forEach(([, listener]) => listener(appState)); }, - addEventListener(type, listener) { + addEventListener(type: ReactNative.AppStateEvent, listener: (state: ReactNative.AppStateStatus) => void) { if (type === 'change') { const originalCount = count; changeListeners[originalCount] = listener; @@ -68,7 +88,7 @@ jest.doMock('react-native', () => { ...ReactNative.Dimensions, addEventListener: jest.fn(), get: () => dimensions, - set: (newDimensions) => { + set: (newDimensions: Record) => { dimensions = newDimensions; }, }, @@ -78,9 +98,11 @@ jest.doMock('react-native', () => { // so it seems easier to just run the callback immediately in tests. InteractionManager: { ...ReactNative.InteractionManager, - runAfterInteractions: (callback) => callback(), + runAfterInteractions: (callback: () => void) => callback(), }, }, ReactNative, ); + + return reactNativeMock; }); diff --git a/android/app/build.gradle b/android/app/build.gradle index 9886cd5cccec..e285d0bff26f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001044600 - versionName "1.4.46-0" + versionCode 1001044601 + versionName "1.4.46-1" } flavorDimensions "default" diff --git a/assets/images/make-admin.svg b/assets/images/make-admin.svg new file mode 100644 index 000000000000..383708e0523c --- /dev/null +++ b/assets/images/make-admin.svg @@ -0,0 +1,12 @@ + + + + + + + diff --git a/assets/images/remove-members.svg b/assets/images/remove-members.svg new file mode 100644 index 000000000000..e9d7e08f5e5e --- /dev/null +++ b/assets/images/remove-members.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md "b/docs/articles/expensify-classic/expensify-card/Set-Up-the-Expensify-Visa\302\256-Commercial-Card-for-your-Company.md" similarity index 98% rename from docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md rename to "docs/articles/expensify-classic/expensify-card/Set-Up-the-Expensify-Visa\302\256-Commercial-Card-for-your-Company.md" index 1cf29531f696..8f6a3f1a908d 100644 --- a/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md +++ "b/docs/articles/expensify-classic/expensify-card/Set-Up-the-Expensify-Visa\302\256-Commercial-Card-for-your-Company.md" @@ -1,5 +1,5 @@ --- -title: Set Up the Card for your Company +title: Set Up the Expensify Visa® Commercial Card for your Company description: Details on setting up the Expensify Card for your company as an admin --- # Overview diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index dff05f61933e..374594698200 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.46.0 + 1.4.46.1 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index fa6995f65b5a..e314ea1595e1 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.46.0 + 1.4.46.1 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index e8cd0ebb4e0a..aaec6344175f 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 1.4.46 CFBundleVersion - 1.4.46.0 + 1.4.46.1 NSExtension NSExtensionPointIdentifier diff --git a/jest.config.js b/jest.config.js index 441507af4228..5b36e44c7581 100644 --- a/jest.config.js +++ b/jest.config.js @@ -23,7 +23,7 @@ module.exports = { }, testEnvironment: 'jsdom', setupFiles: ['/jest/setup.ts', './node_modules/@react-native-google-signin/google-signin/jest/build/setup.js'], - setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect', '/jest/setupAfterEnv.ts', '/tests/perf-test/setupAfterEnv.js'], + setupFilesAfterEnv: ['/jest/setupAfterEnv.ts', '/tests/perf-test/setupAfterEnv.js'], cacheDirectory: '/.jest-cache', moduleNameMapper: { '\\.(lottie)$': '/__mocks__/fileMock.ts', diff --git a/jest/setupAfterEnv.ts b/jest/setupAfterEnv.ts index 6f7836b64dbb..d59495874588 100644 --- a/jest/setupAfterEnv.ts +++ b/jest/setupAfterEnv.ts @@ -1 +1,4 @@ +// This is required in order for jest to recognize custom matchers like toBeDisabled. This can be removed once testing-library/react-native version is bumped to v12.4 or later +import '@testing-library/jest-native/extend-expect'; + jest.useRealTimers(); diff --git a/package-lock.json b/package-lock.json index 5f55ddd82868..80d3d1c6e911 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.46-0", + "version": "1.4.46-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.46-0", + "version": "1.4.46-1", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -37,7 +37,7 @@ "@react-native-google-signin/google-signin": "^10.0.1", "@react-native-picker/picker": "2.5.1", "@react-navigation/material-top-tabs": "^6.6.3", - "@react-navigation/native": "6.1.8", + "@react-navigation/native": "6.1.12", "@react-navigation/stack": "6.3.16", "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "^10.1.11", @@ -202,7 +202,7 @@ "css-loader": "^6.7.2", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", - "electron": "^26.6.8", + "electron": "^29.0.0", "electron-builder": "24.6.4", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", @@ -10317,9 +10317,9 @@ } }, "node_modules/@react-navigation/core": { - "version": "6.4.10", - "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-6.4.10.tgz", - "integrity": "sha512-oYhqxETRHNHKsipm/BtGL0LI43Hs2VSFoWMbBdHK9OqgQPjTVUitslgLcPpo4zApCcmBWoOLX2qPxhsBda644A==", + "version": "6.4.11", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-6.4.11.tgz", + "integrity": "sha512-kOCyOc1L0lAl53DbyNl3OkUJwSFKSaVCsV8leJawUXMXJ1FTT3nbS3xMOqbZuchxIbl8T62sZ7YnlWG/21rcMw==", "dependencies": { "@react-navigation/routers": "^6.1.9", "escape-string-regexp": "^4.0.0", @@ -10345,6 +10345,17 @@ "react": "*" } }, + "node_modules/@react-navigation/elements": { + "version": "1.3.24", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.24.tgz", + "integrity": "sha512-zgZV50qjlv3/N+MzNV0DIRmtg30IZcR0+LaTQRP/OxLtveQkgUG6wIEKl6SXO2ykC9yF9V82msdCzKl9uPSQCA==", + "peerDependencies": { + "@react-navigation/native": "^6.0.0", + "react": "*", + "react-native": "*", + "react-native-safe-area-context": ">= 3.0.0" + } + }, "node_modules/@react-navigation/material-top-tabs": { "version": "6.6.3", "resolved": "https://registry.npmjs.org/@react-navigation/material-top-tabs/-/material-top-tabs-6.6.3.tgz", @@ -10362,11 +10373,11 @@ } }, "node_modules/@react-navigation/native": { - "version": "6.1.8", - "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-6.1.8.tgz", - "integrity": "sha512-0alti852nV+8oCVm9H80G6kZvrHoy51+rXBvVCRUs2rNDDozC/xPZs8tyeCJkqdw3cpxZDK8ndXF22uWq28+0Q==", + "version": "6.1.12", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-6.1.12.tgz", + "integrity": "sha512-t6y7sDCr0HlMf+5TuVjLjyi0ySs0eNGfreDKcWOMEi5wooNFM4LhcUCdEVylpwCPfjQMW/lNVomNromqZFM6HQ==", "dependencies": { - "@react-navigation/core": "^6.4.9", + "@react-navigation/core": "^6.4.11", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.1.23" @@ -10402,17 +10413,6 @@ "react-native-screens": ">= 3.0.0" } }, - "node_modules/@react-navigation/stack/node_modules/@react-navigation/elements": { - "version": "1.3.17", - "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.17.tgz", - "integrity": "sha512-sui8AzHm6TxeEvWT/NEXlz3egYvCUog4tlXA4Xlb2Vxvy3purVXDq/XsM56lJl344U5Aj/jDzkVanOTMWyk4UA==", - "peerDependencies": { - "@react-navigation/native": "^6.0.0", - "react": "*", - "react-native": "*", - "react-native-safe-area-context": ">= 3.0.0" - } - }, "node_modules/@react-ng/bounds-observer": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@react-ng/bounds-observer/-/bounds-observer-0.2.1.tgz", @@ -24894,20 +24894,22 @@ } }, "node_modules/browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", - "license": "ISC", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz", + "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==", "dependencies": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", + "elliptic": "^6.5.4", "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" + "parse-asn1": "^5.1.6", + "readable-stream": "^3.6.2", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 4" } }, "node_modules/browserify-sign/node_modules/readable-stream": { @@ -28644,14 +28646,14 @@ } }, "node_modules/electron": { - "version": "26.6.8", - "resolved": "https://registry.npmjs.org/electron/-/electron-26.6.8.tgz", - "integrity": "sha512-nuzJ5nVButL1jErc97IVb+A6jbContMg5Uuz5fhmZ4NLcygLkSW8FZpnOT7A4k8Saa95xDJOvqGZyQdI/OPNFw==", + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-29.0.0.tgz", + "integrity": "sha512-HhrRC5vWb6fAbWXP3A6ABwKUO9JvYSC4E141RzWFgnDBqNiNtabfmgC8hsVeCR65RQA2MLSDgC8uP52I9zFllQ==", "dev": true, "hasInstallScript": true, "dependencies": { "@electron/get": "^2.0.0", - "@types/node": "^18.11.18", + "@types/node": "^20.9.0", "extract-zip": "^2.0.1" }, "bin": { @@ -28932,15 +28934,6 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.435.tgz", "integrity": "sha512-B0CBWVFhvoQCW/XtjRzgrmqcgVWg6RXOEM/dK59+wFV93BFGR6AeNKc4OyhM+T3IhJaOOG8o/V+33Y2mwJWtzw==" }, - "node_modules/electron/node_modules/@types/node": { - "version": "18.19.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.8.tgz", - "integrity": "sha512-g1pZtPhsvGVTwmeVoexWZLTQaOvXwoSq//pTL0DHeNzUDrFnir4fgETdhjhIxjVnN+hKOuh98+E1eMLnUXstFg==", - "dev": true, - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/element-resize-detector": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/element-resize-detector/-/element-resize-detector-1.2.4.tgz", diff --git a/package.json b/package.json index e3c23d4538d3..c927c41134db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.46-0", + "version": "1.4.46-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.", @@ -55,7 +55,8 @@ "gh-actions-unused-styles": "./.github/scripts/findUnusedKeys.sh", "workflow-test": "./workflow_tests/scripts/runWorkflowTests.sh", "workflow-test:generate": "ts-node workflow_tests/utils/preGenerateTest.js", - "setup-https": "mkcert -install && mkcert -cert-file config/webpack/certificate.pem -key-file config/webpack/key.pem dev.new.expensify.com localhost 127.0.0.1" + "setup-https": "mkcert -install && mkcert -cert-file config/webpack/certificate.pem -key-file config/webpack/key.pem dev.new.expensify.com localhost 127.0.0.1", + "e2e-test-runner-build": "ncc build tests/e2e/testRunner.js -o tests/e2e/dist/" }, "dependencies": { "@dotlottie/react-player": "^1.6.3", @@ -85,7 +86,7 @@ "@react-native-google-signin/google-signin": "^10.0.1", "@react-native-picker/picker": "2.5.1", "@react-navigation/material-top-tabs": "^6.6.3", - "@react-navigation/native": "6.1.8", + "@react-navigation/native": "6.1.12", "@react-navigation/stack": "6.3.16", "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "^10.1.11", @@ -250,7 +251,7 @@ "css-loader": "^6.7.2", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", - "electron": "^26.6.8", + "electron": "^29.0.0", "electron-builder": "24.6.4", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", diff --git a/patches/@react-navigation+native+6.1.8.patch b/patches/@react-navigation+native+6.1.12.patch similarity index 97% rename from patches/@react-navigation+native+6.1.8.patch rename to patches/@react-navigation+native+6.1.12.patch index c461d7e510fe..d451d89d687c 100644 --- a/patches/@react-navigation+native+6.1.8.patch +++ b/patches/@react-navigation+native+6.1.12.patch @@ -133,7 +133,7 @@ index 0000000..16da117 +//# sourceMappingURL=findFocusedRouteKey.js.map \ No newline at end of file diff --git a/node_modules/@react-navigation/native/lib/module/useLinking.js b/node_modules/@react-navigation/native/lib/module/useLinking.js -index 6f0ac51..a77b608 100644 +index 6688c62..95a0e32 100644 --- a/node_modules/@react-navigation/native/lib/module/useLinking.js +++ b/node_modules/@react-navigation/native/lib/module/useLinking.js @@ -2,6 +2,7 @@ import { findFocusedRoute, getActionFromState as getActionFromStateDefault, getP @@ -189,17 +189,17 @@ index 6f0ac51..a77b608 100644 export default function useLinking(ref, _ref) { let { independent, -@@ -231,6 +270,9 @@ export default function useLinking(ref, _ref) { +@@ -234,6 +273,9 @@ export default function useLinking(ref, _ref) { // Otherwise it's likely a change triggered by `popstate` path !== pendingPath) { const historyDelta = (focusedState.history ? focusedState.history.length : focusedState.routes.length) - (previousFocusedState.history ? previousFocusedState.history.length : previousFocusedState.routes.length); -+ ++ + // The historyDelta and historyDeltaByKeys may differ if the new state has an entry that didn't exist in previous state + const historyDeltaByKeys = getHistoryDeltaByKeys(focusedState, previousFocusedState); if (historyDelta > 0) { // If history length is increased, we should pushState // Note that path might not actually change here, for example, drawer open should pushState -@@ -242,34 +284,55 @@ export default function useLinking(ref, _ref) { +@@ -245,7 +287,8 @@ export default function useLinking(ref, _ref) { // If history length is decreased, i.e. entries were removed, we want to go back const nextIndex = history.backIndex({ @@ -209,7 +209,8 @@ index 6f0ac51..a77b608 100644 }); const currentIndex = history.index; try { - if (nextIndex !== -1 && nextIndex < currentIndex) { +@@ -254,27 +297,47 @@ export default function useLinking(ref, _ref) { + history.get(nextIndex - currentIndex)) { // An existing entry for this path exists and it's less than current index, go back to that await history.go(nextIndex - currentIndex); + history.replace({ @@ -263,7 +264,7 @@ index 6f0ac51..a77b608 100644 + path, + state + }); -+ } ++ } } } else { // If no common navigation state was found, assume it's a replace diff --git a/src/CONST.ts b/src/CONST.ts index 8d4eaac44a38..9ed2903941b6 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -308,6 +308,7 @@ const CONST = { BETA_COMMENT_LINKING: 'commentLinking', VIOLATIONS: 'violations', REPORT_FIELDS: 'reportFields', + WORKFLOWS_DELAYED_SUBMISSION: 'workflowsDelayedSubmission', }, BUTTON_STATES: { DEFAULT: 'default', @@ -690,6 +691,7 @@ const CONST = { DOMAIN_ALL: 'domainAll', POLICY_ROOM: 'policyRoom', POLICY_EXPENSE_CHAT: 'policyExpenseChat', + SELF_DM: 'selfDM', }, WORKSPACE_CHAT_ROOMS: { ANNOUNCE: '#announce', @@ -1387,6 +1389,11 @@ const CONST = { }, ID_FAKE: '_FAKE_', EMPTY: 'EMPTY', + MEMBERS_BULK_ACTION_TYPES: { + REMOVE: 'remove', + MAKE_MEMBER: 'makeMember', + MAKE_ADMIN: 'makeAdmin', + }, }, CUSTOM_UNITS: { @@ -3332,6 +3339,10 @@ const CONST = { SESSION_STORAGE_KEYS: { INITIAL_URL: 'INITIAL_URL', }, + + AUTH_TOKEN_TYPE: { + ANONYMOUS: 'anonymousAccount', + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 22ebffd52eec..e9cdce4f6ed9 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -486,6 +486,18 @@ const ROUTES = { route: 'workspace/:policyID/workflows', getRoute: (policyID: string) => `workspace/${policyID}/workflows` as const, }, + WORKSPACE_WORKFLOWS_APPROVER: { + route: 'workspace/:policyID/settings/workflows/approver', + getRoute: (policyId: string) => `workspace/${policyId}/settings/workflows/approver` as const, + }, + WORKSPACE_WORKFLOWS_AUTOREPORTING_FREQUENCY: { + route: 'workspace/:policyID/settings/workflows/auto-reporting-frequency', + getRoute: (policyID: string) => `workspace/${policyID}/settings/workflows/auto-reporting-frequency` as const, + }, + WORKSPACE_WORKFLOWS_AUTOREPORTING_MONTHLY_OFFSET: { + route: 'workspace/:policyID/settings/workflows/auto-reporting-frequency/monthly-offset', + getRoute: (policyID: string) => `workspace/${policyID}/settings/workflows/auto-reporting-frequency/monthly-offset` as const, + }, WORKSPACE_CARD: { route: 'workspace/:policyID/card', getRoute: (policyID: string) => `workspace/${policyID}/card` as const, @@ -526,6 +538,10 @@ const ROUTES = { route: 'workspace/:policyID/categories', getRoute: (policyID: string) => `workspace/${policyID}/categories` as const, }, + WORKSPACE_CATEGORY_SETTINGS: { + route: 'workspace/:policyID/categories/:categoryName', + getRoute: (policyID: string, categoryName: string) => `workspace/${policyID}/categories/${encodeURI(categoryName)}` as const, + }, WORKSPACE_CATEGORIES_SETTINGS: { route: 'workspace/:policyID/categories/settings', getRoute: (policyID: string) => `workspace/${policyID}/categories/settings` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index ac75968e68b9..ff3dbfd7f901 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -216,9 +216,13 @@ const SCREENS = { CATEGORIES: 'Workspace_Categories', CURRENCY: 'Workspace_Profile_Currency', WORKFLOWS: 'Workspace_Workflows', + WORKFLOWS_APPROVER: 'Workspace_Workflows_Approver', + WORKFLOWS_AUTO_REPORTING_FREQUENCY: 'Workspace_Workflows_Auto_Reporting_Frequency', + WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET: 'Workspace_Workflows_Auto_Reporting_Monthly_Offset', DESCRIPTION: 'Workspace_Profile_Description', SHARE: 'Workspace_Profile_Share', NAME: 'Workspace_Profile_Name', + CATEGORY_SETTINGS: 'Category_Settings', CATEGORIES_SETTINGS: 'Categories_Settings', }, diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index 4f21f4aa1dc3..39c91c2a0789 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -350,7 +350,7 @@ function AddressSearch( return ( {!!title && {title}} - {subtitle} + {subtitle} ); }} diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.js b/src/components/Attachments/AttachmentCarousel/CarouselItem.js index b2c9fed64467..e924cb8c13e9 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.js +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.js @@ -109,6 +109,7 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}) { isHovered={isModalHovered} isFocused={isFocused} optionalVideoDuration={item.duration} + isUsedInCarousel /> diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 8ea8a1bb6f64..2b2d0a60f657 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -1,5 +1,5 @@ import React, {useEffect, useState} from 'react'; -import type {StyleProp, ViewStyle} from 'react-native'; +import type {ImageStyle, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -19,7 +19,7 @@ type AvatarProps = { source?: AvatarSource; /** Extra styles to pass to Image */ - imageStyles?: StyleProp; + imageStyles?: StyleProp; /** Additional styles to pass to Icon */ iconAdditionalStyles?: StyleProp; @@ -81,7 +81,7 @@ function Avatar({ const isWorkspace = type === CONST.ICON_TYPE_WORKSPACE; const iconSize = StyleUtils.getAvatarSize(size); - const imageStyle = [StyleUtils.getAvatarStyle(size), imageStyles, styles.noBorderRadius]; + const imageStyle: StyleProp = [StyleUtils.getAvatarStyle(size), imageStyles, styles.noBorderRadius]; const iconStyle = imageStyles ? [StyleUtils.getAvatarStyle(size), styles.bgTransparent, imageStyles] : undefined; const iconFillColor = isWorkspace ? StyleUtils.getDefaultWorkspaceAvatarColor(name).fill : fill; @@ -92,7 +92,15 @@ function Avatar({ return ( - {typeof avatarSource === 'function' || typeof avatarSource === 'number' ? ( + {typeof avatarSource === 'string' ? ( + + setImageError(true)} + /> + + ) : ( - ) : ( - - setImageError(true)} - /> - )} ); diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx index 4388ebb8f815..5755c69641c8 100644 --- a/src/components/AvatarWithImagePicker.tsx +++ b/src/components/AvatarWithImagePicker.tsx @@ -1,6 +1,6 @@ import React, {useEffect, useRef, useState} from 'react'; import {StyleSheet, View} from 'react-native'; -import type {StyleProp, ViewStyle} from 'react-native'; +import type {ImageStyle, StyleProp, ViewStyle} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -103,7 +103,7 @@ type AvatarWithImagePickerProps = { isFocused: boolean; /** Style applied to the avatar */ - avatarStyle: StyleProp; + avatarStyle: StyleProp; /** Indicates if picker feature should be disabled */ disabled?: boolean; @@ -279,8 +279,6 @@ function AvatarWithImagePicker({ vertical: y + height + variables.spacing2, }); }); - - // eslint-disable-next-line react-hooks/exhaustive-deps }, [isMenuVisible, windowWidth]); return ( diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 1777b239e714..a25c7ff7129c 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -315,7 +315,7 @@ function Button( large ? styles.buttonLarge : undefined, success ? styles.buttonSuccess : undefined, danger ? styles.buttonDanger : undefined, - isDisabled && (success || danger) ? styles.buttonOpacityDisabled : undefined, + isDisabled ? styles.buttonOpacityDisabled : undefined, isDisabled && !danger && !success ? styles.buttonDisabled : undefined, shouldRemoveRightBorderRadius ? styles.noRightBorderRadius : undefined, shouldRemoveLeftBorderRadius ? styles.noLeftBorderRadius : undefined, diff --git a/src/components/ButtonWithDropdownMenu.tsx b/src/components/ButtonWithDropdownMenu/index.tsx similarity index 66% rename from src/components/ButtonWithDropdownMenu.tsx rename to src/components/ButtonWithDropdownMenu/index.tsx index 9466da601825..61d3409c65ab 100644 --- a/src/components/ButtonWithDropdownMenu.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -1,77 +1,26 @@ -import type {RefObject} from 'react'; import React, {useEffect, useRef, useState} from 'react'; -import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; -import type {ValueOf} from 'type-fest'; +import Button from '@components/Button'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import PopoverMenu from '@components/PopoverMenu'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import type {AnchorPosition} from '@styles/index'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; -import type AnchorAlignment from '@src/types/utils/AnchorAlignment'; -import type DeepValueOf from '@src/types/utils/DeepValueOf'; -import type IconAsset from '@src/types/utils/IconAsset'; -import Button from './Button'; -import Icon from './Icon'; -import * as Expensicons from './Icon/Expensicons'; -import PopoverMenu from './PopoverMenu'; +import type {AnchorPosition} from '@src/styles'; +import type {ButtonWithDropdownMenuProps} from './types'; -type PaymentType = DeepValueOf; - -type DropdownOption = { - value: PaymentType; - text: string; - icon: IconAsset; - iconWidth?: number; - iconHeight?: number; - iconDescription?: string; -}; - -type ButtonWithDropdownMenuProps = { - /** Text to display for the menu header */ - menuHeaderText?: string; - - /** Callback to execute when the main button is pressed */ - onPress: (event: GestureResponderEvent | KeyboardEvent | undefined, value: PaymentType) => void; - - /** Callback to execute when a dropdown option is selected */ - onOptionSelected?: (option: DropdownOption) => void; - - /** Call the onPress function on main button when Enter key is pressed */ - pressOnEnter?: boolean; - - /** Whether we should show a loading state for the main button */ - isLoading?: boolean; - - /** The size of button size */ - buttonSize: ValueOf; - - /** Should the confirmation button be disabled? */ - isDisabled?: boolean; - - /** Additional styles to add to the component */ - style?: StyleProp; - - /** Menu options to display */ - /** e.g. [{text: 'Pay with Expensify', icon: Wallet}] */ - options: DropdownOption[]; - - /** The anchor alignment of the popover menu */ - anchorAlignment?: AnchorAlignment; - - /* ref for the button */ - buttonRef: RefObject; - - /** The priority to assign the enter key event listener to buttons. 0 is the highest priority. */ - enterKeyEventListenerPriority?: number; -}; - -function ButtonWithDropdownMenu({ +function ButtonWithDropdownMenu({ + success = false, isLoading = false, isDisabled = false, pressOnEnter = false, + shouldAlwaysShowDropdownMenu = false, menuHeaderText = '', + customText, style, buttonSize = CONST.DROPDOWN_BUTTON_SIZE.MEDIUM, anchorAlignment = { @@ -83,7 +32,7 @@ function ButtonWithDropdownMenu({ options, onOptionSelected, enterKeyEventListenerPriority = 0, -}: ButtonWithDropdownMenuProps) { +}: ButtonWithDropdownMenuProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -118,27 +67,27 @@ function ButtonWithDropdownMenu({ return ( - {options.length > 1 ? ( + {shouldAlwaysShowDropdownMenu || options.length > 1 ? (