diff --git a/.eslintrc.changed.js b/.eslintrc.changed.js index c279c3e67a51..a1c2c452273e 100644 --- a/.eslintrc.changed.js +++ b/.eslintrc.changed.js @@ -6,5 +6,24 @@ module.exports = { }, rules: { 'deprecation/deprecation': 'error', + 'rulesdir/no-default-id-values': 'error', }, + overrides: [ + { + files: [ + 'src/libs/ReportUtils.ts', + 'src/libs/actions/IOU.ts', + 'src/libs/actions/Report.ts', + 'src/libs/actions/Task.ts', + 'src/libs/OptionsListUtils.ts', + 'src/libs/ReportActionsUtils.ts', + 'src/libs/TransactionUtils/index.ts', + 'src/pages/home/ReportScreen.tsx', + 'src/pages/workspace/WorkspaceInitialPage.tsx', + ], + rules: { + 'rulesdir/no-default-id-values': 'off', + }, + }, + ], }; diff --git a/.github/actions/javascript/authorChecklist/index.js b/.github/actions/javascript/authorChecklist/index.js index 168b025b95df..52fa68c65096 100644 --- a/.github/actions/javascript/authorChecklist/index.js +++ b/.github/actions/javascript/authorChecklist/index.js @@ -3847,6 +3847,42 @@ exports.checkBypass = checkBypass; intToChar[i] = c; charToInt[c] = i; } + function decodeInteger(reader, relative) { + let value = 0; + let shift = 0; + let integer = 0; + do { + const c = reader.next(); + integer = charToInt[c]; + value |= (integer & 31) << shift; + shift += 5; + } while (integer & 32); + const shouldNegate = value & 1; + value >>>= 1; + if (shouldNegate) { + value = -0x80000000 | -value; + } + return relative + value; + } + function encodeInteger(builder, num, relative) { + let delta = num - relative; + delta = delta < 0 ? (-delta << 1) | 1 : delta << 1; + do { + let clamped = delta & 0b011111; + delta >>>= 5; + if (delta > 0) + clamped |= 0b100000; + builder.write(intToChar[clamped]); + } while (delta > 0); + return num; + } + function hasMoreVlq(reader, max) { + if (reader.pos >= max) + return false; + return reader.peek() !== comma; + } + + const bufLength = 1024 * 16; // Provide a fallback for older environments. const td = typeof TextDecoder !== 'undefined' ? /* #__PURE__ */ new TextDecoder() @@ -3866,74 +3902,326 @@ exports.checkBypass = checkBypass; return out; }, }; + class StringWriter { + constructor() { + this.pos = 0; + this.out = ''; + this.buffer = new Uint8Array(bufLength); + } + write(v) { + const { buffer } = this; + buffer[this.pos++] = v; + if (this.pos === bufLength) { + this.out += td.decode(buffer); + this.pos = 0; + } + } + flush() { + const { buffer, out, pos } = this; + return pos > 0 ? out + td.decode(buffer.subarray(0, pos)) : out; + } + } + class StringReader { + constructor(buffer) { + this.pos = 0; + this.buffer = buffer; + } + next() { + return this.buffer.charCodeAt(this.pos++); + } + peek() { + return this.buffer.charCodeAt(this.pos); + } + indexOf(char) { + const { buffer, pos } = this; + const idx = buffer.indexOf(char, pos); + return idx === -1 ? buffer.length : idx; + } + } + + const EMPTY = []; + function decodeOriginalScopes(input) { + const { length } = input; + const reader = new StringReader(input); + const scopes = []; + const stack = []; + let line = 0; + for (; reader.pos < length; reader.pos++) { + line = decodeInteger(reader, line); + const column = decodeInteger(reader, 0); + if (!hasMoreVlq(reader, length)) { + const last = stack.pop(); + last[2] = line; + last[3] = column; + continue; + } + const kind = decodeInteger(reader, 0); + const fields = decodeInteger(reader, 0); + const hasName = fields & 0b0001; + const scope = (hasName ? [line, column, 0, 0, kind, decodeInteger(reader, 0)] : [line, column, 0, 0, kind]); + let vars = EMPTY; + if (hasMoreVlq(reader, length)) { + vars = []; + do { + const varsIndex = decodeInteger(reader, 0); + vars.push(varsIndex); + } while (hasMoreVlq(reader, length)); + } + scope.vars = vars; + scopes.push(scope); + stack.push(scope); + } + return scopes; + } + function encodeOriginalScopes(scopes) { + const writer = new StringWriter(); + for (let i = 0; i < scopes.length;) { + i = _encodeOriginalScopes(scopes, i, writer, [0]); + } + return writer.flush(); + } + function _encodeOriginalScopes(scopes, index, writer, state) { + const scope = scopes[index]; + const { 0: startLine, 1: startColumn, 2: endLine, 3: endColumn, 4: kind, vars } = scope; + if (index > 0) + writer.write(comma); + state[0] = encodeInteger(writer, startLine, state[0]); + encodeInteger(writer, startColumn, 0); + encodeInteger(writer, kind, 0); + const fields = scope.length === 6 ? 0b0001 : 0; + encodeInteger(writer, fields, 0); + if (scope.length === 6) + encodeInteger(writer, scope[5], 0); + for (const v of vars) { + encodeInteger(writer, v, 0); + } + for (index++; index < scopes.length;) { + const next = scopes[index]; + const { 0: l, 1: c } = next; + if (l > endLine || (l === endLine && c >= endColumn)) { + break; + } + index = _encodeOriginalScopes(scopes, index, writer, state); + } + writer.write(comma); + state[0] = encodeInteger(writer, endLine, state[0]); + encodeInteger(writer, endColumn, 0); + return index; + } + function decodeGeneratedRanges(input) { + const { length } = input; + const reader = new StringReader(input); + const ranges = []; + const stack = []; + let genLine = 0; + let definitionSourcesIndex = 0; + let definitionScopeIndex = 0; + let callsiteSourcesIndex = 0; + let callsiteLine = 0; + let callsiteColumn = 0; + let bindingLine = 0; + let bindingColumn = 0; + do { + const semi = reader.indexOf(';'); + let genColumn = 0; + for (; reader.pos < semi; reader.pos++) { + genColumn = decodeInteger(reader, genColumn); + if (!hasMoreVlq(reader, semi)) { + const last = stack.pop(); + last[2] = genLine; + last[3] = genColumn; + continue; + } + const fields = decodeInteger(reader, 0); + const hasDefinition = fields & 0b0001; + const hasCallsite = fields & 0b0010; + const hasScope = fields & 0b0100; + let callsite = null; + let bindings = EMPTY; + let range; + if (hasDefinition) { + const defSourcesIndex = decodeInteger(reader, definitionSourcesIndex); + definitionScopeIndex = decodeInteger(reader, definitionSourcesIndex === defSourcesIndex ? definitionScopeIndex : 0); + definitionSourcesIndex = defSourcesIndex; + range = [genLine, genColumn, 0, 0, defSourcesIndex, definitionScopeIndex]; + } + else { + range = [genLine, genColumn, 0, 0]; + } + range.isScope = !!hasScope; + if (hasCallsite) { + const prevCsi = callsiteSourcesIndex; + const prevLine = callsiteLine; + callsiteSourcesIndex = decodeInteger(reader, callsiteSourcesIndex); + const sameSource = prevCsi === callsiteSourcesIndex; + callsiteLine = decodeInteger(reader, sameSource ? callsiteLine : 0); + callsiteColumn = decodeInteger(reader, sameSource && prevLine === callsiteLine ? callsiteColumn : 0); + callsite = [callsiteSourcesIndex, callsiteLine, callsiteColumn]; + } + range.callsite = callsite; + if (hasMoreVlq(reader, semi)) { + bindings = []; + do { + bindingLine = genLine; + bindingColumn = genColumn; + const expressionsCount = decodeInteger(reader, 0); + let expressionRanges; + if (expressionsCount < -1) { + expressionRanges = [[decodeInteger(reader, 0)]]; + for (let i = -1; i > expressionsCount; i--) { + const prevBl = bindingLine; + bindingLine = decodeInteger(reader, bindingLine); + bindingColumn = decodeInteger(reader, bindingLine === prevBl ? bindingColumn : 0); + const expression = decodeInteger(reader, 0); + expressionRanges.push([expression, bindingLine, bindingColumn]); + } + } + else { + expressionRanges = [[expressionsCount]]; + } + bindings.push(expressionRanges); + } while (hasMoreVlq(reader, semi)); + } + range.bindings = bindings; + ranges.push(range); + stack.push(range); + } + genLine++; + reader.pos = semi + 1; + } while (reader.pos < length); + return ranges; + } + function encodeGeneratedRanges(ranges) { + if (ranges.length === 0) + return ''; + const writer = new StringWriter(); + for (let i = 0; i < ranges.length;) { + i = _encodeGeneratedRanges(ranges, i, writer, [0, 0, 0, 0, 0, 0, 0]); + } + return writer.flush(); + } + function _encodeGeneratedRanges(ranges, index, writer, state) { + const range = ranges[index]; + const { 0: startLine, 1: startColumn, 2: endLine, 3: endColumn, isScope, callsite, bindings, } = range; + if (state[0] < startLine) { + catchupLine(writer, state[0], startLine); + state[0] = startLine; + state[1] = 0; + } + else if (index > 0) { + writer.write(comma); + } + state[1] = encodeInteger(writer, range[1], state[1]); + const fields = (range.length === 6 ? 0b0001 : 0) | (callsite ? 0b0010 : 0) | (isScope ? 0b0100 : 0); + encodeInteger(writer, fields, 0); + if (range.length === 6) { + const { 4: sourcesIndex, 5: scopesIndex } = range; + if (sourcesIndex !== state[2]) { + state[3] = 0; + } + state[2] = encodeInteger(writer, sourcesIndex, state[2]); + state[3] = encodeInteger(writer, scopesIndex, state[3]); + } + if (callsite) { + const { 0: sourcesIndex, 1: callLine, 2: callColumn } = range.callsite; + if (sourcesIndex !== state[4]) { + state[5] = 0; + state[6] = 0; + } + else if (callLine !== state[5]) { + state[6] = 0; + } + state[4] = encodeInteger(writer, sourcesIndex, state[4]); + state[5] = encodeInteger(writer, callLine, state[5]); + state[6] = encodeInteger(writer, callColumn, state[6]); + } + if (bindings) { + for (const binding of bindings) { + if (binding.length > 1) + encodeInteger(writer, -binding.length, 0); + const expression = binding[0][0]; + encodeInteger(writer, expression, 0); + let bindingStartLine = startLine; + let bindingStartColumn = startColumn; + for (let i = 1; i < binding.length; i++) { + const expRange = binding[i]; + bindingStartLine = encodeInteger(writer, expRange[1], bindingStartLine); + bindingStartColumn = encodeInteger(writer, expRange[2], bindingStartColumn); + encodeInteger(writer, expRange[0], 0); + } + } + } + for (index++; index < ranges.length;) { + const next = ranges[index]; + const { 0: l, 1: c } = next; + if (l > endLine || (l === endLine && c >= endColumn)) { + break; + } + index = _encodeGeneratedRanges(ranges, index, writer, state); + } + if (state[0] < endLine) { + catchupLine(writer, state[0], endLine); + state[0] = endLine; + state[1] = 0; + } + else { + writer.write(comma); + } + state[1] = encodeInteger(writer, endColumn, state[1]); + return index; + } + function catchupLine(writer, lastLine, line) { + do { + writer.write(semicolon); + } while (++lastLine < line); + } + function decode(mappings) { - const state = new Int32Array(5); + const { length } = mappings; + const reader = new StringReader(mappings); const decoded = []; - let index = 0; + let genColumn = 0; + let sourcesIndex = 0; + let sourceLine = 0; + let sourceColumn = 0; + let namesIndex = 0; do { - const semi = indexOf(mappings, index); + const semi = reader.indexOf(';'); const line = []; let sorted = true; let lastCol = 0; - state[0] = 0; - for (let i = index; i < semi; i++) { + genColumn = 0; + while (reader.pos < semi) { let seg; - i = decodeInteger(mappings, i, state, 0); // genColumn - const col = state[0]; - if (col < lastCol) + genColumn = decodeInteger(reader, genColumn); + if (genColumn < lastCol) sorted = false; - lastCol = col; - if (hasMoreVlq(mappings, i, semi)) { - i = decodeInteger(mappings, i, state, 1); // sourcesIndex - i = decodeInteger(mappings, i, state, 2); // sourceLine - i = decodeInteger(mappings, i, state, 3); // sourceColumn - if (hasMoreVlq(mappings, i, semi)) { - i = decodeInteger(mappings, i, state, 4); // namesIndex - seg = [col, state[1], state[2], state[3], state[4]]; + lastCol = genColumn; + if (hasMoreVlq(reader, semi)) { + sourcesIndex = decodeInteger(reader, sourcesIndex); + sourceLine = decodeInteger(reader, sourceLine); + sourceColumn = decodeInteger(reader, sourceColumn); + if (hasMoreVlq(reader, semi)) { + namesIndex = decodeInteger(reader, namesIndex); + seg = [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex]; } else { - seg = [col, state[1], state[2], state[3]]; + seg = [genColumn, sourcesIndex, sourceLine, sourceColumn]; } } else { - seg = [col]; + seg = [genColumn]; } line.push(seg); + reader.pos++; } if (!sorted) sort(line); decoded.push(line); - index = semi + 1; - } while (index <= mappings.length); + reader.pos = semi + 1; + } while (reader.pos <= length); return decoded; } - function indexOf(mappings, index) { - const idx = mappings.indexOf(';', index); - return idx === -1 ? mappings.length : idx; - } - function decodeInteger(mappings, pos, state, j) { - let value = 0; - let shift = 0; - let integer = 0; - do { - const c = mappings.charCodeAt(pos++); - integer = charToInt[c]; - value |= (integer & 31) << shift; - shift += 5; - } while (integer & 32); - const shouldNegate = value & 1; - value >>>= 1; - if (shouldNegate) { - value = -0x80000000 | -value; - } - state[j] += value; - return pos; - } - function hasMoreVlq(mappings, i, length) { - if (i >= length) - return false; - return mappings.charCodeAt(i) !== comma; - } function sort(line) { line.sort(sortComparator); } @@ -3941,66 +4229,42 @@ exports.checkBypass = checkBypass; return a[0] - b[0]; } function encode(decoded) { - const state = new Int32Array(5); - const bufLength = 1024 * 16; - const subLength = bufLength - 36; - const buf = new Uint8Array(bufLength); - const sub = buf.subarray(0, subLength); - let pos = 0; - let out = ''; + const writer = new StringWriter(); + let sourcesIndex = 0; + let sourceLine = 0; + let sourceColumn = 0; + let namesIndex = 0; for (let i = 0; i < decoded.length; i++) { const line = decoded[i]; - if (i > 0) { - if (pos === bufLength) { - out += td.decode(buf); - pos = 0; - } - buf[pos++] = semicolon; - } + if (i > 0) + writer.write(semicolon); if (line.length === 0) continue; - state[0] = 0; + let genColumn = 0; for (let j = 0; j < line.length; j++) { const segment = line[j]; - // We can push up to 5 ints, each int can take at most 7 chars, and we - // may push a comma. - if (pos > subLength) { - out += td.decode(sub); - buf.copyWithin(0, subLength, pos); - pos -= subLength; - } if (j > 0) - buf[pos++] = comma; - pos = encodeInteger(buf, pos, state, segment, 0); // genColumn + writer.write(comma); + genColumn = encodeInteger(writer, segment[0], genColumn); if (segment.length === 1) continue; - pos = encodeInteger(buf, pos, state, segment, 1); // sourcesIndex - pos = encodeInteger(buf, pos, state, segment, 2); // sourceLine - pos = encodeInteger(buf, pos, state, segment, 3); // sourceColumn + sourcesIndex = encodeInteger(writer, segment[1], sourcesIndex); + sourceLine = encodeInteger(writer, segment[2], sourceLine); + sourceColumn = encodeInteger(writer, segment[3], sourceColumn); if (segment.length === 4) continue; - pos = encodeInteger(buf, pos, state, segment, 4); // namesIndex + namesIndex = encodeInteger(writer, segment[4], namesIndex); } } - return out + td.decode(buf.subarray(0, pos)); - } - function encodeInteger(buf, pos, state, segment, j) { - const next = segment[j]; - let num = next - state[j]; - state[j] = next; - num = num < 0 ? (-num << 1) | 1 : num << 1; - do { - let clamped = num & 0b011111; - num >>>= 5; - if (num > 0) - clamped |= 0b100000; - buf[pos++] = intToChar[clamped]; - } while (num > 0); - return pos; + return writer.flush(); } exports.decode = decode; + exports.decodeGeneratedRanges = decodeGeneratedRanges; + exports.decodeOriginalScopes = decodeOriginalScopes; exports.encode = encode; + exports.encodeGeneratedRanges = encodeGeneratedRanges; + exports.encodeOriginalScopes = encodeOriginalScopes; Object.defineProperty(exports, '__esModule', { value: true }); diff --git a/.github/workflows/createNewVersion.yml b/.github/workflows/createNewVersion.yml index 93fe07be9298..29dddbcd3151 100644 --- a/.github/workflows/createNewVersion.yml +++ b/.github/workflows/createNewVersion.yml @@ -106,9 +106,6 @@ jobs: runs-on: macos-latest needs: [validateActor, createNewVersion] if: ${{ fromJSON(needs.validateActor.outputs.HAS_WRITE_ACCESS) }} - defaults: - run: - working-directory: Mobile-Expensify steps: - name: Run turnstyle uses: softprops/turnstyle@49108bdfa571e62371bd2c3094893c547ab3fc03 @@ -121,22 +118,17 @@ jobs: uses: actions/checkout@v4 with: ref: main + submodules: true # The OS_BOTIFY_COMMIT_TOKEN is a personal access token tied to osbotify # This is a workaround to allow pushes to a protected branch token: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }} - - name: Check out `Mobile-Expensify` repo - uses: actions/checkout@v4 - with: - repository: 'Expensify/Mobile-Expensify' - submodules: true - path: 'Mobile-Expensify' - token: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }} - - - name: Update submodule + - name: Update submodule and checkout the main branch run: | - cd react-native git submodule update --init + cd Mobile-Expensify + git checkout main + git pull origin main - name: Setup git for OSBotify uses: ./.github/actions/composite/setupGitForOSBotify @@ -146,6 +138,7 @@ jobs: - name: Generate HybridApp version run: | + cd Mobile-Expensify # Generate all flavors of the version SHORT_APP_VERSION=$(echo "$NEW_VERSION" | awk -F'-' '{print $1}') BUILD_NUMBER=$(echo "$NEW_VERSION" | awk -F'-' '{print $2}') @@ -156,6 +149,7 @@ jobs: ANDROID_MANIFEST_FILE="Android/AndroidManifest.xml" IOS_INFO_PLIST_FILE="iOS/Expensify/Expensify-Info.plist" IOS_SHARE_EXTENSION_PLIST_FILE="iOS/SmartScanExtension/Info.plist" + IOS_NOTIFICATION_EXTENSION_PLIST_FILE="iOS/NotificationServiceExtension/Info.plist" JS_CONFIG_FILE="app/config/config.json" # Update Android HybridApp Version @@ -167,6 +161,8 @@ jobs: /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $FULL_APP_VERSION" $IOS_INFO_PLIST_FILE /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $SHORT_APP_VERSION" $IOS_SHARE_EXTENSION_PLIST_FILE /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $FULL_APP_VERSION" $IOS_SHARE_EXTENSION_PLIST_FILE + /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $SHORT_APP_VERSION" $IOS_NOTIFICATION_EXTENSION_PLIST_FILE + /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $FULL_APP_VERSION" $IOS_NOTIFICATION_EXTENSION_PLIST_FILE # Update JS HybridApp Version sed -i .bak -E "s/\"version\": \"([0-9\.]*)\"/\"version\": \"$FULL_APP_VERSION\"/" $JS_CONFIG_FILE @@ -175,15 +171,23 @@ jobs: - name: Commit new version run: | + cd Mobile-Expensify git add \ ./Android/AndroidManifest.xml \ ./app/config/config.json \ ./iOS/Expensify/Expensify-Info.plist\ - ./iOS/SmartScanExtension/Info.plist + ./iOS/SmartScanExtension/Info.plist \ + ./iOS/NotificationServiceExtension/Info.plist git commit -m "Update version to ${{ needs.createNewVersion.outputs.NEW_VERSION }}" - - name: Update main branch - run: git push origin main + - name: Update main branch on Mobile-Expensify and App + run: | + cd Mobile-Expensify + git push origin main + cd .. + git add Mobile-Expensify + git commit -m "Update Mobile-Expensify to ${{ needs.createNewVersion.outputs.NEW_VERSION }}" + git push origin main - name: Announce failed workflow in Slack if: ${{ failure() }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0f59295a3463..354b78d437a3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -114,95 +114,34 @@ jobs: env: BROWSERSTACK: ${{ secrets.BROWSERSTACK }} - submitAndroid: - name: Submit Android app for production review - needs: prep - if: ${{ github.ref == 'refs/heads/production' }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Ruby - uses: ruby/setup-ruby@v1.190.0 - with: - bundler-cache: true - - - name: Get Android native version - id: getAndroidVersion - run: echo "VERSION_CODE=$(grep -o 'versionCode\s\+[0-9]\+' android/app/build.gradle | awk '{ print $2 }')" >> "$GITHUB_OUTPUT" - - - name: Decrypt json w/ Google Play credentials - run: gpg --batch --yes --decrypt --passphrase="${{ secrets.LARGE_SECRET_PASSPHRASE }}" --output android-fastlane-json-key.json android-fastlane-json-key.json.gpg - working-directory: android/app - - - name: Submit Android build for review - run: bundle exec fastlane android upload_google_play_production - env: - VERSION: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }} - - - name: Warn deployers if Android production deploy failed - if: ${{ failure() }} - uses: 8398a7/action-slack@v3 - with: - status: custom - custom_payload: | - { - channel: '#deployer', - attachments: [{ - color: "#DB4545", - pretext: ``, - text: `💥 Android production deploy failed. Please manually submit ${{ needs.prep.outputs.APP_VERSION }} in the . 💥`, - }] - } - env: - GITHUB_TOKEN: ${{ github.token }} - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} - android_hybrid: name: Build and deploy Android HybridApp needs: prep runs-on: ubuntu-latest-xl - defaults: - run: - working-directory: Mobile-Expensify/react-native steps: - - name: Checkout App repo - uses: actions/checkout@v4 - - - name: Checkout Mobile-Expensify repo + - name: Checkout App and Mobile-Expensify repo uses: actions/checkout@v4 with: - repository: 'Expensify/Mobile-Expensify' submodules: true - path: 'Mobile-Expensify' token: ${{ secrets.OS_BOTIFY_TOKEN }} # fetch-depth: 0 is required in order to fetch the correct submodule branch fetch-depth: 0 - - name: Update submodule + - name: Update submodule to match main run: | - git submodule update --init - # Update submodule to latest on staging - git fetch - git checkout staging + git submodule update --init --remote - name: Configure MapBox SDK run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} - - uses: actions/setup-node@v4 - with: - node-version-file: 'Mobile-Expensify/react-native/.nvmrc' - cache: npm - cache-dependency-path: 'Mobile-Expensify/react-native' + - name: Setup Node + id: setup-node + uses: ./.github/actions/composite/setupNode - - name: Install node modules + - name: Run grunt build run: | - npm install - cd .. && npm install - - # Fixes https://github.com/Expensify/App/issues/51682 - npm run grunt:build:shared + cd Mobile-Expensify + npm run grunt:build:shared - name: Setup Java uses: actions/setup-java@v4 @@ -214,7 +153,6 @@ jobs: uses: ruby/setup-ruby@v1.190.0 with: bundler-cache: true - working-directory: 'Mobile-Expensify/react-native' - name: Install New Expensify Gems run: bundle install @@ -229,7 +167,7 @@ jobs: op document get --output ./upload-key.keystore upload-key.keystore op document get --output ./android-fastlane-json-key.json android-fastlane-json-key.json # Copy the keystore to the Android directory for Fullstory - cp ./upload-key.keystore ../Android + cp ./upload-key.keystore Mobile-Expensify/Android - name: Load Android upload keystore credentials from 1Password id: load-credentials @@ -244,7 +182,7 @@ jobs: - name: Get Android native version id: getAndroidVersion - run: echo "VERSION_CODE=$(grep -oP 'android:versionCode="\K[0-9]+' ../Android/AndroidManifest.xml)" >> "$GITHUB_OUTPUT" + run: echo "VERSION_CODE=$(grep -oP 'android:versionCode="\K[0-9]+' Mobile-Expensify/Android/AndroidManifest.xml)" >> "$GITHUB_OUTPUT" - name: Build Android app if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} @@ -261,10 +199,11 @@ jobs: VERSION: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }} - name: Get current Android rollout percentage + if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} id: getAndroidRolloutPercentage uses: ./.github/actions/javascript/getAndroidRolloutPercentage with: - GOOGLE_KEY_FILE: Mobile-Expensify/react-native/android-fastlane-json-key.json + GOOGLE_KEY_FILE: ./android-fastlane-json-key.json PACKAGE_NAME: org.me.mobiexpensifyg - name: Submit production build for Google Play review and a slow rollout @@ -446,12 +385,6 @@ jobs: APPLE_DEMO_EMAIL: ${{ secrets.APPLE_DEMO_EMAIL }} APPLE_DEMO_PASSWORD: ${{ secrets.APPLE_DEMO_PASSWORD }} - - name: Submit build for App Store review - if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - run: bundle exec fastlane ios submit_for_review - env: - VERSION: ${{ steps.getIOSVersion.outputs.IOS_VERSION }} - - name: Upload iOS build to Browser Stack if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@/Users/runner/work/App/App/New Expensify.ipa" @@ -496,47 +429,31 @@ jobs: runs-on: macos-13-xlarge env: DEVELOPER_DIR: /Applications/Xcode_15.2.0.app/Contents/Developer - defaults: - run: - working-directory: Mobile-Expensify/react-native steps: - name: Checkout uses: actions/checkout@v4 with: - repository: 'Expensify/Mobile-Expensify' submodules: true - path: 'Mobile-Expensify' token: ${{ secrets.OS_BOTIFY_TOKEN }} # fetch-depth: 0 is required in order to fetch the correct submodule branch fetch-depth: 0 - name: Update submodule run: | - git submodule update --init - # Update submodule to latest on staging - git fetch - git checkout staging + git submodule update --init --remote - name: Configure MapBox SDK run: | ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} - - uses: actions/setup-node@v4 + - name: Setup Node id: setup-node - with: - node-version-file: 'Mobile-Expensify/react-native/.nvmrc' - cache-dependency-path: 'Mobile-Expensify/react-native' - - - name: Install node modules - run: | - npm install - cd .. && npm install + uses: ./.github/actions/composite/setupNode - name: Setup Ruby uses: ruby/setup-ruby@v1.190.0 with: bundler-cache: true - working-directory: 'Mobile-Expensify/react-native' - name: Install New Expensify Gems run: bundle install @@ -545,12 +462,12 @@ jobs: uses: actions/cache@v4 id: pods-cache with: - path: ios/Pods - key: ${{ runner.os }}-pods-cache-${{ hashFiles('ios/Podfile.lock', 'firebase.json') }} + path: Mobile-Expensify/iOS/Pods + key: ${{ runner.os }}-pods-cache-${{ hashFiles('Mobile-Expensify/iOS/Podfile.lock', 'firebase.json') }} - name: Compare Podfile.lock and Manifest.lock id: compare-podfile-and-manifest - run: echo "IS_PODFILE_SAME_AS_MANIFEST=${{ hashFiles('ios/Podfile.lock') == hashFiles('ios/Pods/Manifest.lock') }}" >> "$GITHUB_OUTPUT" + run: echo "IS_PODFILE_SAME_AS_MANIFEST=${{ hashFiles('Mobile-Expensify/iOS/Podfile.lock') == hashFiles('Mobile-Expensify/iOS/Pods/Manifest.lock') }}" >> "$GITHUB_OUTPUT" - name: Install cocoapods uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847 @@ -558,7 +475,7 @@ jobs: with: timeout_minutes: 10 max_attempts: 5 - command: cd Mobile-Expensify/iOS && pod install + command: npm run pod-install - name: Install 1Password CLI uses: 1password/install-cli-action@v1 @@ -746,7 +663,7 @@ jobs: name: Post a Slack message when any platform fails to build or deploy runs-on: ubuntu-latest if: ${{ failure() }} - needs: [buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web] + needs: [buildAndroid, uploadAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web] steps: - name: Checkout uses: actions/checkout@v4 @@ -761,21 +678,15 @@ jobs: outputs: IS_AT_LEAST_ONE_PLATFORM_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAtLeastOnePlatform.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED }} IS_ALL_PLATFORMS_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAllPlatforms.outputs.IS_ALL_PLATFORMS_DEPLOYED }} - needs: [buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web] + needs: [buildAndroid, uploadAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web] if: ${{ always() }} steps: - name: Check deployment success on at least one platform id: checkDeploymentSuccessOnAtLeastOnePlatform run: | isAtLeastOnePlatformDeployed="false" - if [ ${{ github.ref }} == 'refs/heads/production' ]; then - if [ "${{ needs.submitAndroid.result }}" == "success" ]; then - isAtLeastOnePlatformDeployed="true" - fi - else - if [ "${{ needs.uploadAndroid.result }}" == "success" ]; then - isAtLeastOnePlatformDeployed="true" - fi + if [ "${{ needs.uploadAndroid.result }}" == "success" ]; then + isAtLeastOnePlatformDeployed="true" fi if [ "${{ needs.iOS.result }}" == "success" ] || \ @@ -800,14 +711,8 @@ jobs: isAllPlatformsDeployed="true" fi - if [ ${{ github.ref }} == 'refs/heads/production' ]; then - if [ "${{ needs.submitAndroid.result }}" != "success" ]; then - isAllPlatformsDeployed="false" - fi - else - if [ "${{ needs.uploadAndroid.result }}" != "success" ]; then - isAllPlatformsDeployed="false" - fi + if [ "${{ needs.uploadAndroid.result }}" != "success" ]; then + isAllPlatformsDeployed="false" fi echo "IS_ALL_PLATFORMS_DEPLOYED=$isAllPlatformsDeployed" >> "$GITHUB_OUTPUT" @@ -955,7 +860,7 @@ jobs: name: Post a Slack message when all platforms deploy successfully runs-on: ubuntu-latest if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_ALL_PLATFORMS_DEPLOYED) }} - needs: [prep, buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] + needs: [prep, buildAndroid, uploadAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] steps: - name: 'Announces the deploy in the #announce Slack room' uses: 8398a7/action-slack@v3 @@ -1009,11 +914,11 @@ jobs: postGithubComments: uses: ./.github/workflows/postDeployComments.yml if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }} - needs: [prep, buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] + needs: [prep, buildAndroid, uploadAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] with: version: ${{ needs.prep.outputs.APP_VERSION }} env: ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }} - android: ${{ github.ref == 'refs/heads/production' && needs.submitAndroid.result || needs.uploadAndroid.result }} + android: ${{ github.ref == 'refs/heads/production' || needs.uploadAndroid.result }} android_hybrid: ${{ needs.android_hybrid.result }} ios: ${{ needs.iOS.result }} ios_hybrid: ${{ needs.iOS_hybrid.result }} diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml index ed5803c35b42..fb7a34d6fa01 100644 --- a/.github/workflows/reassurePerformanceTests.yml +++ b/.github/workflows/reassurePerformanceTests.yml @@ -13,6 +13,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup NodeJS uses: ./.github/actions/composite/setupNode @@ -22,6 +24,22 @@ jobs: git config --global user.email "test@test.com" git config --global user.name "Test" + - name: Get common ancestor commit + run: | + git fetch origin main + common_ancestor=$(git merge-base "${{ github.sha }}" origin/main) + echo "COMMIT_HASH=$common_ancestor" >> "$GITHUB_ENV" + + - name: Clean up deleted files + run: | + DELETED_FILES=$(git diff --name-only --diff-filter=D "$COMMIT_HASH" "${{ github.sha }}") + for file in $DELETED_FILES; do + if [ -n "$file" ]; then + rm -f "$file" + echo "Deleted file: $file" + fi + done + - name: Run performance testing script shell: bash run: | diff --git a/.github/workflows/sendReassurePerfData.yml b/.github/workflows/sendReassurePerfData.yml index 6ae528557faf..884182bfc896 100644 --- a/.github/workflows/sendReassurePerfData.yml +++ b/.github/workflows/sendReassurePerfData.yml @@ -12,6 +12,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup NodeJS uses: ./.github/actions/composite/setupNode diff --git a/.github/workflows/testBuildHybrid.yml b/.github/workflows/testBuildHybrid.yml index c84fe41fddae..42a5f15f8910 100644 --- a/.github/workflows/testBuildHybrid.yml +++ b/.github/workflows/testBuildHybrid.yml @@ -6,12 +6,16 @@ on: PULL_REQUEST_NUMBER: description: Pull Request number for correct placement of apps required: true + OLD_DOT_COMMIT: + description: The branch, tag or SHA to checkout on Old Dot side + required: false pull_request_target: types: [opened, synchronize, labeled] branches: ['*ci-test/**'] env: PULL_REQUEST_NUMBER: ${{ github.event.number || github.event.inputs.PULL_REQUEST_NUMBER }} + OLD_DOT_COMMIT: ${{ github.event.inputs.OLD_DOT_COMMIT }} jobs: validateActor: @@ -81,37 +85,41 @@ jobs: androidHybrid: name: Build Android HybridApp needs: [validateActor, getBranchRef] + if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }} runs-on: ubuntu-latest-xl - defaults: - run: - working-directory: Mobile-Expensify/react-native outputs: S3_APK_PATH: ${{ steps.exportAndroidS3Path.outputs.S3_APK_PATH }} steps: - name: Checkout uses: actions/checkout@v4 with: - repository: 'Expensify/Mobile-Expensify' submodules: true - path: 'Mobile-Expensify' + ref: ${{ github.event.pull_request.head.sha || needs.getBranchRef.outputs.REF }} token: ${{ secrets.OS_BOTIFY_TOKEN }} # fetch-depth: 0 is required in order to fetch the correct submodule branch fetch-depth: 0 - - name: Update submodule + - name: Update submodule to match main + env: + OLD_DOT_COMMIT: ${{ env.OLD_DOT_COMMIT }} run: | - git submodule update --init - git fetch - git checkout ${{ github.event.pull_request.head.sha || needs.getBranchRef.outputs.REF }} + git submodule update --init --remote + if [[ -z "$OLD_DOT_COMMIT" ]]; then + git fetch + git checkout ${{ env.OLD_DOT_COMMIT }} + fi - name: Configure MapBox SDK run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} - - uses: actions/setup-node@v4 - with: - node-version-file: 'Mobile-Expensify/react-native/.nvmrc' - cache: npm - cache-dependency-path: 'Mobile-Expensify/react-native' + - name: Setup Node + id: setup-node + uses: ./.github/actions/composite/setupNode + + - name: Run grunt build + run: | + cd Mobile-Expensify + npm run grunt:build:shared - name: Setup dotenv run: | @@ -119,14 +127,6 @@ jobs: sed -i 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc echo "PULL_REQUEST_NUMBER=${{ inputs.pull_request_number }}" >> .env.adhoc - - name: Install node modules - run: | - npm install - cd .. && npm install - - # Fixes https://github.com/Expensify/App/issues/51682 - npm run grunt:build:shared - - name: Setup Java uses: actions/setup-java@v4 with: @@ -137,7 +137,6 @@ jobs: uses: ruby/setup-ruby@v1.190.0 with: bundler-cache: true - working-directory: 'Mobile-Expensify/react-native' - name: Install 1Password CLI uses: 1password/install-cli-action@v1 @@ -149,7 +148,7 @@ jobs: op document get --output ./upload-key.keystore upload-key.keystore op document get --output ./android-fastlane-json-key.json android-fastlane-json-key.json # Copy the keystore to the Android directory for Fullstory - cp ./upload-key.keystore ../Android + cp ./upload-key.keystore Mobile-Expensify/Android - name: Load Android upload keystore credentials from 1Password id: load-credentials @@ -162,10 +161,6 @@ jobs: ANDROID_UPLOAD_KEYSTORE_ALIAS: op://Mobile-Deploy-CI/Repository-Secrets/ANDROID_UPLOAD_KEYSTORE_ALIAS ANDROID_UPLOAD_KEY_PASSWORD: op://Mobile-Deploy-CI/Repository-Secrets/ANDROID_UPLOAD_KEY_PASSWORD - - name: Get Android native version - id: getAndroidVersion - run: echo "VERSION_CODE=$(grep -o 'versionCode\s\+[0-9]\+' android/app/build.gradle | awk '{ print $2 }')" >> "$GITHUB_OUTPUT" - - name: Build Android app id: build env: @@ -194,11 +189,120 @@ jobs: run: | # $s3APKPath is set from within the Fastfile, android upload_s3 lane echo "S3_APK_PATH=$s3APKPath" >> "$GITHUB_OUTPUT" + + iosHybrid: + name: Build and deploy iOS for testing + needs: [validateActor, getBranchRef] + if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }} + env: + DEVELOPER_DIR: /Applications/Xcode_15.2.0.app/Contents/Developer + runs-on: macos-13-xlarge + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: true + ref: ${{ github.event.pull_request.head.sha || needs.getBranchRef.outputs.REF }} + token: ${{ secrets.OS_BOTIFY_TOKEN }} + # fetch-depth: 0 is required in order to fetch the correct submodule branch + fetch-depth: 0 + + - name: Update submodule to match main + env: + OLD_DOT_COMMIT: ${{ env.OLD_DOT_COMMIT }} + run: | + git submodule update --init --remote + if [[ -z "$OLD_DOT_COMMIT" ]]; then + git fetch + git checkout ${{ env.OLD_DOT_COMMIT }} + fi + + - name: Configure MapBox SDK + run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} + + - name: Setup Node + id: setup-node + uses: ./.github/actions/composite/setupNode + + - name: Create .env.adhoc file based on staging and add PULL_REQUEST_NUMBER env to it + run: | + cp .env.staging .env.adhoc + sed -i '' 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc + echo "PULL_REQUEST_NUMBER=$PULL_REQUEST_NUMBER" >> .env.adhoc + + - name: Setup Ruby + uses: ruby/setup-ruby@v1.190.0 + with: + bundler-cache: true + + - name: Install New Expensify Gems + run: bundle install + + - name: Cache Pod dependencies + uses: actions/cache@v4 + id: pods-cache + with: + path: Mobile-Expensify/iOS/Pods + key: ${{ runner.os }}-pods-cache-${{ hashFiles('Mobile-Expensify/iOS/Podfile.lock', 'firebase.json') }} + + - name: Compare Podfile.lock and Manifest.lock + id: compare-podfile-and-manifest + run: echo "IS_PODFILE_SAME_AS_MANIFEST=${{ hashFiles('Mobile-Expensify/iOS/Podfile.lock') == hashFiles('Mobile-Expensify/iOS/Manifest.lock') }}" >> "$GITHUB_OUTPUT" + + - name: Install cocoapods + uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847 + if: steps.pods-cache.outputs.cache-hit != 'true' || steps.compare-podfile-and-manifest.outputs.IS_PODFILE_SAME_AS_MANIFEST != 'true' || steps.setup-node.outputs.cache-hit != 'true' + with: + timeout_minutes: 10 + max_attempts: 5 + command: npm run pod-install + + - name: Install 1Password CLI + uses: 1password/install-cli-action@v1 + + - name: Load files from 1Password + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + run: | + op document get --output ./OldApp_AdHoc.mobileprovision OldApp_AdHoc + op document get --output ./OldApp_AdHoc_Share_Extension.mobileprovision OldApp_AdHoc_Share_Extension + op document get --output ./OldApp_AdHoc_Notification_Service.mobileprovision OldApp_AdHoc_Notification_Service + + - name: Decrypt certificate + run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output Certificates.p12 Certificates.p12.gpg + env: + LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + + - name: Build AdHoc app + run: bundle exec fastlane ios build_adhoc_hybrid + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Upload AdHoc build to S3 + run: bundle exec fastlane ios upload_s3 + env: + S3_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY_ID }} + S3_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + S3_BUCKET: ad-hoc-expensify-cash + S3_REGION: us-east-1 + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: ios + path: ./ios_paths.json + + postGithubComment: runs-on: ubuntu-latest name: Post a GitHub comment with app download links for testing - needs: [validateActor, getBranchRef, androidHybrid] + needs: [validateActor, getBranchRef, androidHybrid, iosHybrid] if: ${{ always() }} steps: - name: Checkout @@ -211,6 +315,17 @@ jobs: uses: actions/download-artifact@v4 if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }} + - name: Read JSONs with iOS paths + id: get_ios_path + if: ${{ needs.iosHybrid.result == 'success' }} + run: | + content_ios="$(cat ./ios/ios_paths.json)" + content_ios="${content_ios//'%'/'%25'}" + content_ios="${content_ios//$'\n'/'%0A'}" + content_ios="${content_ios//$'\r'/'%0D'}" + ios_path=$(echo "$content_ios" | jq -r '.html_path') + echo "ios_path=$ios_path" >> "$GITHUB_OUTPUT" + - name: Publish links to apps for download if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }} uses: ./.github/actions/javascript/postTestBuildComment @@ -218,4 +333,6 @@ jobs: PR_NUMBER: ${{ env.PULL_REQUEST_NUMBER }} GITHUB_TOKEN: ${{ github.token }} ANDROID: ${{ needs.androidHybrid.result }} - ANDROID_LINK: ${{ needs.androidHybrid.outputs.S3_APK_PATH }} \ No newline at end of file + IOS: ${{ needs.iosHybrid.result }} + ANDROID_LINK: ${{ needs.androidHybrid.outputs.S3_APK_PATH }} + IOS_LINK: ${{ steps.get_ios_path.outputs.ios_path }} diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000000..59abf2448f1d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "Mobile-Expensify"] + path = Mobile-Expensify + url = git@github.com:Expensify/Mobile-Expensify.git diff --git a/.prettierignore b/.prettierignore index b428978a1563..8584ae14b917 100644 --- a/.prettierignore +++ b/.prettierignore @@ -22,3 +22,6 @@ src/libs/E2E/reactNativeLaunchingTest.ts # Automatically generated files src/libs/SearchParser/searchParser.js src/libs/SearchParser/autocompleteParser.js + +# Disable prettier in the submodule +Mobile-Expensify diff --git a/.storybook/main.ts b/.storybook/main.ts index 37f443219f4d..26fd25df58e2 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,4 +1,4 @@ -import type {StorybookConfig} from '@storybook/types'; +import type {StorybookConfig} from 'storybook/internal/types'; const main: StorybookConfig = { stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index d3829fe01779..1330093dffd1 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,8 +1,8 @@ import {PortalProvider} from '@gorhom/portal'; -import type {Parameters} from '@storybook/types'; import React from 'react'; import Onyx from 'react-native-onyx'; import {SafeAreaProvider} from 'react-native-safe-area-context'; +import type {Parameters} from 'storybook/internal/types'; import ComposeProviders from '@src/components/ComposeProviders'; import HTMLEngineProvider from '@src/components/HTMLEngineProvider'; import {LocaleContextProvider} from '@src/components/LocaleContextProvider'; diff --git a/Mobile-Expensify b/Mobile-Expensify new file mode 160000 index 000000000000..768d69540612 --- /dev/null +++ b/Mobile-Expensify @@ -0,0 +1 @@ +Subproject commit 768d695406126652ce222a46c95e643ba2e51e45 diff --git a/README.md b/README.md index 9f73a0012bef..455f2f61197d 100644 --- a/README.md +++ b/README.md @@ -437,6 +437,102 @@ export default withOnyx({ 1. The application uses [`react-navigation`](https://reactnavigation.org/) for navigating between parts of the app. 1. [Higher Order Components](https://reactjs.org/docs/higher-order-components.html) are used to connect React components to persistent storage via [`react-native-onyx`](https://github.com/Expensify/react-native-onyx). +---- +# HybridApp + +Currently, the production Expensify app contains both "Expensify Classic" and "New Expensify". The file structure is as follows: + +- 📂 [**App**](https://github.com/Expensify/App) + - 📂 [**android**](https://github.com/Expensify/App/tree/main/android): New Expensify Android specific code (not a part of HybridApp native code) + - 📂 [**ios**](https://github.com/Expensify/App/tree/main/ios): New Expensify iOS specific code (not a part of HybridApp native code) + - 📂 [**src**](https://github.com/Expensify/App/tree/main/src): New Expensify TypeScript logic + - 📂 [**Mobile-Expensify**](https://github.com/Expensify/Mobile-Expensify): `git` submodule that is pointed to [Mobile-Expensify](https://github.com/Expensify/Mobile-Expensify) + - 📂 [**Android**](https://github.com/Expensify/Mobile-Expensify/tree/main/Android): Expensify Classic Android specific code + - 📂 [**iOS**](https://github.com/Expensify/Mobile-Expensify/tree/main/iOS): Expensify Classic iOS specific code + - 📂 [**app**](https://github.com/Expensify/Mobile-Expensify/tree/main/app): Expensify Classic JavaScript logic (aka YAPL) + +You can only build HybridApp if you have been granted access to [`Mobile-Expensify`](https://github.com/Expensify/Mobile-Expensify). For most contributors, you will be working on the standalone NewDot application. + +## Getting started with HybridApp + +1. If you haven't, please follow [these instructions](https://github.com/Expensify/App?tab=readme-ov-file#getting-started) to setup the NewDot local environment. +2. Run `git submodule update --init --progress --depth 100` to download the `Mobile-Expensify` sourcecode. +- If you have access to `Mobile-Expensify` and the command fails, add this to your `~/.gitconfig` file: + + ``` + [url "https://github.com/"] + insteadOf = ssh://git@github.com/ + ``` + +At this point, the default behavior of some `npm` scripts will change to target HybridApp: +- `npm run android` - build HybridApp for Android +- `npm run ios` - build HybridApp for iOS +- `npm run ipad` - build HybridApp for iPad +- `npm run ipad-sm` - build HybridApp for small iPad +- `npm run pod-install` - install pods for HybridApp +- `npm run clean` - clean native code of HybridApp + +If for some reason, you need to target the standalone NewDot application, you can append `*-standalone` to each of these scripts (eg. `npm run ios-standalone` will build NewDot instead of HybridApp). The same concept applies to the installation of standalone NewDot node modules. To skip the installation of HybridApp-specific patches and node modules, use `npm run i-standalone` or `npm run install-standalone`. + +## Working with HybridApp +Day-to-day work with HybridApp shouldn't differ much from the work on the standalone NewDot repo. + +The main difference is that the native code which runs React Native is located in `./Mobile-Expensify/Android` and `./Mobile-Expensify/iOS` directories. It means, that changes in `./android` and `./ios` folders in the root **won't affect the HybridApp build**. + +In that case, if you'd like to eg. remove `Pods`, you need to do it in `./Mobile-Expensify/iOS`. The same rule applies to Android builds - if you'd like to delete `.cxx`, `build` or `.gradle` directories, you need to go to `./Mobile-Expensify/android` first. + +Additionally, If you'd like to open the HybridApp project in Android Studio or XCode, you **must choose a workspace located in the `Mobile-Expensify`** directory: + +- Android: `./Mobile-Expensify/Android` +- iOS: `./Mobile-Expensify/iOS/Expensify.xcworkspace` + +### Updating the Mobile-Expensify submodule + +`Mobile-Expensify` directory is a git submodule. It means, that it points to a specific commit on the `Mobile-Expensify` repository. If you'd like to download the most recent changes from `main`, please use the following command: + +`git submodule update --remote` + +### Modifying Mobile-Expensify code + +It's important to emphasise that a git submodule is just a **regular git repository** after all. It means that you can switch branches, pull the newest changes, and execute all regular git commands within the `Mobile-Expensify` directory. + +> [!Note] +> #### For external contributors +> +> If you'd like to modify the `Mobile-Expensify` source code, it is best that you create your own fork. Then, you can swap origin of the remote repository by executing this command: +> +> `cd Mobile-Expensify && git remote set-url origin ` +> +> This way, you'll attach the submodule to your fork repository. + +### Adding HybridApp-related patches + +Applying patches from the `patches` directory is performed automatically with the `npm install` command executed in `Expensify/App`. + +If you'd like to add HybridApp-specific patches, use the `--patch-dir` flag: + +`npx patch-package --patch-dir Mobile-Expensify/patches` + +### HybridApp troubleshooting + +#### Cleaning the repo +- `npm run clean` - deep clean of all HybridApp artifacts (including NewDot's `node_modules`) +- `npm run clean -- --ios` - clean only iOS HybridApp artifacts (`Pods`, `build` folder, `DerivedData`) +- `npm run clean -- --android` - clean only Android HybridApp artifacts (`.cxx`, `build`, and `.gradle` folders, execute `./gradlew clean`) + +If you'd like to do it manually, remember to `cd Mobile-Expensify` first! + +#### Common errors +1. **Please check your internet connection** - set `_isOnDev` in `api.js` to always return `false` +2. **CDN: trunk URL couldn't be downloaded** - `cd Mobile-Expensify/iOS && pod repo remove trunk` + +3. **Task :validateSigningRelease FAILED** - open `Mobile-Expensify/Android/build.gradle` and do the following: + ``` + - signingConfig signingConfigs.release + + signingConfig signingConfigs.debug + ``` +4. **Build service could not create build operation: unknown error while handling message: MsgHandlingError(message: "unable to initiate PIF transfer session (operation in progress?)")** - reopen XCode + ---- # Philosophy diff --git a/__mocks__/@expensify/react-native-live-markdown.ts b/__mocks__/@expensify/react-native-live-markdown.ts new file mode 100644 index 000000000000..3ee327efed40 --- /dev/null +++ b/__mocks__/@expensify/react-native-live-markdown.ts @@ -0,0 +1 @@ +export * from '@expensify/react-native-live-markdown/mock'; diff --git a/android/app/build.gradle b/android/app/build.gradle index ad6493f7c3ee..09bf1473e841 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009007201 - versionName "9.0.72-1" + versionCode 1009007801 + versionName "9.0.78-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/assets/images/buildings.svg b/assets/images/buildings.svg new file mode 100644 index 000000000000..42171d499f26 --- /dev/null +++ b/assets/images/buildings.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/assets/images/companyCards/card-bofa.svg b/assets/images/companyCards/card-bofa.svg index 3cc7cf1de2cc..c58229f1b242 100644 --- a/assets/images/companyCards/card-bofa.svg +++ b/assets/images/companyCards/card-bofa.svg @@ -1 +1,27 @@ - \ No newline at end of file + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/companyCards/card-capitalone.svg b/assets/images/companyCards/card-capitalone.svg index a7c54c7bf529..9f1402298683 100644 --- a/assets/images/companyCards/card-capitalone.svg +++ b/assets/images/companyCards/card-capitalone.svg @@ -1 +1,23 @@ - \ No newline at end of file + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/companyCards/large/card-bofa-large.svg b/assets/images/companyCards/large/card-bofa-large.svg index a842bc93d80b..c83e06ffb65d 100644 --- a/assets/images/companyCards/large/card-bofa-large.svg +++ b/assets/images/companyCards/large/card-bofa-large.svg @@ -1,6 +1,6 @@ - + - - - - - - - + + + + + + + \ No newline at end of file diff --git a/assets/images/companyCards/large/card-capital_one-large.svg b/assets/images/companyCards/large/card-capital_one-large.svg index b71e209a4c11..20f3bd442d9e 100644 --- a/assets/images/companyCards/large/card-capital_one-large.svg +++ b/assets/images/companyCards/large/card-capital_one-large.svg @@ -1,15 +1,15 @@ - + - - + + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__building.svg b/assets/images/simple-illustrations/simple-illustration__building.svg new file mode 100644 index 000000000000..94a7320d8471 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__building.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__buildings.svg b/assets/images/simple-illustrations/simple-illustration__buildings.svg new file mode 100644 index 000000000000..cb22c3a29ce4 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__buildings.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/train.svg b/assets/images/train.svg new file mode 100644 index 000000000000..40d8c9d1af8a --- /dev/null +++ b/assets/images/train.svg @@ -0,0 +1,3 @@ + + + diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts index c60670c72324..ac086d3a9bed 100644 --- a/config/webpack/webpack.common.ts +++ b/config/webpack/webpack.common.ts @@ -175,7 +175,7 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment): // We are importing this worker as a string by using asset/source otherwise it will default to loading via an HTTPS request later. // This causes issues if we have gone offline before the pdfjs web worker is set up as we won't be able to load it from the server. { - test: new RegExp('node_modules/pdfjs-dist/legacy/build/pdf.worker.min.mjs'), + test: new RegExp('node_modules/pdfjs-dist/build/pdf.worker.min.mjs'), type: 'asset/source', }, diff --git a/docs/articles/expensify-classic/connect-credit-cards/company-cards/Troubleshooting.md b/docs/articles/expensify-classic/connect-credit-cards/company-cards/Troubleshooting.md index f94e692f5e56..1398e02a7a03 100644 --- a/docs/articles/expensify-classic/connect-credit-cards/company-cards/Troubleshooting.md +++ b/docs/articles/expensify-classic/connect-credit-cards/company-cards/Troubleshooting.md @@ -3,97 +3,156 @@ title: Troubleshooting description: How to troubleshoot company card importing in Expensify --- # Overview -Whether you're encountering issues related to company cards, require assistance with company card account access, or have questions about company card import features, you've come to the right place. +This guide helps you troubleshoot common issues with company cards in Expensify, including connection errors, missing transactions, and account setup problems. -## How to add company cards to Expensify -You can add company credit cards under the Domain settings in your Expensify account by navigating to *Settings* > *Domain* > _Domain Name_ > *Company Cards* and clicking *Import Card/Bank* and following the prompts. +## Adding company cards to Expensify +To add company credit cards: -## To Locate Missing Card Transactions in Expensify -1. **Wait for Posting**: Bank transactions may take up to 24 hours to import into Expensify after they have "posted" at your bank. Ensure sufficient time has passed for transactions to appear. -2. **Update Company Cards**: Go to Settings > Domains > Company Cards. Click on the card in question and click "Update" to refresh the card feed. -3. **Reconcile Cards**: Navigate to the Reconciliation section under Settings > Domains > Company Cards. Refer to the detailed guide on how to use the [Reconciliation Dashboard](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Reconciliation#identifying-outstanding-unapproved-expenses-using-the-reconciliation-dashboard). -4. **Review Transactions**: Use the Reconciliation Dashboard to view all transactions within a specific timeframe. Transactions will display on the Expenses page based on their "Posted Date". If needed, uncheck the "use posted date" checkbox near the filters to view transactions based on their "Transaction Date" instead. -5. **Address Gaps**: If there is a significant gap in transactions or if transactions are still missing, contact Expensify's Concierge or your Account Manager. They can initiate a historical data update on your card feed to ensure all transactions are properly imported. +1. Go to **Settings** > **Domain** > _[Domain Name]_ > **Company Cards**. +2. Click **Import Card/Bank** and follow the prompts. -Following these steps should help you identify and resolve any issues with missing card transactions in Expensify. +{% include info.html %} +Only Domain Admins can connect and assign company cards in Expensify. If you're not a Domain Admin and want to connect your own credit card, follow the steps [here](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Personal-Credit-Cards) to connect it as a personal card. +{% include end-info.html %} -## Known issues importing transactions -The first step should always be to "Update" your card, either from Settings > Your Account > Credit Card Import or Settings > Domain > [Domain Name] > Company Cards for centrally managed cards. If a "Fix" or "Fix card" option appears, follow the steps to fix the connection. If this fails to import your missing transactions, there is a known issue whereby some transactions will not import for certain API-based company card connections. So far this has been reported on American Express, Chase and Wells Fargo. This can be temporarily resolved by creating the expenses manually instead: +## Best practices for establishing the initial card connection +To ensure a successful initial card connection in Expensify, follow these best practices: -- [Manually add the expenses](https://help.expensify.com/articles/expensify-classic/expenses/expenses/Add-an-expense) -- [Upload the expenses via CSV](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/CSV-Import) +- **Import in the Correct Location**: For company cards, navigate to **Settings** > **Domains** > _[Domain Name]_ > **Company Cards** > **Import Card** to establish the connection. For personal or individual card accounts, refer to the instructions [here](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Personal-Credit-Cards). +- **Select the Appropriate Bank Connection**: Ensure you’re selecting the appropriate bank connection for your cards. +- **Use Master or Parent Administrative Credentials**: For company cards, always use the master administrative credentials to import the entire set of cards. +- **Disable Two-Factor Authentication (2FA)**: Expensify cannot bypass bank-imposed 2FA requirements. To maintain a stable connection, temporarily disable 2FA on your bank account before attempting to connect. -# Errors connecting company cards +By following these steps, you can avoid common issues and establish a stable card connection with Expensify. + +# Resolving missing card transactions + +Here are some common steps to resolve issues with missing imported expenses: + +1. **Wait for posting.** Bank transactions may take up to 24 hours to import into Expensify after they have posted at your bank. Ensure sufficient time has passed for transactions to appear. +2. **Update company cards.** Go to **Settings** > **Domains** > _[Domain Name]_ > **Company Cards**. Click on the card in question and select **Update** to refresh the card feed. +3. **Reconcile cards.** Navigate to the **Reconciliation** section under **Settings** > **Domains** > _[Domain Name]_ > **Company Cards**. Refer to the detailed guide on how to use the [Reconciliation Dashboard](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Reconciliation#identifying-outstanding-unapproved-expenses-using-the-reconciliation-dashboard). +4. **Review transactions.** Use the [Reconciliation Dashboard](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Reconciliation#identifying-outstanding-unapproved-expenses-using-the-reconciliation-dashboard) to view all transactions within a specific timeframe. Transactions will display on the **Expenses** page based on their posted date. If needed, uncheck the Use Posted Date checkbox near the filters to view transactions based on their Transaction Date instead. +5. **Address gaps.** If there is a significant gap in transactions or if transactions are still missing, contact Concierge or your Account Manager. They can initiate a historical data update on your card feed to ensure all transactions are properly imported. + +# General troubleshooting + +## Common import problems + +If company cards seem to be disconnected or not working as expected, troubleshoot by: +- Clicking **Update Card** under: + - **Settings** > **Account** > **Credit Card Import** for personal cards, or + - **Settings** > **Domains** > _[Domain Name]_ > **Company Cards** for company cards. +- If a **Fix** option appears, click on it and follow the steps to fix the connection. + +## Alternative workarounds +For persistent issues with API-based connections (e.g., American Express, Chase, Wells Fargo), the alternative option is to [manually add expenses](https://help.expensify.com/articles/expensify-classic/expenses/expenses/Add-an-expense), or [upload expenses via CSV](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/CSV-Import). + +## The connection is established but there are no cards to assign +When establishing the connection, you must assign cards during the same session. It isn't possible to create the connection, log out, and assign the cards later, as the connection will not stick and will require you to reattempt the connection. + +# Addressing duplicate expenses + +If a workspace member is experiencing duplicated expenses, this is typically due to: + + - A cardholder having accidentally imported the card as a personal credit card, in addition to being assigned the company card by a Domain Admin. + - To troubleshoot, have the employee navigate to **Settings** > **Account** > **Credit Card Import** and confirm that their card is only listed once. + + - The card was reassigned to the cardholder without the appropriate transaction start date being selected, resulting in a period of overlap. + - To troubleshoot, ensure expenses on the new card assignment have not been submitted. Then unassign the card and reassign it with a more appropriate start date. This action will delete all unsubmitted expenses from the new card feed. + +{% include info.html %} +Deleting a duplicate card will remove all Unapproved and Open expenses linked to that card. However, transactions associated with the remaining assigned card will remain unaffected. Any receipts attached to the deleted transactions will still appear on the Expenses page and can be reattached to the corresponding imported expense on the remaining assigned card. +{% include end-info.html %} + +# Tips for stable bank connections + +## Causes for connection breaks +Banks frequently update their APIs to enhance the security of financial information. However, for security reasons, they may not notify third-party services like Expensify in advance of these changes. Expensify's engineering team works diligently to minimize interruptions by monitoring bank connections and collaborating with banks to address updates promptly. + +## Resolving connection issues +Expensify's API-based banking connections rely on the online banking login credentials to maintain the connection. If your online banking username, password, security questions, login authentication, or card numbers change, the connection may need to be reestablished. Domain Admins can update this information in Expensify and manually reestablish the connection via **Settings** > **Domains** > _[Domain Name]_ > **Company Cards** > **Fix**. The Domain Admin will be prompted to enter the new credentials or updated information, which should reestablish the connection. + +# Common errors and resolutions + +Here are some errors that can occur when working with bank connections, and steps for resolving them: ## Error: Too many attempts -If you've been locked out while trying to import a new card, you'll need to wait a full 24 hours before trying again. This lock happens when incorrect online banking credentials are entered multiple times, and it's there for your security — it can't be removed. To avoid this, make sure your online banking credentials are correct before attempting to import your card again. - -## Error: Invalid credentials/Login failed -Verify your ability to log into your online banking portal by attempting to log into your bank account via the banking website. -Check for any potential temporary outages on your bank's end that may affect third-party connections like Expensify. -For specific card types: -- *Chase Card*: Confirm your password meets their new 8-32 character requirement. -- *Wells Fargo Card*: Ensure your password is under 14 characters. Reset it if necessary before importing your card to Expensify. If your card is already imported, update it and use the "Fix Card" option to reestablish the connection. -- *SVB Card*: Enable Direct Connect from the SVB website and use your online banking username and Direct Connect PIN instead of your password when connecting an SVB card. If connecting via *Settings* > *Domain* > _[Domain Name]_ > *Company Cards*, contact SVB for CDF feed setup. +If you've been locked out while trying to import a new card, you will need to wait a full 24 hours before trying again. This lock happens when incorrect online banking credentials are entered multiple times, and it cannot be bypassed. To avoid this, make sure your online banking credentials are correct before attempting to import your card again. + +## Error: Invalid credentials/login failed +Verify the online banking login details by accessing your bank's website directly. +- Some known bank-specific requirements are: + - **Chase**. Password must meet their 8-32 character requirement. + - **Wells Fargo**. Password must be under 14 characters. + - **SVB**. Enable Direct Connect and use the Direct Connect PIN for login. ## Error: Direct Connect not enabled -Direct Connect will need to be enabled in your account for your bank/credit card provider before you can import your card to Expensify. Please reach out to your bank to confirm if this option is available for your account, as well as get instructions on how to get this setup. +Direct Connect needs to be enabled on the bank account by your bank or credit card provider before it can be connected to Expensify. Please reach out to your bank to confirm if this option is available for your account and get instructions on how to enable it. -## Error: Account Setup -This error message typically indicates that there's something you need to do on your bank account's end. Please visit your online banking portal and check if there are any pending actions required. Once you've addressed those, you can try connecting your card again. -For Amex cardholders with multiple card programs in your Amex US Business account: To import multiple card programs into Expensify, you'll need to contact Amex and request that they separate the multiple card programs into distinct logins. For instance, you'll want to have your _Business Platinum_ cards under *"username1/password1"* and _Business Gold_ cards under *"username2/password2."* This ensures smooth integration with Expensify. +## Error: Account setup +This error message indicates that there is something you need to do on your bank account's end. Please visit your online banking portal and check if there are any pending actions required before attempting to connect your card again. -## Error: Account type not supported -If Expensify doesn't have a direct connection to your bank/credit card provider, we can still support the connection via spreadsheet import, which you can learn more about [here](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/CSV-Import). If the cards you're trying to import are company cards, it’s possible that you might be able to obtain a commercial feed directly from your bank. Please find more information on this [here](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Commercial-Card-Feeds). +# Troubleshooting American Express connections -## Error: Username/Password/Questions out of date -Your company card connection is broken because we're missing some answers to some security questions. Please head to *Settings* > *Domain* > _[Domain Name]_ > *Company Cards* and click _Fix Card_. -This will require you to answer your bank's security questions. You will need to do this for each security question you have with your bank; so if you have 3 security questions, you will need to do this 3 times. +## Account roles and permissions +When connecting American Express cards to Expensify, you must use the Amex login credentials of the Primary/Basic account holder. Using other credentials, such as Supplemental Cardmember or Authorized Account Manager, will fail to load card data or may result in an error. -## Error: Account not found/Card number changed -This error message appears when you have been issued a new card, or if there's been a significant change to the account in some other way (password and/or card number change). -When your online bank/card account password has been changed, you may need to update the details on the Expensify end as well. To do this, navigate to *Settings* > *Domain* > _[Domain Name]_ > *Company Cards* and click _Fix Card_. -If there’s been a recent change to the card number, you’ll have to remove the card with the previous number and re-import the card using the new number. A Domain Admin will have to re-assign the card via *Settings* > *Domain* > _Domain Name_ > *Company Cards*. Before removing the card, please ensure *all Open reports have been submitted*, as removing the card will remove all imported transactions from the account that are associated with that card. +{% include info.html %} +In American Express, the Primary/Basic Account Holder is typically the person who applied for the American Express Business card, owns the account, manages its finances, and controls card issuance and account management. They can see all charges made by other cardmembers on their account. -## Error: General connection error -This error message states that your bank or credit card provider is under maintenance and is unavailable at this time. Try waiting a few hours before trying to import your credit card again. Check out our [status page](https://status.expensify.com/) for updates on bank/credit card connections, or you can also choose to subscribe to updates for your specific account type. +By contrast, a Supplemental Cardmember or Employee Cardmember is typically an employee on American Express accounts with access to their own card and payments. An Authorized Account Manager (AAM) has management privileges allowing them to manage the account and Supplemental Cardmembers' accounts. These roles do not have sufficient permissions in American Express to authorize the connection to Expensify, and therefore only the Primary/Basic Account Holder credentials can be used. +{% include end-info.html %} -## Error: Not seeing cards listed after a successful login -The card will only appear in the drop-down list for assignment once it’s activated and there are transactions that have been incurred and posted on the card. If not, the card won't be available to assign to the card holder until then. +## Importing multiple card programs +If you have multiple American Express card programs, contact Amex and request that they separate the multiple card programs into distinct logins. For example, you can have your _Business Platinum_ cards under *"username1/password1"* and _Business Gold_ cards under *"username2/password2"*. This ensures smooth integration with Expensify. -# Troubleshooting issues assigning company cards +## Connecting multiple company card programs under the same credentials +If you have multiple company card programs using the same credentials, you can import all programs together, which will display them under a single dropdown. Be sure to select all relevant cards each time you add cards from any program. -## Why do bank connections break? -Banks often make changes to safeguard your confidential information, and when they do, we need to update the connection between Expensify and the bank. We have a team of engineers who work closely with banks to monitor this and update our software accordingly when this happens. -The first step is to check if there have been any changes to your bank information. Have you recently changed your banking password without updating it in Expensify? Has your banking username or card number been updated? Did you update your security questions for your bank? -If you've answered "yes" to any of these questions, a Domain Admins need to update this information in Expensify and manually re-establish the connection by heading to *Settings* > *Domains* > _Domain Name_ > *Company Cards* > *Fix*. The Domain Admin will be prompted to enter the new credentials/updated information and this should reestablish the connection. +If you prefer to manage card programs separately, you can import them one at a time, ensuring you select all cards within the specific program during each import. After authorizing the account, you will be guided back to Expensify to assign the cards as needed. -## How do I resolve errors while I’m trying to import my card?* -Make sure you're importing your card in the correct spot in Expensify and selecting the right bank connection. For company cards, use the master administrative credentials to import your set of cards at *Settings* > *Domains* > _Domain Name_ > *Company Cards* > *Import Card*. -Please note there are some things that cannot be bypassed within Expensify, including two-factor authentication being enabled within your bank account. This will prevent the connection from remaining stable and will need to be turned off on the bank side. +*Important Reminder*: Whenever you need to access the connection to assign a new card, you must still choose all card programs. For example, if you have a new employee with a card under your Business Gold Rewards Card program, you will still need to authorize all the cards in that program or all the programs if you have only one dropdown menu. -## Why Can’t I See the Transactions Before a Certain Date? -When importing a card into Expensify, the platform typically retrieves 30-90 days of historical transactions, depending on the card or account type. For commercial feeds, transactions cannot be imported before the bank starts sending data. If needed, banks can send backdated files, and Expensify can run a historical update upon request. +## Adding cards under different programs with different logins +If you have multiple card programs with different credentials, you will need another Domain Admin account to add each card program from their own account. Once all Domain Admins have connected and assigned the cards they are the Primary account holder for, all cards will be listed under one *American Express (New and Upgraded)* list on the Domain Company Card page. -Additionally, Expensify does not import transactions dated before the "start date" you specify when assigning the card. Unless transitioning from an old card to a new one to avoid duplicates, it's advisable to set the start date to "earliest possible" or leave it blank. +## Amex error: Username, password, or security questions out of date +Your company card connection is broken because Expensify is missing answers to your security questions. Go to **Settings** > **Domain** > _[Domain Name]_ > **Company Cards** and click **Fix**. Answer your bank's security questions to restore the connection. Repeat this process for each security question your bank requires. -For historical expenses that cannot be imported automatically, consider using Expensify's [company card](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/CSV-Import) or [personal card](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Personal-Credit-Cards#importing-expenses-via-a-spreadsheet) spreadsheet import method. This allows you to manually input missing transactions into the system. +## Amex error: Account not found or card number changed +This error occurs when you have been issued a new card or if there has been a significant change to the account, such as a password or card number update. -## Why Am I / Why Is My Employee Seeing Duplicates? -If an employee is seeing duplicate expenses, they may have accidentally imported the card as a personal credit card as well as having the Domain Admin assign them a company card. +To update the connection: +1. Go to **Settings** > **Domain** > _[Domain Name]_ > **Company Cards** and click **Fix**. +2. If there has been a card number change, remove the card with the previous number and re-import the card with the new number. +3. Before removing the card, ensure all open reports have been submitted. Removing the card will delete all imported transactions associated with that card. A Domain Admin will need to re-assign the card after re-importing it. -To troubleshoot: -- Have the employee navigate to their Settings > Your Account > Credit Card Import and confirm that their card is only listed once. -- If the card is listed twice, delete the entry without the "padlock" icon. +## Amex error: General connection error +This error indicates that your bank or credit card provider is under maintenance and unavailable. Wait a few hours before trying to import your credit card again. Check Expensify's [status page](https://status.expensify.com/) for updates on bank or credit card connections, or subscribe to updates for your account type. + +## Amex error: Session has expired +If you see an error stating "Your session has expired. Please return to Expensify and try again," this means you are using incorrect Amex credentials. Use the Primary/Basic account holder credentials. If you are unsure which credentials to use, contact American Express for guidance. + +## Amex error: Card isn't eligible +This error occurs when the account is not a business account or the credentials used are not for the Primary account holder. Verify the account type and credentials before attempting to connect again. -**Important:** Deleting a duplicate card will delete all unapproved expenses from that transaction feed. Transactions associated with the remaining card will not be affected. If receipts were attached to those transactions, they will still be on the Expenses page, and the employee can click to SmartScan them again. +# Troubleshooting Chase connections -Duplicate expenses might also occur if you recently unassigned and reassigned a company card with an overlapping start date. If this is the case and expenses on the “new” copy have not been submitted, you can unassign the card again and reassign it with a more appropriate start date. This action will delete all unsubmitted expenses from the new card feed. +## Resetting Chase access to Expensify +If you are experiencing issues with your Chase connection in Expensify, resetting access can often resolve the problem. Follow these steps to troubleshoot: -## What are the most reliable bank connections in Expensify?* -All bank connections listed below are extremely reliable, but we recommend transacting with the Expensify Visa® Commercial Card. It also offers daily and monthly settlement, unapproved expense limits, realtime compliance for secure and efficient spending, and cash back on all US purchases. [Click here to learn more about the Expensify Card](https://use.expensify.com/company-credit-card). +1. Log in to your Chase account portal and visit the [Linked Apps & Websites](https://www.chase.com/digital/data-sharing) page in the Security Center. +2. Locate Expensify in the Linked Apps & Websites list. +3. Select **Stop sharing data** to disconnect Expensify's access to your Chase account. +4. After resetting access, follow the instructions [here](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Troubleshooting#how-to-add-company-cards-to-expensify) to reestablish the connection to Chase. -We've also teamed up with major banks worldwide to ensure a smooth import of credit card transactions into your accounts: +{% include faq-begin.md %} + +## What bank connections does Expensify offer? +Expensify offers highly reliable bank connections, but we recommend using the Expensify Visa® Commercial Card. It provides daily and monthly settlement, unapproved expense limits, real-time compliance for secure and efficient spending, and cash back on all US purchases. [Click here to learn more about the Expensify Card](https://use.expensify.com/company-credit-card). + +Alternatively, Expensify has partnered with major banks worldwide to ensure a smooth import of credit card transactions into your accounts, including: - American Express - Bank of America - Brex @@ -103,33 +162,13 @@ We've also teamed up with major banks worldwide to ensure a smooth import of cre - Stripe - Wells Fargo -Commercial feeds for company cards are the dependable connections in Expensify. If you have a corporate or commercial card account, you might have access to a daily transaction feed where expenses from Visa, Mastercard, and American Express are automatically sent to Expensify. Reach out to your banking relationship manager to check if your card program qualifies for this feature. +## What are the most stable bank connections? +Commercial feeds for company cards are the most dependable connections in Expensify and are considered more stable than API-based connections. If you have a corporate or commercial card account, you might have access to a daily transaction feed where expenses from Visa, Mastercard, and American Express are automatically sent to Expensify. Contact your banking relationship manager to check if your card program qualifies for this feature. -# Troubleshooting American Express Business +## Why can’t I see the transactions before a certain date? +When importing a card into Expensify, the bank typically provides 30-90 days of historical transactions, depending on the card or account type. For commercial feeds, transactions cannot be imported before the bank starts sending data, however banks can send backdated files if historical transactions are needed. -## Amex account roles -American Express provides three different roles for accessing accounts on their website. When connecting Amex cards to Expensify, it's crucial to use the credentials of the Primary/Basic account holder. Here's what each role means: -- *Primary/Basic Account Holder*: The person who applied for the American Express Business card, owns the account, manages its finances, and controls card issuance and account management. They can view all charges by other cardmembers on their account. They can see all charges made by other cardmembers on their account. -- *Supplemental Cardmember (Employee Cardmember)*: Chosen by the Primary Card Member (typically an employee on business accounts), they can access their own card info and make payments but can't see other account details. -- *Authorized Account Manager (AAM)*: Chosen by the Primary Card Member, AAMs can manage the account online or by phone, but they can't link cards to services like Expensify. They have admin rights, including adding cards, making payments, canceling cards, and setting limits. To connect cards to Expensify, use the Primary Card Holder's credentials for full access. - -## The connection is established but there are no cards to assign +Additionally, Expensify does not import transactions dated before the "start date" you specify when assigning the card. Unless transitioning from an old card to a new one to avoid duplicates, it is advisable to set the start date to "earliest possible" or leave it blank. For historical expenses that cannot be imported automatically, consider using Expensify's [company card](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/CSV-Import) or [personal card](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Personal-Credit-Cards#importing-expenses-via-a-spreadsheet) spreadsheet import method to manually input missing transactions into the system. -When establishing the connection, you must assign cards during the same session. It isn't possible to create the connection, log out, and assign the cards later, as the connection will not stick, and require you to reattempt the connection again. +{% include faq-end.md %} -## Amex error: Card isn't eligible -This error comes directly from American Express and is typically related to an account that is not a business account or using credentials that are not the primary account holder credentials. - -## Amex error: Session has expired -If you get an error stating an American Express Business Card “Your session has expired. Please return to Expensify and try again, this always means that you are using the incorrect credentials. Remember, you need to use primary/basic cardholder credentials. If you are not sure which credentials you should use, reach out to American Express for guidance. - -## Connect multiple company card programs under the same credentials -If you have multiple company card programs with the same credentials, you can select ALL programs at once. With this, all programs will be under one dropdown. Make sure to select all cards each time you are adding any cards from any program. -If you would like your card programs listed under separate dropdowns, you can select only that group making sure to select all cards from that group each time you are adding a new card. -Once you have authorized the account, you’ll be guided back to Expensify where you’ll assign all necessary cards across all programs. -This will store all cards under the same American Express Business connection dropdown and allow all cards to be added to Expensify for you to assign to users. -*Important Reminder*: Whenever you need to access the connection to assign a new card, you must still choose "ALL card programs." For instance, if you have a new employee with a card under your Business Gold Rewards Card program, you'll still need to authorize all the cards in that program or all the programs if you have only one dropdown menu! - -## Add cards under different programs with different logins -If you have multiple card programs with different credentials, you will need to have another Domain Admin account add each card program from their own account. -Once all Domain Admins have connected and assigned the cards that they are the Primary account holder for, all cards will be listed under one *American Express (New and Upgraded)* list in the Domain Company Card page. diff --git a/docs/articles/expensify-classic/connections/Expensify-API.md b/docs/articles/expensify-classic/connections/Expensify-API.md new file mode 100644 index 000000000000..e2fbdbfd7703 --- /dev/null +++ b/docs/articles/expensify-classic/connections/Expensify-API.md @@ -0,0 +1,231 @@ +--- +title: Expensify API +description: User-sourced tips and tricks for using Expensify’s API. +--- +# Overview +An API (Application Programming Interface) allows two programs to communicate with each other. Expensify's API connects with various software platforms like NetSuite or Xero, and it can also link to other systems that don’t have a pre-made connection, such as [Workday](https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/Workday). + +{% include info.html %} +To begin, review our [Integration Server Manual](https://integrations.expensify.com/Integration-Server/doc/#introduction) thoroughly, as it will be your primary resource. The Expensify API is a self-serve tool, and your internal team is responsible for setting it up and ensuring it meets your needs. We can assist with basic troubleshooting, but the level of support may vary based on the support agent or account manager. It’s important for your team to be familiar with the setup process. +{% include end-info.html %} + +We've compiled answers to some frequently asked questions to help you get started. + +## Should I give your support team my API credentials when I need help? + +If you’re seeking help with Expensify's API, do not share your partnerUserSecret. If you do, immediately rotate your credentials on [this page](https://www.expensify.com/tools/integrations/). + +## Is there a rate limit? + +To keep our platform stable and handle high traffic, Expensify limits how many API requests you can send: +- Up to 5 requests every 10 seconds +- Up to 20 requests every 60 seconds + +Sending more requests than allowed may result in an error with status code `429`. + +## What is a Policy ID? + +This is also known as a Workspace ID. To find your Policy/Workspace ID, +Hover over Settings and click Workspaces. +Click the name of the Workspace. +Copy the ID number from the URL. For example, if the URL is https://www.expensify.com/policy?param={"policyID":"0810E551A5F2A9C2”}, then your workspace ID is 0810E551A5F2A9C2. + +## Can I use the parent type `file` to export workspace/policy data? + +No. The parent type `file` can only be used to export expense and report data — not policy information. To export policy data (e.g., categories, tags), you must use the `get` type with `inputSettings.type` set to `policy`. + +## Can I use the API to create Domain Groups? + +No, you cannot create domain groups. You can only assign users to them. + +## I’m exporting expense IDs `${expense.transactionID}` but when I open my CSV in Excel, it’s changing all the IDs and making them look the same. How can I prevent this? + +Try prepending a non-numeric character like a quote to force Excel to interpret the value as a string and not a number (i.e., `'${expense.transactionID}`). + +## How can we export the person who will approve a report while the reports are still processing? + +Use the field ${report.managerEmail}. + +## Why won’t my boolean field return any data? + +Boolean fields won't output values without a string. For example, instead of using `${expense.billable}`, use `${expense.billable?string("Yes", "No")}`. This will display "Yes" if the expense is billable and "No" if it is not. + +## Can I export the reports for just one user? + +Not in a quick convenient way, as you would need to include the user in your template. The simplest approach is to export data for all users and then apply a filter in your preferred spreadsheet program. + +## Can I create expenses on behalf of users? + +Yes. However, to access the Expense Creator API on behalf of employees, Expensify needs to verify the following setup: + +Ensure you are properly configured (e.g., Domain Control, Domain Admin, Policy Admin). +Verify you have internal authorization to add data to other accounts within your domain. + +If you need this access, contact concierge@expensify.com and reference this help page. + +# Using Postman + +Many customers use Postman to help them build out their APIs. Below are some guides contributed by our customers. Please note, in all cases, you will need to first generate your authentication credentials, the steps for which can be found [here](https://integrations.expensify.com/Integration-Server/doc/#introduction) and have them ready: + +## Download expenses from a report as a CSV file + +**Step 1: Get the ID of a report you want to export in Expensify** + +Find the ID by opening the expense report and clicking Details at the top right corner of the page. At the top of the menu, the ID is provided as the “Long ID.” + +**Step 2: Export (generate) a "Report" as a CSV file** +{% include info.html %} +For this you'll use the Documentation under [Report Exporter](https://integrations.expensify.com/Integration-Server/doc/#export). +{% include end-info.html %} + +In Postman, set the following: + +- HTTP Action: POST +- URL: https://integrations.expensify.com/Integration-Server/ExpensifyIntegrations +- Your only Parameters ("Params") will be "requestJobDescription", described below +- Body: "x-www-form-encoded", with a key "template", described below + +The requestJobDescription key will have a value like below: + +``` +{ + "type": "file", + "credentials": { + "partnerUserID": "my_user_id", + "partnerUserSecret": "my_user_secret" + }, + "onReceive": { + "immediateResponse": [ + "returnRandomFileName" + ] + }, + "inputSettings": { + "type": "combinedReportData", + "filters": { + "reportIDList": "50352738" + } + }, + "outputSettings": { + "fileExtension": "csv" + } +} +``` +Take the above and replace it with your own partnerUserID, partnerUserSecret, and reportIDList. To download multiple reports, you can use a comma-separated list as the reportIDList, such as "12345,45678,11111". + +The template key will have the value like below: + +``` +<#if addHeader> + Merchant,Amount,Transaction Date<#lt> + +<#list reports as report> + <#list report.transactionList as expense> + <#if expense.modifiedMerchant?has_content> + <#assign merchant = expense.modifiedMerchant> + <#else> + <#assign merchant = expense.merchant> + + <#if expense.convertedAmount?has_content> + <#assign amount = expense.convertedAmount/100> + <#elseif expense.modifiedAmount?has_content> + <#assign amount = expense.modifiedAmount/100> + <#else> + <#assign amount = expense.amount/100> + + <#if expense.modifiedCreated?has_content> + <#assign created = expense.modifiedCreated> + <#else> + <#assign created = expense.created> + + ${merchant},<#t> + ${amount},<#t> + ${created}<#lt> + + +``` + +The template variable determines what information is saved in your CSV file. If you want more columns than merchant, amount, and transaction date, follow the syntax as defined in the export template format documentation. + +**Step 3: Save your generated file name** + +Expensify currently supports only the "onReceive":{"immediateResponse":["returnRandomFileName"]} option in step 2, so you should receive a random filename back from the API like "exportc111111d-a1a1-a1a1-a1a1-d1111111f.csv". You will need to document this filename if you plan on running the download command after this one. + +**Step 4: Download your exported report** + +Set up another API call in almost the same way you did before. You don't need the template key in the Body anymore, so delete that and set the Body type to "none". Then modify your requestJobDescription to read like below, but with your own credentials and file name: + +``` +{ + "type": "download", + "credentials": { + "partnerUserID": "my_user_id", + "partnerUserSecret": "my_user_secret" + }, + "fileName": "exportc111111d-a1a1-a1a1-a1a1-d1111111f.csv", + "fileSystem": "integrationServer" +} +``` + +Click Go and you should see the CSV in the response body. + +*Thank you to our customer Frederico Pettinella who originally wrote and shared this guide.* + +## Use Advanced Employee Updater API with Postman + +1. Create a new request. +2. Select POST as the method. +3. Copy-paste this to the URL section: https://integrations.expensify.com/Integration-Server/ExpensifyIntegrations +4. Do not add anything to "Params", "Authorization", or "Header". Go straight to "Body". +5. Select "x-www-form-urlencoded" and add 2 keys "requestJobDescription" and "data". +6. For "requestJobDescription" copy and paste the following text, and replace the values for "partnerUserID", "partner_UserSecret", and "recipients". Remember that "dry-run"=true means that it's just for testing. Set it to false whenever you are ready to modify that in production. + +``` +{ + "type": "update", + "dry-run" : true, + "credentials": { + "partnerUserID": "aa_api_domain_com", + "partnerUserSecret": "xxx" + }, + "dataSource" : "request", + "inputSettings": { + "type": "employees", + "entity": "generic" + }, + "onFinish":[ + {"actionName": "email", "recipients":"admin1@domain.com"} + ] + }' +For "data" copy-paste the following text and replace values as needed +{ + "Employees":[ + { + "employeeEmail": "user@domain.com", + "managerEmail": "usermanager@domain.com", + "policyID": "1D1BC525C4892584", +"isTerminated": "false", + } +]} +``` + +7. Click SEND. + +This is how it should look on Postman: + +![Image of API credentials request]({{site.url}}/assets/images/ExpensifyHelp-Postman-userID-userSecret-request.png){:width="100%"} + +![Image of API data request]({{site.url}}/assets/images/ExpensifyHelp-Postman-Request-data.png){:width="100%"} + +This is how the value looks inside those keys: + +![Image of API dry run]({{site.url}}/assets/images/ExpensifyHelp-Postman-Successful-dryrun-response.png){:width="100%"} + +Remember that there are 4 [required fields](https://integrations.expensify.com/Integration-Server/doc/employeeUpdater/#api-principles) needed to make this API call to work: + +- employeeEmail +- managerEmail +- employeeID +- policyID + +*Thank you to our customer Raul Hernandez who originally wrote and shared this guide.* + diff --git a/docs/articles/expensify-classic/connections/accelo/Accelo-Troubleshooting.md b/docs/articles/expensify-classic/connections/accelo/Accelo-Troubleshooting.md index fd0a6ca59069..f49ac1ead30e 100644 --- a/docs/articles/expensify-classic/connections/accelo/Accelo-Troubleshooting.md +++ b/docs/articles/expensify-classic/connections/accelo/Accelo-Troubleshooting.md @@ -1,7 +1,42 @@ --- title: Accelo Troubleshooting -description: Accelo Troubleshooting -order: 3 +description: Resources to help you solve issues with your Accelo integration. --- -# Coming soon +# Overview +Most of the Accelo integration with Expensify is managed on the Accelo side. You will find their [help site](https://help.accelo.com/guides/integrations-guide/expensify/) helpful, especially the [FAQs](https://help.accelo.com/guides/integrations-guide/expensify/#faq). + +## Information sync between Expensify and Accelo +The Accelo integration does a one-way sync, bringing expenses from Expensify into Accelo. When this happens, it transfers specific information from Expensify expenses to Accelo: + +| Expensify | Accelo | +|---------------------|-----------------------| +| Description | Title | +| Date | Date Incurred | +| Category | Type | +| Tags | Against (relevant Project, Ticket or Retainer) | +| Distance (mileage) | Quantity | +| Hours (time expenses) | Quantity | +| Amount | Purchase Price and Sale Price | +| Reimbursable? | Reimbursable? | +| Billable? | Billable? | +| Receipt | Attachment | +| Tax Rate | Tax Code | +| Attendees | Submitted By | + +## Expense Status +The status of your expense report in Expensify is also synced in Accelo. + +| Expensify Report Status | Accelo Expense Status | +|-------------------------|-----------------------| +| Open | Submitted | +| Submitted | Submitted | +| Approved | Approved | +| Reimbursed | Approved | +| Rejected | Declined | +| Archived | Approved | +| Closed | Approved | + + +## Can I use an Accelo and an accounting integration in Expensify at the same time? +Yes, you can use Accelo and an accounting system simultaneously. In order to update your Expensify tags with your Accelo Projects, Tickets, or Retainers, you will need to have a special switch enabled that allows you to have non-accounting tags alongside your accounting connection. Please contact Concierge to request that our support team enable the “Indirect Tag Uploads” switch for you. diff --git a/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md b/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md index 068e4dd5bca9..ec3d45b3ac08 100644 --- a/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md +++ b/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md @@ -36,11 +36,17 @@ The three options for the date your report will export with are: - Submitted date: The date the employee submitted the report - Exported date: The date you export the report to NetSuite +## Accounting Method + +This dictates when reimbursable expenses will export, according to your preferred accounting method: +- Accrual: Out-of-pocket expenses will export immediately when the report is final approved +- Cash: Out-of-pocket expenses will export when paid via Expensify or marked as Reimbursed + ## Export Settings for Reimbursable Expenses **Expense Reports:** Expensify transactions will export reimbursable expenses as expense reports by default, which will be posted to the payables account designated in NetSuite. -**Vendor Bills:** Expensify transactions export as vendor bills in NetSuite and will be mapped to the subsidiary associated with the corresponding workspace. Each report will be posted as payable to the vendor associated with the employee who submitted the report. You can also set an approval level in NetSuite for vendor bills. +**Vendor Bills:** Expensify transactions export as vendor bills in NetSuite and are mapped to the subsidiary associated with the corresponding workspace. Each report is posted as payable to the vendor associated with the employee who submitted it. You can also set an approval level in NetSuite for vendor bills. **Journal Entries:** Expensify transactions that are set to export as journal entries in NetSuite will be mapped to the subsidiary associated with this workspace. All the transactions will be posted to the payable account specified in the workspace. You can also set an approval level in NetSuite for the journal entries. @@ -57,7 +63,7 @@ The three options for the date your report will export with are: - Journal entry forms do not contain a customer column, so it is not possible to export customers or projects with this export option - The credit line and header level classifications are pulled from the employee record -**Expense Reports:** To use the expense report option for your corporate card expenses, you will need to set up your default corporate cards in NetSuite. +**Expense Reports:** To use the expense report option for your corporate card expenses, you must set up your default corporate cards in NetSuite. To use a default corporate card for non-reimbursable expenses, you must select the correct card on the employee records (for individual accounts) or the subsidiary record (If you use a non-one world account, the default is found in your accounting preferences). @@ -81,6 +87,8 @@ When selecting the option to export non-reimbursable expenses as vendor bills, t The Coding tab is where NetSuite information is configured in Expensify, which allows employees to code expenses and reports accurately. There are several coding options in NetSuite. Let’s go over each of those below. +![Insert alt text for accessibility here]({{site.url}}/assets/images/NetSuite_Configure_08.png){:width="100%"} + ## Expense Categories Expensify's integration with NetSuite automatically imports NetSuite Expense Categories as Categories in Expensify. @@ -219,6 +227,8 @@ From there, you should see the values for the Custom Lists under the Tag or Repo The NetSuite integration’s advanced configuration settings are accessed under **Settings > Workspaces > Group > _[Workspace Name]_ > Connections > NetSuite > Configure > Advanced tab**. +![Insert alt text for accessibility here]({{site.url}}/assets/images/NetSuite_Configure_09.png){:width="100%"} + Let’s review the different advanced settings and how they interact with the integration. ## Auto Sync diff --git a/docs/articles/expensify-classic/connections/sage-intacct/Configure-Sage-Intacct.md b/docs/articles/expensify-classic/connections/sage-intacct/Configure-Sage-Intacct.md index 0c9e6c87f9ab..1eb3f634a61c 100644 --- a/docs/articles/expensify-classic/connections/sage-intacct/Configure-Sage-Intacct.md +++ b/docs/articles/expensify-classic/connections/sage-intacct/Configure-Sage-Intacct.md @@ -86,7 +86,7 @@ These settings are particularly relevant to billable expenses and can be configu ### Tax -As of September 2023, our Sage Intacct integration supports native VAT and GST tax. To enable this feature, open the Sage Intacct configuration settings in your workspace, go to the Coding tab, and enable Tax. For existing Sage Intacct connectings, simply resync your workspace and the tax toggle will appear. For new Sage Intacct connections, the tax toggle will be available when you complete the integration steps. +As of September 2023, our Sage Intacct integration supports native VAT and GST tax. To enable this feature, open the Sage Intacct configuration settings in your workspace, go to the Coding tab, and enable Tax. For existing Sage Intacct connections, simply resync your workspace and the tax toggle will appear. For new Sage Intacct connections, the tax toggle will be available when you complete the integration steps. Enabling this option will import your native tax rates from Sage Intacct into Expensify. From there, you can select default rates for each category. ### User-Defined Dimensions diff --git a/docs/articles/expensify-classic/domains/SAML-SSO.md b/docs/articles/expensify-classic/domains/SAML-SSO.md index da4bd5639120..df73cf5d54c0 100644 --- a/docs/articles/expensify-classic/domains/SAML-SSO.md +++ b/docs/articles/expensify-classic/domains/SAML-SSO.md @@ -17,7 +17,7 @@ Once the domain is verified, you can access the SSO settings by navigating to Se **Below are instructions for setting up Expensify for specific SSO providers:** - [Amazon Web Services (AWS SSO)](https://static.global.sso.amazonaws.com/app-202a715cb67cddd9/instructions/index.htm) - [Google SAML](https://support.google.com/a/answer/7371682) (for GSuite, not Google SSO) -- [Microsoft Azure Active Directory](https://azure.microsoft.com/en-us/documentation/articles/active-directory-saas-expensify-tutorial/) +- [Microsoft Entra ID (formerly Azure Active Directory)](https://learn.microsoft.com/en-us/entra/identity/saas-apps/expensify-tutorial) - [Okta](https://saml-doc.okta.com/SAML_Docs/How-to-Configure-SAML-2.0-for-Expensify.html) - [OneLogin](https://onelogin.service-now.com/support?id=kb_article&sys_id=e44c9e52db187410fe39dde7489619ba) - [Oracle Identity Cloud Service](https://docs.oracle.com/en/cloud/paas/identity-cloud/idcsc/expensify.html#Expensify) @@ -39,13 +39,13 @@ The entityID for Expensify is https://expensify.com. Remember not to copy and pa ## Can you have multiple domains with only one entity ID? Yes. Please send a message to the Concierge or your account manager, and we will enable the use of the same entity ID with multiple domains. -## How can I update the Microsoft Azure SSO Certificate? +## How can I update the Microsoft Entra ID SSO Certificate? Expensify's SAML configuration doesn't support multiple active certificates. This means that if you create the new certification ahead of time without first removing the old one, the respective IDP will include two unique x509 certificates instead of one, and the connection will break. Should you need to access Expensify, switching back to the old certificate will continue to allow access while that certificate is still valid. -**To transfer from one Microsoft Azure certificate to another, please follow the below steps:** -1. In Azure Directory, create your new certificate. -2. In Azure Director, remove the old, expiring certificate. -3. In Azure Directory, activate the remaining certificate and get a new IDP for Expensify from it. +**To transfer from one Microsoft Entra certificate to another, please follow the below steps:** +1. In Microsoft Entra, create your new certificate. +2. In Microsoft Entra, remove the old, expiring certificate. +3. In Microsoft Entra, activate the remaining certificate and get a new IDP for Expensify from it. 4. In Expensify, replace the previous IDP with the new IDP. 5. Log in via SSO. If login continues to fail, write to Concierge for assistance. diff --git a/docs/articles/expensify-classic/expenses/Apply-Tax.md b/docs/articles/expensify-classic/expenses/Apply-Tax.md deleted file mode 100644 index 9360962cb2ba..000000000000 --- a/docs/articles/expensify-classic/expenses/Apply-Tax.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: Apply Tax -description: This is article shows you how to apply taxes to your expenses! ---- - - - -# About - -There are two types of tax in Expensify: Simple Tax (i.e. one tax rate) and Complex Tax (i.e. more than one tax rate). This article shows you how to apply both to your expenses! - - -# How-to Apply Tax - -When Tax Tracking is enabled on a Workspace, the default tax rate is selected under **Settings > Workspace > _Workspace Name_ > Tax**, with the default tax rate applied to all expenses automatically. - -There may be multiple tax rates set up within your Workspace, so if the tax on your receipt is different to the default tax that has been applied, you can select the appropriate rate from the tax drop-down on the web expense editor or the mobile app. - -If the tax amount on your receipt is different to the calculated amount or the tax rate doesn’t show up, you can always manually type in the correct tax amount. - - -{% include faq-begin.md %} - -## How do I set up multiple taxes (GST/PST/QST) on indirect connections? -Expenses sometimes have more than one tax applied to them - for example in Canada, expenses can have both a Federal GST and a provincial PST or QST. - -To handle these, you can create a single tax that combines both taxes into a single effective tax rate. For example, if you have a GST of 5% and PST of 7%, adding the two tax rates together gives you an effective tax rate of 12%. - -From the Reports page, you can select Reports and then click **Export To > Tax Report** to generate a CSV containing all the expense information, including the split-out taxes. - -## Why is the tax amount different than I expect? - -In Expensify, tax is *inclusive*, meaning it's already part of the total amount shown. - -To determine the inclusive tax from a total price that already includes tax, you can use the following formula: - -### **Tax amount = (Total price x Tax rate) ÷ (1 + Tax Rate)** - -For example, if an item costs $100 and the tax rate is 20%: -Tax amount = (**$100** x .20) ÷ (1 + .**20**) = **$16.67** -This means the tax amount $16.67 is included in the total. - -If you are simply trying to calculate the price before tax, you can use the formula: - -### **Price before tax = (Total price) ÷ (1 + Tax rate)** - -# Deep Dive - -If you have a receipt that has more than one tax rate (i.e. Complex Tax) on it, then there are two options for handling this in Expensify! - -Many tax authorities do not require the reporting of tax amounts by rate and the easiest approach is to apply the highest rate on the receipt and then modify the tax amount to reflect the amount shown on the receipt if this is less. Please check with your local tax advisor if this approach will be allowed. - -Alternatively, you can apply each specific tax rate by splitting the expense into the components that each rate will be applied to. To do this, click on **Split Expense** and apply the correct tax rate to each part. - -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expensify-billing/Tax-Exempt.md b/docs/articles/expensify-classic/expensify-billing/Tax-Exempt.md index 92c92e4e3a44..a91454b4965b 100644 --- a/docs/articles/expensify-classic/expensify-billing/Tax-Exempt.md +++ b/docs/articles/expensify-classic/expensify-billing/Tax-Exempt.md @@ -11,6 +11,8 @@ If your organization is recognized by the IRS or other local tax authorities as 1. Our team will review your document and let you know if we need any more information. 1. Once everything is verified, we'll update your account accordingly. +![Click the request tax exempt status button]({{site.url}}/assets/images/Tax Exempt - Classic.png){:width="100%"} + Once your account is marked as tax-exempt, the corresponding state tax will no longer be applied to future billing. If you need to remove your tax-exempt status, let your Account Manager know or contact Concierge. diff --git a/docs/articles/expensify-classic/travel/Configure-travel-policy-and-preferences.md b/docs/articles/expensify-classic/travel/Configure-travel-policy-and-preferences.md index f34ed373c1bb..2b2731fae117 100644 --- a/docs/articles/expensify-classic/travel/Configure-travel-policy-and-preferences.md +++ b/docs/articles/expensify-classic/travel/Configure-travel-policy-and-preferences.md @@ -72,6 +72,13 @@ Flight preferences include multiple sections with different settings: - **Maximum price:** Set a daily price cap per car (not including taxes and fees). - **Out of policy reason codes:** If enabled, travelers will be asked to enter a reason code for an out-of-policy car booking. This gives them a way to provide context for why the booking is still being placed. You can also modify the reason codes by clicking Manage reason codes below the toggle. +# Rail + +- **Maximum price:** Set a maximum price per booking or customise by rail trip duration. +- **Highest travel class:** Set a maximum travel class per booking or customise by rail trip duration. +- **Booking window:** Add a time limit to prevent employees from booking less than a certain number of days in advance to prohibit bookings too close to the journey time. +- **Out-of-policy reason code for rail:** If enabled, travelers will be asked to enter a reason code for an out-of-policy rail booking. This gives them a way to provide context for why the booking is still being placed. You can also modify the reason codes by clicking Manage reason codes below the toggle. + # FAQ How do travel policy rules interact with Expensify’s [approval flows](https://help.expensify.com/articles/expensify-classic/travel/Approve-travel-expenses)? diff --git a/docs/articles/expensify-classic/workspaces/Tax-Tracking.md b/docs/articles/expensify-classic/workspaces/Tax-Tracking.md deleted file mode 100644 index c47e5ed51f32..000000000000 --- a/docs/articles/expensify-classic/workspaces/Tax-Tracking.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: Tax -description: How to track expense taxes ---- -# Overview -Expensify’s tax tracking feature allows you to: -- Add tax names, rates, and codes whether you’re connected to an accounting system or not. -- Enable/disable taxes you’d like to make available to users. -- Set a default tax for Workspace currency expenses and, optionally, another default tax (including exempt) for foreign currency expenses which - will automatically apply to all new expenses. - -# How to Enable Tax Tracking -Tax tracking can be enabled in the Tax section of the Workspace settings of any Workspace, whether group or individual. -## If Connected to an Accounting Integration -If your group Workspace is connected to Xero, QuickBooks Online, Sage Intacct, or NetSuite, make sure to first enable tax via the connection configuration page (Settings > Workspaces > Group > [Workspace Name] > Connections > Configure) and then sync the connection. Your tax rates will be imported from the accounting system and indicated by its logo. -## Not Connected to an Accounting Integration -If your Workspace is not connected to an accounting system, go to Settings > Workspaces > Group > [Workspace Name] > Tax to enable tax. - -# Tracking Tax by Expense Category -To set a different tax rate for a specific expense type in the Workspace currency, go to Settings > Workspaces > Group > [Workspace Name] > Categories page. Click "Edit Rules" next to the desired category and set the "Category default tax". This will be applied to new expenses, overriding the default Workspace currency tax rate. diff --git a/docs/articles/expensify-classic/workspaces/Track-Taxes.md b/docs/articles/expensify-classic/workspaces/Track-Taxes.md new file mode 100644 index 000000000000..c75058dc8447 --- /dev/null +++ b/docs/articles/expensify-classic/workspaces/Track-Taxes.md @@ -0,0 +1,76 @@ +--- +title: Track Taxes +description: How to track taxes and apply them to expenses +--- +Expensify's tax tracking allows you to create tax rates and codes for domestic and foreign currencies, and even for different expense categories. Once you've enabled tax tracking, your default tax rate is automatically applied to all expenses. + +# Tax Tracking - Connected to an accounting integration + +If your Workspace is connected to Xero, QuickBooks Online, Sage Intacct, or NetSuite, you can run through the following steps to set up tax tracking: +1. Hover over **Settings**, then click **Workspaces**. +2. Click the desired workspace name. +3. Click the **Connections** tab on the left. +4. Click **Configure**. +5. Click **Sync Connection**. + +Your tax rates will be imported from the accounting system and indicated by its logo. + +# Tax Tracking - Not connected to an accounting integration + +If your Workspace is not connected to an accounting system, you can run through the following steps to set up tax tracking: +1. Hover over **Settings**, then click **Workspaces**. +2. Click the desired workspace name. +3. Click the **Tax** tab on the left. +4. Enable the toggle to allow taxes to be added to expenses. +5. You can modify the existing tax rate, or you can click New Option to add a new tax rate. For each tax rate, you can enable/disable them individually, add a specific name for the rate, add a percent value, and (if desired) add a unique tax code. +6. Once you have your tax codes added, go to the top of the screen to enter the name that taxes will appear as on expenses. You'll also select which of your tax rates you will use as your defaults for expenses submitted under your workspace currency and foreign currency. + +## Track tax by expense category + +You can also set tax rates for specific expense categories: +1. Hover over **Settings**, then click **Workspaces**. +2. Click the desired workspace name. +3. Click the **Categories** tab on the left. +4. Click **Edit** next to the desired category. +5. Click the Default Tax dropdown and select the desired tax rate. + +This rate will be applied to all new expenses under this category, overriding the workspace's default currency tax rate. + +{% include faq-begin.md %} + +## How do I set up multiple taxes (GST/PST/QST) for indirect connections? + +Expenses sometimes have more than one tax applied to them (for example in Canada, expenses can have both a Federal GST and a provincial PST or QST). + +To handle multiple tax rates, you can create a new tax rate that combines both into a single rate. For example, if you have a GST of 5% and PST of 7%, you can add them together and create a new tax rate of 12%. + +From the Reports page, you can generate a CSV containing all the expense information, including the split-out taxes, by going to the Reports tab, clicking **Export To**, and selecting **Tax Report**. + +## How do I handle the taxes for a receipt that includes more than one tax rate? + +If your receipt includes more than one tax rate, there are two ways you can handle the tax rate: + +- Many tax authorities do not require the reporting of tax amounts by rate; therefore, you can apply the highest rate on the receipt and then modify the tax amount on the receipt if necessary. Please check with your tax advisor to determine if this approach is appropriate for you. +- Alternatively, you can apply each specific tax rate by splitting the expense by the applicable expenses for each rate. To do this, open the expense and click **Split Expense**. Then apply the correct tax rate to each. + +## What if my workspace has multiple tax rates? + +You'll have the option to change the tax rate from within the expense as needed. + +## What should I do if the tax amount for my expense does not show up, or is it showing as a different amount than what I expected? + +In Expensify, tax is *inclusive*, meaning it's already part of the total amount shown. If the tax amount doesn't show up on your receipt or is different than the calculated amount, you can manually type in the correct tax amount. + +To determine the inclusive tax from a total price that already includes tax, you can use the following formula: + +**Tax amount = (Total price x Tax rate) ÷ (1 + Tax Rate)** + +For example, if an item costs $100 and the tax rate is 20%: +Tax amount = (**$100** x .20) ÷ (1 + .**20**) = **$16.67** +This means the tax amount of $16.67 is included in the total. + +If you are simply trying to calculate the price before tax, you can use the formula: + +**Price before tax = (Total price) ÷ (1 + Tax rate)** + +{% include faq-end.md %} diff --git a/docs/articles/new-expensify/billing-and-subscriptions/Tax-exempt.md b/docs/articles/new-expensify/billing-and-subscriptions/Tax-exempt.md new file mode 100644 index 000000000000..fa38741d3b97 --- /dev/null +++ b/docs/articles/new-expensify/billing-and-subscriptions/Tax-exempt.md @@ -0,0 +1,26 @@ +--- +title: Tax Exempt +description: Tax-exempt status in Expensify for organizations recognized by the IRS or local tax authorities. +--- + +# Overview +If your organization is recognized by the IRS or other local tax authorities as tax-exempt, that means you don’t need to pay any tax on your Expensify monthly bill. Please follow these instructions to request tax-exempt status. +# How to request tax-exempt status in Expensify +1. Go to **Settings > Subscription > Subscription details**. +2. Click **More** in the top right, then **Request tax exempt status**. +3. After you've requested tax-exempt status, Concierge (our support service) will start a conversation with you. They will ask you to upload a PDF of your tax-exempt documentation. This document should include your VAT number (or "RUT" in Chile). You can use one of the following documents: 501(c), ST-119, or a foreign tax-exempt declaration. +4. Our team will review your document and let you know if we need any more information. +5. Once everything is verified, we'll update your account accordingly. + +![Tap More and then Request tax exempt status]({{site.url}}/assets/images/Tax Exempt - New Expensify.png){:width="100%"} + +Once your account is marked as tax-exempt, the corresponding state tax will no longer be applied to future billing. + +If you need to remove your tax-exempt status, let your account manager know or contact Concierge. + +{% include faq-begin.md %} +## What happens to my past Expensify bills that incorrectly had tax added to them? +Expensify can provide a refund for the tax you were charged on your previous bills. Please let your Account Manager know or contact Concierge if this is the case. + +{% include faq-end.md %} + diff --git a/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md b/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md index ea058df9c1b1..1b1702c6fcc7 100644 --- a/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md +++ b/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md @@ -51,6 +51,11 @@ When an expense is submitted to a workspace, your approver will receive an email {% include end-selector.html %} +![Click Global Create]({{site.url}}/assets/images/ExpensifyHelp-CreateExpense-1.png){:width="100%"} +![Click Submit expense]({{site.url}}/assets/images/ExpensifyHelp-CreateExpense-2.png){:width="100%"} +![Click Scan]({{site.url}}/assets/images/ExpensifyHelp-CreateExpense-3.png){:width="100%"} +![Enter workspace or individual's name]({{site.url}}/assets/images/ExpensifyHelp-CreateExpense-4.png){:width="100%"} + {% include info.html %} You can also forward receipts to receipts@expensify.com using your primary or secondary email address. SmartScan will automatically extract all the details from the receipt and add them to your expenses. {% include end-info.html %} diff --git a/docs/articles/new-expensify/expenses-&-payments/Export-download-expenses.md b/docs/articles/new-expensify/expenses-&-payments/Export-download-expenses.md new file mode 100644 index 000000000000..8593ab65205b --- /dev/null +++ b/docs/articles/new-expensify/expenses-&-payments/Export-download-expenses.md @@ -0,0 +1,57 @@ +--- +title: Search and Download Expenses +description: Find expenses and export expense data to a CSV file +--- +
+ +Expensify allows you to export expense data to a downloaded CSV file, which you can then import into your favorite spreadsheet tool for deeper analysis. + +## Search Expenses + +The first step to exporting and downloading expenses is finding the data you need. + + 1. Click/tap the **Search** icon in the bottom left menu to go to the Search page + 2. Select the Expenses tab on the top left + 3. Select your Filters on the top right to filter by credit card used, coding, date range, keyword, expense value and a number of other useful criteria + 4. Hit View Results to see all expenses that match your filters + +## Download Expenses + + 1. Select the checkbox to the left of the expenses or select all with the very top checkbox. + 2. Click **# selected** at the top-right and select **Download**. + 3. This will save a CSV file to your default download folder with the file naming prefix _“Expensify.”_ + 4. You can now open in your favorite spreadsheet tool + +![Select the expenses to download]({{site.url}}/assets/images/search-download.png){:width="100%"} + +This file provides the following data for each expense: + - Date + - Merchant + - Description + - From + - To + - Category + GL code + - Tag + GL code + - Tax + Tax code + - Amount + - Currency + - Type (i.e. cash, card, distance) + - Receipt URL + +{% include faq-begin.md %} + +**Can I export in a different format, like PDF or XLS?** + +No, currently Expensify supports CSV export only. + +**Can I add columns to the CSV download to capture additional data points?** + +No, the CSV template cannot be customized. + +**Can I select expenses or reports in bulk?** + +Yes, you can select expenses or reports in bulk by selecting the **Select multiple** or **Select all** option. To display these options on the mobile app, simply long press an item. + +{% include faq-end.md %} + +
diff --git a/docs/articles/new-expensify/expenses-&-payments/Export-expenses.md b/docs/articles/new-expensify/expenses-&-payments/Export-expenses.md deleted file mode 100644 index f06c436449eb..000000000000 --- a/docs/articles/new-expensify/expenses-&-payments/Export-expenses.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: Export Expenses -description: Export expense data to a CSV file ---- -
- -Expensify allows you to export expense data to a CSV file that you can import into your favorite spreadsheet tool for deeper analysis. - -To export your expense data to a CSV, - - 1. Click the **[Search](https://new.expensify.com/search/all?sortBy=date&sortOrder=desc)** tab in the bottom left menu. - 2. Select the checkbox to the left of the expenses or reports you wish to export. - 3. Click **# selected** at the top-right and select **Download**. - -![Select the expenses to download]({{site.url}}/assets/images/Export-Expenses.png){:width="100%"} - -The CSV download will save locally to your device with the file naming prefix _“Expensify.”_ This file provides the following data for each expense: - - Date - - Merchant - - Description - - From - - To - - Category - - Tag - - Tax - - Amount - - Currency - - Type (i.e. cash, card, distance) - - Receipt URL - -{% include faq-begin.md %} - -**Can I export in a different format, like PDF or XLS?** - -No, currently Expensify supports CSV export only. - -**Can I add columns to the CSV download to capture additional data points?** - -No, the CSV template cannot be customized. - -**Can I select expenses or reports in bulk?** - -Yes, you can select expenses or reports in bulk by selecting the **Select multiple** or **Select all** option. To display these options on the mobile app, simply long press an item. - -{% include faq-end.md %} - -
diff --git a/docs/articles/new-expensify/getting-started/Join-your-company's-workspace.md b/docs/articles/new-expensify/getting-started/Join-your-company's-workspace.md index a4747c2d95e5..1e8ae38b3991 100644 --- a/docs/articles/new-expensify/getting-started/Join-your-company's-workspace.md +++ b/docs/articles/new-expensify/getting-started/Join-your-company's-workspace.md @@ -5,14 +5,33 @@ description: Get started with Expensify by joining your company's workspace
-Welcome to Expensify! If you received an invitation to join your company’s Expensify workspace, follow the 5 steps below to get started. +Welcome to Expensify! If you received an invitation to join your company’s Expensify workspace, follow the steps below to get started. -# 1. Download the mobile app +# 1. Access Expensify on your preferred device or browser -Upload your expenses and check your reports right from your phone by downloading the Expensify mobile app. You can search for “Expensify” in the app store, or tap one of the links below. +Get started by downloading Expensify mobile or desktop apps and ensure you’re using a supported web browser. Here’s how: -[iOS](https://apps.apple.com/us/app/expensify-expense-tracker/id471713959) -| [Android](https://play.google.com/store/apps/details?id=org.me.mobiexpensifyg&hl=en_US&gl=US) +## Download the Expensify Apps: + +- **Mobile Devices:** Download the Expensify app for [Android](https://play.google.com/store/apps/details?id=com.expensify.chat) or [iOS](https://apps.apple.com/us/app/expensify-cash/id1530278510). +- **Desktop Devices:** Download the Expensify app for [macOS](https://new.expensify.com/NewExpensify.dmg). + +## Use Expensify on the Web: + +Expensify is also accessible via the web and supports the following browsers: + +- [Google Chrome](https://www.google.com/chrome/) +- [Mozilla Firefox](https://www.mozilla.com/firefox) +- [Microsoft Edge](https://www.microsoft.com/edge) +- [Apple Safari](https://www.apple.com/safari) (Apple devices only) +- [Opera](https://www.opera.com/) +- [Microsoft Internet Explorer](https://www.microsoft.com/ie) (Version 11 only) + +{% include info.html %} +Note: Microsoft no longer provides support or security updates for Internet Explorer versions below 11. To avoid accessibility and security issues, we recommend updating your IE version or switching to a different browser. +{% include end-info.html %} + +By using a compatible device or browser, you’ll ensure the best experience with Expensify. # 2. Add your name, photo, and preferences diff --git a/docs/articles/new-expensify/travel/Configure-travel-policy-and-preferences.md b/docs/articles/new-expensify/travel/Configure-travel-policy-and-preferences.md index 16067624d720..237aad83169a 100644 --- a/docs/articles/new-expensify/travel/Configure-travel-policy-and-preferences.md +++ b/docs/articles/new-expensify/travel/Configure-travel-policy-and-preferences.md @@ -71,6 +71,13 @@ Flight preferences include multiple sections with different settings: - **Maximum price:** Set a daily price cap per car (not including taxes and fees). - **Out of policy reason codes:** If enabled, travelers will be asked to enter a reason code for an out-of-policy car booking. This gives them a way to provide context for why the booking is still being placed. You can also modify the reason codes by clicking Manage reason codes below the toggle. +# Rail + +- **Maximum price:** Set a maximum price per booking or customise by rail trip duration. +- **Highest travel class:** Set a maximum travel class per booking or customise by rail trip duration. +- **Booking window:** Add a time limit to prevent employees from booking less than a certain number of days in advance to prohibit bookings too close to the journey time. +- **Out-of-policy reason code for rail:** If enabled, travelers will be asked to enter a reason code for an out-of-policy rail booking. This gives them a way to provide context for why the booking is still being placed. You can also modify the reason codes by clicking Manage reason codes below the toggle. + # FAQ How do travel policy rules interact with Expensify’s [approval flows](https://help.expensify.com/articles/expensify-classic/travel/Approve-travel-expenses)? diff --git a/docs/assets/images/ExpensifyHelp-CreateExpense-1.png b/docs/assets/images/ExpensifyHelp-CreateExpense-1.png new file mode 100644 index 000000000000..7b6459440d5e Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-CreateExpense-1.png differ diff --git a/docs/assets/images/ExpensifyHelp-CreateExpense-2.png b/docs/assets/images/ExpensifyHelp-CreateExpense-2.png new file mode 100644 index 000000000000..65aaf8017a32 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-CreateExpense-2.png differ diff --git a/docs/assets/images/ExpensifyHelp-CreateExpense-3.png b/docs/assets/images/ExpensifyHelp-CreateExpense-3.png new file mode 100644 index 000000000000..0173de29d68d Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-CreateExpense-3.png differ diff --git a/docs/assets/images/ExpensifyHelp-CreateExpense-4.png b/docs/assets/images/ExpensifyHelp-CreateExpense-4.png new file mode 100644 index 000000000000..901d08f1771d Binary files /dev/null and b/docs/assets/images/ExpensifyHelp-CreateExpense-4.png differ diff --git a/docs/assets/images/Tax Exempt - Classic.png b/docs/assets/images/Tax Exempt - Classic.png new file mode 100644 index 000000000000..0987f5e4ca7d Binary files /dev/null and b/docs/assets/images/Tax Exempt - Classic.png differ diff --git a/docs/assets/images/Tax Exempt - New Expensify.png b/docs/assets/images/Tax Exempt - New Expensify.png new file mode 100644 index 000000000000..9ff6673da6b3 Binary files /dev/null and b/docs/assets/images/Tax Exempt - New Expensify.png differ diff --git a/docs/assets/images/search-hold-01.png b/docs/assets/images/search-hold-01.png new file mode 100644 index 000000000000..04745c570367 Binary files /dev/null and b/docs/assets/images/search-hold-01.png differ diff --git a/docs/assets/images/search-hold-02.png b/docs/assets/images/search-hold-02.png new file mode 100644 index 000000000000..3c7c39defd66 Binary files /dev/null and b/docs/assets/images/search-hold-02.png differ diff --git a/docs/assets/images/search-hold-03.png b/docs/assets/images/search-hold-03.png new file mode 100644 index 000000000000..81fbddcf5d75 Binary files /dev/null and b/docs/assets/images/search-hold-03.png differ diff --git a/docs/assets/images/search-hold-04.png b/docs/assets/images/search-hold-04.png new file mode 100644 index 000000000000..e5c1b71c0e37 Binary files /dev/null and b/docs/assets/images/search-hold-04.png differ diff --git a/docs/assets/images/search-hold-05.png b/docs/assets/images/search-hold-05.png new file mode 100644 index 000000000000..2d111abecb65 Binary files /dev/null and b/docs/assets/images/search-hold-05.png differ diff --git a/docs/assets/js/selector.js b/docs/assets/js/selector.js index 7373c7892767..cf13a643ce21 100644 --- a/docs/assets/js/selector.js +++ b/docs/assets/js/selector.js @@ -1,5 +1,6 @@ function syncSelectors(selectedIndex) { const allSelects = document.querySelectorAll('select'); + // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < allSelects.length; i++) { allSelects[i].selectedIndex = selectedIndex; } @@ -19,6 +20,7 @@ function selectOption(select) { allOptions.forEach((option) => { if (option.value === selectedValue) { const toShow = document.getElementsByClassName(option.value); + // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < toShow.length; i++) { toShow[i].classList.remove('hidden'); } @@ -26,6 +28,7 @@ function selectOption(select) { } const toHide = document.getElementsByClassName(option.value); + // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < toHide.length; i++) { toHide[i].classList.add('hidden'); } diff --git a/docs/redirects.csv b/docs/redirects.csv index ca73543098d2..04eba2e6152c 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -385,7 +385,7 @@ https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Vac https://community.expensify.com/discussion/5678/deep-dive-secondary-login-merge-accounts-what-does-this-mean,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Merge-accounts https://community.expensify.com/discussion/5103/how-to-create-and-use-custom-units/,https://help.expensify.com/ https://community.expensify.com/discussion/6530/how-to-set-your-time-zone-for-report-history-comments,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Set-time-zone -https://community.expensify.com/discussion/5651/deep-dive--practices-when-youre-running-into-trouble-receiving-emails-from-expensify,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Set-Notifications +https://community.expensify.com/discussion/5651/deep-dive-best-practices-when-youre-running-into-trouble-receiving-emails-from-expensify,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Set-Notifications https://community.expensify.com/discussion/5793/how-to-connect-your-personal-card-to-import-expenses/,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Personal-Credit-Cards https://community.expensify.com/discussion/5677/deep-dive-security-how-expensify-protects-your-information,https://help.expensify.com/articles/new-expensify/settings/Encryption-and-Data-Security https://community.expensify.com/discussion/4641/how-to-add-a-u-s-deposit-account,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-Personal-US-Bank-Account @@ -607,3 +607,6 @@ https://help.expensify.com/articles/expensify-classic/spending-insights/Other-Ex https://help.expensify.com/articles/expensify-classic/travel/Edit-or-cancel-travel-arrangements,https://help.expensify.com/articles/expensify-classic/travel/Book-with-Expensify-Travel https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-and-Pay-Bills https://help.expensify.com/articles/expensify-classic/settings/Set-Notifications,https://help.expensify.com/articles/expensify-classic/settings/Email-Notifications +https://help.expensify.com/articles/new-expensify/expenses-&-payments/Export-expenses,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Export-download-expenses +https://help.expensify.com/articles/expensify-classic/expenses/Apply-Tax,https://help.expensify.com/articles/expensify-classic/workspaces/Track-Taxes +https://help.expensify.com/articles/expensify-classic/workspaces/Tax-Tracking,https://help.expensify.com/articles/expensify-classic/workspaces/Track-Taxes \ No newline at end of file diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 8dbf67a150bd..798e328f73fa 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -71,9 +71,9 @@ platform :android do desc "Generate a production HybridApp AAB" lane :build_hybrid do - ENV["ENVFILE"]="../.env.production.hybridapp" + ENV["ENVFILE"]="Mobile-Expensify/.env.production.hybridapp" gradle( - project_dir: '../Android', + project_dir: 'Mobile-Expensify/Android', task: "bundleRelease", flags: "--refresh-dependencies", properties: { @@ -88,9 +88,9 @@ platform :android do desc "Generate AdHoc HybridApp apk" lane :build_adhoc_hybrid do - ENV["ENVFILE"]="../.env.adhoc.hybridapp" + ENV["ENVFILE"]="Mobile-Expensify/.env.adhoc.hybridapp" gradle( - project_dir: '../Android', + project_dir: 'Mobile-Expensify/Android', task: 'assembleAdhoc', properties: { "android.injected.signing.store.file" => './upload-key.keystore', @@ -118,7 +118,7 @@ platform :android do lane :build_local_hybrid do ENV["ENVFILE"]=".env.production" gradle( - project_dir: '../Android', + project_dir: 'Mobile-Expensify/Android', task: 'assemble', flavor: 'Production', build_type: 'Release', @@ -372,7 +372,7 @@ platform :ios do desc "Build an iOS HybridApp production build" lane :build_hybrid do - ENV["ENVFILE"]="../.env.production.hybridapp" + ENV["ENVFILE"]="Mobile-Expensify/.env.production.hybridapp" setupIOSSigningCertificate() @@ -389,7 +389,7 @@ platform :ios do ) build_app( - workspace: "../iOS/Expensify.xcworkspace", + workspace: "Mobile-Expensify/iOS/Expensify.xcworkspace", scheme: "Expensify", output_name: "Expensify.ipa", export_method: "app-store", @@ -406,6 +406,42 @@ platform :ios do setIOSBuildOutputsInEnv() end + desc "Build an iOS HybridApp Adhoc build" + lane :build_adhoc_hybrid do + ENV["ENVFILE"]="Mobile-Expensify/.env.adhoc.hybridapp" + + setupIOSSigningCertificate() + + install_provisioning_profile( + path: "./OldApp_AdHoc.mobileprovision" + ) + + install_provisioning_profile( + path: "./OldApp_AdHoc_Share_Extension.mobileprovision" + ) + + install_provisioning_profile( + path: "./OldApp_AdHoc_Notification_Service.mobileprovision" + ) + + build_app( + workspace: "Mobile-Expensify/iOS/Expensify.xcworkspace", + scheme: "Expensify", + output_name: "Expensify.ipa", + export_method: "app-store", + export_options: { + manageAppVersionAndBuildNumber: false, + provisioningProfiles: { + "com.expensify.expensifylite.adhoc" => "(OldApp) AppStore", + "com.expensify.expensifylite.adhoc.SmartScanExtension" => "(OldApp) AppStore: Share Extension", + "com.expensify.expensifylite.adhoc.NotificationServiceExtension" => "(OldApp) AppStore: Notification Service", + } + } + ) + + setIOSBuildOutputsInEnv() + end + desc "Build an unsigned iOS production build" lane :build_unsigned do ENV["ENVFILE"]=".env.production" @@ -418,9 +454,9 @@ platform :ios do desc "Build an unsigned iOS HybridApp production build" lane :build_unsigned_hybrid do - ENV["ENVFILE"]="../Mobile-Expensify/.env.production.hybridapp" + ENV["ENVFILE"]="./Mobile-Expensify/.env.production.hybridapp" build_app( - workspace: "../Mobile-Expensify/iOS/Expensify.xcworkspace", + workspace: "./Mobile-Expensify/iOS/Expensify.xcworkspace", scheme: "Expensify" ) setIOSBuildOutputsInEnv() @@ -537,7 +573,7 @@ platform :ios do dsym_path: ENV[KEY_DSYM_PATH], gsp_path: "./ios/GoogleService-Info.plist", # Assuming we are running this from the react-native submodule directory for HybridApp - binary_path: "../iOS/Pods/FirebaseCrashlytics/upload-symbols" + binary_path: "./Mobile-Expensify/iOS/Pods/FirebaseCrashlytics/upload-symbols" ) end diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index cd2598608a0f..3d9119add122 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -637,7 +637,6 @@ "${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", - "${PODS_ROOT}/../../node_modules/@expensify/react-native-live-markdown/parser/react-native-live-markdown-parser.js", "${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", @@ -658,7 +657,6 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/react-native-live-markdown-parser.js", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", @@ -843,7 +841,6 @@ "${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", - "${PODS_ROOT}/../../node_modules/@expensify/react-native-live-markdown/parser/react-native-live-markdown-parser.js", "${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", @@ -864,7 +861,6 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/react-native-live-markdown-parser.js", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 9d846dfbe5f6..6a6d5bef5d7d 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.72 + 9.0.78 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.72.1 + 9.0.78.1 FullStory OrgId @@ -67,6 +67,8 @@ NSCameraUsageDescription Your camera is used to create chat attachments, documents, and facial capture. + NSContactsUsageDescription + Import contacts from your phone so your favorite people are always a tap away. NSLocationAlwaysAndWhenInUseUsageDescription Your location is used to determine your default currency and timezone. NSLocationWhenInUseUsageDescription diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index f20e45286bed..c996883405b2 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.72 + 9.0.78 CFBundleSignature ???? CFBundleVersion - 9.0.72.1 + 9.0.78.1 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index e04c76769d99..4e4305e1cbab 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.72 + 9.0.78 CFBundleVersion - 9.0.72.1 + 9.0.78.1 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile b/ios/Podfile index 4d139711ef01..41dc5179752d 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -3,6 +3,7 @@ require File.join(File.dirname(`node --print "require.resolve('expo/package.json # This value is used by $RNMapboxMaps $RNMapboxMapsImpl = 'mapbox' $VCDisableFrameProcessors = true +ENV['PROJECT_ROOT_PATH'] = "./"; def node_require(script) # Resolve script with node to allow for hoisting @@ -82,6 +83,7 @@ target 'NewExpensify' do # ENV Variable enables/disables TurboModules ENV['RCT_NEW_ARCH_ENABLED'] = '1'; + use_react_native!( :path => config[:reactNativePath], # An absolute path to your application root. diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c8e92768eb9a..e9532fc1ae30 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1722,7 +1722,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-keyboard-controller (1.14.4): + - react-native-keyboard-controller (1.15.0): - DoubleConversion - glog - hermes-engine @@ -1981,8 +1981,27 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-view-shot (3.8.0): + - react-native-view-shot (4.0.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - react-native-webview (13.8.6): - DoubleConversion - glog @@ -2391,7 +2410,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.187): + - RNLiveMarkdown (0.1.209): - DoubleConversion - glog - hermes-engine @@ -2411,9 +2430,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/newarch (= 0.1.187) + - RNLiveMarkdown/newarch (= 0.1.209) + - RNReanimated/worklets - Yoga - - RNLiveMarkdown/newarch (0.1.187): + - RNLiveMarkdown/newarch (0.1.209): - DoubleConversion - glog - hermes-engine @@ -2433,6 +2453,7 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core + - RNReanimated/worklets - Yoga - RNLocalize (2.2.6): - React-Core @@ -2503,7 +2524,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNReanimated (3.16.3): + - RNReanimated (3.16.4): - DoubleConversion - glog - hermes-engine @@ -2523,10 +2544,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated (= 3.16.3) - - RNReanimated/worklets (= 3.16.3) + - RNReanimated/reanimated (= 3.16.4) + - RNReanimated/worklets (= 3.16.4) - Yoga - - RNReanimated/reanimated (3.16.3): + - RNReanimated/reanimated (3.16.4): - DoubleConversion - glog - hermes-engine @@ -2546,9 +2567,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated/apple (= 3.16.3) + - RNReanimated/reanimated/apple (= 3.16.4) - Yoga - - RNReanimated/reanimated/apple (3.16.3): + - RNReanimated/reanimated/apple (3.16.4): - DoubleConversion - glog - hermes-engine @@ -2569,7 +2590,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNReanimated/worklets (3.16.3): + - RNReanimated/worklets (3.16.4): - DoubleConversion - glog - hermes-engine @@ -3221,7 +3242,7 @@ SPEC CHECKSUMS: react-native-geolocation: b9bd12beaf0ebca61a01514517ca8455bd26fa06 react-native-image-picker: f8a13ff106bcc7eb00c71ce11fdc36aac2a44440 react-native-key-command: aae312752fcdfaa2240be9a015fc41ce54087546 - react-native-keyboard-controller: 97bb7b48fa427c7455afdc8870c2978efd9bfa3a + react-native-keyboard-controller: 3428e4761623fd6a242d9bf3573112f8ebe92238 react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d react-native-netinfo: fb5112b1fa754975485884ae85a3fb6a684f49d5 react-native-pager-view: abc5ef92699233eb726442c7f452cac82f73d0cb @@ -3231,7 +3252,7 @@ SPEC CHECKSUMS: react-native-quick-sqlite: 7c793c9f5834e756b336257a8d8b8239b7ceb451 react-native-release-profiler: 131ec5e4145d900b2be2a8d6641e2ce0dd784259 react-native-safe-area-context: 38fdd9b3c5561de7cabae64bd0cd2ce05d2768a1 - react-native-view-shot: 6b7ed61d77d88580fed10954d45fad0eb2d47688 + react-native-view-shot: 6bafd491eb295b5834e05c469a37ecbd796d5b22 react-native-webview: ad29375839c9aa0409ce8e8693291b42bdc067a4 React-nativeconfig: 57781b79e11d5af7573e6f77cbf1143b71802a6d React-NativeModulesApple: 7ff2e2cfb2e5fa5bdedcecf28ce37e696c6ef1e1 @@ -3271,12 +3292,12 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 8781e2529230a1bc3ea8d75e5c3cd071b6c6aed7 RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: 8338447b39fcd86596c74b9e0e9509e365a2dd3b + RNLiveMarkdown: f19d3c962fba4fb87bb9bc27ce9119216d86d92e RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: 460d6ff97ae49c7d5708c3212c6521697c36a0c4 RNPermissions: 0b1429b55af59d1d08b75a8be2459f65a8ac3f28 RNReactNativeHapticFeedback: 73756a3477a5a622fa16862a3ab0d0fc5e5edff5 - RNReanimated: 03ba2447d5a7789e2843df2ee05108d93b6441d6 + RNReanimated: d95f865e1e42c34ca56b987e0719a8c72fc02dbc RNScreens: de6e57426ba0e6cbc3fb5b4f496e7f08cb2773c2 RNShare: bd4fe9b95d1ee89a200778cc0753ebe650154bb0 RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 @@ -3290,6 +3311,6 @@ SPEC CHECKSUMS: VisionCamera: c95a8ad535f527562be1fb05fb2fd324578e769c Yoga: a1d7895431387402a674fd0d1c04ec85e87909b8 -PODFILE CHECKSUM: 15e2f095b9c80d658459723edf84005a6867debf +PODFILE CHECKSUM: 615266329434ea4a994dccf622008a2197313c88 COCOAPODS: 1.15.2 diff --git a/metro.config.js b/metro.config.js index c6e4ba6bb4ec..98bea7be80ed 100644 --- a/metro.config.js +++ b/metro.config.js @@ -4,6 +4,7 @@ const {getDefaultConfig: getReactNativeDefaultConfig} = require('@react-native/m const {mergeConfig} = require('@react-native/metro-config'); const defaultAssetExts = require('metro-config/src/defaults/defaults').assetExts; const defaultSourceExts = require('metro-config/src/defaults/defaults').sourceExts; +const {wrapWithReanimatedMetroConfig} = require('react-native-reanimated/metro-config'); require('dotenv').config(); const defaultConfig = getReactNativeDefaultConfig(__dirname); @@ -26,4 +27,4 @@ const config = { }, }; -module.exports = mergeConfig(defaultConfig, expoConfig, config); +module.exports = wrapWithReanimatedMetroConfig(mergeConfig(defaultConfig, expoConfig, config)); diff --git a/package-lock.json b/package-lock.json index d1f91d23d60f..7b865a4bb49c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "new.expensify", - "version": "9.0.72-1", + "version": "9.0.78-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.72-1", + "version": "9.0.78-1", "hasInstallScript": true, "license": "MIT", "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.187", + "@expensify/react-native-live-markdown": "0.1.209", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -51,7 +51,7 @@ "date-fns-tz": "^3.2.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.106", + "expensify-common": "2.0.109", "expo": "51.0.31", "expo-av": "14.0.7", "expo-image": "1.12.15", @@ -73,7 +73,7 @@ "react-content-loader": "^7.0.0", "react-dom": "18.3.1", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "1.0.20", + "react-fast-pdf": "1.0.21", "react-map-gl": "^7.1.3", "react-native": "0.75.2", "react-native-android-location-enabler": "^2.0.1", @@ -91,11 +91,11 @@ "react-native-image-picker": "^7.0.3", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#cb392140db4953a283590d7cf93b4d0461baa2a9", "react-native-key-command": "^1.0.8", - "react-native-keyboard-controller": "1.14.4", + "react-native-keyboard-controller": "1.15.0", "react-native-launch-arguments": "^4.0.2", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.82", + "react-native-onyx": "2.0.86", "react-native-pager-view": "6.5.1", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -104,7 +104,7 @@ "react-native-plaid-link-sdk": "11.11.0", "react-native-qrcode-svg": "6.3.11", "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-nitro-sqlite#99f34ebefa91698945f3ed26622e002bd79489e0", - "react-native-reanimated": "3.16.3", + "react-native-reanimated": "3.16.4", "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.10.9", @@ -114,7 +114,7 @@ "react-native-svg": "15.9.0", "react-native-tab-view": "^3.5.2", "react-native-url-polyfill": "^2.0.0", - "react-native-view-shot": "3.8.0", + "react-native-view-shot": "4.0.0", "react-native-vision-camera": "^4.6.1", "react-native-web": "0.19.13", "react-native-webview": "13.8.6", @@ -164,9 +164,9 @@ "@storybook/addon-a11y": "^8.1.10", "@storybook/addon-essentials": "^8.1.10", "@storybook/addon-webpack5-compiler-babel": "^3.0.3", - "@storybook/cli": "^8.3.0", + "@storybook/cli": "^8.4.0", "@storybook/react": "^8.1.10", - "@storybook/react-webpack5": "^8.1.6", + "@storybook/react-webpack5": "^8.4.0", "@storybook/theming": "^8.1.10", "@svgr/webpack": "^6.0.0", "@testing-library/jest-native": "5.4.1", @@ -219,7 +219,7 @@ "electron-builder": "25.0.0", "eslint": "^8.57.0", "eslint-config-airbnb-typescript": "^18.0.0", - "eslint-config-expensify": "^2.0.74", + "eslint-config-expensify": "2.0.75", "eslint-config-prettier": "^9.1.0", "eslint-plugin-deprecation": "^3.0.0", "eslint-plugin-jest": "^28.6.0", @@ -260,7 +260,7 @@ "setimmediate": "^1.0.5", "shellcheck": "^1.1.0", "source-map": "^0.7.4", - "storybook": "^8.3.0", + "storybook": "^8.4.0", "style-loader": "^2.0.0", "time-analytics-webpack-plugin": "^0.1.17", "ts-jest": "^29.1.2", @@ -2706,11 +2706,6 @@ "node": ">=6.9.0" } }, - "node_modules/@base2/pretty-print-object": { - "version": "1.0.1", - "dev": true, - "license": "BSD-2-Clause" - }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "dev": true, @@ -3503,12 +3498,12 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.187", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.187.tgz", - "integrity": "sha512-bw+dfhRN31u2xfG8LCI3e28g5EG/BfkyX1EqjPBRQlDZo4fZsdA61UFW6P8Y4rHlqspjYXJ0vk4ctECRWYl4Yg==", + "version": "0.1.209", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.209.tgz", + "integrity": "sha512-u+RRY+Jog/llEu9T1v0okSLgRhG5jGlX9H1Je0A8HWv0439XFLnAWSvN2eQ2T7bvT8Yjdj5CcC0hkgJiB9oCQw==", + "hasInstallScript": true, "license": "MIT", "workspaces": [ - "./parser", "./example", "./WebExample" ], @@ -3516,8 +3511,10 @@ "node": ">= 18.0.0" }, "peerDependencies": { + "expensify-common": ">=2.0.108", "react": "*", - "react-native": "*" + "react-native": "*", + "react-native-reanimated": ">=3.16.4" } }, "node_modules/@expo/bunyan": { @@ -6468,7 +6465,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -7248,18 +7247,6 @@ } } }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/schema-utils": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", @@ -10502,18 +10489,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@storybook/addon-essentials/node_modules/util": { - "version": "0.12.5", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, "node_modules/@storybook/addon-highlight": { "version": "8.1.10", "dev": true, @@ -11200,32 +11175,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@storybook/blocks/node_modules/util": { - "version": "0.12.5", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, "node_modules/@storybook/builder-webpack5": { - "version": "8.1.6", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-8.4.0.tgz", + "integrity": "sha512-NVPEB31x1LU73ghgPaynY603Pi0MKPlM/YovevlwZtTIU9st+DSEss1qSjC0As2Lq/bHZTJu+jhTCIB76MK7wQ==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/channels": "8.1.6", - "@storybook/client-logger": "8.1.6", - "@storybook/core-common": "8.1.6", - "@storybook/core-events": "8.1.6", - "@storybook/core-webpack": "8.1.6", - "@storybook/node-logger": "8.1.6", - "@storybook/preview": "8.1.6", - "@storybook/preview-api": "8.1.6", - "@types/node": "^18.0.0", + "@storybook/core-webpack": "8.4.0", + "@types/node": "^22.0.0", "@types/semver": "^7.3.4", "browser-assert": "^1.2.1", "case-sensitive-paths-webpack-plugin": "^2.4.0", @@ -11233,9 +11191,7 @@ "constants-browserify": "^1.0.0", "css-loader": "^6.7.1", "es-module-lexer": "^1.5.0", - "express": "^4.17.3", "fork-ts-checker-webpack-plugin": "^8.0.0", - "fs-extra": "^11.1.0", "html-webpack-plugin": "^5.5.0", "magic-string": "^0.30.5", "path-browserify": "^1.0.1", @@ -11250,12 +11206,15 @@ "webpack": "5", "webpack-dev-middleware": "^6.1.2", "webpack-hot-middleware": "^2.25.1", - "webpack-virtual-modules": "^0.5.0" + "webpack-virtual-modules": "^0.6.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, + "peerDependencies": { + "storybook": "^8.4.0" + }, "peerDependenciesMeta": { "typescript": { "optional": true @@ -11263,33 +11222,19 @@ } }, "node_modules/@storybook/builder-webpack5/node_modules/@types/node": { - "version": "18.19.34", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@storybook/builder-webpack5/node_modules/fs-extra": { - "version": "11.2.0", + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", "dev": true, "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" + "undici-types": "~6.20.0" } }, - "node_modules/@storybook/builder-webpack5/node_modules/path-browserify": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, "node_modules/@storybook/builder-webpack5/node_modules/style-loader": { "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", "dev": true, "license": "MIT", "engines": { @@ -11303,53 +11248,30 @@ "webpack": "^5.0.0" } }, - "node_modules/@storybook/builder-webpack5/node_modules/util": { - "version": "0.12.5", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/@storybook/channels": { - "version": "8.1.6", + "node_modules/@storybook/builder-webpack5/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/client-logger": "8.1.6", - "@storybook/core-events": "8.1.6", - "@storybook/global": "^5.0.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } + "license": "MIT" }, "node_modules/@storybook/cli": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@storybook/cli/-/cli-8.3.0.tgz", - "integrity": "sha512-kR2x43BU/keIUPr+jHXK16BkhUXk+t4I6DgYgKyjYfFpjX2+tNYZ2b1f7RW+TjjUy4V6cf9FXl5N+GFmih8oiQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@storybook/cli/-/cli-8.4.0.tgz", + "integrity": "sha512-jiQ5/9KEl2Rd9jf4AmNwCQitHkmH3chO+yWjQJFYb6n/47GpWNGnUtUkTJbgxPsmt/b3CpwkjimusEucE/xD6g==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.24.4", "@babel/types": "^7.24.0", - "@storybook/codemod": "8.3.0", + "@storybook/codemod": "8.4.0", "@types/semver": "^7.3.4", - "chalk": "^4.1.0", "commander": "^12.1.0", - "create-storybook": "8.3.0", + "create-storybook": "8.4.0", "cross-spawn": "^7.0.3", "envinfo": "^7.7.3", "fd-package-json": "^1.2.0", "find-up": "^5.0.0", - "fs-extra": "^11.1.0", "giget": "^1.0.0", "glob": "^10.0.0", "globby": "^14.0.1", @@ -11357,7 +11279,7 @@ "leven": "^3.1.0", "prompts": "^2.4.0", "semver": "^7.3.7", - "storybook": "8.3.0", + "storybook": "8.4.0", "tiny-invariant": "^1.3.1", "ts-dedent": "^2.0.0" }, @@ -11432,21 +11354,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@storybook/cli/node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/@storybook/cli/node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -11647,35 +11554,23 @@ "node": ">=8" } }, - "node_modules/@storybook/client-logger": { - "version": "8.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, "node_modules/@storybook/codemod": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-8.3.0.tgz", - "integrity": "sha512-WwHgQLJw02eflkAzkUfuNP8Hu7Z12E6diUN2AWDXVYZJXyJjYhivGzONt2inrHhT3LTB9iSNVo0WsDE9AZU9RA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-8.4.0.tgz", + "integrity": "sha512-H3hEsFc02e9ce+IhDXblFTD5IvPxL6uejJfasPzO0TER5FUaVc15coJtJ7Qk4LzhPM4M5thDqLrfPJ8HK6WZRA==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.24.4", "@babel/preset-env": "^7.24.4", "@babel/types": "^7.24.0", - "@storybook/core": "8.3.0", + "@storybook/core": "8.4.0", "@storybook/csf": "^0.1.11", "@types/cross-spawn": "^6.0.2", "cross-spawn": "^7.0.3", + "es-toolkit": "^1.22.0", "globby": "^14.0.1", "jscodeshift": "^0.15.1", - "lodash": "^4.17.21", "prettier": "^3.1.1", "recast": "^0.23.5", "tiny-invariant": "^1.3.1" @@ -11823,9 +11718,9 @@ } }, "node_modules/@storybook/codemod/node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, "license": "MIT", "bin": { @@ -11972,64 +11867,24 @@ } }, "node_modules/@storybook/core": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@storybook/core/-/core-8.3.0.tgz", - "integrity": "sha512-UeErpD0xRIP2nFA2TjPYxtEyv24O6VRfq2XXU5ki2QPYnxOxAPBbrMHCADjgBwNS4S2NUWTaVBYxybISVbrj+w==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@storybook/core/-/core-8.4.0.tgz", + "integrity": "sha512-RlvkBNPPLbHtJQ5M3SKfLLtn5GssRBOLBbJLJf8HjraeDI+YRt+J9FVXqNa9aHhOGoxam+hFinmuy9gyMbPW1A==", "dev": true, "license": "MIT", "dependencies": { "@storybook/csf": "^0.1.11", - "@types/express": "^4.17.21", + "better-opn": "^3.0.2", "browser-assert": "^1.2.1", - "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0", + "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0", "esbuild-register": "^3.5.0", - "express": "^4.19.2", + "jsdoc-type-pratt-parser": "^4.0.0", "process": "^0.11.10", "recast": "^0.23.5", "semver": "^7.6.2", "util": "^0.12.5", "ws": "^8.2.3" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-common": { - "version": "8.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/core-events": "8.1.6", - "@storybook/csf-tools": "8.1.6", - "@storybook/node-logger": "8.1.6", - "@storybook/types": "8.1.6", - "@yarnpkg/fslib": "2.10.3", - "@yarnpkg/libzip": "2.3.0", - "chalk": "^4.1.0", - "cross-spawn": "^7.0.3", - "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0", - "esbuild-register": "^3.5.0", - "execa": "^5.0.0", - "file-system-cache": "2.3.0", - "find-cache-dir": "^3.0.0", - "find-up": "^5.0.0", - "fs-extra": "^11.1.0", - "glob": "^10.0.0", - "handlebars": "^4.7.7", - "lazy-universal-dotenv": "^4.0.0", - "node-fetch": "^2.0.0", - "picomatch": "^2.3.0", - "pkg-dir": "^5.0.0", - "prettier-fallback": "npm:prettier@^3", - "pretty-hrtime": "^1.0.3", - "resolve-from": "^5.0.0", - "semver": "^7.3.7", - "tempy": "^3.1.0", - "tiny-invariant": "^1.3.1", - "ts-dedent": "^2.0.0", - "util": "^0.12.4" - }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" @@ -12043,374 +11898,41 @@ } } }, - "node_modules/@storybook/core-common/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/@storybook/core-common/node_modules/brace-expansion": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@storybook/core-common/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/@storybook/core-common/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/@storybook/core-common/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@storybook/core-common/node_modules/crypto-random-string": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^1.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/core-common/node_modules/crypto-random-string/node_modules/type-fest": { - "version": "1.4.0", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/core-common/node_modules/find-cache-dir": { - "version": "3.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "node_modules/@storybook/core-common/node_modules/find-cache-dir/node_modules/find-up": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/core-common/node_modules/find-cache-dir/node_modules/pkg-dir": { - "version": "4.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/core-common/node_modules/fs-extra": { - "version": "11.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@storybook/core-common/node_modules/glob": { - "version": "10.3.12", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.10.2" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@storybook/core-common/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/core-common/node_modules/is-stream": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/core-common/node_modules/locate-path": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/core-common/node_modules/make-dir": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/core-common/node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@storybook/core-common/node_modules/minimatch": { - "version": "9.0.4", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@storybook/core-common/node_modules/minipass": { - "version": "7.1.2", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/@storybook/core-common/node_modules/p-limit": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/core-common/node_modules/p-locate": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/core-common/node_modules/path-exists": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/core-common/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/core-common/node_modules/temp-dir": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - } - }, - "node_modules/@storybook/core-common/node_modules/tempy": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-stream": "^3.0.0", - "temp-dir": "^3.0.0", - "type-fest": "^2.12.2", - "unique-string": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/core-common/node_modules/type-fest": { - "version": "2.19.0", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/core-common/node_modules/unique-string": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "crypto-random-string": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/core-common/node_modules/util": { - "version": "0.12.5", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/@storybook/core-events": { - "version": "8.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/csf": "^0.1.7", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, "node_modules/@storybook/core-webpack": { - "version": "8.1.6", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-8.4.0.tgz", + "integrity": "sha512-14UnJ7zFSLEyaBvYe7+K1t/TWJc41KxstMHgVxHyE6TDy9MGi+GLfmq2xB5OIVE4nxtjSon3tIOf/hVBrtbt0A==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/core-common": "8.1.6", - "@storybook/node-logger": "8.1.6", - "@storybook/types": "8.1.6", - "@types/node": "^18.0.0", + "@types/node": "^22.0.0", "ts-dedent": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.4.0" } }, "node_modules/@storybook/core-webpack/node_modules/@types/node": { - "version": "18.19.34", + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, + "node_modules/@storybook/core-webpack/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "node_modules/@storybook/core/node_modules/recast": { "version": "0.23.9", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.9.tgz", @@ -12433,6 +11955,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -12450,20 +11973,6 @@ "node": ">=0.10.0" } }, - "node_modules/@storybook/core/node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, "node_modules/@storybook/csf": { "version": "0.1.11", "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.1.11.tgz", @@ -12618,82 +12127,6 @@ "node": ">=0.10.0" } }, - "node_modules/@storybook/csf-tools": { - "version": "8.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/generator": "^7.24.4", - "@babel/parser": "^7.24.4", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0", - "@storybook/csf": "^0.1.7", - "@storybook/types": "8.1.6", - "fs-extra": "^11.1.0", - "recast": "^0.23.5", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/csf-tools/node_modules/@babel/traverse": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@storybook/csf-tools/node_modules/fs-extra": { - "version": "11.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@storybook/csf-tools/node_modules/recast": { - "version": "0.23.9", - "dev": true, - "license": "MIT", - "dependencies": { - "ast-types": "^0.16.1", - "esprima": "~4.0.0", - "source-map": "~0.6.1", - "tiny-invariant": "^1.3.3", - "tslib": "^2.0.1" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/@storybook/csf-tools/node_modules/source-map": { - "version": "0.6.1", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@storybook/csf/node_modules/type-fest": { "version": "2.19.0", "dev": true, @@ -12705,49 +12138,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@storybook/docs-tools": { - "version": "8.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/core-common": "8.1.6", - "@storybook/core-events": "8.1.6", - "@storybook/preview-api": "8.1.6", - "@storybook/types": "8.1.6", - "@types/doctrine": "^0.0.3", - "assert": "^2.1.0", - "doctrine": "^3.0.0", - "lodash": "^4.17.21" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/docs-tools/node_modules/assert": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "is-nan": "^1.3.2", - "object-is": "^1.1.5", - "object.assign": "^4.1.4", - "util": "^0.12.5" - } - }, - "node_modules/@storybook/docs-tools/node_modules/util": { - "version": "0.12.5", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, "node_modules/@storybook/global": { "version": "5.0.0", "dev": true, @@ -12846,29 +12236,19 @@ "url": "https://opencollective.com/storybook" } }, - "node_modules/@storybook/node-logger": { - "version": "8.1.6", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, "node_modules/@storybook/preset-react-webpack": { - "version": "8.1.6", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-8.4.0.tgz", + "integrity": "sha512-me5gqQqfU/jxQMJJljdID3lbKH2RGvdgxVwLhvrUSmEhimcuWXgJxvxE4hHGbUiYcwiM/xmQLrf286/B3agN7w==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/core-webpack": "8.1.6", - "@storybook/docs-tools": "8.1.6", - "@storybook/node-logger": "8.1.6", - "@storybook/react": "8.1.6", + "@storybook/core-webpack": "8.4.0", + "@storybook/react": "8.4.0", "@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0", - "@types/node": "^18.0.0", + "@types/node": "^22.0.0", "@types/semver": "^7.3.4", "find-up": "^5.0.0", - "fs-extra": "^11.1.0", "magic-string": "^0.30.5", "react-docgen": "^7.0.0", "resolve": "^1.22.8", @@ -12883,54 +12263,10 @@ "type": "opencollective", "url": "https://opencollective.com/storybook" }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@storybook/preset-react-webpack/node_modules/@storybook/react": { - "version": "8.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/client-logger": "8.1.6", - "@storybook/docs-tools": "8.1.6", - "@storybook/global": "^5.0.0", - "@storybook/preview-api": "8.1.6", - "@storybook/react-dom-shim": "8.1.6", - "@storybook/types": "8.1.6", - "@types/escodegen": "^0.0.6", - "@types/estree": "^0.0.51", - "@types/node": "^18.0.0", - "acorn": "^7.4.1", - "acorn-jsx": "^5.3.1", - "acorn-walk": "^7.2.0", - "escodegen": "^2.1.0", - "html-tags": "^3.1.0", - "lodash": "^4.17.21", - "prop-types": "^15.7.2", - "react-element-to-jsx-string": "^15.0.0", - "semver": "^7.3.7", - "ts-dedent": "^2.0.0", - "type-fest": "~2.19", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "typescript": ">= 4.2.x" + "storybook": "^8.4.0" }, "peerDependenciesMeta": { "typescript": { @@ -12938,111 +12274,50 @@ } } }, - "node_modules/@storybook/preset-react-webpack/node_modules/@storybook/react-dom-shim": { - "version": "8.1.6", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta" - } - }, "node_modules/@storybook/preset-react-webpack/node_modules/@types/node": { - "version": "18.19.30", + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, - "node_modules/@storybook/preset-react-webpack/node_modules/fs-extra": { - "version": "11.2.0", + "node_modules/@storybook/preset-react-webpack/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@storybook/preset-react-webpack/node_modules/type-fest": { - "version": "2.19.0", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/preview": { - "version": "8.1.6", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } + "license": "MIT" }, "node_modules/@storybook/preview-api": { - "version": "8.1.6", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.4.7.tgz", + "integrity": "sha512-0QVQwHw+OyZGHAJEXo6Knx+6/4er7n2rTDE5RYJ9F2E2Lg42E19pfdLlq2Jhoods2Xrclo3wj6GWR//Ahi39Eg==", "dev": true, "license": "MIT", - "dependencies": { - "@storybook/channels": "8.1.6", - "@storybook/client-logger": "8.1.6", - "@storybook/core-events": "8.1.6", - "@storybook/csf": "^0.1.7", - "@storybook/global": "^5.0.0", - "@storybook/types": "8.1.6", - "@types/qs": "^6.9.5", - "dequal": "^2.0.2", - "lodash": "^4.17.21", - "memoizerific": "^1.11.3", - "qs": "^6.10.0", - "tiny-invariant": "^1.3.1", - "ts-dedent": "^2.0.0", - "util-deprecate": "^1.0.2" - }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, "node_modules/@storybook/react": { - "version": "8.1.10", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-8.4.0.tgz", + "integrity": "sha512-jB7SNGdxFHFR9GgAPjrUUigE0pgOy3Bv3MaR9VdSGOZOnP+mjvZAO+ItPeKWHcQ7JnNujjtmMa2A80YcBfqBzQ==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/client-logger": "8.1.10", - "@storybook/docs-tools": "8.1.10", + "@storybook/components": "^8.4.0", "@storybook/global": "^5.0.0", - "@storybook/preview-api": "8.1.10", - "@storybook/react-dom-shim": "8.1.10", - "@storybook/types": "8.1.10", - "@types/escodegen": "^0.0.6", - "@types/estree": "^0.0.51", - "@types/node": "^18.0.0", - "acorn": "^7.4.1", - "acorn-jsx": "^5.3.1", - "acorn-walk": "^7.2.0", - "escodegen": "^2.1.0", - "html-tags": "^3.1.0", - "lodash": "^4.17.21", - "prop-types": "^15.7.2", - "react-element-to-jsx-string": "^15.0.0", - "semver": "^7.3.7", - "ts-dedent": "^2.0.0", - "type-fest": "~2.19", - "util-deprecate": "^1.0.2" + "@storybook/manager-api": "^8.4.0", + "@storybook/preview-api": "^8.4.0", + "@storybook/react-dom-shim": "8.4.0", + "@storybook/theming": "^8.4.0" }, "engines": { "node": ">=18.0.0" @@ -13052,11 +12327,16 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { + "@storybook/test": "8.4.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^8.4.0", "typescript": ">= 4.2.x" }, "peerDependenciesMeta": { + "@storybook/test": { + "optional": true + }, "typescript": { "optional": true } @@ -13064,6 +12344,8 @@ }, "node_modules/@storybook/react-docgen-typescript-plugin": { "version": "1.0.6--canary.9.0c3f3b7.0", + "resolved": "https://registry.npmjs.org/@storybook/react-docgen-typescript-plugin/-/react-docgen-typescript-plugin-1.0.6--canary.9.0c3f3b7.0.tgz", + "integrity": "sha512-KUqXC3oa9JuQ0kZJLBhVdS4lOneKTOopnNBK4tUAgoxWQ3u/IjzdueZjFr7gyBrXMoU6duutk3RQR9u8ZpYJ4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -13082,6 +12364,8 @@ }, "node_modules/@storybook/react-docgen-typescript-plugin/node_modules/find-cache-dir": { "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", "dev": true, "license": "MIT", "dependencies": { @@ -13098,6 +12382,8 @@ }, "node_modules/@storybook/react-docgen-typescript-plugin/node_modules/find-up": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "license": "MIT", "dependencies": { @@ -13110,6 +12396,8 @@ }, "node_modules/@storybook/react-docgen-typescript-plugin/node_modules/locate-path": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "license": "MIT", "dependencies": { @@ -13121,6 +12409,8 @@ }, "node_modules/@storybook/react-docgen-typescript-plugin/node_modules/make-dir": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dev": true, "license": "MIT", "dependencies": { @@ -13135,6 +12425,8 @@ }, "node_modules/@storybook/react-docgen-typescript-plugin/node_modules/p-limit": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "license": "MIT", "dependencies": { @@ -13149,6 +12441,8 @@ }, "node_modules/@storybook/react-docgen-typescript-plugin/node_modules/p-locate": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "license": "MIT", "dependencies": { @@ -13160,6 +12454,8 @@ }, "node_modules/@storybook/react-docgen-typescript-plugin/node_modules/path-exists": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", "engines": { @@ -13168,6 +12464,8 @@ }, "node_modules/@storybook/react-docgen-typescript-plugin/node_modules/pkg-dir": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13179,6 +12477,8 @@ }, "node_modules/@storybook/react-docgen-typescript-plugin/node_modules/semver": { "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -13199,15 +12499,16 @@ } }, "node_modules/@storybook/react-webpack5": { - "version": "8.1.6", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-8.4.0.tgz", + "integrity": "sha512-hhfXKVMwpidwYJIT3HL2YXB12sEfPOvKHVT0w9LgVVYebj5B5ClZ9jwwZFYaqeFQH2QlZb01RlScFPjwe9tqpg==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/builder-webpack5": "8.1.6", - "@storybook/preset-react-webpack": "8.1.6", - "@storybook/react": "8.1.6", - "@storybook/types": "8.1.6", - "@types/node": "^18.0.0" + "@storybook/builder-webpack5": "8.4.0", + "@storybook/preset-react-webpack": "8.4.0", + "@storybook/react": "8.4.0", + "@types/node": "^22.0.0" }, "engines": { "node": ">=18.0.0" @@ -13219,6 +12520,7 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^8.4.0", "typescript": ">= 4.2.x" }, "peerDependenciesMeta": { @@ -13227,669 +12529,79 @@ } } }, - "node_modules/@storybook/react-webpack5/node_modules/@storybook/react": { - "version": "8.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/client-logger": "8.1.6", - "@storybook/docs-tools": "8.1.6", - "@storybook/global": "^5.0.0", - "@storybook/preview-api": "8.1.6", - "@storybook/react-dom-shim": "8.1.6", - "@storybook/types": "8.1.6", - "@types/escodegen": "^0.0.6", - "@types/estree": "^0.0.51", - "@types/node": "^18.0.0", - "acorn": "^7.4.1", - "acorn-jsx": "^5.3.1", - "acorn-walk": "^7.2.0", - "escodegen": "^2.1.0", - "html-tags": "^3.1.0", - "lodash": "^4.17.21", - "prop-types": "^15.7.2", - "react-element-to-jsx-string": "^15.0.0", - "semver": "^7.3.7", - "ts-dedent": "^2.0.0", - "type-fest": "~2.19", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "typescript": ">= 4.2.x" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@storybook/react-webpack5/node_modules/@storybook/react-dom-shim": { - "version": "8.1.6", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta" - } - }, "node_modules/@storybook/react-webpack5/node_modules/@types/node": { - "version": "18.19.28", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@storybook/react-webpack5/node_modules/type-fest": { - "version": "2.19.0", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/react/node_modules/@babel/traverse": { - "version": "7.24.7", + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@storybook/react/node_modules/@storybook/channels": { - "version": "8.1.10", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/client-logger": "8.1.10", - "@storybook/core-events": "8.1.10", - "@storybook/global": "^5.0.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" + "undici-types": "~6.20.0" } }, - "node_modules/@storybook/react/node_modules/@storybook/client-logger": { - "version": "8.1.10", + "node_modules/@storybook/react-webpack5/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } + "license": "MIT" }, - "node_modules/@storybook/react/node_modules/@storybook/core-common": { - "version": "8.1.10", + "node_modules/@storybook/react/node_modules/@storybook/components": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/components/-/components-8.4.7.tgz", + "integrity": "sha512-uyJIcoyeMWKAvjrG9tJBUCKxr2WZk+PomgrgrUwejkIfXMO76i6jw9BwLa0NZjYdlthDv30r9FfbYZyeNPmF0g==", "dev": true, "license": "MIT", - "dependencies": { - "@storybook/core-events": "8.1.10", - "@storybook/csf-tools": "8.1.10", - "@storybook/node-logger": "8.1.10", - "@storybook/types": "8.1.10", - "@yarnpkg/fslib": "2.10.3", - "@yarnpkg/libzip": "2.3.0", - "chalk": "^4.1.0", - "cross-spawn": "^7.0.3", - "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0", - "esbuild-register": "^3.5.0", - "execa": "^5.0.0", - "file-system-cache": "2.3.0", - "find-cache-dir": "^3.0.0", - "find-up": "^5.0.0", - "fs-extra": "^11.1.0", - "glob": "^10.0.0", - "handlebars": "^4.7.7", - "lazy-universal-dotenv": "^4.0.0", - "node-fetch": "^2.0.0", - "picomatch": "^2.3.0", - "pkg-dir": "^5.0.0", - "prettier-fallback": "npm:prettier@^3", - "pretty-hrtime": "^1.0.3", - "resolve-from": "^5.0.0", - "semver": "^7.3.7", - "tempy": "^3.1.0", - "tiny-invariant": "^1.3.1", - "ts-dedent": "^2.0.0", - "util": "^0.12.4" - }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "prettier": "^2 || ^3" - }, - "peerDependenciesMeta": { - "prettier": { - "optional": true - } - } - }, - "node_modules/@storybook/react/node_modules/@storybook/core-events": { - "version": "8.1.10", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/csf": "^0.1.7", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, - "node_modules/@storybook/react/node_modules/@storybook/csf-tools": { - "version": "8.1.10", + "node_modules/@storybook/react/node_modules/@storybook/manager-api": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-8.4.7.tgz", + "integrity": "sha512-ELqemTviCxAsZ5tqUz39sDmQkvhVAvAgiplYy9Uf15kO0SP2+HKsCMzlrm2ue2FfkUNyqbDayCPPCB0Cdn/mpQ==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/generator": "^7.24.4", - "@babel/parser": "^7.24.4", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0", - "@storybook/csf": "^0.1.7", - "@storybook/types": "8.1.10", - "fs-extra": "^11.1.0", - "recast": "^0.23.5", - "ts-dedent": "^2.0.0" - }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/react/node_modules/@storybook/docs-tools": { - "version": "8.1.10", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/core-common": "8.1.10", - "@storybook/core-events": "8.1.10", - "@storybook/preview-api": "8.1.10", - "@storybook/types": "8.1.10", - "@types/doctrine": "^0.0.3", - "assert": "^2.1.0", - "doctrine": "^3.0.0", - "lodash": "^4.17.21" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" + "peerDependencies": { + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, - "node_modules/@storybook/react/node_modules/@storybook/node-logger": { - "version": "8.1.10", + "node_modules/@storybook/react/node_modules/@storybook/react-dom-shim": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-8.4.0.tgz", + "integrity": "sha512-PYYZVdQ6/ts6hBMAwMEu4hfbyHFPzUYmVsZNtF2egaVJQ44xM4i1Zt+RJuo2NOt5VyBCfXJOs+lSIdmSBY2arw==", "dev": true, "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/react/node_modules/@storybook/preview-api": { - "version": "8.1.10", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/channels": "8.1.10", - "@storybook/client-logger": "8.1.10", - "@storybook/core-events": "8.1.10", - "@storybook/csf": "^0.1.7", - "@storybook/global": "^5.0.0", - "@storybook/types": "8.1.10", - "@types/qs": "^6.9.5", - "dequal": "^2.0.2", - "lodash": "^4.17.21", - "memoizerific": "^1.11.3", - "qs": "^6.10.0", - "tiny-invariant": "^1.3.1", - "ts-dedent": "^2.0.0", - "util-deprecate": "^1.0.2" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^8.4.0" } }, - "node_modules/@storybook/react/node_modules/@storybook/types": { - "version": "8.1.10", + "node_modules/@storybook/react/node_modules/@storybook/theming": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.4.7.tgz", + "integrity": "sha512-99rgLEjf7iwfSEmdqlHkSG3AyLcK0sfExcr0jnc6rLiAkBhzuIsvcHjjUwkR210SOCgXqBPW0ZA6uhnuyppHLw==", "dev": true, "license": "MIT", - "dependencies": { - "@storybook/channels": "8.1.10", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/react/node_modules/@types/node": { - "version": "18.19.39", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@storybook/react/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/@storybook/react/node_modules/assert": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "is-nan": "^1.3.2", - "object-is": "^1.1.5", - "object.assign": "^4.1.4", - "util": "^0.12.5" - } - }, - "node_modules/@storybook/react/node_modules/brace-expansion": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@storybook/react/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/@storybook/react/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/@storybook/react/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@storybook/react/node_modules/crypto-random-string": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^1.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/react/node_modules/crypto-random-string/node_modules/type-fest": { - "version": "1.4.0", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/react/node_modules/find-cache-dir": { - "version": "3.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "node_modules/@storybook/react/node_modules/find-cache-dir/node_modules/find-up": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/react/node_modules/find-cache-dir/node_modules/pkg-dir": { - "version": "4.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/react/node_modules/fs-extra": { - "version": "11.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@storybook/react/node_modules/glob": { - "version": "10.4.2", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@storybook/react/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/react/node_modules/is-stream": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/react/node_modules/jackspeak": { - "version": "3.4.0", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/@storybook/react/node_modules/locate-path": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/react/node_modules/make-dir": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/react/node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@storybook/react/node_modules/minimatch": { - "version": "9.0.4", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@storybook/react/node_modules/minipass": { - "version": "7.1.2", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/@storybook/react/node_modules/p-limit": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/react/node_modules/p-locate": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/react/node_modules/path-exists": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/react/node_modules/recast": { - "version": "0.23.9", - "dev": true, - "license": "MIT", - "dependencies": { - "ast-types": "^0.16.1", - "esprima": "~4.0.0", - "source-map": "~0.6.1", - "tiny-invariant": "^1.3.3", - "tslib": "^2.0.1" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/@storybook/react/node_modules/source-map": { - "version": "0.6.1", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@storybook/react/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/react/node_modules/temp-dir": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - } - }, - "node_modules/@storybook/react/node_modules/tempy": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-stream": "^3.0.0", - "temp-dir": "^3.0.0", - "type-fest": "^2.12.2", - "unique-string": "^3.0.0" - }, - "engines": { - "node": ">=14.16" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/react/node_modules/type-fest": { - "version": "2.19.0", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/react/node_modules/unique-string": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "crypto-random-string": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/react/node_modules/util": { - "version": "0.12.5", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" + "peerDependencies": { + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, "node_modules/@storybook/router": { @@ -13957,20 +12669,6 @@ "url": "https://opencollective.com/storybook" } }, - "node_modules/@storybook/types": { - "version": "8.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/channels": "8.1.6", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, "node_modules/@svgr/babel-plugin-add-jsx-attribute": { "version": "6.5.1", "dev": true, @@ -14694,16 +13392,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/escodegen": { - "version": "0.0.6", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "0.0.51", - "dev": true, - "license": "MIT" - }, "node_modules/@types/express": { "version": "4.17.21", "dev": true, @@ -14797,7 +13485,9 @@ "license": "MIT" }, "node_modules/@types/http-proxy": { - "version": "1.17.9", + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -15099,6 +13789,8 @@ }, "node_modules/@types/resolve": { "version": "1.20.6", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", + "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==", "dev": true, "license": "MIT" }, @@ -16122,6 +14814,7 @@ "version": "7.4.1", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -16165,14 +14858,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/adm-zip": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", @@ -16282,6 +14967,19 @@ } } }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, "node_modules/anser": { "version": "1.4.10", "license": "MIT" @@ -17021,17 +15719,6 @@ "webpack": ">=5" } }, - "node_modules/babel-loader/node_modules/ajv-keywords": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, "node_modules/babel-loader/node_modules/find-cache-dir": { "version": "4.0.0", "dev": true, @@ -18106,6 +16793,8 @@ }, "node_modules/browser-assert": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/browser-assert/-/browser-assert-1.2.1.tgz", + "integrity": "sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==", "dev": true }, "node_modules/browserslist": { @@ -18648,6 +17337,8 @@ }, "node_modules/case-sensitive-paths-webpack-plugin": { "version": "2.4.0", + "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", + "integrity": "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==", "dev": true, "license": "MIT", "engines": { @@ -18793,6 +17484,8 @@ }, "node_modules/classnames": { "version": "2.5.0", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.0.tgz", + "integrity": "sha512-FQuRlyKinxrb5gwJlfVASbSrDlikDJ07426TrfPsdGLvtochowmkbnSFdQGJ2aoXrSetq5KqGV9emvWpy+91xA==", "license": "MIT", "workspaces": [ "benchmarks" @@ -18876,6 +17569,8 @@ }, "node_modules/clipboard": { "version": "2.0.11", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", + "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", "license": "MIT", "dependencies": { "good-listener": "^1.2.2", @@ -19446,6 +18141,8 @@ }, "node_modules/constants-browserify": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", "dev": true, "license": "MIT" }, @@ -19531,17 +18228,6 @@ "webpack": "^5.1.0" } }, - "node_modules/copy-webpack-plugin/node_modules/ajv-keywords": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, "node_modules/copy-webpack-plugin/node_modules/array-union": { "version": "3.0.1", "dev": true, @@ -19732,24 +18418,22 @@ "license": "MIT" }, "node_modules/create-storybook": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/create-storybook/-/create-storybook-8.3.0.tgz", - "integrity": "sha512-MAcMWX7V4VE1W47O6tiwL4xBJprsa7b0cqLECNSKaW8nvr7LSFgveobIqWG7i1DqQg/cGWA09o2YRDc2LOFsmQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/create-storybook/-/create-storybook-8.4.0.tgz", + "integrity": "sha512-zC7asZ8wo9BY7n45G/4ZNH48x8BvAve9c9bcEcTO1HNGvvO8vSlvGc8v74LVNxkgo2h2SucsPZJnXv7g0jA9PQ==", "dev": true, "license": "MIT", "dependencies": { "@types/semver": "^7.3.4", - "chalk": "^4.1.0", "commander": "^12.1.0", "execa": "^5.0.0", "fd-package-json": "^1.2.0", "find-up": "^5.0.0", - "fs-extra": "^11.1.0", "ora": "^5.4.1", "prettier": "^3.1.1", "prompts": "^2.4.0", "semver": "^7.3.7", - "storybook": "8.3.0", + "storybook": "8.4.0", "tiny-invariant": "^1.3.1", "ts-dedent": "^2.0.0" }, @@ -19761,88 +18445,10 @@ "url": "https://opencollective.com/storybook" } }, - "node_modules/create-storybook/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "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/create-storybook/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "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/create-storybook/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/create-storybook/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/create-storybook/node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/create-storybook/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/create-storybook/node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, "license": "MIT", "bin": { @@ -19855,19 +18461,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/create-storybook/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cross-fetch": { "version": "3.1.5", "license": "MIT", @@ -19876,7 +18469,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -20380,17 +18975,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/default-gateway": { - "version": "6.0.3", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/defaults": { "version": "1.0.4", "license": "MIT", @@ -20543,6 +19127,8 @@ }, "node_modules/delegate": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", "license": "MIT" }, "node_modules/delegates": { @@ -21217,6 +19803,8 @@ }, "node_modules/endent": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/endent/-/endent-2.1.0.tgz", + "integrity": "sha512-r8VyPX7XL8U01Xgnb1CjZ3XV+z90cXIJ9JPE/R9SEC9vpw2P6CfsRPJmp20DppC5N7ZAMCmjYkJIa744Iyg96w==", "dev": true, "license": "MIT", "dependencies": { @@ -21483,6 +20071,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.29.0.tgz", + "integrity": "sha512-GjTll+E6APcfAQA09D89HdT8Qn2Yb+TeDSDBTMcxAo+V+w1amAtCI15LJu4YPH/UCPoSo/F47Gr1LIM0TE0lZA==", + "dev": true, + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/es6-error": { "version": "4.1.1", "dev": true, @@ -21705,9 +20304,9 @@ } }, "node_modules/eslint-config-expensify": { - "version": "2.0.74", - "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.74.tgz", - "integrity": "sha512-NTA8fPbfkyCBZG+2/xJqB+HYD2D0XP8Sx1IDLWiwe/XJyNEESeqwQVbpA7FUP9sq4Ik2m2LPMf/G/aQHfw88rQ==", + "version": "2.0.75", + "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.75.tgz", + "integrity": "sha512-eSzQpxmVMGGXZSoB7aPZoWh75NC3oStyQnd+1JBFUQMDrdCyWjkMl8UJjzBqp/dOHazmVgLQUS1vDfk5cGXe6Q==", "dev": true, "license": "ISC", "dependencies": { @@ -22956,9 +21555,9 @@ } }, "node_modules/expensify-common": { - "version": "2.0.106", - "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.106.tgz", - "integrity": "sha512-KmxKvglbIUJb0sAcmNxb/AXYAqa3GIZfu3MbmtlYDNJx24mjDjtbGkKhm+16TICDoPj2PDRNogIqgUGWmSSZFQ==", + "version": "2.0.109", + "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.109.tgz", + "integrity": "sha512-5XTrJxiDSjQhojnJfXH1G+fSgRM92oAJ5HiLo28HppmJQuA350GOONVo88rRalcI029rlYGIMh8WfhMlOuE/gA==", "license": "MIT", "dependencies": { "awesome-phonenumber": "^5.4.0", @@ -22980,6 +21579,7 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -22988,7 +21588,9 @@ } }, "node_modules/expensify-common/node_modules/ua-parser-js": { - "version": "1.0.38", + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.39.tgz", + "integrity": "sha512-k24RCVWlEcjkdOxYmVJgeD/0a1TiSpqLg+ZalVGV9lsnr4yqu0w7tX/x2xX6G4zpkgQnRf89lxuZ1wsbjXM8lw==", "funding": [ { "type": "opencollective", @@ -23004,6 +21606,9 @@ } ], "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, "engines": { "node": "*" } @@ -23964,6 +22569,8 @@ }, "node_modules/fork-ts-checker-webpack-plugin": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz", + "integrity": "sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg==", "dev": true, "license": "MIT", "dependencies": { @@ -23991,6 +22598,8 @@ }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { @@ -24005,6 +22614,8 @@ }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { @@ -24020,6 +22631,8 @@ }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -24031,11 +22644,15 @@ }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-name": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", "dependencies": { @@ -24049,6 +22666,8 @@ }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { @@ -24057,6 +22676,8 @@ }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/memfs": { "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", "dev": true, "license": "Unlicense", "dependencies": { @@ -24068,6 +22689,8 @@ }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -24181,6 +22804,8 @@ }, "node_modules/fs-monkey": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", "dev": true, "license": "Unlicense" }, @@ -24568,6 +23193,8 @@ }, "node_modules/good-listener": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", "license": "MIT", "dependencies": { "delegate": "^3.1.2" @@ -25005,17 +23632,6 @@ "node": ">= 12" } }, - "node_modules/html-tags": { - "version": "3.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/html-webpack-plugin": { "version": "5.5.3", "dev": true, @@ -25132,7 +23748,9 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.6", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", "dev": true, "license": "MIT", "dependencies": { @@ -25156,6 +23774,8 @@ }, "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", "dev": true, "license": "MIT", "engines": { @@ -25397,6 +24017,8 @@ }, "node_modules/immediate": { "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "license": "MIT" }, "node_modules/import-fresh": { @@ -25561,9 +24183,10 @@ } }, "node_modules/internal-ip/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "license": "MIT", "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", @@ -26489,23 +25112,6 @@ "reflect.getprototypeof": "^1.0.3" } }, - "node_modules/jackspeak": { - "version": "2.3.6", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jake": { "version": "10.8.7", "dev": true, @@ -28678,6 +27284,8 @@ }, "node_modules/jquery": { "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", + "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==", "license": "MIT" }, "node_modules/js-base64": { @@ -29092,6 +27700,8 @@ }, "node_modules/lie": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", "license": "MIT", "dependencies": { "immediate": "~3.0.5" @@ -29347,6 +27957,8 @@ }, "node_modules/localforage": { "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", "license": "Apache-2.0", "dependencies": { "lie": "3.1.1" @@ -29607,11 +28219,13 @@ } }, "node_modules/magic-string": { - "version": "0.30.10", + "version": "0.30.14", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.14.tgz", + "integrity": "sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/make-cancellable-promise": { @@ -30806,7 +29420,9 @@ "optional": true }, "node_modules/nanoid": { - "version": "3.3.7", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", @@ -32035,6 +30651,13 @@ "node": ">=0.6.0" } }, + "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" + }, "node_modules/path-exists": { "version": "3.0.0", "license": "MIT", @@ -32745,13 +31368,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/querystring": { - "version": "0.2.0", - "dev": true, - "engines": { - "node": ">=0.4.x" - } - }, "node_modules/querystringify": { "version": "2.2.0", "dev": true, @@ -33055,7 +31671,9 @@ } }, "node_modules/react-docgen": { - "version": "7.0.3", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-7.1.0.tgz", + "integrity": "sha512-APPU8HB2uZnpl6Vt/+0AFoVYgSRtfiP6FLrZgPPTDmqSb2R4qZRbgd0A3VzIFxDt5e+Fozjx79WjLWnF69DK8g==", "dev": true, "license": "MIT", "dependencies": { @@ -33076,6 +31694,8 @@ }, "node_modules/react-docgen-typescript": { "version": "2.2.2", + "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.2.2.tgz", + "integrity": "sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -33084,11 +31704,15 @@ }, "node_modules/react-docgen/node_modules/@types/doctrine": { "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", + "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", "dev": true, "license": "MIT" }, "node_modules/react-docgen/node_modules/strip-indent": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", + "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", "dev": true, "license": "MIT", "dependencies": { @@ -33119,25 +31743,6 @@ "loose-envify": "^1.1.0" } }, - "node_modules/react-element-to-jsx-string": { - "version": "15.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@base2/pretty-print-object": "1.0.1", - "is-plain-object": "5.0.0", - "react-is": "18.1.0" - }, - "peerDependencies": { - "react": "^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0", - "react-dom": "^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0" - } - }, - "node_modules/react-element-to-jsx-string/node_modules/react-is": { - "version": "18.1.0", - "dev": true, - "license": "MIT" - }, "node_modules/react-error-boundary": { "version": "4.0.11", "license": "MIT", @@ -33149,9 +31754,9 @@ } }, "node_modules/react-fast-pdf": { - "version": "1.0.20", - "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.20.tgz", - "integrity": "sha512-E2PJOO5oEqi6eNPllNOlQ8y0DiLZ3AW8t+MCN7AgJPp5pY04SeDveXHWvPN0nPU4X5sRBZ7CejeYce2QMMQDyg==", + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.21.tgz", + "integrity": "sha512-8Uuz/jPHjHqElH+aUj3ldS/Hg/NoZ5ZS/VupGzDkVJST0UiGzxkvDxxFIQuYuiaI4NGwGmqtQGGYsjJKpyWnig==", "license": "MIT", "dependencies": { "react-pdf": "^9.1.1", @@ -33512,9 +32117,9 @@ "license": "MIT" }, "node_modules/react-native-keyboard-controller": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/react-native-keyboard-controller/-/react-native-keyboard-controller-1.14.4.tgz", - "integrity": "sha512-hVt9KhK2dxBNtk4xHTnKLeO9Jv7v5h2TZlIeCQkbBLMd5NIJa4ll0GxIpbuutjP1ctPdhXUVpCfQzgXXJOYlzw==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/react-native-keyboard-controller/-/react-native-keyboard-controller-1.15.0.tgz", + "integrity": "sha512-Laqszs0Uciu9MFkHurLwaHs9kftzUueew75HVOndbdcGR3MbKs2MqKdQEg1AgXSHcGoGg5nKafMOLVIoYjK6kA==", "license": "MIT", "dependencies": { "react-native-is-edge-to-edge": "^1.1.6" @@ -33564,9 +32169,9 @@ } }, "node_modules/react-native-onyx": { - "version": "2.0.82", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.82.tgz", - "integrity": "sha512-12+NgkC4fOeGu2J6s985NKUuLHP4aijBhpE6Us5IfVL+9dwxr/KqUVgV00OzXtYAABcWcpMC5PrvESqe8T5Iyw==", + "version": "2.0.86", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.86.tgz", + "integrity": "sha512-3pjyzlo8We4tSx/xf+4IRnBMcm5rk0E+aHBUSUxJ5jaFermx0SXZJlnvE5Emkw+iu0bXKkwea6zt2LhxD1JSsg==", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", @@ -33693,9 +32298,9 @@ } }, "node_modules/react-native-reanimated": { - "version": "3.16.3", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.16.3.tgz", - "integrity": "sha512-OWlA6e1oHhytTpc7WiSZ7Tmb8OYwLKYZz29Sz6d6WAg60Hm5GuAiKIWUG7Ako7FLcYhFkA0pEQ2xPMEYUo9vlw==", + "version": "3.16.4", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.16.4.tgz", + "integrity": "sha512-dF1Vvu8gG+p0+DmBhKMTx5X9iw/rH1ZF9WaIn2nW0c5rxsVFf00axmDgaAdPxNWblmtLnroaKwrV7SjMUyOx+g==", "license": "MIT", "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", @@ -33868,7 +32473,9 @@ } }, "node_modules/react-native-view-shot": { - "version": "3.8.0", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/react-native-view-shot/-/react-native-view-shot-4.0.0.tgz", + "integrity": "sha512-e7wtfdm981DQVqkW+YE9mkemYarI0VZQ7PzRcHzQOmXlVrGKvNVD2MzRXOg+gK8msQIQ95QxATJKzG/QkQ9QHQ==", "license": "MIT", "dependencies": { "html2canvas": "^1.4.1" @@ -35070,6 +33677,8 @@ }, "node_modules/select": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==", "license": "MIT" }, "node_modules/select-hose": { @@ -35965,13 +34574,13 @@ "license": "MIT" }, "node_modules/storybook": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-8.3.0.tgz", - "integrity": "sha512-XKU+nem9OKX/juvJPwka1Q7DTpSbOe0IMp8ZyLQWorhFKpquJdUjryl7Z9GiFZyyTykCqH4ItQ7h8PaOmqVMOw==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-8.4.0.tgz", + "integrity": "sha512-hLfXPtqfoQUMKVortxXdnQoUwDwtH85eSj9LbqGT/z1f/gLLYGNG3Mv3QbsRjHXhn+EfYffh7wuLpAn+Cicijw==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/core": "8.3.0" + "@storybook/core": "8.4.0" }, "bin": { "getstorybook": "bin/index.cjs", @@ -35981,6 +34590,14 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "prettier": "^2 || ^3" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + } } }, "node_modules/stream-buffers": { @@ -37684,11 +36301,6 @@ "node": ">=10.13.0" } }, - "node_modules/unplugin/node_modules/webpack-virtual-modules": { - "version": "0.6.2", - "dev": true, - "license": "MIT" - }, "node_modules/update-browserslist-db": { "version": "1.0.13", "funding": [ @@ -37729,12 +36341,17 @@ "license": "MIT" }, "node_modules/url": { - "version": "0.11.0", + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", "dev": true, "license": "MIT", "dependencies": { - "punycode": "1.3.2", - "querystring": "0.2.0" + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/url-join": { @@ -37759,7 +36376,9 @@ "license": "BSD" }, "node_modules/url/node_modules/punycode": { - "version": "1.3.2", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", "dev": true, "license": "MIT" }, @@ -37827,6 +36446,20 @@ "dev": true, "license": "(WTFPL OR MIT)" }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "license": "MIT" @@ -38174,6 +36807,8 @@ }, "node_modules/webpack-dev-middleware": { "version": "6.1.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.3.tgz", + "integrity": "sha512-A4ChP0Qj8oGociTs6UdlRUGANIGrCDL3y+pmQMc+dSsraXHCatFpmMey4mYELA+juqwUqwQsUgJJISXl1KWmiw==", "dev": true, "license": "MIT", "dependencies": { @@ -38199,24 +36834,17 @@ } } }, - "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, "node_modules/webpack-dev-middleware/node_modules/colorette": { "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true, "license": "MIT" }, "node_modules/webpack-dev-middleware/node_modules/memfs": { "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", "dev": true, "license": "Unlicense", "dependencies": { @@ -38228,6 +36856,8 @@ }, "node_modules/webpack-dev-middleware/node_modules/schema-utils": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "dev": true, "license": "MIT", "dependencies": { @@ -38245,7 +36875,9 @@ } }, "node_modules/webpack-dev-server": { - "version": "5.0.4", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.0.tgz", + "integrity": "sha512-90SqqYXA2SK36KcT6o1bvwvZfJFcmoamqeJY7+boioffX9g9C0wjjJRGUrQIuh43pb0ttX7+ssavmj/WN2RHtA==", "dev": true, "license": "MIT", "dependencies": { @@ -38262,23 +36894,20 @@ "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^2.0.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", + "express": "^4.21.2", "graceful-fs": "^4.2.6", - "html-entities": "^2.4.0", - "http-proxy-middleware": "^2.0.3", + "http-proxy-middleware": "^2.0.7", "ipaddr.js": "^2.1.0", "launch-editor": "^2.6.1", "open": "^10.0.3", "p-retry": "^6.2.0", - "rimraf": "^5.0.5", "schema-utils": "^4.2.0", "selfsigned": "^2.4.1", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", - "webpack-dev-middleware": "^7.1.0", - "ws": "^8.16.0" + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" @@ -38302,27 +36931,10 @@ } } }, - "node_modules/webpack-dev-server/node_modules/ajv-keywords": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/webpack-dev-server/node_modules/brace-expansion": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/webpack-dev-server/node_modules/colorette": { "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true, "license": "MIT" }, @@ -38337,27 +36949,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/webpack-dev-server/node_modules/glob": { - "version": "10.3.12", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.10.2" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/webpack-dev-server/node_modules/ipaddr.js": { "version": "2.1.0", "dev": true, @@ -38380,28 +36971,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/webpack-dev-server/node_modules/minimatch": { - "version": "9.0.4", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/webpack-dev-server/node_modules/minipass": { - "version": "7.0.4", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/webpack-dev-server/node_modules/open": { "version": "10.1.0", "dev": true, @@ -38419,25 +36988,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/webpack-dev-server/node_modules/rimraf": { - "version": "5.0.5", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/webpack-dev-server/node_modules/schema-utils": { - "version": "4.2.0", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "dev": true, "license": "MIT", "dependencies": { @@ -38447,7 +37001,7 @@ "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", @@ -38455,7 +37009,9 @@ } }, "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { - "version": "7.2.1", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", "dev": true, "license": "MIT", "dependencies": { @@ -38505,7 +37061,9 @@ } }, "node_modules/webpack-virtual-modules": { - "version": "0.5.0", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 0ab7b8074f9a..b76e3f541e09 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,30 @@ { "name": "new.expensify", - "version": "9.0.72-1", + "version": "9.0.78-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.", "license": "MIT", "private": true, "scripts": { + "i-standalone": "STANDALONE_NEW_DOT=true npm i", + "install-standalone": "STANDALONE_NEW_DOT=true npm install", "configure-mapbox": "./scripts/setup-mapbox-sdk-walkthrough.sh", "setupNewDotWebForEmulators": "./scripts/setup-newdot-web-emulators.sh", "startAndroidEmulator": "./scripts/start-android.sh", "postinstall": "./scripts/postInstall.sh", - "clean": "npx react-native clean-project-auto", - "android": "./scripts/set-pusher-suffix.sh && npx react-native run-android --mode=developmentDebug --appId=com.expensify.chat.dev --active-arch-only", - "ios": "./scripts/set-pusher-suffix.sh && npx react-native run-ios --list-devices --mode=\"DebugDevelopment\" --scheme=\"New Expensify Dev\"", + "clean": "./scripts/clean.sh", + "clean-standalone": "STANDALONE_NEW_DOT=true ./scripts/clean.sh", + "android": "./scripts/set-pusher-suffix.sh && ./scripts/run-build.sh --android", + "android-standalone": "./scripts/set-pusher-suffix.sh && STANDALONE_NEW_DOT=true ./scripts/run-build.sh --android", + "ios": "./scripts/set-pusher-suffix.sh && ./scripts/run-build.sh --ios", + "ios-standalone": "./scripts/set-pusher-suffix.sh && STANDALONE_NEW_DOT=true ./scripts/run-build.sh --ios", "pod-install": "./scripts/pod-install.sh", - "ipad": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (12.9-inch) (6th generation)\\\" --mode=\\\"DebugDevelopment\\\" --scheme=\\\"New Expensify Dev\\\"\"", - "ipad-sm": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (11-inch) (4th generation)\\\" --mode=\\\"DebugDevelopment\\\" --scheme=\\\"New Expensify Dev\\\"\"", + "pod-install-standalone": "STANDALONE_NEW_DOT=true ./scripts/pod-install.sh", + "ipad": "concurrently \"./scripts/run-build.sh --ipad\"", + "ipad-standalone": "concurrently \"STANDALONE_NEW_DOT=true ./scripts/run-build.sh --ipad\"", + "ipad-sm": "concurrently \"./scripts/run-build.sh --ipad-sm\"", + "ipad-sm-standalone": "concurrently \"STANDALONE_NEW_DOT=true ./scripts/run-build.sh --ipad-sm\"", "start": "npx react-native start", "web": "./scripts/set-pusher-suffix.sh && concurrently npm:web-proxy npm:web-server", "web-proxy": "ts-node web/proxy.ts", @@ -68,7 +76,7 @@ }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.187", + "@expensify/react-native-live-markdown": "0.1.209", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -108,7 +116,7 @@ "date-fns-tz": "^3.2.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.106", + "expensify-common": "2.0.109", "expo": "51.0.31", "expo-av": "14.0.7", "expo-image": "1.12.15", @@ -130,7 +138,7 @@ "react-content-loader": "^7.0.0", "react-dom": "18.3.1", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "1.0.20", + "react-fast-pdf": "1.0.21", "react-map-gl": "^7.1.3", "react-native": "0.75.2", "react-native-android-location-enabler": "^2.0.1", @@ -148,11 +156,11 @@ "react-native-image-picker": "^7.0.3", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#cb392140db4953a283590d7cf93b4d0461baa2a9", "react-native-key-command": "^1.0.8", - "react-native-keyboard-controller": "1.14.4", + "react-native-keyboard-controller": "1.15.0", "react-native-launch-arguments": "^4.0.2", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.82", + "react-native-onyx": "2.0.86", "react-native-pager-view": "6.5.1", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -161,7 +169,7 @@ "react-native-plaid-link-sdk": "11.11.0", "react-native-qrcode-svg": "6.3.11", "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-nitro-sqlite#99f34ebefa91698945f3ed26622e002bd79489e0", - "react-native-reanimated": "3.16.3", + "react-native-reanimated": "3.16.4", "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.10.9", @@ -171,7 +179,7 @@ "react-native-svg": "15.9.0", "react-native-tab-view": "^3.5.2", "react-native-url-polyfill": "^2.0.0", - "react-native-view-shot": "3.8.0", + "react-native-view-shot": "4.0.0", "react-native-vision-camera": "^4.6.1", "react-native-web": "0.19.13", "react-native-webview": "13.8.6", @@ -221,9 +229,9 @@ "@storybook/addon-a11y": "^8.1.10", "@storybook/addon-essentials": "^8.1.10", "@storybook/addon-webpack5-compiler-babel": "^3.0.3", - "@storybook/cli": "^8.3.0", + "@storybook/cli": "^8.4.0", "@storybook/react": "^8.1.10", - "@storybook/react-webpack5": "^8.1.6", + "@storybook/react-webpack5": "^8.4.0", "@storybook/theming": "^8.1.10", "@svgr/webpack": "^6.0.0", "@testing-library/jest-native": "5.4.1", @@ -276,7 +284,7 @@ "electron-builder": "25.0.0", "eslint": "^8.57.0", "eslint-config-airbnb-typescript": "^18.0.0", - "eslint-config-expensify": "^2.0.74", + "eslint-config-expensify": "2.0.75", "eslint-config-prettier": "^9.1.0", "eslint-plugin-deprecation": "^3.0.0", "eslint-plugin-jest": "^28.6.0", @@ -317,7 +325,7 @@ "setimmediate": "^1.0.5", "shellcheck": "^1.1.0", "source-map": "^0.7.4", - "storybook": "^8.3.0", + "storybook": "^8.4.0", "style-loader": "^2.0.0", "time-analytics-webpack-plugin": "^0.1.17", "ts-jest": "^29.1.2", diff --git a/patches/@onfido+react-native-sdk+10.6.0.patch b/patches/@onfido+react-native-sdk+10.6.0.patch index 201e9ab92c22..87f0aad1618d 100644 --- a/patches/@onfido+react-native-sdk+10.6.0.patch +++ b/patches/@onfido+react-native-sdk+10.6.0.patch @@ -1252,7 +1252,7 @@ index a9de0d0..da83d9f 100644 - s.dependency "Onfido", "~> 29.6.0" + s.dependency "Onfido", "~> 29.7.0" + -+ if ENV['USE_FRAMEWORKS'] == '1' ++ if ENV['USE_FRAMEWORKS'] != nil + s.pod_target_xcconfig = { + "OTHER_CFLAGS" => "$(inherited) -DUSE_FRAMEWORKS", + "OTHER_CPLUSPLUSFLAGS" => "$(inherited) -DUSE_FRAMEWORKS", diff --git a/patches/@react-native+gradle-plugin+0.75.2.patch b/patches/@react-native+gradle-plugin+0.75.2+001+initial.patch similarity index 100% rename from patches/@react-native+gradle-plugin+0.75.2.patch rename to patches/@react-native+gradle-plugin+0.75.2+001+initial.patch diff --git a/patches/@react-native-camera-roll+camera-roll+7.4.0+001+hybrid-app.patch b/patches/@react-native-camera-roll+camera-roll+7.4.0+001+hybrid-app.patch deleted file mode 100644 index 9d848520a943..000000000000 --- a/patches/@react-native-camera-roll+camera-roll+7.4.0+001+hybrid-app.patch +++ /dev/null @@ -1,15 +0,0 @@ -diff --git a/node_modules/@react-native-camera-roll/camera-roll/android/build.gradle b/node_modules/@react-native-camera-roll/camera-roll/android/build.gradle -index 6891fa3..8397f95 100644 ---- a/node_modules/@react-native-camera-roll/camera-roll/android/build.gradle -+++ b/node_modules/@react-native-camera-roll/camera-roll/android/build.gradle -@@ -81,7 +81,9 @@ def findNodeModulePath(baseDir, packageName) { - } - - def resolveReactNativeDirectory() { -- def reactNative = file("${findNodeModulePath(rootProject.projectDir, "react-native")}") -+ def projectDir = this.hasProperty('reactNativeProject') ? this.reactNativeProject : rootProject.projectDir -+ def modulePath = file(projectDir); -+ def reactNative = file("${findNodeModulePath(modulePath, 'react-native')}") - if (reactNative.exists()) { - return reactNative - } diff --git a/patches/@react-native-community+cli-platform-android+14.0.0+001+hybrid-app.patch b/patches/@react-native-community+cli-platform-android+14.0.0+001+hybrid-app.patch deleted file mode 100644 index 7f64391efe4c..000000000000 --- a/patches/@react-native-community+cli-platform-android+14.0.0+001+hybrid-app.patch +++ /dev/null @@ -1,52 +0,0 @@ -diff --git a/node_modules/@react-native-community/cli-platform-android/native_modules.gradle b/node_modules/@react-native-community/cli-platform-android/native_modules.gradle -index 43296c6..0d91033 100644 ---- a/node_modules/@react-native-community/cli-platform-android/native_modules.gradle -+++ b/node_modules/@react-native-community/cli-platform-android/native_modules.gradle -@@ -149,16 +149,18 @@ class ReactNativeModules { - private ProviderFactory providers - private String packageName - private File root -+ private File rnRoot - private ArrayList> reactNativeModules - private HashMap reactNativeModulesBuildVariants - private String reactNativeVersion - - private static String LOG_PREFIX = ":ReactNative:" - -- ReactNativeModules(Logger logger, ProviderFactory providers, File root) { -+ ReactNativeModules(Logger logger, ProviderFactory providers, File root, File rnRoot) { - this.logger = logger - this.providers = providers - this.root = root -+ this.rnRoot = rnRoot - - def (nativeModules, reactNativeModulesBuildVariants, androidProject, reactNativeVersion) = this.getReactNativeConfig() - this.reactNativeModules = nativeModules -@@ -440,10 +442,10 @@ class ReactNativeModules { - */ - def cliResolveScript = "try {console.log(require('@react-native-community/cli').bin);} catch (e) {console.log(require('react-native/cli').bin);}" - String[] nodeCommand = ["node", "-e", cliResolveScript] -- def cliPath = this.getCommandOutput(nodeCommand, this.root) -+ def cliPath = this.getCommandOutput(nodeCommand, this.rnRoot) - - String[] reactNativeConfigCommand = ["node", cliPath, "config", "--platform", "android"] -- def reactNativeConfigOutput = this.getCommandOutput(reactNativeConfigCommand, this.root) -+ def reactNativeConfigOutput = this.getCommandOutput(reactNativeConfigCommand, this.rnRoot) - - def json - try { -@@ -513,7 +515,13 @@ class ReactNativeModules { - */ - def projectRoot = rootProject.projectDir - --def autoModules = new ReactNativeModules(logger, providers, projectRoot) -+def autoModules -+ -+if(this.hasProperty('reactNativeProject')){ -+ autoModules = new ReactNativeModules(logger, providers, projectRoot, new File(projectRoot, reactNativeProject)) -+} else { -+ autoModules = new ReactNativeModules(logger, providers, projectRoot, projectRoot) -+} - - def reactNativeVersionRequireNewArchEnabled(autoModules) { - def rnVersion = autoModules.reactNativeVersion diff --git a/patches/@react-native-community+cli-platform-ios+14.0.0+001+hybrid-app.patch b/patches/@react-native-community+cli-platform-ios+14.0.0+001+hybrid-app.patch deleted file mode 100644 index e54ab17c43dd..000000000000 --- a/patches/@react-native-community+cli-platform-ios+14.0.0+001+hybrid-app.patch +++ /dev/null @@ -1,46 +0,0 @@ -diff --git a/node_modules/@react-native-community/cli-platform-ios/native_modules.rb b/node_modules/@react-native-community/cli-platform-ios/native_modules.rb -index 82f537c..df441e2 100644 ---- a/node_modules/@react-native-community/cli-platform-ios/native_modules.rb -+++ b/node_modules/@react-native-community/cli-platform-ios/native_modules.rb -@@ -12,7 +12,7 @@ - require 'pathname' - require 'cocoapods' - --def use_native_modules!(config = nil) -+def updateConfig(config = nil) - if (config.is_a? String) - Pod::UI.warn("Passing custom root to use_native_modules! is deprecated.", - [ -@@ -24,7 +24,6 @@ def use_native_modules!(config = nil) - # Resolving the path the RN CLI. The `@react-native-community/cli` module may not be there for certain package managers, so we fall back to resolving it through `react-native` package, that's always present in RN projects - cli_resolve_script = "try {console.log(require('@react-native-community/cli').bin);} catch (e) {console.log(require('react-native/cli').bin);}" - cli_bin = Pod::Executable.execute_command("node", ["-e", cli_resolve_script], true).strip -- - if (!config) - json = [] - -@@ -36,9 +35,24 @@ def use_native_modules!(config = nil) - - config = JSON.parse(json.join("\n")) - end -+end -+ -+def use_native_modules!(config = nil) -+ if (ENV['REACT_NATIVE_DIR']) -+ Dir.chdir(ENV['REACT_NATIVE_DIR']) do -+ config = updateConfig(config) -+ end -+ else -+ config = updateConfig(config) -+ end - - project_root = Pathname.new(config["project"]["ios"]["sourceDir"]) - -+ if(ENV["PROJECT_ROOT_DIR"]) -+ project_root = File.join(Dir.pwd, ENV["PROJECT_ROOT_DIR"]) -+ -+ end -+ - packages = config["dependencies"] - found_pods = [] - diff --git a/patches/expo+51.0.31+001+hybrid-app.patch b/patches/expo+51.0.31+001+hybrid-app.patch deleted file mode 100644 index 44048857fc1b..000000000000 --- a/patches/expo+51.0.31+001+hybrid-app.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/node_modules/expo/scripts/autolinking.gradle b/node_modules/expo/scripts/autolinking.gradle -index 929b7f0..c948ebb 100644 ---- a/node_modules/expo/scripts/autolinking.gradle -+++ b/node_modules/expo/scripts/autolinking.gradle -@@ -1,6 +1,6 @@ - // Resolve `expo` > `expo-modules-autolinking` dependency chain - def autolinkingPath = ["node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })"] --apply from: new File( -+apply from: hasProperty("reactNativeProject") ? file('../../expo-modules-autolinking/scripts/android/autolinking_implementation.gradle') : new File( - providers.exec { - workingDir(rootDir) - commandLine(autolinkingPath) diff --git a/patches/expo-av+14.0.7+001+hybrid-app.patch b/patches/expo-av+14.0.7+001+hybrid-app.patch deleted file mode 100644 index 4cf0dee990c5..000000000000 --- a/patches/expo-av+14.0.7+001+hybrid-app.patch +++ /dev/null @@ -1,19 +0,0 @@ -diff --git a/node_modules/expo-av/android/build.gradle b/node_modules/expo-av/android/build.gradle -index 11e7574..6dae6a0 100644 ---- a/node_modules/expo-av/android/build.gradle -+++ b/node_modules/expo-av/android/build.gradle -@@ -3,12 +3,13 @@ apply plugin: 'com.android.library' - group = 'host.exp.exponent' - version = '14.0.7' - -+def REACT_NATIVE_PATH = this.hasProperty('reactNativeProject') ? this.reactNativeProject + '/node_modules/react-native/package.json' : 'react-native/package.json' - def REACT_NATIVE_BUILD_FROM_SOURCE = findProject(":ReactAndroid") != null - def REACT_NATIVE_DIR = REACT_NATIVE_BUILD_FROM_SOURCE - ? findProject(":ReactAndroid").getProjectDir().parent - : file(providers.exec { - workingDir(rootDir) -- commandLine("node", "--print", "require.resolve('react-native/package.json')") -+ commandLine("node", "--print", "require.resolve('${REACT_NATIVE_PATH}')") - }.standardOutput.asText.get().trim()).parent - - def reactNativeArchitectures() { diff --git a/patches/expo-modules-autolinking+1.11.2+001+hybrid-app.patch b/patches/expo-modules-autolinking+1.11.2+001+hybrid-app.patch deleted file mode 100644 index a345f84b8f20..000000000000 --- a/patches/expo-modules-autolinking+1.11.2+001+hybrid-app.patch +++ /dev/null @@ -1,40 +0,0 @@ -diff --git a/node_modules/expo-modules-autolinking/scripts/android/autolinking_implementation.gradle b/node_modules/expo-modules-autolinking/scripts/android/autolinking_implementation.gradle -index f085818..fcb9594 100644 ---- a/node_modules/expo-modules-autolinking/scripts/android/autolinking_implementation.gradle -+++ b/node_modules/expo-modules-autolinking/scripts/android/autolinking_implementation.gradle -@@ -152,12 +152,13 @@ class ExpoAutolinkingManager { - } - - static private String[] convertOptionsToCommandArgs(String command, Map options) { -+ def expoPath = options.searchPaths ? "../react-native/node_modules/expo" : "expo" - String[] args = [ - 'node', - '--no-warnings', - '--eval', - // Resolve the `expo` > `expo-modules-autolinking` chain from the project root -- 'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo\')] }))(process.argv.slice(1))', -+ "require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'${expoPath}\')] }))(process.argv.slice(1))", - '--', - command, - '--platform', -diff --git a/node_modules/expo-modules-autolinking/scripts/ios/project_integrator.rb b/node_modules/expo-modules-autolinking/scripts/ios/project_integrator.rb -index 5d46f1e..fec4f34 100644 ---- a/node_modules/expo-modules-autolinking/scripts/ios/project_integrator.rb -+++ b/node_modules/expo-modules-autolinking/scripts/ios/project_integrator.rb -@@ -215,6 +215,7 @@ module Expo - args = autolinking_manager.base_command_args.map { |arg| "\"#{arg}\"" } - platform = autolinking_manager.platform_name.downcase - package_names = autolinking_manager.packages_to_generate.map { |package| "\"#{package.name}\"" } -+ expo_path = ENV['REACT_NATIVE_DIR'] ? "#{ENV['REACT_NATIVE_DIR']}/node_modules/expo" : "expo" - - <<~SUPPORT_SCRIPT - #!/usr/bin/env bash -@@ -262,7 +263,7 @@ module Expo - - with_node \\ - --no-warnings \\ -- --eval "require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))" \\ -+ --eval "require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'#{expo_path}/package.json\')] }))(process.argv.slice(1))" \\ - generate-modules-provider #{args.join(' ')} \\ - --target "#{modules_provider_path}" \\ - --platform "apple" \\ diff --git a/patches/expo-modules-core+1.12.23+002+hybrid-app.patch b/patches/expo-modules-core+1.12.23+002+hybrid-app.patch deleted file mode 100644 index b32830615aaa..000000000000 --- a/patches/expo-modules-core+1.12.23+002+hybrid-app.patch +++ /dev/null @@ -1,21 +0,0 @@ -diff --git a/node_modules/expo-modules-core/android/build.gradle b/node_modules/expo-modules-core/android/build.gradle -index f22a3c3..4884cea 100644 ---- a/node_modules/expo-modules-core/android/build.gradle -+++ b/node_modules/expo-modules-core/android/build.gradle -@@ -20,12 +20,13 @@ def isExpoModulesCoreTests = { - }.call() - - def REACT_NATIVE_BUILD_FROM_SOURCE = findProject(":packages:react-native:ReactAndroid") != null --def REACT_NATIVE_DIR = REACT_NATIVE_BUILD_FROM_SOURCE -- ? findProject(":packages:react-native:ReactAndroid").getProjectDir().parent -- : file(providers.exec { -+def FALLBACK_REACT_NATIVE_DIR = hasProperty("reactNativeProject") ? file('../../react-native') : file(providers.exec { - workingDir(rootDir) - commandLine("node", "--print", "require.resolve('react-native/package.json')") - }.standardOutput.asText.get().trim()).parent -+def REACT_NATIVE_DIR = REACT_NATIVE_BUILD_FROM_SOURCE -+ ? findProject(":packages:react-native:ReactAndroid").getProjectDir().parent -+ : FALLBACK_REACT_NATIVE_DIR - - def reactProperties = new Properties() - file("$REACT_NATIVE_DIR/ReactAndroid/gradle.properties").withInputStream { reactProperties.load(it) } diff --git a/patches/html-entities+2.5.2.patch b/patches/html-entities+2.5.2.patch new file mode 100644 index 000000000000..0df30b6bd686 --- /dev/null +++ b/patches/html-entities+2.5.2.patch @@ -0,0 +1,9 @@ +diff --git a/node_modules/html-entities/lib/index.js b/node_modules/html-entities/lib/index.js +index 3a44c85..c7dfa67 100644 +--- a/node_modules/html-entities/lib/index.js ++++ b/node_modules/html-entities/lib/index.js +@@ -1,2 +1,3 @@ ++"worklet"; // This function is used in react-native-live-markdown parser and it must be a worklet to run in UI thread (react-native-reanimated) + "use strict";var __assign=this&&this.__assign||function(){__assign=Object.assign||function(t){for(var s,i=1,n=arguments.length;i'"&]/g,nonAscii:/[<>'"&\u0080-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/g,nonAsciiPrintable:/[<>'"&\x01-\x08\x11-\x15\x17-\x1F\x7f-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/g,nonAsciiPrintableOnly:/[\x01-\x08\x11-\x15\x17-\x1F\x7f-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/g,extensive:/[\x01-\x0c\x0e-\x1f\x21-\x2c\x2e-\x2f\x3a-\x40\x5b-\x60\x7b-\x7d\x7f-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/g};var defaultEncodeOptions={mode:"specialChars",level:"all",numeric:"decimal"};function encode(text,_a){var _b=_a===void 0?defaultEncodeOptions:_a,_c=_b.mode,mode=_c===void 0?"specialChars":_c,_d=_b.numeric,numeric=_d===void 0?"decimal":_d,_e=_b.level,level=_e===void 0?"all":_e;if(!text){return""}var encodeRegExp=encodeRegExps[mode];var references=allNamedReferences[level].characters;var isHex=numeric==="hexadecimal";return replaceUsingRegExp(text,encodeRegExp,(function(input){var result=references[input];if(!result){var code=input.length>1?surrogate_pairs_1.getCodePoint(input,0):input.charCodeAt(0);result=(isHex?"&#x"+code.toString(16):"&#"+code)+";"}return result}))}exports.encode=encode;var defaultDecodeOptions={scope:"body",level:"all"};var strict=/&(?:#\d+|#[xX][\da-fA-F]+|[0-9a-zA-Z]+);/g;var attribute=/&(?:#\d+|#[xX][\da-fA-F]+|[0-9a-zA-Z]+)[;=]?/g;var baseDecodeRegExps={xml:{strict:strict,attribute:attribute,body:named_references_1.bodyRegExps.xml},html4:{strict:strict,attribute:attribute,body:named_references_1.bodyRegExps.html4},html5:{strict:strict,attribute:attribute,body:named_references_1.bodyRegExps.html5}};var decodeRegExps=__assign(__assign({},baseDecodeRegExps),{all:baseDecodeRegExps.html5});var fromCharCode=String.fromCharCode;var outOfBoundsChar=fromCharCode(65533);var defaultDecodeEntityOptions={level:"all"};function getDecodedEntity(entity,references,isAttribute,isStrict){var decodeResult=entity;var decodeEntityLastChar=entity[entity.length-1];if(isAttribute&&decodeEntityLastChar==="="){decodeResult=entity}else if(isStrict&&decodeEntityLastChar!==";"){decodeResult=entity}else{var decodeResultByReference=references[entity];if(decodeResultByReference){decodeResult=decodeResultByReference}else if(entity[0]==="&"&&entity[1]==="#"){var decodeSecondChar=entity[2];var decodeCode=decodeSecondChar=="x"||decodeSecondChar=="X"?parseInt(entity.substr(3),16):parseInt(entity.substr(2));decodeResult=decodeCode>=1114111?outOfBoundsChar:decodeCode>65535?surrogate_pairs_1.fromCodePoint(decodeCode):fromCharCode(numeric_unicode_map_1.numericUnicodeMap[decodeCode]||decodeCode)}}return decodeResult}function decodeEntity(entity,_a){var _b=(_a===void 0?defaultDecodeEntityOptions:_a).level,level=_b===void 0?"all":_b;if(!entity){return""}return getDecodedEntity(entity,allNamedReferences[level].entities,false,false)}exports.decodeEntity=decodeEntity;function decode(text,_a){var _b=_a===void 0?defaultDecodeOptions:_a,_c=_b.level,level=_c===void 0?"all":_c,_d=_b.scope,scope=_d===void 0?level==="xml"?"strict":"body":_d;if(!text){return""}var decodeRegExp=decodeRegExps[level][scope];var references=allNamedReferences[level].entities;var isAttribute=scope==="attribute";var isStrict=scope==="strict";return replaceUsingRegExp(text,decodeRegExp,(function(entity){return getDecodedEntity(entity,references,isAttribute,isStrict)}))}exports.decode=decode; + //# sourceMappingURL=./index.js.map +\ No newline at end of file diff --git a/patches/react-native+0.75.2+026+fix-dropping-mutations-in-transactions.patch b/patches/react-native+0.75.2+026+fix-dropping-mutations-in-transactions.patch new file mode 100644 index 000000000000..974a0d090fb9 --- /dev/null +++ b/patches/react-native+0.75.2+026+fix-dropping-mutations-in-transactions.patch @@ -0,0 +1,67 @@ +diff --git a/node_modules/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp b/node_modules/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp +index 572fb3d..0efa1ed 100644 +--- a/node_modules/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp ++++ b/node_modules/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp +@@ -468,7 +468,7 @@ void Binding::schedulerDidFinishTransaction( + mountingTransaction->getSurfaceId(); + }); + +- if (pendingTransaction != pendingTransactions_.end()) { ++ if (pendingTransaction != pendingTransactions_.end() && pendingTransaction->canMergeWith(*mountingTransaction)) { + pendingTransaction->mergeWith(std::move(*mountingTransaction)); + } else { + pendingTransactions_.push_back(std::move(*mountingTransaction)); +diff --git a/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.cpp b/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.cpp +index d7dd1bc..d95d779 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.cpp ++++ b/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.cpp +@@ -5,6 +5,8 @@ + * LICENSE file in the root directory of this source tree. + */ + ++#include ++ + #include "MountingTransaction.h" + + namespace facebook::react { +@@ -54,4 +56,21 @@ void MountingTransaction::mergeWith(MountingTransaction&& transaction) { + telemetry_ = std::move(transaction.telemetry_); + } + ++bool MountingTransaction::canMergeWith(MountingTransaction& transaction) { ++ std::set deletedTags; ++ for (const auto& mutation : mutations_) { ++ if (mutation.type == ShadowViewMutation::Type::Delete) { ++ deletedTags.insert(mutation.oldChildShadowView.tag); ++ } ++ } ++ ++ for (const auto& mutation : transaction.getMutations()) { ++ if (mutation.type == ShadowViewMutation::Type::Create && deletedTags.contains(mutation.newChildShadowView.tag)) { ++ return false; ++ } ++ } ++ ++ return true; ++} ++ + } // namespace facebook::react +diff --git a/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.h b/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.h +index 277e9f4..38629db 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.h ++++ b/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.h +@@ -85,6 +85,14 @@ class MountingTransaction final { + */ + void mergeWith(MountingTransaction&& transaction); + ++ /* ++ * Checks whether the two transactions can be safely merged. Due to ++ * reordering of mutations during mount, the sequence of ++ * REMOVE -> DELETE | CREATE -> INSERT (2 transactions) may get changed to ++ * INSERT -> REMOVE -> DELETE and the state will diverge from there. ++ */ ++ bool canMergeWith(MountingTransaction& transaction); ++ + private: + SurfaceId surfaceId_; + Number number_; diff --git a/patches/react-native-draggable-flatlist+4.0.1.patch b/patches/react-native-draggable-flatlist+4.0.1.patch index 348f1aa5de8a..a3d29b66de7a 100644 --- a/patches/react-native-draggable-flatlist+4.0.1.patch +++ b/patches/react-native-draggable-flatlist+4.0.1.patch @@ -12,7 +12,7 @@ index d7d98c2..2f59c7a 100644 runOnJS(onDragEnd)({ from: activeIndexAnim.value, diff --git a/node_modules/react-native-draggable-flatlist/src/context/refContext.tsx b/node_modules/react-native-draggable-flatlist/src/context/refContext.tsx -index ea21575..66c5eed 100644 +index ea21575..dc6b095 100644 --- a/node_modules/react-native-draggable-flatlist/src/context/refContext.tsx +++ b/node_modules/react-native-draggable-flatlist/src/context/refContext.tsx @@ -1,14 +1,14 @@ @@ -32,14 +32,13 @@ index ea21575..66c5eed 100644 cellDataRef: React.MutableRefObject>; keyToIndexRef: React.MutableRefObject>; containerRef: React.RefObject; -@@ -54,8 +54,8 @@ function useSetupRefs({ +@@ -54,8 +54,7 @@ function useSetupRefs({ ...DEFAULT_PROPS.animationConfig, ...animationConfig, } as WithSpringConfig; - const animationConfigRef = useRef(animConfig); - animationConfigRef.current = animConfig; + const animationConfigRef = useSharedValue(animConfig); -+ animationConfigRef.value = animConfig; const cellDataRef = useRef(new Map()); const keyToIndexRef = useRef(new Map()); @@ -57,7 +56,7 @@ index ce4ab68..efea240 100644 return translate; diff --git a/node_modules/react-native-draggable-flatlist/src/hooks/useOnCellActiveAnimation.ts b/node_modules/react-native-draggable-flatlist/src/hooks/useOnCellActiveAnimation.ts -index 7c20587..857c7d0 100644 +index 7c20587..33042e9 100644 --- a/node_modules/react-native-draggable-flatlist/src/hooks/useOnCellActiveAnimation.ts +++ b/node_modules/react-native-draggable-flatlist/src/hooks/useOnCellActiveAnimation.ts @@ -1,8 +1,9 @@ @@ -72,18 +71,17 @@ index 7c20587..857c7d0 100644 } from "react-native-reanimated"; import { DEFAULT_ANIMATION_CONFIG } from "../constants"; import { useAnimatedValues } from "../context/animatedValueContext"; -@@ -15,8 +16,8 @@ type Params = { +@@ -15,8 +16,7 @@ type Params = { export function useOnCellActiveAnimation( { animationConfig }: Params = { animationConfig: {} } ) { - const animationConfigRef = useRef(animationConfig); - animationConfigRef.current = animationConfig; + const animationConfigRef = useSharedValue(animationConfig); -+ animationConfigRef.value = animationConfig; const isActive = useIsActive(); -@@ -26,7 +27,7 @@ export function useOnCellActiveAnimation( +@@ -26,7 +26,7 @@ export function useOnCellActiveAnimation( const toVal = isActive && isTouchActiveNative.value ? 1 : 0; return withSpring(toVal, { ...DEFAULT_ANIMATION_CONFIG, diff --git a/patches/react-native-plaid-link-sdk+11.11.0.patch b/patches/react-native-plaid-link-sdk+11.11.0.patch index 28e492f6999f..39ae7b3cd1e7 100644 --- a/patches/react-native-plaid-link-sdk+11.11.0.patch +++ b/patches/react-native-plaid-link-sdk+11.11.0.patch @@ -23,7 +23,7 @@ index 7c60081..4a13a3c 100644 # we don't want this to be seen by Swift s.private_header_files = 'ios/PLKFabricHelpers.h' -+ if ENV['USE_FRAMEWORKS'] == '1' ++ if ENV['USE_FRAMEWORKS'] != nil + s.pod_target_xcconfig = { + "OTHER_CFLAGS" => "$(inherited) -DUSE_FRAMEWORKS", + "OTHER_CPLUSPLUSFLAGS" => "$(inherited) -DUSE_FRAMEWORKS", diff --git a/patches/react-native-reanimated+3.16.3+001+hybrid-app.patch b/patches/react-native-reanimated+3.16.3+001+hybrid-app.patch deleted file mode 100644 index 835df1f034a9..000000000000 --- a/patches/react-native-reanimated+3.16.3+001+hybrid-app.patch +++ /dev/null @@ -1,17 +0,0 @@ -diff --git a/node_modules/react-native-reanimated/scripts/reanimated_utils.rb b/node_modules/react-native-reanimated/scripts/reanimated_utils.rb -index 9fc7b15..e453d84 100644 ---- a/node_modules/react-native-reanimated/scripts/reanimated_utils.rb -+++ b/node_modules/react-native-reanimated/scripts/reanimated_utils.rb -@@ -17,7 +17,11 @@ def find_config() - :react_native_reanimated_dir_from_pods_root => nil, - } - -- react_native_node_modules_dir = File.join(File.dirname(`cd "#{Pod::Config.instance.installation_root.to_s}" && node --print "require.resolve('react-native/package.json')"`), '..') -+ root_project = Pod::Config.instance.installation_root.to_s -+ if(ENV['PROJECT_ROOT_DIR']) -+ root_project = ENV['PROJECT_ROOT_DIR'] -+ end -+ react_native_node_modules_dir = File.join(File.dirname(`cd "#{root_project}" && node --print "require.resolve('react-native/package.json')"`), '..') - react_native_json = try_to_parse_react_native_package_json(react_native_node_modules_dir) - - if react_native_json == nil diff --git a/patches/react-native-reanimated+3.16.3+002+dontWhitelistTextProp.patch b/patches/react-native-reanimated+3.16.4+002+dontWhitelistTextProp.patch similarity index 95% rename from patches/react-native-reanimated+3.16.3+002+dontWhitelistTextProp.patch rename to patches/react-native-reanimated+3.16.4+002+dontWhitelistTextProp.patch index 6084dca4adc8..583cc7015ee4 100644 --- a/patches/react-native-reanimated+3.16.3+002+dontWhitelistTextProp.patch +++ b/patches/react-native-reanimated+3.16.4+002+dontWhitelistTextProp.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/react-native-reanimated/src/component/PerformanceMonitor.tsx b/node_modules/react-native-reanimated/src/component/PerformanceMonitor.tsx -index 38e3d39..9936670 100644 +index d4b31f2..ced6561 100644 --- a/node_modules/react-native-reanimated/src/component/PerformanceMonitor.tsx +++ b/node_modules/react-native-reanimated/src/component/PerformanceMonitor.tsx @@ -46,7 +46,6 @@ function createCircularDoublesBuffer(size: number) { diff --git a/patches/react-native-screens+3.34.0+004+ios-custom-animations-native-transitions.patch b/patches/react-native-screens+3.34.0+004+ios-custom-animations-native-transitions.patch index 62cbf68f458d..52f8d76c4fe1 100644 --- a/patches/react-native-screens+3.34.0+004+ios-custom-animations-native-transitions.patch +++ b/patches/react-native-screens+3.34.0+004+ios-custom-animations-native-transitions.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/react-native-screens/ios/RNSScreenStackAnimator.mm b/node_modules/react-native-screens/ios/RNSScreenStackAnimator.mm -index abb2cf6..fb81d52 100644 +index abb2cf6..c21b3e9 100644 --- a/node_modules/react-native-screens/ios/RNSScreenStackAnimator.mm +++ b/node_modules/react-native-screens/ios/RNSScreenStackAnimator.mm @@ -5,13 +5,14 @@ @@ -32,7 +32,7 @@ index abb2cf6..fb81d52 100644 } @@ -129,6 +130,8 @@ - (void)animateSimplePushWithShadowEnabled:(BOOL)shadowEnabled } - + [UIView animateWithDuration:[self transitionDuration:transitionContext] + delay:0 + options:UIViewAnimationOptionCurveDefaultTransition @@ -66,25 +66,7 @@ index abb2cf6..fb81d52 100644 animations:animationBlock completion:completionBlock]; } else { -@@ -251,6 +260,8 @@ - (void)animateFadeWithTransitionContext:(id; replaceAnimation?: WithDefault; swipeDirection?: WithDefault; - hideKeyboardOnSwipe?: boolean; \ No newline at end of file + hideKeyboardOnSwipe?: boolean; diff --git a/react-native.config.js b/react-native.config.js index 6d6dd3f5805f..773375378acd 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -1,7 +1,10 @@ +const iosSourceDir = process.env.PROJECT_ROOT_PATH ? process.env.PROJECT_ROOT_PATH + 'ios' : 'ios'; +const androidSourceDir = process.env.PROJECT_ROOT_PATH ? process.env.PROJECT_ROOT_PATH + 'android' : 'android'; + module.exports = { project: { - ios: {sourceDir: 'ios'}, - android: {}, + ios: {sourceDir: iosSourceDir}, + android: {sourceDir: androidSourceDir}, }, assets: ['./assets/fonts/native'], }; diff --git a/scripts/applyPatches.sh b/scripts/applyPatches.sh index 4ce023755258..fa87b4540b38 100755 --- a/scripts/applyPatches.sh +++ b/scripts/applyPatches.sh @@ -9,9 +9,20 @@ source "$SCRIPTS_DIR/shellUtils.sh" # Wrapper to run patch-package. function patchPackage { + # See if we're in the HybridApp repo + IS_HYBRID_APP_REPO=$(scripts/is-hybrid-app.sh) + NEW_DOT_FLAG="${STANDALONE_NEW_DOT:-false}" + OS="$(uname)" if [[ "$OS" == "Darwin" || "$OS" == "Linux" ]]; then npx patch-package --error-on-fail --color=always + EXIT_CODE=$? + if [[ "$IS_HYBRID_APP_REPO" == "true" && "$NEW_DOT_FLAG" == "false" ]]; then + echo -e "\n${GREEN}Applying HybridApp patches!${NC}" + npx patch-package --patch-dir 'Mobile-Expensify/patches' --error-on-fail --color=always + EXIT_CODE+=$? + fi + exit $EXIT_CODE else error "Unsupported OS: $OS" exit 1 diff --git a/scripts/clean.sh b/scripts/clean.sh new file mode 100755 index 000000000000..fbbfa070d442 --- /dev/null +++ b/scripts/clean.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e + +BLUE='\033[1;34m' +NC='\033[0m' + +# See if we're in the HybridApp repo +IS_HYBRID_APP_REPO=$(scripts/is-hybrid-app.sh) + +# See if we should force standalone NewDot build +NEW_DOT_FLAG="${STANDALONE_NEW_DOT:-false}" + +if [[ "$IS_HYBRID_APP_REPO" == "true" && "$NEW_DOT_FLAG" == "false" ]]; then + echo -e "${BLUE}Cleaning HybridApp project...${NC}" + # Navigate to Mobile-Expensify repository, and clean + cd Mobile-Expensify + npm run clean -- "$@" +else + # Clean NewDot + echo -e "${BLUE}Cleaning standalone NewDot project...${NC}" + npx react-native clean-project-auto +fi diff --git a/scripts/is-hybrid-app.sh b/scripts/is-hybrid-app.sh new file mode 100755 index 000000000000..32ca190ac832 --- /dev/null +++ b/scripts/is-hybrid-app.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -e + +if [[ ! -d Mobile-Expensify ]]; then + echo false + exit 0 +else + cd Mobile-Expensify +fi + +# Check if 'package.json' exists +if [[ -f package.json ]]; then + # Read the 'name' field from 'package.json' + package_name=$(jq -r '.name' package.json 2>/dev/null) + + # Check if the 'name' field is 'mobile-expensify' + if [[ "$package_name" == "mobile-expensify" ]]; then + echo true + exit 0 + fi +else + echo "package.json not found in Mobile-Expensify" + echo false +fi diff --git a/scripts/pod-install.sh b/scripts/pod-install.sh index cb2976d64587..77237bb207b4 100755 --- a/scripts/pod-install.sh +++ b/scripts/pod-install.sh @@ -8,6 +8,9 @@ # Exit immediately if any command exits with a non-zero status set -e +BLUE='\033[1;34m' +NC='\033[0m' + # Go to project root START_DIR="$(pwd)" ROOT_DIR="$(dirname "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)")" @@ -40,6 +43,23 @@ if ! yq --version > /dev/null 2>&1; then cleanupAndExit 1 fi +# See if we're in the HybridApp repo +IS_HYBRID_APP_REPO=$(scripts/is-hybrid-app.sh) + +# See if we should force standalone NewDot build +NEW_DOT_FLAG="${STANDALONE_NEW_DOT:-false}" + +if [[ "$IS_HYBRID_APP_REPO" == "true" && "$NEW_DOT_FLAG" == "false" ]]; then + echo -e "${BLUE}Executing npm run pod-install for HybridApp...${NC}" + # Navigate to the OldDot repository, and run bundle install and pod install + cd Mobile-Expensify/ios + bundle install + bundle exec pod install + exit 0 +fi + +echo -e "${BLUE}Executing npm run pod-install for standalone NewDot...${NC}" + CACHED_PODSPEC_DIR='ios/Pods/Local Podspecs' if [ -d "$CACHED_PODSPEC_DIR" ]; then info "Verifying pods from Podfile.lock match local podspecs..." diff --git a/scripts/postInstall.sh b/scripts/postInstall.sh index 782c8ef5822c..c2adcadc4f43 100755 --- a/scripts/postInstall.sh +++ b/scripts/postInstall.sh @@ -7,6 +7,20 @@ set -e ROOT_DIR=$(dirname "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)") cd "$ROOT_DIR" || exit 1 +# See if we're in the HybridApp repo +IS_HYBRID_APP_REPO=$(scripts/is-hybrid-app.sh) + +# See if we should force standalone NewDot build +NEW_DOT_FLAG="${STANDALONE_NEW_DOT:-false}" + +if [[ "$IS_HYBRID_APP_REPO" == "true" && "$NEW_DOT_FLAG" == "false" ]]; then + echo -e "\n${GREEN}Installing node modules in Mobile-Expensify submodule!${NC}" + cd Mobile-Expensify || exit 1 + npm i + + cd "$ROOT_DIR" || exit 1 +fi + # Apply packages using patch-package scripts/applyPatches.sh diff --git a/scripts/run-build.sh b/scripts/run-build.sh new file mode 100755 index 000000000000..fd38f3c98861 --- /dev/null +++ b/scripts/run-build.sh @@ -0,0 +1,68 @@ +#!/bin/bash +set -e + +export PROJECT_ROOT_PATH + +IOS_MODE="DebugDevelopment" +ANDROID_MODE="developmentDebug" +SCHEME="New Expensify Dev" +APP_ID="com.expensify.chat.dev" + +GREEN='\033[1;32m' +RED='\033[1;31m' +NC='\033[0m' + +# Function to print error message and exit +function print_error_and_exit { + echo -e "${RED}Error: Invalid invocation. Please use one of: [ios, ipad, ipad-sm, android].${NC}" + exit 1 +} + +# Assign the arguments to variables if arguments are correct +if [ "$#" -ne 1 ] || [[ "$1" != "--ios" && "$1" != "--ipad" && "$1" != "--ipad-sm" && "$1" != "--android" ]]; then + print_error_and_exit +fi + +BUILD="$1" + +# See if we're in the HybridApp repo +IS_HYBRID_APP_REPO=$(scripts/is-hybrid-app.sh) + +# See if we should force standalone NewDot build +NEW_DOT_FLAG="${STANDALONE_NEW_DOT:-false}" + + if [[ "$IS_HYBRID_APP_REPO" == "true" && "$NEW_DOT_FLAG" == "false" ]]; then + # Set HybridApp-specific arguments + IOS_MODE="Debug" + ANDROID_MODE="Debug" + SCHEME="Expensify" + APP_ID="org.me.mobiexpensifyg" + + echo -e "\n${GREEN}Starting a HybridApp build!${NC}" + PROJECT_ROOT_PATH="Mobile-Expensify/" + export CUSTOM_APK_NAME="Expensify-debug.apk" +else + echo -e "\n${GREEN}Starting a standalone NewDot build!${NC}" + echo $ANDROID_MODE + PROJECT_ROOT_PATH="./" + unset CUSTOM_APK_NAME +fi + +# Check if the argument is one of the desired values +case "$BUILD" in + --ios) + npx react-native run-ios --list-devices --mode $IOS_MODE --scheme "$SCHEME" + ;; + --ipad) + npx react-native run-ios --simulator "iPad Pro (12.9-inch) (6th generation)" --mode $IOS_MODE --scheme "$SCHEME" + ;; + --ipad-sm) + npx react-native run-ios --simulator "iPad Pro (11-inch) (4th generation)" --mode $IOS_MODE --scheme "$SCHEME" + ;; + --android) + npx react-native run-android --mode $ANDROID_MODE --appId $APP_ID --active-arch-only + ;; + *) + print_error_and_exit + ;; +esac diff --git a/src/CONST.ts b/src/CONST.ts index 9e60ec088695..8a1e9aed461a 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -297,6 +297,9 @@ type OnboardingMessage = { type?: string; }; +const EMAIL_WITH_OPTIONAL_DOMAIN = + /(?=((?=[\w'#%+-]+(?:\.[\w'#%+-]+)*@?)[\w.'#%+-]{1,64}(?:@(?:(?=[a-z\d]+(?:-+[a-z\d]+)*\.)(?:[a-z\d-]{1,63}\.)+[a-z]{2,63}))?(?= |_|\b))(?.*))\S{3,254}(?=\k$)/; + const CONST = { HEIC_SIGNATURES: [ '6674797068656963', // 'ftypheic' - Indicates standard HEIC file @@ -341,11 +344,14 @@ const CONST = { ANIMATION_GYROSCOPE_VALUE: 0.4, ANIMATION_PAID_DURATION: 200, ANIMATION_PAID_CHECKMARK_DELAY: 300, + ANIMATION_THUMBSUP_DURATION: 250, + ANIMATION_THUMBSUP_DELAY: 200, ANIMATION_PAID_BUTTON_HIDE_DELAY: 1000, BACKGROUND_IMAGE_TRANSITION_DURATION: 1000, SCREEN_TRANSITION_END_TIMEOUT: 1000, ARROW_HIDE_DELAY: 3000, MAX_IMAGE_CANVAS_AREA: 16777216, + CHUNK_LOAD_ERROR: 'ChunkLoadError', API_ATTACHMENT_VALIDATIONS: { // 24 megabytes in bytes, this is limit set on servers, do not update without wider internal discussion @@ -508,6 +514,7 @@ const CONST = { MAX_DATE: '9999-12-31', MIN_DATE: '0001-01-01', ORDINAL_DAY_OF_MONTH: 'do', + MONTH_DAY_YEAR_ORDINAL_FORMAT: 'MMMM do, yyyy', }, SMS: { DOMAIN: '@expensify.sms', @@ -894,13 +901,8 @@ const CONST = { DEEP_DIVE_EXPENSIFY_CARD: 'https://community.expensify.com/discussion/4848/deep-dive-expensify-card-and-quickbooks-online-auto-reconciliation-how-it-works', DEEP_DIVE_ERECEIPTS: 'https://community.expensify.com/discussion/5542/deep-dive-what-are-ereceipts/', DEEP_DIVE_PER_DIEM: 'https://community.expensify.com/discussion/4772/how-to-add-a-single-rate-per-diem', + SET_NOTIFICATION_LINK: 'https://community.expensify.com/discussion/5651/deep-dive--practices-when-youre-running-into-trouble-receiving-emails-from-expensify', GITHUB_URL: 'https://github.com/Expensify/App', - TERMS_URL: `${EXPENSIFY_URL}/terms`, - PRIVACY_URL: `${EXPENSIFY_URL}/privacy`, - LICENSES_URL: `${USE_EXPENSIFY_URL}/licenses`, - ACH_TERMS_URL: `${EXPENSIFY_URL}/achterms`, - WALLET_AGREEMENT_URL: `${EXPENSIFY_URL}/expensify-payments-wallet-terms-of-service`, - BANCORP_WALLET_AGREEMENT_URL: `${EXPENSIFY_URL}/bancorp-bank-wallet-terms-of-service`, HELP_LINK_URL: `${USE_EXPENSIFY_URL}/usa-patriot-act`, ELECTRONIC_DISCLOSURES_URL: `${USE_EXPENSIFY_URL}/esignagreement`, GITHUB_RELEASE_URL: 'https://api.github.com/repos/expensify/app/releases/latest', @@ -945,7 +947,14 @@ const CONST = { EMPLOYEE_TOUR_PRODUCTION: 'https://expensify.navattic.com/35609gb', EMPLOYEE_TOUR_STAGING: 'https://expensify.navattic.com/cf15002s', }, - + OLD_DOT_PUBLIC_URLS: { + TERMS_URL: `${EXPENSIFY_URL}/terms`, + PRIVACY_URL: `${EXPENSIFY_URL}/privacy`, + LICENSES_URL: `${USE_EXPENSIFY_URL}/licenses`, + ACH_TERMS_URL: `${EXPENSIFY_URL}/achterms`, + WALLET_AGREEMENT_URL: `${EXPENSIFY_URL}/expensify-payments-wallet-terms-of-service`, + BANCORP_WALLET_AGREEMENT_URL: `${EXPENSIFY_URL}/bancorp-bank-wallet-terms-of-service`, + }, OLDDOT_URLS: { ADMIN_POLICIES_URL: 'admin_policies', ADMIN_DOMAINS_URL: 'admin_domains', @@ -1045,6 +1054,7 @@ const CONST = { MODIFIED_EXPENSE: 'MODIFIEDEXPENSE', MOVED: 'MOVED', OUTDATED_BANK_ACCOUNT: 'OUTDATEDBANKACCOUNT', // OldDot Action + REIMBURSED: 'REIMBURSED', REIMBURSEMENT_ACH_BOUNCE: 'REIMBURSEMENTACHBOUNCE', // OldDot Action REIMBURSEMENT_ACH_CANCELLED: 'REIMBURSEMENTACHCANCELLED', // OldDot Action REIMBURSEMENT_ACCOUNT_CHANGED: 'REIMBURSEMENTACCOUNTCHANGED', // OldDot Action @@ -1142,6 +1152,7 @@ const CONST = { UPDATE_TIME_RATE: 'POLICYCHANGELOG_UPDATE_TIME_RATE', LEAVE_POLICY: 'POLICYCHANGELOG_LEAVE_POLICY', CORPORATE_UPGRADE: 'POLICYCHANGELOG_CORPORATE_UPGRADE', + TEAM_DOWNGRADE: 'POLICYCHANGELOG_TEAM_DOWNGRADE', }, ROOM_CHANGE_LOG: { INVITE_TO_ROOM: 'INVITETOROOM', @@ -1321,10 +1332,13 @@ const CONST = { TEST_TOOLS_MODAL_THROTTLE_TIME: 800, TOOLTIP_SENSE: 1000, TRIE_INITIALIZATION: 'trie_initialization', - COMMENT_LENGTH_DEBOUNCE_TIME: 500, + COMMENT_LENGTH_DEBOUNCE_TIME: 1500, SEARCH_OPTION_LIST_DEBOUNCE_TIME: 300, RESIZE_DEBOUNCE_TIME: 100, UNREAD_UPDATE_DEBOUNCE_TIME: 300, + SEARCH_CONVERT_SEARCH_VALUES: 'search_convert_search_values', + SEARCH_MAKE_TREE: 'search_make_tree', + SEARCH_BUILD_TREE: 'search_build_tree', SEARCH_FILTER_OPTIONS: 'search_filter_options', USE_DEBOUNCED_STATE_DELAY: 300, LIST_SCROLLING_DEBOUNCE_TIME: 200, @@ -3055,6 +3069,14 @@ const CONST = { get EXPENSIFY_POLICY_DOMAIN_NAME() { return new RegExp(`${EXPENSIFY_POLICY_DOMAIN}([a-zA-Z0-9]+)\\${EXPENSIFY_POLICY_DOMAIN_EXTENSION}`); }, + + /** + * Matching task rule by group + * Group 1: Start task rule with [] + * Group 2: Optional email group between \s+....\s* start rule with @+valid email or short mention + * Group 3: Title is remaining characters + */ + TASK_TITLE_WITH_OPTONAL_SHORT_MENTION: `^\\[\\]\\s+(?:@(?:${EMAIL_WITH_OPTIONAL_DOMAIN}))?\\s*([\\s\\S]*)`, }, PRONOUNS: { @@ -3184,7 +3206,6 @@ const CONST = { CANCEL_PAYMENT: 'cancelPayment', UNAPPROVE: 'unapprove', DEBUG: 'debug', - GO_TO_WORKSPACE: 'goToWorkspace', }, EDIT_REQUEST_FIELD: { AMOUNT: 'amount', @@ -4441,7 +4462,7 @@ const CONST = { BOOK_TRAVEL_DEMO_URL: 'https://calendly.com/d/ck2z-xsh-q97/expensify-travel-demo-travel-page', TRAVEL_DOT_URL: 'https://travel.expensify.com', STAGING_TRAVEL_DOT_URL: 'https://staging.travel.expensify.com', - TRIP_ID_PATH: (tripID: string) => `trips/${tripID}`, + TRIP_ID_PATH: (tripID?: string) => (tripID ? `trips/${tripID}` : undefined), SPOTNANA_TMC_ID: '8e8e7258-1cf3-48c0-9cd1-fe78a6e31eed', STAGING_SPOTNANA_TMC_ID: '7a290c6e-5328-4107-aff6-e48765845b81', SCREEN_READER_STATES: { @@ -5942,6 +5963,7 @@ const CONST = { CAR: 'car', HOTEL: 'hotel', FLIGHT: 'flight', + TRAIN: 'train', }, DOT_SEPARATOR: '•', @@ -5970,6 +5992,7 @@ const CONST = { DOWNLOADS_PATH: '/Downloads', DOWNLOADS_TIMEOUT: 5000, NEW_EXPENSIFY_PATH: '/New Expensify', + RECEIPTS_UPLOAD_PATH: '/Receipts-Upload', ENVIRONMENT_SUFFIX: { DEV: ' Dev', @@ -6096,6 +6119,33 @@ const CONST = { AUTOCOMPLETE_SUGGESTION: 'autocompleteSuggestion', SEARCH: 'searchItem', }, + SEARCH_USER_FRIENDLY_KEYS: { + TYPE: 'type', + STATUS: 'status', + SORT_BY: 'sort-by', + SORT_ORDER: 'sort-order', + POLICY_ID: 'workspace', + DATE: 'date', + AMOUNT: 'amount', + EXPENSE_TYPE: 'expense-type', + CURRENCY: 'currency', + MERCHANT: 'merchant', + DESCRIPTION: 'description', + FROM: 'from', + TO: 'to', + CATEGORY: 'category', + TAG: 'tag', + TAX_RATE: 'tax-rate', + CARD_ID: 'card', + REPORT_ID: 'reportid', + KEYWORD: 'keyword', + IN: 'in', + SUBMITTED: 'submitted', + APPROVED: 'approved', + PAID: 'paid', + EXPORTED: 'exported', + POSTED: 'posted', + }, DATE_MODIFIERS: { BEFORE: 'Before', AFTER: 'After', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 45d636c0b1df..86d4c90d67ed 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -104,6 +104,12 @@ const ONYXKEYS = { /** Store the information of magic code */ VALIDATE_ACTION_CODE: 'validate_action_code', + /** A list of policies that a user can join */ + JOINABLE_POLICIES: 'joinablePolicies', + + /** Flag to indicate if the joinablePolicies are loading */ + JOINABLE_POLICIES_LOADING: 'joinablePoliciesLoading', + /** Information about the current session (authToken, accountID, email, loading, error) */ SESSION: 'session', STASHED_SESSION: 'stashedSession', @@ -464,6 +470,9 @@ const ONYXKEYS = { /** The user's Concierge reportID */ CONCIERGE_REPORT_ID: 'conciergeReportID', + /** The user's session that will be preserved when using imported state */ + PRESERVED_USER_SESSION: 'preservedUserSession', + /** Collection Keys */ COLLECTION: { DOWNLOAD: 'download_', @@ -724,6 +733,8 @@ const ONYXKEYS = { RULES_MAX_EXPENSE_AGE_FORM_DRAFT: 'rulesMaxExpenseAgeFormDraft', DEBUG_DETAILS_FORM: 'debugDetailsForm', DEBUG_DETAILS_FORM_DRAFT: 'debugDetailsFormDraft', + WORKSPACE_PER_DIEM_FORM: 'workspacePerDiemForm', + WORKSPACE_PER_DIEM_FORM_DRAFT: 'workspacePerDiemFormDraft', }, } as const; @@ -817,6 +828,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.RULES_MAX_EXPENSE_AGE_FORM]: FormTypes.RulesMaxExpenseAgeForm; [ONYXKEYS.FORMS.SEARCH_SAVED_SEARCH_RENAME_FORM]: FormTypes.SearchSavedSearchRenameForm; [ONYXKEYS.FORMS.DEBUG_DETAILS_FORM]: FormTypes.DebugReportForm | FormTypes.DebugReportActionForm | FormTypes.DebugTransactionForm | FormTypes.DebugTransactionViolationForm; + [ONYXKEYS.FORMS.WORKSPACE_PER_DIEM_FORM]: FormTypes.WorkspacePerDiemForm; }; type OnyxFormDraftValuesMapping = { @@ -878,8 +890,7 @@ type OnyxValuesMapping = { [ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID]: string; [ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER]: boolean; - // NVP_ONBOARDING is an array for old users. - [ONYXKEYS.NVP_ONBOARDING]: Onboarding | []; + [ONYXKEYS.NVP_ONBOARDING]: Onboarding; // ONYXKEYS.NVP_TRYNEWDOT is HybridApp onboarding data [ONYXKEYS.NVP_TRYNEWDOT]: OnyxTypes.TryNewDot; @@ -913,6 +924,8 @@ type OnyxValuesMapping = { [ONYXKEYS.LOGIN_LIST]: OnyxTypes.LoginList; [ONYXKEYS.PENDING_CONTACT_ACTION]: OnyxTypes.PendingContactAction; [ONYXKEYS.VALIDATE_ACTION_CODE]: OnyxTypes.ValidateMagicCodeAction; + [ONYXKEYS.JOINABLE_POLICIES]: OnyxTypes.JoinablePolicies; + [ONYXKEYS.JOINABLE_POLICIES_LOADING]: boolean; [ONYXKEYS.SESSION]: OnyxTypes.Session; [ONYXKEYS.USER_METADATA]: OnyxTypes.UserMetadata; [ONYXKEYS.STASHED_SESSION]: OnyxTypes.Session; @@ -1030,6 +1043,7 @@ type OnyxValuesMapping = { [ONYXKEYS.SHOULD_SHOW_SAVED_SEARCH_RENAME_TOOLTIP]: boolean; [ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES]: Record; [ONYXKEYS.CONCIERGE_REPORT_ID]: string; + [ONYXKEYS.PRESERVED_USER_SESSION]: OnyxTypes.Session; [ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING]: OnyxTypes.DismissedProductTraining; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 4abd5c6d3d49..58d28a46a7b8 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -716,6 +716,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/profile/address', getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/profile/address` as const, backTo), }, + WORKSPACE_PROFILE_PLAN: { + route: 'settings/workspaces/:policyID/profile/plan', + getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/profile/plan` as const, backTo), + }, WORKSPACE_ACCOUNTING: { route: 'settings/workspaces/:policyID/accounting', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting` as const, @@ -979,9 +983,9 @@ const ROUTES = { getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/category/${encodeURIComponent(categoryName)}` as const, }, WORKSPACE_UPGRADE: { - route: 'settings/workspaces/:policyID/upgrade/:featureName', - getRoute: (policyID: string, featureName: string, backTo?: string) => - getUrlWithBackToParam(`settings/workspaces/${policyID}/upgrade/${encodeURIComponent(featureName)}` as const, backTo), + route: 'settings/workspaces/:policyID/upgrade/:featureName?', + getRoute: (policyID: string, featureName?: string, backTo?: string) => + getUrlWithBackToParam(`settings/workspaces/${policyID}/upgrade/${encodeURIComponent(featureName ?? '')}` as const, backTo), }, WORKSPACE_DOWNGRADE: { route: 'settings/workspaces/:policyID/downgrade/', @@ -1162,16 +1166,16 @@ const ROUTES = { }, WORKSPACE_REPORT_FIELDS_LIST_VALUES: { route: 'settings/workspaces/:policyID/reportFields/listValues/:reportFieldID?', - getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/listValues/${encodeURIComponent(reportFieldID ?? '')}` as const, + getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/listValues/${reportFieldID ? encodeURIComponent(reportFieldID) : ''}` as const, }, WORKSPACE_REPORT_FIELDS_ADD_VALUE: { route: 'settings/workspaces/:policyID/reportFields/addValue/:reportFieldID?', - getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/addValue/${encodeURIComponent(reportFieldID ?? '')}` as const, + getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/addValue/${reportFieldID ? encodeURIComponent(reportFieldID) : ''}` as const, }, WORKSPACE_REPORT_FIELDS_VALUE_SETTINGS: { route: 'settings/workspaces/:policyID/reportFields/:valueIndex/:reportFieldID?', getRoute: (policyID: string, valueIndex: number, reportFieldID?: string) => - `settings/workspaces/${policyID}/reportFields/${valueIndex}/${encodeURIComponent(reportFieldID ?? '')}` as const, + `settings/workspaces/${policyID}/reportFields/${valueIndex}/${reportFieldID ? encodeURIComponent(reportFieldID) : ''}` as const, }, WORKSPACE_REPORT_FIELDS_EDIT_VALUE: { route: 'settings/workspaces/:policyID/reportFields/new/:valueIndex/edit', @@ -1321,6 +1325,26 @@ const ROUTES = { route: 'settings/workspaces/:policyID/per-diem/settings', getRoute: (policyID: string) => `settings/workspaces/${policyID}/per-diem/settings` as const, }, + WORKSPACE_PER_DIEM_DETAILS: { + route: 'settings/workspaces/:policyID/per-diem/details/:rateID/:subRateID', + getRoute: (policyID: string, rateID: string, subRateID: string) => `settings/workspaces/${policyID}/per-diem/details/${rateID}/${subRateID}` as const, + }, + WORKSPACE_PER_DIEM_EDIT_DESTINATION: { + route: 'settings/workspaces/:policyID/per-diem/edit/destination/:rateID/:subRateID', + getRoute: (policyID: string, rateID: string, subRateID: string) => `settings/workspaces/${policyID}/per-diem/edit/destination/${rateID}/${subRateID}` as const, + }, + WORKSPACE_PER_DIEM_EDIT_SUBRATE: { + route: 'settings/workspaces/:policyID/per-diem/edit/subrate/:rateID/:subRateID', + getRoute: (policyID: string, rateID: string, subRateID: string) => `settings/workspaces/${policyID}/per-diem/edit/subrate/${rateID}/${subRateID}` as const, + }, + WORKSPACE_PER_DIEM_EDIT_AMOUNT: { + route: 'settings/workspaces/:policyID/per-diem/edit/amount/:rateID/:subRateID', + getRoute: (policyID: string, rateID: string, subRateID: string) => `settings/workspaces/${policyID}/per-diem/edit/amount/${rateID}/${subRateID}` as const, + }, + WORKSPACE_PER_DIEM_EDIT_CURRENCY: { + route: 'settings/workspaces/:policyID/per-diem/edit/currency/:rateID/:subRateID', + getRoute: (policyID: string, rateID: string, subRateID: string) => `settings/workspaces/${policyID}/per-diem/edit/currency/${rateID}/${subRateID}` as const, + }, RULES_CUSTOM_NAME: { route: 'settings/workspaces/:policyID/rules/name', getRoute: (policyID: string) => `settings/workspaces/${policyID}/rules/name` as const, @@ -1365,6 +1389,15 @@ const ROUTES = { TRAVEL_MY_TRIPS: 'travel', TRAVEL_TCS: 'travel/terms', TRACK_TRAINING_MODAL: 'track-training', + TRAVEL_TRIP_SUMMARY: { + route: 'r/:reportID/trip/:transactionID', + getRoute: (reportID: string, transactionID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/trip/${transactionID}`, backTo), + }, + TRAVEL_TRIP_DETAILS: { + route: 'r/:reportID/trip/:transactionID/:reservationIndex', + getRoute: (reportID: string, transactionID: string, reservationIndex: number, backTo?: string) => + getUrlWithBackToParam(`r/${reportID}/trip/${transactionID}/${reservationIndex}`, backTo), + }, ONBOARDING_ROOT: { route: 'onboarding', getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding`, backTo), @@ -1373,6 +1406,10 @@ const ROUTES = { route: 'onboarding/personal-details', getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding/personal-details`, backTo), }, + ONBOARDING_PRIVATE_DOMAIN: { + route: 'onboarding/private-domain', + getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding/private-domain`, backTo), + }, ONBOARDING_EMPLOYEES: { route: 'onboarding/employees', getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding/employees`, backTo), @@ -1385,6 +1422,10 @@ const ROUTES = { route: 'onboarding/purpose', getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding/purpose`, backTo), }, + ONBOARDING_WORKSPACES: { + route: 'onboarding/join-workspaces', + getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding/join-workspaces`, backTo), + }, WELCOME_VIDEO_ROOT: 'onboarding/welcome-video', EXPLANATION_MODAL_ROOT: 'onboarding/explanation', MIGRATED_USER_WELCOME_MODAL: 'onboarding/migrated-user-welcome', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 47090bd7075b..6274be1044b4 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -27,6 +27,8 @@ const SCREENS = { TRAVEL: { MY_TRIPS: 'Travel_MyTrips', TCS: 'Travel_TCS', + TRIP_SUMMARY: 'Travel_TripSummary', + TRIP_DETAILS: 'Travel_TripDetails', }, SEARCH: { CENTRAL_PANE: 'Search_Central_Pane', @@ -503,6 +505,7 @@ const SCREENS = { TAG_GL_CODE: 'Tag_GL_Code', CURRENCY: 'Workspace_Profile_Currency', ADDRESS: 'Workspace_Profile_Address', + PLAN: 'Workspace_Profile_Plan_Type', WORKFLOWS: 'Workspace_Workflows', WORKFLOWS_PAYER: 'Workspace_Workflows_Payer', WORKFLOWS_APPROVALS_NEW: 'Workspace_Approvals_New', @@ -555,6 +558,11 @@ const SCREENS = { PER_DIEM_IMPORT: 'Per_Diem_Import', PER_DIEM_IMPORTED: 'Per_Diem_Imported', PER_DIEM_SETTINGS: 'Per_Diem_Settings', + PER_DIEM_DETAILS: 'Per_Diem_Details', + PER_DIEM_EDIT_DESTINATION: 'Per_Diem_Edit_Destination', + PER_DIEM_EDIT_SUBRATE: 'Per_Diem_Edit_Subrate', + PER_DIEM_EDIT_AMOUNT: 'Per_Diem_Edit_Amount', + PER_DIEM_EDIT_CURRENCY: 'Per_Diem_Edit_Currency', }, EDIT_REQUEST: { @@ -579,8 +587,10 @@ const SCREENS = { ONBOARDING: { PERSONAL_DETAILS: 'Onboarding_Personal_Details', PURPOSE: 'Onboarding_Purpose', + PRIVATE_DOMAIN: 'Onboarding_Private_Domain', EMPLOYEES: 'Onboarding_Employees', ACCOUNTING: 'Onboarding_Accounting', + WORKSPACES: 'Onboarding_Workspaces', }, WELCOME_VIDEO: { diff --git a/src/components/AddPaymentCard/PaymentCardForm.tsx b/src/components/AddPaymentCard/PaymentCardForm.tsx index 9843996602f1..bcb3e27783e8 100644 --- a/src/components/AddPaymentCard/PaymentCardForm.tsx +++ b/src/components/AddPaymentCard/PaymentCardForm.tsx @@ -48,8 +48,8 @@ function IAcceptTheLabel() { return ( {`${translate('common.iAcceptThe')}`} - {`${translate('common.addCardTermsOfService')}`} {`${translate('common.and')}`} - {` ${translate('common.privacyPolicy')} `} + {`${translate('common.addCardTermsOfService')}`} {`${translate('common.and')}`} + {` ${translate('common.privacyPolicy')} `} ); } diff --git a/src/components/AmountForm.tsx b/src/components/AmountForm.tsx index a230dfa1af8d..cf1b5b934324 100644 --- a/src/components/AmountForm.tsx +++ b/src/components/AmountForm.tsx @@ -259,6 +259,9 @@ function AmountForm( prefixCharacter={currency} prefixStyle={styles.colorMuted} keyboardType={CONST.KEYBOARD_TYPE.DECIMAL_PAD} + // On android autoCapitalize="words" is necessary when keyboardType="decimal-pad" or inputMode="decimal" to prevent input lag. + // See https://github.com/Expensify/App/issues/51868 for more information + autoCapitalize="words" inputMode={CONST.INPUT_MODE.DECIMAL} errorText={errorText} // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/src/components/AmountTextInput.tsx b/src/components/AmountTextInput.tsx index 52c32ce1f584..6be2b43c09d7 100644 --- a/src/components/AmountTextInput.tsx +++ b/src/components/AmountTextInput.tsx @@ -71,6 +71,9 @@ function AmountTextInput( value={formattedAmount} placeholder={placeholder} inputMode={CONST.INPUT_MODE.DECIMAL} + // On android autoCapitalize="words" is necessary when keyboardType="decimal-pad" or inputMode="decimal" to prevent input lag. + // See https://github.com/Expensify/App/issues/51868 for more information + autoCapitalize="words" blurOnSubmit={false} selection={selection} onSelectionChange={onSelectionChange} diff --git a/src/components/AmountWithoutCurrencyForm.tsx b/src/components/AmountWithoutCurrencyForm.tsx index 78b7c84ecb54..de65f40b3b4f 100644 --- a/src/components/AmountWithoutCurrencyForm.tsx +++ b/src/components/AmountWithoutCurrencyForm.tsx @@ -12,10 +12,13 @@ type AmountFormProps = { /** Callback to update the amount in the FormProvider */ onInputChange?: (value: string) => void; + + /** Should we allow negative number as valid input */ + shouldAllowNegative?: boolean; } & Partial; function AmountWithoutCurrencyForm( - {value: amount, onInputChange, inputID, name, defaultValue, accessibilityLabel, role, label, ...rest}: AmountFormProps, + {value: amount, onInputChange, shouldAllowNegative = false, inputID, name, defaultValue, accessibilityLabel, role, label, ...rest}: AmountFormProps, ref: ForwardedRef, ) { const {toLocaleDigit} = useLocalize(); @@ -32,13 +35,13 @@ function AmountWithoutCurrencyForm( // More info: https://github.com/Expensify/App/issues/16974 const newAmountWithoutSpaces = stripSpacesFromAmount(newAmount); const replacedCommasAmount = replaceCommasWithPeriod(newAmountWithoutSpaces); - const withLeadingZero = addLeadingZero(replacedCommasAmount); - if (!validateAmount(withLeadingZero, 2)) { + const withLeadingZero = addLeadingZero(replacedCommasAmount, shouldAllowNegative); + if (!validateAmount(withLeadingZero, 2, CONST.IOU.AMOUNT_MAX_LENGTH, shouldAllowNegative)) { return; } onInputChange?.(withLeadingZero); }, - [onInputChange], + [onInputChange, shouldAllowNegative], ); const formattedAmount = replaceAllDigits(currentAmount, toLocaleDigit); @@ -54,7 +57,10 @@ function AmountWithoutCurrencyForm( accessibilityLabel={accessibilityLabel} role={role} ref={ref} - keyboardType={CONST.KEYBOARD_TYPE.DECIMAL_PAD} + keyboardType={!shouldAllowNegative ? CONST.KEYBOARD_TYPE.DECIMAL_PAD : undefined} + // On android autoCapitalize="words" is necessary when keyboardType="decimal-pad" or inputMode="decimal" to prevent input lag. + // See https://github.com/Expensify/App/issues/51868 for more information + autoCapitalize="words" // eslint-disable-next-line react/jsx-props-no-spreading {...rest} /> diff --git a/src/components/ArchivedReportFooter.tsx b/src/components/ArchivedReportFooter.tsx index af77a20b4caa..fc5c77958635 100644 --- a/src/components/ArchivedReportFooter.tsx +++ b/src/components/ArchivedReportFooter.tsx @@ -1,7 +1,6 @@ import lodashEscape from 'lodash/escape'; import React from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import {getCurrentUserAccountID} from '@libs/actions/Report'; @@ -10,26 +9,20 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetailsList, Report, ReportAction} from '@src/types/onyx'; +import type {Report} from '@src/types/onyx'; import Banner from './Banner'; -type ArchivedReportFooterOnyxProps = { - /** The reason this report was archived */ - reportClosedAction: OnyxEntry; - - /** Personal details of all users */ - personalDetails: OnyxEntry; -}; - -type ArchivedReportFooterProps = ArchivedReportFooterOnyxProps & { +type ArchivedReportFooterProps = { /** The archived report */ report: Report; }; -function ArchivedReportFooter({report, reportClosedAction, personalDetails = {}}: ArchivedReportFooterProps) { +function ArchivedReportFooter({report}: ArchivedReportFooterProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {initialValue: {}}); + const [reportClosedAction] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, {canEvict: false, selector: ReportActionsUtils.getLastClosedReportAction}); const originalMessage = ReportActionsUtils.isClosedAction(reportClosedAction) ? ReportActionsUtils.getOriginalMessage(reportClosedAction) : null; const archiveReason = originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT; const actorPersonalDetails = personalDetails?.[reportClosedAction?.actorAccountID ?? -1]; @@ -78,13 +71,4 @@ function ArchivedReportFooter({report, reportClosedAction, personalDetails = {}} ArchivedReportFooter.displayName = 'ArchivedReportFooter'; -export default withOnyx({ - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - reportClosedAction: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, - canEvict: false, - selector: ReportActionsUtils.getLastClosedReportAction, - }, -})(ArchivedReportFooter); +export default ArchivedReportFooter; diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 3dc058c6975b..0ad6dfbb8f7f 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -1,15 +1,14 @@ import {Str} from 'expensify-common'; import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {Animated, Keyboard, View} from 'react-native'; +import {Keyboard, View} from 'react-native'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; import {useOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; -import {useSharedValue} from 'react-native-reanimated'; +import Animated, {FadeIn, useSharedValue} from 'react-native-reanimated'; import type {ValueOf} from 'type-fest'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; @@ -167,7 +166,6 @@ function AttachmentModal({ attachmentLink = '', }: AttachmentModalProps) { const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); const [isModalOpen, setIsModalOpen] = useState(defaultOpen); const [shouldLoadAttachment, setShouldLoadAttachment] = useState(false); const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false); @@ -178,7 +176,6 @@ function AttachmentModal({ const [sourceState, setSourceState] = useState(() => source); const [modalType, setModalType] = useState(CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE); const [isConfirmButtonDisabled, setIsConfirmButtonDisabled] = useState(false); - const [confirmButtonFadeAnimation] = useState(() => new Animated.Value(1)); const [isDownloadButtonReadyToBeShown, setIsDownloadButtonReadyToBeShown] = React.useState(true); const isPDFLoadError = useRef(false); const {windowWidth} = useWindowDimensions(); @@ -382,17 +379,28 @@ function AttachmentModal({ ); /** - * close the modal + * Closes the modal. + * @param {boolean} [shouldCallDirectly] If true, directly calls `onModalClose`. + * This is useful when you plan to continue navigating to another page after closing the modal, to avoid freezing the app due to navigating to another page first and dismissing the modal later. + * If `shouldCallDirectly` is false or undefined, it calls `attachmentModalHandler.handleModalClose` to close the modal. + * This ensures smooth modal closing behavior without causing delays in closing. */ - const closeModal = useCallback(() => { - setIsModalOpen(false); + const closeModal = useCallback( + (shouldCallDirectly?: boolean) => { + setIsModalOpen(false); - if (typeof onModalClose === 'function') { - attachmentModalHandler.handleModalClose(onModalClose); - } + if (typeof onModalClose === 'function') { + if (shouldCallDirectly) { + onModalClose(); + return; + } + attachmentModalHandler.handleModalClose(onModalClose); + } - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [onModalClose]); + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + }, + [onModalClose], + ); /** * open the modal @@ -422,7 +430,7 @@ function AttachmentModal({ icon: Expensicons.Camera, text: translate('common.replace'), onSelected: () => { - closeModal(); + closeModal(true); Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute( CONST.IOU.ACTION.EDIT, @@ -590,7 +598,10 @@ function AttachmentModal({ {!!onConfirm && !isConfirmButtonDisabled && ( {({safeAreaPaddingBottomStyle}) => ( - + + )} + {/** + These are the actionable buttons that appear at the bottom of a Concierge message + for example: Invite a user mentioned but not a member of the room + https://github.com/Expensify/App/issues/32741 + */} + {actionableItemButtons.length > 0 && ( + + )} + + ) : ( + + )} + + + + ); + } + const numberOfThreadReplies = action.childVisibleActionCount ?? 0; + + const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(action, isThreadReportParentAction); + const oldestFourAccountIDs = + action.childOldestFourAccountIDs + ?.split(',') + .map((accountID) => Number(accountID)) + .filter((accountID): accountID is number => typeof accountID === 'number') ?? []; + const draftMessageRightAlign = draftMessage !== undefined ? styles.chatItemReactionsDraftRight : {}; + + return ( + <> + {children} + {Permissions.canUseLinkPreviews() && !isHidden && (action.linkMetadata?.length ?? 0) > 0 && ( + + !isEmptyObject(item))} /> + + )} + {!ReportActionsUtils.isMessageDeleted(action) && ( + + { + if (Session.isAnonymousUser()) { + hideContextMenu(false); + + InteractionManager.runAfterInteractions(() => { + Session.signOutAndRedirectToSignIn(); + }); + } else { + toggleReaction(emoji, ignoreSkinToneOnCompare); + } + }} + setIsEmojiPickerActive={setIsEmojiPickerActive} + /> + + )} + + {shouldDisplayThreadReplies && ( + + + + )} + + ); + }; + + /** + * Get ReportActionItem with a proper wrapper + * @param hovered whether the ReportActionItem is hovered + * @param isWhisper whether the ReportActionItem is a whisper + * @param hasErrors whether the report action has any errors + * @returns report action item + */ + + const renderReportActionItem = (hovered: boolean, isWhisper: boolean, hasErrors: boolean): React.JSX.Element => { + const content = renderItemContent(hovered || isContextMenuActive || isEmojiPickerActive, isWhisper, hasErrors); + + if (draftMessage !== undefined) { + return {content}; + } + + if (!displayAsGroup) { + return ( + item === moderationDecision) && + !ReportActionsUtils.isPendingRemove(action) + } + > + {content} + + ); + } + + return {content}; + }; + + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { + const transactionID = ReportActionsUtils.isMoneyRequestAction(parentReportActionForTransactionThread) + ? ReportActionsUtils.getOriginalMessage(parentReportActionForTransactionThread)?.IOUTransactionID + : '-1'; + + return ( + + ); + } + if (ReportActionsUtils.isChronosOOOListAction(action)) { + return ( + + ); + } + + // For the `pay` IOU action on non-pay expense flow, we don't want to render anything if `isWaitingOnBankAccount` is true + // Otherwise, we will see two system messages informing the payee needs to add a bank account or wallet + if ( + ReportActionsUtils.isMoneyRequestAction(action) && + !!report?.isWaitingOnBankAccount && + ReportActionsUtils.getOriginalMessage(action)?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && + !isSendingMoney + ) { + return null; + } + + // If action is actionable whisper and resolved by user, then we don't want to render anything + if (isActionableWhisper && (hasResolutionInOriginalMessage ? originalMessage.resolution : null)) { + return null; + } + + // We currently send whispers to all report participants and hide them in the UI for users that shouldn't see them. + // This is a temporary solution needed for comment-linking. + // The long term solution will leverage end-to-end encryption and only targeted users will be able to decrypt. + if (ReportActionsUtils.isWhisperActionTargetedToOthers(action)) { + return null; + } + + const hasErrors = !isEmptyObject(action.errors); + const whisperedTo = ReportActionsUtils.getWhisperedTo(action); + const isMultipleParticipant = whisperedTo.length > 1; + + const iouReportID = + ReportActionsUtils.isMoneyRequestAction(action) && ReportActionsUtils.getOriginalMessage(action)?.IOUReportID + ? (ReportActionsUtils.getOriginalMessage(action)?.IOUReportID ?? '').toString() + : '-1'; + const transactionsWithReceipts = getTransactionsWithReceipts(iouReportID); + const isWhisper = whisperedTo.length > 0 && transactionsWithReceipts.length === 0; + const whisperedToPersonalDetails = isWhisper + ? (Object.values(personalDetails ?? {}).filter((details) => whisperedTo.includes(details?.accountID ?? -1)) as OnyxTypes.PersonalDetails[]) + : []; + const isWhisperOnlyVisibleByUser = isWhisper && isCurrentUserTheOnlyParticipant(whisperedTo); + const displayNamesWithTooltips = isWhisper ? ReportUtils.getDisplayNamesWithTooltips(whisperedToPersonalDetails, isMultipleParticipant) : []; + + return ( + shouldUseNarrowLayout && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + onPressOut={() => ControlSelection.unblock()} + onSecondaryInteraction={showPopover} + preventDefaultContextMenu={draftMessage === undefined && !hasErrors} + withoutFocusOnSecondaryInteraction + accessibilityLabel={translate('accessibilityHints.chatMessage')} + accessible + > + + {(hovered) => ( + + {shouldDisplayNewMarker && (!shouldUseThreadDividerLine || !isFirstVisibleReportAction) && } + {shouldDisplayContextMenu && ( + + )} + + { + const transactionID = ReportActionsUtils.isMoneyRequestAction(action) ? ReportActionsUtils.getOriginalMessage(action)?.IOUTransactionID : undefined; + if (transactionID) { + clearError(transactionID); + } + clearAllRelatedReportActionErrors(reportID, action); + }} + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + pendingAction={ + draftMessage !== undefined ? undefined : action.pendingAction ?? (action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : undefined) + } + shouldHideOnDelete={!isThreadReportParentAction} + errors={linkedTransactionRouteError ?? ErrorUtils.getLatestErrorMessageField(action as ErrorUtils.OnyxDataWithErrors)} + errorRowStyles={[styles.ml10, styles.mr2]} + needsOffscreenAlphaCompositing={ReportActionsUtils.isMoneyRequestAction(action)} + shouldDisableStrikeThrough + > + {isWhisper && ( + + + + + + {translate('reportActionContextMenu.onlyVisible')} +   + + + + )} + {renderReportActionItem(!!hovered || !!isReportActionLinked, isWhisper, hasErrors)} + + + + )} + + + + + + ); +} +export type {PureReportActionItemProps}; +export default memo(PureReportActionItem, (prevProps, nextProps) => { + const prevParentReportAction = prevProps.parentReportAction; + const nextParentReportAction = nextProps.parentReportAction; + return ( + prevProps.displayAsGroup === nextProps.displayAsGroup && + prevProps.isMostRecentIOUReportAction === nextProps.isMostRecentIOUReportAction && + prevProps.shouldDisplayNewMarker === nextProps.shouldDisplayNewMarker && + lodashIsEqual(prevProps.action, nextProps.action) && + lodashIsEqual(prevProps.report?.pendingFields, nextProps.report?.pendingFields) && + lodashIsEqual(prevProps.report?.isDeletedParentAction, nextProps.report?.isDeletedParentAction) && + lodashIsEqual(prevProps.report?.errorFields, nextProps.report?.errorFields) && + prevProps.report?.statusNum === nextProps.report?.statusNum && + prevProps.report?.stateNum === nextProps.report?.stateNum && + prevProps.report?.parentReportID === nextProps.report?.parentReportID && + prevProps.report?.parentReportActionID === nextProps.report?.parentReportActionID && + // TaskReport's created actions render the TaskView, which updates depending on certain fields in the TaskReport + ReportUtils.isTaskReport(prevProps.report) === ReportUtils.isTaskReport(nextProps.report) && + prevProps.action.actionName === nextProps.action.actionName && + prevProps.report?.reportName === nextProps.report?.reportName && + prevProps.report?.description === nextProps.report?.description && + ReportUtils.isCompletedTaskReport(prevProps.report) === ReportUtils.isCompletedTaskReport(nextProps.report) && + prevProps.report?.managerID === nextProps.report?.managerID && + prevProps.shouldHideThreadDividerLine === nextProps.shouldHideThreadDividerLine && + prevProps.report?.total === nextProps.report?.total && + prevProps.report?.nonReimbursableTotal === nextProps.report?.nonReimbursableTotal && + prevProps.report?.policyAvatar === nextProps.report?.policyAvatar && + prevProps.linkedReportActionID === nextProps.linkedReportActionID && + lodashIsEqual(prevProps.report?.fieldList, nextProps.report?.fieldList) && + lodashIsEqual(prevProps.transactionThreadReport, nextProps.transactionThreadReport) && + lodashIsEqual(prevProps.reportActions, nextProps.reportActions) && + lodashIsEqual(prevParentReportAction, nextParentReportAction) && + prevProps.draftMessage === nextProps.draftMessage && + prevProps.iouReport?.reportID === nextProps.iouReport?.reportID && + lodashIsEqual(prevProps.emojiReactions, nextProps.emojiReactions) && + lodashIsEqual(prevProps.linkedTransactionRouteError, nextProps.linkedTransactionRouteError) && + lodashIsEqual(prevProps.reportNameValuePairs, nextProps.reportNameValuePairs) && + prevProps.isUserValidated === nextProps.isUserValidated && + prevProps.parentReport?.reportID === nextProps.parentReport?.reportID && + lodashIsEqual(prevProps.personalDetails, nextProps.personalDetails) && + lodashIsEqual(prevProps.blockedFromConcierge, nextProps.blockedFromConcierge) && + prevProps.originalReportID === nextProps.originalReportID && + prevProps.isArchivedRoom === nextProps.isArchivedRoom && + prevProps.isChronosReport === nextProps.isChronosReport && + prevProps.isClosedExpenseReportWithNoExpenses === nextProps.isClosedExpenseReportWithNoExpenses && + lodashIsEqual(prevProps.missingPaymentMethod, nextProps.missingPaymentMethod) && + prevProps.reimbursementDeQueuedActionMessage === nextProps.reimbursementDeQueuedActionMessage && + prevProps.modifiedExpenseMessage === nextProps.modifiedExpenseMessage && + prevProps.userBillingFundID === nextProps.userBillingFundID && + prevProps.reportAutomaticallyForwardedMessage === nextProps.reportAutomaticallyForwardedMessage + ); +}); diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 7634a5317eac..893d2b3060d9 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -1,4 +1,5 @@ import {useNavigation} from '@react-navigation/native'; +import lodashDebounce from 'lodash/debounce'; import noop from 'lodash/noop'; import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInputFocusEventData, TextInputSelectionChangeEventData} from 'react-native'; @@ -23,6 +24,7 @@ import EducationalTooltip from '@components/Tooltip/EducationalTooltip'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDebounce from '@hooks/useDebounce'; import useHandleExceedMaxCommentLength from '@hooks/useHandleExceedMaxCommentLength'; +import useHandleExceedMaxTaskTitleLength from '@hooks/useHandleExceedMaxTaskTitleLength'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -122,7 +124,7 @@ function ReportActionCompose({ const {isOffline} = useNetwork(); const actionButtonRef = useRef(null); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; + const personalDetails = usePersonalDetails(); const navigation = useNavigation(); const [blockedFromConcierge] = useOnyx(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE); const [shouldShowComposeInput = true] = useOnyx(ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT); @@ -171,7 +173,9 @@ function ReportActionCompose({ * Updates the composer when the comment length is exceeded * Shows red borders and prevents the comment from being sent */ - const {hasExceededMaxCommentLength, validateCommentMaxLength} = useHandleExceedMaxCommentLength(); + const {hasExceededMaxCommentLength, validateCommentMaxLength, setHasExceededMaxCommentLength} = useHandleExceedMaxCommentLength(); + const {hasExceededMaxTaskTitleLength, validateTaskTitleMaxLength, setHasExceededMaxTitleLength} = useHandleExceedMaxTaskTitleLength(); + const [exceededMaxLength, setExceededMaxLength] = useState(null); const suggestionsRef = useRef(null); const composerRef = useRef(); @@ -306,6 +310,16 @@ function ReportActionCompose({ onComposerFocus?.(); }, [onComposerFocus]); + useEffect(() => { + if (hasExceededMaxTaskTitleLength) { + setExceededMaxLength(CONST.TITLE_CHARACTER_LIMIT); + } else if (hasExceededMaxCommentLength) { + setExceededMaxLength(CONST.MAX_COMMENT_LENGTH); + } else { + setExceededMaxLength(null); + } + }, [hasExceededMaxTaskTitleLength, hasExceededMaxCommentLength]); + // We are returning a callback here as we want to incoke the method on unmount only useEffect( () => () => { @@ -328,12 +342,12 @@ function ReportActionCompose({ // When we invite someone to a room they don't have the policy object, but we still want them to be able to mention other reports they are members of, so we only check if the policyID in the report is from a workspace const isGroupPolicyReport = useMemo(() => !!report?.policyID && report.policyID !== CONST.POLICY.ID_FAKE, [report]); const reportRecipientAcountIDs = ReportUtils.getReportRecipientAccountIDs(report, currentUserPersonalDetails.accountID); - const reportRecipient = personalDetails[reportRecipientAcountIDs[0]]; + const reportRecipient = personalDetails?.[reportRecipientAcountIDs[0]]; const shouldUseFocusedColor = !isBlockedFromConcierge && !disabled && isFocused; const hasReportRecipient = !isEmptyObject(reportRecipient); - const isSendDisabled = isCommentEmpty || isBlockedFromConcierge || !!disabled || hasExceededMaxCommentLength; + const isSendDisabled = isCommentEmpty || isBlockedFromConcierge || !!disabled || !!exceededMaxLength; // Note: using JS refs is not well supported in reanimated, thus we need to store the function in a shared value // useSharedValue on web doesn't support functions, so we need to wrap it in an object. @@ -394,14 +408,31 @@ function ReportActionCompose({ ], ); + const validateMaxLength = useCallback( + (value: string) => { + const taskCommentMatch = value?.match(CONST.REGEX.TASK_TITLE_WITH_OPTONAL_SHORT_MENTION); + if (taskCommentMatch) { + const title = taskCommentMatch?.[3] ? taskCommentMatch[3].trim().replace(/\n/g, ' ') : ''; + setHasExceededMaxCommentLength(false); + validateTaskTitleMaxLength(title); + } else { + setHasExceededMaxTitleLength(false); + validateCommentMaxLength(value, {reportID}); + } + }, + [setHasExceededMaxCommentLength, setHasExceededMaxTitleLength, validateTaskTitleMaxLength, validateCommentMaxLength, reportID], + ); + + const debouncedValidate = useMemo(() => lodashDebounce(validateMaxLength, CONST.TIMING.COMMENT_LENGTH_DEBOUNCE_TIME, {leading: true}), [validateMaxLength]); + const onValueChange = useCallback( (value: string) => { if (value.length === 0 && isComposerFullSize) { Report.setIsComposerFullSize(reportID, false); } - validateCommentMaxLength(value, {reportID}); + debouncedValidate(value); }, - [isComposerFullSize, reportID, validateCommentMaxLength], + [isComposerFullSize, reportID, debouncedValidate], ); return ( @@ -436,7 +467,7 @@ function ReportActionCompose({ styles.flexRow, styles.chatItemComposeBox, isComposerFullSize && styles.chatItemFullComposeBox, - hasExceededMaxCommentLength && styles.borderColorDanger, + !!exceededMaxLength && styles.borderColorDanger, ]} > setIsAttachmentPreviewActive(true)} onModalHide={onAttachmentPreviewClose} - shouldDisableSendButton={hasExceededMaxCommentLength} + shouldDisableSendButton={!!exceededMaxLength} > {({displayFileInModal}) => ( <> @@ -469,7 +500,7 @@ function ReportActionCompose({ focus(); }} actionButtonRef={actionButtonRef} - shouldDisableAttachmentItem={hasExceededMaxCommentLength} + shouldDisableAttachmentItem={!!exceededMaxLength} /> { @@ -554,7 +585,12 @@ function ReportActionCompose({ > {!shouldUseNarrowLayout && } - {hasExceededMaxCommentLength && } + {!!exceededMaxLength && ( + + )} {!isSmallScreenWidth && ( diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx index 6a62201058e8..b69a26e5f90e 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx @@ -85,7 +85,7 @@ function SuggestionMention( {value, selection, setSelection, updateComment, isAutoSuggestionPickerLarge, measureParentContainerAndReportCursor, isComposerFocused, isGroupPolicyReport, policyID}: SuggestionProps, ref: ForwardedRef, ) { - const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; + const personalDetails = usePersonalDetails(); const {translate, formatPhoneNumber} = useLocalize(); const [suggestionValues, setSuggestionValues] = useState(defaultSuggestionsValues); const suggestionValuesRef = useRef(suggestionValues); @@ -112,7 +112,7 @@ function SuggestionMention( }, [currentReport], ); - const weightedPersonalDetails: PersonalDetailsList | SuggestionPersonalDetailsList = useMemo(() => { + const weightedPersonalDetails: PersonalDetailsList | SuggestionPersonalDetailsList | undefined = useMemo(() => { const policyEmployeeAccountIDs = getPolicyEmployeeAccountIDs(policyID); if (!ReportUtils.isGroupChat(currentReport) && !ReportUtils.doesReportBelongToWorkspace(currentReport, policyEmployeeAccountIDs, policyID)) { return personalDetails; @@ -264,7 +264,7 @@ function SuggestionMention( ); const getUserMentionOptions = useCallback( - (personalDetailsParam: PersonalDetailsList | SuggestionPersonalDetailsList, searchValue = ''): Mention[] => { + (personalDetailsParam: PersonalDetailsList | SuggestionPersonalDetailsList | undefined, searchValue = ''): Mention[] => { const suggestions = []; if (CONST.AUTO_COMPLETE_SUGGESTER.HERE_TEXT.includes(searchValue.toLowerCase())) { diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 2e1696d1c464..1887bf9d348a 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -1,170 +1,20 @@ -import lodashIsEqual from 'lodash/isEqual'; -import React, {memo, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; -import type {GestureResponderEvent, TextInput} from 'react-native'; -import {InteractionManager, View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; +import React, {useMemo} from 'react'; import {useOnyx} from 'react-native-onyx'; -import type {Emoji} from '@assets/emojis/types'; -import {AttachmentContext} from '@components/AttachmentContext'; -import Button from '@components/Button'; -import DisplayNames from '@components/DisplayNames'; -import Hoverable from '@components/Hoverable'; -import MentionReportContext from '@components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext'; -import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; -import InlineSystemMessage from '@components/InlineSystemMessage'; -import KYCWall from '@components/KYCWall'; -import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import type {OnyxEntry} from 'react-native-onyx'; import {useBlockedFromConcierge, usePersonalDetails} from '@components/OnyxProvider'; -import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; -import ReportActionItemEmojiReactions from '@components/Reactions/ReportActionItemEmojiReactions'; -import RenderHTML from '@components/RenderHTML'; -import type {ActionableItem} from '@components/ReportActionItem/ActionableItemButtons'; -import ActionableItemButtons from '@components/ReportActionItem/ActionableItemButtons'; -import ChronosOOOListActions from '@components/ReportActionItem/ChronosOOOListActions'; -import ExportIntegration from '@components/ReportActionItem/ExportIntegration'; -import IssueCardMessage from '@components/ReportActionItem/IssueCardMessage'; -import MoneyRequestAction from '@components/ReportActionItem/MoneyRequestAction'; -import ReportPreview from '@components/ReportActionItem/ReportPreview'; -import TaskAction from '@components/ReportActionItem/TaskAction'; -import TaskPreview from '@components/ReportActionItem/TaskPreview'; -import TripRoomPreview from '@components/ReportActionItem/TripRoomPreview'; -import {ShowContextMenuContext} from '@components/ShowContextMenuContext'; -import Text from '@components/Text'; -import UnreadActionIndicator from '@components/UnreadActionIndicator'; -import useLocalize from '@hooks/useLocalize'; -import usePrevious from '@hooks/usePrevious'; -import useReportScrollManager from '@hooks/useReportScrollManager'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import ControlSelection from '@libs/ControlSelection'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import * as ErrorUtils from '@libs/ErrorUtils'; -import focusComposerWithDelay from '@libs/focusComposerWithDelay'; import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; -import Navigation from '@libs/Navigation/Navigation'; -import Permissions from '@libs/Permissions'; -import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; -import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import SelectionScraper from '@libs/SelectionScraper'; -import shouldRenderAddPaymentCard from '@libs/shouldRenderAppPaymentCard'; -import {ReactionListContext} from '@pages/home/ReportScreenContext'; -import * as BankAccounts from '@userActions/BankAccounts'; -import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; -import * as Member from '@userActions/Policy/Member'; import * as Report from '@userActions/Report'; import * as ReportActions from '@userActions/ReportActions'; -import * as Session from '@userActions/Session'; import * as Transaction from '@userActions/Transaction'; -import * as User from '@userActions/User'; -import CONST from '@src/CONST'; +import type CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import type * as OnyxTypes from '@src/types/onyx'; -import type {JoinWorkspaceResolution} from '@src/types/onyx/OriginalMessage'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import {RestrictedReadOnlyContextMenuActions} from './ContextMenu/ContextMenuActions'; -import MiniReportActionContextMenu from './ContextMenu/MiniReportActionContextMenu'; -import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu'; -import {hideContextMenu} from './ContextMenu/ReportActionContextMenu'; -import LinkPreviewer from './LinkPreviewer'; -import ReportActionItemBasicMessage from './ReportActionItemBasicMessage'; -import ReportActionItemContentCreated from './ReportActionItemContentCreated'; -import ReportActionItemDraft from './ReportActionItemDraft'; -import ReportActionItemGrouped from './ReportActionItemGrouped'; -import ReportActionItemMessage from './ReportActionItemMessage'; -import ReportActionItemMessageEdit from './ReportActionItemMessageEdit'; -import ReportActionItemSingle from './ReportActionItemSingle'; -import ReportActionItemThread from './ReportActionItemThread'; -import ReportAttachmentsContext from './ReportAttachmentsContext'; - -type ReportActionItemProps = { - /** Report for this action */ - report: OnyxEntry; - - /** The transaction thread report associated with the report for this action, if any */ - transactionThreadReport?: OnyxEntry; - - /** Array of report actions for the report for this action */ - // eslint-disable-next-line react/no-unused-prop-types - reportActions: OnyxTypes.ReportAction[]; - - /** Report action belonging to the report's parent */ - parentReportAction: OnyxEntry; - - /** The transaction thread report's parentReportAction */ - /** It's used by withOnyx HOC */ - // eslint-disable-next-line react/no-unused-prop-types - parentReportActionForTransactionThread?: OnyxEntry; - - /** All the data of the action item */ - action: OnyxTypes.ReportAction; - - /** Should the comment have the appearance of being grouped with the previous comment? */ - displayAsGroup: boolean; - - /** Is this the most recent IOU Action? */ - isMostRecentIOUReportAction: boolean; - - /** Should we display the new marker on top of the comment? */ - shouldDisplayNewMarker: boolean; +import type {ReportAction} from '@src/types/onyx'; +import type {PureReportActionItemProps} from './PureReportActionItem'; +import PureReportActionItem from './PureReportActionItem'; - /** Determines if the avatar is displayed as a subscript (positioned lower than normal) */ - shouldShowSubscriptAvatar?: boolean; - - /** Position index of the report action in the overall report FlatList view */ - index: number; - - /** Flag to show, hide the thread divider line */ - shouldHideThreadDividerLine?: boolean; - - linkedReportActionID?: string; - - /** Callback to be called on onPress */ - onPress?: () => void; - - /** If this is the first visible report action */ - isFirstVisibleReportAction: boolean; - - /** - * Is the action a thread's parent reportAction viewed from within the thread report? - * It will be false if we're viewing the same parent report action from the report it belongs to rather than the thread. - */ - isThreadReportParentAction?: boolean; - - /** IF the thread divider line will be used */ - shouldUseThreadDividerLine?: boolean; - - /** Whether context menu should be displayed */ - shouldDisplayContextMenu?: boolean; -}; - -function ReportActionItem({ - action, - report, - transactionThreadReport, - linkedReportActionID, - displayAsGroup, - index, - isMostRecentIOUReportAction, - parentReportAction, - shouldDisplayNewMarker, - shouldHideThreadDividerLine = false, - shouldShowSubscriptAvatar = false, - onPress = undefined, - isFirstVisibleReportAction = false, - isThreadReportParentAction = false, - shouldUseThreadDividerLine = false, - shouldDisplayContextMenu = true, - parentReportActionForTransactionThread, -}: ReportActionItemProps) { - const {translate} = useLocalize(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); - const blockedFromConcierge = useBlockedFromConcierge(); +function ReportActionItem({action, report, ...props}: PureReportActionItemProps) { const reportID = report?.reportID ?? ''; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const originalReportID = useMemo(() => ReportUtils.getOriginalReportID(reportID, action) || '-1', [reportID, action]); @@ -181,909 +31,58 @@ function ReportActionItem({ `${ONYXKEYS.COLLECTION.TRANSACTION}${ReportActionsUtils.isMoneyRequestAction(action) ? ReportActionsUtils.getOriginalMessage(action)?.IOUTransactionID ?? -1 : -1}`, {selector: (transaction) => transaction?.errorFields?.route ?? null}, ); - const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID); - const theme = useTheme(); - const styles = useThemeStyles(); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- This is needed to prevent the app from crashing when the app is using imported state. const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID || '-1'}`); - const StyleUtils = useStyleUtils(); - const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; - const [isContextMenuActive, setIsContextMenuActive] = useState(() => ReportActionContextMenu.isActiveReportAction(action.reportActionID)); - const [isEmojiPickerActive, setIsEmojiPickerActive] = useState(); - const [isPaymentMethodPopoverActive, setIsPaymentMethodPopoverActive] = useState(); - const [isHidden, setIsHidden] = useState(false); - const [moderationDecision, setModerationDecision] = useState(CONST.MODERATION.MODERATOR_DECISION_APPROVED); - const reactionListRef = useContext(ReactionListContext); - const {updateHiddenAttachments} = useContext(ReportAttachmentsContext); - const textInputRef = useRef(null); - const popoverAnchorRef = useRef>(null); - const downloadedPreviews = useRef([]); - const prevDraftMessage = usePrevious(draftMessage); const [isUserValidated] = useOnyx(ONYXKEYS.USER, {selector: (user) => !!user?.validated}); // The app would crash due to subscribing to the entire report collection if parentReportID is an empty string. So we should have a fallback ID here. // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID || -1}`); - const isReportActionLinked = linkedReportActionID && action.reportActionID && linkedReportActionID === action.reportActionID; - const reportScrollManager = useReportScrollManager(); - const isActionableWhisper = - ReportActionsUtils.isActionableMentionWhisper(action) || ReportActionsUtils.isActionableTrackExpense(action) || ReportActionsUtils.isActionableReportMentionWhisper(action); - const originalMessage = ReportActionsUtils.getOriginalMessage(action); - - const highlightedBackgroundColorIfNeeded = useMemo( - () => (isReportActionLinked ? StyleUtils.getBackgroundColorStyle(theme.messageHighlightBG) : {}), - [StyleUtils, isReportActionLinked, theme.messageHighlightBG], - ); - - const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action); - const isOriginalMessageAnObject = originalMessage && typeof originalMessage === 'object'; - const hasResolutionInOriginalMessage = isOriginalMessageAnObject && 'resolution' in originalMessage; - const prevActionResolution = usePrevious(isActionableWhisper && hasResolutionInOriginalMessage ? originalMessage?.resolution : null); - - // IOUDetails only exists when we are sending money - const isSendingMoney = - ReportActionsUtils.isMoneyRequestAction(action) && - ReportActionsUtils.getOriginalMessage(action)?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && - ReportActionsUtils.getOriginalMessage(action)?.IOUDetails; - - const updateHiddenState = useCallback( - (isHiddenValue: boolean) => { - setIsHidden(isHiddenValue); - const message = Array.isArray(action.message) ? action.message?.at(-1) : action.message; - const isAttachment = ReportUtils.isReportMessageAttachment(message); - if (!isAttachment) { - return; - } - updateHiddenAttachments(action.reportActionID, isHiddenValue); - }, - [action.reportActionID, action.message, updateHiddenAttachments], - ); - - useEffect( - () => () => { - // ReportActionContextMenu, EmojiPicker and PopoverReactionList are global components, - // we should also hide them when the current component is destroyed - if (ReportActionContextMenu.isActiveReportAction(action.reportActionID)) { - ReportActionContextMenu.hideContextMenu(); - ReportActionContextMenu.hideDeleteModal(); - } - if (EmojiPickerAction.isActive(action.reportActionID)) { - EmojiPickerAction.hideEmojiPicker(true); - } - if (reactionListRef?.current?.isActiveReportAction(action.reportActionID)) { - reactionListRef?.current?.hideReactionList(); - } - }, - [action.reportActionID, reactionListRef], - ); - - useEffect(() => { - // We need to hide EmojiPicker when this is a deleted parent action - if (!isDeletedParentAction || !EmojiPickerAction.isActive(action.reportActionID)) { - return; - } - - EmojiPickerAction.hideEmojiPicker(true); - }, [isDeletedParentAction, action.reportActionID]); - - useEffect(() => { - if (prevDraftMessage !== undefined || draftMessage === undefined) { - return; - } - - focusComposerWithDelay(textInputRef.current)(true); - }, [prevDraftMessage, draftMessage]); - - useEffect(() => { - if (!Permissions.canUseLinkPreviews()) { - return; - } - - const urls = ReportActionsUtils.extractLinksFromMessageHtml(action); - if (lodashIsEqual(downloadedPreviews.current, urls) || action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { - return; - } - - downloadedPreviews.current = urls; - Report.expandURLPreview(reportID, action.reportActionID); - }, [action, reportID]); - - useEffect(() => { - if (draftMessage === undefined || !ReportActionsUtils.isDeletedAction(action)) { - return; - } - Report.deleteReportActionDraft(reportID, action); - }, [draftMessage, action, reportID]); - - // Hide the message if it is being moderated for a higher offense, or is hidden by a moderator - // Removed messages should not be shown anyway and should not need this flow - const latestDecision = ReportActionsUtils.getReportActionMessage(action)?.moderationDecision?.decision ?? ''; - useEffect(() => { - if (action.actionName !== CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT) { - return; - } - - // Hide reveal message button and show the message if latestDecision is changed to empty - if (!latestDecision) { - setModerationDecision(CONST.MODERATION.MODERATOR_DECISION_APPROVED); - setIsHidden(false); - return; - } - - setModerationDecision(latestDecision); - if ( - ![CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING].some((item) => item === latestDecision) && - !ReportActionsUtils.isPendingRemove(action) - ) { - setIsHidden(true); - return; - } - setIsHidden(false); - }, [latestDecision, action]); - - const toggleContextMenuFromActiveReportAction = useCallback(() => { - setIsContextMenuActive(ReportActionContextMenu.isActiveReportAction(action.reportActionID)); - }, [action.reportActionID]); - - const isArchivedRoom = ReportUtils.isArchivedRoomWithID(originalReportID); - const disabledActions = useMemo(() => (!ReportUtils.canWriteInReport(report) ? RestrictedReadOnlyContextMenuActions : []), [report]); - const isChronosReport = ReportUtils.chatIncludesChronosWithID(originalReportID); - /** - * Show the ReportActionContextMenu modal popover. - * - * @param [event] - A press event. - */ - const showPopover = useCallback( - (event: GestureResponderEvent | MouseEvent) => { - // Block menu on the message being Edited or if the report action item has errors - if (draftMessage !== undefined || !isEmptyObject(action.errors) || !shouldDisplayContextMenu) { - return; - } - - setIsContextMenuActive(true); - const selection = SelectionScraper.getCurrentSelection(); - ReportActionContextMenu.showContextMenu( - CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, - event, - selection, - popoverAnchorRef.current, - reportID, - action.reportActionID, - originalReportID, - draftMessage ?? '', - () => setIsContextMenuActive(true), - toggleContextMenuFromActiveReportAction, - isArchivedRoom, - isChronosReport, - false, - false, - disabledActions, - false, - setIsEmojiPickerActive as () => void, - undefined, - isThreadReportParentAction, - ); - }, - [ - draftMessage, - action, - reportID, - toggleContextMenuFromActiveReportAction, - originalReportID, - shouldDisplayContextMenu, - disabledActions, - isArchivedRoom, - isChronosReport, - isThreadReportParentAction, - ], - ); - - // Handles manual scrolling to the bottom of the chat when the last message is an actionable whisper and it's resolved. - // This fixes an issue where InvertedFlatList fails to auto scroll down and results in an empty space at the bottom of the chat in IOS. - useEffect(() => { - if (index !== 0 || !isActionableWhisper) { - return; - } - - if (prevActionResolution !== (hasResolutionInOriginalMessage ? originalMessage.resolution : null)) { - reportScrollManager.scrollToIndex(index); - } - }, [index, originalMessage, prevActionResolution, reportScrollManager, isActionableWhisper, hasResolutionInOriginalMessage]); - - const toggleReaction = useCallback( - (emoji: Emoji, ignoreSkinToneOnCompare?: boolean) => { - Report.toggleEmojiReaction(reportID, action, emoji, emojiReactions, undefined, ignoreSkinToneOnCompare); - }, - [reportID, action, emojiReactions], - ); - - const contextValue = useMemo( - () => ({ - anchor: popoverAnchorRef.current, - report: {...report, reportID: report?.reportID ?? ''}, - reportNameValuePairs, - action, - transactionThreadReport, - checkIfContextMenuActive: toggleContextMenuFromActiveReportAction, - isDisabled: false, - }), - [report, action, toggleContextMenuFromActiveReportAction, transactionThreadReport, reportNameValuePairs], - ); - - const attachmentContextValue = useMemo(() => ({reportID, type: CONST.ATTACHMENT_TYPE.REPORT}), [reportID]); - - const mentionReportContextValue = useMemo(() => ({currentReportID: report?.reportID ?? '-1'}), [report?.reportID]); - - const actionableItemButtons: ActionableItem[] = useMemo(() => { - if (ReportActionsUtils.isActionableAddPaymentCard(action) && userBillingFundID === undefined && shouldRenderAddPaymentCard()) { - return [ - { - text: 'subscription.cardSection.addCardButton', - key: `${action.reportActionID}-actionableAddPaymentCard-submit`, - onPress: () => { - Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD); - }, - isMediumSized: true, - isPrimary: true, - }, - ]; - } - - if (!isActionableWhisper && (!ReportActionsUtils.isActionableJoinRequest(action) || ReportActionsUtils.getOriginalMessage(action)?.choice !== ('' as JoinWorkspaceResolution))) { - return []; - } - - if (ReportActionsUtils.isActionableTrackExpense(action)) { - const transactionID = ReportActionsUtils.getOriginalMessage(action)?.transactionID; - return [ - { - text: 'actionableMentionTrackExpense.submit', - key: `${action.reportActionID}-actionableMentionTrackExpense-submit`, - onPress: () => { - ReportUtils.createDraftTransactionAndNavigateToParticipantSelector(transactionID ?? '0', reportID, CONST.IOU.ACTION.SUBMIT, action.reportActionID); - }, - isMediumSized: true, - }, - { - text: 'actionableMentionTrackExpense.categorize', - key: `${action.reportActionID}-actionableMentionTrackExpense-categorize`, - onPress: () => { - ReportUtils.createDraftTransactionAndNavigateToParticipantSelector(transactionID ?? '0', reportID, CONST.IOU.ACTION.CATEGORIZE, action.reportActionID); - }, - isMediumSized: true, - }, - { - text: 'actionableMentionTrackExpense.share', - key: `${action.reportActionID}-actionableMentionTrackExpense-share`, - onPress: () => { - ReportUtils.createDraftTransactionAndNavigateToParticipantSelector(transactionID ?? '0', reportID, CONST.IOU.ACTION.SHARE, action.reportActionID); - }, - isMediumSized: true, - }, - { - text: 'actionableMentionTrackExpense.nothing', - key: `${action.reportActionID}-actionableMentionTrackExpense-nothing`, - onPress: () => { - Report.dismissTrackExpenseActionableWhisper(reportID, action); - }, - isMediumSized: true, - }, - ]; - } - - if (ReportActionsUtils.isActionableJoinRequest(action)) { - return [ - { - text: 'actionableMentionJoinWorkspaceOptions.accept', - key: `${action.reportActionID}-actionableMentionJoinWorkspace-${CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.ACCEPT}`, - onPress: () => Member.acceptJoinRequest(reportID, action), - isPrimary: true, - }, - { - text: 'actionableMentionJoinWorkspaceOptions.decline', - key: `${action.reportActionID}-actionableMentionJoinWorkspace-${CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.DECLINE}`, - onPress: () => Member.declineJoinRequest(reportID, action), - }, - ]; - } - - if (ReportActionsUtils.isActionableReportMentionWhisper(action)) { - return [ - { - text: 'common.yes', - key: `${action.reportActionID}-actionableReportMentionWhisper-${CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION.CREATE}`, - onPress: () => Report.resolveActionableReportMentionWhisper(reportID, action, CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION.CREATE), - isPrimary: true, - }, - { - text: 'common.no', - key: `${action.reportActionID}-actionableReportMentionWhisper-${CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION.NOTHING}`, - onPress: () => Report.resolveActionableReportMentionWhisper(reportID, action, CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION.NOTHING), - }, - ]; - } - - return [ - { - text: 'actionableMentionWhisperOptions.invite', - key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE}`, - onPress: () => Report.resolveActionableMentionWhisper(reportID, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE), - isPrimary: true, - }, - { - text: 'actionableMentionWhisperOptions.nothing', - key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING}`, - onPress: () => Report.resolveActionableMentionWhisper(reportID, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING), - }, - ]; - }, [action, isActionableWhisper, reportID, userBillingFundID]); - - /** - * Get the content of ReportActionItem - * @param hovered whether the ReportActionItem is hovered - * @param isWhisper whether the report action is a whisper - * @param hasErrors whether the report action has any errors - * @returns child component(s) - */ - const renderItemContent = (hovered = false, isWhisper = false, hasErrors = false): React.JSX.Element => { - let children; - - // Show the MoneyRequestPreview for when expense is present - if ( - ReportActionsUtils.isMoneyRequestAction(action) && - ReportActionsUtils.getOriginalMessage(action) && - // For the pay flow, we only want to show MoneyRequestAction when sending money. When paying, we display a regular system message - (ReportActionsUtils.getOriginalMessage(action)?.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || - ReportActionsUtils.getOriginalMessage(action)?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT || - ReportActionsUtils.getOriginalMessage(action)?.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK) - ) { - // There is no single iouReport for bill splits, so only 1:1 requests require an iouReportID - const iouReportID = ReportActionsUtils.getOriginalMessage(action)?.IOUReportID ? ReportActionsUtils.getOriginalMessage(action)?.IOUReportID?.toString() ?? '-1' : '-1'; - children = ( - - ); - } else if (ReportActionsUtils.isTripPreview(action)) { - children = ( - - ); - } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW) { - children = ReportUtils.isClosedExpenseReportWithNoExpenses(iouReport) ? ( - ${translate('parentReportAction.deletedReport')}`} /> - ) : ( - setIsPaymentMethodPopoverActive(true)} - onPaymentOptionsHide={() => setIsPaymentMethodPopoverActive(false)} - isWhisper={isWhisper} - /> - ); - } else if (ReportActionsUtils.isTaskAction(action)) { - children = ; - } else if (ReportActionsUtils.isCreatedTaskReportAction(action)) { - children = ( - - - - ); - } else if (ReportActionsUtils.isReimbursementQueuedAction(action)) { - const linkedReport = ReportUtils.isChatThread(report) ? parentReport : report; - const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails[linkedReport?.ownerAccountID ?? -1]); - const paymentType = ReportActionsUtils.getOriginalMessage(action)?.paymentType ?? ''; - - const missingPaymentMethod = ReportUtils.getIndicatedMissingPaymentMethod(userWallet, linkedReport?.reportID ?? '-1', action); - children = ( - - <> - {missingPaymentMethod === 'bankAccount' && ( - - )} - {/** - These are the actionable buttons that appear at the bottom of a Concierge message - for example: Invite a user mentioned but not a member of the room - https://github.com/Expensify/App/issues/32741 - */} - {actionableItemButtons.length > 0 && ( - - )} - - ) : ( - - )} - - - - ); - } - const numberOfThreadReplies = action.childVisibleActionCount ?? 0; - - const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(action, isThreadReportParentAction); - const oldestFourAccountIDs = - action.childOldestFourAccountIDs - ?.split(',') - .map((accountID) => Number(accountID)) - .filter((accountID): accountID is number => typeof accountID === 'number') ?? []; - const draftMessageRightAlign = draftMessage !== undefined ? styles.chatItemReactionsDraftRight : {}; - - return ( - <> - {children} - {Permissions.canUseLinkPreviews() && !isHidden && (action.linkMetadata?.length ?? 0) > 0 && ( - - !isEmptyObject(item))} /> - - )} - {!ReportActionsUtils.isMessageDeleted(action) && ( - - { - if (Session.isAnonymousUser()) { - hideContextMenu(false); - - InteractionManager.runAfterInteractions(() => { - Session.signOutAndRedirectToSignIn(); - }); - } else { - toggleReaction(emoji, ignoreSkinToneOnCompare); - } - }} - setIsEmojiPickerActive={setIsEmojiPickerActive} - /> - - )} - - {shouldDisplayThreadReplies && ( - - - - )} - - ); - }; - - /** - * Get ReportActionItem with a proper wrapper - * @param hovered whether the ReportActionItem is hovered - * @param isWhisper whether the ReportActionItem is a whisper - * @param hasErrors whether the report action has any errors - * @returns report action item - */ - - const renderReportActionItem = (hovered: boolean, isWhisper: boolean, hasErrors: boolean): React.JSX.Element => { - const content = renderItemContent(hovered || isContextMenuActive || isEmojiPickerActive, isWhisper, hasErrors); - - if (draftMessage !== undefined) { - return {content}; - } - - if (!displayAsGroup) { - return ( - item === moderationDecision) && - !ReportActionsUtils.isPendingRemove(action) - } - > - {content} - - ); - } - - return {content}; - }; - - if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { - const transactionID = ReportActionsUtils.isMoneyRequestAction(parentReportActionForTransactionThread) - ? ReportActionsUtils.getOriginalMessage(parentReportActionForTransactionThread)?.IOUTransactionID - : '-1'; - - return ( - - ); - } - if (ReportActionsUtils.isChronosOOOListAction(action)) { - return ( - - ); - } - - // For the `pay` IOU action on non-pay expense flow, we don't want to render anything if `isWaitingOnBankAccount` is true - // Otherwise, we will see two system messages informing the payee needs to add a bank account or wallet - if ( - ReportActionsUtils.isMoneyRequestAction(action) && - !!report?.isWaitingOnBankAccount && - ReportActionsUtils.getOriginalMessage(action)?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && - !isSendingMoney - ) { - return null; - } - - // If action is actionable whisper and resolved by user, then we don't want to render anything - if (isActionableWhisper && (hasResolutionInOriginalMessage ? originalMessage.resolution : null)) { - return null; - } - - // We currently send whispers to all report participants and hide them in the UI for users that shouldn't see them. - // This is a temporary solution needed for comment-linking. - // The long term solution will leverage end-to-end encryption and only targeted users will be able to decrypt. - if (ReportActionsUtils.isWhisperActionTargetedToOthers(action)) { - return null; - } - - const hasErrors = !isEmptyObject(action.errors); - const whisperedTo = ReportActionsUtils.getWhisperedTo(action); - const isMultipleParticipant = whisperedTo.length > 1; - - const iouReportID = - ReportActionsUtils.isMoneyRequestAction(action) && ReportActionsUtils.getOriginalMessage(action)?.IOUReportID - ? (ReportActionsUtils.getOriginalMessage(action)?.IOUReportID ?? '').toString() - : '-1'; - const transactionsWithReceipts = ReportUtils.getTransactionsWithReceipts(iouReportID); - const isWhisper = whisperedTo.length > 0 && transactionsWithReceipts.length === 0; - const whisperedToPersonalDetails = isWhisper - ? (Object.values(personalDetails ?? {}).filter((details) => whisperedTo.includes(details?.accountID ?? -1)) as OnyxTypes.PersonalDetails[]) - : []; - const isWhisperOnlyVisibleByUser = isWhisper && ReportUtils.isCurrentUserTheOnlyParticipant(whisperedTo); - const displayNamesWithTooltips = isWhisper ? ReportUtils.getDisplayNamesWithTooltips(whisperedToPersonalDetails, isMultipleParticipant) : []; + const personalDetails = usePersonalDetails(); + const blockedFromConcierge = useBlockedFromConcierge(); + const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID); + const linkedReport = ReportUtils.isChatThread(report) ? parentReport : report; + const missingPaymentMethod = ReportUtils.getIndicatedMissingPaymentMethod(userWallet, linkedReport?.reportID ?? '-1', action); return ( - shouldUseNarrowLayout && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} - onPressOut={() => ControlSelection.unblock()} - onSecondaryInteraction={showPopover} - preventDefaultContextMenu={draftMessage === undefined && !hasErrors} - withoutFocusOnSecondaryInteraction - accessibilityLabel={translate('accessibilityHints.chatMessage')} - accessible - > - - {(hovered) => ( - - {shouldDisplayNewMarker && (!shouldUseThreadDividerLine || !isFirstVisibleReportAction) && } - {shouldDisplayContextMenu && ( - - )} - - { - const transactionID = ReportActionsUtils.isMoneyRequestAction(action) ? ReportActionsUtils.getOriginalMessage(action)?.IOUTransactionID : undefined; - if (transactionID) { - Transaction.clearError(transactionID); - } - ReportActions.clearAllRelatedReportActionErrors(reportID, action); - }} - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - pendingAction={ - draftMessage !== undefined ? undefined : action.pendingAction ?? (action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : undefined) - } - shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(action, reportID)} - errors={linkedTransactionRouteError ?? ErrorUtils.getLatestErrorMessageField(action as ErrorUtils.OnyxDataWithErrors)} - errorRowStyles={[styles.ml10, styles.mr2]} - needsOffscreenAlphaCompositing={ReportActionsUtils.isMoneyRequestAction(action)} - shouldDisableStrikeThrough - > - {isWhisper && ( - - - - - - {translate('reportActionContextMenu.onlyVisible')} -   - - - - )} - {renderReportActionItem(!!hovered || !!isReportActionLinked, isWhisper, hasErrors)} - - - - )} - - - - - + >, + report, + )} + modifiedExpenseMessage={ModifiedExpenseMessage.getForReportAction(reportID, action)} + getTransactionsWithReceipts={ReportUtils.getTransactionsWithReceipts} + clearError={Transaction.clearError} + clearAllRelatedReportActionErrors={ReportActions.clearAllRelatedReportActionErrors} + dismissTrackExpenseActionableWhisper={Report.dismissTrackExpenseActionableWhisper} + userBillingFundID={userBillingFundID} + reportAutomaticallyForwardedMessage={ReportUtils.getReportAutomaticallyForwardedMessage(action as ReportAction, reportID)} + /> ); } -export default memo(ReportActionItem, (prevProps, nextProps) => { - const prevParentReportAction = prevProps.parentReportAction; - const nextParentReportAction = nextProps.parentReportAction; - return ( - prevProps.displayAsGroup === nextProps.displayAsGroup && - prevProps.isMostRecentIOUReportAction === nextProps.isMostRecentIOUReportAction && - prevProps.shouldDisplayNewMarker === nextProps.shouldDisplayNewMarker && - lodashIsEqual(prevProps.action, nextProps.action) && - lodashIsEqual(prevProps.report?.pendingFields, nextProps.report?.pendingFields) && - lodashIsEqual(prevProps.report?.isDeletedParentAction, nextProps.report?.isDeletedParentAction) && - lodashIsEqual(prevProps.report?.errorFields, nextProps.report?.errorFields) && - prevProps.report?.statusNum === nextProps.report?.statusNum && - prevProps.report?.stateNum === nextProps.report?.stateNum && - prevProps.report?.parentReportID === nextProps.report?.parentReportID && - prevProps.report?.parentReportActionID === nextProps.report?.parentReportActionID && - // TaskReport's created actions render the TaskView, which updates depending on certain fields in the TaskReport - ReportUtils.isTaskReport(prevProps.report) === ReportUtils.isTaskReport(nextProps.report) && - prevProps.action.actionName === nextProps.action.actionName && - prevProps.report?.reportName === nextProps.report?.reportName && - prevProps.report?.description === nextProps.report?.description && - ReportUtils.isCompletedTaskReport(prevProps.report) === ReportUtils.isCompletedTaskReport(nextProps.report) && - prevProps.report?.managerID === nextProps.report?.managerID && - prevProps.shouldHideThreadDividerLine === nextProps.shouldHideThreadDividerLine && - prevProps.report?.total === nextProps.report?.total && - prevProps.report?.nonReimbursableTotal === nextProps.report?.nonReimbursableTotal && - prevProps.report?.policyAvatar === nextProps.report?.policyAvatar && - prevProps.linkedReportActionID === nextProps.linkedReportActionID && - lodashIsEqual(prevProps.report?.fieldList, nextProps.report?.fieldList) && - lodashIsEqual(prevProps.transactionThreadReport, nextProps.transactionThreadReport) && - lodashIsEqual(prevProps.reportActions, nextProps.reportActions) && - lodashIsEqual(prevParentReportAction, nextParentReportAction) - ); -}); +export default ReportActionItem; diff --git a/src/pages/home/report/ReportActionItemMessage.tsx b/src/pages/home/report/ReportActionItemMessage.tsx index da2f3dd151c8..647c17f70d88 100644 --- a/src/pages/home/report/ReportActionItemMessage.tsx +++ b/src/pages/home/report/ReportActionItemMessage.tsx @@ -37,6 +37,7 @@ type ReportActionItemMessageProps = { function ReportActionItemMessage({action, displayAsGroup, reportID, style, isHidden = false}: ReportActionItemMessageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${ReportActionsUtils.getLinkedTransactionID(action) ?? -1}`); const fragments = ReportActionsUtils.getReportActionMessageFragments(action); @@ -122,7 +123,7 @@ function ReportActionItemMessage({action, displayAsGroup, reportID, style, isHid }; const openWorkspaceInvoicesPage = () => { - const policyID = ReportUtils.getReport(reportID)?.policyID; + const policyID = report?.policyID; if (!policyID) { return; @@ -131,12 +132,14 @@ function ReportActionItemMessage({action, displayAsGroup, reportID, style, isHid Navigation.navigate(ROUTES.WORKSPACE_INVOICES.getRoute(policyID)); }; + const shouldShowAddBankAccountButton = action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && ReportUtils.hasMissingInvoiceBankAccount(reportID) && !ReportUtils.isSettled(reportID); + return ( {!isHidden ? ( <> {renderReportActionItemFragments(isApprovedOrSubmittedReportAction)} - {action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && ReportUtils.hasMissingInvoiceBankAccount(reportID) && ( + {shouldShowAddBankAccountButton && (