diff --git a/.github/actions/composite/setupNode/action.yml b/.github/actions/composite/setupNode/action.yml
index c6a6029e06e0..b3bbf7a537ff 100644
--- a/.github/actions/composite/setupNode/action.yml
+++ b/.github/actions/composite/setupNode/action.yml
@@ -31,7 +31,7 @@ runs:
- name: Install root project node packages
if: steps.cache-node-modules.outputs.cache-hit != 'true'
- uses: nick-fields/retry@v2
+ uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847
with:
timeout_minutes: 30
max_attempts: 3
@@ -39,7 +39,7 @@ runs:
- name: Install node packages for desktop submodule
if: steps.cache-desktop-node-modules.outputs.cache-hit != 'true'
- uses: nick-fields/retry@v2
+ uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847
with:
timeout_minutes: 30
max_attempts: 3
diff --git a/.github/actions/javascript/proposalPoliceComment/index.js b/.github/actions/javascript/proposalPoliceComment/index.js
index e000c99bc4ca..49dbe3b52cf7 100644
--- a/.github/actions/javascript/proposalPoliceComment/index.js
+++ b/.github/actions/javascript/proposalPoliceComment/index.js
@@ -18487,12 +18487,6 @@ class GithubUtils {
})
.then((response) => response.data.workflow_runs[0]?.id);
}
- /**
- * Generate the well-formatted body of a production release.
- */
- static getReleaseBody(pullRequests) {
- return pullRequests.map((number) => `- ${this.getPullRequestURLFromNumber(number)}`).join('\r\n');
- }
/**
* Generate the URL of an New Expensify pull request given the PR number.
*/
diff --git a/.github/scripts/verifyPodfile.sh b/.github/scripts/verifyPodfile.sh
index f4b1c5521733..2c9a7dee672a 100755
--- a/.github/scripts/verifyPodfile.sh
+++ b/.github/scripts/verifyPodfile.sh
@@ -1,5 +1,7 @@
#!/bin/bash
+set -e
+
START_DIR=$(pwd)
ROOT_DIR=$(dirname "$(dirname "$(dirname "${BASH_SOURCE[0]}")")")
cd "$ROOT_DIR" || exit 1
diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml
index f2fda2bc56ff..1eb86cb981b4 100644
--- a/.github/workflows/platformDeploy.yml
+++ b/.github/workflows/platformDeploy.yml
@@ -209,14 +209,13 @@ jobs:
with:
path: ios/Pods
key: ${{ runner.os }}-pods-cache-${{ hashFiles('ios/Podfile.lock', 'firebase.json') }}
- restore-keys: ${{ runner.os }}-pods-cache-
- 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"
- name: Install cocoapods
- uses: nick-invision/retry@0711ba3d7808574133d713a0d92d2941be03a350
+ 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
@@ -379,7 +378,7 @@ jobs:
- name: Upload web build to GitHub Release
if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
- run:
+ run: |
tar -czvf webBuild.tar.gz dist
zip -r webBuild.zip dist
gh release upload ${{ github.event.release.tag_name }} webBuild.tar.gz webBuild.zip
diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml
index 1abe22dc395d..024f5b712a3f 100644
--- a/.github/workflows/testBuild.yml
+++ b/.github/workflows/testBuild.yml
@@ -173,14 +173,13 @@ jobs:
with:
path: ios/Pods
key: ${{ runner.os }}-pods-cache-${{ hashFiles('ios/Podfile.lock', 'firebase.json') }}
- restore-keys: ${{ runner.os }}-pods-cache-
- 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"
- name: Install cocoapods
- uses: nick-invision/retry@0711ba3d7808574133d713a0d92d2941be03a350
+ 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
diff --git a/.well-known/apple-app-site-association b/.well-known/apple-app-site-association
index 394e45f8d9ae..dce724440adf 100644
--- a/.well-known/apple-app-site-association
+++ b/.well-known/apple-app-site-association
@@ -91,6 +91,14 @@
{
"/": "/money2020/*",
"comment": "Money 2020"
+ },
+ {
+ "/": "/track-expense/*",
+ "comment": "Track Expense"
+ },
+ {
+ "/": "/submit-expense/*",
+ "comment": "Submit Expense"
}
]
}
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 66a2ba20102d..2f83c52a1713 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -108,8 +108,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1009001303
- versionName "9.0.13-3"
+ versionCode 1009001402
+ versionName "9.0.14-2"
// 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/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 520602a28a02..142d919a7a18 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -73,6 +73,8 @@
+
+
@@ -94,6 +96,8 @@
+
+
diff --git a/assets/emojis/common.ts b/assets/emojis/common.ts
index c19d958812d1..5162a71367b2 100644
--- a/assets/emojis/common.ts
+++ b/assets/emojis/common.ts
@@ -162,10 +162,26 @@ const emojis: PickerEmojis = [
name: 'hand_over_mouth',
code: '🤭',
},
+ {
+ name: 'face_with_open_eyes_and_hand_over_mouth',
+ code: '🫢',
+ },
+ {
+ name: 'saluting_face',
+ code: '🫡',
+ },
{
name: 'shushing_face',
code: '🤫',
},
+ {
+ name: 'face_with_peeking_eye',
+ code: '🫣',
+ },
+ {
+ name: 'melting_face',
+ code: '🫠',
+ },
{
name: 'thinking',
code: '🤔',
@@ -174,6 +190,10 @@ const emojis: PickerEmojis = [
name: 'zipper_mouth_face',
code: '🤐',
},
+ {
+ name: 'dotted_line_face',
+ code: '🫥',
+ },
{
name: 'raised_eyebrow',
code: '🤨',
@@ -182,10 +202,18 @@ const emojis: PickerEmojis = [
name: 'neutral_face',
code: '😐',
},
+ {
+ name: 'face_with_diagonal_mouth',
+ code: '🫤',
+ },
{
name: 'expressionless',
code: '😑',
},
+ {
+ name: 'shaking_face',
+ code: '🫨',
+ },
{
name: 'no_mouth',
code: '😶',
@@ -362,6 +390,10 @@ const emojis: PickerEmojis = [
name: 'cold_sweat',
code: '😰',
},
+ {
+ name: 'face_holding_back_tears',
+ code: '🥹',
+ },
{
name: 'disappointed_relieved',
code: '😥',
@@ -578,6 +610,18 @@ const emojis: PickerEmojis = [
name: 'heart',
code: '❤️',
},
+ {
+ name: 'pink_heart',
+ code: '🩷',
+ },
+ {
+ name: 'light_blue_heart',
+ code: '🩵',
+ },
+ {
+ name: 'grey_heart',
+ code: '🩶',
+ },
{
name: 'orange_heart',
code: '🧡',
@@ -630,6 +674,10 @@ const emojis: PickerEmojis = [
name: 'sweat_drops',
code: '💦',
},
+ {
+ name: 'bubbles',
+ code: '🫧',
+ },
{
name: 'dash',
code: '💨',
@@ -706,6 +754,16 @@ const emojis: PickerEmojis = [
code: '🤏',
types: ['🤏🏿', '🤏🏾', '🤏🏽', '🤏🏼', '🤏🏻'],
},
+ {
+ name: 'palm_down_hand',
+ code: '🫳',
+ types: ['🫳🏿', '🫳🏾', '🫳🏽', '🫳🏼', '🫳🏻'],
+ },
+ {
+ name: 'palm_up_hand',
+ code: '🫴',
+ types: ['🫴🏿', '🫴🏾', '🫴🏽', '🫴🏼', '🫴🏻'],
+ },
{
name: 'v',
code: '✌️',
@@ -716,6 +774,11 @@ const emojis: PickerEmojis = [
code: '🤞',
types: ['🤞🏿', '🤞🏾', '🤞🏽', '🤞🏼', '🤞🏻'],
},
+ {
+ name: 'hand_with_index_finger_and_thumb_crossed',
+ code: '🫰',
+ types: ['🫰🏿', '🫰🏾', '🫰🏽', '🫰🏼', '🫰🏻'],
+ },
{
name: 'love_you_gesture',
code: '🤟',
@@ -731,6 +794,16 @@ const emojis: PickerEmojis = [
code: '🤙',
types: ['🤙🏿', '🤙🏾', '🤙🏽', '🤙🏼', '🤙🏻'],
},
+ {
+ name: 'rightwards_hand',
+ code: '🫱',
+ types: ['🫱🏿', '🫱🏾', '🫱🏽', '🫱🏼', '🫱🏻'],
+ },
+ {
+ name: 'leftwards_hand',
+ code: '🫲',
+ types: ['🫲🏿', '🫲🏾', '🫲🏽', '🫲🏼', '🫲🏻'],
+ },
{
name: 'point_left',
code: '👈',
@@ -791,6 +864,16 @@ const emojis: PickerEmojis = [
code: '🤜',
types: ['🤜🏿', '🤜🏾', '🤜🏽', '🤜🏼', '🤜🏻'],
},
+ {
+ name: 'leftwards_pushing_hand',
+ code: '🫷',
+ types: ['🫷🏿', '🫷🏾', '🫷🏽', '🫷🏼', '🫷🏻'],
+ },
+ {
+ name: 'rightwards_pushing_hand',
+ code: '🫸',
+ types: ['🫸🏿', '🫸🏾', '🫸🏽', '🫸🏼', '🫸🏻'],
+ },
{
name: 'clap',
code: '👏',
@@ -801,6 +884,11 @@ const emojis: PickerEmojis = [
code: '🙌',
types: ['🙌🏿', '🙌🏾', '🙌🏽', '🙌🏼', '🙌🏻'],
},
+ {
+ name: 'heart_hands',
+ code: '🫶',
+ types: ['🫶🏿', '🫶🏾', '🫶🏽', '🫶🏼', '🫶🏻'],
+ },
{
name: 'open_hands',
code: '👐',
@@ -821,6 +909,11 @@ const emojis: PickerEmojis = [
code: '🙏',
types: ['🙏🏿', '🙏🏾', '🙏🏽', '🙏🏼', '🙏🏻'],
},
+ {
+ name: 'index_pointing_at_the_viewer',
+ code: '🫵',
+ types: ['🫵🏿', '🫵🏾', '🫵🏽', '🫵🏼', '🫵🏻'],
+ },
{
name: 'writing_hand',
code: '✍️',
@@ -910,6 +1003,10 @@ const emojis: PickerEmojis = [
name: 'lips',
code: '👄',
},
+ {
+ name: 'biting_lip',
+ code: '🫦',
+ },
{
name: 'baby',
code: '👶',
@@ -1510,6 +1607,11 @@ const emojis: PickerEmojis = [
code: '🤴',
types: ['🤴🏿', '🤴🏾', '🤴🏽', '🤴🏼', '🤴🏻'],
},
+ {
+ name: 'person_with_crown',
+ code: '🫅',
+ types: ['🫅🏿', '🫅🏾', '🫅🏽', '🫅🏼', '🫅🏻'],
+ },
{
name: 'princess',
code: '👸',
@@ -1575,6 +1677,16 @@ const emojis: PickerEmojis = [
code: '🤰',
types: ['🤰🏿', '🤰🏾', '🤰🏽', '🤰🏼', '🤰🏻'],
},
+ {
+ name: 'pregnant_person',
+ code: '🫄',
+ types: ['🫄🏿', '🫄🏾', '🫄🏽', '🫄🏼', '🫄🏻'],
+ },
+ {
+ name: 'pregnant_man',
+ code: '🫃',
+ types: ['🫃🏿', '🫃🏾', '🫃🏽', '🫃🏼', '🫃🏻'],
+ },
{
name: 'breast_feeding',
code: '🤱',
@@ -1720,6 +1832,10 @@ const emojis: PickerEmojis = [
code: '🧝♀️',
types: ['🧝🏿♀️', '🧝🏾♀️', '🧝🏽♀️', '🧝🏼♀️', '🧝🏻♀️'],
},
+ {
+ name: 'troll',
+ code: '🧌',
+ },
{
name: 'genie',
code: '🧞',
@@ -2448,6 +2564,30 @@ const emojis: PickerEmojis = [
name: 'unicorn',
code: '🦄',
},
+ {
+ name: 'moose',
+ code: '🫎',
+ },
+ {
+ name: 'donkey',
+ code: '🫏',
+ },
+ {
+ name: 'wing',
+ code: '🪽',
+ },
+ {
+ name: 'black_bird',
+ code: '🐦⬛',
+ },
+ {
+ name: 'goose',
+ code: '🪿',
+ },
+ {
+ name: 'jellyfish',
+ code: '🪼',
+ },
{
name: 'zebra',
code: '🦓',
@@ -2764,6 +2904,10 @@ const emojis: PickerEmojis = [
name: 'shell',
code: '🐚',
},
+ {
+ name: 'coral',
+ code: '🪸',
+ },
{
name: 'snail',
code: '🐌',
@@ -2852,6 +2996,14 @@ const emojis: PickerEmojis = [
name: 'wilted_flower',
code: '🥀',
},
+ {
+ name: 'hyacinth',
+ code: '🪻',
+ },
+ {
+ name: 'lotus',
+ code: '🪷',
+ },
{
name: 'hibiscus',
code: '🌺',
@@ -2920,6 +3072,14 @@ const emojis: PickerEmojis = [
name: 'leaves',
code: '🍃',
},
+ {
+ name: 'nest_with_eggs',
+ code: '🪺',
+ },
+ {
+ name: 'empty_nest',
+ code: '🪹',
+ },
{
header: true,
icon: FoodAndDrink,
@@ -3057,6 +3217,10 @@ const emojis: PickerEmojis = [
name: 'peanuts',
code: '🥜',
},
+ {
+ name: 'beans',
+ code: '🫘',
+ },
{
name: 'chestnut',
code: '🌰',
@@ -3197,6 +3361,10 @@ const emojis: PickerEmojis = [
name: 'canned_food',
code: '🥫',
},
+ {
+ name: 'jar',
+ code: '🫙',
+ },
{
name: 'bento',
code: '🍱',
@@ -3229,6 +3397,14 @@ const emojis: PickerEmojis = [
name: 'sweet_potato',
code: '🍠',
},
+ {
+ name: 'ginger',
+ code: '🫚',
+ },
+ {
+ name: 'pea_pod',
+ code: '🫛',
+ },
{
name: 'oden',
code: '🍢',
@@ -3349,6 +3525,10 @@ const emojis: PickerEmojis = [
name: 'milk_glass',
code: '🥛',
},
+ {
+ name: 'pouring_liquid',
+ code: '🫗',
+ },
{
name: 'coffee',
code: '☕',
@@ -3842,6 +4022,10 @@ const emojis: PickerEmojis = [
name: 'motorized_wheelchair',
code: '🦼',
},
+ {
+ name: 'crutch',
+ code: '🩼',
+ },
{
name: 'auto_rickshaw',
code: '🛺',
@@ -3862,6 +4046,10 @@ const emojis: PickerEmojis = [
name: 'roller_skate',
code: '🛼',
},
+ {
+ name: 'wheel',
+ code: '🛞',
+ },
{
name: 'busstop',
code: '🚏',
@@ -3934,6 +4122,10 @@ const emojis: PickerEmojis = [
name: 'ship',
code: '🚢',
},
+ {
+ name: 'ring_buoy',
+ code: '🛟',
+ },
{
name: 'airplane',
code: '✈️',
@@ -4359,6 +4551,10 @@ const emojis: PickerEmojis = [
name: 'dolls',
code: '🎎',
},
+ {
+ name: 'folding_hand_fan',
+ code: '🪭',
+ },
{
name: 'flags',
code: '🎏',
@@ -4367,6 +4563,10 @@ const emojis: PickerEmojis = [
name: 'wind_chime',
code: '🎐',
},
+ {
+ name: 'mirror_ball',
+ code: '🪩',
+ },
{
name: 'rice_scene',
code: '🎑',
@@ -4539,6 +4739,10 @@ const emojis: PickerEmojis = [
name: 'kite',
code: '🪁',
},
+ {
+ name: 'playground_slide',
+ code: '🛝',
+ },
{
name: '8ball',
code: '🎱',
@@ -4555,6 +4759,10 @@ const emojis: PickerEmojis = [
name: 'nazar_amulet',
code: '🧿',
},
+ {
+ name: 'hamsa',
+ code: '🪬',
+ },
{
name: 'video_game',
code: '🎮',
@@ -4920,6 +5128,10 @@ const emojis: PickerEmojis = [
name: 'musical_keyboard',
code: '🎹',
},
+ {
+ name: 'maracas',
+ code: '🪇',
+ },
{
name: 'trumpet',
code: '🎺',
@@ -4928,6 +5140,10 @@ const emojis: PickerEmojis = [
name: 'violin',
code: '🎻',
},
+ {
+ name: 'flute',
+ code: '🪈',
+ },
{
name: 'banjo',
code: '🪕',
@@ -4968,6 +5184,10 @@ const emojis: PickerEmojis = [
name: 'battery',
code: '🔋',
},
+ {
+ name: 'low_battery',
+ code: '🪫',
+ },
{
name: 'electric_plug',
code: '🔌',
@@ -5180,6 +5400,10 @@ const emojis: PickerEmojis = [
name: 'credit_card',
code: '💳',
},
+ {
+ name: 'identification_card',
+ code: '🪪',
+ },
{
name: 'receipt',
code: '🧾',
@@ -5508,6 +5732,10 @@ const emojis: PickerEmojis = [
name: 'telescope',
code: '🔭',
},
+ {
+ name: 'x_ray',
+ code: '🩻',
+ },
{
name: 'satellite',
code: '📡',
@@ -5584,6 +5812,10 @@ const emojis: PickerEmojis = [
name: 'razor',
code: '🪒',
},
+ {
+ name: 'hair_pick',
+ code: '🪮',
+ },
{
name: 'lotion_bottle',
code: '🧴',
@@ -5709,6 +5941,10 @@ const emojis: PickerEmojis = [
name: 'left_luggage',
code: '🛅',
},
+ {
+ name: 'wireless',
+ code: '🛜',
+ },
{
name: 'warning',
code: '⚠️',
@@ -5865,6 +6101,10 @@ const emojis: PickerEmojis = [
name: 'wheel_of_dharma',
code: '☸️',
},
+ {
+ name: 'khanda',
+ code: '🪯',
+ },
{
name: 'yin_yang',
code: '☯️',
@@ -6069,6 +6309,10 @@ const emojis: PickerEmojis = [
name: 'heavy_division_sign',
code: '➗',
},
+ {
+ name: 'heavy_equals_sign',
+ code: '🟰',
+ },
{
name: 'infinity',
code: '♾️',
diff --git a/assets/emojis/en.ts b/assets/emojis/en.ts
index 28051e5ecd99..adac235f56ce 100644
--- a/assets/emojis/en.ts
+++ b/assets/emojis/en.ts
@@ -92,9 +92,21 @@ const enEmojis: EmojisList = {
'🤭': {
keywords: ['quiet', 'whoops'],
},
+ '🫡': {
+ keywords: ['face', 'salute', 'respect', 'military', 'honor'],
+ },
+ '🫣': {
+ keywords: ['face', 'peek', 'eye', 'curious', 'shy'],
+ },
+ '🫢': {
+ keywords: ['face', 'open eyes', 'hand over mouth', 'surprised', 'shock'],
+ },
'🤫': {
keywords: ['silence', 'quiet'],
},
+ '🫠': {
+ keywords: ['face', 'disappear', 'dissolve', 'liquid', 'melt', 'melting face'],
+ },
'🤔': {
keywords: ['face'],
},
@@ -104,12 +116,21 @@ const enEmojis: EmojisList = {
'🤨': {
keywords: ['suspicious'],
},
+ '🫥': {
+ keywords: ['face', 'invisible', 'hidden', 'dotted line', 'disappear'],
+ },
'😐': {
keywords: ['meh', 'deadpan', 'face', 'neutral'],
},
+ '🫤': {
+ keywords: ['face', 'diagonal mouth', 'meh', 'neutral', 'uncertain'],
+ },
'😑': {
keywords: ['face', 'inexpressive', 'unexpressive'],
},
+ '🫨': {
+ keywords: ['shaking', 'face', 'shock', 'vibration', 'tremble', 'emotion'],
+ },
'😶': {
keywords: ['mute', 'silence', 'face', 'mouth', 'quiet', 'silent'],
},
@@ -242,6 +263,9 @@ const enEmojis: EmojisList = {
'😰': {
keywords: ['nervous', 'blue', 'cold', 'face', 'mouth', 'open', 'rushed', 'sweat'],
},
+ '🥹': {
+ keywords: ['face', 'tears', 'emotional', 'holding back', 'crying'],
+ },
'😥': {
keywords: ['phew', 'sweat', 'nervous', 'disappointed', 'face', 'relieved', 'whew'],
},
@@ -404,6 +428,15 @@ const enEmojis: EmojisList = {
'❤️': {
keywords: ['love'],
},
+ '🩷': {
+ keywords: ['pink', 'heart', 'love', 'affection', 'romance', 'valentine'],
+ },
+ '🩵': {
+ keywords: ['light blue', 'heart', 'love', 'affection', 'calm', 'tranquility'],
+ },
+ '🩶': {
+ keywords: ['grey', 'heart', 'love', 'affection', 'neutral', 'balance'],
+ },
'🧡': {
keywords: [],
},
@@ -443,6 +476,9 @@ const enEmojis: EmojisList = {
'💦': {
keywords: ['water', 'workout', 'comic', 'splashing', 'sweat'],
},
+ '🫧': {
+ keywords: ['bubbles', 'soap', 'water', 'float'],
+ },
'💨': {
keywords: ['wind', 'blow', 'fast', 'comic', 'running'],
},
@@ -494,12 +530,21 @@ const enEmojis: EmojisList = {
'🤏': {
keywords: [],
},
+ '🫳': {
+ keywords: ['hand', 'palm down', 'gesture'],
+ },
+ '🫴': {
+ keywords: ['hand', 'palm up', 'gesture'],
+ },
'✌️': {
keywords: ['victory', 'peace'],
},
'🤞': {
keywords: ['luck', 'hopeful', 'cross', 'finger', 'hand'],
},
+ '🫰': {
+ keywords: ['hand', 'finger', 'thumb', 'crossed', 'gesture'],
+ },
'🤟': {
keywords: [],
},
@@ -509,6 +554,12 @@ const enEmojis: EmojisList = {
'🤙': {
keywords: ['call', 'hand', 'shaka'],
},
+ '🫱': {
+ keywords: ['hand', 'right', 'pointing', 'gesture'],
+ },
+ '🫲': {
+ keywords: ['hand', 'left', 'pointing', 'gesture'],
+ },
'👈': {
keywords: ['backhand', 'body', 'finger', 'hand', 'index', 'point'],
},
@@ -545,12 +596,21 @@ const enEmojis: EmojisList = {
'🤜': {
keywords: ['fist', 'rightwards'],
},
+ '🫷': {
+ keywords: ['leftwards', 'pushing', 'hand', 'gesture', 'stop', 'block'],
+ },
+ '🫸': {
+ keywords: ['rightwards', 'pushing', 'hand', 'gesture', 'stop', 'block'],
+ },
'👏': {
keywords: ['praise', 'applause', 'body', 'hand'],
},
'🙌': {
keywords: ['hooray', 'body', 'celebration', 'gesture', 'hand', 'raised'],
},
+ '🫶': {
+ keywords: ['hand', 'heart', 'gesture', 'love'],
+ },
'👐': {
keywords: ['body', 'hand', 'open'],
},
@@ -563,6 +623,9 @@ const enEmojis: EmojisList = {
'🙏': {
keywords: ['please', 'hope', 'wish', 'ask', 'body', 'bow', 'folded', 'gesture', 'hand', 'thanks'],
},
+ '🫵': {
+ keywords: ['hand', 'pointing', 'viewer', 'gesture'],
+ },
'✍️': {
keywords: [],
},
@@ -623,6 +686,9 @@ const enEmojis: EmojisList = {
'👄': {
keywords: ['kiss', 'body', 'mouth'],
},
+ '🫦': {
+ keywords: ['biting', 'lip', 'nervous', 'flirt'],
+ },
'👶': {
keywords: ['child', 'newborn'],
},
@@ -983,6 +1049,9 @@ const enEmojis: EmojisList = {
'🤴': {
keywords: ['crown', 'royal'],
},
+ '🫅': {
+ keywords: ['person', 'crown', 'royalty', 'king', 'queen'],
+ },
'👸': {
keywords: ['crown', 'royal', 'fairy tale', 'fantasy'],
},
@@ -1022,6 +1091,12 @@ const enEmojis: EmojisList = {
'🤰': {
keywords: ['pregnant', 'woman'],
},
+ '🫄': {
+ keywords: ['pregnant', 'person', 'expecting', 'parent'],
+ },
+ '🫃': {
+ keywords: ['pregnant', 'man', 'expecting', 'parent'],
+ },
'🤱': {
keywords: ['nursing'],
},
@@ -1109,6 +1184,9 @@ const enEmojis: EmojisList = {
'🧝♀️': {
keywords: [],
},
+ '🧌': {
+ keywords: ['troll', 'mythical', 'creature', 'fantasy'],
+ },
'🧞': {
keywords: [],
},
@@ -1583,6 +1661,25 @@ const enEmojis: EmojisList = {
'🦄': {
keywords: ['face'],
},
+ '🫎': {
+ keywords: ['moose', 'animal', 'wildlife', 'antlers', 'forest', 'nature'],
+ },
+ '🫏': {
+ keywords: ['donkey', 'animal', 'mule', 'farm', 'stubborn', 'nature'],
+ },
+ '🪽': {
+ keywords: ['wing', 'bird', 'fly', 'angel', 'freedom', 'flight'],
+ },
+ '🐦⬛': {
+ keywords: ['black', 'bird', 'animal', 'crow', 'raven', 'flight'],
+ },
+ '🪿': {
+ keywords: ['goose', 'animal', 'bird', 'waterfowl', 'nature', 'pond'],
+ },
+ '🪼': {
+ keywords: ['jellyfish', 'animal', 'sea', 'ocean', 'tentacles', 'marine'],
+ },
+
'🦓': {
keywords: [],
},
@@ -1820,6 +1917,9 @@ const enEmojis: EmojisList = {
'🐚': {
keywords: ['sea', 'beach', 'spiral'],
},
+ '🪸': {
+ keywords: ['coral', 'reef', 'sea', 'ocean', 'marine'],
+ },
'🐌': {
keywords: ['slow'],
},
@@ -1886,6 +1986,12 @@ const enEmojis: EmojisList = {
'🥀': {
keywords: ['flower', 'wilted'],
},
+ '🪻': {
+ keywords: ['hyacinth', 'flower', 'plant', 'blossom', 'garden', 'nature'],
+ },
+ '🪷': {
+ keywords: ['lotus', 'flower', 'bloom', 'plant'],
+ },
'🌺': {
keywords: ['flower', 'plant'],
},
@@ -1937,6 +2043,12 @@ const enEmojis: EmojisList = {
'🍃': {
keywords: ['leaf', 'blow', 'flutter', 'plant', 'wind'],
},
+ '🪺': {
+ keywords: ['nest', 'eggs', 'bird', 'home'],
+ },
+ '🪹': {
+ keywords: ['nest', 'empty', 'bird', 'home'],
+ },
'🍇': {
keywords: ['fruit', 'grape', 'plant'],
},
@@ -2036,6 +2148,9 @@ const enEmojis: EmojisList = {
'🥜': {
keywords: ['nut', 'peanut', 'vegetable'],
},
+ '🫘': {
+ keywords: ['beans', 'food', 'legume'],
+ },
'🌰': {
keywords: ['plant'],
},
@@ -2141,6 +2256,9 @@ const enEmojis: EmojisList = {
'🥫': {
keywords: [],
},
+ '🫙': {
+ keywords: ['jar', 'container', 'storage'],
+ },
'🍱': {
keywords: ['box'],
},
@@ -2165,6 +2283,12 @@ const enEmojis: EmojisList = {
'🍠': {
keywords: ['potato', 'roasted', 'sweet'],
},
+ '🫚': {
+ keywords: ['ginger', 'root', 'spice', 'food', 'cooking', 'health'],
+ },
+ '🫛': {
+ keywords: ['pea', 'pod', 'vegetable', 'food', 'plant', 'garden'],
+ },
'🍢': {
keywords: ['kebab', 'seafood', 'skewer', 'stick'],
},
@@ -2255,6 +2379,9 @@ const enEmojis: EmojisList = {
'🥛': {
keywords: ['drink', 'glass', 'milk'],
},
+ '🫗': {
+ keywords: ['pouring', 'liquid', 'drink', 'water'],
+ },
'☕': {
keywords: ['cafe', 'espresso', 'beverage', 'drink', 'hot', 'steaming', 'tea'],
},
@@ -2621,6 +2748,9 @@ const enEmojis: EmojisList = {
'🦼': {
keywords: [],
},
+ '🩼': {
+ keywords: ['crutch', 'support', 'injury', 'aid'],
+ },
'🛺': {
keywords: [],
},
@@ -2636,6 +2766,9 @@ const enEmojis: EmojisList = {
'🛼': {
keywords: [],
},
+ '🛞': {
+ keywords: ['wheel', 'vehicle', 'transportation'],
+ },
'🚏': {
keywords: ['bus', 'stop'],
},
@@ -2690,6 +2823,9 @@ const enEmojis: EmojisList = {
'🚢': {
keywords: ['vehicle'],
},
+ '🛟': {
+ keywords: ['ring', 'buoy', 'lifesaver', 'safety'],
+ },
'✈️': {
keywords: ['flight', 'vehicle'],
},
@@ -3005,12 +3141,18 @@ const enEmojis: EmojisList = {
'🎎': {
keywords: ['activity', 'celebration', 'doll', 'entertainment', 'festival', 'japanese'],
},
+ '🪭': {
+ keywords: ['folding', 'hand', 'fan', 'cool', 'breeze', 'accessory'],
+ },
'🎏': {
keywords: ['activity', 'carp', 'celebration', 'entertainment', 'flag', 'streamer'],
},
'🎐': {
keywords: ['activity', 'bell', 'celebration', 'chime', 'entertainment', 'wind'],
},
+ '🪩': {
+ keywords: ['mirror', 'ball', 'disco', 'party'],
+ },
'🎑': {
keywords: ['activity', 'celebration', 'ceremony', 'entertainment', 'moon'],
},
@@ -3140,6 +3282,9 @@ const enEmojis: EmojisList = {
'🪁': {
keywords: [],
},
+ '🛝': {
+ keywords: ['playground', 'slide', 'play', 'park'],
+ },
'🎱': {
keywords: ['pool', 'billiards', '8', '8 ball', 'ball', 'billiard', 'eight', 'game'],
},
@@ -3152,6 +3297,9 @@ const enEmojis: EmojisList = {
'🧿': {
keywords: [],
},
+ '🪬': {
+ keywords: ['hamsa', 'hand', 'protection', 'luck'],
+ },
'🎮': {
keywords: ['play', 'controller', 'console', 'entertainment', 'game', 'video game'],
},
@@ -3422,12 +3570,18 @@ const enEmojis: EmojisList = {
'🎹': {
keywords: ['piano', 'activity', 'entertainment', 'instrument', 'keyboard', 'music'],
},
+ '🪇': {
+ keywords: ['maracas', 'instrument', 'music', 'percussion', 'rhythm', 'shake'],
+ },
'🎺': {
keywords: ['activity', 'entertainment', 'instrument', 'music'],
},
'🎻': {
keywords: ['activity', 'entertainment', 'instrument', 'music'],
},
+ '🪈': {
+ keywords: ['flute', 'instrument', 'music', 'wind', 'melody', 'play'],
+ },
'🪕': {
keywords: [],
},
@@ -3458,6 +3612,9 @@ const enEmojis: EmojisList = {
'🔋': {
keywords: ['power'],
},
+ '🪫': {
+ keywords: ['low', 'battery', 'power', 'charge'],
+ },
'🔌': {
keywords: ['electric', 'electricity', 'plug'],
},
@@ -3617,6 +3774,9 @@ const enEmojis: EmojisList = {
'💳': {
keywords: ['subscription', 'bank', 'card', 'credit', 'money'],
},
+ '🪪': {
+ keywords: ['identification', 'card', 'ID', 'document'],
+ },
'🧾': {
keywords: [],
},
@@ -3863,6 +4023,9 @@ const enEmojis: EmojisList = {
'🔭': {
keywords: ['tool'],
},
+ '🩻': {
+ keywords: ['x-ray', 'medical', 'scan', 'radiology'],
+ },
'📡': {
keywords: ['signal', 'antenna', 'communication', 'dish'],
},
@@ -3920,6 +4083,9 @@ const enEmojis: EmojisList = {
'🪒': {
keywords: [],
},
+ '🪮': {
+ keywords: ['hair', 'pick', 'comb', 'grooming', 'accessory', 'style'],
+ },
'🧴': {
keywords: [],
},
@@ -4010,6 +4176,9 @@ const enEmojis: EmojisList = {
'🛅': {
keywords: ['baggage', 'left luggage', 'locker', 'luggage'],
},
+ '🛜': {
+ keywords: ['wireless', 'network', 'signal', 'connection', 'internet', 'wifi'],
+ },
'⚠️': {
keywords: ['wip'],
},
@@ -4127,6 +4296,9 @@ const enEmojis: EmojisList = {
'☸️': {
keywords: ['buddhist', 'dharma', 'religion', 'wheel'],
},
+ '🪯': {
+ keywords: ['khanda', 'sikh', 'symbol', 'religion', 'faith', 'sikhism'],
+ },
'☯️': {
keywords: [],
},
@@ -4280,6 +4452,9 @@ const enEmojis: EmojisList = {
'➗': {
keywords: ['division', 'math'],
},
+ '🟰': {
+ keywords: ['equals', 'sign', 'math', 'symbol'],
+ },
'♾️': {
keywords: [],
},
diff --git a/assets/emojis/es.ts b/assets/emojis/es.ts
index 0d23f887f556..67e97caf2121 100644
--- a/assets/emojis/es.ts
+++ b/assets/emojis/es.ts
@@ -122,10 +122,26 @@ const esEmojis: EmojisList = {
name: 'cara_con_mano_sobre_boca',
keywords: ['ostras', 'uy', 'vaya', 'cara con mano sobre la boca'],
},
+ '🫣': {
+ name: 'cara_espiando',
+ keywords: ['cara', 'espiar', 'ojo', 'curioso', 'tímido'],
+ },
+ '🫢': {
+ name: 'cara_con_ojos_abiertos_y_mano_sobre_boca',
+ keywords: ['cara', 'ojos abiertos', 'mano sobre boca', 'sorprendido', 'choque'],
+ },
+ '🫡': {
+ name: 'cara_saludando',
+ keywords: ['cara', 'saludo', 'respeto', 'militar', 'honor'],
+ },
'🤫': {
name: 'calla',
keywords: ['callado', 'silencio', 'cara pidiendo silencio'],
},
+ '🫠': {
+ name: 'cara_derritiéndose',
+ keywords: ['calor', 'cara', 'derritiéndose', 'derretido', 'derretirse', 'desaparecer', 'fundirse', 'líquido'],
+ },
'🤔': {
name: 'cara_pensativa',
keywords: ['cara', 'duda', 'pensando', 'cara pensativa'],
@@ -138,14 +154,26 @@ const esEmojis: EmojisList = {
name: 'cara_con_ceja_levantada',
keywords: ['desconfiado', 'escéptico', 'cara con ceja alzada'],
},
+ '🫥': {
+ name: 'cara_invisible',
+ keywords: ['cara', 'invisible', 'oculto', 'línea discontinua', 'desaparecer'],
+ },
'😐': {
name: 'cara_neutra',
keywords: ['cara', 'inexpresivo', 'neutral'],
},
+ '🫤': {
+ name: 'cara_con_boca_diagonal',
+ keywords: ['cara', 'boca diagonal', 'meh', 'neutral', 'incierto'],
+ },
'😑': {
name: 'inexpresivo',
keywords: ['cara', 'inexpresión', 'inexpresiva', 'inexpresivo', 'cara sin expresión'],
},
+ '🫨': {
+ name: 'cara_temblorosa',
+ keywords: ['cara', 'temblorosa', 'sacudida', 'temblor'],
+ },
'😶': {
name: 'prohibido_hablar',
keywords: ['boca', 'callado', 'cara', 'silencio', 'cara sin boca'],
@@ -322,6 +350,10 @@ const esEmojis: EmojisList = {
name: 'sudor_frío',
keywords: ['ansiedad', 'cara', 'frío', 'sudor', 'cara con ansiedad y sudor'],
},
+ '🥹': {
+ name: 'cara_con_lágrimas',
+ keywords: ['cara', 'lágrimas', 'emocional', 'contener', 'llorando'],
+ },
'😥': {
name: 'decepcionado_aliviado',
keywords: ['aliviado', 'cara', 'decepcionado', 'menos mal', 'cara triste pero aliviada'],
@@ -538,6 +570,18 @@ const esEmojis: EmojisList = {
name: 'corazón',
keywords: ['corazón', 'emoción', 'rojo'],
},
+ '🩷': {
+ name: 'corazón_rosa',
+ keywords: ['corazón', 'rosa', 'amor', 'afecto'],
+ },
+ '🩵': {
+ name: 'corazón_azul_claro',
+ keywords: ['corazón', 'azul', 'claro', 'amor', 'afecto'],
+ },
+ '🩶': {
+ name: 'corazón_gris',
+ keywords: ['corazón', 'gris', 'amor', 'afecto'],
+ },
'🧡': {
name: 'corazón_naranja',
keywords: ['corazón', 'emoción', 'naranja'],
@@ -590,6 +634,10 @@ const esEmojis: EmojisList = {
name: 'gotas_de_sudor',
keywords: ['cómic', 'emoción', 'sudor', 'gotas de sudor'],
},
+ '🫧': {
+ name: 'burbujas',
+ keywords: ['burbujas', 'jabón', 'agua', 'flotar'],
+ },
'💨': {
name: 'guión',
keywords: ['carrera', 'cómic', 'correr', 'humo', 'salir corriendo'],
@@ -658,6 +706,14 @@ const esEmojis: EmojisList = {
name: 'mano_pellizcando',
keywords: ['pellizco', 'poco', 'poquito', 'mano pellizcando'],
},
+ '🫳': {
+ name: 'mano_con_palma_hacia_abajo',
+ keywords: ['mano', 'palma abajo', 'gesto'],
+ },
+ '🫴': {
+ name: 'mano_con_palma_hacia_arriba',
+ keywords: ['mano', 'palma arriba', 'gesto'],
+ },
'✌️': {
name: 'v',
keywords: ['mano', 'señal de victoria', 'victoria', 'mano con señal de victoria'],
@@ -666,6 +722,10 @@ const esEmojis: EmojisList = {
name: 'dedos_cruzados',
keywords: ['cruzar', 'dedos', 'mano', 'suerte', 'dedos cruzados'],
},
+ '🫰': {
+ name: 'mano_con_dedos_cruzados',
+ keywords: ['mano', 'dedo', 'pulgar', 'cruzado', 'gesto'],
+ },
'🤟': {
name: 'te_amo_en_lenguaje_de_señas',
keywords: ['mano', 'quiero', 'gesto de te quiero'],
@@ -678,6 +738,14 @@ const esEmojis: EmojisList = {
name: 'mano_llámame',
keywords: ['llamar', 'mano', 'meñique', 'pulgar', 'mano haciendo el gesto de llamar'],
},
+ '🫱': {
+ name: 'mano_derecha',
+ keywords: ['mano', 'derecha', 'apuntar', 'gesto'],
+ },
+ '🫲': {
+ name: 'mano_izquierda',
+ keywords: ['mano', 'izquierda', 'apuntar', 'gesto'],
+ },
'👈': {
name: 'apuntando_hacia_la_izquierda',
keywords: ['dedo', 'índice', 'izquierda', 'mano', 'dorso de mano con índice a la izquierda'],
@@ -726,6 +794,14 @@ const esEmojis: EmojisList = {
name: 'puño_hacia_la_derecha',
keywords: ['derecha', 'puño', 'puño hacia la derecha'],
},
+ '🫷': {
+ name: 'mano_empujando_hacia_la_izquierda',
+ keywords: ['mano', 'empujando', 'izquierda', 'gesto'],
+ },
+ '🫸': {
+ name: 'mano_empujando_hacia_la_derecha',
+ keywords: ['mano', 'empujando', 'derecha', 'gesto'],
+ },
'👏': {
name: 'aplauso',
keywords: ['aplaudir', 'manos', 'palmas', 'señal', 'manos aplaudiendo'],
@@ -734,6 +810,10 @@ const esEmojis: EmojisList = {
name: 'manos_levantadas',
keywords: ['celebración', 'gesto', 'hurra', 'mano', 'manos levantadas celebrando'],
},
+ '🫶': {
+ name: 'manos_haciendo_corazón',
+ keywords: ['mano', 'corazón', 'gesto', 'amor'],
+ },
'👐': {
name: 'manos_abiertas',
keywords: ['abiertas', 'manos'],
@@ -750,6 +830,10 @@ const esEmojis: EmojisList = {
name: 'rezo',
keywords: ['gracias', 'mano', 'oración', 'orar', 'por favor', 'rezar', 'manos en oración'],
},
+ '🫵': {
+ name: 'mano_apuntando',
+ keywords: ['mano', 'apuntar', 'espectador', 'gesto'],
+ },
'✍️': {
name: 'mano_escribiendo',
keywords: ['escribir', 'lápiz', 'mano', 'mano escribiendo'],
@@ -830,6 +914,10 @@ const esEmojis: EmojisList = {
name: 'labios',
keywords: ['labios', 'boca'],
},
+ '🫦': {
+ name: 'labios_mordiendo',
+ keywords: ['mordiendo', 'labio', 'nervioso', 'coqueteo'],
+ },
'👶': {
name: 'bebé',
keywords: ['joven', 'niño', 'bebé'],
@@ -1290,6 +1378,7 @@ const esEmojis: EmojisList = {
name: 'guardia_mujer',
keywords: ['guardia', 'mujer', 'vigilante'],
},
+
'🥷': {
name: 'ninja',
keywords: ['furtivo', 'guerrero', 'luchador', 'oculto', 'sigilo', 'ninja'],
@@ -1310,6 +1399,10 @@ const esEmojis: EmojisList = {
name: 'príncipe',
keywords: ['corona', 'príncipe'],
},
+ '🫅': {
+ name: 'persona_con_corona',
+ keywords: ['persona', 'corona', 'realeza', 'rey', 'reina'],
+ },
'👸': {
name: 'princesa',
keywords: ['cuento', 'fantasía', 'hadas', 'princesa'],
@@ -1362,6 +1455,14 @@ const esEmojis: EmojisList = {
name: 'embarazada',
keywords: ['embarazada', 'mujer'],
},
+ '🫄': {
+ name: 'persona_embarazada',
+ keywords: ['embarazado', 'persona', 'esperando', 'padre'],
+ },
+ '🫃': {
+ name: 'hombre_embarazado',
+ keywords: ['embarazado', 'hombre', 'esperando', 'padre'],
+ },
'🤱': {
name: 'amamantar',
keywords: ['amamantar', 'bebé', 'dar pecho', 'pecho', 'lactancia materna'],
@@ -1478,6 +1579,10 @@ const esEmojis: EmojisList = {
name: 'elfa',
keywords: ['mágico', 'mujer', 'elfa'],
},
+ '🧌': {
+ name: 'trol',
+ keywords: ['trol', 'mítico', 'criatura', 'fantasía'],
+ },
'🧞': {
name: 'genio',
keywords: ['lámpara', 'genio'],
@@ -2110,6 +2215,30 @@ const esEmojis: EmojisList = {
name: 'cara_de_unicornio',
keywords: ['cara', 'unicornio'],
},
+ '🫎': {
+ name: 'alce',
+ keywords: ['alce', 'animal', 'cuernos', 'naturaleza'],
+ },
+ '🫏': {
+ name: 'burro',
+ keywords: ['burro', 'animal', 'granja', 'naturaleza'],
+ },
+ '🪽': {
+ name: 'ala',
+ keywords: ['ala', 'volar', 'pájaro', 'ángel'],
+ },
+ '🐦⬛': {
+ name: 'pájaro_negro',
+ keywords: ['pájaro', 'negro', 'animal', 'naturaleza'],
+ },
+ '🪿': {
+ name: 'ganso',
+ keywords: ['ganso', 'animal', 'ave', 'naturaleza'],
+ },
+ '🪼': {
+ name: 'medusa',
+ keywords: ['medusa', 'animal', 'mar', 'naturaleza'],
+ },
'🦓': {
name: 'cara_zebra',
keywords: ['raya', 'cebra'],
@@ -2426,6 +2555,10 @@ const esEmojis: EmojisList = {
name: 'caracola',
keywords: ['concha', 'mar', 'concha de mar'],
},
+ '🪸': {
+ name: 'coral',
+ keywords: ['coral', 'arrecife', 'mar', 'océano', 'marino'],
+ },
'🐌': {
name: 'caracol',
keywords: ['caracola', 'molusco', 'caracol'],
@@ -2514,6 +2647,14 @@ const esEmojis: EmojisList = {
name: 'flor_marchita',
keywords: ['flor', 'marchita', 'marchitada', 'marchitarse'],
},
+ '🪻': {
+ name: 'jacinto',
+ keywords: ['jacinto', 'flor', 'planta', 'naturaleza'],
+ },
+ '🪷': {
+ name: 'flor_de_loto',
+ keywords: ['loto', 'flor', 'florecer', 'planta'],
+ },
'🌺': {
name: 'hibisco',
keywords: ['flor', 'hibisco', 'flor de hibisco'],
@@ -2582,6 +2723,14 @@ const esEmojis: EmojisList = {
name: 'hojas',
keywords: ['hoja', 'revolotear', 'soplar', 'viento', 'hojas revoloteando al viento'],
},
+ '🪺': {
+ name: 'nido_con_huevos',
+ keywords: ['nido', 'huevos', 'pájaro', 'hogar'],
+ },
+ '🪹': {
+ name: 'nido_vacío',
+ keywords: ['nido', 'vacío', 'pájaro', 'hogar'],
+ },
'🍇': {
name: 'uvas',
keywords: ['agracejo', 'fruta', 'racimo', 'uva', 'uvas'],
@@ -2714,6 +2863,10 @@ const esEmojis: EmojisList = {
name: 'cacahuetes',
keywords: ['cacahuete', 'comida', 'fruto seco', 'verdura', 'cacahuetes'],
},
+ '🫘': {
+ name: 'frijoles',
+ keywords: ['frijoles', 'comida', 'legumbre'],
+ },
'🌰': {
name: 'castaña',
keywords: ['castaño', 'fruto seco', 'castaña'],
@@ -2854,6 +3007,10 @@ const esEmojis: EmojisList = {
name: 'comida_enlatada',
keywords: ['conserva', 'lata', 'comida enlatada'],
},
+ '🫙': {
+ name: 'jarra',
+ keywords: ['jarra', 'contenedor', 'almacenamiento'],
+ },
'🍱': {
name: 'bento',
keywords: ['bento', 'caja', 'comida', 'restaurante', 'caja de bento'],
@@ -2886,6 +3043,14 @@ const esEmojis: EmojisList = {
name: 'batata',
keywords: ['asada', 'papa asada', 'patata', 'restaurante'],
},
+ '🫚': {
+ name: 'jengibre',
+ keywords: ['jengibre', 'especia', 'planta', 'cocina'],
+ },
+ '🫛': {
+ name: 'vaina_de_guisante',
+ keywords: ['vaina', 'guisante', 'vegetal', 'planta'],
+ },
'🍢': {
name: 'oden',
keywords: ['japonés', 'marisco', 'oden', 'pincho', 'brocheta'],
@@ -3006,6 +3171,10 @@ const esEmojis: EmojisList = {
name: 'vaso_de_leche',
keywords: ['bebida', 'leche', 'vaso', 'vaso de leche'],
},
+ '🫗': {
+ name: 'vertiendo_líquido',
+ keywords: ['vertiendo', 'líquido', 'bebida', 'agua'],
+ },
'☕': {
name: 'café',
keywords: ['bebida', 'café', 'caliente', 'té'],
@@ -3494,6 +3663,10 @@ const esEmojis: EmojisList = {
name: 'silla_de_ruedas_eléctrica',
keywords: ['accesibilidad', 'silla de ruedas eléctrica'],
},
+ '🩼': {
+ name: 'muleta',
+ keywords: ['muleta', 'soporte', 'lesión', 'ayuda'],
+ },
'🛺': {
name: 'mototaxi',
keywords: ['rickshaw', 'tuk tuk', 'mototaxi'],
@@ -3514,6 +3687,10 @@ const esEmojis: EmojisList = {
name: 'patines',
keywords: ['patín', 'patín de 4 ruedas', 'patín de cuatro ruedas', 'patines'],
},
+ '🛞': {
+ name: 'rueda',
+ keywords: ['rueda', 'vehículo', 'transporte'],
+ },
'🚏': {
name: 'parada_de_autobús',
keywords: ['autobús', 'parada', 'parada de autobús'],
@@ -3586,6 +3763,10 @@ const esEmojis: EmojisList = {
name: 'barco',
keywords: ['vehículo', 'barco'],
},
+ '🛟': {
+ name: 'aro_salvavidas',
+ keywords: ['aro', 'salvavidas', 'seguridad'],
+ },
'✈️': {
name: 'avión',
keywords: ['aeroplano', 'avión'],
@@ -4006,6 +4187,10 @@ const esEmojis: EmojisList = {
name: 'muñecas',
keywords: ['celebración', 'festival', 'hinamatsuri', 'muñecas', 'muñecas japonesas'],
},
+ '🪭': {
+ name: 'abanico_plegable',
+ keywords: ['abanico', 'plegable', 'viento', 'accesorio'],
+ },
'🎏': {
name: 'banderas',
keywords: ['banderín', 'carpa', 'celebración', 'koinobori', 'banderín de carpas'],
@@ -4014,6 +4199,10 @@ const esEmojis: EmojisList = {
name: 'campanilla_de_viento',
keywords: ['campanilla', 'furin', 'viento', 'campanilla de viento'],
},
+ '🪩': {
+ name: 'bola_de_disco',
+ keywords: ['bola', 'espejo', 'disco', 'fiesta'],
+ },
'🎑': {
name: 'espiga_de_arroz',
keywords: ['celebración', 'contemplación', 'luna', 'tsukimi', 'ceremonia de contemplación de la luna'],
@@ -4186,6 +4375,10 @@ const esEmojis: EmojisList = {
name: 'cometa',
keywords: ['juguete', 'planear', 'viento', 'volar', 'cometa'],
},
+ '🛝': {
+ name: 'resbaladilla',
+ keywords: ['parque', 'resbaladilla', 'jugar', 'parque'],
+ },
'🎱': {
name: 'bola_ocho',
keywords: ['8', 'billar', 'bola ocho', 'juego', 'bola negra de billar'],
@@ -4202,6 +4395,10 @@ const esEmojis: EmojisList = {
name: 'ojo_turco',
keywords: ['amuleto', 'mal de ojo', 'nazar', 'talismán', 'ojo turco'],
},
+ '🪬': {
+ name: 'hamsa',
+ keywords: ['hamsa', 'mano', 'protección', 'suerte'],
+ },
'🎮': {
name: 'videojuego',
keywords: ['juego', 'mando', 'videojuego', 'mando de videoconsola'],
@@ -4562,6 +4759,10 @@ const esEmojis: EmojisList = {
name: 'teclado_musical',
keywords: ['instrumento', 'instrumento musical', 'música', 'teclado', 'piano', 'teclado musical'],
},
+ '🪇': {
+ name: 'maracas',
+ keywords: ['maracas', 'música', 'instrumento', 'ritmo'],
+ },
'🎺': {
name: 'trompeta',
keywords: ['instrumento', 'instrumento musical', 'música', 'trompeta'],
@@ -4570,6 +4771,10 @@ const esEmojis: EmojisList = {
name: 'violín',
keywords: ['instrumento', 'instrumento musical', 'música', 'violín'],
},
+ '🪈': {
+ name: 'flauta',
+ keywords: ['flauta', 'música', 'instrumento', 'viento'],
+ },
'🪕': {
name: 'banjo',
keywords: ['banyo', 'cuerda', 'instrumento', 'música', 'banjo'],
@@ -4610,6 +4815,10 @@ const esEmojis: EmojisList = {
name: 'batería',
keywords: ['batería', 'pila'],
},
+ '🪫': {
+ name: 'batería_baja',
+ keywords: ['bajo', 'batería', 'poder', 'carga'],
+ },
'🔌': {
name: 'enchufe_eléctrico',
keywords: ['corriente', 'electricidad', 'eléctrico', 'enchufe'],
@@ -4822,6 +5031,10 @@ const esEmojis: EmojisList = {
name: 'tarjeta_de_crédito',
keywords: ['crédito', 'tarjeta', 'tarjeta de crédito'],
},
+ '🪪': {
+ name: 'tarjeta_de_identificación',
+ keywords: ['identificación', 'tarjeta', 'ID', 'documento'],
+ },
'🧾': {
name: 'recibo',
keywords: ['contabilidad', 'prueba', 'teneduría de libros', 'testimonio', 'recibo'],
@@ -5150,6 +5363,10 @@ const esEmojis: EmojisList = {
name: 'telescopio',
keywords: ['astronomía', 'instrumento', 'telescopio'],
},
+ '🩻': {
+ name: 'rayos_x',
+ keywords: ['rayos x', 'médico', 'escáner', 'radiología'],
+ },
'📡': {
name: 'antena_de_satélite',
keywords: ['antena', 'comunicación', 'satélite', 'antena de satélite'],
@@ -5226,6 +5443,10 @@ const esEmojis: EmojisList = {
name: 'cuchilla_de_afeitar',
keywords: ['afeitado', 'afeitar', 'afilado', 'barbero', 'navaja', 'cuchilla de afeitar'],
},
+ '🪮': {
+ name: 'peine_para_cabello',
+ keywords: ['peine', 'cabello', 'herramienta', 'accesorio'],
+ },
'🧴': {
name: 'bote_de_crema',
keywords: ['champú', 'crema', 'hidratante', 'protector solar', 'bote de crema'],
@@ -5346,6 +5567,10 @@ const esEmojis: EmojisList = {
name: 'consigna',
keywords: ['depósito', 'equipaje', 'servicio de equipaje en depósito', 'consigna'],
},
+ '🛜': {
+ name: 'inalámbrico',
+ keywords: ['inalámbrico', 'conexión', 'wifi', 'red'],
+ },
'⚠️': {
name: 'advertencia',
keywords: ['cuidado', 'señal', 'advertencia'],
@@ -5502,6 +5727,10 @@ const esEmojis: EmojisList = {
name: 'rueda_del_dharma',
keywords: ['budismo', 'dharma', 'religión', 'rueda', 'rueda del dharma'],
},
+ '🪯': {
+ name: 'khanda',
+ keywords: ['khanda', 'símbolo', 'sijismo', 'religión'],
+ },
'☯️': {
name: 'yin_yang',
keywords: ['religión', 'taoísmo', 'yang', 'yin'],
@@ -5706,6 +5935,10 @@ const esEmojis: EmojisList = {
name: 'signo_de_división_grueso',
keywords: ['÷', 'signo', 'signo de división', 'división'],
},
+ '🟰': {
+ name: 'signo_igual',
+ keywords: ['igual', 'signo', 'matemáticas', 'símbolo'],
+ },
'♾️': {
name: 'infinito',
keywords: ['ilimitado', 'siempre', 'universal', 'infinito'],
diff --git a/docs/_includes/hub.html b/docs/_includes/hub.html
index cac0eaeec382..32d81d88e981 100644
--- a/docs/_includes/hub.html
+++ b/docs/_includes/hub.html
@@ -4,20 +4,19 @@
{% assign activeHub = page.url | remove: activePlatform | remove: "/hubs/" | remove: "/" | remove: ".html" %}
{% assign hub = platform.hubs | where: "href", activeHub | first %}
-
- {{ hub.title }}
-
+{{ hub.title }}
-
- {{ hub.description }}
-
+{{ hub.description }}
-{% assign sortedSectionsAndArticles = hub.sections | concat: hub.articles | sort: 'title' %}
+{% if hub.articles %}
+ {% assign sortedSectionsAndArticles = hub.sections | concat: hub.articles | sort: 'title' %}
+{% else %}
+ {% assign sortedSectionsAndArticles = hub.sections | sort: 'title' %}
+{% endif%}
{% for item in sortedSectionsAndArticles %}
-
{% if item.articles %}
{% include section-card.html platform=activePlatform hub=hub.href section=item.href title=item.title %}
{% else %}
diff --git a/docs/_includes/lhn-template.html b/docs/_includes/lhn-template.html
index d8298aa22aa7..294094214c8e 100644
--- a/docs/_includes/lhn-template.html
+++ b/docs/_includes/lhn-template.html
@@ -33,7 +33,11 @@
{{ hub.title }}
- {% assign sortedSectionsAndArticles = hub.sections | concat: hub.articles | sort: 'title' %}
+ {% if hub.articles %}
+ {% assign sortedSectionsAndArticles = hub.sections | concat: hub.articles | sort: 'title' %}
+ {% else %}
+ {% assign sortedSectionsAndArticles = hub.sections | sort: 'title' %}
+ {% endif%}
{% for item in sortedSectionsAndArticles %}
{% if item.articles %}
diff --git a/docs/_includes/section.html b/docs/_includes/section.html
index b6def157e954..cd48a40585be 100644
--- a/docs/_includes/section.html
+++ b/docs/_includes/section.html
@@ -15,10 +15,12 @@
- {% assign sortedArticles = section.articles | sort: 'order', 'last' | default: 999 %}
- {% for article in sortedArticles %}
- {% assign article_href = section.href | append: '/' | append: article.href %}
- {% include article-card.html hub=hub.href href=article_href title=article.title platform=activePlatform %}
- {% endfor %}
+ {% if section.articles %}
+ {% assign sortedArticles = section.articles | sort: 'order', 'last' | default: 999 %}
+ {% for article in sortedArticles %}
+ {% assign article_href = section.href | append: '/' | append: article.href %}
+ {% include article-card.html hub=hub.href href=article_href title=article.title platform=activePlatform %}
+ {% endfor %}
+ {% endif %}
diff --git a/docs/articles/expensify-classic/expenses/Merge-expenses.md b/docs/articles/expensify-classic/expenses/Merge-expenses.md
index 8b1f573a64b4..e1430ee67543 100644
--- a/docs/articles/expensify-classic/expenses/Merge-expenses.md
+++ b/docs/articles/expensify-classic/expenses/Merge-expenses.md
@@ -38,20 +38,46 @@ If the expenses exist on two different reports, you will be asked which report y
# FAQs
-**Why can’t I merge expenses that are on my submitted report?**
-You cannot merge expenses that are on reports that have already been submitted.
+**Helpful terminology**
-**Can I merge expenses that are under different accounts?**
-No, you cannot merge expenses across two separate accounts.
+- SmartScanned: Any receipt where the data is automatically entered by Expensify. Expenses are SmartScanned by default unless a user stops the SmartScan and enters the details manually.
+- Credit card expense: Any transaction imported from a personal card, company card feed, or CSV.
+- “Cash” expense: Any expense that wasn't imported from a personal card, company card feed, or CSV.
-**Can you merge expenses with different currencies?**
-Yes, you can merge expenses with different currencies. The conversion amount will be based on the daily exchange rate for the date of the transaction, as long as the converted rates are within +/- 5%. If the currencies are the same, then the amounts must be an exact match to merge.
+**How can the icons and receipt images help me diagnose my issue?**
+
+Look carefully at your expenses. Each expense has an icon that denotes where the expense came from:
+- Cash (bill & coins) icon: Added manually or by SmartScanning an expense
+- Credit Card icon: Imported from a connected personal credit card
+- Spreadsheet icon: Imported from a personal CSV import
+- Locked Credit Card icon: Imported from a company card feed or CSV upload
+
+Ideally, your credit card expenses will all also have a SmartScanned receipt attached. If you are in the US and your admin has allowed eReceipts for low-value expenses, your expense may include the locked credit card icon and a QR code for the receipt image.
+
+![Image of different expenses]({{site.url}}/assets/images/Expenses.png){:width="100%"}
+
+If you see any other combination of icon and image, there is likely a duplicate expense and you will need to manually merge the expenses using the steps above.
**Can Expensify automatically merge a cash expense with a credit card expense?**
-Yes, Expensify merges a cash expense with a credit card expense if the receipt is SmartScanned or forwarded to receipts@expensify.com. However, these expenses will not merge if:
-- The card expense added to your Expensify account is older than the receipt you’re trying to merge it with
-- The receipt is dated older than 7 days of the card expense date
-- Either expense date (the date the Expense was incurred, not the date it was added into Expensify) is older than 90 days
-- The transaction was imported with the Expensify API
-However, if a receipt does not automatically merge with the card entry, you can complete this process manually.
+Yes, when a card expense is imported that matches the date and amount for a SmartScanned expense, Expensify automatically merges the new expense into the existing SmartScanned expense, and the expense will now show a credit card icon. The same is true if a receipt is SmartScanned and the transaction has already been imported—it will merge as soon as the SmartScan is complete.
+
+When expenses merge automatically, Expensify uses the SmartScanned merchant name over the merchant data from the bank statement. If the SmartScan is stopped, Expensify can no longer guarantee that the data entry is accurate, so the expenses will not merge.
+
+{% include info.html %}
+Expenses created via the Expensify [Expense Importer API](https://integrations.expensify.com/Integration-Server/doc/#expense-creator) will not automatically merge with card feed transactions.
+{% include end-info.html %}
+
+**Why didn’t my expenses merge automatically?**
+
+Here are some possible reasons for receipt merge failures:
+- The cash receipt was not SmartScanned (manual override by the user)
+- The card expense has a different date than the cash expense
+- The expenses have different amounts (same currency)
+- The expense amounts are outside the FX 5% threshold (different currencies)
+- The expense date is older than 90 days (cash or card)
+- It’s a duplicate - the same receipt was added twice (and the other one merged)
+- The cash receipt was submitted as a reimbursable expense, and reimbursed/exported before the credit card transaction was imported
+- The credit card transaction and the receipt are not in the same Expensify user accounts
+
+Using the above instructions, you can manually merge any Unreported/Open cash receipt and card transaction in the same account.
diff --git a/docs/articles/expensify-classic/reports/Assign-report-approvers-to-specific-employees.md b/docs/articles/expensify-classic/reports/Assign-report-approvers-to-specific-employees.md
index 38279781cec9..dbeca5a49f04 100644
--- a/docs/articles/expensify-classic/reports/Assign-report-approvers-to-specific-employees.md
+++ b/docs/articles/expensify-classic/reports/Assign-report-approvers-to-specific-employees.md
@@ -26,7 +26,7 @@ To assign a report approver to a specific member of your workspace,
- Approves To: Determines who must approve a report after this user has approved it. This creates an approval chain. When added, a note is visible in the Details column of the Workspace Members table. If blank, the user is a “final approver.”
- If Report Total is Over $X then Approves To: These two fields add an extra approver if the report total exceeds the set amount. When added, a note is visible in the Details column of the Workspace Members table.
-[Image coming soon]
+![Image of user approval settings]({{site.url}}/assets/images/Approves_To.png){:width="100%"}
## Example
diff --git a/docs/articles/expensify-classic/reports/How-Complex-Approval-Workflows-Work.md b/docs/articles/expensify-classic/reports/How-Complex-Approval-Workflows-Work.md
new file mode 100644
index 000000000000..372a4783378f
--- /dev/null
+++ b/docs/articles/expensify-classic/reports/How-Complex-Approval-Workflows-Work.md
@@ -0,0 +1,64 @@
+---
+title: How Complex Approval Workflows Work
+description: Examples of how Advanced Approval Workflows apply in real life
+---
+Approval workflows can get complex. Let’s look at the lifecycle of an expense report from submission to final approval.
+
+## 1.Submission
+The approval workflow for all reports starts as soon as the report is submitted. Reports can be submitted manually or set to [submit automatically](https://help.expensify.com/articles/expensify-classic/reports/Automatically-submit-employee-reports) by Concierge.
+
+If you change part of your workflow after a report has been submitted, the workflow for that report will not change unless it is retracted and resubmitted.
+
+## 2.Category & tag approvers
+If you have [special approvers for categories or tags](https://help.expensify.com/articles/expensify-classic/reports/Assign-tag-and-category-approvers) that are added to a report, the report will go to these people for approval first. They will be notified about the report via email or an in-app notification.
+
+## 3.Approval mode
+The report will now travel through the approval workflow for the workspace:
+- Submit & Close: If you use a [Submit & Close](https://help.expensify.com/articles/expensify-classic/reports/Create-a-report-approval-workflow) workflow, the report will now close and notify the set person.
+- Submit & Approve: If you use a [Submit & Approve](https://help.expensify.com/articles/expensify-classic/reports/Create-a-report-approval-workflow) workflow, the report will now go to the set person for their approval.
+- Advanced Approval: If you use an [Advanced Approval](https://help.expensify.com/articles/expensify-classic/reports/Create-a-report-approval-workflow) workflow, the report will now go to the person in the submitter’s “Submits to” column of the Workspace Members table. Once that person approves the report,
+ a. The report then goes to the person in the approver’s Approves To column.
+ b. If the approver has an [approval value limit](https://help.expensify.com/articles/expensify-classic/reports/Require-review-for-over-limit-expenses) (i.e. they have approval restrictions based on the total amount of the report), the report will go to the set person.
+ c. The report continues through the approval workflow until it reaches someone who does not have anyone in their Approves To column. This person is the final approver.
+
+Once the report receives final approval, it may be exported to a [connected accounting software](https://help.expensify.com/expensify-classic/hubs/connections/) and/or reimbursed.
+
+## 4.Concierge approval
+If you’ve chosen to [require manual approval for expenses that exceed a set limit](https://help.expensify.com/articles/expensify-classic/reports/Require-review-for-over-limit-expenses), any report that doesn’t contain a single expense over this amount will be approved by Concierge.
+
+## Workflow examples
+Here are some scenarios to demonstrate the different approval workflows.
+
+### Submit & Close
+Two business partners named Terry and Dana run a small business together. They don’t need approvals for their expenses, so they use the Submit & Close workflow and have Terry set as the person to submit to. Once submitted, their reports are closed.
+
+*Outcome: All submitted reports go to the main approver and are then closed automatically. No action is required for approval.*
+
+### Submit & Approve with category approvers
+Pat does the accounting for a small engineering firm. Everyone submits their reports to Pat for approval. However, all plant and equipment purchases must be seen by Dale. They created a category called “3005 Plant and Equipment” and assigned Dale as the category approver. This way, when a report is coded with this category, it will go to Dale once it is submitted. Then Pat will receive the report for final approval, reimbursement, and to export it to their accounting software connection.
+
+*Outcome: All submitted reports go to the main approver unless they contain expenses coded with a category that requires review by another approver first.*
+
+### Submit & Approve with Scheduled Submit and Concierge approval
+Sandra’s company has a manual approval threshold of $100 and has Scheduled Submit set to weekly. That means that as long as each expense is under $100, reports will be automatically submitted once a week.
+
+Her sales rep David regularly hosts client meetings, which results in a report filled with coffee, lunch, and parking expenses paid for on his company card. David has dutifully SmartScanned his receipts and coding them after each purchase. None of the individual expenses are over $100, so his report submits itself on Sunday evening. The report is instantly final approved within moments of submission, and the report history notes that Concierge both submitted and final approved the report.
+
+*Outcome: Report submission and approval are automated unless there are large expenses.*
+
+### Advanced Approval - Example 1
+Amal is a photojournalist for a major magazine, and they have been traveling for a week. The last report they submitted contains meals, accommodations, and emergency camera equipment.
+1. Their report first goes to Tony, head of photography, who is the category approver for the “6050 Cameras and AV” category that was added to Amal’s report.
+2. The report then goes to Jamie, who is Amal’s manager and “submits to.”
+3. Jamie approves and forwards the report to her “approves to” person, which is her manager Ali.
+4. Ali approves and forwards the report to his “approves to” person, which is the finance team.
+5. The finance team is the final approver for the report. They check the coding, approve the report, export it to the accounting system, and reimburse Amal.
+
+*Outcome: Reports go through category-specific approvers first, then through the multiple levels of approval.*
+
+### Advanced Approval - Example 2
+Amal had another travel week, but this time to Hong Kong. Their report total includes a $1,200 flight as well as $950 for accommodations and food.
+
+The report goes to Jamie for approval, but Jamie has an “If Report Total is Over $2000 then Approves To” rule that requires large expenses to be approved by Lee. That means once Jamie approves the report, Lee must approve the expense next. Once Lee approves, the report goes to the finance team who completes the final approval.
+
+*Outcome: Reports with multiple levels of approval go through each approver whose approval limit is not great enough until it reaches the approver with the required approval limit.*
diff --git a/docs/articles/new-expensify/connections/Netsuite/Configure-Netsuite.md b/docs/articles/new-expensify/connections/Netsuite/Configure-Netsuite.md
new file mode 100644
index 000000000000..7ae0aa577468
--- /dev/null
+++ b/docs/articles/new-expensify/connections/Netsuite/Configure-Netsuite.md
@@ -0,0 +1,6 @@
+---
+title: Configure Netsuite
+description: Coming soon
+---
+
+# Coming soon
diff --git a/docs/articles/new-expensify/connections/Set-Up-NetSuite-Connection.md b/docs/articles/new-expensify/connections/Netsuite/Connect-to-NetSuite.md
similarity index 99%
rename from docs/articles/new-expensify/connections/Set-Up-NetSuite-Connection.md
rename to docs/articles/new-expensify/connections/Netsuite/Connect-to-NetSuite.md
index 5c6678e068be..7cf70cca5abc 100644
--- a/docs/articles/new-expensify/connections/Set-Up-NetSuite-Connection.md
+++ b/docs/articles/new-expensify/connections/Netsuite/Connect-to-NetSuite.md
@@ -1,8 +1,8 @@
---
-title: Set up NetSuite connection
+title: Connect to NetSuite
description: Integrate NetSuite with Expensify
+order: 1
---
-
# Connect to NetSuite
diff --git a/docs/articles/new-expensify/connections/Netsuite/Netsuite-Troubleshooting.md b/docs/articles/new-expensify/connections/Netsuite/Netsuite-Troubleshooting.md
new file mode 100644
index 000000000000..2ac1aaadbef4
--- /dev/null
+++ b/docs/articles/new-expensify/connections/Netsuite/Netsuite-Troubleshooting.md
@@ -0,0 +1,6 @@
+---
+title: Netsuite Troubleshooting
+description: Coming soon
+---
+
+# Coming soon
diff --git a/docs/articles/new-expensify/connections/Quickbooks-Online/Configure-Quickbooks-Online.md b/docs/articles/new-expensify/connections/Quickbooks-Online/Configure-Quickbooks-Online.md
new file mode 100644
index 000000000000..db050e5be312
--- /dev/null
+++ b/docs/articles/new-expensify/connections/Quickbooks-Online/Configure-Quickbooks-Online.md
@@ -0,0 +1,6 @@
+---
+title: Configure Quickbooks Online
+description: Coming soon
+---
+
+# Coming soon
diff --git a/docs/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection.md b/docs/articles/new-expensify/connections/Quickbooks-Online/Connect-to-QuickBooks-Online.md
similarity index 99%
rename from docs/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection.md
rename to docs/articles/new-expensify/connections/Quickbooks-Online/Connect-to-QuickBooks-Online.md
index 79d5b17055f7..60fdbe94b33b 100644
--- a/docs/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection.md
+++ b/docs/articles/new-expensify/connections/Quickbooks-Online/Connect-to-QuickBooks-Online.md
@@ -1,8 +1,8 @@
---
-title: Set up QuickBooks Online connection
+title: Connect to QuickBooks Online
description: Integrate QuickBooks Online with Expensify
+order: 1
---
-
{% include info.html %}
To use the QuickBooks Online connection, you must have a QuickBooks Online account and an Expensify Collect plan. The QuickBooks Self-employed subscription is not supported.
@@ -134,5 +134,3 @@ This may occur if you incorrectly enter your QuickBooks Online login information
3. Enter your Intuit login details (the login information you use for QuickBooks Online) to establish the connection.
{% include faq-end.md %}
-
-
diff --git a/docs/articles/new-expensify/connections/Quickbooks-Online/Quickbooks-Online-Troubleshooting.md b/docs/articles/new-expensify/connections/Quickbooks-Online/Quickbooks-Online-Troubleshooting.md
new file mode 100644
index 000000000000..5256459d6f9a
--- /dev/null
+++ b/docs/articles/new-expensify/connections/Quickbooks-Online/Quickbooks-Online-Troubleshooting.md
@@ -0,0 +1,6 @@
+---
+title: Quickbooks Online Troubleshooting
+description: Coming soon
+---
+
+# Coming soon
diff --git a/docs/articles/new-expensify/connections/Sage-Intacct/Configure-Sage-Intacct.md b/docs/articles/new-expensify/connections/Sage-Intacct/Configure-Sage-Intacct.md
new file mode 100644
index 000000000000..c5e549ff74d1
--- /dev/null
+++ b/docs/articles/new-expensify/connections/Sage-Intacct/Configure-Sage-Intacct.md
@@ -0,0 +1,6 @@
+---
+title: Configure Sage Intacct
+description: Coming soon
+---
+
+# Coming soon
diff --git a/docs/articles/new-expensify/connections/Set-Up-Sage-Intacct-connection.md b/docs/articles/new-expensify/connections/Sage-Intacct/Connect-to-Sage-Intacct.md
similarity index 99%
rename from docs/articles/new-expensify/connections/Set-Up-Sage-Intacct-connection.md
rename to docs/articles/new-expensify/connections/Sage-Intacct/Connect-to-Sage-Intacct.md
index 1f5d9662bb4f..35a009ae8d4a 100644
--- a/docs/articles/new-expensify/connections/Set-Up-Sage-Intacct-connection.md
+++ b/docs/articles/new-expensify/connections/Sage-Intacct/Connect-to-Sage-Intacct.md
@@ -1,8 +1,8 @@
---
-title: Set up Sage Intacct connection
+title: Connect to Sage Intacct
description: Integrate Sage Intacct with Expensify
+order: 1
---
-
# Connect to Sage Intacct
diff --git a/docs/articles/new-expensify/connections/Sage-Intacct/Sage-Intacct-Troubleshooting.md b/docs/articles/new-expensify/connections/Sage-Intacct/Sage-Intacct-Troubleshooting.md
new file mode 100644
index 000000000000..ae8a0f16d9b9
--- /dev/null
+++ b/docs/articles/new-expensify/connections/Sage-Intacct/Sage-Intacct-Troubleshooting.md
@@ -0,0 +1,6 @@
+---
+title: Sage Intacct Troubleshooting
+description: Coming soon
+---
+
+# Coming soon
diff --git a/docs/articles/new-expensify/connections/Xero/Configure-Xero.md b/docs/articles/new-expensify/connections/Xero/Configure-Xero.md
new file mode 100644
index 000000000000..0c65db1b4fd9
--- /dev/null
+++ b/docs/articles/new-expensify/connections/Xero/Configure-Xero.md
@@ -0,0 +1,6 @@
+---
+title: Configure Xero
+description: Coming soon
+---
+
+# Coming soon
diff --git a/docs/articles/new-expensify/connections/Set-up-Xero-connection.md b/docs/articles/new-expensify/connections/Xero/Connect-to-Xero.md
similarity index 98%
rename from docs/articles/new-expensify/connections/Set-up-Xero-connection.md
rename to docs/articles/new-expensify/connections/Xero/Connect-to-Xero.md
index 47917f2dffc3..eb35b1589db4 100644
--- a/docs/articles/new-expensify/connections/Set-up-Xero-connection.md
+++ b/docs/articles/new-expensify/connections/Xero/Connect-to-Xero.md
@@ -1,8 +1,8 @@
---
-title: Set up Xero connection
+title: Connect to Xero
description: Integrate Xero with Expensify
+order: 1
---
-
{% include info.html %}
To use the Xero connection, you must have a Xero account and an Expensify Collect plan.
@@ -104,5 +104,3 @@ The following steps help you determine the advanced settings for your connection
You will no longer see the imported options from Xero.
{% include faq-end.md %}
-
-
diff --git a/docs/articles/new-expensify/connections/Xero/Xero-Troubleshooting.md b/docs/articles/new-expensify/connections/Xero/Xero-Troubleshooting.md
new file mode 100644
index 000000000000..9c211efbaf24
--- /dev/null
+++ b/docs/articles/new-expensify/connections/Xero/Xero-Troubleshooting.md
@@ -0,0 +1,6 @@
+---
+title: Xero Troubleshooting
+description: Coming soon
+---
+
+# Coming soon
diff --git a/docs/new-expensify/hubs/connections/netsuite.html b/docs/new-expensify/hubs/connections/netsuite.html
new file mode 100644
index 000000000000..86641ee60b7d
--- /dev/null
+++ b/docs/new-expensify/hubs/connections/netsuite.html
@@ -0,0 +1,5 @@
+---
+layout: default
+---
+
+{% include section.html %}
diff --git a/docs/new-expensify/hubs/connections/quickbooks-online.html b/docs/new-expensify/hubs/connections/quickbooks-online.html
new file mode 100644
index 000000000000..86641ee60b7d
--- /dev/null
+++ b/docs/new-expensify/hubs/connections/quickbooks-online.html
@@ -0,0 +1,5 @@
+---
+layout: default
+---
+
+{% include section.html %}
diff --git a/docs/new-expensify/hubs/connections/sage-intacct.html b/docs/new-expensify/hubs/connections/sage-intacct.html
new file mode 100644
index 000000000000..86641ee60b7d
--- /dev/null
+++ b/docs/new-expensify/hubs/connections/sage-intacct.html
@@ -0,0 +1,5 @@
+---
+layout: default
+---
+
+{% include section.html %}
diff --git a/docs/new-expensify/hubs/connections/xero.html b/docs/new-expensify/hubs/connections/xero.html
new file mode 100644
index 000000000000..86641ee60b7d
--- /dev/null
+++ b/docs/new-expensify/hubs/connections/xero.html
@@ -0,0 +1,5 @@
+---
+layout: default
+---
+
+{% include section.html %}
diff --git a/docs/redirects.csv b/docs/redirects.csv
index 897cd4e95775..4b7cc43bd072 100644
--- a/docs/redirects.csv
+++ b/docs/redirects.csv
@@ -275,3 +275,11 @@ https://help.expensify.com/articles/expensify-classic/integrations/HR-integratio
https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/Xero.html,https://help.expensify.com/expensify-classic/hubs/connections/xero
https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/Zenefits.html,https://help.expensify.com/articles/expensify-classic/connections/Zenefits
https://help.expensify.com/articles/expensify-classic/settings/Close-or-reopen-account,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Close-or-reopen-account
+https://help.expensify.com/articles/new-expensify/connections/Set-Up-NetSuite-Connection,https://help.expensify.com/articles/new-expensify/connections/netsuite/Connect-to-NetSuite
+https://help.expensify.com/articles/new-expensify/connections/Set-Up-NetSuite-Connection.html,https://help.expensify.com/articles/new-expensify/connections/netsuite/Connect-to-NetSuite
+https://help.expensify.com/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection,https://help.expensify.com/articles/new-expensify/connections/quickbooks-online/Connect-to-QuickBooks-Online
+https://help.expensify.com/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection.html,https://help.expensify.com/articles/new-expensify/connections/quickbooks-online/Connect-to-QuickBooks-Online
+https://help.expensify.com/articles/new-expensify/connections/Set-Up-Sage-Intacct-connection,https://help.expensify.com/articles/new-expensify/connections/sage-intacct/Connect-to-Sage-Intacct
+https://help.expensify.com/articles/new-expensify/connections/Set-Up-Sage-Intacct-connection.html,https://help.expensify.com/articles/new-expensify/connections/sage-intacct/Connect-to-Sage-Intacct
+https://help.expensify.com/articles/new-expensify/connections/Set-up-Xero-connection,https://help.expensify.com/articles/new-expensify/connections/xero/Connect-to-Xero
+https://help.expensify.com/articles/new-expensify/connections/Set-up-Xero-connection.html,https://help.expensify.com/articles/new-expensify/connections/xero/Connect-to-Xero
\ No newline at end of file
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index ef1647dd148a..f224dcb9db92 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
-
9.0.13
+
9.0.14
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
-
9.0.13.3
+
9.0.14.2
FullStory
OrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 892b4b6304b6..520c82a45f77 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 9.0.13
+ 9.0.14
CFBundleSignature
????
CFBundleVersion
- 9.0.13.3
+ 9.0.14.2
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 7afb5537890e..77a3afc2e271 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName
$(PRODUCT_NAME)
CFBundleShortVersionString
-
9.0.13
+
9.0.14
CFBundleVersion
-
9.0.13.3
+
9.0.14.2
NSExtension
NSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index 4dea9d28cd9b..9082f9183c23 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.0.13-3",
+ "version": "9.0.14-2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.13-3",
+ "version": "9.0.14-2",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 34db4f413eb0..89d2a89aeb42 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.13-3",
+ "version": "9.0.14-2",
"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.",
diff --git a/src/CONST.ts b/src/CONST.ts
index d929a01e030a..55096bb279b2 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -363,7 +363,6 @@ const CONST = {
},
BETAS: {
ALL: 'all',
- CHRONOS_IN_CASH: 'chronosInCash',
DEFAULT_ROOMS: 'defaultRooms',
VIOLATIONS: 'violations',
DUPE_DETECTION: 'dupeDetection',
@@ -682,6 +681,9 @@ const CONST = {
ACTIONABLE_TRACK_EXPENSE_WHISPER: 'ACTIONABLETRACKEXPENSEWHISPER',
ADD_COMMENT: 'ADDCOMMENT',
APPROVED: 'APPROVED',
+ CARD_MISSING_ADDRESS: 'CARDMISSINGADDRESS',
+ CARD_ISSUED: 'CARDISSUED',
+ CARD_ISSUED_VIRTUAL: 'CARDISSUEDVIRTUAL',
CHANGE_FIELD: 'CHANGEFIELD', // OldDot Action
CHANGE_POLICY: 'CHANGEPOLICY', // OldDot Action
CHANGE_TYPE: 'CHANGETYPE', // OldDot Action
@@ -5251,6 +5253,9 @@ const CONST = {
DRAFTS: 'drafts',
FINISHED: 'finished',
},
+ TYPE: {
+ EXPENSE: 'expense',
+ },
TAB: {
EXPENSE: {
ALL: 'type:expense status:all',
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index af29a7fdbbb4..0811ea02e9d6 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -33,8 +33,6 @@ const ROUTES = {
// This route renders the list of reports.
HOME: 'home',
- ALL_SETTINGS: 'all-settings',
-
SEARCH_CENTRAL_PANE: {
route: 'search',
getRoute: ({query, isCustomQuery = false, policyIDs}: {query: SearchQueryString; isCustomQuery?: boolean; policyIDs?: string}) =>
@@ -47,6 +45,8 @@ const ROUTES = {
SEARCH_ADVANCED_FILTERS_TYPE: 'search/filters/type',
+ SEARCH_ADVANCED_FILTERS_STATUS: 'search/filters/status',
+
SEARCH_REPORT: {
route: 'search/view/:reportID',
getRoute: (reportID: string) => `search/view/${reportID}` as const,
@@ -56,6 +56,8 @@ const ROUTES = {
// This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated
CONCIERGE: 'concierge',
+ TRACK_EXPENSE: 'track-expense',
+ SUBMIT_EXPENSE: 'submit-expense',
FLAG_COMMENT: {
route: 'flag/:reportID/:reportActionID',
getRoute: (reportID: string, reportActionID: string) => `flag/${reportID}/${reportActionID}` as const,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 74d4a628e696..4047b0a851bc 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -8,11 +8,12 @@ const PROTECTED_SCREENS = {
HOME: 'Home',
CONCIERGE: 'Concierge',
ATTACHMENTS: 'Attachments',
+ TRACK_EXPENSE: 'TrackExpense',
+ SUBMIT_EXPENSE: 'SubmitExpense',
} as const;
const SCREENS = {
...PROTECTED_SCREENS,
- ALL_SETTINGS: 'AllSettings',
REPORT: 'Report',
PROFILE_AVATAR: 'ProfileAvatar',
WORKSPACE_AVATAR: 'WorkspaceAvatar',
@@ -33,6 +34,7 @@ const SCREENS = {
ADVANCED_FILTERS_RHP: 'Search_Advanced_Filters_RHP',
ADVANCED_FILTERS_DATE_RHP: 'Search_Advanced_Filters_Date_RHP',
ADVANCED_FILTERS_TYPE_RHP: 'Search_Advanced_Filters_Type_RHP',
+ ADVANCED_FILTERS_STATUS_RHP: 'Search_Advanced_Filters_Status_RHP',
TRANSACTION_HOLD_REASON_RHP: 'Search_Transaction_Hold_Reason_RHP',
BOTTOM_TAB: 'Search_Bottom_Tab',
},
diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx
index 9d41f7823e6e..12802476ed0d 100644
--- a/src/components/ButtonWithDropdownMenu/index.tsx
+++ b/src/components/ButtonWithDropdownMenu/index.tsx
@@ -31,6 +31,8 @@ function ButtonWithDropdownMenu({
onPress,
options,
onOptionSelected,
+ onOptionsMenuShow,
+ onOptionsMenuHide,
enterKeyEventListenerPriority = 0,
wrapperStyle,
}: ButtonWithDropdownMenuProps) {
@@ -136,7 +138,11 @@ function ButtonWithDropdownMenu({
{(shouldAlwaysShowDropdownMenu || options.length > 1) && popoverAnchorPosition && (
setIsMenuVisible(false)}
+ onClose={() => {
+ setIsMenuVisible(false);
+ onOptionsMenuHide?.();
+ }}
+ onModalShow={onOptionsMenuShow}
onItemSelected={() => setIsMenuVisible(false)}
anchorPosition={popoverAnchorPosition}
anchorRef={caretButton}
diff --git a/src/components/ButtonWithDropdownMenu/types.ts b/src/components/ButtonWithDropdownMenu/types.ts
index baac60190ce5..f20729380b60 100644
--- a/src/components/ButtonWithDropdownMenu/types.ts
+++ b/src/components/ButtonWithDropdownMenu/types.ts
@@ -45,6 +45,12 @@ type ButtonWithDropdownMenuProps = {
/** Callback to execute when a dropdown option is selected */
onOptionSelected?: (option: DropdownOption) => void;
+ /** Callback when the options popover is shown */
+ onOptionsMenuShow?: () => void;
+
+ /** Callback when the options popover is shown */
+ onOptionsMenuHide?: () => void;
+
/** Call the onPress function on main button when Enter key is pressed */
pressOnEnter?: boolean;
diff --git a/src/components/ConnectToNetSuiteButton/index.tsx b/src/components/ConnectToNetSuiteButton/index.tsx
index bd6dabf09097..928bc01f12c1 100644
--- a/src/components/ConnectToNetSuiteButton/index.tsx
+++ b/src/components/ConnectToNetSuiteButton/index.tsx
@@ -56,9 +56,7 @@ function ConnectToNetSuiteButton({policyID, shouldDisconnectIntegrationBeforeCon
{
if (!isControlPolicy(policy)) {
- Navigation.navigate(
- ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.netsuite.alias, ROUTES.POLICY_ACCOUNTING_NETSUITE_TOKEN_INPUT.getRoute(policyID)),
- );
+ Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.netsuite.alias));
return;
}
diff --git a/src/components/ConnectToSageIntacctButton/index.tsx b/src/components/ConnectToSageIntacctButton/index.tsx
index 1286e3b8fc59..6c6523ad6e75 100644
--- a/src/components/ConnectToSageIntacctButton/index.tsx
+++ b/src/components/ConnectToSageIntacctButton/index.tsx
@@ -63,13 +63,7 @@ function ConnectToSageIntacctButton({policyID, shouldDisconnectIntegrationBefore
{
if (!isControlPolicy(policy)) {
- Navigation.navigate(
- ROUTES.WORKSPACE_UPGRADE.getRoute(
- policyID,
- CONST.UPGRADE_FEATURE_INTRO_MAPPING.intacct.alias,
- ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_PREREQUISITES.getRoute(policyID),
- ),
- );
+ Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.intacct.alias));
return;
}
diff --git a/src/components/CustomStatusBarAndBackground/index.tsx b/src/components/CustomStatusBarAndBackground/index.tsx
index 356fbd3726a3..d21c0b3f9be0 100644
--- a/src/components/CustomStatusBarAndBackground/index.tsx
+++ b/src/components/CustomStatusBarAndBackground/index.tsx
@@ -95,20 +95,34 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack
prevStatusBarBackgroundColor.current = statusBarBackgroundColor.current;
statusBarBackgroundColor.current = currentScreenBackgroundColor;
- if (currentScreenBackgroundColor !== theme.appBG || prevStatusBarBackgroundColor.current !== theme.appBG) {
+ const callUpdateStatusBarAppearance = () => {
+ updateStatusBarAppearance({statusBarStyle: newStatusBarStyle});
+ setStatusBarStyle(newStatusBarStyle);
+ };
+
+ const callUpdateStatusBarBackgroundColor = () => {
statusBarAnimation.value = 0;
statusBarAnimation.value = withDelay(300, withTiming(1));
- }
+ };
// Don't update the status bar style if it's the same as the current one, to prevent flashing.
// Force update if the root status bar is back on active or it won't overwirte the nested status bar style
- if ((!didForceUpdateStatusBarRef.current && !prevIsRootStatusBarEnabled && isRootStatusBarEnabled) || newStatusBarStyle !== statusBarStyle) {
- updateStatusBarAppearance({statusBarStyle: newStatusBarStyle});
- setStatusBarStyle(newStatusBarStyle);
+ if (!didForceUpdateStatusBarRef.current && !prevIsRootStatusBarEnabled && isRootStatusBarEnabled) {
+ callUpdateStatusBarAppearance();
+ callUpdateStatusBarBackgroundColor();
if (!prevIsRootStatusBarEnabled && isRootStatusBarEnabled) {
didForceUpdateStatusBarRef.current = true;
}
+ return;
+ }
+
+ if (newStatusBarStyle !== statusBarStyle) {
+ callUpdateStatusBarAppearance();
+ }
+
+ if (currentScreenBackgroundColor !== theme.appBG || prevStatusBarBackgroundColor.current !== theme.appBG) {
+ callUpdateStatusBarBackgroundColor();
}
},
[prevIsRootStatusBarEnabled, isRootStatusBarEnabled, statusBarAnimation, statusBarStyle, theme.PAGE_THEMES, theme.appBG, theme.statusBarStyle],
diff --git a/src/components/EmojiPicker/EmojiPicker.tsx b/src/components/EmojiPicker/EmojiPicker.tsx
index a1b496838529..777d77a2411b 100644
--- a/src/components/EmojiPicker/EmojiPicker.tsx
+++ b/src/components/EmojiPicker/EmojiPicker.tsx
@@ -41,7 +41,7 @@ function EmojiPicker({viewportOffsetTop}: EmojiPickerProps, ref: ForwardedRef {});
+ const onModalHide = useRef(() => {});
const onEmojiSelected = useRef(() => {});
const activeEmoji = useRef();
const emojiSearchInput = useRef();
@@ -112,13 +112,10 @@ function EmojiPicker({viewportOffsetTop}: EmojiPickerProps, ref: ForwardedRef {
- if (isNavigating) {
- onModalHide.current = () => {};
- }
const currOnModalHide = onModalHide.current;
onModalHide.current = () => {
if (currOnModalHide) {
- currOnModalHide();
+ currOnModalHide(!!isNavigating);
}
// eslint-disable-next-line react-compiler/react-compiler
emojiPopoverAnchorRef.current = null;
diff --git a/src/components/EmptyStateComponent/index.tsx b/src/components/EmptyStateComponent/index.tsx
index 41d1a42931c6..56f8fa5e3d83 100644
--- a/src/components/EmptyStateComponent/index.tsx
+++ b/src/components/EmptyStateComponent/index.tsx
@@ -24,7 +24,7 @@ function EmptyStateComponent({
subtitle,
headerStyles,
headerContentStyles,
- emptyStateContentStyles,
+ emptyStateForegroundStyles,
}: EmptyStateComponentProps) {
const styles = useThemeStyles();
const {isSmallScreenWidth} = useWindowDimensions();
@@ -86,8 +86,8 @@ function EmptyStateComponent({
shouldAnimate={false}
/>
-
-
+
+
{HeaderComponent}
{title}
diff --git a/src/components/EmptyStateComponent/types.ts b/src/components/EmptyStateComponent/types.ts
index 96a60fa98513..258e8a610e16 100644
--- a/src/components/EmptyStateComponent/types.ts
+++ b/src/components/EmptyStateComponent/types.ts
@@ -19,7 +19,7 @@ type SharedProps = {
headerStyles?: StyleProp;
headerMediaType: T;
headerContentStyles?: StyleProp;
- emptyStateContentStyles?: StyleProp;
+ emptyStateForegroundStyles?: StyleProp;
};
type MediaType = SharedProps & {
diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx
index 77ef44343792..e9f14315486d 100644
--- a/src/components/Form/FormWrapper.tsx
+++ b/src/components/Form/FormWrapper.tsx
@@ -63,7 +63,7 @@ function FormWrapper({
shouldUseScrollView = true,
scrollContextEnabled = false,
shouldHideFixErrorsAlert = false,
- disablePressOnEnter = true,
+ disablePressOnEnter = false,
isSubmitDisabled = false,
}: FormWrapperProps) {
const styles = useThemeStyles();
diff --git a/src/components/HybridAppMiddleware/index.ios.tsx b/src/components/HybridAppMiddleware/index.ios.tsx
index 1ea8e62ab17e..aee837e02dea 100644
--- a/src/components/HybridAppMiddleware/index.ios.tsx
+++ b/src/components/HybridAppMiddleware/index.ios.tsx
@@ -1,5 +1,5 @@
import type React from 'react';
-import {useContext, useEffect, useState} from 'react';
+import {useContext, useEffect, useRef, useState} from 'react';
import {NativeEventEmitter, NativeModules} from 'react-native';
import type {NativeModule} from 'react-native';
import {useOnyx} from 'react-native-onyx';
@@ -15,6 +15,7 @@ import * as Welcome from '@userActions/Welcome';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {HybridAppRoute, Route} from '@src/ROUTES';
+import ROUTES from '@src/ROUTES';
import type {TryNewDot} from '@src/types/onyx';
type HybridAppMiddlewareProps = {
@@ -50,6 +51,20 @@ function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps
const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email});
const [completedHybridAppOnboarding] = useOnyx(ONYXKEYS.NVP_TRYNEWDOT, {selector: onboardingStatusSelector});
+ const maxTimeoutRef = useRef(null);
+
+ // We need to ensure that the BootSplash is always hidden after a certain period.
+ useEffect(() => {
+ if (!NativeModules.HybridAppModule) {
+ return;
+ }
+
+ maxTimeoutRef.current = setTimeout(() => {
+ Log.info('[HybridApp] Forcing transition due to unknown problem', true);
+ setStartedTransition(true);
+ setExitTo(ROUTES.HOME);
+ }, 3000);
+ }, []);
/**
* This useEffect tracks changes of `nvp_tryNewDot` value.
* We propagate it from OldDot to NewDot with native method due to limitations of old app.
diff --git a/src/components/HybridAppMiddleware/index.tsx b/src/components/HybridAppMiddleware/index.tsx
index 5a8d8d6dfebe..bb5d7803e52e 100644
--- a/src/components/HybridAppMiddleware/index.tsx
+++ b/src/components/HybridAppMiddleware/index.tsx
@@ -1,5 +1,5 @@
import type React from 'react';
-import {useContext, useEffect, useState} from 'react';
+import {useContext, useEffect, useRef, useState} from 'react';
import {NativeModules} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
@@ -14,6 +14,7 @@ import * as Welcome from '@userActions/Welcome';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {HybridAppRoute, Route} from '@src/ROUTES';
+import ROUTES from '@src/ROUTES';
import type {TryNewDot} from '@src/types/onyx';
type HybridAppMiddlewareProps = {
@@ -49,6 +50,20 @@ function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps
const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email});
const [completedHybridAppOnboarding] = useOnyx(ONYXKEYS.NVP_TRYNEWDOT, {selector: onboardingStatusSelector});
+ const maxTimeoutRef = useRef(null);
+
+ // We need to ensure that the BootSplash is always hidden after a certain period.
+ useEffect(() => {
+ if (!NativeModules.HybridAppModule) {
+ return;
+ }
+
+ maxTimeoutRef.current = setTimeout(() => {
+ Log.info('[HybridApp] Forcing transition due to unknown problem', true);
+ setStartedTransition(true);
+ setExitTo(ROUTES.HOME);
+ }, 3000);
+ }, []);
/**
* This useEffect tracks changes of `nvp_tryNewDot` value.
* We propagate it from OldDot to NewDot with native method due to limitations of old app.
@@ -93,13 +108,15 @@ function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps
Navigation.isNavigationReady().then(() => {
// We need to remove /transition from route history.
// `useExitTo` returns undefined for routes other than /transition.
- if (exitToParam) {
+ if (exitToParam && Navigation.getActiveRoute().includes(ROUTES.TRANSITION_BETWEEN_APPS)) {
Log.info('[HybridApp] Removing /transition route from history', true);
Navigation.goBack();
}
- Log.info('[HybridApp] Navigating to `exitTo` route', true, {exitTo});
- Navigation.navigate(Navigation.parseHybridAppUrl(exitTo));
+ if (exitTo !== ROUTES.HOME) {
+ Log.info('[HybridApp] Navigating to `exitTo` route', true, {exitTo});
+ Navigation.navigate(Navigation.parseHybridAppUrl(exitTo));
+ }
setExitTo(undefined);
setTimeout(() => {
diff --git a/src/components/Popover/index.tsx b/src/components/Popover/index.tsx
index 47486fd32791..ebf30d773f09 100644
--- a/src/components/Popover/index.tsx
+++ b/src/components/Popover/index.tsx
@@ -27,6 +27,7 @@ function Popover(props: PopoverProps) {
anchorRef = () => {},
animationIn = 'fadeIn',
animationOut = 'fadeOut',
+ shouldCloseWhenBrowserNavigationChanged = true,
} = props;
const {isSmallScreenWidth} = useResponsiveLayout();
@@ -36,18 +37,20 @@ function Popover(props: PopoverProps) {
// Not adding this inside the PopoverProvider
// because this is an issue on smaller screens as well.
React.useEffect(() => {
+ if (!shouldCloseWhenBrowserNavigationChanged) {
+ return;
+ }
const listener = () => {
if (!isVisible) {
return;
}
-
onClose();
};
window.addEventListener('popstate', listener);
return () => {
window.removeEventListener('popstate', listener);
};
- }, [onClose, isVisible]);
+ }, [onClose, isVisible, shouldCloseWhenBrowserNavigationChanged]);
const onCloseWithPopoverContext = () => {
if (popover && 'current' in anchorRef) {
diff --git a/src/components/Popover/types.ts b/src/components/Popover/types.ts
index 4e2f38293f6e..1db09d0b2f9f 100644
--- a/src/components/Popover/types.ts
+++ b/src/components/Popover/types.ts
@@ -38,8 +38,8 @@ type PopoverProps = BaseModalProps &
/** Whether we want to show the popover on the right side of the screen */
fromSidebarMediumScreen?: boolean;
- /** Whether handle navigation back when modal show. */
- shouldHandleNavigationBack?: boolean;
+ /** Whether we should close when browser navigation change. This doesn't affect native platform */
+ shouldCloseWhenBrowserNavigationChanged?: boolean;
};
type PopoverWithWindowDimensionsProps = PopoverProps & WindowDimensionsProps;
diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx
index d1fd32a0a6b8..d4cb30d626a9 100644
--- a/src/components/PopoverMenu.tsx
+++ b/src/components/PopoverMenu.tsx
@@ -42,6 +42,9 @@ type PopoverMenuProps = Partial & {
/** Callback method fired when the user requests to close the modal */
onClose: () => void;
+ /** Callback method fired when the modal is shown */
+ onModalShow?: () => void;
+
/** State that determines whether to display the modal or not */
isVisible: boolean;
@@ -89,6 +92,7 @@ function PopoverMenu({
anchorPosition,
anchorRef,
onClose,
+ onModalShow,
headerText,
fromSidebarMediumScreen,
anchorAlignment = {
@@ -211,6 +215,7 @@ function PopoverMenu({
}}
isVisible={isVisible}
onModalHide={onModalHide}
+ onModalShow={onModalShow}
animationIn={animationIn}
animationOut={animationOut}
animationInTiming={animationInTiming}
diff --git a/src/components/ProcessMoneyRequestHoldMenu.tsx b/src/components/ProcessMoneyRequestHoldMenu.tsx
index 5860791818c4..bb76ea0290f3 100644
--- a/src/components/ProcessMoneyRequestHoldMenu.tsx
+++ b/src/components/ProcessMoneyRequestHoldMenu.tsx
@@ -1,4 +1,5 @@
-import React, {useRef} from 'react';
+import {useNavigation} from '@react-navigation/native';
+import React, {useEffect, useRef} from 'react';
import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -31,6 +32,14 @@ function ProcessMoneyRequestHoldMenu({isVisible, onClose, onConfirm, anchorPosit
const {translate} = useLocalize();
const styles = useThemeStyles();
const popoverRef = useRef(null);
+ const navigation = useNavigation();
+
+ useEffect(() => {
+ const unsub = navigation.addListener('beforeRemove', () => {
+ onClose();
+ });
+ return unsub;
+ }, [navigation, onClose]);
return (
diff --git a/src/components/ReportActionItem/ExportWithDropdownMenu.tsx b/src/components/ReportActionItem/ExportWithDropdownMenu.tsx
index a13c0a266689..87ddb3b42bf0 100644
--- a/src/components/ReportActionItem/ExportWithDropdownMenu.tsx
+++ b/src/components/ReportActionItem/ExportWithDropdownMenu.tsx
@@ -74,7 +74,7 @@ function ExportWithDropdownMenu({policy, report, connectionName}: ExportWithDrop
if (modalStatus === CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION) {
ReportActions.exportToIntegration(reportID, connectionName);
} else if (modalStatus === CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED) {
- ReportActions.markAsManuallyExported(reportID);
+ ReportActions.markAsManuallyExported(reportID, connectionName);
}
}, [connectionName, modalStatus, reportID]);
@@ -106,7 +106,7 @@ function ExportWithDropdownMenu({policy, report, connectionName}: ExportWithDrop
if (value === CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION) {
ReportActions.exportToIntegration(reportID, connectionName);
} else if (value === CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED) {
- ReportActions.markAsManuallyExported(reportID);
+ ReportActions.markAsManuallyExported(reportID, connectionName);
}
}}
onOptionSelected={({value}) => savePreferredExportMethod(value)}
diff --git a/src/components/ReportActionItem/IssueCardMessage.tsx b/src/components/ReportActionItem/IssueCardMessage.tsx
new file mode 100644
index 000000000000..292b010cd851
--- /dev/null
+++ b/src/components/ReportActionItem/IssueCardMessage.tsx
@@ -0,0 +1,66 @@
+import React from 'react';
+import type {OnyxEntry} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
+import Button from '@components/Button';
+import RenderHTML from '@components/RenderHTML';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
+import useEnvironment from '@hooks/useEnvironment';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type {ReportAction} from '@src/types/onyx';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+
+type IssueCardMessageProps = {
+ action: OnyxEntry;
+};
+
+function IssueCardMessage({action}: IssueCardMessageProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ const {environmentURL} = useEnvironment();
+ // TODO: now mocking accountID with current user accountID instead of action.message.assigneeAccountID
+ const personalData = useCurrentUserPersonalDetails();
+ const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS);
+
+ // TODO: now mocking accountID with current user accountID instead of action.message.assigneeAccountID
+ const assignee = ` `;
+ const link = `${translate('cardPage.expensifyCard')} `;
+
+ const noMailingAddress = action?.actionName === CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS && isEmptyObject(privatePersonalDetails?.address);
+
+ const getTranslation = () => {
+ switch (action?.actionName) {
+ case CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED:
+ return translate('workspace.expensifyCard.issuedCard', assignee);
+ case CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED_VIRTUAL:
+ return translate('workspace.expensifyCard.issuedCardVirtual', {assignee, link});
+ case CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS:
+ return translate(`workspace.expensifyCard.${noMailingAddress ? 'issuedCardNoMailingAddress' : 'addedAddress'}`, assignee);
+ default:
+ return '';
+ }
+ };
+
+ return (
+ <>
+ ${getTranslation()}`} />
+ {noMailingAddress && (
+ Navigation.navigate(ROUTES.SETTINGS_ADDRESS)}
+ success
+ medium
+ style={[styles.alignSelfStart, styles.mt3]}
+ text={translate('workspace.expensifyCard.addMailingAddress')}
+ />
+ )}
+ >
+ );
+}
+
+IssueCardMessage.displayName = 'IssueCardMessage';
+
+export default IssueCardMessage;
diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index 7a527610422b..bff62f247eee 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -84,6 +84,12 @@ type ReportPreviewProps = ReportPreviewOnyxProps & {
/** Callback for updating context menu active state, used for showing context menu */
checkIfContextMenuActive?: () => void;
+ /** Callback when the payment options popover is shown */
+ onPaymentOptionsShow?: () => void;
+
+ /** Callback when the payment options popover is closed */
+ onPaymentOptionsHide?: () => void;
+
/** Whether a message is a whisper */
isWhisper?: boolean;
@@ -106,6 +112,8 @@ function ReportPreview({
isHovered = false,
isWhisper = false,
checkIfContextMenuActive = () => {},
+ onPaymentOptionsShow,
+ onPaymentOptionsHide,
userWallet,
}: ReportPreviewProps) {
const theme = useTheme();
@@ -152,7 +160,7 @@ function ReportPreview({
const hasReceipts = transactionsWithReceipts.length > 0;
const isScanning = hasReceipts && areAllRequestsBeingSmartScanned;
const hasErrors =
- hasMissingSmartscanFields ||
+ (hasMissingSmartscanFields && !iouSettled) ||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
(canUseViolations && (ReportUtils.hasViolations(iouReportID, transactionViolations) || ReportUtils.hasWarningTypeViolations(iouReportID, transactionViolations))) ||
(ReportUtils.isReportOwner(iouReport) && ReportUtils.hasReportViolations(iouReportID)) ||
@@ -285,7 +293,7 @@ function ReportPreview({
const shouldShowSettlementButton = (shouldShowPayButton || shouldShowApproveButton) && !showRTERViolationMessage;
const shouldPromptUserToAddBankAccount = ReportUtils.hasMissingPaymentMethod(userWallet, iouReportID);
- const shouldShowRBR = !iouSettled && hasErrors;
+ const shouldShowRBR = hasErrors;
/*
Show subtitle if at least one of the expenses is not being smart scanned, and either:
@@ -425,7 +433,7 @@ function ReportPreview({
)}
- {shouldShowSettlementButton && !shouldShowExportIntegrationButton && (
+ {shouldShowSettlementButton && (
)}
- {shouldShowExportIntegrationButton && (
+ {shouldShowExportIntegrationButton && !shouldShowSettlementButton && (
| ValueOf;
+type AdvancedFiltersKeys = ValueOf | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE | typeof CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS;
type QueryFilters = {
- [K in AllFieldKeys]: QueryFilter | QueryFilter[];
+ [K in AdvancedFiltersKeys]?: QueryFilter | QueryFilter[];
};
type SearchQueryString = string;
@@ -61,7 +61,7 @@ type SearchQueryAST = {
};
type SearchQueryJSON = {
- input: string;
+ inputQuery: SearchQueryString;
hash: number;
} & SearchQueryAST;
@@ -78,5 +78,5 @@ export type {
ASTNode,
QueryFilter,
QueryFilters,
- AllFieldKeys,
+ AdvancedFiltersKeys,
};
diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx
index 7c3c021a08eb..d1803f403469 100644
--- a/src/components/SettlementButton.tsx
+++ b/src/components/SettlementButton.tsx
@@ -43,6 +43,12 @@ type SettlementButtonProps = SettlementButtonOnyxProps & {
/** Callback to execute when this button is pressed. Receives a single payment type argument. */
onPress: (paymentType?: PaymentMethodType) => void;
+ /** Callback when the payment options popover is shown */
+ onPaymentOptionsShow?: () => void;
+
+ /** Callback when the payment options popover is closed */
+ onPaymentOptionsHide?: () => void;
+
/** The route to redirect if user does not have a payment method setup */
enablePaymentsRoute: EnablePaymentsRoute;
@@ -140,6 +146,8 @@ function SettlementButton({
enterKeyEventListenerPriority = 0,
confirmApproval,
policy,
+ onPaymentOptionsShow,
+ onPaymentOptionsHide,
}: SettlementButtonProps) {
const {translate} = useLocalize();
const {isOffline} = useNetwork();
@@ -273,6 +281,8 @@ function SettlementButton({
{(triggerKYCFlow, buttonRef) => (
success
+ onOptionsMenuShow={onPaymentOptionsShow}
+ onOptionsMenuHide={onPaymentOptionsHide}
buttonRef={buttonRef}
shouldAlwaysShowDropdownMenu={isInvoiceReport}
customText={isInvoiceReport ? translate('iou.settlePayment', {formattedAmount}) : undefined}
diff --git a/src/components/ValidateCode/ExpiredValidateCodeModal.tsx b/src/components/ValidateCode/ExpiredValidateCodeModal.tsx
index 3dd6a639a956..b390770d78d4 100644
--- a/src/components/ValidateCode/ExpiredValidateCodeModal.tsx
+++ b/src/components/ValidateCode/ExpiredValidateCodeModal.tsx
@@ -31,18 +31,22 @@ function ExpiredValidateCodeModal() {
{translate('validateCodeModal.expiredCodeTitle')}
-
- {translate('validateCodeModal.expiredCodeDescription')}
- {translate('validateCodeModal.or')}{' '}
- {
- Session.beginSignIn(credentials?.login ?? '');
- Navigation.setNavigationActionToMicrotaskQueue(Navigation.goBack);
- }}
- >
- {translate('validateCodeModal.requestOneHere')}
-
-
+ {credentials?.login ? (
+
+ {translate('validateCodeModal.expiredCodeDescription')}
+ {translate('validateCodeModal.or')}{' '}
+ {
+ Session.beginSignIn(credentials?.login ?? '');
+ Navigation.setNavigationActionToMicrotaskQueue(Navigation.goBack);
+ }}
+ >
+ {translate('validateCodeModal.requestOneHere')}
+
+
+ ) : (
+ {translate('validateCodeModal.expiredCodeDescription')}.
+ )}
diff --git a/src/hooks/useAutoFocusInput.ts b/src/hooks/useAutoFocusInput.ts
index 2e2d9a086ab1..26f045ebd579 100644
--- a/src/hooks/useAutoFocusInput.ts
+++ b/src/hooks/useAutoFocusInput.ts
@@ -1,5 +1,6 @@
import {useFocusEffect} from '@react-navigation/native';
import {useCallback, useContext, useEffect, useRef, useState} from 'react';
+import type {RefObject} from 'react';
import type {TextInput} from 'react-native';
import {InteractionManager} from 'react-native';
import CONST from '@src/CONST';
@@ -7,6 +8,7 @@ import * as Expensify from '@src/Expensify';
type UseAutoFocusInput = {
inputCallbackRef: (ref: TextInput | null) => void;
+ inputRef: RefObject;
};
export default function useAutoFocusInput(): UseAutoFocusInput {
@@ -55,5 +57,5 @@ export default function useAutoFocusInput(): UseAutoFocusInput {
setIsInputInitialized(true);
};
- return {inputCallbackRef};
+ return {inputCallbackRef, inputRef};
}
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 3e717a1c15b3..0118aed32f4c 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -36,6 +36,7 @@ import type {
GoBackMessageParams,
GoToRoomParams,
InstantSummaryParams,
+ IssueVirtualCardParams,
LocalTimeParams,
LoggedInAsParams,
LogSizeParams,
@@ -835,6 +836,8 @@ export default {
headsUp: 'Heads up!',
unapproveWithIntegrationWarning: (accountingIntegration: string) =>
`This report has already been exported to ${accountingIntegration}. Changes to this report in Expensify may lead to data discrepancies and Expensify Card reconciliation issues. Are you sure you want to unapprove this report?`,
+ reimbursable: 'reimbursable',
+ nonReimbursable: 'non-reimbursable',
},
notificationPreferencesPage: {
header: 'Notification preferences',
@@ -2708,6 +2711,11 @@ export default {
`If you change this card's limit type to Smart Limit, new transactions will be declined because the ${limit} unapproved limit has already been reached.`,
changeCardMonthlyLimitTypeWarning: (limit: string) =>
`If you change this card's limit type to Monthly, new transactions will be declined because the ${limit} monthly limit has already been reached.`,
+ addMailingAddress: 'Add mailing address',
+ issuedCard: (assignee: string) => `issued ${assignee} an Expensify Card! The card will arrive in 2-3 business days.`,
+ issuedCardNoMailingAddress: (assignee: string) => `issued ${assignee} an Expensify Card! The card will be shipped once a mailing address is added.`,
+ issuedCardVirtual: ({assignee, link}: IssueVirtualCardParams) => `issued ${assignee} a virtual ${link}! The card can be used right away.`,
+ addedAddress: (assignee: string) => `${assignee} added the address. Expensify Card will arrive in 2-3 business days.`,
},
categories: {
deleteCategories: 'Delete categories',
@@ -3212,6 +3220,7 @@ export default {
exportAs: 'Export as',
exportOutOfPocket: 'Export out-of-pocket expenses as',
exportCompanyCard: 'Export company card expenses as',
+ exportDate: 'Export date',
defaultVendor: 'Default vendor',
autoSync: 'Auto-sync',
reimbursedReports: 'Sync reimbursed reports',
@@ -3570,6 +3579,7 @@ export default {
},
search: {
resultsAreLimited: 'Search results are limited.',
+ viewResults: 'View results',
searchResults: {
emptyResults: {
title: 'Nothing to show',
@@ -3587,9 +3597,10 @@ export default {
filtersHeader: 'Filters',
filters: {
date: {
- before: 'Before',
- after: 'After',
+ before: (date?: string) => `Before ${date ?? ''}`,
+ after: (date?: string) => `After ${date ?? ''}`,
},
+ status: 'Status',
},
},
genericErrorPage: {
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 0136bdbb141b..e117534e5ac7 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -35,6 +35,7 @@ import type {
GoBackMessageParams,
GoToRoomParams,
InstantSummaryParams,
+ IssueVirtualCardParams,
LocalTimeParams,
LoggedInAsParams,
LogSizeParams,
@@ -757,7 +758,7 @@ export default {
`estableció la distancia a ${newDistanceToDisplay}, lo que estableció el importe a ${newAmountToDisplay}`,
removedTheRequest: ({valueName, oldValueToDisplay}: RemovedTheRequestParams) => `${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} (previamente ${oldValueToDisplay})`,
updatedTheRequest: ({valueName, newValueToDisplay, oldValueToDisplay}: UpdatedTheRequestParams) =>
- `${valueName === 'comerciante' || valueName === 'importe' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay} (previamente ${oldValueToDisplay})`,
+ `${valueName === 'comerciante' || valueName === 'importe' || valueName === 'gasto' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay} (previamente ${oldValueToDisplay})`,
updatedTheDistance: ({newDistanceToDisplay, oldDistanceToDisplay, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceParams) =>
`cambió la distancia a ${newDistanceToDisplay} (previamente ${oldDistanceToDisplay}), lo que cambió el importe a ${newAmountToDisplay} (previamente ${oldAmountToDisplay})`,
threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${comment ? `${formattedAmount} para ${comment}` : `Gasto de ${formattedAmount}`}`,
@@ -841,6 +842,8 @@ export default {
headsUp: 'Atención!',
unapproveWithIntegrationWarning: (accountingIntegration: string) =>
`Este informe ya se ha exportado a ${accountingIntegration}. Los cambios realizados en este informe en Expensify pueden provocar discrepancias en los datos y problemas de conciliación de la tarjeta Expensify. ¿Está seguro de que desea anular la aprobación de este informe?`,
+ reimbursable: 'reembolsable',
+ nonReimbursable: 'no reembolsable',
},
notificationPreferencesPage: {
header: 'Preferencias de avisos',
@@ -2760,6 +2763,11 @@ export default {
`Si cambias el tipo de límite de esta tarjeta a Límite inteligente, las nuevas transacciones serán rechazadas porque ya se ha alcanzado el límite de ${limit} no aprobado.`,
changeCardMonthlyLimitTypeWarning: (limit: string) =>
`Si cambias el tipo de límite de esta tarjeta a Mensual, las nuevas transacciones serán rechazadas porque ya se ha alcanzado el límite de ${limit} mensual.`,
+ addMailingAddress: 'Añadir dirección de postal',
+ issuedCard: (assignee: string) => `¡emitió a ${assignee} una Tarjeta Expensify! La tarjeta llegará en 2-3 días laborables.`,
+ issuedCardNoMailingAddress: (assignee: string) => `¡emitió a ${assignee} una Tarjeta Expensify! La tarjeta se enviará una vez que se añada una dirección postal.`,
+ issuedCardVirtual: ({assignee, link}: IssueVirtualCardParams) => `¡emitió a ${assignee} una ${link} virtual! La tarjeta puede utilizarse inmediatamente.`,
+ addedAddress: (assignee: string) => `${assignee} ha añadido la dirección. Tarjeta Expensify llegará en 2-3 días hábiles.`,
},
categories: {
deleteCategories: 'Eliminar categorías',
@@ -3197,6 +3205,7 @@ export default {
exportAs: 'Exportar cómo',
exportOutOfPocket: ' Exportar gastos por cuenta propia como',
exportCompanyCard: 'Exportar gastos de la tarjeta de empresa como',
+ exportDate: 'Fecha de exportación',
defaultVendor: 'Proveedor predeterminado',
autoSync: 'Autosincronización',
reimbursedReports: 'Sincronizar informes reembolsados',
@@ -3627,6 +3636,7 @@ export default {
},
search: {
resultsAreLimited: 'Los resultados de búsqueda están limitados.',
+ viewResults: 'Ver resultados',
searchResults: {
emptyResults: {
title: 'No hay nada que ver aquí',
@@ -3644,9 +3654,10 @@ export default {
filtersHeader: 'Filtros',
filters: {
date: {
- before: 'Antes de',
- after: 'Después de',
+ before: (date?: string) => `Antes de ${date ?? ''}`,
+ after: (date?: string) => `Después de ${date ?? ''}`,
},
+ status: 'Estado',
},
},
genericErrorPage: {
diff --git a/src/languages/types.ts b/src/languages/types.ts
index 24117f257d8f..c246864b3c03 100644
--- a/src/languages/types.ts
+++ b/src/languages/types.ts
@@ -311,7 +311,7 @@ type ChangeTypeParams = {oldType: string; newType: string};
type DelegateSubmitParams = {delegateUser: string; originalManager: string};
-type ExportedToIntegrationParams = {label: string};
+type ExportedToIntegrationParams = {label: string; markedManually?: boolean; inProgress?: boolean; lastModified?: string};
type ForwardedParams = {amount: string; currency: string};
@@ -348,6 +348,11 @@ type DeleteExpenseTranslationParams = {
count: number;
};
+type IssueVirtualCardParams = {
+ assignee: string;
+ link: string;
+};
+
export type {
AddressLineParams,
AdminCanceledRequestParams,
@@ -376,6 +381,7 @@ export type {
GoToRoomParams,
HeldRequestParams,
InstantSummaryParams,
+ IssueVirtualCardParams,
LocalTimeParams,
LogSizeParams,
LoggedInAsParams,
diff --git a/src/libs/API/parameters/MarkAsExportedParams.ts b/src/libs/API/parameters/MarkAsExportedParams.ts
index 03348e856b15..ed457afb42e3 100644
--- a/src/libs/API/parameters/MarkAsExportedParams.ts
+++ b/src/libs/API/parameters/MarkAsExportedParams.ts
@@ -1,6 +1,14 @@
type MarkAsExportedParams = {
- reportIDList: string;
- markedManually: true;
+ markedManually: boolean;
+ /**
+ * Stringified JSON object with type of following structure:
+ * Array<{
+ * reportID: number;
+ * label: string;
+ * optimisticReportActionID: string;
+ * }>
+ */
+ data: string;
};
export default MarkAsExportedParams;
diff --git a/src/libs/API/parameters/ReportExportParams.ts b/src/libs/API/parameters/ReportExportParams.ts
index dc87ce2170c4..c6a4b7b58ee8 100644
--- a/src/libs/API/parameters/ReportExportParams.ts
+++ b/src/libs/API/parameters/ReportExportParams.ts
@@ -5,6 +5,13 @@ type ReportExportParams = {
reportIDList: string;
connectionName: ValueOf;
type: 'MANUAL';
+ /**
+ * Stringified JSON object with type of following structure:
+ * {
+ * [reportID]: optimisticReportActionID;
+ * }>
+ */
+ optimisticReportActions: string;
};
export default ReportExportParams;
diff --git a/src/libs/API/parameters/StartIssueNewCardFlowParams.ts b/src/libs/API/parameters/StartIssueNewCardFlowParams.ts
new file mode 100644
index 000000000000..8ed04b756a10
--- /dev/null
+++ b/src/libs/API/parameters/StartIssueNewCardFlowParams.ts
@@ -0,0 +1,5 @@
+type StartIssueNewCardFlowParams = {
+ policyID: string;
+};
+
+export default StartIssueNewCardFlowParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index e16691c992f2..6b95190cd1ed 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -268,3 +268,4 @@ export type {default as CopyExistingPolicyConnectionParams} from './CopyExisting
export type {default as ExportSearchItemsToCSVParams} from './ExportSearchItemsToCSVParams';
export type {default as UpdateExpensifyCardLimitParams} from './UpdateExpensifyCardLimitParams';
export type {CreateWorkspaceApprovalParams, UpdateWorkspaceApprovalParams, RemoveWorkspaceApprovalParams} from './WorkspaceApprovalParams';
+export type {default as StartIssueNewCardFlowParams} from './StartIssueNewCardFlowParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index c4218b44f165..f15fba20bc98 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -703,6 +703,7 @@ const READ_COMMANDS = {
SEARCH: 'Search',
OPEN_SUBSCRIPTION_PAGE: 'OpenSubscriptionPage',
OPEN_DRAFT_DISTANCE_EXPENSE: 'OpenDraftDistanceExpense',
+ START_ISSUE_NEW_CARD_FLOW: 'StartIssueNewCardFlow',
} as const;
type ReadCommand = ValueOf;
@@ -758,6 +759,7 @@ type ReadCommandParameters = {
[READ_COMMANDS.SEARCH]: Parameters.SearchParams;
[READ_COMMANDS.OPEN_SUBSCRIPTION_PAGE]: null;
[READ_COMMANDS.OPEN_DRAFT_DISTANCE_EXPENSE]: null;
+ [READ_COMMANDS.START_ISSUE_NEW_CARD_FLOW]: Parameters.StartIssueNewCardFlowParams;
};
const SIDE_EFFECT_REQUEST_COMMANDS = {
diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts
index 38562edb7704..d2a776941a9a 100644
--- a/src/libs/ModifiedExpenseMessage.ts
+++ b/src/libs/ModifiedExpenseMessage.ts
@@ -24,6 +24,13 @@ Onyx.connect({
},
});
+/**
+ * Utility to get message based on boolean literal value.
+ */
+function getBooleanLiteralMessage(value: string | undefined, truthyMessage: string, falsyMessage: string): string {
+ return value === 'true' ? truthyMessage : falsyMessage;
+}
+
/**
* Builds the partial message fragment for a modified field on the expense.
*/
@@ -261,6 +268,19 @@ function getForReportAction(reportID: string | undefined, reportAction: OnyxEntr
);
}
+ const hasModifiedReimbursable = reportActionOriginalMessage && 'oldReimbursable' in reportActionOriginalMessage && 'reimbursable' in reportActionOriginalMessage;
+ if (hasModifiedReimbursable) {
+ buildMessageFragmentForValue(
+ getBooleanLiteralMessage(reportActionOriginalMessage?.reimbursable, Localize.translateLocal('iou.reimbursable'), Localize.translateLocal('iou.nonReimbursable')),
+ getBooleanLiteralMessage(reportActionOriginalMessage?.oldReimbursable, Localize.translateLocal('iou.reimbursable'), Localize.translateLocal('iou.nonReimbursable')),
+ Localize.translateLocal('iou.expense'),
+ true,
+ setFragments,
+ removalFragments,
+ changeFragments,
+ );
+ }
+
const message =
getMessageLine(`\n${Localize.translateLocal('iou.changed')}`, changeFragments) +
getMessageLine(`\n${Localize.translateLocal('iou.set')}`, setFragments) +
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index d3bee5021988..d9cd2f5ded85 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -75,6 +75,8 @@ const loadReportAttachments = () => require('../../../page
const loadValidateLoginPage = () => require('../../../pages/ValidateLoginPage').default;
const loadLogOutPreviousUserPage = () => require('../../../pages/LogOutPreviousUserPage').default;
const loadConciergePage = () => require('../../../pages/ConciergePage').default;
+const loadTrackExpensePage = () => require('../../../pages/TrackExpensePage').default;
+const loadSubmitExpensePage = () => require('../../../pages/SubmitExpensePage').default;
const loadProfileAvatar = () => require('../../../pages/settings/Profile/ProfileAvatar').default;
const loadWorkspaceAvatar = () => require('../../../pages/workspace/WorkspaceAvatar').default;
const loadReportAvatar = () => require('../../../pages/ReportAvatar').default;
@@ -377,6 +379,16 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
options={defaultScreenOptions}
getComponent={loadConciergePage}
/>
+
+
require('../../../../pages/workspace/taxes/ValuePage').default,
[SCREENS.WORKSPACE.TAX_CREATE]: () => require('../../../../pages/workspace/taxes/WorkspaceCreateTaxPage').default,
[SCREENS.WORKSPACE.TAX_CODE]: () => require('../../../../pages/workspace/taxes/WorkspaceTaxCodePage').default,
- [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: () => require('../../../../pages/workspace/card/issueNew/IssueNewCardPage').default,
+ [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: () => require('../../../../pages/workspace/expensifyCard/issueNew/IssueNewCardPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceCardSettingsPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_ACCOUNT]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceSettlementAccountPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_FREQUENCY]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceSettlementFrequencyPage').default,
@@ -513,6 +513,7 @@ const SearchAdvancedFiltersModalStackNavigator = createModalStackNavigator require('../../../../pages/Search/SearchAdvancedFiltersPage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_DATE_RHP]: () => require('../../../../pages/Search/SearchFiltersDatePage').default,
[SCREENS.SEARCH.ADVANCED_FILTERS_TYPE_RHP]: () => require('../../../../pages/Search/SearchFiltersTypePage').default,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_STATUS_RHP]: () => require('../../../../pages/Search/SearchFiltersStatusPage').default,
});
const RestrictedActionModalStackNavigator = createModalStackNavigator({
diff --git a/src/libs/Navigation/AppNavigator/index.native.tsx b/src/libs/Navigation/AppNavigator/index.native.tsx
index e848979e6993..0da93237d617 100644
--- a/src/libs/Navigation/AppNavigator/index.native.tsx
+++ b/src/libs/Navigation/AppNavigator/index.native.tsx
@@ -2,6 +2,7 @@ import React, {memo, useContext, useEffect} from 'react';
import {NativeModules} from 'react-native';
import {InitialURLContext} from '@components/InitialURLContextProvider';
import Navigation from '@libs/Navigation/Navigation';
+import ROUTES from '@src/ROUTES';
import type ReactComponentModule from '@src/types/utils/ReactComponentModule';
type AppNavigatorProps = {
@@ -13,7 +14,7 @@ function AppNavigator({authenticated}: AppNavigatorProps) {
const initUrl = useContext(InitialURLContext);
useEffect(() => {
- if (!NativeModules.HybridAppModule || !initUrl) {
+ if (!NativeModules.HybridAppModule || !initUrl || !initUrl.includes(ROUTES.TRANSITION_BETWEEN_APPS)) {
return;
}
diff --git a/src/libs/Navigation/AppNavigator/index.tsx b/src/libs/Navigation/AppNavigator/index.tsx
index 787ede6c14f2..e0f1dae94f62 100644
--- a/src/libs/Navigation/AppNavigator/index.tsx
+++ b/src/libs/Navigation/AppNavigator/index.tsx
@@ -2,6 +2,7 @@ import React, {lazy, memo, Suspense, useContext, useEffect} from 'react';
import {NativeModules} from 'react-native';
import {InitialURLContext} from '@components/InitialURLContextProvider';
import Navigation from '@libs/Navigation/Navigation';
+import ROUTES from '@src/ROUTES';
import lazyRetry from '@src/utils/lazyRetry';
const AuthScreens = lazy(() => lazyRetry(() => import('./AuthScreens')));
@@ -16,7 +17,7 @@ function AppNavigator({authenticated}: AppNavigatorProps) {
const initUrl = useContext(InitialURLContext);
useEffect(() => {
- if (!NativeModules.HybridAppModule || !initUrl) {
+ if (!NativeModules.HybridAppModule || !initUrl || !initUrl.includes(ROUTES.TRANSITION_BETWEEN_APPS)) {
return;
}
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 586b4e9d4506..d0aeb644f27d 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -17,6 +17,8 @@ const config: LinkingOptions['config'] = {
[SCREENS.TRANSITION_BETWEEN_APPS]: ROUTES.TRANSITION_BETWEEN_APPS,
[SCREENS.CONNECTION_COMPLETE]: ROUTES.CONNECTION_COMPLETE,
[SCREENS.CONCIERGE]: ROUTES.CONCIERGE,
+ [SCREENS.TRACK_EXPENSE]: ROUTES.TRACK_EXPENSE,
+ [SCREENS.SUBMIT_EXPENSE]: ROUTES.SUBMIT_EXPENSE,
[SCREENS.SIGN_IN_WITH_APPLE_DESKTOP]: ROUTES.APPLE_SIGN_IN,
[SCREENS.SIGN_IN_WITH_GOOGLE_DESKTOP]: ROUTES.GOOGLE_SIGN_IN,
[SCREENS.SAML_SIGN_IN]: ROUTES.SAML_SIGN_IN,
@@ -1009,6 +1011,7 @@ const config: LinkingOptions['config'] = {
[SCREENS.SEARCH.ADVANCED_FILTERS_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS,
[SCREENS.SEARCH.ADVANCED_FILTERS_DATE_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_DATE,
[SCREENS.SEARCH.ADVANCED_FILTERS_TYPE_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_TYPE,
+ [SCREENS.SEARCH.ADVANCED_FILTERS_STATUS_RHP]: ROUTES.SEARCH_ADVANCED_FILTERS_STATUS,
},
},
[SCREENS.RIGHT_MODAL.RESTRICTED_ACTION]: {
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 1d3299b64112..9a7aafb1963c 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1240,6 +1240,8 @@ type PublicScreensParamList = SharedScreensParamList & {
type AuthScreensParamList = CentralPaneScreensParamList &
SharedScreensParamList & {
[SCREENS.CONCIERGE]: undefined;
+ [SCREENS.TRACK_EXPENSE]: undefined;
+ [SCREENS.SUBMIT_EXPENSE]: undefined;
[SCREENS.ATTACHMENTS]: {
reportID: string;
source: string;
diff --git a/src/libs/Notification/PushNotification/subscribePushNotification/index.ts b/src/libs/Notification/PushNotification/subscribePushNotification/index.ts
index d60e66f8d535..c1a1442b1e53 100644
--- a/src/libs/Notification/PushNotification/subscribePushNotification/index.ts
+++ b/src/libs/Notification/PushNotification/subscribePushNotification/index.ts
@@ -1,3 +1,4 @@
+import {NativeModules} from 'react-native';
import Onyx from 'react-native-onyx';
import applyOnyxUpdatesReliably from '@libs/actions/applyOnyxUpdatesReliably';
import * as ActiveClientManager from '@libs/ActiveClientManager';
@@ -85,6 +86,10 @@ function navigateToReport({reportID, reportActionID}: ReportActionPushNotificati
// The attachment modal remains open when navigating to the report so we need to close it
Modal.close(() => {
try {
+ // Get rid of the transition screen, if it is on the top of the stack
+ if (NativeModules.HybridAppModule && Navigation.getActiveRoute().includes(ROUTES.TRANSITION_BETWEEN_APPS)) {
+ Navigation.goBack();
+ }
// If a chat is visible other than the one we are trying to navigate to, then we need to navigate back
if (Navigation.getActiveRoute().slice(1, 2) === ROUTES.REPORT && !Navigation.isActiveRoute(`r/${reportID}`)) {
Navigation.goBack();
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index 033f92b914f1..2fe9ea55a0ee 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -1713,14 +1713,12 @@ function canCreateOptimisticPersonalDetailOption({
* - There's no matching recent report and personal detail option
* - The searchValue is a valid email or phone number
* - The searchValue isn't the current personal detail login
- * - We can use chronos or the search value is not the chronos email
*/
function getUserToInviteOption({
searchValue,
excludeUnknownUsers = false,
optionsToExclude = [],
selectedOptions = [],
- betas,
reportActions = {},
showChatPreviewLine = false,
}: GetUserToInviteConfig): ReportUtils.OptionData | null {
@@ -1731,17 +1729,8 @@ function getUserToInviteOption({
const isValidPhoneNumber = parsedPhoneNumber.possible && Str.isValidE164Phone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? ''));
const isInOptionToExclude =
optionsToExclude.findIndex((optionToExclude) => 'login' in optionToExclude && optionToExclude.login === PhoneNumber.addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) !== -1;
- const isChronosEmail = searchValue === CONST.EMAIL.CHRONOS;
- if (
- !searchValue ||
- isCurrentUserLogin ||
- isInSelectedOption ||
- (!isValidEmail && !isValidPhoneNumber) ||
- isInOptionToExclude ||
- (isChronosEmail && !Permissions.canUseChronos(betas)) ||
- excludeUnknownUsers
- ) {
+ if (!searchValue || isCurrentUserLogin || isInSelectedOption || (!isValidEmail && !isValidPhoneNumber) || isInOptionToExclude || excludeUnknownUsers) {
return null;
}
diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts
index ee1ce27103de..3bfddca16401 100644
--- a/src/libs/Permissions.ts
+++ b/src/libs/Permissions.ts
@@ -7,10 +7,6 @@ function canUseAllBetas(betas: OnyxEntry): boolean {
return !!betas?.includes(CONST.BETAS.ALL);
}
-function canUseChronos(betas: OnyxEntry): boolean {
- return !!betas?.includes(CONST.BETAS.CHRONOS_IN_CASH) || canUseAllBetas(betas);
-}
-
function canUseDefaultRooms(betas: OnyxEntry): boolean {
return !!betas?.includes(CONST.BETAS.DEFAULT_ROOMS) || canUseAllBetas(betas);
}
@@ -60,7 +56,6 @@ function canUseLinkPreviews(): boolean {
}
export default {
- canUseChronos,
canUseDefaultRooms,
canUseLinkPreviews,
canUseViolations,
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index 27c9aa69f699..2f27f0185759 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -15,10 +15,13 @@ import type {
ConnectionName,
Connections,
CustomUnit,
+ InvoiceItem,
NetSuiteAccount,
NetSuiteConnection,
NetSuiteCustomList,
NetSuiteCustomSegment,
+ NetSuiteTaxAccount,
+ NetSuiteVendor,
PolicyFeatureName,
Rate,
Tenant,
@@ -549,61 +552,90 @@ function xeroSettingsPendingAction(settings?: XeroSettings, pendingFields?: Pend
return pendingFields[key ?? '-1'];
}
+function findSelectedVendorWithDefaultSelect(vendors: NetSuiteVendor[] | undefined, selectedVendorId: string | undefined) {
+ const selectedVendor = (vendors ?? []).find(({id}) => id === selectedVendorId);
+ return selectedVendor ?? vendors?.[0] ?? undefined;
+}
+
+function findSelectedBankAccountWithDefaultSelect(accounts: NetSuiteAccount[] | undefined, selectedBankAccountId: string | undefined) {
+ const selectedBankAccount = (accounts ?? []).find(({id}) => id === selectedBankAccountId);
+ return selectedBankAccount ?? accounts?.[0] ?? undefined;
+}
+
+function findSelectedInvoiceItemWithDefaultSelect(invoiceItems: InvoiceItem[] | undefined, selectedItemId: string | undefined) {
+ const selectedInvoiceItem = (invoiceItems ?? []).find(({id}) => id === selectedItemId);
+ return selectedInvoiceItem ?? invoiceItems?.[0] ?? undefined;
+}
+
+function findSelectedTaxAccountWithDefaultSelect(taxAccounts: NetSuiteTaxAccount[] | undefined, selectedAccountId: string | undefined) {
+ const selectedTaxAccount = (taxAccounts ?? []).find(({externalID}) => externalID === selectedAccountId);
+ return selectedTaxAccount ?? taxAccounts?.[0] ?? undefined;
+}
+
function getNetSuiteVendorOptions(policy: Policy | undefined, selectedVendorId: string | undefined): SelectorType[] {
- const vendors = policy?.connections?.netsuite.options.data.vendors ?? [];
+ const vendors = policy?.connections?.netsuite.options.data.vendors;
+
+ const selectedVendor = findSelectedVendorWithDefaultSelect(vendors, selectedVendorId);
return (vendors ?? []).map(({id, name}) => ({
value: id,
text: name,
keyForList: id,
- isSelected: selectedVendorId === id,
+ isSelected: selectedVendor?.id === id,
}));
}
function getNetSuitePayableAccountOptions(policy: Policy | undefined, selectedBankAccountId: string | undefined): SelectorType[] {
- const payableAccounts = policy?.connections?.netsuite.options.data.payableList ?? [];
+ const payableAccounts = policy?.connections?.netsuite.options.data.payableList;
+
+ const selectedPayableAccount = findSelectedBankAccountWithDefaultSelect(payableAccounts, selectedBankAccountId);
return (payableAccounts ?? []).map(({id, name}) => ({
value: id,
text: name,
keyForList: id,
- isSelected: selectedBankAccountId === id,
+ isSelected: selectedPayableAccount?.id === id,
}));
}
function getNetSuiteReceivableAccountOptions(policy: Policy | undefined, selectedBankAccountId: string | undefined): SelectorType[] {
- const receivableAccounts = policy?.connections?.netsuite.options.data.receivableList ?? [];
+ const receivableAccounts = policy?.connections?.netsuite.options.data.receivableList;
+
+ const selectedReceivableAccount = findSelectedBankAccountWithDefaultSelect(receivableAccounts, selectedBankAccountId);
return (receivableAccounts ?? []).map(({id, name}) => ({
value: id,
text: name,
keyForList: id,
- isSelected: selectedBankAccountId === id,
+ isSelected: selectedReceivableAccount?.id === id,
}));
}
function getNetSuiteInvoiceItemOptions(policy: Policy | undefined, selectedItemId: string | undefined): SelectorType[] {
- const invoiceItems = policy?.connections?.netsuite.options.data.items ?? [];
+ const invoiceItems = policy?.connections?.netsuite.options.data.items;
+
+ const selectedInvoiceItem = findSelectedInvoiceItemWithDefaultSelect(invoiceItems, selectedItemId);
return (invoiceItems ?? []).map(({id, name}) => ({
value: id,
text: name,
keyForList: id,
- isSelected: selectedItemId === id,
+ isSelected: selectedInvoiceItem?.id === id,
}));
}
function getNetSuiteTaxAccountOptions(policy: Policy | undefined, subsidiaryCountry?: string, selectedAccountId?: string): SelectorType[] {
- const taxAccounts = policy?.connections?.netsuite.options.data.taxAccountsList ?? [];
+ const taxAccounts = policy?.connections?.netsuite.options.data.taxAccountsList;
+ const accountOptions = (taxAccounts ?? []).filter(({country}) => country === subsidiaryCountry);
- return (taxAccounts ?? [])
- .filter(({country}) => country === subsidiaryCountry)
- .map(({externalID, name}) => ({
- value: externalID,
- text: name,
- keyForList: externalID,
- isSelected: selectedAccountId === externalID,
- }));
+ const selectedTaxAccount = findSelectedTaxAccountWithDefaultSelect(accountOptions, selectedAccountId);
+
+ return accountOptions.map(({externalID, name}) => ({
+ value: externalID,
+ text: name,
+ keyForList: externalID,
+ isSelected: selectedTaxAccount?.externalID === externalID,
+ }));
}
function canUseTaxNetSuite(canUseNetSuiteUSATax?: boolean, subsidiaryCountry?: string) {
@@ -614,44 +646,62 @@ function canUseProvincialTaxNetSuite(subsidiaryCountry?: string) {
return subsidiaryCountry === '_canada';
}
+function getFilteredReimbursableAccountOptions(payableAccounts: NetSuiteAccount[] | undefined) {
+ return (payableAccounts ?? []).filter(({type}) => type === CONST.NETSUITE_ACCOUNT_TYPE.BANK || type === CONST.NETSUITE_ACCOUNT_TYPE.CREDIT_CARD);
+}
+
function getNetSuiteReimbursableAccountOptions(policy: Policy | undefined, selectedBankAccountId: string | undefined): SelectorType[] {
- const payableAccounts = policy?.connections?.netsuite.options.data.payableList ?? [];
- const accountOptions = (payableAccounts ?? []).filter(({type}) => type === CONST.NETSUITE_ACCOUNT_TYPE.BANK || type === CONST.NETSUITE_ACCOUNT_TYPE.CREDIT_CARD);
+ const payableAccounts = policy?.connections?.netsuite.options.data.payableList;
+ const accountOptions = getFilteredReimbursableAccountOptions(payableAccounts);
+
+ const selectedPayableAccount = findSelectedBankAccountWithDefaultSelect(accountOptions, selectedBankAccountId);
return accountOptions.map(({id, name}) => ({
value: id,
text: name,
keyForList: id,
- isSelected: selectedBankAccountId === id,
+ isSelected: selectedPayableAccount?.id === id,
}));
}
+function getFilteredCollectionAccountOptions(payableAccounts: NetSuiteAccount[] | undefined) {
+ return (payableAccounts ?? []).filter(({type}) => type === CONST.NETSUITE_ACCOUNT_TYPE.BANK);
+}
+
function getNetSuiteCollectionAccountOptions(policy: Policy | undefined, selectedBankAccountId: string | undefined): SelectorType[] {
- const payableAccounts = policy?.connections?.netsuite.options.data.payableList ?? [];
- const accountOptions = (payableAccounts ?? []).filter(({type}) => type === CONST.NETSUITE_ACCOUNT_TYPE.BANK);
+ const payableAccounts = policy?.connections?.netsuite.options.data.payableList;
+ const accountOptions = getFilteredCollectionAccountOptions(payableAccounts);
+
+ const selectedPayableAccount = findSelectedBankAccountWithDefaultSelect(accountOptions, selectedBankAccountId);
return accountOptions.map(({id, name}) => ({
value: id,
text: name,
keyForList: id,
- isSelected: selectedBankAccountId === id,
+ isSelected: selectedPayableAccount?.id === id,
}));
}
+function getFilteredApprovalAccountOptions(payableAccounts: NetSuiteAccount[] | undefined) {
+ return (payableAccounts ?? []).filter(({type}) => type === CONST.NETSUITE_ACCOUNT_TYPE.ACCOUNTS_PAYABLE);
+}
+
function getNetSuiteApprovalAccountOptions(policy: Policy | undefined, selectedBankAccountId: string | undefined): SelectorType[] {
- const payableAccounts = policy?.connections?.netsuite.options.data.payableList ?? [];
+ const payableAccounts = policy?.connections?.netsuite.options.data.payableList;
const defaultApprovalAccount: NetSuiteAccount = {
id: CONST.NETSUITE_APPROVAL_ACCOUNT_DEFAULT,
name: Localize.translateLocal('workspace.netsuite.advancedConfig.defaultApprovalAccount'),
type: CONST.NETSUITE_ACCOUNT_TYPE.ACCOUNTS_PAYABLE,
};
- const accountOptions = [defaultApprovalAccount].concat((payableAccounts ?? []).filter(({type}) => type === CONST.NETSUITE_ACCOUNT_TYPE.ACCOUNTS_PAYABLE));
+ const accountOptions = getFilteredApprovalAccountOptions([defaultApprovalAccount].concat(payableAccounts ?? []));
+
+ const selectedPayableAccount = findSelectedBankAccountWithDefaultSelect(accountOptions, selectedBankAccountId);
return accountOptions.map(({id, name}) => ({
value: id,
text: name,
keyForList: id,
- isSelected: selectedBankAccountId === id,
+ isSelected: selectedPayableAccount?.id === id,
}));
}
@@ -874,11 +924,18 @@ export {
findCurrentXeroOrganization,
getCurrentXeroOrganizationName,
getXeroBankAccountsWithDefaultSelect,
+ findSelectedVendorWithDefaultSelect,
+ findSelectedBankAccountWithDefaultSelect,
+ findSelectedInvoiceItemWithDefaultSelect,
+ findSelectedTaxAccountWithDefaultSelect,
getNetSuiteVendorOptions,
canUseTaxNetSuite,
canUseProvincialTaxNetSuite,
+ getFilteredReimbursableAccountOptions,
getNetSuiteReimbursableAccountOptions,
+ getFilteredCollectionAccountOptions,
getNetSuiteCollectionAccountOptions,
+ getFilteredApprovalAccountOptions,
getNetSuiteApprovalAccountOptions,
getNetSuitePayableAccountOptions,
getNetSuiteReceivableAccountOptions,
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index 0bd5d096db7a..8e9926648c2c 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -955,6 +955,13 @@ function isTaskAction(reportAction: OnyxEntry): boolean {
);
}
+// Get all IOU report actions for the report.
+const iouRequestTypes = new Set>([
+ CONST.IOU.REPORT_ACTION_TYPE.CREATE,
+ CONST.IOU.REPORT_ACTION_TYPE.SPLIT,
+ CONST.IOU.REPORT_ACTION_TYPE.PAY,
+ CONST.IOU.REPORT_ACTION_TYPE.TRACK,
+]);
/**
* Gets the reportID for the transaction thread associated with a report by iterating over the reportActions and identifying the IOU report actions.
* Returns a reportID if there is exactly one transaction thread for the report, and null otherwise.
@@ -966,28 +973,23 @@ function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEn
return;
}
- const reportActionsArray = Object.values(reportActions ?? {});
+ const reportActionsArray = Array.isArray(reportActions) ? reportActions : Object.values(reportActions ?? {});
if (!reportActionsArray.length) {
return;
}
- // Get all IOU report actions for the report.
- const iouRequestTypes: Array> = [
- CONST.IOU.REPORT_ACTION_TYPE.CREATE,
- CONST.IOU.REPORT_ACTION_TYPE.SPLIT,
- CONST.IOU.REPORT_ACTION_TYPE.PAY,
- CONST.IOU.REPORT_ACTION_TYPE.TRACK,
- ];
-
- const iouRequestActions = reportActionsArray.filter((action) => {
+ const iouRequestActions = [];
+ for (const action of reportActionsArray) {
if (!isMoneyRequestAction(action)) {
- return false;
+ // eslint-disable-next-line no-continue
+ continue;
}
+
const originalMessage = getOriginalMessage(action);
const actionType = originalMessage?.type;
- return (
+ if (
actionType &&
- (iouRequestTypes.includes(actionType) ?? []) &&
+ iouRequestTypes.has(actionType) &&
action.childReportID &&
// Include deleted IOU reportActions if:
// - they have an assocaited IOU transaction ID or
@@ -997,22 +999,27 @@ function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEn
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
(isMessageDeleted(action) && action.childVisibleActionCount) ||
(action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && (isOffline ?? isNetworkOffline)))
- );
- });
+ ) {
+ iouRequestActions.push(action);
+ }
+ }
// If we don't have any IOU request actions, or we have more than one IOU request actions, this isn't a oneTransaction report
if (!iouRequestActions.length || iouRequestActions.length > 1) {
return;
}
+ const singleAction = iouRequestActions[0];
+ const originalMessage = getOriginalMessage(singleAction);
+
// If there's only one IOU request action associated with the report but it's been deleted, then we don't consider this a oneTransaction report
// and want to display it using the standard view
- if (isMoneyRequestAction(iouRequestActions[0]) && (getOriginalMessage(iouRequestActions[0])?.deleted ?? '') !== '') {
+ if ((originalMessage?.deleted ?? '') !== '' && isMoneyRequestAction(singleAction)) {
return;
}
// Ensure we have a childReportID associated with the IOU report action
- return iouRequestActions[0].childReportID;
+ return singleAction.childReportID;
}
/**
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 8b08e5ac6c8f..305bde1dc387 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -43,6 +43,7 @@ import type {
UserWallet,
} from '@src/types/onyx';
import type {Participant} from '@src/types/onyx/IOU';
+import type {OriginalMessageExportedToIntegration} from '@src/types/onyx/OldDotAction';
import type Onboarding from '@src/types/onyx/Onboarding';
import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon';
import type {OriginalMessageChangeLog, PaymentMethodType} from '@src/types/onyx/OriginalMessage';
@@ -288,6 +289,12 @@ type OptimisticChatReport = Pick<
isOptimisticReport: true;
};
+type OptimisticExportIntegrationAction = OriginalMessageExportedToIntegration &
+ Pick<
+ ReportAction,
+ 'reportActionID' | 'actorAccountID' | 'avatar' | 'created' | 'lastModified' | 'message' | 'person' | 'shouldShow' | 'pendingAction' | 'errors' | 'automatic'
+ >;
+
type OptimisticTaskReportAction = Pick<
ReportAction,
| 'reportActionID'
@@ -1897,6 +1904,13 @@ function getPersonalDetailsForAccountID(accountID: number): Partial | undefined | null): Partial {
+ return personalDetails ?? {isOptimisticPersonalDetail: true};
+}
+
const hiddenTranslation = Localize.translateLocal('common.hidden');
const phoneNumberCache: Record = {};
@@ -1909,7 +1923,7 @@ function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = f
return '';
}
- const personalDetails = getPersonalDetailsForAccountID(accountID);
+ const personalDetails = getPersonalDetailsOrDefault(allPersonalDetails?.[accountID]);
if (!personalDetails) {
return '';
}
@@ -5202,6 +5216,40 @@ function buildOptimisticTaskReport(
};
}
+/**
+ * Builds an optimistic EXPORTED_TO_INTEGRATION report action
+ *
+ * @param integration - The connectionName of the integration
+ * @param markedManually - Whether the integration was marked as manually exported
+ */
+function buildOptimisticExportIntegrationAction(integration: ConnectionName, markedManually = false): OptimisticExportIntegrationAction {
+ const label = CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[integration];
+ return {
+ reportActionID: NumberUtils.rand64(),
+ actionName: CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_INTEGRATION,
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ actorAccountID: currentUserAccountID,
+ message: [],
+ person: [
+ {
+ type: CONST.REPORT.MESSAGE.TYPE.TEXT,
+ style: 'strong',
+ text: getCurrentUserDisplayNameOrEmail(),
+ },
+ ],
+ automatic: false,
+ avatar: getCurrentUserAvatar(),
+ created: DateUtils.getDBTime(),
+ shouldShow: true,
+ originalMessage: {
+ label,
+ lastModified: DateUtils.getDBTime(),
+ markedManually,
+ inProgress: true,
+ },
+ };
+}
+
/**
* A helper method to create transaction thread
*
@@ -7555,6 +7603,7 @@ export {
isAdminOwnerApproverOrReportOwner,
createDraftWorkspaceAndNavigateToConfirmationScreen,
isChatUsedForOnboarding,
+ buildOptimisticExportIntegrationAction,
getChatUsedForOnboarding,
getFieldViolationTranslation,
getFieldViolation,
diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts
index 18888903053e..4c3229760d71 100644
--- a/src/libs/SearchUtils.ts
+++ b/src/libs/SearchUtils.ts
@@ -1,10 +1,12 @@
import type {ValueOf} from 'type-fest';
-import type {AllFieldKeys, ASTNode, QueryFilter, QueryFilters, SearchColumnType, SearchQueryJSON, SearchQueryString, SortOrder} from '@components/Search/types';
+import type {AdvancedFiltersKeys, ASTNode, QueryFilter, QueryFilters, SearchColumnType, SearchQueryJSON, SearchQueryString, SortOrder} from '@components/Search/types';
import ReportListItem from '@components/SelectionList/Search/ReportListItem';
import TransactionListItem from '@components/SelectionList/Search/TransactionListItem';
import type {ListItem, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import type {SearchAdvancedFiltersForm} from '@src/types/form';
+import INPUT_IDS from '@src/types/form/SearchAdvancedFiltersForm';
import type * as OnyxTypes from '@src/types/onyx';
import type {SearchAccountDetails, SearchDataTypes, SearchPersonalDetails, SearchTransaction, SearchTypeToItemMap, SectionsType} from '@src/types/onyx/SearchResults';
import type SearchResults from '@src/types/onyx/SearchResults';
@@ -316,7 +318,7 @@ function buildSearchQueryJSON(query: SearchQueryString, policyID?: string) {
try {
// Add the full input and hash to the results
const result = searchParser.parse(query) as SearchQueryJSON;
- result.input = query;
+ result.inputQuery = query;
// Temporary solution until we move policyID filter into the AST - then remove this line and keep only query
const policyIDPart = policyID ?? '';
@@ -351,7 +353,54 @@ function normalizeQuery(query: string) {
return buildSearchQueryString(normalizedQueryJSON);
}
-function getFilters(query: SearchQueryString, fields: Array>) {
+/**
+ * @private
+ * returns Date filter query string part, which needs special logic
+ */
+function buildDateFilterQuery(filterValues: Partial) {
+ const dateBefore = filterValues[INPUT_IDS.DATE_BEFORE];
+ const dateAfter = filterValues[INPUT_IDS.DATE_AFTER];
+
+ let dateFilter = '';
+ if (dateBefore) {
+ dateFilter += `${CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE}<${dateBefore}`;
+ }
+ if (dateBefore && dateAfter) {
+ dateFilter += ' ';
+ }
+ if (dateAfter) {
+ dateFilter += `${CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE}>${dateAfter}`;
+ }
+
+ return dateFilter;
+}
+
+/**
+ * Given object with chosen search filters builds correct query string from them
+ */
+function buildQueryStringFromFilters(filterValues: Partial) {
+ // TODO add handling of multiple values picked
+ const filtersString = Object.entries(filterValues)
+ .map(([filterKey, filterValue]) => {
+ if (filterKey === INPUT_IDS.TYPE && filterValue) {
+ return `${CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE}:${filterValue as string}`;
+ }
+
+ if (filterKey === INPUT_IDS.STATUS && filterValue) {
+ return `${CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS}:${filterValue as string}`;
+ }
+
+ return undefined;
+ })
+ .filter(Boolean)
+ .join(' ');
+
+ const dateFilter = buildDateFilterQuery(filterValues);
+
+ return dateFilter ? `${filtersString} ${dateFilter}` : filtersString;
+}
+
+function getFilters(query: SearchQueryString, fields: Array>) {
let queryAST;
try {
@@ -427,4 +476,5 @@ export {
isSearchResultsEmpty,
getFilters,
normalizeQuery,
+ buildQueryStringFromFilters,
};
diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts
index cb2304fc46a4..6fbbd61dd212 100644
--- a/src/libs/ValidationUtils.ts
+++ b/src/libs/ValidationUtils.ts
@@ -338,7 +338,7 @@ function isValidCompanyName(name: string) {
}
function isValidReportName(name: string) {
- return name.trim().length <= CONST.REPORT_NAME_LIMIT;
+ return new Blob([name.trim()]).size <= CONST.REPORT_NAME_LIMIT;
}
/**
diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts
index b128e8f3045d..205a8dc41bba 100644
--- a/src/libs/actions/Card.ts
+++ b/src/libs/actions/Card.ts
@@ -7,9 +7,10 @@ import type {
ReportVirtualExpensifyCardFraudParams,
RequestReplacementExpensifyCardParams,
RevealExpensifyCardDetailsParams,
+ StartIssueNewCardFlowParams,
UpdateExpensifyCardLimitParams,
} from '@libs/API/parameters';
-import {SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
+import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as ErrorUtils from '@libs/ErrorUtils';
import * as NetworkStore from '@libs/Network/NetworkStore';
import CONST from '@src/CONST';
@@ -372,6 +373,14 @@ function updateExpensifyCardLimit(policyID: string, cardID: number, newLimit: nu
API.write(WRITE_COMMANDS.UPDATE_EXPENSIFY_CARD_LIMIT, parameters, {optimisticData, successData, failureData});
}
+function startIssueNewCardFlow(policyID: string) {
+ const parameters: StartIssueNewCardFlowParams = {
+ policyID,
+ };
+
+ API.read(READ_COMMANDS.START_ISSUE_NEW_CARD_FLOW, parameters);
+}
+
export {
requestReplacementExpensifyCard,
activatePhysicalExpensifyCard,
@@ -383,5 +392,6 @@ export {
clearIssueNewCardFlow,
updateExpensifyCardLimit,
updateSettlementAccount,
+ startIssueNewCardFlow,
};
export type {ReplacementReason};
diff --git a/src/libs/actions/EmojiPickerAction.ts b/src/libs/actions/EmojiPickerAction.ts
index 787f105e4939..e6123733b0e8 100644
--- a/src/libs/actions/EmojiPickerAction.ts
+++ b/src/libs/actions/EmojiPickerAction.ts
@@ -16,7 +16,7 @@ type EmojiPopoverAnchor = MutableRefObject void;
-type OnModalHideValue = () => void;
+type OnModalHideValue = (isNavigating?: boolean) => void;
type EmojiPickerRef = {
showEmojiPicker: (
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 8a7ee66bc886..961976960536 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -2,7 +2,7 @@ import {format} from 'date-fns';
import {fastMerge, Str} from 'expensify-common';
import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxInputValue, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
-import type {ValueOf} from 'type-fest';
+import type {PartialDeep, ValueOf} from 'type-fest';
import ReceiptGeneric from '@assets/images/receipt-generic.png';
import * as API from '@libs/API';
import type {
@@ -6628,9 +6628,8 @@ function canApproveIOU(
return false;
}
- const isOnInstantSubmitPolicy = PolicyUtils.isInstantSubmitEnabled(policy);
const isOnSubmitAndClosePolicy = PolicyUtils.isSubmitAndClose(policy);
- if (isOnInstantSubmitPolicy && isOnSubmitAndClosePolicy) {
+ if (isOnSubmitAndClosePolicy) {
return false;
}
@@ -7677,11 +7676,98 @@ function mergeDuplicates(params: TransactionMergeParams) {
};
});
+ const duplicateTransactionTotals = params.transactionIDList.reduce((total, id) => {
+ const duplicateTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`];
+ if (!duplicateTransaction) {
+ return total;
+ }
+ return total + duplicateTransaction.amount;
+ }, 0);
+
+ const expenseReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${params.reportID}`];
+ const expenseReportOptimisticData: OnyxUpdate = {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${params.reportID}`,
+ value: {
+ total: (expenseReport?.total ?? 0) - duplicateTransactionTotals,
+ },
+ };
+ const expenseReportFailureData: OnyxUpdate = {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${params.reportID}`,
+ value: {
+ total: expenseReport?.total,
+ },
+ };
+
+ const iouActionsToDelete = Object.values(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${params.reportID}`] ?? {})?.filter(
+ (reportAction): reportAction is ReportAction => {
+ if (!ReportActionsUtils.isMoneyRequestAction(reportAction)) {
+ return false;
+ }
+ const message = ReportActionsUtils.getOriginalMessage(reportAction);
+ if (!message?.IOUTransactionID) {
+ return false;
+ }
+ return params.transactionIDList.includes(message.IOUTransactionID);
+ },
+ );
+
+ const deletedTime = DateUtils.getDBTime();
+ const expenseReportActionsOptimisticData: OnyxUpdate = {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${params.reportID}`,
+ value: iouActionsToDelete.reduce>>>((val, reportAction) => {
+ // eslint-disable-next-line no-param-reassign
+ val[reportAction.reportActionID] = {
+ originalMessage: {
+ deleted: deletedTime,
+ },
+ ...(Array.isArray(reportAction.message) &&
+ !!reportAction.message[0] && {
+ message: [
+ {
+ ...reportAction.message[0],
+ deleted: deletedTime,
+ },
+ ...reportAction.message.slice(1),
+ ],
+ }),
+ ...(!Array.isArray(reportAction.message) && {
+ message: {
+ deleted: deletedTime,
+ },
+ }),
+ };
+ return val;
+ }, {}),
+ };
+ const expenseReportActionsFailureData: OnyxUpdate = {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${params.reportID}`,
+ value: iouActionsToDelete.reduce>>>>((val, reportAction) => {
+ // eslint-disable-next-line no-param-reassign
+ val[reportAction.reportActionID] = {
+ originalMessage: {
+ deleted: null,
+ },
+ message: reportAction.message,
+ };
+ return val;
+ }, {}),
+ };
+
const optimisticData: OnyxUpdate[] = [];
const failureData: OnyxUpdate[] = [];
- optimisticData.push(optimisticTransactionData, ...optimisticTransactionDuplicatesData, ...optimisticTransactionViolations);
- failureData.push(failureTransactionData, ...failureTransactionDuplicatesData, ...failureTransactionViolations);
+ optimisticData.push(
+ optimisticTransactionData,
+ ...optimisticTransactionDuplicatesData,
+ ...optimisticTransactionViolations,
+ expenseReportOptimisticData,
+ expenseReportActionsOptimisticData,
+ );
+ failureData.push(failureTransactionData, ...failureTransactionDuplicatesData, ...failureTransactionViolations, expenseReportFailureData, expenseReportActionsFailureData);
API.write(WRITE_COMMANDS.TRANSACTION_MERGE, params, {optimisticData, failureData});
}
diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts
index 53f33c4fd3d7..1bce525e22bf 100644
--- a/src/libs/actions/Policy/Policy.ts
+++ b/src/libs/actions/Policy/Policy.ts
@@ -1562,6 +1562,8 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName
autoReporting: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
approvalMode: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
reimbursementChoice: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ generalSettings: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ description: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
},
},
},
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index bd15e13494d4..f106c6cdb18d 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -25,6 +25,7 @@ import type {
InviteToGroupChatParams,
InviteToRoomParams,
LeaveRoomParams,
+ MarkAsExportedParams,
MarkAsUnreadParams,
OpenReportParams,
OpenRoomMembersPageParams,
@@ -32,6 +33,7 @@ import type {
RemoveEmojiReactionParams,
RemoveFromGroupChatParams,
RemoveFromRoomParams,
+ ReportExportParams,
ResolveActionableMentionWhisperParams,
ResolveActionableReportMentionWhisperParams,
SearchForReportsParams,
@@ -3835,18 +3837,94 @@ function setGroupDraft(newGroupDraft: Partial) {
}
function exportToIntegration(reportID: string, connectionName: ConnectionName) {
- API.write(WRITE_COMMANDS.REPORT_EXPORT, {
+ const action = ReportUtils.buildOptimisticExportIntegrationAction(connectionName);
+ const optimisticReportActionID = action.reportActionID;
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [optimisticReportActionID]: action,
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [optimisticReportActionID]: {
+ errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
+ },
+ },
+ },
+ ];
+
+ const params = {
reportIDList: reportID,
connectionName,
type: 'MANUAL',
- });
+ optimisticReportActions: JSON.stringify({
+ [reportID]: optimisticReportActionID,
+ }),
+ } satisfies ReportExportParams;
+
+ API.write(WRITE_COMMANDS.REPORT_EXPORT, params, {optimisticData, failureData});
}
-function markAsManuallyExported(reportID: string) {
- API.write(WRITE_COMMANDS.MARK_AS_EXPORTED, {
- reportIDList: reportID,
+function markAsManuallyExported(reportID: string, connectionName: ConnectionName) {
+ const action = ReportUtils.buildOptimisticExportIntegrationAction(connectionName, true);
+ const label = CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName];
+ const optimisticReportActionID = action.reportActionID;
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [optimisticReportActionID]: action,
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [optimisticReportActionID]: {
+ pendingAction: null,
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ value: {
+ [optimisticReportActionID]: {
+ errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
+ },
+ },
+ },
+ ];
+
+ const params = {
markedManually: true,
- });
+ data: JSON.stringify([
+ {
+ reportID,
+ label,
+ optimisticReportActionID,
+ },
+ ]),
+ } satisfies MarkAsExportedParams;
+
+ API.write(WRITE_COMMANDS.MARK_AS_EXPORTED, params, {optimisticData, successData, failureData});
}
export {
diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts
index 5aff8682abb9..4b782e8b103c 100644
--- a/src/libs/actions/Search.ts
+++ b/src/libs/actions/Search.ts
@@ -129,10 +129,26 @@ function exportSearchItemsToCSV({query, reportIDList, transactionIDList, policyI
}
/**
- * Updates the form values for the advanced search form.
+ * Updates the form values for the advanced filters search form.
*/
-function updateAdvancedFilters(values: FormOnyxValues) {
+function updateAdvancedFilters(values: Partial>) {
Onyx.merge(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, values);
}
-export {search, createTransactionThread, deleteMoneyRequestOnSearch, holdMoneyRequestOnSearch, unholdMoneyRequestOnSearch, exportSearchItemsToCSV, updateAdvancedFilters};
+/**
+ * Clears all values for the advanced filters search form.
+ */
+function clearAdvancedFilters() {
+ Onyx.set(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, null);
+}
+
+export {
+ search,
+ createTransactionThread,
+ deleteMoneyRequestOnSearch,
+ holdMoneyRequestOnSearch,
+ unholdMoneyRequestOnSearch,
+ exportSearchItemsToCSV,
+ updateAdvancedFilters,
+ clearAdvancedFilters,
+};
diff --git a/src/libs/actions/connections/index.ts b/src/libs/actions/connections/index.ts
index 01dca61cdfe9..b611a37faf64 100644
--- a/src/libs/actions/connections/index.ts
+++ b/src/libs/actions/connections/index.ts
@@ -371,6 +371,16 @@ function hasSynchronizationError(policy: OnyxEntry, connectionName: Poli
return !isSyncInProgress && policy?.connections?.[connectionName]?.lastSync?.isSuccessful === false;
}
+function isConnectionUnverified(policy: OnyxEntry, connectionName: PolicyConnectionName): boolean {
+ // A verified connection is one that has been successfully synced at least once
+ // We'll always err on the side of considering a connection as verified connected even if we can't find a lastSync property saying as such
+ // i.e. this is a property that is explicitly set to false, not just missing
+ if (connectionName === CONST.POLICY.CONNECTIONS.NAME.NETSUITE) {
+ return !(policy?.connections?.[CONST.POLICY.CONNECTIONS.NAME.NETSUITE]?.verified ?? true);
+ }
+ return !(policy?.connections?.[connectionName]?.lastSync?.isConnected ?? true);
+}
+
function copyExistingPolicyConnection(connectedPolicyID: string, targetPolicyID: string, connectionName: ConnectionName) {
let stageInProgress;
switch (connectionName) {
@@ -414,4 +424,5 @@ export {
hasSynchronizationError,
syncConnection,
copyExistingPolicyConnection,
+ isConnectionUnverified,
};
diff --git a/src/libs/navigateAfterJoinRequest/index.desktop.ts b/src/libs/navigateAfterJoinRequest/index.desktop.ts
index 47180c6a1368..cf6d009291c8 100644
--- a/src/libs/navigateAfterJoinRequest/index.desktop.ts
+++ b/src/libs/navigateAfterJoinRequest/index.desktop.ts
@@ -1,8 +1,12 @@
+import getIsSmallScreenWidth from '@libs/getIsSmallScreenWidth';
import Navigation from '@navigation/Navigation';
import ROUTES from '@src/ROUTES';
const navigateAfterJoinRequest = () => {
Navigation.goBack(undefined, false, true);
+ if (getIsSmallScreenWidth()) {
+ Navigation.navigate(ROUTES.SETTINGS);
+ }
Navigation.navigate(ROUTES.SETTINGS_WORKSPACES);
};
export default navigateAfterJoinRequest;
diff --git a/src/libs/navigateAfterJoinRequest/index.ts b/src/libs/navigateAfterJoinRequest/index.ts
index b53c59d678c9..42e91d18c6ba 100644
--- a/src/libs/navigateAfterJoinRequest/index.ts
+++ b/src/libs/navigateAfterJoinRequest/index.ts
@@ -3,7 +3,7 @@ import ROUTES from '@src/ROUTES';
const navigateAfterJoinRequest = () => {
Navigation.goBack(undefined, false, true);
- Navigation.navigate(ROUTES.ALL_SETTINGS);
+ Navigation.navigate(ROUTES.SETTINGS);
Navigation.navigate(ROUTES.SETTINGS_WORKSPACES);
};
export default navigateAfterJoinRequest;
diff --git a/src/libs/navigateAfterJoinRequest/index.web.ts b/src/libs/navigateAfterJoinRequest/index.web.ts
index 47180c6a1368..cf6d009291c8 100644
--- a/src/libs/navigateAfterJoinRequest/index.web.ts
+++ b/src/libs/navigateAfterJoinRequest/index.web.ts
@@ -1,8 +1,12 @@
+import getIsSmallScreenWidth from '@libs/getIsSmallScreenWidth';
import Navigation from '@navigation/Navigation';
import ROUTES from '@src/ROUTES';
const navigateAfterJoinRequest = () => {
Navigation.goBack(undefined, false, true);
+ if (getIsSmallScreenWidth()) {
+ Navigation.navigate(ROUTES.SETTINGS);
+ }
Navigation.navigate(ROUTES.SETTINGS_WORKSPACES);
};
export default navigateAfterJoinRequest;
diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx
index 8ee260065b63..1779fe8e085e 100644
--- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx
+++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx
@@ -162,8 +162,10 @@ function PrivateNotesEditPage({route, personalDetailsList, report, session}: Pri
if (!el) {
return;
}
+ if (!privateNotesInput.current) {
+ updateMultilineInputRange(el);
+ }
privateNotesInput.current = el;
- updateMultilineInputRange(privateNotesInput.current);
}}
isMarkdownEnabled
/>
diff --git a/src/pages/RoomDescriptionPage.tsx b/src/pages/RoomDescriptionPage.tsx
index 1d64ca9e1129..1103b0ba3d8a 100644
--- a/src/pages/RoomDescriptionPage.tsx
+++ b/src/pages/RoomDescriptionPage.tsx
@@ -90,8 +90,10 @@ function RoomDescriptionPage({report, policies}: RoomDescriptionPageProps) {
if (!el) {
return;
}
+ if (!reportDescriptionInputRef.current) {
+ updateMultilineInputRange(el);
+ }
reportDescriptionInputRef.current = el;
- updateMultilineInputRange(el);
}}
value={description}
onChangeText={handleReportDescriptionChange}
diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx
index 514e1f462e6b..ce9bae168684 100644
--- a/src/pages/RoomMembersPage.tsx
+++ b/src/pages/RoomMembersPage.tsx
@@ -173,8 +173,13 @@ function RoomMembersPage({report, session, policies}: RoomMembersPageProps) {
participants.forEach((accountID) => {
const details = personalDetails[accountID];
+ // When adding a new member to a room (whose personal detail does not exist in Onyx), an optimistic personal detail
+ // is created. However, when the real personal detail is returned from the backend, a duplicate member may appear
+ // briefly before the optimistic personal detail is deleted. To address this, we filter out the optimistically created
+ // member here.
+ const isDuplicateOptimisticDetail = details?.isOptimisticPersonalDetail && participants.some((accID) => accID !== accountID && details.login === personalDetails[accID]?.login);
- if (!details) {
+ if (!details || isDuplicateOptimisticDetail) {
Log.hmmm(`[RoomMembersPage] no personal details found for room member with accountID: ${accountID}`);
return;
}
diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx
index 171e2c45dbd7..eff58f140aa1 100644
--- a/src/pages/Search/AdvancedSearchFilters.tsx
+++ b/src/pages/Search/AdvancedSearchFilters.tsx
@@ -1,54 +1,111 @@
+import {Str} from 'expensify-common';
import React, {useMemo} from 'react';
import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
+import type {LocaleContextProps} from '@components/LocaleContextProvider';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import type {AdvancedFiltersKeys} from '@components/Search/types';
import useLocalize from '@hooks/useLocalize';
import useSingleExecution from '@hooks/useSingleExecution';
+import useThemeStyles from '@hooks/useThemeStyles';
import useWaitForNavigation from '@hooks/useWaitForNavigation';
import Navigation from '@libs/Navigation/Navigation';
+import * as SearchUtils from '@libs/SearchUtils';
+import * as SearchActions from '@userActions/Search';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type {SearchAdvancedFiltersForm} from '@src/types/form';
-function getFilterDisplayTitle(filters: Record, fieldName: string) {
- // This is temporary because the full parsing of search query is not yet done
- // TODO once we have values from query, this value should be `filters[fieldName].value`
- return fieldName;
+function getFilterDisplayTitle(filters: Partial, fieldName: AdvancedFiltersKeys, translate: LocaleContextProps['translate']) {
+ if (fieldName === CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE) {
+ // the value of date filter is a combination of dateBefore + dateAfter values
+ const {dateAfter, dateBefore} = filters;
+ let dateValue = '';
+ if (dateBefore) {
+ dateValue = translate('search.filters.date.before', dateBefore);
+ }
+ if (dateBefore && dateAfter) {
+ dateValue += ', ';
+ }
+ if (dateAfter) {
+ dateValue += translate('search.filters.date.after', dateAfter);
+ }
+
+ return dateValue;
+ }
+
+ // Todo Once all Advanced filters are implemented this line can be cleaned up. See: https://github.com/Expensify/App/issues/45026
+ // @ts-expect-error this property access is temporarily an error, because not every SYNTAX_FILTER_KEYS is handled by form.
+ // When all filters are updated here: src/types/form/SearchAdvancedFiltersForm.ts this line comment + type cast can be removed.
+ const filterValue = filters[fieldName] as string;
+ return filterValue ? Str.recapitalize(filterValue) : undefined;
}
function AdvancedSearchFilters() {
const {translate} = useLocalize();
+ const styles = useThemeStyles();
const {singleExecution} = useSingleExecution();
const waitForNavigate = useWaitForNavigation();
+ const [searchAdvancedFilters = {}] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM);
+
const advancedFilters = useMemo(
() => [
{
- title: getFilterDisplayTitle({}, 'title'),
+ title: getFilterDisplayTitle(searchAdvancedFilters, CONST.SEARCH.SYNTAX_ROOT_KEYS.TYPE, translate),
description: 'common.type' as const,
route: ROUTES.SEARCH_ADVANCED_FILTERS_TYPE,
},
{
- title: getFilterDisplayTitle({}, 'date'),
+ title: getFilterDisplayTitle(searchAdvancedFilters, CONST.SEARCH.SYNTAX_ROOT_KEYS.STATUS, translate),
+ description: 'search.filters.status' as const,
+ route: ROUTES.SEARCH_ADVANCED_FILTERS_STATUS,
+ },
+ {
+ title: getFilterDisplayTitle(searchAdvancedFilters, CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE, translate),
description: 'common.date' as const,
route: ROUTES.SEARCH_ADVANCED_FILTERS_DATE,
},
],
- [],
+ [searchAdvancedFilters, translate],
);
+ const onFormSubmit = () => {
+ const query = SearchUtils.buildQueryStringFromFilters(searchAdvancedFilters);
+ SearchActions.clearAdvancedFilters();
+ Navigation.navigate(
+ ROUTES.SEARCH_CENTRAL_PANE.getRoute({
+ query,
+ isCustomQuery: true,
+ }),
+ );
+ };
+
return (
-
- {advancedFilters.map((item) => {
- const onPress = singleExecution(waitForNavigate(() => Navigation.navigate(item.route)));
-
- return (
-
- );
- })}
+
+
+ {advancedFilters.map((item) => {
+ const onPress = singleExecution(waitForNavigate(() => Navigation.navigate(item.route)));
+
+ return (
+
+ );
+ })}
+
+
);
}
diff --git a/src/pages/Search/SearchFiltersStatusPage.tsx b/src/pages/Search/SearchFiltersStatusPage.tsx
new file mode 100644
index 000000000000..55274b770adc
--- /dev/null
+++ b/src/pages/Search/SearchFiltersStatusPage.tsx
@@ -0,0 +1,89 @@
+import React, {useMemo} from 'react';
+import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
+import type {FormOnyxValues} from '@components/Form/types';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import SelectionList from '@components/SelectionList';
+import RadioListItem from '@components/SelectionList/RadioListItem';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@navigation/Navigation';
+import * as SearchActions from '@userActions/Search';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+
+function SearchFiltersStatusPage() {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM);
+
+ const activeItem = searchAdvancedFiltersForm?.status;
+
+ const filterStatusItems = useMemo(
+ () => [
+ {
+ text: translate('common.all'),
+ value: CONST.SEARCH.STATUS.ALL,
+ keyForList: CONST.SEARCH.STATUS.ALL,
+ isSelected: activeItem === CONST.SEARCH.STATUS.ALL,
+ },
+ {
+ text: translate('common.shared'),
+ value: CONST.SEARCH.STATUS.SHARED,
+ keyForList: CONST.SEARCH.STATUS.SHARED,
+ isSelected: activeItem === CONST.SEARCH.STATUS.SHARED,
+ },
+ {
+ text: translate('common.drafts'),
+ value: CONST.SEARCH.STATUS.DRAFTS,
+ keyForList: CONST.SEARCH.STATUS.DRAFTS,
+ isSelected: activeItem === CONST.SEARCH.STATUS.DRAFTS,
+ },
+ {
+ text: translate('common.finished'),
+ value: CONST.SEARCH.STATUS.FINISHED,
+ keyForList: CONST.SEARCH.STATUS.FINISHED,
+ isSelected: activeItem === CONST.SEARCH.STATUS.FINISHED,
+ },
+ ],
+ [translate, activeItem],
+ );
+
+ const updateStatus = (values: Partial>) => {
+ SearchActions.updateAdvancedFilters(values);
+ Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS);
+ };
+
+ return (
+
+
+
+
+ {
+ updateStatus({
+ status: item.value,
+ });
+ }}
+ initiallyFocusedOptionKey={activeItem}
+ shouldStopPropagation
+ ListItem={RadioListItem}
+ />
+
+
+
+ );
+}
+
+SearchFiltersStatusPage.displayName = 'SearchFiltersStatusPage';
+
+export default SearchFiltersStatusPage;
diff --git a/src/pages/Search/SearchFiltersTypePage.tsx b/src/pages/Search/SearchFiltersTypePage.tsx
index e18b865f20ef..df5d55739884 100644
--- a/src/pages/Search/SearchFiltersTypePage.tsx
+++ b/src/pages/Search/SearchFiltersTypePage.tsx
@@ -1,16 +1,45 @@
-import React from 'react';
+import React, {useMemo} from 'react';
import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
+import type {FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
+import SelectionList from '@components/SelectionList';
+import RadioListItem from '@components/SelectionList/RadioListItem';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import Text from '@src/components/Text';
+import Navigation from '@navigation/Navigation';
+import * as SearchActions from '@userActions/Search';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
function SearchFiltersTypePage() {
const styles = useThemeStyles();
const {translate} = useLocalize();
+ const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM);
+
+ const activeItem = searchAdvancedFiltersForm?.type;
+
+ const filterTypeItems = useMemo(
+ () => [
+ {
+ text: translate('common.expenses'),
+ value: CONST.SEARCH.TYPE.EXPENSE,
+ keyForList: CONST.SEARCH.TYPE.EXPENSE,
+ isSelected: activeItem === CONST.SEARCH.TYPE.EXPENSE,
+ },
+ ],
+ [translate, activeItem],
+ );
+
+ const updateType = (values: Partial>) => {
+ SearchActions.updateAdvancedFilters(values);
+ Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS);
+ };
+
return (
-
- {/* temporary placeholder, will be implemented in https://github.com/Expensify/App/issues/45026 */}
- Advanced filters Type form
+
+ {
+ updateType({
+ type: item.value,
+ });
+ }}
+ initiallyFocusedOptionKey={activeItem}
+ shouldStopPropagation
+ ListItem={RadioListItem}
+ />
diff --git a/src/pages/SubmitExpensePage.tsx b/src/pages/SubmitExpensePage.tsx
new file mode 100644
index 000000000000..f4f6ea623c34
--- /dev/null
+++ b/src/pages/SubmitExpensePage.tsx
@@ -0,0 +1,56 @@
+import {useFocusEffect} from '@react-navigation/native';
+import React, {useEffect, useRef} from 'react';
+import {View} from 'react-native';
+import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView';
+import ReportHeaderSkeletonView from '@components/ReportHeaderSkeletonView';
+import ScreenWrapper from '@components/ScreenWrapper';
+import useThemeStyles from '@hooks/useThemeStyles';
+import interceptAnonymousUser from '@libs/interceptAnonymousUser';
+import Navigation from '@libs/Navigation/Navigation';
+import * as ReportUtils from '@libs/ReportUtils';
+import * as App from '@userActions/App';
+import * as IOU from '@userActions/IOU';
+import CONST from '@src/CONST';
+
+/*
+ * This is a "utility page", that does this:
+ * - If the user is authenticated, start Submit Expense
+ * - Else re-route to the login page
+ */
+function SubmitExpensePage() {
+ const styles = useThemeStyles();
+ const isUnmounted = useRef(false);
+
+ useFocusEffect(() => {
+ interceptAnonymousUser(() => {
+ App.confirmReadyToOpenApp();
+ Navigation.isNavigationReady().then(() => {
+ if (isUnmounted.current) {
+ return;
+ }
+ Navigation.goBack();
+ IOU.startMoneyRequest(CONST.IOU.TYPE.SUBMIT, ReportUtils.generateReportID());
+ });
+ });
+ });
+
+ useEffect(
+ () => () => {
+ isUnmounted.current = true;
+ },
+ [],
+ );
+
+ return (
+
+
+
+
+
+
+ );
+}
+
+SubmitExpensePage.displayName = 'SubmitExpensePage';
+
+export default SubmitExpensePage;
diff --git a/src/pages/TrackExpensePage.tsx b/src/pages/TrackExpensePage.tsx
new file mode 100644
index 000000000000..2e08c49721be
--- /dev/null
+++ b/src/pages/TrackExpensePage.tsx
@@ -0,0 +1,74 @@
+import {useFocusEffect} from '@react-navigation/native';
+import React, {useEffect, useRef} from 'react';
+import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView';
+import ReportHeaderSkeletonView from '@components/ReportHeaderSkeletonView';
+import ScreenWrapper from '@components/ScreenWrapper';
+import useNetwork from '@hooks/useNetwork';
+import useThemeStyles from '@hooks/useThemeStyles';
+import interceptAnonymousUser from '@libs/interceptAnonymousUser';
+import Navigation from '@libs/Navigation/Navigation';
+import * as ReportUtils from '@libs/ReportUtils';
+import * as App from '@userActions/App';
+import * as IOU from '@userActions/IOU';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
+
+/*
+ * This is a "utility page", that does this:
+ * - If the user is authenticated, find their self DM and and start a Track Expense
+ * - Else re-route to the login page
+ */
+function TrackExpensePage() {
+ const styles = useThemeStyles();
+ const isUnmounted = useRef(false);
+ const {isOffline} = useNetwork();
+ const [hasSeenTrackTraining, hasSeenTrackTrainingResult] = useOnyx(ONYXKEYS.NVP_HAS_SEEN_TRACK_TRAINING);
+ const isLoadingHasSeenTrackTraining = isLoadingOnyxValue(hasSeenTrackTrainingResult);
+
+ useFocusEffect(() => {
+ interceptAnonymousUser(() => {
+ App.confirmReadyToOpenApp();
+ Navigation.isNavigationReady().then(() => {
+ if (isUnmounted.current || isLoadingHasSeenTrackTraining) {
+ return;
+ }
+ Navigation.goBack();
+ IOU.startMoneyRequest(
+ CONST.IOU.TYPE.TRACK,
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ ReportUtils.findSelfDMReportID() || ReportUtils.generateReportID(),
+ );
+
+ if (!hasSeenTrackTraining && !isOffline) {
+ setTimeout(() => {
+ Navigation.navigate(ROUTES.TRACK_TRAINING_MODAL);
+ }, CONST.ANIMATED_TRANSITION);
+ }
+ });
+ });
+ });
+
+ useEffect(
+ () => () => {
+ isUnmounted.current = true;
+ },
+ [],
+ );
+
+ return (
+
+
+
+
+
+
+ );
+}
+
+TrackExpensePage.displayName = 'TrackExpensePage';
+
+export default TrackExpensePage;
diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
index 5220347d4711..3a57f057a938 100644
--- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
@@ -482,7 +482,12 @@ function ReportActionCompose({
{DeviceCapabilities.canUseTouchScreen() && isMediumScreenWidth ? null : (
{
+ if (isNavigating) {
+ return;
+ }
+ focus();
+ }}
onEmojiSelected={(...args) => composerRef.current?.replaceSelectionWithText(...args)}
emojiPickerID={report?.reportID}
shiftVertical={emojiShiftVertical}
diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx
index c741be4ca3b4..0cb9876b69f8 100644
--- a/src/pages/home/report/ReportActionItem.tsx
+++ b/src/pages/home/report/ReportActionItem.tsx
@@ -22,6 +22,7 @@ import type {ActionableItem} from '@components/ReportActionItem/ActionableItemBu
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 RenameAction from '@components/ReportActionItem/RenameAction';
import ReportPreview from '@components/ReportActionItem/ReportPreview';
@@ -90,9 +91,6 @@ const getDraftMessage = (drafts: OnyxCollection,
};
type ReportActionItemOnyxProps = {
- /** Get modal status */
- modal: OnyxEntry;
-
/** IOU report for this action, if any */
iouReport: OnyxEntry;
@@ -161,7 +159,6 @@ type ReportActionItemProps = {
} & ReportActionItemOnyxProps;
function ReportActionItem({
- modal,
action,
report,
transactionThreadReport,
@@ -195,6 +192,7 @@ function ReportActionItem({
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);
@@ -566,6 +564,8 @@ function ReportActionItem({
isHovered={hovered}
contextMenuAnchor={popoverAnchorRef.current}
checkIfContextMenuActive={toggleContextMenuFromActiveReportAction}
+ onPaymentOptionsShow={() => setIsPaymentMethodPopoverActive(true)}
+ onPaymentOptionsHide={() => setIsPaymentMethodPopoverActive(false)}
isWhisper={isWhisper}
/>
);
@@ -653,6 +653,10 @@ function ReportActionItem({
children = ;
} else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_TAG) {
children = ;
+ } else if (
+ ReportActionsUtils.isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED, CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED_VIRTUAL, CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS)
+ ) {
+ children = ;
} else if (ReportActionsUtils.isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_INTEGRATION)) {
children = ;
} else {
@@ -901,7 +905,6 @@ function ReportActionItem({
accessible
>
@@ -926,7 +929,7 @@ function ReportActionItem({
)}
@@ -998,15 +1001,11 @@ export default withOnyx({
`${ONYXKEYS.COLLECTION.TRANSACTION}${ReportActionsUtils.isMoneyRequestAction(action) ? ReportActionsUtils.getOriginalMessage(action)?.IOUTransactionID ?? -1 : -1}`,
selector: (transaction: OnyxEntry) => transaction?.errorFields?.route ?? null,
},
- modal: {
- key: ONYXKEYS.MODAL,
- },
})(
memo(ReportActionItem, (prevProps, nextProps) => {
const prevParentReportAction = prevProps.parentReportAction;
const nextParentReportAction = nextProps.parentReportAction;
return (
- prevProps.modal?.willAlertModalBecomeVisible === nextProps.modal?.willAlertModalBecomeVisible &&
prevProps.displayAsGroup === nextProps.displayAsGroup &&
prevProps.isMostRecentIOUReportAction === nextProps.isMostRecentIOUReportAction &&
prevProps.shouldDisplayNewMarker === nextProps.shouldDisplayNewMarker &&
@@ -1035,8 +1034,7 @@ export default withOnyx({
lodashIsEqual(prevProps.transactionThreadReport, nextProps.transactionThreadReport) &&
lodashIsEqual(prevProps.reportActions, nextProps.reportActions) &&
lodashIsEqual(prevProps.linkedTransactionRouteError, nextProps.linkedTransactionRouteError) &&
- lodashIsEqual(prevParentReportAction, nextParentReportAction) &&
- prevProps.modal?.willAlertModalBecomeVisible === nextProps.modal?.willAlertModalBecomeVisible
+ lodashIsEqual(prevParentReportAction, nextParentReportAction)
);
}),
);
diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx
index 12d886cd30f9..a7d2046a7ef1 100644
--- a/src/pages/home/report/ReportActionsList.tsx
+++ b/src/pages/home/report/ReportActionsList.tsx
@@ -177,7 +177,8 @@ function ReportActionsList({
const userActiveSince = useRef(null);
const lastMessageTime = useRef(null);
- const [isVisible, setIsVisible] = useState(false);
+ const [isVisible, setIsVisible] = useState(Visibility.isVisible());
+ const hasCalledReadNewestAction = useRef(false);
const isFocused = useIsFocused();
useEffect(() => {
@@ -267,6 +268,9 @@ function ReportActionsList({
if (!userActiveSince.current || report.reportID !== prevReportID) {
return;
}
+ if (hasCalledReadNewestAction.current) {
+ return;
+ }
if (ReportUtils.isUnread(report)) {
// On desktop, when the notification center is displayed, Visibility.isVisible() will return false.
// Currently, there's no programmatic way to dismiss the notification center panel.
@@ -274,6 +278,7 @@ function ReportActionsList({
const isFromNotification = route?.params?.referrer === CONST.REFERRER.NOTIFICATION;
if ((Visibility.isVisible() || isFromNotification) && scrollingVerticalOffset.current < MSG_VISIBLE_THRESHOLD) {
Report.readNewestAction(report.reportID);
+ hasCalledReadNewestAction.current = true;
if (isFromNotification) {
Navigation.setParams({referrer: undefined});
}
@@ -524,6 +529,10 @@ function ReportActionsList({
return;
}
+ if (hasCalledReadNewestAction.current) {
+ return;
+ }
+
if (!isVisible || !isFocused) {
if (!lastMessageTime.current) {
lastMessageTime.current = sortedVisibleReportActions[0]?.created ?? '';
@@ -535,24 +544,27 @@ function ReportActionsList({
// show marker based on report.lastReadTime
const newMessageTimeReference = lastMessageTime.current && report.lastReadTime && lastMessageTime.current > report.lastReadTime ? userActiveSince.current : report.lastReadTime;
lastMessageTime.current = null;
- if (
- scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD ||
- !sortedVisibleReportActions.some(
- (reportAction) =>
- newMessageTimeReference &&
- newMessageTimeReference < reportAction.created &&
- (ReportActionsUtils.isReportPreviewAction(reportAction) ? reportAction.childLastActorAccountID : reportAction.actorAccountID) !== Report.getCurrentUserAccountID(),
- )
- ) {
+ const areSomeReportActionsUnread = sortedVisibleReportActions.some((reportAction) => {
+ /**
+ * The archived reports should not be marked as unread. So we are checking if the report is archived or not.
+ * If the report is archived, we will mark the report as read.
+ */
+ const isArchivedReport = ReportUtils.isArchivedRoom(report);
+ const isUnread = isArchivedReport || (newMessageTimeReference && newMessageTimeReference < reportAction.created);
+ return (
+ isUnread && (ReportActionsUtils.isReportPreviewAction(reportAction) ? reportAction.childLastActorAccountID : reportAction.actorAccountID) !== Report.getCurrentUserAccountID()
+ );
+ });
+ if (scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || !areSomeReportActionsUnread) {
return;
}
-
Report.readNewestAction(report.reportID);
userActiveSince.current = DateUtils.getDBTime();
lastReadTimeRef.current = newMessageTimeReference;
setCurrentUnreadMarker(null);
cacheUnreadMarkers.delete(report.reportID);
calculateUnreadMarker();
+ hasCalledReadNewestAction.current = true;
// This effect logic to `mark as read` will only run when the report focused has new messages and the App visibility
// is changed to visible(meaning user switched to app/web, while user was previously using different tab or application).
diff --git a/src/pages/home/report/ReportDetailsExportPage.tsx b/src/pages/home/report/ReportDetailsExportPage.tsx
index 99b7305cc7a9..6f16786682af 100644
--- a/src/pages/home/report/ReportDetailsExportPage.tsx
+++ b/src/pages/home/report/ReportDetailsExportPage.tsx
@@ -45,7 +45,7 @@ function ReportDetailsExportPage({route}: ReportDetailsExportPageProps) {
if (type === CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION) {
ReportActions.exportToIntegration(reportID, connectionName);
} else if (type === CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED) {
- ReportActions.markAsManuallyExported(reportID);
+ ReportActions.markAsManuallyExported(reportID, connectionName);
}
setModalStatus(null);
Navigation.dismissModal();
diff --git a/src/pages/iou/request/step/IOURequestStepDescription.tsx b/src/pages/iou/request/step/IOURequestStepDescription.tsx
index 081f0b104355..7505b88d0745 100644
--- a/src/pages/iou/request/step/IOURequestStepDescription.tsx
+++ b/src/pages/iou/request/step/IOURequestStepDescription.tsx
@@ -178,8 +178,10 @@ function IOURequestStepDescription({
if (!el) {
return;
}
+ if (!inputRef.current) {
+ updateMultilineInputRange(el);
+ }
inputRef.current = el;
- updateMultilineInputRange(inputRef.current);
}}
autoGrowHeight
maxAutoGrowHeight={variables.textInputAutoGrowMaxHeight}
diff --git a/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx b/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx
index 4b8c22c2cdcb..6e540efc7887 100644
--- a/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx
+++ b/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx
@@ -84,15 +84,16 @@ function ExitSurveyConfirmPage({exitReason, isLoading, route, navigation}: ExitS
large
text={translate('exitSurvey.goToExpensifyClassic')}
onPress={() => {
- ExitSurvey.switchToOldDot().then(() => {
- if (NativeModules.HybridAppModule) {
+ const promise = ExitSurvey.switchToOldDot();
+ if (NativeModules.HybridAppModule) {
+ promise.then(() => {
Navigation.resetToHome();
NativeModules.HybridAppModule.closeReactNativeApp();
- return;
- }
- Link.openOldDotLink(CONST.OLDDOT_URLS.INBOX);
- Navigation.dismissModal();
- });
+ });
+ return;
+ }
+ Navigation.dismissModal();
+ Link.openOldDotLink(CONST.OLDDOT_URLS.INBOX);
}}
isLoading={isLoading ?? false}
isDisabled={isOffline}
diff --git a/src/pages/settings/ExitSurvey/ExitSurveyResponsePage.tsx b/src/pages/settings/ExitSurvey/ExitSurveyResponsePage.tsx
index 64e8159334d9..b523d8102b50 100644
--- a/src/pages/settings/ExitSurvey/ExitSurveyResponsePage.tsx
+++ b/src/pages/settings/ExitSurvey/ExitSurveyResponsePage.tsx
@@ -44,7 +44,7 @@ function ExitSurveyResponsePage({draftResponse, route, navigation}: ExitSurveyRe
const {keyboardHeight} = useKeyboardState();
const {windowHeight} = useWindowDimensions();
const {top: safeAreaInsetsTop} = useSafeAreaInsets();
- const {inputCallbackRef} = useAutoFocusInput();
+ const {inputCallbackRef, inputRef} = useAutoFocusInput();
const {reason, backTo} = route.params;
const {isOffline} = useNetwork({
@@ -134,7 +134,9 @@ function ExitSurveyResponsePage({draftResponse, route, navigation}: ExitSurveyRe
if (!el) {
return;
}
- updateMultilineInputRange(el);
+ if (!inputRef.current) {
+ updateMultilineInputRange(el);
+ }
inputCallbackRef(el);
}}
containerStyles={[baseResponseInputContainerStyle]}
diff --git a/src/pages/tasks/NewTaskDescriptionPage.tsx b/src/pages/tasks/NewTaskDescriptionPage.tsx
index f5aaf9ea8ffd..f18829e8c803 100644
--- a/src/pages/tasks/NewTaskDescriptionPage.tsx
+++ b/src/pages/tasks/NewTaskDescriptionPage.tsx
@@ -37,7 +37,7 @@ type NewTaskDescriptionPageProps = NewTaskDescriptionPageOnyxProps & StackScreen
function NewTaskDescriptionPage({task}: NewTaskDescriptionPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
- const {inputCallbackRef} = useAutoFocusInput();
+ const {inputCallbackRef, inputRef} = useAutoFocusInput();
const onSubmit = (values: FormOnyxValues) => {
TaskActions.setDescriptionValue(values.taskDescription);
@@ -83,8 +83,10 @@ function NewTaskDescriptionPage({task}: NewTaskDescriptionPageProps) {
accessibilityLabel={translate('newTaskPage.descriptionOptional')}
role={CONST.ROLE.PRESENTATION}
ref={(el) => {
+ if (!inputRef.current) {
+ updateMultilineInputRange(el);
+ }
inputCallbackRef(el);
- updateMultilineInputRange(el);
}}
autoGrowHeight
maxAutoGrowHeight={variables.textInputAutoGrowMaxHeight}
diff --git a/src/pages/tasks/TaskDescriptionPage.tsx b/src/pages/tasks/TaskDescriptionPage.tsx
index 92f5b2394308..230185f2b4f0 100644
--- a/src/pages/tasks/TaskDescriptionPage.tsx
+++ b/src/pages/tasks/TaskDescriptionPage.tsx
@@ -36,7 +36,8 @@ function TaskDescriptionPage({report, currentUserPersonalDetails}: TaskDescripti
const validate = useCallback(
(values: FormOnyxValues): FormInputErrors => {
const errors = {};
- const taskDescriptionLength = ReportUtils.getCommentLength(values.description);
+ const parsedDescription = ReportUtils.getParsedComment(values?.description);
+ const taskDescriptionLength = ReportUtils.getCommentLength(parsedDescription);
if (values?.description && taskDescriptionLength > CONST.DESCRIPTION_LIMIT) {
ErrorUtils.addErrorMessage(errors, 'description', translate('common.error.characterLimitExceedCounter', {length: taskDescriptionLength, limit: CONST.DESCRIPTION_LIMIT}));
}
@@ -116,8 +117,10 @@ function TaskDescriptionPage({report, currentUserPersonalDetails}: TaskDescripti
if (!element) {
return;
}
+ if (!inputRef.current) {
+ updateMultilineInputRange(inputRef.current);
+ }
inputRef.current = element;
- updateMultilineInputRange(inputRef.current);
}}
autoGrowHeight
maxAutoGrowHeight={variables.textInputAutoGrowMaxHeight}
diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx
index ff6fd22b7565..df34875f5fa6 100644
--- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx
+++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx
@@ -71,7 +71,7 @@ function WorkspaceInviteMessagePage({
const [welcomeNote, setWelcomeNote] = useState();
- const {inputCallbackRef} = useAutoFocusInput();
+ const {inputCallbackRef, inputRef} = useAutoFocusInput();
const welcomeNoteSubject = useMemo(
() => `# ${currentUserPersonalDetails?.displayName ?? ''} invited you to ${policy?.name ?? 'a workspace'}`,
@@ -207,8 +207,10 @@ function WorkspaceInviteMessagePage({
if (!element) {
return;
}
+ if (!inputRef.current) {
+ updateMultilineInputRange(element);
+ }
inputCallbackRef(element);
- updateMultilineInputRange(element);
}}
/>
diff --git a/src/pages/workspace/WorkspaceNewRoomPage.tsx b/src/pages/workspace/WorkspaceNewRoomPage.tsx
index af5c758d3ccf..c848e538bd1c 100644
--- a/src/pages/workspace/WorkspaceNewRoomPage.tsx
+++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx
@@ -272,7 +272,6 @@ function WorkspaceNewRoomPage({policies, reports, formState, session, activePoli
validate={validate}
onSubmit={submit}
enabledWhenOffline
- disablePressOnEnter={false}
>
Parser.htmlToMarkdown(
// policy?.description can be an empty string
@@ -109,7 +110,10 @@ function WorkspaceProfileDescriptionPage({policy}: Props) {
autoGrowHeight
isMarkdownEnabled
ref={(el: BaseTextInputRef | null): void => {
- updateMultilineInputRange(el);
+ if (!isInputInitializedRef.current) {
+ updateMultilineInputRange(el);
+ }
+ isInputInitializedRef.current = true;
}}
/>
diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx
index 2da483c884df..89bbfd75d538 100644
--- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx
+++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx
@@ -30,7 +30,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
-import {hasSynchronizationError, removePolicyConnection, syncConnection} from '@libs/actions/connections';
+import {hasSynchronizationError, isConnectionUnverified, removePolicyConnection, syncConnection} from '@libs/actions/connections';
import {
areXeroSettingsInErrorFields,
findCurrentXeroOrganization,
@@ -315,6 +315,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
return [];
}
const shouldShowSynchronizationError = hasSynchronizationError(policy, connectedIntegration, isSyncInProgress);
+ const shouldHideConfigurationOptions = isConnectionUnverified(policy, connectedIntegration);
const integrationData = accountingIntegrationData(connectedIntegration, policyID, translate, undefined, undefined, policy);
const iconProps = integrationData?.icon ? {icon: integrationData.icon, iconType: CONST.ICON_TYPE_AVATAR} : {};
return [
@@ -354,7 +355,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
),
},
...(isEmptyObject(integrationSpecificMenuItems) || shouldShowSynchronizationError || isEmptyObject(policy?.connections) ? [] : [integrationSpecificMenuItems]),
- ...(isEmptyObject(policy?.connections) || shouldShowSynchronizationError
+ ...(isEmptyObject(policy?.connections) || shouldHideConfigurationOptions
? []
: [
{
diff --git a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx
index eb2c6489241f..1b86832c374a 100644
--- a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx
@@ -8,6 +8,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import * as Connections from '@libs/actions/connections/NetSuiteCommands';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
+import {findSelectedBankAccountWithDefaultSelect, getFilteredApprovalAccountOptions, getFilteredCollectionAccountOptions, getFilteredReimbursableAccountOptions} from '@libs/PolicyUtils';
import type {DividerLineItem, MenuItem, ToggleItem} from '@pages/workspace/accounting/netsuite/types';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
@@ -27,10 +28,13 @@ function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) {
const {payableList} = policy?.connections?.netsuite?.options?.data ?? {};
const selectedReimbursementAccount = useMemo(
- () => (payableList ?? []).find((payableAccount) => payableAccount.id === config?.reimbursementAccountID),
+ () => findSelectedBankAccountWithDefaultSelect(getFilteredReimbursableAccountOptions(payableList), config?.reimbursementAccountID),
[payableList, config?.reimbursementAccountID],
);
- const selectedCollectionAccount = useMemo(() => (payableList ?? []).find((payableAccount) => payableAccount.id === config?.collectionAccount), [payableList, config?.collectionAccount]);
+ const selectedCollectionAccount = useMemo(
+ () => findSelectedBankAccountWithDefaultSelect(getFilteredCollectionAccountOptions(payableList), config?.collectionAccount),
+ [payableList, config?.collectionAccount],
+ );
const selectedApprovalAccount = useMemo(() => {
if (config?.approvalAccount === CONST.NETSUITE_APPROVAL_ACCOUNT_DEFAULT) {
return {
@@ -38,7 +42,7 @@ function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) {
name: translate('workspace.netsuite.advancedConfig.defaultApprovalAccount'),
};
}
- return (payableList ?? []).find((payableAccount) => payableAccount.id === config?.approvalAccount);
+ return findSelectedBankAccountWithDefaultSelect(getFilteredApprovalAccountOptions(payableList), config?.approvalAccount);
}, [config?.approvalAccount, payableList, translate]);
const menuItems: Array = [
@@ -207,7 +211,7 @@ function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) {
description: translate('workspace.netsuite.advancedConfig.customFormIDReimbursable'),
onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_CUSTOM_FORM_ID.getRoute(policyID, CONST.NETSUITE_EXPENSE_TYPE.REIMBURSABLE)),
brickRoadIndicator: config?.errorFields?.customFormIDOptions ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
- title: config?.customFormIDOptions?.reimbursable[CONST.NETSUITE_MAP_EXPORT_DESTINATION[config.reimbursableExpensesExportDestination]],
+ title: config?.customFormIDOptions?.reimbursable?.[CONST.NETSUITE_MAP_EXPORT_DESTINATION[config.reimbursableExpensesExportDestination]],
pendingAction: config?.pendingFields?.customFormIDOptions,
errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.CUSTOM_FORM_ID_OPTIONS),
onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.CUSTOM_FORM_ID_OPTIONS),
@@ -218,7 +222,7 @@ function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) {
description: translate('workspace.netsuite.advancedConfig.customFormIDNonReimbursable'),
onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_CUSTOM_FORM_ID.getRoute(policyID, CONST.NETSUITE_EXPENSE_TYPE.NON_REIMBURSABLE)),
brickRoadIndicator: config?.errorFields?.customFormIDOptions ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
- title: config?.customFormIDOptions?.nonReimbursable[CONST.NETSUITE_MAP_EXPORT_DESTINATION[config.nonreimbursableExpensesExportDestination]],
+ title: config?.customFormIDOptions?.nonReimbursable?.[CONST.NETSUITE_MAP_EXPORT_DESTINATION[config.nonreimbursableExpensesExportDestination]],
pendingAction: config?.pendingFields?.customFormIDOptions,
errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.CUSTOM_FORM_ID_OPTIONS),
onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.CUSTOM_FORM_ID_OPTIONS),
diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteDateSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteDateSelectPage.tsx
index 921d584eaed8..42d014ed4299 100644
--- a/src/pages/workspace/accounting/netsuite/export/NetSuiteDateSelectPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteDateSelectPage.tsx
@@ -24,12 +24,13 @@ function NetSuiteDateSelectPage({policy}: WithPolicyConnectionsProps) {
const policyID = policy?.id ?? '-1';
const styles = useThemeStyles();
const config = policy?.connections?.netsuite.options.config;
+ const selectedValue = Object.values(CONST.NETSUITE_EXPORT_DATE).find((value) => value === config?.exportDate) ?? CONST.NETSUITE_EXPORT_DATE.LAST_EXPENSE;
const data: MenuListItem[] = Object.values(CONST.NETSUITE_EXPORT_DATE).map((dateType) => ({
value: dateType,
text: translate(`workspace.netsuite.exportDate.values.${dateType}.label`),
alternateText: translate(`workspace.netsuite.exportDate.values.${dateType}.description`),
keyForList: dateType,
- isSelected: config?.exportDate === dateType,
+ isSelected: selectedValue === dateType,
}));
const headerContent = useMemo(
diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx
index 935a94bf763f..648ec7d77a64 100644
--- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx
@@ -9,7 +9,13 @@ import useThemeStyles from '@hooks/useThemeStyles';
import * as Connections from '@libs/actions/connections/NetSuiteCommands';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
-import {canUseProvincialTaxNetSuite, canUseTaxNetSuite} from '@libs/PolicyUtils';
+import {
+ canUseProvincialTaxNetSuite,
+ canUseTaxNetSuite,
+ findSelectedBankAccountWithDefaultSelect,
+ findSelectedInvoiceItemWithDefaultSelect,
+ findSelectedTaxAccountWithDefaultSelect,
+} from '@libs/PolicyUtils';
import type {DividerLineItem, MenuItem, ToggleItem} from '@pages/workspace/accounting/netsuite/types';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
@@ -30,13 +36,13 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) {
const {subsidiaryList, receivableList, taxAccountsList, items} = policy?.connections?.netsuite?.options?.data ?? {};
const selectedSubsidiary = useMemo(() => (subsidiaryList ?? []).find((subsidiary) => subsidiary.internalID === config?.subsidiaryID), [subsidiaryList, config?.subsidiaryID]);
- const selectedReceivable = useMemo(() => (receivableList ?? []).find((receivable) => receivable.id === config?.receivableAccount), [receivableList, config?.receivableAccount]);
+ const selectedReceivable = useMemo(() => findSelectedBankAccountWithDefaultSelect(receivableList, config?.receivableAccount), [receivableList, config?.receivableAccount]);
- const selectedItem = useMemo(() => (items ?? []).find((item) => item.id === config?.invoiceItem), [items, config?.invoiceItem]);
+ const selectedItem = useMemo(() => findSelectedInvoiceItemWithDefaultSelect(items, config?.invoiceItem), [items, config?.invoiceItem]);
const invoiceItemValue = useMemo(() => {
if (!config?.invoiceItemPreference) {
- return undefined;
+ return translate('workspace.netsuite.invoiceItem.values.create.label');
}
if (config.invoiceItemPreference === CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.CREATE) {
return translate('workspace.netsuite.invoiceItem.values.create.label');
@@ -47,13 +53,10 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) {
return selectedItem.name;
}, [config?.invoiceItemPreference, selectedItem, translate]);
- const selectedTaxPostingAccount = useMemo(
- () => (taxAccountsList ?? []).find((taxAccount) => taxAccount.externalID === config?.taxPostingAccount),
- [taxAccountsList, config?.taxPostingAccount],
- );
+ const selectedTaxPostingAccount = useMemo(() => findSelectedTaxAccountWithDefaultSelect(taxAccountsList, config?.taxPostingAccount), [taxAccountsList, config?.taxPostingAccount]);
const selectedProvTaxPostingAccount = useMemo(
- () => (taxAccountsList ?? []).find((taxAccount) => taxAccount.externalID === config?.provincialTaxPostingAccount),
+ () => findSelectedTaxAccountWithDefaultSelect(taxAccountsList, config?.provincialTaxPostingAccount),
[taxAccountsList, config?.provincialTaxPostingAccount],
);
@@ -76,10 +79,12 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) {
},
{
type: 'menuitem',
- description: translate('common.date'),
+ description: translate('workspace.accounting.exportDate'),
onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_DATE_SELECT.getRoute(policyID)),
brickRoadIndicator: config?.errorFields?.exportDate ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
- title: config?.exportDate ? translate(`workspace.netsuite.exportDate.values.${config.exportDate}.label`) : undefined,
+ title: config?.exportDate
+ ? translate(`workspace.netsuite.exportDate.values.${config.exportDate}.label`)
+ : translate(`workspace.netsuite.exportDate.values.${CONST.NETSUITE_EXPORT_DATE.LAST_EXPENSE}.label`),
pendingAction: config?.pendingFields?.exportDate,
errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.EXPORT_DATE),
onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.EXPORT_DATE),
diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx
index 3d01f96f9a64..544cd0cc20cd 100644
--- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx
@@ -27,11 +27,15 @@ function NetSuiteExportExpensesJournalPostingPreferenceSelectPage({policy}: With
const params = route.params as ExpenseRouteParams;
const isReimbursable = params.expenseType === CONST.NETSUITE_EXPENSE_TYPE.REIMBURSABLE;
+ const selectedValue =
+ Object.values(CONST.NETSUITE_JOURNAL_POSTING_PREFERENCE).find((value) => value === config?.journalPostingPreference) ??
+ CONST.NETSUITE_JOURNAL_POSTING_PREFERENCE.JOURNALS_POSTING_INDIVIDUAL_LINE;
+
const data: MenuListItem[] = Object.values(CONST.NETSUITE_JOURNAL_POSTING_PREFERENCE).map((postingPreference) => ({
value: postingPreference,
text: translate(`workspace.netsuite.journalPostingPreference.values.${postingPreference}`),
keyForList: postingPreference,
- isSelected: config?.journalPostingPreference === postingPreference,
+ isSelected: selectedValue === postingPreference,
}));
const selectPostingPreference = useCallback(
diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx
index 690caa37b49c..c3ffe000e1e5 100644
--- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx
@@ -7,6 +7,7 @@ import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
+import {findSelectedBankAccountWithDefaultSelect, findSelectedVendorWithDefaultSelect} from '@libs/PolicyUtils';
import type {ExpenseRouteParams, MenuItem} from '@pages/workspace/accounting/netsuite/types';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
@@ -34,12 +35,12 @@ function NetSuiteExportExpensesPage({policy}: WithPolicyConnectionsProps) {
const {vendors, payableList} = policy?.connections?.netsuite?.options?.data ?? {};
- const defaultVendor = useMemo(() => (vendors ?? []).find((vendor) => vendor.id === config?.defaultVendor), [vendors, config?.defaultVendor]);
+ const defaultVendor = useMemo(() => findSelectedVendorWithDefaultSelect(vendors, config?.defaultVendor), [vendors, config?.defaultVendor]);
- const selectedPayableAccount = useMemo(() => (payableList ?? []).find((payableAccount) => payableAccount.id === config?.payableAcct), [payableList, config?.payableAcct]);
+ const selectedPayableAccount = useMemo(() => findSelectedBankAccountWithDefaultSelect(payableList, config?.payableAcct), [payableList, config?.payableAcct]);
const selectedReimbursablePayableAccount = useMemo(
- () => (payableList ?? []).find((payableAccount) => payableAccount.id === config?.reimbursablePayableAccount),
+ () => findSelectedBankAccountWithDefaultSelect(payableList, config?.reimbursablePayableAccount),
[payableList, config?.reimbursablePayableAccount],
);
@@ -89,7 +90,9 @@ function NetSuiteExportExpensesPage({policy}: WithPolicyConnectionsProps) {
description: translate('workspace.netsuite.journalPostingPreference.label'),
onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_JOURNAL_POSTING_PREFERENCE_SELECT.getRoute(policyID, params.expenseType)),
brickRoadIndicator: config?.errorFields?.journalPostingPreference ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
- title: config?.journalPostingPreference ? translate(`workspace.netsuite.journalPostingPreference.values.${config.journalPostingPreference}`) : undefined,
+ title: config?.journalPostingPreference
+ ? translate(`workspace.netsuite.journalPostingPreference.values.${config.journalPostingPreference}`)
+ : translate(`workspace.netsuite.journalPostingPreference.values.${CONST.NETSUITE_JOURNAL_POSTING_PREFERENCE.JOURNALS_POSTING_INDIVIDUAL_LINE}`),
pendingAction: config?.pendingFields?.journalPostingPreference,
errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.JOURNAL_POSTING_PREFERENCE),
onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.JOURNAL_POSTING_PREFERENCE),
diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx
index cc28aaaeb4c5..fe8e9b7e4d70 100644
--- a/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx
@@ -12,6 +12,7 @@ import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Connections from '@libs/actions/connections/NetSuiteCommands';
import * as ErrorUtils from '@libs/ErrorUtils';
+import {findSelectedInvoiceItemWithDefaultSelect} from '@libs/PolicyUtils';
import Navigation from '@navigation/Navigation';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
@@ -30,13 +31,15 @@ function NetSuiteInvoiceItemPreferenceSelectPage({policy}: WithPolicyConnections
const config = policy?.connections?.netsuite.options.config;
const {items} = policy?.connections?.netsuite.options.data ?? {};
- const selectedItem = useMemo(() => (items ?? []).find((item) => item.id === config?.invoiceItem), [items, config?.invoiceItem]);
+ const selectedItem = useMemo(() => findSelectedInvoiceItemWithDefaultSelect(items, config?.invoiceItem), [items, config?.invoiceItem]);
+
+ const selectedValue = Object.values(CONST.NETSUITE_INVOICE_ITEM_PREFERENCE).find((value) => value === config?.invoiceItemPreference) ?? CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.CREATE;
const data: MenuListItem[] = Object.values(CONST.NETSUITE_INVOICE_ITEM_PREFERENCE).map((postingPreference) => ({
value: postingPreference,
text: translate(`workspace.netsuite.invoiceItem.values.${postingPreference}.label`),
keyForList: postingPreference,
- isSelected: config?.invoiceItemPreference === postingPreference,
+ isSelected: selectedValue === postingPreference,
}));
const selectInvoicePreference = useCallback(
diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx
index 70ab4d1724b5..2732a928b6a4 100644
--- a/src/pages/workspace/categories/CategorySettingsPage.tsx
+++ b/src/pages/workspace/categories/CategorySettingsPage.tsx
@@ -142,13 +142,7 @@ function CategorySettingsPage({route, policyCategories, navigation}: CategorySet
description={translate(`workspace.categories.glCode`)}
onPress={() => {
if (!isControlPolicy(policy)) {
- Navigation.navigate(
- ROUTES.WORKSPACE_UPGRADE.getRoute(
- route.params.policyID,
- CONST.UPGRADE_FEATURE_INTRO_MAPPING.glAndPayrollCodes.alias,
- ROUTES.WORKSPACE_CATEGORY_GL_CODE.getRoute(route.params.policyID, policyCategory.name),
- ),
- );
+ Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(route.params.policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.glAndPayrollCodes.alias));
return;
}
Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_GL_CODE.getRoute(route.params.policyID, policyCategory.name));
@@ -162,13 +156,7 @@ function CategorySettingsPage({route, policyCategories, navigation}: CategorySet
description={translate(`workspace.categories.payrollCode`)}
onPress={() => {
if (!isControlPolicy(policy)) {
- Navigation.navigate(
- ROUTES.WORKSPACE_UPGRADE.getRoute(
- route.params.policyID,
- CONST.UPGRADE_FEATURE_INTRO_MAPPING.glAndPayrollCodes.alias,
- ROUTES.WORKSPACE_CATEGORY_PAYROLL_CODE.getRoute(route.params.policyID, policyCategory.name),
- ),
- );
+ Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(route.params.policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.glAndPayrollCodes.alias));
return;
}
Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_PAYROLL_CODE.getRoute(route.params.policyID, policyCategory.name));
diff --git a/src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx b/src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx
index 2738b50e1901..f86a5a1dbb73 100644
--- a/src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx
+++ b/src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx
@@ -81,7 +81,6 @@ function CreateDistanceRatePage({policy, route}: CreateDistanceRatePageProps) {
shouldHideFixErrorsAlert
submitFlexEnabled={false}
submitButtonStyles={[styles.mh5, styles.mt0]}
- disablePressOnEnter={false}
>
-
+
{translate('workspace.expensifyCard.disclaimer')}
diff --git a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx
index 73865be72dbc..30808b32e478 100644
--- a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx
+++ b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx
@@ -73,7 +73,7 @@ function WorkspaceEditCardLimitPage({route}: WorkspaceEditCardLimitPageProps) {
const submit = (values: FormOnyxValues) => {
const currentLimit = card.nameValuePairs?.limit ?? 0;
- const currentSpend = currentLimit - card.availableSpend;
+ const currentSpend = currentLimit - (card.availableSpend ?? 0);
const newLimit = Number(values[INPUT_IDS.LIMIT]) * 100;
const newAvailableSpend = newLimit - currentSpend;
diff --git a/src/pages/workspace/card/issueNew/AssigneeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx
similarity index 100%
rename from src/pages/workspace/card/issueNew/AssigneeStep.tsx
rename to src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx
diff --git a/src/pages/workspace/card/issueNew/CardNameStep.tsx b/src/pages/workspace/expensifyCard/issueNew/CardNameStep.tsx
similarity index 96%
rename from src/pages/workspace/card/issueNew/CardNameStep.tsx
rename to src/pages/workspace/expensifyCard/issueNew/CardNameStep.tsx
index 58b0748e438a..556f512c725d 100644
--- a/src/pages/workspace/card/issueNew/CardNameStep.tsx
+++ b/src/pages/workspace/expensifyCard/issueNew/CardNameStep.tsx
@@ -75,8 +75,7 @@ function CardNameStep() {
{translate('workspace.card.issueNewCard.giveItName')}
{translate('workspace.card.issueNewCard.letsDoubleCheck')}
- {translate('workspace.card.issueNewCard.willBeReady')}
+ {translate('workspace.card.issueNewCard.willBeReady')}
{
+ Card.startIssueNewCardFlow(policy?.id ?? '-1');
+ }, [policy?.id]);
switch (currentStep) {
case CONST.EXPENSIFY_CARD.STEP.ASSIGNEE:
diff --git a/src/pages/workspace/card/issueNew/LimitStep.tsx b/src/pages/workspace/expensifyCard/issueNew/LimitStep.tsx
similarity index 96%
rename from src/pages/workspace/card/issueNew/LimitStep.tsx
rename to src/pages/workspace/expensifyCard/issueNew/LimitStep.tsx
index 5f97db54ff21..eb7c2e7d8e0f 100644
--- a/src/pages/workspace/card/issueNew/LimitStep.tsx
+++ b/src/pages/workspace/expensifyCard/issueNew/LimitStep.tsx
@@ -79,15 +79,15 @@ function LimitStep() {
{translate('workspace.card.issueNewCard.setLimit')}
setTypeSelected(value)}
sections={[{data}]}
shouldDebounceRowSelect
+ initiallyFocusedOptionKey={typeSelected}
+ shouldUpdateFocusedIndex
/>
diff --git a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage.tsx b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage.tsx
index af11a83f13f8..c8a2b3373230 100644
--- a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage.tsx
+++ b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage.tsx
@@ -78,7 +78,6 @@ function WorkspaceRatePage(props: WorkspaceRatePageProps) {
shouldHideFixErrorsAlert
submitFlexEnabled={false}
submitButtonStyles={[styles.mh5, styles.mt0]}
- disablePressOnEnter={false}
>
{({inputValues}) => (
diff --git a/src/pages/workspace/tags/TagSettingsPage.tsx b/src/pages/workspace/tags/TagSettingsPage.tsx
index 62330bd8c697..d73127593d37 100644
--- a/src/pages/workspace/tags/TagSettingsPage.tsx
+++ b/src/pages/workspace/tags/TagSettingsPage.tsx
@@ -72,13 +72,7 @@ function TagSettingsPage({route, policyTags, navigation}: TagSettingsPageProps)
const navigateToEditGlCode = () => {
if (!PolicyUtils.isControlPolicy(policy)) {
- Navigation.navigate(
- ROUTES.WORKSPACE_UPGRADE.getRoute(
- route.params.policyID,
- CONST.UPGRADE_FEATURE_INTRO_MAPPING.glCodes.alias,
- ROUTES.WORKSPACE_TAG_GL_CODE.getRoute(policy?.id ?? '', route.params.orderWeight, route.params.tagName),
- ),
- );
+ Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(route.params.policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.glCodes.alias));
return;
}
Navigation.navigate(ROUTES.WORKSPACE_TAG_GL_CODE.getRoute(route.params.policyID, route.params.orderWeight, currentPolicyTag.name));
diff --git a/src/pages/workspace/taxes/ValuePage.tsx b/src/pages/workspace/taxes/ValuePage.tsx
index 3632eb531e88..5b724d625e5b 100644
--- a/src/pages/workspace/taxes/ValuePage.tsx
+++ b/src/pages/workspace/taxes/ValuePage.tsx
@@ -80,7 +80,6 @@ function ValuePage({
validate={validateTaxValue}
onSubmit={submit}
enabledWhenOffline
- disablePressOnEnter={false}
shouldHideFixErrorsAlert
submitFlexEnabled={false}
submitButtonStyles={[styles.mh5, styles.mt0]}
diff --git a/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx b/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx
index 080b92c4e454..4bc1bcc1fcf8 100644
--- a/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx
+++ b/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx
@@ -84,7 +84,6 @@ function WorkspaceCreateTaxPage({
submitButtonText={translate('common.save')}
enabledWhenOffline
shouldValidateOnBlur={false}
- disablePressOnEnter={false}
>
void;
+ policyID: string;
};
-function UpgradeConfirmation({policyName, onConfirmUpgrade}: Props) {
+function UpgradeConfirmation({policyName, policyID}: Props) {
const {translate} = useLocalize();
const styles = useThemeStyles();
@@ -31,7 +31,7 @@ function UpgradeConfirmation({policyName, onConfirmUpgrade}: Props) {
>
}
shouldShowButton
- onButtonPress={onConfirmUpgrade}
+ onButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(policyID))}
buttonText={translate('workspace.upgrade.completed.gotIt')}
/>
);
diff --git a/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx b/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx
index c0041bddb1b5..69eb396e9e26 100644
--- a/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx
+++ b/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx
@@ -8,7 +8,6 @@ import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
-import {isControlPolicy} from '@libs/PolicyUtils';
import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
import CONST from '@src/CONST';
import * as Policy from '@src/libs/actions/Policy/Policy';
@@ -27,25 +26,15 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) {
const [policy] = useOnyx(`policy_${policyID}`);
const {isOffline} = useNetwork();
- const isUpgraded = React.useMemo(() => isControlPolicy(policy), [policy]);
-
if (!feature || !policy) {
return ;
}
const upgradeToCorporate = () => {
- Policy.upgradeToCorporate(policy.id, feature.name);
+ Policy.upgradeToCorporate(policy.id, feature.id);
};
- const confirmUpgrade = () => {
- switch (feature.id) {
- case CONST.UPGRADE_FEATURE_INTRO_MAPPING.reportFields.id:
- Policy.enablePolicyReportFields(policyID, true);
- return Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID), CONST.NAVIGATION.TYPE.UP);
- default:
- return route.params.backTo ? Navigation.navigate(route.params.backTo, CONST.NAVIGATION.TYPE.UP) : Navigation.goBack();
- }
- };
+ const isUpgraded = policy.type === CONST.POLICY.TYPE.CORPORATE;
return (
Navigation.goBack()}
+ onBackButtonPress={() => Navigation.goBack(route.params.backTo ?? ROUTES.WORKSPACE_PROFILE.getRoute(policyID))}
/>
{isUpgraded && (
)}
diff --git a/src/styles/index.ts b/src/styles/index.ts
index 3f0173ddf446..a3ad37923a01 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -115,7 +115,8 @@ const link = (theme: ThemeColors) =>
({
color: theme.link,
textDecorationColor: theme.link,
- ...FontUtils.fontFamily.platform.EXP_NEUE,
+ // We set fontFamily directly in order to avoid overriding fontWeight and fontStyle.
+ fontFamily: FontUtils.fontFamily.platform.EXP_NEUE.fontFamily,
} satisfies ViewStyle & MixedStyleDeclaration);
const baseCodeTagStyles = (theme: ThemeColors) =>
@@ -2761,10 +2762,15 @@ const styles = (theme: ThemeColors) =>
width: 110,
},
- workspaceUpgradeIntroBox: ({isExtraSmallScreenWidth}: WorkspaceUpgradeIntroBoxParams): ViewStyle => {
+ workspaceUpgradeIntroBox: ({isExtraSmallScreenWidth, isSmallScreenWidth}: WorkspaceUpgradeIntroBoxParams): ViewStyle => {
let paddingHorizontal = spacing.ph5;
let paddingVertical = spacing.pv5;
+ if (isSmallScreenWidth) {
+ paddingHorizontal = spacing.ph4;
+ paddingVertical = spacing.pv4;
+ }
+
if (isExtraSmallScreenWidth) {
paddingHorizontal = spacing.ph2;
paddingVertical = spacing.pv2;
diff --git a/src/types/form/SearchAdvancedFiltersForm.ts b/src/types/form/SearchAdvancedFiltersForm.ts
index 46808954f661..3c9a64a06976 100644
--- a/src/types/form/SearchAdvancedFiltersForm.ts
+++ b/src/types/form/SearchAdvancedFiltersForm.ts
@@ -3,6 +3,7 @@ import type Form from './Form';
const INPUT_IDS = {
TYPE: 'type',
+ STATUS: 'status',
DATE_AFTER: 'dateAfter',
DATE_BEFORE: 'dateBefore',
} as const;
@@ -15,6 +16,7 @@ type SearchAdvancedFiltersForm = Form<
[INPUT_IDS.TYPE]: string;
[INPUT_IDS.DATE_AFTER]: string;
[INPUT_IDS.DATE_BEFORE]: string;
+ [INPUT_IDS.STATUS]: string;
}
>;
diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts
index eb584fa73ab0..eac8d91c3651 100644
--- a/src/types/onyx/Card.ts
+++ b/src/types/onyx/Card.ts
@@ -14,7 +14,7 @@ type Card = {
bank: string;
/** Available amount to spend */
- availableSpend: number;
+ availableSpend?: number;
/** Domain name */
domainName: string;
diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts
index 06b31f242989..27a3e292e1d7 100644
--- a/src/types/onyx/OriginalMessage.ts
+++ b/src/types/onyx/OriginalMessage.ts
@@ -317,6 +317,12 @@ type OriginalMessageModifiedExpense = {
/** Old expense tax rate */
oldTaxRate?: string;
+ /** Edited expense reimbursable */
+ reimbursable?: string;
+
+ /** Old expense reimbursable */
+ oldReimbursable?: string;
+
/** Collection of accountIDs of users mentioned in expense report */
whisperedTo?: number[];
@@ -579,6 +585,12 @@ type OriginalMessageMap = {
[CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENT_SETUP]: never;
/** */
[CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENT_SETUP_REQUESTED]: never;
+ /** */
+ [CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED]: never;
+ /** */
+ [CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS]: never;
+ /** */
+ [CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED_VIRTUAL]: never;
} & OldDotOriginalMessageMap & {
[T in ValueOf]: OriginalMessageChangeLog;
} & {
diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts
index 10e5fe71ba66..72bb220b0215 100644
--- a/src/types/onyx/Policy.ts
+++ b/src/types/onyx/Policy.ts
@@ -183,6 +183,12 @@ type ConnectionLastSync = {
/** Where did the connection's last sync job come from */
source: JobSourceValues;
+
+ /**
+ * Sometimes we'll have a connection that is not connected, but the connection object is still present, so we can
+ * show an error message
+ */
+ isConnected?: boolean;
};
/** Financial account (bank account, debit card, etc) */
@@ -1577,6 +1583,9 @@ export type {
NetSuiteCustomListSource,
NetSuiteCustomFieldMapping,
NetSuiteAccount,
+ NetSuiteVendor,
+ InvoiceItem,
+ NetSuiteTaxAccount,
NetSuiteCustomFormIDOptions,
NetSuiteCustomFormID,
SageIntacctMappingValue,
diff --git a/tests/unit/EmojiTest.ts b/tests/unit/EmojiTest.ts
index 84442db92553..6af24553bee9 100644
--- a/tests/unit/EmojiTest.ts
+++ b/tests/unit/EmojiTest.ts
@@ -153,7 +153,30 @@ describe('EmojiTest', () => {
});
it('correct suggests emojis accounting for keywords', () => {
- const thumbEmojis: Emoji[] = [
+ const thumbEmojisEn: Emoji[] = [
+ {
+ name: 'hand_with_index_finger_and_thumb_crossed',
+ code: '🫰',
+ types: ['🫰🏿', '🫰🏾', '🫰🏽', '🫰🏼', '🫰🏻'],
+ },
+ {
+ code: '👍',
+ name: '+1',
+ types: ['👍🏿', '👍🏾', '👍🏽', '👍🏼', '👍🏻'],
+ },
+ {
+ code: '👎',
+ name: '-1',
+ types: ['👎🏿', '👎🏾', '👎🏽', '👎🏼', '👎🏻'],
+ },
+ ];
+
+ const thumbEmojisEs: Emoji[] = [
+ {
+ name: 'mano_con_dedos_cruzados',
+ code: '🫰',
+ types: ['🫰🏿', '🫰🏾', '🫰🏽', '🫰🏼', '🫰🏻'],
+ },
{
code: '👍',
name: '+1',
@@ -166,11 +189,16 @@ describe('EmojiTest', () => {
},
];
- expect(EmojiUtils.suggestEmojis(':thumb', 'en')).toEqual(thumbEmojis);
+ expect(EmojiUtils.suggestEmojis(':thumb', 'en')).toEqual(thumbEmojisEn);
- expect(EmojiUtils.suggestEmojis(':thumb', 'es')).toEqual(thumbEmojis);
+ expect(EmojiUtils.suggestEmojis(':thumb', 'es')).toEqual(thumbEmojisEs);
expect(EmojiUtils.suggestEmojis(':pulgar', 'es')).toEqual([
+ {
+ name: 'mano_con_dedos_cruzados',
+ code: '🫰',
+ types: ['🫰🏿', '🫰🏾', '🫰🏽', '🫰🏼', '🫰🏻'],
+ },
{
code: '🤙',
name: 'mano_llámame',
diff --git a/workflow_tests/assertions/platformDeployAssertions.ts b/workflow_tests/assertions/platformDeployAssertions.ts
index af80e9f2beb3..31db7167c452 100644
--- a/workflow_tests/assertions/platformDeployAssertions.ts
+++ b/workflow_tests/assertions/platformDeployAssertions.ts
@@ -154,7 +154,6 @@ function assertIOSJobExecuted(workflowResult: Step[], didExecute = true, isProdu
createStepAssertion('Cache Pod dependencies', true, null, 'IOS', 'Cache Pod dependencies', [
{key: 'path', value: 'ios/Pods'},
{key: 'key', value: 'Linux-pods-cache-'},
- {key: 'restore-keys', value: 'Linux-pods-cache-'},
]),
createStepAssertion('Compare Podfile.lock and Manifest.lock', true, null, 'IOS', 'Compare Podfile.lock and Manifest.lock'),
createStepAssertion('Install cocoapods', true, null, 'IOS', 'Installing cocoapods', [
diff --git a/workflow_tests/assertions/testBuildAssertions.ts b/workflow_tests/assertions/testBuildAssertions.ts
index 72b36e47639a..8e1ba1a439d5 100644
--- a/workflow_tests/assertions/testBuildAssertions.ts
+++ b/workflow_tests/assertions/testBuildAssertions.ts
@@ -158,7 +158,6 @@ function assertIOSJobExecuted(workflowResult: Step[], ref = '', didExecute = tru
createStepAssertion('Cache Pod dependencies', true, null, 'IOS', 'Cache Pod dependencies', [
{key: 'path', value: 'ios/Pods'},
{key: 'key', value: 'Linux-pods-cache-'},
- {key: 'restore-keys', value: 'Linux-pods-cache-'},
]),
createStepAssertion('Compare Podfile.lock and Manifest.lock', true, null, 'IOS', 'Compare Podfile.lock and Manifest.lock'),
createStepAssertion(
diff --git a/workflow_tests/mocks/platformDeployMocks.ts b/workflow_tests/mocks/platformDeployMocks.ts
index 965c45b01ade..38d4cebb86da 100644
--- a/workflow_tests/mocks/platformDeployMocks.ts
+++ b/workflow_tests/mocks/platformDeployMocks.ts
@@ -110,7 +110,7 @@ const PLATFORM_DEPLOY__IOS__CHECKOUT__STEP_MOCK = createMockStep('Checkout', 'Ch
const PLATFORM_DEPLOY__IOS__CONFIGURE_MAPBOX_SDK__STEP_MOCK = createMockStep('Configure MapBox SDK', 'Configure MapBox SDK', 'IOS');
const PLATFORM_DEPLOY__IOS__SETUP_NODE__STEP_MOCK = createMockStep('Setup Node', 'Setting up Node', 'IOS');
const PLATFORM_DEPLOY__IOS__SETUP_RUBY__STEP_MOCK = createMockStep('Setup Ruby', 'Setting up Ruby', 'IOS', ['ruby-version', 'bundler-cache']);
-const PLATFORM_DEPLOY__IOS__CACHE_POD_DEPENDENCIES__STEP_MOCK = createMockStep('Cache Pod dependencies', 'Cache Pod dependencies', 'IOS', ['path', 'key', 'restore-keys'], [], {
+const PLATFORM_DEPLOY__IOS__CACHE_POD_DEPENDENCIES__STEP_MOCK = createMockStep('Cache Pod dependencies', 'Cache Pod dependencies', 'IOS', ['path', 'key'], [], {
'cache-hit': false,
});
const PLATFORM_DEPLOY__IOS__COMPARE_PODFILE_AND_MANIFEST__STEP_MOCK = createMockStep('Compare Podfile.lock and Manifest.lock', 'Compare Podfile.lock and Manifest.lock', 'IOS', [], [], {
diff --git a/workflow_tests/mocks/testBuildMocks.ts b/workflow_tests/mocks/testBuildMocks.ts
index cfd194d07185..643e57ddf675 100644
--- a/workflow_tests/mocks/testBuildMocks.ts
+++ b/workflow_tests/mocks/testBuildMocks.ts
@@ -110,7 +110,7 @@ const TESTBUILD__IOS__CREATE_ENV_ADHOC__STEP_MOCK = createMockStep(
const TESTBUILD__IOS__SETUP_NODE__STEP_MOCK = createMockStep('Setup Node', 'Setup Node', 'IOS', [], []);
const TESTBUILD__IOS__SETUP_XCODE__STEP_MOCK = createMockStep('Setup XCode', 'Setup XCode', 'IOS', [], []);
const TESTBUILD__IOS__SETUP_RUBY__STEP_MOCK = createMockStep('Setup Ruby', 'Setup Ruby', 'IOS', ['ruby-version', 'bundler-cache'], []);
-const TESTBUILD__IOS__CACHE_POD_DEPENDENCIES__STEP_MOCK = createMockStep('Cache Pod dependencies', 'Cache Pod dependencies', 'IOS', ['path', 'key', 'restore-keys'], [], {
+const TESTBUILD__IOS__CACHE_POD_DEPENDENCIES__STEP_MOCK = createMockStep('Cache Pod dependencies', 'Cache Pod dependencies', 'IOS', ['path', 'key'], [], {
'cache-hit': false,
});
const TESTBUILD__IOS__COMPARE_PODFILE_AND_MANIFEST__STEP_MOCK = createMockStep('Compare Podfile.lock and Manifest.lock', 'Compare Podfile.lock and Manifest.lock', 'IOS', [], [], {