diff --git a/.github/ISSUE_TEMPLATE/Standard.md b/.github/ISSUE_TEMPLATE/Standard.md
index 5c96d8736bcd..663c6004a534 100644
--- a/.github/ISSUE_TEMPLATE/Standard.md
+++ b/.github/ISSUE_TEMPLATE/Standard.md
@@ -43,8 +43,10 @@ Which of our officially supported platforms is this issue occurring on?
## Screenshots/Videos
-Add any screenshot/video evidence
+
+ Add any screenshot/video evidence
+
[View all open jobs on GitHub](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 64b903fdbe24..ae7625810a14 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -110,8 +110,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1009004301
- versionName "9.0.43-1"
+ versionCode 1009004400
+ versionName "9.0.44-0"
// 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/desktop/package-lock.json b/desktop/package-lock.json
index 152ad1a4c5ba..4803d189074c 100644
--- a/desktop/package-lock.json
+++ b/desktop/package-lock.json
@@ -9,7 +9,7 @@
"dependencies": {
"electron-context-menu": "^2.3.0",
"electron-log": "^4.4.8",
- "electron-updater": "^6.3.4",
+ "electron-updater": "^6.3.5",
"mime-types": "^2.1.35",
"node-machine-id": "^1.1.12"
},
@@ -59,9 +59,10 @@
}
},
"node_modules/builder-util-runtime": {
- "version": "9.2.5",
- "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.5.tgz",
- "integrity": "sha512-HjIDfhvqx/8B3TDN4GbABQcgpewTU4LMRTQPkVpKYV3lsuxEJoIfvg09GyWTNmfVNSUAYf+fbTN//JX4TH20pg==",
+ "version": "9.2.6",
+ "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.6.tgz",
+ "integrity": "sha512-sCNP0uykVxn1vdYdPGW3+8D4kMOF8PR9eL5HgUcQXhpoIoUGxdD03yQgZcuMQt4iGLKb5DD62evElwGq1ylEag==",
+ "license": "MIT",
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
@@ -102,11 +103,12 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/debug": {
- "version": "4.3.6",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
- "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
"dependencies": {
- "ms": "2.1.2"
+ "ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
@@ -154,12 +156,12 @@
"integrity": "sha512-QQ4GvrXO+HkgqqEOYbi+DHL7hj5JM+nHi/j+qrN9zeeXVKy8ZABgbu4CnG+BBqDZ2+tbeq9tUC4DZfIWFU5AZA=="
},
"node_modules/electron-updater": {
- "version": "6.3.4",
- "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.4.tgz",
- "integrity": "sha512-uZUo7p1Y53G4tl6Cgw07X1yF8Jlz6zhaL7CQJDZ1fVVkOaBfE2cWtx80avwDVi8jHp+I/FWawrMgTAeCCNIfAg==",
+ "version": "6.3.5",
+ "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.5.tgz",
+ "integrity": "sha512-8bUtfe3UHnM+D7971N/zo3NCG2TIHuE4GFUtHRVmVOQg0prXEd+uZoVekakdPiTDkkXJv4b09CZMw/ZJJfag1A==",
"license": "MIT",
"dependencies": {
- "builder-util-runtime": "9.2.5",
+ "builder-util-runtime": "9.2.6",
"fs-extra": "^10.1.0",
"js-yaml": "^4.1.0",
"lazy-val": "^1.0.5",
@@ -304,9 +306,10 @@
}
},
"node_modules/ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
},
"node_modules/node-machine-id": {
"version": "1.1.12",
@@ -335,7 +338,8 @@
"node_modules/sax": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
- "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="
+ "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
+ "license": "ISC"
},
"node_modules/semver": {
"version": "7.6.3",
@@ -465,9 +469,9 @@
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="
},
"builder-util-runtime": {
- "version": "9.2.5",
- "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.5.tgz",
- "integrity": "sha512-HjIDfhvqx/8B3TDN4GbABQcgpewTU4LMRTQPkVpKYV3lsuxEJoIfvg09GyWTNmfVNSUAYf+fbTN//JX4TH20pg==",
+ "version": "9.2.6",
+ "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.6.tgz",
+ "integrity": "sha512-sCNP0uykVxn1vdYdPGW3+8D4kMOF8PR9eL5HgUcQXhpoIoUGxdD03yQgZcuMQt4iGLKb5DD62evElwGq1ylEag==",
"requires": {
"debug": "^4.3.4",
"sax": "^1.2.4"
@@ -496,11 +500,11 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"debug": {
- "version": "4.3.6",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
- "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"requires": {
- "ms": "2.1.2"
+ "ms": "^2.1.3"
}
},
"electron-context-menu": {
@@ -534,11 +538,11 @@
"integrity": "sha512-QQ4GvrXO+HkgqqEOYbi+DHL7hj5JM+nHi/j+qrN9zeeXVKy8ZABgbu4CnG+BBqDZ2+tbeq9tUC4DZfIWFU5AZA=="
},
"electron-updater": {
- "version": "6.3.4",
- "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.4.tgz",
- "integrity": "sha512-uZUo7p1Y53G4tl6Cgw07X1yF8Jlz6zhaL7CQJDZ1fVVkOaBfE2cWtx80avwDVi8jHp+I/FWawrMgTAeCCNIfAg==",
+ "version": "6.3.5",
+ "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.5.tgz",
+ "integrity": "sha512-8bUtfe3UHnM+D7971N/zo3NCG2TIHuE4GFUtHRVmVOQg0prXEd+uZoVekakdPiTDkkXJv4b09CZMw/ZJJfag1A==",
"requires": {
- "builder-util-runtime": "9.2.5",
+ "builder-util-runtime": "9.2.6",
"fs-extra": "^10.1.0",
"js-yaml": "^4.1.0",
"lazy-val": "^1.0.5",
@@ -651,9 +655,9 @@
"integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE="
},
"ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node-machine-id": {
"version": "1.1.12",
diff --git a/desktop/package.json b/desktop/package.json
index 6c2158a74978..b8e1e175b0fe 100644
--- a/desktop/package.json
+++ b/desktop/package.json
@@ -6,7 +6,7 @@
"dependencies": {
"electron-context-menu": "^2.3.0",
"electron-log": "^4.4.8",
- "electron-updater": "^6.3.4",
+ "electron-updater": "^6.3.5",
"mime-types": "^2.1.35",
"node-machine-id": "^1.1.12"
},
diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-Personal-US-Bank-Account.md b/docs/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-Personal-Bank-Account.md
similarity index 97%
rename from docs/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-Personal-US-Bank-Account.md
rename to docs/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-Personal-Bank-Account.md
index 402337140419..a7b7ed1c4f4f 100644
--- a/docs/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-Personal-US-Bank-Account.md
+++ b/docs/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-Personal-Bank-Account.md
@@ -1,5 +1,5 @@
---
-title: Connect personal U.S. bank account
+title: Connect personal bank account
description: Receive reimbursements for expense reports submitted to your employer
---
diff --git a/docs/articles/expensify-classic/workspaces/Enable-per-diem-expenses.md b/docs/articles/expensify-classic/workspaces/Enable-per-diem-expenses.md
index 2d2f1b5afddc..87b03e2e69ee 100644
--- a/docs/articles/expensify-classic/workspaces/Enable-per-diem-expenses.md
+++ b/docs/articles/expensify-classic/workspaces/Enable-per-diem-expenses.md
@@ -16,6 +16,7 @@ To enable and set per diem rates,
6. Create a .csv, .txt, .xls, or .xlsx spreadsheet containing four columns: Destination, Sub-rate, Amount, and Currency. You’ll want a different row for each location that an employee may travel to, which may include states and/or countries to help account for cost differences across various locations. Here are some example templates you can use:
- [Germany rates]({{site.url}}/assets/Files/Germany-per-diem.csv)
- [Sweden rates]({{site.url}}/assets/Files/Sweden-per-diem.csv)
+ - [Finland rates]({{site.url}}/assets/Files/Finland-per-diem.csv)
- [South Africa single rates]({{site.url}}/assets/Files/South-Africa-per-diem.csv)
7. Click **Import from spreadsheet**.
8. Click **Upload** to select your spreadsheet.
diff --git a/docs/articles/new-expensify/workspaces/Set-up-workflows.md b/docs/articles/new-expensify/workspaces/Set-up-workflows.md
index 07d770d3ad50..7c44e3792122 100644
--- a/docs/articles/new-expensify/workspaces/Set-up-workflows.md
+++ b/docs/articles/new-expensify/workspaces/Set-up-workflows.md
@@ -17,6 +17,10 @@ Workflows are available for Collect and Control workspaces. Additionally, you mu
4. Click **More features** in the left menu.
5. Under the Spend section, enable the Workflows toggle.
+![Click Account Settings > Workspaces > click on the workspace]({{site.url}}/assets/images/ExpensifyHelp-Workflows-1.png){:width="100%"}
+
+![Click More Features > Enable Workflows]({{site.url}}/assets/images/ExpensifyHelp-Workflows-2.png){:width="100%"}
+
# Select workflows
You can choose to require additional approvals and/or allow delayed submissions.
@@ -29,6 +33,8 @@ You can choose to require additional approvals and/or allow delayed submissions.
-- With delayed submission **enabled**, all reimbursable and non-reimbursable expenses will be submitted at a designated frequency.
-- If delay submission is **disabled**, all reimbursable and non-reimbursable expenses are submitted instantly.
+![Enable workflow features]({{site.url}}/assets/images/ExpensifyHelp-Workflows-3.png){:width="100%"}
+
# Set up payment account
The payments section is where you’ll set up your business bank account for payments of expenses and invoices.
diff --git a/docs/assets/Files/Finland-per-diem.csv b/docs/assets/Files/Finland-per-diem.csv
new file mode 100644
index 000000000000..beb7abc5ef62
--- /dev/null
+++ b/docs/assets/Files/Finland-per-diem.csv
@@ -0,0 +1,1071 @@
+Destination,Amount,Currency,Subrate
+*Exceptional,12.75,EUR,1 meal (no destination)
+*Exceptional,15.5,EUR,2+ Meals (no destination)
+*Exceptional,18,EUR,Travel (no destination)
+*Finland,51,EUR,Full day (over 10 hours)
+*Finland,24,EUR,Partial day (over 6 hours)
+*Finland,51,EUR,Final day (over 6 hours)
+*Finland,24,EUR,Final day (over 2 hours)
+*Finland,16,EUR,Night Travel supplement
+*Finland,-24,EUR,1 meal
+*Finland,-51,EUR,2+ Meals
+Afghanistan,59,EUR,Full day (over 24 hours)
+Afghanistan,59,EUR,Final day (over 10 hours)
+Afghanistan,29.5,EUR,Final day (over 2 hours)
+Afghanistan,-29.5,EUR,2+ Meals
+Afghanistan,16,EUR,Night Travel supplement
+Albania,81,EUR,Full day (over 24 hours)
+Albania,81,EUR,Final day (over 10 hours)
+Albania,40.5,EUR,Final day (over 2 hours)
+Albania,-40.5,EUR,2+ Meals
+Albania,16,EUR,Night Travel supplement
+Algeria,78,EUR,Full day (over 24 hours)
+Algeria,78,EUR,Final day (over 10 hours)
+Algeria,39,EUR,Final day (over 2 hours)
+Algeria,-39,EUR,2+ Meals
+Algeria,16,EUR,Night Travel supplement
+Andorra,63,EUR,Full day (over 24 hours)
+Andorra,63,EUR,Final day (over 10 hours)
+Andorra,31.5,EUR,Final day (over 2 hours)
+Andorra,-31.5,EUR,2+ Meals
+Andorra,16,EUR,Night Travel supplement
+Angola,71,EUR,Full day (over 24 hours)
+Angola,71,EUR,Final day (over 10 hours)
+Angola,35.5,EUR,Final day (over 2 hours)
+Angola,-35.5,EUR,2+ Meals
+Angola,16,EUR,Night Travel supplement
+Antiqua and Barbuda,94,EUR,Full day (over 24 hours)
+Antiqua and Barbuda,94,EUR,Final day (over 10 hours)
+Antiqua and Barbuda,47,EUR,Final day (over 2 hours)
+Antiqua and Barbuda,-47,EUR,2+ Meals
+Antiqua and Barbuda,16,EUR,Night Travel supplement
+"Any other country, not specified above",52,EUR,Full day (over 24 hours)
+"Any other country, not specified above",52,EUR,Final day (over 10 hours)
+"Any other country, not specified above",26,EUR,Final day (over 2 hours)
+"Any other country, not specified above",-26,EUR,2+ Meals
+"Any other country, not specified above",16,EUR,Night Travel supplement
+Argentina,38,EUR,Full day (over 24 hours)
+Argentina,38,EUR,Final day (over 10 hours)
+Argentina,19,EUR,Final day (over 2 hours)
+Argentina,-19,EUR,2+ Meals
+Argentina,16,EUR,Night Travel supplement
+Armenia,61,EUR,Full day (over 24 hours)
+Armenia,61,EUR,Final day (over 10 hours)
+Armenia,30.5,EUR,Final day (over 2 hours)
+Armenia,-30.5,EUR,2+ Meals
+Armenia,16,EUR,Night Travel supplement
+Aruba,70,EUR,Full day (over 24 hours)
+Aruba,70,EUR,Final day (over 10 hours)
+Aruba,35,EUR,Final day (over 2 hours)
+Aruba,-35,EUR,2+ Meals
+Aruba,16,EUR,Night Travel supplement
+Australia,74,EUR,Full day (over 24 hours)
+Australia,74,EUR,Final day (over 10 hours)
+Australia,37,EUR,Final day (over 2 hours)
+Australia,-37,EUR,2+ Meals
+Australia,16,EUR,Night Travel supplement
+Austria,80,EUR,Full day (over 24 hours)
+Austria,80,EUR,Final day (over 10 hours)
+Austria,40,EUR,Final day (over 2 hours)
+Austria,-40,EUR,2+ Meals
+Austria,16,EUR,Night Travel supplement
+Azerbaidzhan,70,EUR,Full day (over 24 hours)
+Azerbaidzhan,70,EUR,Final day (over 10 hours)
+Azerbaidzhan,35,EUR,Final day (over 2 hours)
+Azerbaidzhan,-35,EUR,2+ Meals
+Azerbaidzhan,16,EUR,Night Travel supplement
+Azores,69,EUR,Full day (over 24 hours)
+Azores,69,EUR,Final day (over 10 hours)
+Azores,34.5,EUR,Final day (over 2 hours)
+Azores,-34.5,EUR,2+ Meals
+Azores,16,EUR,Night Travel supplement
+Bahamas,91,EUR,Full day (over 24 hours)
+Bahamas,91,EUR,Final day (over 10 hours)
+Bahamas,45.5,EUR,Final day (over 2 hours)
+Bahamas,-45.5,EUR,2+ Meals
+Bahamas,16,EUR,Night Travel supplement
+Bahrain,80,EUR,Full day (over 24 hours)
+Bahrain,80,EUR,Final day (over 10 hours)
+Bahrain,40,EUR,Final day (over 2 hours)
+Bahrain,-40,EUR,2+ Meals
+Bahrain,16,EUR,Night Travel supplement
+Bangladesh,57,EUR,Full day (over 24 hours)
+Bangladesh,57,EUR,Final day (over 10 hours)
+Bangladesh,28.5,EUR,Final day (over 2 hours)
+Bangladesh,-28.5,EUR,2+ Meals
+Bangladesh,16,EUR,Night Travel supplement
+Barbados,83,EUR,Full day (over 24 hours)
+Barbados,83,EUR,Final day (over 10 hours)
+Barbados,41.5,EUR,Final day (over 2 hours)
+Barbados,-41.5,EUR,2+ Meals
+Barbados,16,EUR,Night Travel supplement
+Belarus,63,EUR,Full day (over 24 hours)
+Belarus,63,EUR,Final day (over 10 hours)
+Belarus,31.5,EUR,Final day (over 2 hours)
+Belarus,-31.5,EUR,2+ Meals
+Belarus,16,EUR,Night Travel supplement
+Belgium,77,EUR,Full day (over 24 hours)
+Belgium,77,EUR,Final day (over 10 hours)
+Belgium,38.5,EUR,Final day (over 2 hours)
+Belgium,-38.5,EUR,2+ Meals
+Belgium,16,EUR,Night Travel supplement
+Belize,52,EUR,Full day (over 24 hours)
+Belize,52,EUR,Final day (over 10 hours)
+Belize,26,EUR,Final day (over 2 hours)
+Belize,-26,EUR,2+ Meals
+Belize,16,EUR,Night Travel supplement
+Benin,47,EUR,Full day (over 24 hours)
+Benin,47,EUR,Final day (over 10 hours)
+Benin,23.5,EUR,Final day (over 2 hours)
+Benin,-23.5,EUR,2+ Meals
+Benin,16,EUR,Night Travel supplement
+Bermuda,90,EUR,Full day (over 24 hours)
+Bermuda,90,EUR,Final day (over 10 hours)
+Bermuda,45,EUR,Final day (over 2 hours)
+Bermuda,-45,EUR,2+ Meals
+Bermuda,16,EUR,Night Travel supplement
+Bhutan,49,EUR,Full day (over 24 hours)
+Bhutan,49,EUR,Final day (over 10 hours)
+Bhutan,24.5,EUR,Final day (over 2 hours)
+Bhutan,-24.5,EUR,2+ Meals
+Bhutan,16,EUR,Night Travel supplement
+Bolivia,48,EUR,Full day (over 24 hours)
+Bolivia,48,EUR,Final day (over 10 hours)
+Bolivia,24,EUR,Final day (over 2 hours)
+Bolivia,-24,EUR,2+ Meals
+Bolivia,16,EUR,Night Travel supplement
+Bosnia and Hercegovina,54,EUR,Full day (over 24 hours)
+Bosnia and Hercegovina,54,EUR,Final day (over 10 hours)
+Bosnia and Hercegovina,27,EUR,Final day (over 2 hours)
+Bosnia and Hercegovina,-27,EUR,2+ Meals
+Bosnia and Hercegovina,16,EUR,Night Travel supplement
+Botswana,41,EUR,Full day (over 24 hours)
+Botswana,41,EUR,Final day (over 10 hours)
+Botswana,20.5,EUR,Final day (over 2 hours)
+Botswana,-20.5,EUR,2+ Meals
+Botswana,16,EUR,Night Travel supplement
+Brazil,80,EUR,Full day (over 24 hours)
+Brazil,80,EUR,Final day (over 10 hours)
+Brazil,40,EUR,Final day (over 2 hours)
+Brazil,-40,EUR,2+ Meals
+Brazil,16,EUR,Night Travel supplement
+Brunei,45,EUR,Full day (over 24 hours)
+Brunei,45,EUR,Final day (over 10 hours)
+Brunei,22.5,EUR,Final day (over 2 hours)
+Brunei,-22.5,EUR,2+ Meals
+Brunei,16,EUR,Night Travel supplement
+Bulgaria,64,EUR,Full day (over 24 hours)
+Bulgaria,64,EUR,Final day (over 10 hours)
+Bulgaria,32,EUR,Final day (over 2 hours)
+Bulgaria,-32,EUR,2+ Meals
+Bulgaria,16,EUR,Night Travel supplement
+Burkina Faso,40,EUR,Full day (over 24 hours)
+Burkina Faso,40,EUR,Final day (over 10 hours)
+Burkina Faso,20,EUR,Final day (over 2 hours)
+Burkina Faso,-20,EUR,2+ Meals
+Burkina Faso,16,EUR,Night Travel supplement
+Burundi,46,EUR,Full day (over 24 hours)
+Burundi,46,EUR,Final day (over 10 hours)
+Burundi,23,EUR,Final day (over 2 hours)
+Burundi,-23,EUR,2+ Meals
+Burundi,16,EUR,Night Travel supplement
+Cambodia,67,EUR,Full day (over 24 hours)
+Cambodia,67,EUR,Final day (over 10 hours)
+Cambodia,33.5,EUR,Final day (over 2 hours)
+Cambodia,-33.5,EUR,2+ Meals
+Cambodia,16,EUR,Night Travel supplement
+Cameroon,59,EUR,Full day (over 24 hours)
+Cameroon,59,EUR,Final day (over 10 hours)
+Cameroon,29.5,EUR,Final day (over 2 hours)
+Cameroon,-29.5,EUR,2+ Meals
+Cameroon,16,EUR,Night Travel supplement
+Canada,82,EUR,Full day (over 24 hours)
+Canada,82,EUR,Final day (over 10 hours)
+Canada,41,EUR,Final day (over 2 hours)
+Canada,-41,EUR,2+ Meals
+Canada,16,EUR,Night Travel supplement
+Canary Islands,71,EUR,Full day (over 24 hours)
+Canary Islands,71,EUR,Final day (over 10 hours)
+Canary Islands,35.5,EUR,Final day (over 2 hours)
+Canary Islands,-35.5,EUR,2+ Meals
+Canary Islands,16,EUR,Night Travel supplement
+Cape Verde,45,EUR,Full day (over 24 hours)
+Cape Verde,45,EUR,Final day (over 10 hours)
+Cape Verde,22.5,EUR,Final day (over 2 hours)
+Cape Verde,-22.5,EUR,2+ Meals
+Cape Verde,16,EUR,Night Travel supplement
+Central African Republic,101,EUR,Full day (over 24 hours)
+Central African Republic,101,EUR,Final day (over 10 hours)
+Central African Republic,50.5,EUR,Final day (over 2 hours)
+Central African Republic,-50.5,EUR,2+ Meals
+Central African Republic,16,EUR,Night Travel supplement
+Chad,47,EUR,Full day (over 24 hours)
+Chad,47,EUR,Final day (over 10 hours)
+Chad,23.5,EUR,Final day (over 2 hours)
+Chad,-23.5,EUR,2+ Meals
+Chad,16,EUR,Night Travel supplement
+Chile,56,EUR,Full day (over 24 hours)
+Chile,56,EUR,Final day (over 10 hours)
+Chile,28,EUR,Final day (over 2 hours)
+Chile,-28,EUR,2+ Meals
+Chile,16,EUR,Night Travel supplement
+China,74,EUR,Full day (over 24 hours)
+China,74,EUR,Final day (over 10 hours)
+China,37,EUR,Final day (over 2 hours)
+China,-37,EUR,2+ Meals
+China,16,EUR,Night Travel supplement
+Colombia,64,EUR,Full day (over 24 hours)
+Colombia,64,EUR,Final day (over 10 hours)
+Colombia,32,EUR,Final day (over 2 hours)
+Colombia,-32,EUR,2+ Meals
+Colombia,16,EUR,Night Travel supplement
+Comoros,42,EUR,Full day (over 24 hours)
+Comoros,42,EUR,Final day (over 10 hours)
+Comoros,21,EUR,Final day (over 2 hours)
+Comoros,-21,EUR,2+ Meals
+Comoros,16,EUR,Night Travel supplement
+Congo (Congo-Brazzaville),64,EUR,Full day (over 24 hours)
+Congo (Congo-Brazzaville),64,EUR,Final day (over 10 hours)
+Congo (Congo-Brazzaville),32,EUR,Final day (over 2 hours)
+Congo (Congo-Brazzaville),-32,EUR,2+ Meals
+Congo (Congo-Brazzaville),16,EUR,Night Travel supplement
+"Congo, Democratic Republic of (Congo-Kinshasa)",51,EUR,Full day (over 24 hours)
+"Congo, Democratic Republic of (Congo-Kinshasa)",51,EUR,Final day (over 10 hours)
+"Congo, Democratic Republic of (Congo-Kinshasa)",25.5,EUR,Final day (over 2 hours)
+"Congo, Democratic Republic of (Congo-Kinshasa)",-25.5,EUR,2+ Meals
+"Congo, Democratic Republic of (Congo-Kinshasa)",16,EUR,Night Travel supplement
+Cook Islands,70,EUR,Full day (over 24 hours)
+Cook Islands,70,EUR,Final day (over 10 hours)
+Cook Islands,35,EUR,Final day (over 2 hours)
+Cook Islands,-35,EUR,2+ Meals
+Cook Islands,16,EUR,Night Travel supplement
+Costa Rica,65,EUR,Full day (over 24 hours)
+Costa Rica,65,EUR,Final day (over 10 hours)
+Costa Rica,32.5,EUR,Final day (over 2 hours)
+Costa Rica,-32.5,EUR,2+ Meals
+Costa Rica,16,EUR,Night Travel supplement
+"Côte d’Ivoire, Ivory Coast",80,EUR,Full day (over 24 hours)
+"Côte d’Ivoire, Ivory Coast",80,EUR,Final day (over 10 hours)
+"Côte d’Ivoire, Ivory Coast",40,EUR,Final day (over 2 hours)
+"Côte d’Ivoire, Ivory Coast",-40,EUR,2+ Meals
+"Côte d’Ivoire, Ivory Coast",16,EUR,Night Travel supplement
+Croatia,69,EUR,Full day (over 24 hours)
+Croatia,69,EUR,Final day (over 10 hours)
+Croatia,34.5,EUR,Final day (over 2 hours)
+Croatia,-34.5,EUR,2+ Meals
+Croatia,16,EUR,Night Travel supplement
+Cuba,68,EUR,Full day (over 24 hours)
+Cuba,68,EUR,Final day (over 10 hours)
+Cuba,34,EUR,Final day (over 2 hours)
+Cuba,-34,EUR,2+ Meals
+Cuba,16,EUR,Night Travel supplement
+Curaçao,58,EUR,Full day (over 24 hours)
+Curaçao,58,EUR,Final day (over 10 hours)
+Curaçao,29,EUR,Final day (over 2 hours)
+Curaçao,-29,EUR,2+ Meals
+Curaçao,16,EUR,Night Travel supplement
+Cyprus,65,EUR,Full day (over 24 hours)
+Cyprus,65,EUR,Final day (over 10 hours)
+Cyprus,32.5,EUR,Final day (over 2 hours)
+Cyprus,-32.5,EUR,2+ Meals
+Cyprus,16,EUR,Night Travel supplement
+Czech Republic,89,EUR,Full day (over 24 hours)
+Czech Republic,89,EUR,Final day (over 10 hours)
+Czech Republic,44.5,EUR,Final day (over 2 hours)
+Czech Republic,-44.5,EUR,2+ Meals
+Czech Republic,16,EUR,Night Travel supplement
+Denmark,79,EUR,Full day (over 24 hours)
+Denmark,79,EUR,Final day (over 10 hours)
+Denmark,39.5,EUR,Final day (over 2 hours)
+Denmark,-39.5,EUR,2+ Meals
+Denmark,16,EUR,Night Travel supplement
+Djibouti,83,EUR,Full day (over 24 hours)
+Djibouti,83,EUR,Final day (over 10 hours)
+Djibouti,41.5,EUR,Final day (over 2 hours)
+Djibouti,-41.5,EUR,2+ Meals
+Djibouti,16,EUR,Night Travel supplement
+Dominica,61,EUR,Full day (over 24 hours)
+Dominica,61,EUR,Final day (over 10 hours)
+Dominica,30.5,EUR,Final day (over 2 hours)
+Dominica,-30.5,EUR,2+ Meals
+Dominica,16,EUR,Night Travel supplement
+Dominican Republic,53,EUR,Full day (over 24 hours)
+Dominican Republic,53,EUR,Final day (over 10 hours)
+Dominican Republic,26.5,EUR,Final day (over 2 hours)
+Dominican Republic,-26.5,EUR,2+ Meals
+Dominican Republic,16,EUR,Night Travel supplement
+East Timor,46,EUR,Full day (over 24 hours)
+East Timor,46,EUR,Final day (over 10 hours)
+East Timor,23,EUR,Final day (over 2 hours)
+East Timor,-23,EUR,2+ Meals
+East Timor,16,EUR,Night Travel supplement
+Ecuador,63,EUR,Full day (over 24 hours)
+Ecuador,63,EUR,Final day (over 10 hours)
+Ecuador,31.5,EUR,Final day (over 2 hours)
+Ecuador,-31.5,EUR,2+ Meals
+Ecuador,16,EUR,Night Travel supplement
+Egypt,66,EUR,Full day (over 24 hours)
+Egypt,66,EUR,Final day (over 10 hours)
+Egypt,33,EUR,Final day (over 2 hours)
+Egypt,-33,EUR,2+ Meals
+Egypt,16,EUR,Night Travel supplement
+El Salvador,60,EUR,Full day (over 24 hours)
+El Salvador,60,EUR,Final day (over 10 hours)
+El Salvador,30,EUR,Final day (over 2 hours)
+El Salvador,-30,EUR,2+ Meals
+El Salvador,16,EUR,Night Travel supplement
+Eritrea,95,EUR,Full day (over 24 hours)
+Eritrea,95,EUR,Final day (over 10 hours)
+Eritrea,47.5,EUR,Final day (over 2 hours)
+Eritrea,-47.5,EUR,2+ Meals
+Eritrea,16,EUR,Night Travel supplement
+Estonia,75,EUR,Full day (over 24 hours)
+Estonia,75,EUR,Final day (over 10 hours)
+Estonia,37.5,EUR,Final day (over 2 hours)
+Estonia,-37.5,EUR,2+ Meals
+Estonia,16,EUR,Night Travel supplement
+Eswatini,37,EUR,Full day (over 24 hours)
+Eswatini,37,EUR,Final day (over 10 hours)
+Eswatini,18.5,EUR,Final day (over 2 hours)
+Eswatini,-18.5,EUR,2+ Meals
+Eswatini,16,EUR,Night Travel supplement
+Ethiopia,49,EUR,Full day (over 24 hours)
+Ethiopia,49,EUR,Final day (over 10 hours)
+Ethiopia,24.5,EUR,Final day (over 2 hours)
+Ethiopia,-24.5,EUR,2+ Meals
+Ethiopia,16,EUR,Night Travel supplement
+Faroe Islands,61,EUR,Full day (over 24 hours)
+Faroe Islands,61,EUR,Final day (over 10 hours)
+Faroe Islands,30.5,EUR,Final day (over 2 hours)
+Faroe Islands,-30.5,EUR,2+ Meals
+Faroe Islands,16,EUR,Night Travel supplement
+Fiji,52,EUR,Full day (over 24 hours)
+Fiji,52,EUR,Final day (over 10 hours)
+Fiji,26,EUR,Final day (over 2 hours)
+Fiji,-26,EUR,2+ Meals
+Fiji,16,EUR,Night Travel supplement
+France,78,EUR,Full day (over 24 hours)
+France,78,EUR,Final day (over 10 hours)
+France,39,EUR,Final day (over 2 hours)
+France,-39,EUR,2+ Meals
+France,16,EUR,Night Travel supplement
+Gabon,92,EUR,Full day (over 24 hours)
+Gabon,92,EUR,Final day (over 10 hours)
+Gabon,46,EUR,Final day (over 2 hours)
+Gabon,-46,EUR,2+ Meals
+Gabon,16,EUR,Night Travel supplement
+Gambia,46,EUR,Full day (over 24 hours)
+Gambia,46,EUR,Final day (over 10 hours)
+Gambia,23,EUR,Final day (over 2 hours)
+Gambia,-23,EUR,2+ Meals
+Gambia,16,EUR,Night Travel supplement
+Georgia,49,EUR,Full day (over 24 hours)
+Georgia,49,EUR,Final day (over 10 hours)
+Georgia,24.5,EUR,Final day (over 2 hours)
+Georgia,-24.5,EUR,2+ Meals
+Georgia,16,EUR,Night Travel supplement
+Germany,76,EUR,Full day (over 24 hours)
+Germany,76,EUR,Final day (over 10 hours)
+Germany,38,EUR,Final day (over 2 hours)
+Germany,-38,EUR,2+ Meals
+Germany,16,EUR,Night Travel supplement
+Ghana,47,EUR,Full day (over 24 hours)
+Ghana,47,EUR,Final day (over 10 hours)
+Ghana,23.5,EUR,Final day (over 2 hours)
+Ghana,-23.5,EUR,2+ Meals
+Ghana,16,EUR,Night Travel supplement
+Greece,68,EUR,Full day (over 24 hours)
+Greece,68,EUR,Final day (over 10 hours)
+Greece,34,EUR,Final day (over 2 hours)
+Greece,-34,EUR,2+ Meals
+Greece,16,EUR,Night Travel supplement
+Greenland,63,EUR,Full day (over 24 hours)
+Greenland,63,EUR,Final day (over 10 hours)
+Greenland,31.5,EUR,Final day (over 2 hours)
+Greenland,-31.5,EUR,2+ Meals
+Greenland,16,EUR,Night Travel supplement
+Grenada,73,EUR,Full day (over 24 hours)
+Grenada,73,EUR,Final day (over 10 hours)
+Grenada,36.5,EUR,Final day (over 2 hours)
+Grenada,-36.5,EUR,2+ Meals
+Grenada,16,EUR,Night Travel supplement
+Guadeloupe,53,EUR,Full day (over 24 hours)
+Guadeloupe,53,EUR,Final day (over 10 hours)
+Guadeloupe,26.5,EUR,Final day (over 2 hours)
+Guadeloupe,-26.5,EUR,2+ Meals
+Guadeloupe,16,EUR,Night Travel supplement
+Guatemala,76,EUR,Full day (over 24 hours)
+Guatemala,76,EUR,Final day (over 10 hours)
+Guatemala,38,EUR,Final day (over 2 hours)
+Guatemala,-38,EUR,2+ Meals
+Guatemala,16,EUR,Night Travel supplement
+Guinea,83,EUR,Full day (over 24 hours)
+Guinea,83,EUR,Final day (over 10 hours)
+Guinea,41.5,EUR,Final day (over 2 hours)
+Guinea,-41.5,EUR,2+ Meals
+Guinea,16,EUR,Night Travel supplement
+Guinea-Bissau,41,EUR,Full day (over 24 hours)
+Guinea-Bissau,41,EUR,Final day (over 10 hours)
+Guinea-Bissau,20.5,EUR,Final day (over 2 hours)
+Guinea-Bissau,-20.5,EUR,2+ Meals
+Guinea-Bissau,16,EUR,Night Travel supplement
+Guyana,51,EUR,Full day (over 24 hours)
+Guyana,51,EUR,Final day (over 10 hours)
+Guyana,25.5,EUR,Final day (over 2 hours)
+Guyana,-25.5,EUR,2+ Meals
+Guyana,16,EUR,Night Travel supplement
+Haiti,62,EUR,Full day (over 24 hours)
+Haiti,62,EUR,Final day (over 10 hours)
+Haiti,31,EUR,Final day (over 2 hours)
+Haiti,-31,EUR,2+ Meals
+Haiti,16,EUR,Night Travel supplement
+Honduras,58,EUR,Full day (over 24 hours)
+Honduras,58,EUR,Final day (over 10 hours)
+Honduras,29,EUR,Final day (over 2 hours)
+Honduras,-29,EUR,2+ Meals
+Honduras,16,EUR,Night Travel supplement
+Hong Kong,86,EUR,Full day (over 24 hours)
+Hong Kong,86,EUR,Final day (over 10 hours)
+Hong Kong,43,EUR,Final day (over 2 hours)
+Hong Kong,-43,EUR,2+ Meals
+Hong Kong,16,EUR,Night Travel supplement
+Hungary,69,EUR,Full day (over 24 hours)
+Hungary,69,EUR,Final day (over 10 hours)
+Hungary,34.5,EUR,Final day (over 2 hours)
+Hungary,-34.5,EUR,2+ Meals
+Hungary,16,EUR,Night Travel supplement
+Iceland,92,EUR,Full day (over 24 hours)
+Iceland,92,EUR,Final day (over 10 hours)
+Iceland,46,EUR,Final day (over 2 hours)
+Iceland,-46,EUR,2+ Meals
+Iceland,16,EUR,Night Travel supplement
+India,62,EUR,Full day (over 24 hours)
+India,62,EUR,Final day (over 10 hours)
+India,31,EUR,Final day (over 2 hours)
+India,-31,EUR,2+ Meals
+India,16,EUR,Night Travel supplement
+Indonesia,57,EUR,Full day (over 24 hours)
+Indonesia,57,EUR,Final day (over 10 hours)
+Indonesia,28.5,EUR,Final day (over 2 hours)
+Indonesia,-28.5,EUR,2+ Meals
+Indonesia,16,EUR,Night Travel supplement
+Iran,102,EUR,Full day (over 24 hours)
+Iran,102,EUR,Final day (over 10 hours)
+Iran,51,EUR,Final day (over 2 hours)
+Iran,-51,EUR,2+ Meals
+Iran,16,EUR,Night Travel supplement
+Iraq,70,EUR,Full day (over 24 hours)
+Iraq,70,EUR,Final day (over 10 hours)
+Iraq,35,EUR,Final day (over 2 hours)
+Iraq,-35,EUR,2+ Meals
+Iraq,16,EUR,Night Travel supplement
+Ireland,78,EUR,Full day (over 24 hours)
+Ireland,78,EUR,Final day (over 10 hours)
+Ireland,39,EUR,Final day (over 2 hours)
+Ireland,-39,EUR,2+ Meals
+Ireland,16,EUR,Night Travel supplement
+Israel,88,EUR,Full day (over 24 hours)
+Israel,88,EUR,Final day (over 10 hours)
+Israel,44,EUR,Final day (over 2 hours)
+Israel,-44,EUR,2+ Meals
+Israel,16,EUR,Night Travel supplement
+Istanbul,37,EUR,Full day (over 24 hours)
+Istanbul,37,EUR,Final day (over 10 hours)
+Istanbul,18.5,EUR,Final day (over 2 hours)
+Istanbul,-18.5,EUR,2+ Meals
+Istanbul,16,EUR,Night Travel supplement
+Italy,76,EUR,Full day (over 24 hours)
+Italy,76,EUR,Final day (over 10 hours)
+Italy,38,EUR,Final day (over 2 hours)
+Italy,-38,EUR,2+ Meals
+Italy,16,EUR,Night Travel supplement
+"Ivory Coast, Côte d’Ivoire",80,EUR,Full day (over 24 hours)
+"Ivory Coast, Côte d’Ivoire",80,EUR,Final day (over 10 hours)
+"Ivory Coast, Côte d’Ivoire",40,EUR,Final day (over 2 hours)
+"Ivory Coast, Côte d’Ivoire",-40,EUR,2+ Meals
+"Ivory Coast, Côte d’Ivoire",16,EUR,Night Travel supplement
+Jamaica,62,EUR,Full day (over 24 hours)
+Jamaica,62,EUR,Final day (over 10 hours)
+Jamaica,31,EUR,Final day (over 2 hours)
+Jamaica,-31,EUR,2+ Meals
+Jamaica,16,EUR,Night Travel supplement
+Japan,66,EUR,Full day (over 24 hours)
+Japan,66,EUR,Final day (over 10 hours)
+Japan,33,EUR,Final day (over 2 hours)
+Japan,-33,EUR,2+ Meals
+Japan,16,EUR,Night Travel supplement
+Jordania,90,EUR,Full day (over 24 hours)
+Jordania,90,EUR,Final day (over 10 hours)
+Jordania,45,EUR,Final day (over 2 hours)
+Jordania,-45,EUR,2+ Meals
+Jordania,16,EUR,Night Travel supplement
+Kazakhstan,59,EUR,Full day (over 24 hours)
+Kazakhstan,59,EUR,Final day (over 10 hours)
+Kazakhstan,29.5,EUR,Final day (over 2 hours)
+Kazakhstan,-29.5,EUR,2+ Meals
+Kazakhstan,16,EUR,Night Travel supplement
+Kenya,70,EUR,Full day (over 24 hours)
+Kenya,70,EUR,Final day (over 10 hours)
+Kenya,35,EUR,Final day (over 2 hours)
+Kenya,-35,EUR,2+ Meals
+Kenya,16,EUR,Night Travel supplement
+"Korea, Democratic People's Republic (North Korea)",70,EUR,Full day (over 24 hours)
+"Korea, Democratic People's Republic (North Korea)",70,EUR,Final day (over 10 hours)
+"Korea, Democratic People's Republic (North Korea)",35,EUR,Final day (over 2 hours)
+"Korea, Democratic People's Republic (North Korea)",-35,EUR,2+ Meals
+"Korea, Democratic People's Republic (North Korea)",16,EUR,Night Travel supplement
+"Korea, Republic of (South Korea)",87,EUR,Full day (over 24 hours)
+"Korea, Republic of (South Korea)",87,EUR,Final day (over 10 hours)
+"Korea, Republic of (South Korea)",43.5,EUR,Final day (over 2 hours)
+"Korea, Republic of (South Korea)",-43.5,EUR,2+ Meals
+"Korea, Republic of (South Korea)",16,EUR,Night Travel supplement
+Kosovo,58,EUR,Full day (over 24 hours)
+Kosovo,58,EUR,Final day (over 10 hours)
+Kosovo,29,EUR,Final day (over 2 hours)
+Kosovo,-29,EUR,2+ Meals
+Kosovo,16,EUR,Night Travel supplement
+Kuwait,84,EUR,Full day (over 24 hours)
+Kuwait,84,EUR,Final day (over 10 hours)
+Kuwait,42,EUR,Final day (over 2 hours)
+Kuwait,-42,EUR,2+ Meals
+Kuwait,16,EUR,Night Travel supplement
+Kyrgystan,41,EUR,Full day (over 24 hours)
+Kyrgystan,41,EUR,Final day (over 10 hours)
+Kyrgystan,20.5,EUR,Final day (over 2 hours)
+Kyrgystan,-20.5,EUR,2+ Meals
+Kyrgystan,16,EUR,Night Travel supplement
+Laos,32,EUR,Full day (over 24 hours)
+Laos,32,EUR,Final day (over 10 hours)
+Laos,16,EUR,Final day (over 2 hours)
+Laos,-16,EUR,2+ Meals
+Laos,16,EUR,Night Travel supplement
+Latvia,73,EUR,Full day (over 24 hours)
+Latvia,73,EUR,Final day (over 10 hours)
+Latvia,36.5,EUR,Final day (over 2 hours)
+Latvia,-36.5,EUR,2+ Meals
+Latvia,16,EUR,Night Travel supplement
+Lebanon,102,EUR,Full day (over 24 hours)
+Lebanon,102,EUR,Final day (over 10 hours)
+Lebanon,51,EUR,Final day (over 2 hours)
+Lebanon,-51,EUR,2+ Meals
+Lebanon,16,EUR,Night Travel supplement
+Lesotho,34,EUR,Full day (over 24 hours)
+Lesotho,34,EUR,Final day (over 10 hours)
+Lesotho,17,EUR,Final day (over 2 hours)
+Lesotho,-17,EUR,2+ Meals
+Lesotho,16,EUR,Night Travel supplement
+Liberia,60,EUR,Full day (over 24 hours)
+Liberia,60,EUR,Final day (over 10 hours)
+Liberia,30,EUR,Final day (over 2 hours)
+Liberia,-30,EUR,2+ Meals
+Liberia,16,EUR,Night Travel supplement
+Libya,52,EUR,Full day (over 24 hours)
+Libya,52,EUR,Final day (over 10 hours)
+Libya,26,EUR,Final day (over 2 hours)
+Libya,-26,EUR,2+ Meals
+Libya,16,EUR,Night Travel supplement
+Liechtenstein,79,EUR,Full day (over 24 hours)
+Liechtenstein,79,EUR,Final day (over 10 hours)
+Liechtenstein,39.5,EUR,Final day (over 2 hours)
+Liechtenstein,-39.5,EUR,2+ Meals
+Liechtenstein,16,EUR,Night Travel supplement
+Lithuania,72,EUR,Full day (over 24 hours)
+Lithuania,72,EUR,Final day (over 10 hours)
+Lithuania,36,EUR,Final day (over 2 hours)
+Lithuania,-36,EUR,2+ Meals
+Lithuania,16,EUR,Night Travel supplement
+London and Edinburgh,83,EUR,Full day (over 24 hours)
+London and Edinburgh,83,EUR,Final day (over 10 hours)
+London and Edinburgh,41.5,EUR,Final day (over 2 hours)
+London and Edinburgh,-41.5,EUR,2+ Meals
+London and Edinburgh,16,EUR,Night Travel supplement
+Luxembourg,77,EUR,Full day (over 24 hours)
+Luxembourg,77,EUR,Final day (over 10 hours)
+Luxembourg,38.5,EUR,Final day (over 2 hours)
+Luxembourg,-38.5,EUR,2+ Meals
+Luxembourg,16,EUR,Night Travel supplement
+Madagascar,45,EUR,Full day (over 24 hours)
+Madagascar,45,EUR,Final day (over 10 hours)
+Madagascar,22.5,EUR,Final day (over 2 hours)
+Madagascar,-22.5,EUR,2+ Meals
+Madagascar,16,EUR,Night Travel supplement
+Madeira,68,EUR,Full day (over 24 hours)
+Madeira,68,EUR,Final day (over 10 hours)
+Madeira,34,EUR,Final day (over 2 hours)
+Madeira,-34,EUR,2+ Meals
+Madeira,16,EUR,Night Travel supplement
+Malawi,77,EUR,Full day (over 24 hours)
+Malawi,77,EUR,Final day (over 10 hours)
+Malawi,38.5,EUR,Final day (over 2 hours)
+Malawi,-38.5,EUR,2+ Meals
+Malawi,16,EUR,Night Travel supplement
+Malaysia,50,EUR,Full day (over 24 hours)
+Malaysia,50,EUR,Final day (over 10 hours)
+Malaysia,25,EUR,Final day (over 2 hours)
+Malaysia,-25,EUR,2+ Meals
+Malaysia,16,EUR,Night Travel supplement
+Maldives,68,EUR,Full day (over 24 hours)
+Maldives,68,EUR,Final day (over 10 hours)
+Maldives,34,EUR,Final day (over 2 hours)
+Maldives,-34,EUR,2+ Meals
+Maldives,16,EUR,Night Travel supplement
+Mali,47,EUR,Full day (over 24 hours)
+Mali,47,EUR,Final day (over 10 hours)
+Mali,23.5,EUR,Final day (over 2 hours)
+Mali,-23.5,EUR,2+ Meals
+Mali,16,EUR,Night Travel supplement
+Malta,71,EUR,Full day (over 24 hours)
+Malta,71,EUR,Final day (over 10 hours)
+Malta,35.5,EUR,Final day (over 2 hours)
+Malta,-35.5,EUR,2+ Meals
+Malta,16,EUR,Night Travel supplement
+Marshall Islands,65,EUR,Full day (over 24 hours)
+Marshall Islands,65,EUR,Final day (over 10 hours)
+Marshall Islands,32.5,EUR,Final day (over 2 hours)
+Marshall Islands,-32.5,EUR,2+ Meals
+Marshall Islands,16,EUR,Night Travel supplement
+Martinique,55,EUR,Full day (over 24 hours)
+Martinique,55,EUR,Final day (over 10 hours)
+Martinique,27.5,EUR,Final day (over 2 hours)
+Martinique,-27.5,EUR,2+ Meals
+Martinique,16,EUR,Night Travel supplement
+Mauritania,52,EUR,Full day (over 24 hours)
+Mauritania,52,EUR,Final day (over 10 hours)
+Mauritania,26,EUR,Final day (over 2 hours)
+Mauritania,-26,EUR,2+ Meals
+Mauritania,16,EUR,Night Travel supplement
+Mauritius,53,EUR,Full day (over 24 hours)
+Mauritius,53,EUR,Final day (over 10 hours)
+Mauritius,26.5,EUR,Final day (over 2 hours)
+Mauritius,-26.5,EUR,2+ Meals
+Mauritius,16,EUR,Night Travel supplement
+Mexico,81,EUR,Full day (over 24 hours)
+Mexico,81,EUR,Final day (over 10 hours)
+Mexico,40.5,EUR,Final day (over 2 hours)
+Mexico,-40.5,EUR,2+ Meals
+Mexico,16,EUR,Night Travel supplement
+Micronesia,59,EUR,Full day (over 24 hours)
+Micronesia,59,EUR,Final day (over 10 hours)
+Micronesia,29.5,EUR,Final day (over 2 hours)
+Micronesia,-29.5,EUR,2+ Meals
+Micronesia,16,EUR,Night Travel supplement
+Moldova,73,EUR,Full day (over 24 hours)
+Moldova,73,EUR,Final day (over 10 hours)
+Moldova,36.5,EUR,Final day (over 2 hours)
+Moldova,-36.5,EUR,2+ Meals
+Moldova,16,EUR,Night Travel supplement
+Monaco,92,EUR,Full day (over 24 hours)
+Monaco,92,EUR,Final day (over 10 hours)
+Monaco,46,EUR,Final day (over 2 hours)
+Monaco,-46,EUR,2+ Meals
+Monaco,16,EUR,Night Travel supplement
+Mongolia,42,EUR,Full day (over 24 hours)
+Mongolia,42,EUR,Final day (over 10 hours)
+Mongolia,21,EUR,Final day (over 2 hours)
+Mongolia,-21,EUR,2+ Meals
+Mongolia,16,EUR,Night Travel supplement
+Montenegro,66,EUR,Full day (over 24 hours)
+Montenegro,66,EUR,Final day (over 10 hours)
+Montenegro,33,EUR,Final day (over 2 hours)
+Montenegro,-33,EUR,2+ Meals
+Montenegro,16,EUR,Night Travel supplement
+Morocco,71,EUR,Full day (over 24 hours)
+Morocco,71,EUR,Final day (over 10 hours)
+Morocco,35.5,EUR,Final day (over 2 hours)
+Morocco,-35.5,EUR,2+ Meals
+Morocco,16,EUR,Night Travel supplement
+Moscow,82,EUR,Full day (over 24 hours)
+Moscow,82,EUR,Final day (over 10 hours)
+Moscow,41,EUR,Final day (over 2 hours)
+Moscow,-41,EUR,2+ Meals
+Moscow,16,EUR,Night Travel supplement
+Mozambique,53,EUR,Full day (over 24 hours)
+Mozambique,53,EUR,Final day (over 10 hours)
+Mozambique,26.5,EUR,Final day (over 2 hours)
+Mozambique,-26.5,EUR,2+ Meals
+Mozambique,16,EUR,Night Travel supplement
+Myanmar (formerly Burma),58,EUR,Full day (over 24 hours)
+Myanmar (formerly Burma),58,EUR,Final day (over 10 hours)
+Myanmar (formerly Burma),29,EUR,Final day (over 2 hours)
+Myanmar (formerly Burma),-29,EUR,2+ Meals
+Myanmar (formerly Burma),16,EUR,Night Travel supplement
+Namibia,36,EUR,Full day (over 24 hours)
+Namibia,36,EUR,Final day (over 10 hours)
+Namibia,18,EUR,Final day (over 2 hours)
+Namibia,-18,EUR,2+ Meals
+Namibia,16,EUR,Night Travel supplement
+Nepal,51,EUR,Full day (over 24 hours)
+Nepal,51,EUR,Final day (over 10 hours)
+Nepal,25.5,EUR,Final day (over 2 hours)
+Nepal,-25.5,EUR,2+ Meals
+Nepal,16,EUR,Night Travel supplement
+Netherlands,83,EUR,Full day (over 24 hours)
+Netherlands,83,EUR,Final day (over 10 hours)
+Netherlands,41.5,EUR,Final day (over 2 hours)
+Netherlands,-41.5,EUR,2+ Meals
+Netherlands,16,EUR,Night Travel supplement
+"New York, Los Angeles, Washington",97,EUR,Full day (over 24 hours)
+"New York, Los Angeles, Washington",97,EUR,Final day (over 10 hours)
+"New York, Los Angeles, Washington",48.5,EUR,Final day (over 2 hours)
+"New York, Los Angeles, Washington",-48.5,EUR,2+ Meals
+"New York, Los Angeles, Washington",16,EUR,Night Travel supplement
+New Zealand,74,EUR,Full day (over 24 hours)
+New Zealand,74,EUR,Final day (over 10 hours)
+New Zealand,37,EUR,Final day (over 2 hours)
+New Zealand,-37,EUR,2+ Meals
+New Zealand,16,EUR,Night Travel supplement
+Nicaragua,51,EUR,Full day (over 24 hours)
+Nicaragua,51,EUR,Final day (over 10 hours)
+Nicaragua,25.5,EUR,Final day (over 2 hours)
+Nicaragua,-25.5,EUR,2+ Meals
+Nicaragua,16,EUR,Night Travel supplement
+Niger,50,EUR,Full day (over 24 hours)
+Niger,50,EUR,Final day (over 10 hours)
+Niger,25,EUR,Final day (over 2 hours)
+Niger,-25,EUR,2+ Meals
+Niger,16,EUR,Night Travel supplement
+Nigeria,78,EUR,Full day (over 24 hours)
+Nigeria,78,EUR,Final day (over 10 hours)
+Nigeria,39,EUR,Final day (over 2 hours)
+Nigeria,-39,EUR,2+ Meals
+Nigeria,16,EUR,Night Travel supplement
+North Macedonia,64,EUR,Full day (over 24 hours)
+North Macedonia,64,EUR,Final day (over 10 hours)
+North Macedonia,32,EUR,Final day (over 2 hours)
+North Macedonia,-32,EUR,2+ Meals
+North Macedonia,16,EUR,Night Travel supplement
+Norway,70,EUR,Full day (over 24 hours)
+Norway,70,EUR,Final day (over 10 hours)
+Norway,35,EUR,Final day (over 2 hours)
+Norway,-35,EUR,2+ Meals
+Norway,16,EUR,Night Travel supplement
+Oman,74,EUR,Full day (over 24 hours)
+Oman,74,EUR,Final day (over 10 hours)
+Oman,37,EUR,Final day (over 2 hours)
+Oman,-37,EUR,2+ Meals
+Oman,16,EUR,Night Travel supplement
+Pakistan,29,EUR,Full day (over 24 hours)
+Pakistan,29,EUR,Final day (over 10 hours)
+Pakistan,14.5,EUR,Final day (over 2 hours)
+Pakistan,-14.5,EUR,2+ Meals
+Pakistan,16,EUR,Night Travel supplement
+Palau,99,EUR,Full day (over 24 hours)
+Palau,99,EUR,Final day (over 10 hours)
+Palau,49.5,EUR,Final day (over 2 hours)
+Palau,-49.5,EUR,2+ Meals
+Palau,16,EUR,Night Travel supplement
+Palestinian territory,76,EUR,Full day (over 24 hours)
+Palestinian territory,76,EUR,Final day (over 10 hours)
+Palestinian territory,38,EUR,Final day (over 2 hours)
+Palestinian territory,-38,EUR,2+ Meals
+Palestinian territory,16,EUR,Night Travel supplement
+Panama,61,EUR,Full day (over 24 hours)
+Panama,61,EUR,Final day (over 10 hours)
+Panama,30.5,EUR,Final day (over 2 hours)
+Panama,-30.5,EUR,2+ Meals
+Panama,16,EUR,Night Travel supplement
+Papua New Guinea,76,EUR,Full day (over 24 hours)
+Papua New Guinea,76,EUR,Final day (over 10 hours)
+Papua New Guinea,38,EUR,Final day (over 2 hours)
+Papua New Guinea,-38,EUR,2+ Meals
+Papua New Guinea,16,EUR,Night Travel supplement
+Paraguay,36,EUR,Full day (over 24 hours)
+Paraguay,36,EUR,Final day (over 10 hours)
+Paraguay,18,EUR,Final day (over 2 hours)
+Paraguay,-18,EUR,2+ Meals
+Paraguay,16,EUR,Night Travel supplement
+Peru,52,EUR,Full day (over 24 hours)
+Peru,52,EUR,Final day (over 10 hours)
+Peru,26,EUR,Final day (over 2 hours)
+Peru,-26,EUR,2+ Meals
+Peru,16,EUR,Night Travel supplement
+Philippines,69,EUR,Full day (over 24 hours)
+Philippines,69,EUR,Final day (over 10 hours)
+Philippines,34.5,EUR,Final day (over 2 hours)
+Philippines,-34.5,EUR,2+ Meals
+Philippines,16,EUR,Night Travel supplement
+Poland,72,EUR,Full day (over 24 hours)
+Poland,72,EUR,Final day (over 10 hours)
+Poland,36,EUR,Final day (over 2 hours)
+Poland,-36,EUR,2+ Meals
+Poland,16,EUR,Night Travel supplement
+Portugal,70,EUR,Full day (over 24 hours)
+Portugal,70,EUR,Final day (over 10 hours)
+Portugal,35,EUR,Final day (over 2 hours)
+Portugal,-35,EUR,2+ Meals
+Portugal,16,EUR,Night Travel supplement
+Puerto Rico,70,EUR,Full day (over 24 hours)
+Puerto Rico,70,EUR,Final day (over 10 hours)
+Puerto Rico,35,EUR,Final day (over 2 hours)
+Puerto Rico,-35,EUR,2+ Meals
+Puerto Rico,16,EUR,Night Travel supplement
+Qatar,78,EUR,Full day (over 24 hours)
+Qatar,78,EUR,Final day (over 10 hours)
+Qatar,39,EUR,Final day (over 2 hours)
+Qatar,-39,EUR,2+ Meals
+Qatar,16,EUR,Night Travel supplement
+Romania,68,EUR,Full day (over 24 hours)
+Romania,68,EUR,Final day (over 10 hours)
+Romania,34,EUR,Final day (over 2 hours)
+Romania,-34,EUR,2+ Meals
+Romania,16,EUR,Night Travel supplement
+Russian Federation,66,EUR,Full day (over 24 hours)
+Russian Federation,66,EUR,Final day (over 10 hours)
+Russian Federation,33,EUR,Final day (over 2 hours)
+Russian Federation,-33,EUR,2+ Meals
+Russian Federation,16,EUR,Night Travel supplement
+Rwanda,37,EUR,Full day (over 24 hours)
+Rwanda,37,EUR,Final day (over 10 hours)
+Rwanda,18.5,EUR,Final day (over 2 hours)
+Rwanda,-18.5,EUR,2+ Meals
+Rwanda,16,EUR,Night Travel supplement
+Saint Kitts and Nevis,68,EUR,Full day (over 24 hours)
+Saint Kitts and Nevis,68,EUR,Final day (over 10 hours)
+Saint Kitts and Nevis,34,EUR,Final day (over 2 hours)
+Saint Kitts and Nevis,-34,EUR,2+ Meals
+Saint Kitts and Nevis,16,EUR,Night Travel supplement
+Saint Lucia,86,EUR,Full day (over 24 hours)
+Saint Lucia,86,EUR,Final day (over 10 hours)
+Saint Lucia,43,EUR,Final day (over 2 hours)
+Saint Lucia,-43,EUR,2+ Meals
+Saint Lucia,16,EUR,Night Travel supplement
+Saint Vincent and the Grenadines,85,EUR,Full day (over 24 hours)
+Saint Vincent and the Grenadines,85,EUR,Final day (over 10 hours)
+Saint Vincent and the Grenadines,42.5,EUR,Final day (over 2 hours)
+Saint Vincent and the Grenadines,-42.5,EUR,2+ Meals
+Saint Vincent and the Grenadines,16,EUR,Night Travel supplement
+Samoa,61,EUR,Full day (over 24 hours)
+Samoa,61,EUR,Final day (over 10 hours)
+Samoa,30.5,EUR,Final day (over 2 hours)
+Samoa,-30.5,EUR,2+ Meals
+Samoa,16,EUR,Night Travel supplement
+San Marino,59,EUR,Full day (over 24 hours)
+San Marino,59,EUR,Final day (over 10 hours)
+San Marino,29.5,EUR,Final day (over 2 hours)
+San Marino,-29.5,EUR,2+ Meals
+San Marino,16,EUR,Night Travel supplement
+Sao Tome and Principe,102,EUR,Full day (over 24 hours)
+Sao Tome and Principe,102,EUR,Final day (over 10 hours)
+Sao Tome and Principe,51,EUR,Final day (over 2 hours)
+Sao Tome and Principe,-51,EUR,2+ Meals
+Sao Tome and Principe,16,EUR,Night Travel supplement
+Saudi Arabia,80,EUR,Full day (over 24 hours)
+Saudi Arabia,80,EUR,Final day (over 10 hours)
+Saudi Arabia,40,EUR,Final day (over 2 hours)
+Saudi Arabia,-40,EUR,2+ Meals
+Saudi Arabia,16,EUR,Night Travel supplement
+Senegal,58,EUR,Full day (over 24 hours)
+Senegal,58,EUR,Final day (over 10 hours)
+Senegal,29,EUR,Final day (over 2 hours)
+Senegal,-29,EUR,2+ Meals
+Senegal,16,EUR,Night Travel supplement
+Serbia,75,EUR,Full day (over 24 hours)
+Serbia,75,EUR,Final day (over 10 hours)
+Serbia,37.5,EUR,Final day (over 2 hours)
+Serbia,-37.5,EUR,2+ Meals
+Serbia,16,EUR,Night Travel supplement
+Seychelles,87,EUR,Full day (over 24 hours)
+Seychelles,87,EUR,Final day (over 10 hours)
+Seychelles,43.5,EUR,Final day (over 2 hours)
+Seychelles,-43.5,EUR,2+ Meals
+Seychelles,16,EUR,Night Travel supplement
+Sierra Leone,47,EUR,Full day (over 24 hours)
+Sierra Leone,47,EUR,Final day (over 10 hours)
+Sierra Leone,23.5,EUR,Final day (over 2 hours)
+Sierra Leone,-23.5,EUR,2+ Meals
+Sierra Leone,16,EUR,Night Travel supplement
+Singapore,79,EUR,Full day (over 24 hours)
+Singapore,79,EUR,Final day (over 10 hours)
+Singapore,39.5,EUR,Final day (over 2 hours)
+Singapore,-39.5,EUR,2+ Meals
+Singapore,16,EUR,Night Travel supplement
+Slovakia,79,EUR,Full day (over 24 hours)
+Slovakia,79,EUR,Final day (over 10 hours)
+Slovakia,39.5,EUR,Final day (over 2 hours)
+Slovakia,-39.5,EUR,2+ Meals
+Slovakia,16,EUR,Night Travel supplement
+Slovenia,72,EUR,Full day (over 24 hours)
+Slovenia,72,EUR,Final day (over 10 hours)
+Slovenia,36,EUR,Final day (over 2 hours)
+Slovenia,-36,EUR,2+ Meals
+Slovenia,16,EUR,Night Travel supplement
+Solomon Islands,63,EUR,Full day (over 24 hours)
+Solomon Islands,63,EUR,Final day (over 10 hours)
+Solomon Islands,31.5,EUR,Final day (over 2 hours)
+Solomon Islands,-31.5,EUR,2+ Meals
+Solomon Islands,16,EUR,Night Travel supplement
+Somalia,86,EUR,Full day (over 24 hours)
+Somalia,86,EUR,Final day (over 10 hours)
+Somalia,43,EUR,Final day (over 2 hours)
+Somalia,-43,EUR,2+ Meals
+Somalia,16,EUR,Night Travel supplement
+South Africa,50,EUR,Full day (over 24 hours)
+South Africa,50,EUR,Final day (over 10 hours)
+South Africa,25,EUR,Final day (over 2 hours)
+South Africa,-25,EUR,2+ Meals
+South Africa,16,EUR,Night Travel supplement
+South Sudan,102,EUR,Full day (over 24 hours)
+South Sudan,102,EUR,Final day (over 10 hours)
+South Sudan,51,EUR,Final day (over 2 hours)
+South Sudan,-51,EUR,2+ Meals
+South Sudan,16,EUR,Night Travel supplement
+Spain,74,EUR,Full day (over 24 hours)
+Spain,74,EUR,Final day (over 10 hours)
+Spain,37,EUR,Final day (over 2 hours)
+Spain,-37,EUR,2+ Meals
+Spain,16,EUR,Night Travel supplement
+Sri Lanka,29,EUR,Full day (over 24 hours)
+Sri Lanka,29,EUR,Final day (over 10 hours)
+Sri Lanka,14.5,EUR,Final day (over 2 hours)
+Sri Lanka,-14.5,EUR,2+ Meals
+Sri Lanka,16,EUR,Night Travel supplement
+St. Petersburg,76,EUR,Full day (over 24 hours)
+St. Petersburg,76,EUR,Final day (over 10 hours)
+St. Petersburg,38,EUR,Final day (over 2 hours)
+St. Petersburg,-38,EUR,2+ Meals
+St. Petersburg,16,EUR,Night Travel supplement
+Sudan,83,EUR,Full day (over 24 hours)
+Sudan,83,EUR,Final day (over 10 hours)
+Sudan,41.5,EUR,Final day (over 2 hours)
+Sudan,-41.5,EUR,2+ Meals
+Sudan,16,EUR,Night Travel supplement
+Suriname,78,EUR,Full day (over 24 hours)
+Suriname,78,EUR,Final day (over 10 hours)
+Suriname,39,EUR,Final day (over 2 hours)
+Suriname,-39,EUR,2+ Meals
+Suriname,16,EUR,Night Travel supplement
+Sweden,64,EUR,Full day (over 24 hours)
+Sweden,64,EUR,Final day (over 10 hours)
+Sweden,32,EUR,Final day (over 2 hours)
+Sweden,-32,EUR,2+ Meals
+Sweden,16,EUR,Night Travel supplement
+Switzerland,93,EUR,Full day (over 24 hours)
+Switzerland,93,EUR,Final day (over 10 hours)
+Switzerland,46.5,EUR,Final day (over 2 hours)
+Switzerland,-46.5,EUR,2+ Meals
+Switzerland,16,EUR,Night Travel supplement
+Syria,91,EUR,Full day (over 24 hours)
+Syria,91,EUR,Final day (over 10 hours)
+Syria,45.5,EUR,Final day (over 2 hours)
+Syria,-45.5,EUR,2+ Meals
+Syria,16,EUR,Night Travel supplement
+Tadzhikistan,35,EUR,Full day (over 24 hours)
+Tadzhikistan,35,EUR,Final day (over 10 hours)
+Tadzhikistan,17.5,EUR,Final day (over 2 hours)
+Tadzhikistan,-17.5,EUR,2+ Meals
+Tadzhikistan,16,EUR,Night Travel supplement
+Taiwan,69,EUR,Full day (over 24 hours)
+Taiwan,69,EUR,Final day (over 10 hours)
+Taiwan,34.5,EUR,Final day (over 2 hours)
+Taiwan,-34.5,EUR,2+ Meals
+Taiwan,16,EUR,Night Travel supplement
+Tanzania,54,EUR,Full day (over 24 hours)
+Tanzania,54,EUR,Final day (over 10 hours)
+Tanzania,27,EUR,Final day (over 2 hours)
+Tanzania,-27,EUR,2+ Meals
+Tanzania,16,EUR,Night Travel supplement
+Thailand,63,EUR,Full day (over 24 hours)
+Thailand,63,EUR,Final day (over 10 hours)
+Thailand,31.5,EUR,Final day (over 2 hours)
+Thailand,-31.5,EUR,2+ Meals
+Thailand,16,EUR,Night Travel supplement
+Togo,58,EUR,Full day (over 24 hours)
+Togo,58,EUR,Final day (over 10 hours)
+Togo,29,EUR,Final day (over 2 hours)
+Togo,-29,EUR,2+ Meals
+Togo,16,EUR,Night Travel supplement
+Tonga,62,EUR,Full day (over 24 hours)
+Tonga,62,EUR,Final day (over 10 hours)
+Tonga,31,EUR,Final day (over 2 hours)
+Tonga,-31,EUR,2+ Meals
+Tonga,16,EUR,Night Travel supplement
+Trinidad and Tobago,83,EUR,Full day (over 24 hours)
+Trinidad and Tobago,83,EUR,Final day (over 10 hours)
+Trinidad and Tobago,41.5,EUR,Final day (over 2 hours)
+Trinidad and Tobago,-41.5,EUR,2+ Meals
+Trinidad and Tobago,16,EUR,Night Travel supplement
+Tunisia,61,EUR,Full day (over 24 hours)
+Tunisia,61,EUR,Final day (over 10 hours)
+Tunisia,30.5,EUR,Final day (over 2 hours)
+Tunisia,-30.5,EUR,2+ Meals
+Tunisia,16,EUR,Night Travel supplement
+Turkey,35,EUR,Full day (over 24 hours)
+Turkey,35,EUR,Final day (over 10 hours)
+Turkey,17.5,EUR,Final day (over 2 hours)
+Turkey,-17.5,EUR,2+ Meals
+Turkey,16,EUR,Night Travel supplement
+Turkmenistan,92,EUR,Full day (over 24 hours)
+Turkmenistan,92,EUR,Final day (over 10 hours)
+Turkmenistan,46,EUR,Final day (over 2 hours)
+Turkmenistan,-46,EUR,2+ Meals
+Turkmenistan,16,EUR,Night Travel supplement
+Uganda,49,EUR,Full day (over 24 hours)
+Uganda,49,EUR,Final day (over 10 hours)
+Uganda,24.5,EUR,Final day (over 2 hours)
+Uganda,-24.5,EUR,2+ Meals
+Uganda,16,EUR,Night Travel supplement
+Ukraine,64,EUR,Full day (over 24 hours)
+Ukraine,64,EUR,Final day (over 10 hours)
+Ukraine,32,EUR,Final day (over 2 hours)
+Ukraine,-32,EUR,2+ Meals
+Ukraine,16,EUR,Night Travel supplement
+United Arab Emirates,73,EUR,Full day (over 24 hours)
+United Arab Emirates,73,EUR,Final day (over 10 hours)
+United Arab Emirates,36.5,EUR,Final day (over 2 hours)
+United Arab Emirates,-36.5,EUR,2+ Meals
+United Arab Emirates,16,EUR,Night Travel supplement
+United Kingdom,79,EUR,Full day (over 24 hours)
+United Kingdom,79,EUR,Final day (over 10 hours)
+United Kingdom,39.5,EUR,Final day (over 2 hours)
+United Kingdom,-39.5,EUR,2+ Meals
+United Kingdom,16,EUR,Night Travel supplement
+United States,89,EUR,Full day (over 24 hours)
+United States,89,EUR,Final day (over 10 hours)
+United States,44.5,EUR,Final day (over 2 hours)
+United States,-44.5,EUR,2+ Meals
+United States,16,EUR,Night Travel supplement
+Uruguay,59,EUR,Full day (over 24 hours)
+Uruguay,59,EUR,Final day (over 10 hours)
+Uruguay,29.5,EUR,Final day (over 2 hours)
+Uruguay,-29.5,EUR,2+ Meals
+Uruguay,16,EUR,Night Travel supplement
+Uzbekistan,32,EUR,Full day (over 24 hours)
+Uzbekistan,32,EUR,Final day (over 10 hours)
+Uzbekistan,16,EUR,Final day (over 2 hours)
+Uzbekistan,-16,EUR,2+ Meals
+Uzbekistan,16,EUR,Night Travel supplement
+Vanuatu,70,EUR,Full day (over 24 hours)
+Vanuatu,70,EUR,Final day (over 10 hours)
+Vanuatu,35,EUR,Final day (over 2 hours)
+Vanuatu,-35,EUR,2+ Meals
+Vanuatu,16,EUR,Night Travel supplement
+Venezuela,102,EUR,Full day (over 24 hours)
+Venezuela,102,EUR,Final day (over 10 hours)
+Venezuela,51,EUR,Final day (over 2 hours)
+Venezuela,-51,EUR,2+ Meals
+Venezuela,16,EUR,Night Travel supplement
+Viet Nam,69,EUR,Full day (over 24 hours)
+Viet Nam,69,EUR,Final day (over 10 hours)
+Viet Nam,34.5,EUR,Final day (over 2 hours)
+Viet Nam,-34.5,EUR,2+ Meals
+Viet Nam,16,EUR,Night Travel supplement
+Virgin Islands (USA),64,EUR,Full day (over 24 hours)
+Virgin Islands (USA),64,EUR,Final day (over 10 hours)
+Virgin Islands (USA),32,EUR,Final day (over 2 hours)
+Virgin Islands (USA),-32,EUR,2+ Meals
+Virgin Islands (USA),16,EUR,Night Travel supplement
+Yemen,102,EUR,Full day (over 24 hours)
+Yemen,102,EUR,Final day (over 10 hours)
+Yemen,51,EUR,Final day (over 2 hours)
+Yemen,-51,EUR,2+ Meals
+Yemen,16,EUR,Night Travel supplement
+Zambia,55,EUR,Full day (over 24 hours)
+Zambia,55,EUR,Final day (over 10 hours)
+Zambia,27.5,EUR,Final day (over 2 hours)
+Zambia,-27.5,EUR,2+ Meals
+Zambia,16,EUR,Night Travel supplement
+Zimbabwe,102,EUR,Full day (over 24 hours)
+Zimbabwe,102,EUR,Final day (over 10 hours)
+Zimbabwe,51,EUR,Final day (over 2 hours)
+Zimbabwe,-51,EUR,2+ Meals
+Zimbabwe,16,EUR,Night Travel supplement
diff --git a/docs/redirects.csv b/docs/redirects.csv
index b47d6f2ae25c..0a5007b4fa61 100644
--- a/docs/redirects.csv
+++ b/docs/redirects.csv
@@ -577,4 +577,5 @@ https://help.expensify.com/articles/new-expensify/expenses-&-payments/pay-an-inv
https://community.expensify.com/discussion/4707/how-to-set-up-your-mobile-app,https://help.expensify.com/articles/expensify-classic/getting-started/Join-your-company's-workspace#download-the-mobile-app
https://community.expensify.com//discussion/6927/deep-dive-how-can-i-estimate-the-savings-applied-to-my-bill,https://help.expensify.com/articles/expensify-classic/expensify-billing/Billing-Overview#savings-calculator
https://community.expensify.com/discussion/5179/faq-what-does-a-policy-for-which-you-are-an-admin-has-out-of-date-billing-information-mean,https://help.expensify.com/articles/expensify-classic/expensify-billing/Out-of-date-Billing
-https://community.expensify.com/discussion/6179/setting-up-a-receipt-or-travel-integration-with-expensify,https://help.expensify.com/articles/expensify-classic/connections/Additional-Travel-Integrations
\ No newline at end of file
+https://community.expensify.com/discussion/6179/setting-up-a-receipt-or-travel-integration-with-expensify,https://help.expensify.com/articles/expensify-classic/connections/Additional-Travel-Integrations
+https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-Personal-US-Bank-Account,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-Personal-Bank-Account
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index a62749c0629e..43cae757b784 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
-
9.0.43
+
9.0.44
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
-
9.0.43.1
+
9.0.44.0
FullStory
OrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 16d99226f854..b1715a829e2a 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 9.0.43
+ 9.0.44
CFBundleSignature
????
CFBundleVersion
- 9.0.43.1
+ 9.0.44.0
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 16c7823a1cf8..7b8d7e40f1f6 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName
$(PRODUCT_NAME)
CFBundleShortVersionString
-
9.0.43
+
9.0.44
CFBundleVersion
-
9.0.43.1
+
9.0.44.0
NSExtension
NSExtensionPointIdentifier
diff --git a/ios/NotificationServiceExtension/NotificationService.swift b/ios/NotificationServiceExtension/NotificationService.swift
index b3c56a36619d..e489cb368d17 100644
--- a/ios/NotificationServiceExtension/NotificationService.swift
+++ b/ios/NotificationServiceExtension/NotificationService.swift
@@ -8,18 +8,12 @@
import AirshipServiceExtension
import os.log
import Intents
-import AppLogs
class NotificationService: UANotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
let log = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "com.expensify.chat.dev.NotificationServiceExtension", category: "NotificationService")
- let appLogs: AppLogs = .init()
-
- deinit {
- appLogs.forwardLogsTo(appGroup: "group.com.expensify.new")
- }
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
os_log("[NotificationService] didReceive() - received notification", log: log)
@@ -48,7 +42,7 @@ class NotificationService: UANotificationServiceExtension {
do {
notificationData = try parsePayload(notificationContent: notificationContent)
} catch ExpError.runtimeError(let errorMessage) {
- os_log("[NotificationService] configureCommunicationNotification() - couldn't parse the payload '%{public}@'", log: log, type: .error, errorMessage)
+ os_log("[NotificationService] configureCommunicationNotification() - couldn't parse the payload '%@'", log: log, type: .error, errorMessage)
contentHandler(notificationContent)
return
} catch {
@@ -218,7 +212,7 @@ class NotificationService: UANotificationServiceExtension {
let data = try Data(contentsOf: url)
return INImage(imageData: data)
} catch {
- os_log("[NotificationService] fetchINImage() - failed to fetch avatar. reportActionID: %{public}@", log: self.log, type: .error, reportActionID)
+ os_log("[NotificationService] fetchINImage() - failed to fetch avatar. reportActionID: %@", log: self.log, type: .error, reportActionID)
return nil
}
}
diff --git a/ios/Podfile b/ios/Podfile
index 4d139711ef01..e807089c26b9 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -119,7 +119,6 @@ end
target 'NotificationServiceExtension' do
pod 'AirshipServiceExtension'
- pod 'AppLogs', :path => '../node_modules/react-native-app-logs/AppLogsPod'
end
pod 'FullStory', :http => 'https://ios-releases.fullstory.com/fullstory-1.52.0-xcframework.tar.gz'
\ No newline at end of file
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 64f8e0365423..beac64acd083 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -26,7 +26,6 @@ PODS:
- AppAuth/Core (1.7.5)
- AppAuth/ExternalUserAgent (1.7.5):
- AppAuth/Core
- - AppLogs (0.1.0)
- boost (1.84.0)
- DoubleConversion (1.1.6)
- EXAV (14.0.7):
@@ -1565,27 +1564,6 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- - react-native-app-logs (0.2.2):
- - DoubleConversion
- - glog
- - hermes-engine
- - RCT-Folly (= 2024.01.01.00)
- - RCTRequired
- - RCTTypeSafety
- - React-Core
- - React-debug
- - React-Fabric
- - React-featureflags
- - React-graphics
- - React-ImageManager
- - React-NativeModulesApple
- - React-RCTFabric
- - React-rendererdebug
- - React-utils
- - ReactCodegen
- - ReactCommon/turbomodule/bridging
- - ReactCommon/turbomodule/core
- - Yoga
- react-native-blob-util (0.19.4):
- DoubleConversion
- glog
@@ -1968,7 +1946,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- - react-native-view-shot (4.0.0-alpha.3):
+ - react-native-view-shot (3.8.0):
- React-Core
- react-native-webview (13.8.6):
- DoubleConversion
@@ -2711,7 +2689,6 @@ PODS:
DEPENDENCIES:
- AirshipServiceExtension
- - AppLogs (from `../node_modules/react-native-app-logs/AppLogsPod`)
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- EXAV (from `../node_modules/expo-av/ios`)
@@ -2761,7 +2738,6 @@ DEPENDENCIES:
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
- React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
- "react-native-airship (from `../node_modules/@ua/react-native-airship`)"
- - react-native-app-logs (from `../node_modules/react-native-app-logs`)
- react-native-blob-util (from `../node_modules/react-native-blob-util`)
- "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)"
- react-native-config (from `../node_modules/react-native-config`)
@@ -2875,8 +2851,6 @@ SPEC REPOS:
- Turf
EXTERNAL SOURCES:
- AppLogs:
- :path: "../node_modules/react-native-app-logs/AppLogsPod"
boost:
:podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec"
DoubleConversion:
@@ -2972,8 +2946,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
react-native-airship:
:path: "../node_modules/@ua/react-native-airship"
- react-native-app-logs:
- :path: "../node_modules/react-native-app-logs"
react-native-blob-util:
:path: "../node_modules/react-native-blob-util"
react-native-cameraroll:
@@ -3124,7 +3096,6 @@ SPEC CHECKSUMS:
AirshipFrameworkProxy: dbd862dc6fb21b13e8b196458d626123e2a43a50
AirshipServiceExtension: 9c73369f426396d9fb9ff222d86d842fac76ba46
AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa
- AppLogs: 3bc4e9b141dbf265b9464409caaa40416a9ee0e0
boost: 26992d1adf73c1c7676360643e687aee6dda994b
DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
EXAV: afa491e598334bbbb92a92a2f4dd33d7149ad37f
@@ -3200,7 +3171,6 @@ SPEC CHECKSUMS:
React-Mapbuffer: 1c08607305558666fd16678b85ef135e455d5c96
React-microtasksnativemodule: f13f03163b6a5ec66665dfe80a0df4468bb766a6
react-native-airship: e10f6823d8da49bbcb2db4bdb16ff954188afccc
- react-native-app-logs: 91a04f691f2db7c1d6153bce31cab3922e6873f4
react-native-blob-util: 221c61c98ae507b758472ac4d2d489119d1a6c44
react-native-cameraroll: 478a0c1fcdd39f08f6ac272b7ed06e92b2c7c129
react-native-config: 5ce986133b07fc258828b20b9506de0e683efc1c
@@ -3218,7 +3188,7 @@ SPEC CHECKSUMS:
react-native-quick-sqlite: 7c793c9f5834e756b336257a8d8b8239b7ceb451
react-native-release-profiler: 131ec5e4145d900b2be2a8d6641e2ce0dd784259
react-native-safe-area-context: 38fdd9b3c5561de7cabae64bd0cd2ce05d2768a1
- react-native-view-shot: ee44129a7c470310d3c7e67085834fc8cc077655
+ react-native-view-shot: 6b7ed61d77d88580fed10954d45fad0eb2d47688
react-native-webview: ad29375839c9aa0409ce8e8693291b42bdc067a4
React-nativeconfig: 57781b79e11d5af7573e6f77cbf1143b71802a6d
React-NativeModulesApple: 7ff2e2cfb2e5fa5bdedcecf28ce37e696c6ef1e1
@@ -3276,8 +3246,8 @@ SPEC CHECKSUMS:
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
Turf: aa2ede4298009639d10db36aba1a7ebaad072a5e
VisionCamera: c6c8aa4b028501fc87644550fbc35a537d4da3fb
- Yoga: 2a45d7e59592db061217551fd3bbe2dd993817ae
+ Yoga: a1d7895431387402a674fd0d1c04ec85e87909b8
-PODFILE CHECKSUM: 15e2f095b9c80d658459723edf84005a6867debf
+PODFILE CHECKSUM: a07e55247056ec5d84d1af31d694506efff3cfe2
COCOAPODS: 1.15.2
diff --git a/jest/setup.ts b/jest/setup.ts
index 7dbe91c32fda..6901ad3c66f3 100644
--- a/jest/setup.ts
+++ b/jest/setup.ts
@@ -1,6 +1,5 @@
/* eslint-disable max-classes-per-file */
import '@shopify/flash-list/jestSetup';
-import type * as RNAppLogs from 'react-native-app-logs';
import 'react-native-gesture-handler/jestSetup';
import type * as RNKeyboardController from 'react-native-keyboard-controller';
import mockStorage from 'react-native-onyx/dist/storage/__mocks__';
@@ -76,8 +75,6 @@ jest.mock('react-native-reanimated', () => ({
jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest'));
-jest.mock('react-native-app-logs', () => require('react-native-app-logs/jest'));
-
jest.mock('@src/libs/actions/Timing', () => ({
start: jest.fn(),
end: jest.fn(),
diff --git a/package-lock.json b/package-lock.json
index c46a6987c49a..33baf6a35084 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.0.43-1",
+ "version": "9.0.44-0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.43-1",
+ "version": "9.0.44-0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -75,7 +75,6 @@
"react-map-gl": "^7.1.3",
"react-native": "0.75.2",
"react-native-android-location-enabler": "^2.0.1",
- "react-native-app-logs": "git+https://github.com/margelo/react-native-app-logs#4653bc25b600497c5c64f2897f9778c796193238",
"react-native-blob-util": "0.19.4",
"react-native-collapsible": "^1.6.2",
"react-native-config": "1.5.0",
@@ -210,6 +209,7 @@
"copy-webpack-plugin": "^10.1.0",
"css-loader": "^6.7.2",
"csv-parse": "^5.5.5",
+ "csv-writer": "^1.6.0",
"diff-so-fancy": "^1.3.0",
"dotenv": "^16.0.3",
"electron": "^29.4.6",
@@ -21225,6 +21225,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/csv-writer": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/csv-writer/-/csv-writer-1.6.0.tgz",
+ "integrity": "sha512-NOx7YDFWEsM/fTRAJjRpPp8t+MKRVvniAg9wQlUKx20MFrPs73WLJhFf5iteqrxNYnsy924K3Iroh3yNHeYd2g==",
+ "dev": true
+ },
"node_modules/dag-map": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/dag-map/-/dag-map-1.0.2.tgz",
@@ -34382,18 +34388,6 @@
"prop-types": "^15.7.2"
}
},
- "node_modules/react-native-app-logs": {
- "version": "0.2.2",
- "resolved": "git+ssh://git@github.com/margelo/react-native-app-logs.git#4653bc25b600497c5c64f2897f9778c796193238",
- "integrity": "sha512-nPZhRCtobnGQB9rm0q4vxNWVNtyU5vgR/9wfg8KHaZgp6Bqb7jMTljZLXNJKPewhlQhvf0u4b/cHlt/CkMyU9Q==",
- "workspaces": [
- "example"
- ],
- "peerDependencies": {
- "react": "*",
- "react-native": "*"
- }
- },
"node_modules/react-native-blob-util": {
"version": "0.19.4",
"license": "MIT",
diff --git a/package.json b/package.json
index 645767ec1b44..527a293a6a9e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.43-1",
+ "version": "9.0.44-0",
"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.",
@@ -132,7 +132,6 @@
"react-map-gl": "^7.1.3",
"react-native": "0.75.2",
"react-native-android-location-enabler": "^2.0.1",
- "react-native-app-logs": "git+https://github.com/margelo/react-native-app-logs#4653bc25b600497c5c64f2897f9778c796193238",
"react-native-blob-util": "0.19.4",
"react-native-collapsible": "^1.6.2",
"react-native-config": "1.5.0",
@@ -267,6 +266,7 @@
"copy-webpack-plugin": "^10.1.0",
"css-loader": "^6.7.2",
"csv-parse": "^5.5.5",
+ "csv-writer": "^1.6.0",
"diff-so-fancy": "^1.3.0",
"dotenv": "^16.0.3",
"electron": "^29.4.6",
diff --git a/patches/react-native-pdf+6.7.3.patch b/patches/react-native-pdf+6.7.3+001+initial.patch
similarity index 100%
rename from patches/react-native-pdf+6.7.3.patch
rename to patches/react-native-pdf+6.7.3+001+initial.patch
diff --git a/patches/react-native-pdf+6.7.3+002+fix-incorrect-decoding.patch b/patches/react-native-pdf+6.7.3+002+fix-incorrect-decoding.patch
new file mode 100644
index 000000000000..1061335b85fe
--- /dev/null
+++ b/patches/react-native-pdf+6.7.3+002+fix-incorrect-decoding.patch
@@ -0,0 +1,13 @@
+diff --git a/node_modules/react-native-pdf/index.js b/node_modules/react-native-pdf/index.js
+index bea7af8..bf767c9 100644
+--- a/node_modules/react-native-pdf/index.js
++++ b/node_modules/react-native-pdf/index.js
+@@ -233,7 +233,7 @@ export default class Pdf extends Component {
+ } else {
+ if (this._mounted) {
+ this.setState({
+- path: unescape(uri.replace(/file:\/\//i, '')),
++ path: decodeURIComponent(uri.replace(/file:\/\//i, '')),
+ isDownloaded: true,
+ });
+ }
diff --git a/scripts/aggregateGitHubDataFromUpwork.ts b/scripts/aggregateGitHubDataFromUpwork.ts
new file mode 100644
index 000000000000..f47b2b43e5cc
--- /dev/null
+++ b/scripts/aggregateGitHubDataFromUpwork.ts
@@ -0,0 +1,175 @@
+/**
+ * This script is used for categorizing upwork costs into cost buckets for accounting purposes.
+ *
+ * To run this script from the root of E/App:
+ *
+ * ts-node ./scripts/aggregateGitHubDataFromUpwork.js
+ *
+ * The input file must be a CSV with a single column containing just the GitHub issue number. The CSV must have a single header row.
+ */
+import {getOctokitOptions, GitHub} from '@actions/github/lib/utils';
+import {paginateRest} from '@octokit/plugin-paginate-rest';
+import {throttling} from '@octokit/plugin-throttling';
+import {createObjectCsvWriter} from 'csv-writer';
+import fs from 'fs';
+
+type OctokitOptions = {method: string; url: string; request: {retryCount: number}};
+type IssueType = 'bug' | 'feature' | 'other';
+
+if (process.argv.length < 3) {
+ throw new Error('Error: must provide filepath for CSV data');
+}
+
+if (process.argv.length < 4) {
+ throw new Error('Error: must provide GitHub token');
+}
+
+if (process.argv.length < 5) {
+ throw new Error('Error: must provide output file path');
+}
+
+// Get filepath for csv
+const inputFilepath = process.argv.at(2);
+if (!inputFilepath) {
+ throw new Error('Error: must provide filepath for CSV data');
+}
+
+// Get GitHub token
+const token = (process.argv.at(3) ?? '').trim();
+if (!token) {
+ throw new Error('Error: must provide GitHub token');
+}
+
+const Octokit = GitHub.plugin(throttling, paginateRest);
+const octokit = new Octokit(
+ getOctokitOptions(token, {
+ throttle: {
+ onRateLimit: (retryAfter: number, options: OctokitOptions) => {
+ console.warn(`Request quota exhausted for request ${options.method} ${options.url}`);
+
+ // Retry once after hitting a rate limit error, then give up
+ if (options.request.retryCount <= 1) {
+ console.log(`Retrying after ${retryAfter} seconds!`);
+ return true;
+ }
+ },
+ onAbuseLimit: (retryAfter: number, options: OctokitOptions) => {
+ // does not retry, only logs a warning
+ console.warn(`Abuse detected for request ${options.method} ${options.url}`);
+ },
+ },
+ }),
+);
+
+// Get output filepath
+const outputFilepath = process.argv.at(4);
+if (!outputFilepath) {
+ throw new Error('Error: must provide output file path');
+}
+
+// Get data from csv
+const issues = fs
+ .readFileSync(inputFilepath)
+ .toString()
+ .split('\n')
+ .reduce((acc, issue) => {
+ if (!issue) {
+ return acc;
+ }
+ const issueNum = Number(issue.trim());
+ if (!issueNum) {
+ return acc;
+ }
+ acc.push(issueNum);
+ return acc;
+ }, [] as number[]);
+
+const csvWriter = createObjectCsvWriter({
+ path: outputFilepath,
+ header: [
+ {id: 'number', title: 'number'},
+ {id: 'title', title: 'title'},
+ {id: 'labels', title: 'labels'},
+ {id: 'type', title: 'type'},
+ {id: 'capSWProjects', title: 'capSWProjects'},
+ ],
+});
+
+function getIssueTypeFromLabels(labels: string[]): IssueType {
+ if (labels.includes('NewFeature')) {
+ return 'feature';
+ }
+ if (labels.includes('Bug')) {
+ return 'bug';
+ }
+ return 'other';
+}
+
+/**
+ * Returns a comma-delimited string with all projects associated with the given issue.
+ */
+async function getProjectsForIssue(issueNumber: number): Promise {
+ const response = await octokit.graphql(
+ `
+ {
+ repository(owner: "Expensify", name: "App") {
+ issue(number: ${issueNumber}) {
+ projectsV2(last: 30) {
+ nodes {
+ title
+ }
+ }
+ }
+ }
+ }
+ `,
+ );
+ return (response as {repository: {issue: {projectsV2: {nodes: Array<{title: string}>}}}}).repository.issue.projectsV2.nodes.map((node) => node.title).join(',');
+}
+
+async function getGitHubData() {
+ const gitHubData = [];
+ // Note: we fetch issues in a loop rather than in parallel to help address rate limiting issues with a PAT
+ for (const issueNumber of issues) {
+ console.info(`Fetching ${issueNumber}`);
+ const result = await octokit.rest.issues
+ .get({
+ owner: 'Expensify',
+ repo: 'App',
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ issue_number: issueNumber,
+ })
+ .catch(() => {
+ console.warn(`Error getting issue ${issueNumber}`);
+ });
+ if (result) {
+ const issue = result.data;
+ const labels = issue.labels.reduce((acc, label) => {
+ if (typeof label === 'string') {
+ acc.push(label);
+ } else if (label.name) {
+ acc.push(label.name);
+ }
+ return acc;
+ }, [] as string[]);
+ const type = getIssueTypeFromLabels(labels);
+ let capSWProjects = '';
+ if (type === 'feature') {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+ capSWProjects = await getProjectsForIssue(issueNumber);
+ }
+ gitHubData.push({
+ number: issue.number,
+ title: issue.title,
+ labels,
+ type,
+ capSWProjects,
+ });
+ }
+ }
+ return gitHubData;
+}
+
+getGitHubData()
+ .then((gitHubData) => csvWriter.writeRecords(gitHubData))
+ .then(() => console.info(`Done ✅ Wrote file to ${outputFilepath}`));
diff --git a/src/CONST.ts b/src/CONST.ts
index 4820bbb62b52..bbc92ddadbde 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1840,6 +1840,7 @@ const CONST = {
DATE_OF_BIRTH: 1,
ADDRESS: 2,
PHONE_NUMBER: 3,
+ CONFIRM: 4,
},
INDEX_LIST: ['1', '2', '3', '4'],
},
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index cb8bf2fdb5d3..fbc7465bc023 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -32,6 +32,7 @@ const ONYXKEYS = {
/** Note: These are Persisted Requests - not all requests in the main queue as the key name might lead one to believe */
PERSISTED_REQUESTS: 'networkRequestQueue',
+ PERSISTED_ONGOING_REQUESTS: 'networkOngoingRequestQueue',
/** Stores current date */
CURRENT_DATE: 'currentDate',
@@ -422,6 +423,9 @@ const ONYXKEYS = {
/** Stores the information about the saved searches */
SAVED_SEARCHES: 'nvp_savedSearches',
+ /** Stores the information about the recent searches */
+ RECENT_SEARCHES: 'nvp_recentSearches',
+
/** Stores recently used currencies */
RECENTLY_USED_CURRENCIES: 'nvp_recentlyUsedCurrencies',
@@ -849,12 +853,14 @@ type OnyxValuesMapping = {
// ONYXKEYS.NVP_TRYNEWDOT is HybridApp onboarding data
[ONYXKEYS.NVP_TRYNEWDOT]: OnyxTypes.TryNewDot;
+ [ONYXKEYS.RECENT_SEARCHES]: Record;
[ONYXKEYS.SAVED_SEARCHES]: OnyxTypes.SaveSearch;
[ONYXKEYS.RECENTLY_USED_CURRENCIES]: string[];
[ONYXKEYS.ACTIVE_CLIENTS]: string[];
[ONYXKEYS.DEVICE_ID]: string;
[ONYXKEYS.IS_SIDEBAR_LOADED]: boolean;
[ONYXKEYS.PERSISTED_REQUESTS]: OnyxTypes.Request[];
+ [ONYXKEYS.PERSISTED_ONGOING_REQUESTS]: OnyxTypes.Request;
[ONYXKEYS.CURRENT_DATE]: string;
[ONYXKEYS.CREDENTIALS]: OnyxTypes.Credentials;
[ONYXKEYS.STASHED_CREDENTIALS]: OnyxTypes.Credentials;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index dfcb42d3c4fe..9c429dd3e909 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -149,6 +149,7 @@ const ROUTES = {
SETTINGS_ABOUT: 'settings/about',
SETTINGS_APP_DOWNLOAD_LINKS: 'settings/about/app-download-links',
SETTINGS_WALLET: 'settings/wallet',
+ SETTINGS_WALLET_VERIFY_ACCOUNT: {route: 'settings/wallet/verify', getRoute: (backTo?: string) => getUrlWithBackToParam('settings/wallet/verify', backTo)},
SETTINGS_WALLET_DOMAINCARD: {
route: 'settings/wallet/card/:cardID?',
getRoute: (cardID: string) => `settings/wallet/card/${cardID}` as const,
@@ -1274,10 +1275,7 @@ const ROUTES = {
route: 'restricted-action/workspace/:policyID',
getRoute: (policyID: string) => `restricted-action/workspace/${policyID}` as const,
},
- MISSING_PERSONAL_DETAILS: {
- route: 'missing-personal-details/workspace/:policyID',
- getRoute: (policyID: string) => `missing-personal-details/workspace/${policyID}` as const,
- },
+ MISSING_PERSONAL_DETAILS: 'missing-personal-details',
POLICY_ACCOUNTING_NETSUITE_SUBSIDIARY_SELECTOR: {
route: 'settings/workspaces/:policyID/accounting/netsuite/subsidiary-selector',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/netsuite/subsidiary-selector` as const,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 395f1c4d5fb1..9a94d612dc80 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -112,6 +112,7 @@ const SCREENS = {
CARD_ACTIVATE: 'Settings_Wallet_Card_Activate',
REPORT_VIRTUAL_CARD_FRAUD: 'Settings_Wallet_ReportVirtualCardFraud',
CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS: 'Settings_Wallet_Cards_Digital_Details_Update_Address',
+ VERIFY_ACCOUNT: 'Settings_Wallet_Verify_Account',
},
EXIT_SURVEY: {
diff --git a/src/components/AddPaymentMethodMenu.tsx b/src/components/AddPaymentMethodMenu.tsx
index 5621c031f959..0057438e3913 100644
--- a/src/components/AddPaymentMethodMenu.tsx
+++ b/src/components/AddPaymentMethodMenu.tsx
@@ -2,26 +2,22 @@ import type {RefObject} from 'react';
import React, {useEffect, useState} from 'react';
import type {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
+import {completePaymentOnboarding} from '@libs/actions/IOU';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {AnchorPosition} from '@src/styles';
-import type {Report, Session} from '@src/types/onyx';
+import type {Report} from '@src/types/onyx';
import type AnchorAlignment from '@src/types/utils/AnchorAlignment';
import * as Expensicons from './Icon/Expensicons';
import type {PaymentMethod} from './KYCWall/types';
import type BaseModalProps from './Modal/types';
import PopoverMenu from './PopoverMenu';
-type AddPaymentMethodMenuOnyxProps = {
- /** Session info for the currently logged-in user. */
- session: OnyxEntry;
-};
-
-type AddPaymentMethodMenuProps = AddPaymentMethodMenuOnyxProps & {
+type AddPaymentMethodMenuProps = {
/** Should the component be visible? */
isVisible: boolean;
@@ -58,11 +54,11 @@ function AddPaymentMethodMenu({
anchorRef,
iouReport,
onItemSelected,
- session,
shouldShowPersonalBankAccountOption = false,
}: AddPaymentMethodMenuProps) {
const {translate} = useLocalize();
const [restoreFocusType, setRestoreFocusType] = useState();
+ const [session] = useOnyx(ONYXKEYS.SESSION);
// Users can choose to pay with business bank account in case of Expense reports or in case of P2P IOU report
// which then starts a bottom up flow and creates a Collect workspace where the payer is an admin and payee is an employee.
@@ -80,6 +76,7 @@ function AddPaymentMethodMenu({
return;
}
+ completePaymentOnboarding(CONST.PAYMENT_SELECTED.PBA);
onItemSelected(CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT);
}, [isPersonalOnlyOption, isVisible, onItemSelected]);
@@ -108,6 +105,7 @@ function AddPaymentMethodMenu({
text: translate('common.personalBankAccount'),
icon: Expensicons.Bank,
onSelected: () => {
+ completePaymentOnboarding(CONST.PAYMENT_SELECTED.PBA);
onItemSelected(CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT);
},
},
@@ -118,7 +116,10 @@ function AddPaymentMethodMenu({
{
text: translate('common.businessBankAccount'),
icon: Expensicons.Building,
- onSelected: () => onItemSelected(CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT),
+ onSelected: () => {
+ completePaymentOnboarding(CONST.PAYMENT_SELECTED.BBA);
+ onItemSelected(CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT);
+ },
},
]
: []),
@@ -140,8 +141,4 @@ function AddPaymentMethodMenu({
AddPaymentMethodMenu.displayName = 'AddPaymentMethodMenu';
-export default withOnyx({
- session: {
- key: ONYXKEYS.SESSION,
- },
-})(AddPaymentMethodMenu);
+export default AddPaymentMethodMenu;
diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx
index 6ac0174da466..04607ef1cc7f 100644
--- a/src/components/Button/index.tsx
+++ b/src/components/Button/index.tsx
@@ -139,13 +139,24 @@ type ButtonProps = Partial & {
/** Whether button's content should be centered */
isContentCentered?: boolean;
+
+ /** Whether the Enter keyboard listening is active whether or not the screen that contains the button is focused */
+ isPressOnEnterActive?: boolean;
};
-type KeyboardShortcutComponentProps = Pick;
+type KeyboardShortcutComponentProps = Pick;
const accessibilityRoles: string[] = Object.values(CONST.ROLE);
-function KeyboardShortcutComponent({isDisabled = false, isLoading = false, onPress = () => {}, pressOnEnter, allowBubble, enterKeyEventListenerPriority}: KeyboardShortcutComponentProps) {
+function KeyboardShortcutComponent({
+ isDisabled = false,
+ isLoading = false,
+ onPress = () => {},
+ pressOnEnter,
+ allowBubble,
+ enterKeyEventListenerPriority,
+ isPressOnEnterActive = false,
+}: KeyboardShortcutComponentProps) {
const isFocused = useIsFocused();
const activeElementRole = useActiveElementRole();
@@ -163,7 +174,7 @@ function KeyboardShortcutComponent({isDisabled = false, isLoading = false, onPre
const config = useMemo(
() => ({
- isActive: pressOnEnter && !shouldDisableEnterShortcut && isFocused,
+ isActive: pressOnEnter && !shouldDisableEnterShortcut && (isFocused || isPressOnEnterActive),
shouldBubble: allowBubble,
priority: enterKeyEventListenerPriority,
shouldPreventDefault: false,
@@ -230,6 +241,7 @@ function Button(
isSplitButton = false,
link = false,
isContentCentered = false,
+ isPressOnEnterActive,
...rest
}: ButtonProps,
ref: ForwardedRef,
@@ -329,6 +341,7 @@ function Button(
onPress={onPress}
pressOnEnter={pressOnEnter}
enterKeyEventListenerPriority={enterKeyEventListenerPriority}
+ isPressOnEnterActive={isPressOnEnterActive}
/>
)}
diff --git a/src/components/ConfirmModal.tsx b/src/components/ConfirmModal.tsx
index 9d6bd3a0a76a..e63b8bb91874 100755
--- a/src/components/ConfirmModal.tsx
+++ b/src/components/ConfirmModal.tsx
@@ -164,6 +164,7 @@ function ConfirmModal({
prompt={prompt}
success={success}
danger={danger}
+ isVisible={isVisible}
shouldDisableConfirmButtonWhenOffline={shouldDisableConfirmButtonWhenOffline}
shouldShowCancelButton={shouldShowCancelButton}
shouldCenterContent={shouldCenterContent}
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 12da12b8b15d..fdf6f8edd825 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -113,10 +113,15 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const isArchivedReport = ReportUtils.isArchivedRoomWithID(moneyRequestReport?.reportID);
const [archiveReason] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID ?? '-1'}`, {selector: ReportUtils.getArchiveReason});
- const shouldShowPayButton = useMemo(
- () => IOU.canIOUBePaid(moneyRequestReport, chatReport, policy, transaction ? [transaction] : undefined),
+ const getCanIOUBePaid = useCallback(
+ (onlyShowPayElsewhere = false) => IOU.canIOUBePaid(moneyRequestReport, chatReport, policy, transaction ? [transaction] : undefined, onlyShowPayElsewhere),
[moneyRequestReport, chatReport, policy, transaction],
);
+ const canIOUBePaid = useMemo(() => getCanIOUBePaid(), [getCanIOUBePaid]);
+
+ const onlyShowPayElsewhere = useMemo(() => !canIOUBePaid && getCanIOUBePaid(true), [canIOUBePaid, getCanIOUBePaid]);
+
+ const shouldShowPayButton = canIOUBePaid || onlyShowPayElsewhere;
const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(moneyRequestReport, policy), [moneyRequestReport, policy]);
@@ -292,6 +297,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
{shouldShowSettlementButton && !shouldUseNarrowLayout && (
{shouldShowSettlementButton && shouldUseNarrowLayout && (
;
-
- policyID: string | undefined;
};
-function IssueCardMessage({action, policyID}: IssueCardMessageProps) {
+function IssueCardMessage({action}: IssueCardMessageProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS);
const [session] = useOnyx(ONYXKEYS.SESSION);
- const assigneeAccountID = (action?.originalMessage as IssueNewCardOriginalMessage)?.assigneeAccountID;
+ const assigneeAccountID = (ReportActionsUtils.getOriginalMessage(action) as IssueNewCardOriginalMessage)?.assigneeAccountID;
const missingDetails =
!privatePersonalDetails?.legalFirstName ||
@@ -45,12 +43,7 @@ function IssueCardMessage({action, policyID}: IssueCardMessageProps) {
${ReportActionsUtils.getCardIssuedMessage(action, true)}`} />
{shouldShowAddMissingDetailsButton && (
{shouldShowSettlementButton && (
void;
+
+ /** Style for the task preview container */
+ style: StyleProp;
};
-function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, checkIfContextMenuActive, currentUserPersonalDetails, isHovered = false}: TaskPreviewProps) {
+function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, checkIfContextMenuActive, currentUserPersonalDetails, isHovered = false, style}: TaskPreviewProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
@@ -70,29 +75,33 @@ function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, che
: action?.childStateNum === CONST.REPORT.STATE_NUM.APPROVED && action?.childStatusNum === CONST.REPORT.STATUS_NUM.APPROVED;
const taskTitle = Str.htmlEncode(TaskUtils.getTaskTitle(taskReportID, action?.childReportName ?? ''));
const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(taskReport) ?? action?.childManagerAccountID ?? -1;
+ const hasAssignee = taskAssigneeAccountID > 0;
const personalDetails = usePersonalDetails();
const avatar = personalDetails?.[taskAssigneeAccountID]?.avatar ?? Expensicons.FallbackAvatar;
- const htmlForTaskPreview = `${taskTitle}`;
+ const avatarSize = CONST.AVATAR_SIZE.SMALL;
const isDeletedParentAction = ReportUtils.isCanceledTaskReport(taskReport, action);
+ const iconWrapperStyle = StyleUtils.getTaskPreviewIconWrapper(hasAssignee ? avatarSize : undefined);
+ const titleStyle = StyleUtils.getTaskPreviewTitleStyle(iconWrapperStyle.height, isTaskCompleted);
+
const shouldShowGreenDotIndicator = ReportUtils.isOpenTaskReport(taskReport, action) && ReportUtils.isReportManager(taskReport);
if (isDeletedParentAction) {
return ${translate('parentReportAction.deletedTask')}`} />;
}
return (
-
+
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(taskReportID))}
onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
onPressOut={() => ControlSelection.unblock()}
onLongPress={(event) => showContextMenuForReport(event, contextMenuAnchor, chatReportID, action, checkIfContextMenuActive)}
shouldUseHapticsOnLongPress
- style={[styles.flexRow, styles.justifyContentBetween]}
+ style={[styles.flexRow, styles.justifyContentBetween, style]}
role={CONST.ROLE.BUTTON}
accessibilityLabel={translate('task.task')}
>
-
-
+
+
- {taskAssigneeAccountID > 0 && (
+ {hasAssignee && (
)}
-
- ${htmlForTaskPreview}` : htmlForTaskPreview} />
-
+ {taskTitle}
{shouldShowGreenDotIndicator && (
@@ -131,6 +138,7 @@ function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, che
diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx
index 5fff92213969..f50540346e6d 100644
--- a/src/components/Search/SearchPageHeader.tsx
+++ b/src/components/Search/SearchPageHeader.tsx
@@ -69,12 +69,15 @@ function HeaderWrapper({icon, children, text, isCannedQuery}: HeaderWrapperProps
) : (
{}}
+ updateSearch={() => {}}
disabled
isFullWidth
wrapperStyle={[styles.searchRouterInputResults, styles.br2]}
wrapperFocusedStyle={styles.searchRouterInputResultsFocused}
- defaultValue={text}
rightComponent={children}
+ routerListRef={undefined}
/>
)}
diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx
index dfe2cbbe16c6..b3f147b7ac28 100644
--- a/src/components/Search/SearchRouter/SearchRouter.tsx
+++ b/src/components/Search/SearchRouter/SearchRouter.tsx
@@ -1,71 +1,167 @@
+import {useNavigationState} from '@react-navigation/native';
import debounce from 'lodash/debounce';
-import React, {useCallback, useState} from 'react';
+import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Modal from '@components/Modal';
+import {useOptionsList} from '@components/OptionListContextProvider';
import type {SearchQueryJSON} from '@components/Search/types';
+import type {SelectionListHandle} from '@components/SelectionList/types';
+import useDebouncedState from '@hooks/useDebouncedState';
import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
+import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
+import Log from '@libs/Log';
+import * as OptionsListUtils from '@libs/OptionsListUtils';
+import type {OptionData} from '@libs/ReportUtils';
import * as SearchUtils from '@libs/SearchUtils';
import Navigation from '@navigation/Navigation';
+import variables from '@styles/variables';
+import * as Report from '@userActions/Report';
import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import {useSearchRouterContext} from './SearchRouterContext';
import SearchRouterInput from './SearchRouterInput';
+import SearchRouterList from './SearchRouterList';
const SEARCH_DEBOUNCE_DELAY = 150;
function SearchRouter() {
const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const [betas] = useOnyx(ONYXKEYS.BETAS);
+ const [recentSearches] = useOnyx(ONYXKEYS.RECENT_SEARCHES);
+ const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false});
const {isSmallScreenWidth} = useResponsiveLayout();
const {isSearchRouterDisplayed, closeSearchRouter} = useSearchRouterContext();
+ const listRef = useRef(null);
+ const [textInputValue, debouncedInputValue, setTextInputValue] = useDebouncedState('', 500);
const [userSearchQuery, setUserSearchQuery] = useState(undefined);
-
- const clearUserQuery = () => {
- setUserSearchQuery(undefined);
- };
-
- const onSearchChange = debounce((userQuery: string) => {
- if (!userQuery) {
- clearUserQuery();
- return;
+ const contextualReportID = useNavigationState, string | undefined>((state) => {
+ return state?.routes.at(-1)?.params?.reportID;
+ });
+ const sortedRecentSearches = useMemo(() => {
+ return Object.values(recentSearches ?? {}).sort((a, b) => b.timestamp.localeCompare(a.timestamp));
+ }, [recentSearches]);
+
+ const {options, areOptionsInitialized} = useOptionsList();
+ const searchOptions = useMemo(() => {
+ if (!areOptionsInitialized) {
+ return {recentReports: [], personalDetails: [], userToInvite: null, currentUserOption: null, categoryOptions: [], tagOptions: [], taxRatesOptions: []};
+ }
+ return OptionsListUtils.getSearchOptions(options, '', betas ?? []);
+ }, [areOptionsInitialized, betas, options]);
+
+ const filteredOptions = useMemo(() => {
+ if (debouncedInputValue.trim() === '') {
+ return {
+ recentReports: [],
+ personalDetails: [],
+ userToInvite: null,
+ };
}
- const queryJSON = SearchUtils.buildSearchQueryJSON(userQuery);
+ const newOptions = OptionsListUtils.filterOptions(searchOptions, debouncedInputValue, {sortByReportTypeInSearch: true, preferChatroomsOverThreads: true});
- if (queryJSON) {
- // eslint-disable-next-line
- console.log('parsedQuery', queryJSON);
+ return {
+ recentReports: newOptions.recentReports,
+ personalDetails: newOptions.personalDetails,
+ userToInvite: newOptions.userToInvite,
+ };
+ }, [debouncedInputValue, searchOptions]);
- setUserSearchQuery(queryJSON);
- } else {
- // Handle query parsing error
+ const recentReports: OptionData[] = useMemo(() => {
+ const currentSearchOptions = debouncedInputValue === '' ? searchOptions : filteredOptions;
+ const reports: OptionData[] = [...currentSearchOptions.recentReports, ...currentSearchOptions.personalDetails];
+ if (currentSearchOptions.userToInvite) {
+ reports.push(currentSearchOptions.userToInvite);
}
- }, SEARCH_DEBOUNCE_DELAY);
+ return reports.slice(0, 10);
+ }, [debouncedInputValue, filteredOptions, searchOptions]);
- const onSearchSubmit = useCallback(() => {
- if (!userSearchQuery) {
+ useEffect(() => {
+ Report.searchInServer(debouncedInputValue.trim());
+ }, [debouncedInputValue]);
+
+ useEffect(() => {
+ if (!textInputValue && isSearchRouterDisplayed) {
return;
}
+ listRef.current?.updateAndScrollToFocusedIndex(0);
+ // eslint-disable-next-line react-compiler/react-compiler
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isSearchRouterDisplayed]);
- closeSearchRouter();
+ const contextualReportData = contextualReportID ? searchOptions.recentReports?.find((option) => option.reportID === contextualReportID) : undefined;
- const query = SearchUtils.buildSearchQueryString(userSearchQuery);
- Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query}));
+ const clearUserQuery = () => {
+ setTextInputValue('');
+ setUserSearchQuery(undefined);
+ };
+ const onSearchChange = useMemo(
+ // eslint-disable-next-line react-compiler/react-compiler
+ () =>
+ debounce((userQuery: string) => {
+ if (!userQuery) {
+ clearUserQuery();
+ listRef.current?.updateAndScrollToFocusedIndex(-1);
+ return;
+ }
+ listRef.current?.updateAndScrollToFocusedIndex(0);
+ const queryJSON = SearchUtils.buildSearchQueryJSON(userQuery);
+
+ if (queryJSON) {
+ setUserSearchQuery(queryJSON);
+ } else {
+ Log.alert(`${CONST.ERROR.ENSURE_BUGBOT} user query failed to parse`, userQuery, false);
+ }
+ }, SEARCH_DEBOUNCE_DELAY),
+ // eslint-disable-next-line react-compiler/react-compiler
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [],
+ );
+
+ const updateUserSearchQuery = (newSearchQuery: string) => {
+ setTextInputValue(newSearchQuery);
+ onSearchChange(newSearchQuery);
+ };
+
+ const closeAndClearRouter = useCallback(() => {
+ closeSearchRouter();
clearUserQuery();
- }, [closeSearchRouter, userSearchQuery]);
+ // eslint-disable-next-line react-compiler/react-compiler
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [closeSearchRouter]);
+
+ const onSearchSubmit = useCallback(
+ (query: SearchQueryJSON | undefined) => {
+ if (!query) {
+ return;
+ }
+ closeSearchRouter();
+ const queryString = SearchUtils.buildSearchQueryString(query);
+ Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: queryString}));
+ clearUserQuery();
+ },
+ // eslint-disable-next-line react-compiler/react-compiler
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [closeSearchRouter],
+ );
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => {
closeSearchRouter();
clearUserQuery();
});
- const modalType = isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.CENTERED : CONST.MODAL.MODAL_TYPE.POPOVER;
- const isFullWidth = isSmallScreenWidth;
+ const modalType = isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE : CONST.MODAL.MODAL_TYPE.POPOVER;
+ const modalWidth = isSmallScreenWidth ? styles.w100 : {width: variables.popoverWidth};
return (
-
+
+ {isSmallScreenWidth && (
+ closeSearchRouter()}
+ />
+ )}
+
diff --git a/src/components/Search/SearchRouter/SearchRouterInput.tsx b/src/components/Search/SearchRouter/SearchRouterInput.tsx
index 046386416259..460ff37c88b2 100644
--- a/src/components/Search/SearchRouter/SearchRouterInput.tsx
+++ b/src/components/Search/SearchRouter/SearchRouterInput.tsx
@@ -1,25 +1,30 @@
import React, {useState} from 'react';
-import type {ReactNode} from 'react';
+import type {ReactNode, RefObject} from 'react';
import {View} from 'react-native';
import type {StyleProp, ViewStyle} from 'react-native';
+import type {SelectionListHandle} from '@components/SelectionList/types';
import TextInput from '@components/TextInput';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
import CONST from '@src/CONST';
type SearchRouterInputProps = {
- /** Callback triggered when the input text changes */
- onChange?: (searchTerm: string) => void;
+ /** Value of TextInput */
+ value: string;
- /** Callback invoked when the user submits the input */
- onSubmit?: () => void;
+ /** Setter to TextInput value */
+ setValue: (searchTerm: string) => void;
+
+ /** Callback to update search in SearchRouter */
+ updateSearch: (searchTerm: string) => void;
+
+ /** SearchRouterList ref for managing TextInput and SearchRouterList focus */
+ routerListRef?: RefObject;
/** Whether the input is full width */
isFullWidth: boolean;
- /** Default value for text input */
- defaultValue?: string;
-
/** Whether the input is disabled */
disabled?: boolean;
@@ -31,37 +36,58 @@ type SearchRouterInputProps = {
/** Component to be displayed on the right */
rightComponent?: ReactNode;
+
+ /** Whether the search reports API call is running */
+ isSearchingForReports?: boolean;
};
-function SearchRouterInput({isFullWidth, onChange, onSubmit, defaultValue = '', disabled = false, wrapperStyle, wrapperFocusedStyle, rightComponent}: SearchRouterInputProps) {
+function SearchRouterInput({
+ value,
+ setValue,
+ updateSearch,
+ routerListRef,
+ isFullWidth,
+ disabled = false,
+ wrapperStyle,
+ wrapperFocusedStyle,
+ rightComponent,
+ isSearchingForReports,
+}: SearchRouterInputProps) {
const styles = useThemeStyles();
-
- const [value, setValue] = useState(defaultValue);
+ const {translate} = useLocalize();
const [isFocused, setIsFocused] = useState(false);
const onChangeText = (text: string) => {
setValue(text);
- onChange?.(text);
+ updateSearch(text);
};
const inputWidth = isFullWidth ? styles.w100 : {width: variables.popoverWidth};
return (
-
+
setIsFocused(true)}
- onBlur={() => setIsFocused(false)}
+ onFocus={() => {
+ setIsFocused(true);
+ routerListRef?.current?.updateExternalTextInputFocus(true);
+ }}
+ onBlur={() => {
+ setIsFocused(false);
+ routerListRef?.current?.updateExternalTextInputFocus(false);
+ }}
+ isLoading={!!isSearchingForReports}
/>
{rightComponent && {rightComponent}}
diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx
new file mode 100644
index 000000000000..96c11b2fa353
--- /dev/null
+++ b/src/components/Search/SearchRouter/SearchRouterList.tsx
@@ -0,0 +1,184 @@
+import React, {forwardRef, useCallback} from 'react';
+import type {ForwardedRef} from 'react';
+import {useOnyx} from 'react-native-onyx';
+import * as Expensicons from '@components/Icon/Expensicons';
+import {usePersonalDetails} from '@components/OnyxProvider';
+import type {SearchQueryJSON} from '@components/Search/types';
+import SelectionList from '@components/SelectionList';
+import SearchQueryListItem from '@components/SelectionList/Search/SearchQueryListItem';
+import type {SearchQueryItem, SearchQueryListItemProps} from '@components/SelectionList/Search/SearchQueryListItem';
+import type {SectionListDataType, SelectionListHandle, UserListItemProps} from '@components/SelectionList/types';
+import UserListItem from '@components/SelectionList/UserListItem';
+import useLocalize from '@hooks/useLocalize';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import {getAllTaxRates} from '@libs/PolicyUtils';
+import type {OptionData} from '@libs/ReportUtils';
+import * as SearchUtils from '@libs/SearchUtils';
+import * as Report from '@userActions/Report';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+
+type ItemWithQuery = {
+ query: string;
+};
+
+type SearchRouterListProps = {
+ /** currentQuery value computed coming from parsed TextInput value */
+ currentQuery: SearchQueryJSON | undefined;
+
+ /** Recent searches */
+ recentSearches: ItemWithQuery[] | undefined;
+
+ /** Recent reports */
+ recentReports: OptionData[];
+
+ /** Callback to submit query when selecting a list item */
+ onSearchSubmit: (query: SearchQueryJSON | undefined) => void;
+
+ /** Context present when opening SearchRouter from a report, invoice or workspace page */
+ reportForContextualSearch?: OptionData;
+
+ /** Callback to update search query when selecting contextual suggestion */
+ updateUserSearchQuery: (newSearchQuery: string) => void;
+
+ /** Callback to close and clear SearchRouter */
+ closeAndClearRouter: () => void;
+};
+
+function isSearchQueryItem(item: OptionData | SearchQueryItem): item is SearchQueryItem {
+ if ('singleIcon' in item && item.singleIcon && 'query' in item && item.query) {
+ return true;
+ }
+ return false;
+}
+
+function isSearchQueryListItem(listItem: UserListItemProps | SearchQueryListItemProps): listItem is SearchQueryListItemProps {
+ return isSearchQueryItem(listItem.item);
+}
+
+function SearchRouterItem(props: UserListItemProps | SearchQueryListItemProps) {
+ const styles = useThemeStyles();
+
+ if (isSearchQueryListItem(props)) {
+ return (
+
+ );
+ }
+ return (
+
+ );
+}
+
+function SearchRouterList(
+ {currentQuery, reportForContextualSearch, recentSearches, recentReports, onSearchSubmit, updateUserSearchQuery, closeAndClearRouter}: SearchRouterListProps,
+ ref: ForwardedRef,
+) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const {isSmallScreenWidth} = useResponsiveLayout();
+
+ const personalDetails = usePersonalDetails();
+ const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
+ const taxRates = getAllTaxRates();
+ const [cardList = {}] = useOnyx(ONYXKEYS.CARD_LIST);
+ const contextualQuery = `in:${reportForContextualSearch?.reportID}`;
+ const sections: Array> = [];
+
+ if (currentQuery?.inputQuery) {
+ sections.push({
+ data: [
+ {
+ text: currentQuery?.inputQuery,
+ singleIcon: Expensicons.MagnifyingGlass,
+ query: currentQuery?.inputQuery,
+ itemStyle: styles.activeComponentBG,
+ keyForList: 'findItem',
+ },
+ ],
+ });
+ }
+
+ if (reportForContextualSearch && !currentQuery?.inputQuery?.includes(contextualQuery)) {
+ sections.push({
+ data: [
+ {
+ text: `${translate('search.searchIn')} ${reportForContextualSearch.text ?? reportForContextualSearch.alternateText}`,
+ singleIcon: Expensicons.MagnifyingGlass,
+ query: SearchUtils.getContextualSuggestionQuery(reportForContextualSearch.reportID),
+ itemStyle: styles.activeComponentBG,
+ keyForList: 'contextualSearch',
+ isContextualSearchItem: true,
+ },
+ ],
+ });
+ }
+
+ const recentSearchesData = recentSearches?.map(({query}) => {
+ const searchQueryJSON = SearchUtils.buildSearchQueryJSON(query);
+ return {
+ text: searchQueryJSON ? SearchUtils.getSearchHeaderTitle(searchQueryJSON, personalDetails, cardList, reports, taxRates) : query,
+ singleIcon: Expensicons.History,
+ query,
+ keyForList: query,
+ };
+ });
+
+ if (!currentQuery?.inputQuery && recentSearchesData && recentSearchesData.length > 0) {
+ sections.push({title: translate('search.recentSearches'), data: recentSearchesData});
+ }
+
+ const styledRecentReports = recentReports.map((item) => ({...item, pressableStyle: styles.br2}));
+ sections.push({title: translate('search.recentChats'), data: styledRecentReports});
+
+ const onSelectRow = useCallback(
+ (item: OptionData | SearchQueryItem) => {
+ if (isSearchQueryItem(item)) {
+ if (item.isContextualSearchItem) {
+ // Handle selection of "Contextual search suggestion"
+ updateUserSearchQuery(`${item?.query} ${currentQuery?.inputQuery ?? ''}`);
+ return;
+ }
+
+ // Handle selection of "Recent search"
+ if (!item?.query) {
+ return;
+ }
+ onSearchSubmit(SearchUtils.buildSearchQueryJSON(item?.query));
+ }
+
+ // Handle selection of "Recent chat"
+ closeAndClearRouter();
+ if ('reportID' in item && item?.reportID) {
+ Navigation.closeAndNavigate(ROUTES.REPORT_WITH_ID.getRoute(item?.reportID));
+ } else if ('login' in item) {
+ Report.navigateToAndOpenReport(item?.login ? [item.login] : []);
+ }
+ },
+ [closeAndClearRouter, onSearchSubmit, currentQuery, updateUserSearchQuery],
+ );
+
+ return (
+
+ sections={sections}
+ onSelectRow={onSelectRow}
+ ListItem={SearchRouterItem}
+ containerStyle={[styles.mh100]}
+ sectionListStyle={[isSmallScreenWidth ? styles.ph5 : styles.ph2, styles.pb2]}
+ ref={ref}
+ />
+ );
+}
+
+export default forwardRef(SearchRouterList);
+export {SearchRouterItem};
+export type {ItemWithQuery};
diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx
index 5f43f8088fc3..b95865b37d02 100644
--- a/src/components/SelectionList/BaseListItem.tsx
+++ b/src/components/SelectionList/BaseListItem.tsx
@@ -4,13 +4,11 @@ import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
-import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle';
import useHover from '@hooks/useHover';
import {useMouseContext} from '@hooks/useMouseContext';
import useSyncFocus from '@hooks/useSyncFocus';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import variables from '@styles/variables';
import CONST from '@src/CONST';
import type {BaseListItemProps, ListItem} from './types';
@@ -18,6 +16,7 @@ function BaseListItem({
item,
pressableStyle,
wrapperStyle,
+ pressableWrapperStyle,
containerStyle,
isDisabled = false,
shouldPreventEnterKeySubmit = false,
@@ -36,7 +35,6 @@ function BaseListItem({
onFocus = () => {},
hoverStyle,
onLongPressRow,
- hasAnimateInHighlightStyle = false,
}: BaseListItemProps) {
const theme = useTheme();
const styles = useThemeStyles();
@@ -64,13 +62,6 @@ function BaseListItem({
return rightHandSideComponent;
};
- const animatedHighlightStyle = useAnimatedHighlightStyle({
- borderRadius: variables.componentBorderRadius,
- shouldHighlight: item?.shouldAnimateInHighlight ?? false,
- highlightColor: theme.messageHighlightBG,
- backgroundColor: theme.highlightBG,
- });
-
return (
onDismissError(item)}
@@ -109,7 +100,7 @@ function BaseListItem({
onFocus={onFocus}
onMouseLeave={handleMouseLeave}
tabIndex={item.tabIndex}
- wrapperStyle={hasAnimateInHighlightStyle ? [styles.mh5, animatedHighlightStyle] : []}
+ wrapperStyle={pressableWrapperStyle}
>
{typeof children === 'function' ? children(hovered) : children}
diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx
index 36b2cf873416..0643b0b1ba36 100644
--- a/src/components/SelectionList/BaseSelectionList.tsx
+++ b/src/components/SelectionList/BaseSelectionList.tsx
@@ -101,6 +101,7 @@ function BaseSelectionList(
onLongPressRow,
shouldShowTextInput = !!textInputLabel || !!textInputIconLeft,
shouldShowListEmptyContent = true,
+ shouldIgnoreFocus = false,
scrollEventThrottle,
contentContainerStyle,
}: BaseSelectionListProps,
@@ -468,7 +469,7 @@ function BaseSelectionList(
isAlternateTextMultilineSupported={isAlternateTextMultilineSupported}
alternateTextNumberOfLines={alternateTextNumberOfLines}
onFocus={() => {
- if (isDisabled) {
+ if (shouldIgnoreFocus || isDisabled) {
return;
}
setFocusedIndex(normalizedIndex);
@@ -619,7 +620,22 @@ function BaseSelectionList(
[flattenedSections.allOptions, setFocusedIndex, updateAndScrollToFocusedIndex],
);
- useImperativeHandle(ref, () => ({scrollAndHighlightItem, clearInputAfterSelect, scrollToIndex}), [scrollAndHighlightItem, clearInputAfterSelect, scrollToIndex]);
+ /**
+ * Handles isTextInputFocusedRef value when using external TextInput, so external TextInput is not defocused when typing in it.
+ *
+ * @param isTextInputFocused - Is external TextInput focused.
+ */
+ const updateExternalTextInputFocus = useCallback((isTextInputFocused: boolean) => {
+ isTextInputFocusedRef.current = isTextInputFocused;
+ }, []);
+
+ useImperativeHandle(ref, () => ({scrollAndHighlightItem, clearInputAfterSelect, updateAndScrollToFocusedIndex, updateExternalTextInputFocus, scrollToIndex}), [
+ scrollAndHighlightItem,
+ clearInputAfterSelect,
+ updateAndScrollToFocusedIndex,
+ updateExternalTextInputFocus,
+ scrollToIndex,
+ ]);
/** Selects row when pressing Enter */
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedOption, {
diff --git a/src/components/SelectionList/ChatListItem.tsx b/src/components/SelectionList/ChatListItem.tsx
index 8322c7e1406a..6e9fe0ddc35b 100644
--- a/src/components/SelectionList/ChatListItem.tsx
+++ b/src/components/SelectionList/ChatListItem.tsx
@@ -1,6 +1,7 @@
-import React from 'react';
+import React, {useMemo} from 'react';
import {View} from 'react-native';
import {AttachmentContext} from '@components/AttachmentContext';
+import MentionReportContext from '@components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext';
import MultipleAvatars from '@components/MultipleAvatars';
import {ShowContextMenuContext} from '@components/ShowContextMenuContext';
import TextWithTooltip from '@components/TextWithTooltip';
@@ -54,6 +55,8 @@ function ChatListItem({
const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor;
const hoveredBackgroundColor = styles.sidebarLinkHover?.backgroundColor ? styles.sidebarLinkHover.backgroundColor : theme.sidebar;
+ const mentionReportContextValue = useMemo(() => ({currentReportID: item?.reportID ?? '-1'}), [item.reportID]);
+
return (
({
hoverStyle={item.isSelected && styles.activeComponentBG}
>
{(hovered) => (
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ {reportActionItem.message.map((fragment, index) => (
+
+ ))}
-
-
-
- {reportActionItem.message.map((fragment, index) => (
-
- ))}
-
-
-
+
+
+
)}
);
diff --git a/src/components/SelectionList/Search/ReportListItem.tsx b/src/components/SelectionList/Search/ReportListItem.tsx
index ea36ba44ccca..2cc3e7d5b2a4 100644
--- a/src/components/SelectionList/Search/ReportListItem.tsx
+++ b/src/components/SelectionList/Search/ReportListItem.tsx
@@ -5,11 +5,14 @@ import BaseListItem from '@components/SelectionList/BaseListItem';
import type {ListItem, ReportListItemProps, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
import Text from '@components/Text';
import TextWithTooltip from '@components/TextWithTooltip';
+import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
+import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import Navigation from '@libs/Navigation/Navigation';
+import variables from '@styles/variables';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import ActionCell from './ActionCell';
@@ -62,10 +65,18 @@ function ReportListItem({
}: ReportListItemProps) {
const reportItem = item as unknown as ReportListItemType;
+ const theme = useTheme();
const styles = useThemeStyles();
const {isLargeScreenWidth} = useResponsiveLayout();
const StyleUtils = useStyleUtils();
+ const animatedHighlightStyle = useAnimatedHighlightStyle({
+ borderRadius: variables.componentBorderRadius,
+ shouldHighlight: item?.shouldAnimateInHighlight ?? false,
+ highlightColor: theme.messageHighlightBG,
+ backgroundColor: theme.highlightBG,
+ });
+
if (reportItem.transactions.length === 0) {
return;
}
@@ -75,10 +86,10 @@ function ReportListItem({
styles.pv1half,
styles.ph0,
styles.overflowHidden,
+ // Removing background style because they are added to the parent OpacityView via animatedHighlightStyle
+ styles.bgTransparent,
item.isSelected && styles.activeComponentBG,
isFocused && styles.sidebarLinkActive,
- // Removing some of the styles because they are added to the parent OpacityView via animatedHighlightStyle
- {backgroundColor: 'unset'},
styles.mh0,
];
@@ -143,7 +154,7 @@ function ReportListItem({
onFocus={onFocus}
shouldSyncFocus={shouldSyncFocus}
hoverStyle={item.isSelected && styles.activeComponentBG}
- hasAnimateInHighlightStyle
+ pressableWrapperStyle={[styles.mh5, animatedHighlightStyle]}
>
{!isLargeScreenWidth && (
diff --git a/src/components/SelectionList/Search/SearchQueryListItem.tsx b/src/components/SelectionList/Search/SearchQueryListItem.tsx
new file mode 100644
index 000000000000..369f527cdeba
--- /dev/null
+++ b/src/components/SelectionList/Search/SearchQueryListItem.tsx
@@ -0,0 +1,81 @@
+import React from 'react';
+import {View} from 'react-native';
+import Icon from '@components/Icon';
+import BaseListItem from '@components/SelectionList/BaseListItem';
+import type {ListItem} from '@components/SelectionList/types';
+import TextWithTooltip from '@components/TextWithTooltip';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import type IconAsset from '@src/types/utils/IconAsset';
+
+type SearchQueryItem = ListItem & {
+ singleIcon?: IconAsset;
+ query?: string;
+ isContextualSearchItem?: boolean;
+};
+
+type SearchQueryListItemProps = {
+ item: SearchQueryItem;
+ isFocused?: boolean;
+ showTooltip: boolean;
+ onSelectRow: (item: SearchQueryItem) => void;
+ onFocus?: () => void;
+ shouldSyncFocus?: boolean;
+};
+
+function SearchQueryListItem({item, isFocused, showTooltip, onSelectRow, onFocus, shouldSyncFocus}: SearchQueryListItemProps) {
+ const styles = useThemeStyles();
+ const theme = useTheme();
+
+ return (
+
+ <>
+ {item.singleIcon && (
+
+ )}
+
+
+ {item.alternateText && (
+
+ )}
+
+ >
+
+ );
+}
+
+SearchQueryListItem.displayName = 'SearchQueryListItem';
+
+export default SearchQueryListItem;
+export type {SearchQueryItem, SearchQueryListItemProps};
diff --git a/src/components/SelectionList/Search/TransactionListItem.tsx b/src/components/SelectionList/Search/TransactionListItem.tsx
index 39172711516e..9259583c9f9d 100644
--- a/src/components/SelectionList/Search/TransactionListItem.tsx
+++ b/src/components/SelectionList/Search/TransactionListItem.tsx
@@ -1,8 +1,11 @@
import React from 'react';
import BaseListItem from '@components/SelectionList/BaseListItem';
import type {ListItem, TransactionListItemProps, TransactionListItemType} from '@components/SelectionList/types';
+import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import variables from '@styles/variables';
import TransactionListItemRow from './TransactionListItemRow';
function TransactionListItem({
@@ -20,6 +23,7 @@ function TransactionListItem({
}: TransactionListItemProps) {
const transactionItem = item as unknown as TransactionListItemType;
const styles = useThemeStyles();
+ const theme = useTheme();
const {isLargeScreenWidth} = useResponsiveLayout();
@@ -27,10 +31,10 @@ function TransactionListItem({
styles.selectionListPressableItemWrapper,
styles.pv3,
styles.ph3,
+ // Removing background style because they are added to the parent OpacityView via animatedHighlightStyle
+ styles.bgTransparent,
item.isSelected && styles.activeComponentBG,
isFocused && styles.sidebarLinkActive,
- // Removing some of the styles because they are added to the parent OpacityView via animatedHighlightStyle
- {backgroundColor: 'unset'},
styles.mh0,
];
@@ -40,6 +44,13 @@ function TransactionListItem({
isLargeScreenWidth ? {...styles.flexRow, ...styles.justifyContentBetween, ...styles.alignItemsCenter} : {...styles.flexColumn, ...styles.alignItemsStretch},
];
+ const animatedHighlightStyle = useAnimatedHighlightStyle({
+ borderRadius: variables.componentBorderRadius,
+ shouldHighlight: item?.shouldAnimateInHighlight ?? false,
+ highlightColor: theme.messageHighlightBG,
+ backgroundColor: theme.highlightBG,
+ });
+
return (
({
onLongPressRow={onLongPressRow}
shouldSyncFocus={shouldSyncFocus}
hoverStyle={item.isSelected && styles.activeComponentBG}
- hasAnimateInHighlightStyle
+ pressableWrapperStyle={[styles.mh5, animatedHighlightStyle]}
>
({
rightHandSideComponent,
onFocus,
shouldSyncFocus,
+ wrapperStyle,
pressableStyle,
}: UserListItemProps) {
const styles = useThemeStyles();
@@ -60,7 +61,7 @@ function UserListItem({
return (
({
rightHandSideComponent={rightHandSideComponent}
errors={item.errors}
pendingAction={item.pendingAction}
- pressableStyle={pressableStyle}
+ pressableStyle={[isFocused && styles.sidebarLinkActive, pressableStyle]}
FooterComponent={
item.invitedSecondaryLogin ? (
diff --git a/src/components/SelectionList/index.tsx b/src/components/SelectionList/index.tsx
index b719737a963b..fc788a7e2b4b 100644
--- a/src/components/SelectionList/index.tsx
+++ b/src/components/SelectionList/index.tsx
@@ -1,6 +1,7 @@
import React, {forwardRef, useEffect, useState} from 'react';
import type {ForwardedRef} from 'react';
import {Keyboard} from 'react-native';
+import * as Browser from '@libs/Browser';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import BaseSelectionList from './BaseSelectionList';
import type {BaseSelectionListProps, ListItem, SelectionListHandle} from './types';
@@ -42,6 +43,9 @@ function SelectionList({onScroll, ...props}: BaseSelecti
{...props}
ref={ref}
onScroll={onScroll ?? defaultOnScroll}
+ // Ignore the focus if it's caused by a touch event on mobile chrome.
+ // For example, a long press will trigger a focus event on mobile chrome.
+ shouldIgnoreFocus={Browser.isMobileChrome() && isScreenTouched}
/>
);
}
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index 089a5ac5d99c..8625dbde7093 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -11,6 +11,8 @@ import type {
TextStyle,
ViewStyle,
} from 'react-native';
+import type {AnimatedStyle} from 'react-native-reanimated';
+import type {SearchRouterItem} from '@components/Search/SearchRouter/SearchRouterList';
import type {BrickRoad} from '@libs/WorkspacesSettingsUtils';
// eslint-disable-next-line no-restricted-imports
import type CursorStyles from '@styles/utils/cursor/types';
@@ -24,6 +26,7 @@ import type ChatListItem from './ChatListItem';
import type InviteMemberListItem from './InviteMemberListItem';
import type RadioListItem from './RadioListItem';
import type ReportListItem from './Search/ReportListItem';
+import type SearchQueryListItem from './Search/SearchQueryListItem';
import type TransactionListItem from './Search/TransactionListItem';
import type TableListItem from './TableListItem';
import type UserListItem from './UserListItem';
@@ -58,6 +61,9 @@ type CommonListItemProps = {
/** Styles for the pressable component */
pressableStyle?: StyleProp;
+ /** Styles for the pressable component wrapper view */
+ pressableWrapperStyle?: StyleProp>;
+
/** Styles for the wrapper view */
wrapperStyle?: StyleProp;
@@ -178,6 +184,9 @@ type ListItem = {
/** Determines whether the newly added item should animate in / highlight */
shouldAnimateInHighlight?: boolean;
+
+ /** The style to override the default appearance */
+ itemStyle?: StyleProp;
};
type TransactionListItemType = ListItem &
@@ -291,7 +300,6 @@ type BaseListItemProps = CommonListItemProps & {
children?: ReactElement> | ((hovered: boolean) => ReactElement>);
shouldSyncFocus?: boolean;
hoverStyle?: StyleProp;
- hasAnimateInHighlightStyle?: boolean;
/** Errors that this user may contain */
shouldDisplayRBR?: boolean;
};
@@ -326,7 +334,9 @@ type ValidListItem =
| typeof InviteMemberListItem
| typeof TransactionListItem
| typeof ReportListItem
- | typeof ChatListItem;
+ | typeof ChatListItem
+ | typeof SearchQueryListItem
+ | typeof SearchRouterItem;
type Section = {
/** Title of the section */
@@ -489,6 +499,9 @@ type BaseSelectionListProps = Partial & {
/** Styles to apply to SectionList component */
sectionListStyle?: StyleProp;
+ /** Whether to ignore the focus event */
+ shouldIgnoreFocus?: boolean;
+
/** Whether focus event should be delayed */
shouldDelayFocus?: boolean;
@@ -571,6 +584,8 @@ type SelectionListHandle = {
scrollAndHighlightItem?: (items: string[], timeout: number) => void;
clearInputAfterSelect?: () => void;
scrollToIndex: (index: number, animated?: boolean) => void;
+ updateAndScrollToFocusedIndex: (newFocusedIndex: number) => void;
+ updateExternalTextInputFocus: (isTextInputFocused: boolean) => void;
};
type ItemLayout = {
diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx
index 35e152ec99d8..8d3add9b6fd0 100644
--- a/src/components/SettlementButton/index.tsx
+++ b/src/components/SettlementButton/index.tsx
@@ -58,6 +58,7 @@ function SettlementButton({
useKeyboardShortcuts = false,
onPaymentOptionsShow,
onPaymentOptionsHide,
+ onlyShowPayElsewhere,
}: SettlementButtonProps) {
const {translate} = useLocalize();
const {isOffline} = useNetwork();
@@ -102,6 +103,10 @@ function SettlementButton({
return [approveButtonOption];
}
+ if (onlyShowPayElsewhere) {
+ return [paymentMethods[CONST.IOU.PAYMENT_TYPE.ELSEWHERE]];
+ }
+
// To achieve the one tap pay experience we need to choose the correct payment type as default.
if (canUseWallet) {
buttonOptions.push(paymentMethods[CONST.IOU.PAYMENT_TYPE.EXPENSIFY]);
@@ -173,6 +178,7 @@ function SettlementButton({
shouldShowPayElsewhereOption,
chatReport,
onPress,
+ onlyShowPayElsewhere,
]);
const selectPaymentType = (event: KYCFlowEvent, iouPaymentType: PaymentMethodType, triggerKYCFlow: TriggerKYCFlow) => {
diff --git a/src/components/SettlementButton/types.ts b/src/components/SettlementButton/types.ts
index 0a26aec914e0..b3ad0c1c9bd0 100644
--- a/src/components/SettlementButton/types.ts
+++ b/src/components/SettlementButton/types.ts
@@ -87,6 +87,9 @@ type SettlementButtonProps = {
/** Whether to use keyboard shortcuts for confirmation or not */
useKeyboardShortcuts?: boolean;
+
+ /** Whether we only show pay elsewhere button */
+ onlyShowPayElsewhere?: boolean;
};
export default SettlementButtonProps;
diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx
index a03e9dbb9aa2..93755cf0ce40 100644
--- a/src/components/TextInput/BaseTextInput/index.native.tsx
+++ b/src/components/TextInput/BaseTextInput/index.native.tsx
@@ -66,6 +66,7 @@ function BaseTextInput(
prefixContainerStyle = [],
prefixStyle = [],
contentWidth,
+ loadingSpinnerStyle,
...props
}: BaseTextInputProps,
ref: ForwardedRef,
@@ -379,7 +380,7 @@ function BaseTextInput(
)}
{!!inputProps.secureTextEntry && (
diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx
index bfc3ab213dd0..0bfe3a46365d 100644
--- a/src/components/TextInput/BaseTextInput/index.tsx
+++ b/src/components/TextInput/BaseTextInput/index.tsx
@@ -72,6 +72,7 @@ function BaseTextInput(
suffixContainerStyle = [],
suffixStyle = [],
contentWidth,
+ loadingSpinnerStyle,
...inputProps
}: BaseTextInputProps,
ref: ForwardedRef,
@@ -425,7 +426,7 @@ function BaseTextInput(
)}
{!!inputProps.secureTextEntry && (
diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts
index 92fe6b7dbbfc..c9844e33d594 100644
--- a/src/components/TextInput/BaseTextInput/types.ts
+++ b/src/components/TextInput/BaseTextInput/types.ts
@@ -137,6 +137,9 @@ type CustomBaseTextInputProps = {
/** Style for the suffix container */
suffixContainerStyle?: StyleProp;
+ /** Style for the loading spinner */
+ loadingSpinnerStyle?: StyleProp;
+
/** The width of inner content */
contentWidth?: number;
};
diff --git a/src/components/TextWithTooltip/types.ts b/src/components/TextWithTooltip/types.ts
index 4705e2b69a68..e0211adcdba2 100644
--- a/src/components/TextWithTooltip/types.ts
+++ b/src/components/TextWithTooltip/types.ts
@@ -5,7 +5,7 @@ type TextWithTooltipProps = {
text: string;
/** Whether to show the tooltip text */
- shouldShowTooltip: boolean;
+ shouldShowTooltip?: boolean;
/** Additional styles */
style?: StyleProp;
diff --git a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx
index 247c0c606901..1716249c597d 100644
--- a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx
+++ b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx
@@ -2,8 +2,8 @@ import {useFocusEffect} from '@react-navigation/native';
import type {ForwardedRef} from 'react';
import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
import {View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
+import type {StyleProp, ViewStyle} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import DotIndicatorMessage from '@components/DotIndicatorMessage';
import MagicCodeInput from '@components/MagicCodeInput';
@@ -22,7 +22,7 @@ import * as User from '@userActions/User';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {Account, ValidateMagicCodeAction} from '@src/types/onyx';
+import type {ValidateMagicCodeAction} from '@src/types/onyx';
import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -35,11 +35,6 @@ type ValidateCodeFormError = {
validateCode?: TranslationPaths;
};
-type BaseValidateCodeFormOnyxProps = {
- /** The details about the account that the user is signing in with */
- account: OnyxEntry;
-};
-
type ValidateCodeFormProps = {
/** If the magic code has been resent previously */
hasMagicCodeBeenSent?: boolean;
@@ -62,14 +57,14 @@ type ValidateCodeFormProps = {
/** Function is called when submitting form */
handleSubmitForm: (validateCode: string) => void;
+ /** Styles for the button */
+ buttonStyles?: StyleProp;
+
/** Function to clear error of the form */
clearError: () => void;
};
-type BaseValidateCodeFormProps = BaseValidateCodeFormOnyxProps & ValidateCodeFormProps;
-
function BaseValidateCodeForm({
- account = {},
hasMagicCodeBeenSent,
autoComplete = 'one-time-code',
innerRef = () => {},
@@ -78,7 +73,8 @@ function BaseValidateCodeForm({
validateError,
handleSubmitForm,
clearError,
-}: BaseValidateCodeFormProps) {
+ buttonStyles,
+}: ValidateCodeFormProps) {
const {translate} = useLocalize();
const {isOffline} = useNetwork();
const theme = useTheme();
@@ -87,6 +83,7 @@ function BaseValidateCodeForm({
const [formError, setFormError] = useState({});
const [validateCode, setValidateCode] = useState('');
const inputValidateCodeRef = useRef(null);
+ const [account = {}] = useOnyx(ONYXKEYS.ACCOUNT);
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing doesn't achieve the same result in this case
const shouldDisableResendValidateCode = !!isOffline || account?.isLoading;
const focusTimeoutRef = useRef(null);
@@ -204,7 +201,7 @@ function BaseValidateCodeForm({
errorRowStyles={[styles.mt2]}
onClose={() => User.clearValidateCodeActionError('actionVerified')}
>
-
+
clearError()}
+ style={buttonStyles}
>
+ ) : (
+
+ )}
+
+
+
);
}
const numberOfThreadReplies = action.childVisibleActionCount ?? 0;
diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx
index 7449042141f3..e8f02f0c1975 100644
--- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx
+++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx
@@ -76,7 +76,7 @@ function IOURequestStepParticipants({
}, [iouType, translate, isSplitRequest, action]);
const selfDMReportID = useMemo(() => ReportUtils.findSelfDMReportID(), []);
- const shouldDisplayTrackExpenseButton = !!selfDMReportID;
+ const shouldDisplayTrackExpenseButton = !!selfDMReportID && action === CONST.IOU.ACTION.CREATE;
const receiptFilename = transaction?.filename;
const receiptPath = transaction?.receipt?.source;
diff --git a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx
index 491c37c9a402..0ddddf7ff878 100644
--- a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx
+++ b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx
@@ -2,8 +2,8 @@ import type {RouteProp} from '@react-navigation/native';
import {useIsFocused} from '@react-navigation/native';
import type {ComponentType, ForwardedRef, RefAttributes} from 'react';
import React, {forwardRef} from 'react';
+import {useOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import getComponentDisplayName from '@libs/getComponentDisplayName';
import * as IOUUtils from '@libs/IOUUtils';
@@ -38,14 +38,25 @@ type MoneyRequestRouteName =
| typeof SCREENS.MONEY_REQUEST.STEP_SEND_FROM
| typeof SCREENS.MONEY_REQUEST.STEP_COMPANY_INFO;
-type Route = RouteProp;
+type Route = RouteProp;
-type WithFullTransactionOrNotFoundProps = WithFullTransactionOrNotFoundOnyxProps & {route: Route};
+type WithFullTransactionOrNotFoundProps = WithFullTransactionOrNotFoundOnyxProps & {
+ route: Route;
+};
-export default function , TRef>(WrappedComponent: ComponentType>) {
+export default function , TRef>(
+ WrappedComponent: ComponentType>,
+): React.ComponentType & RefAttributes> {
// eslint-disable-next-line rulesdir/no-negated-variables
- function WithFullTransactionOrNotFound(props: TProps, ref: ForwardedRef) {
- const transactionID = props.transaction?.transactionID;
+ function WithFullTransactionOrNotFound(props: Omit, ref: ForwardedRef) {
+ const {route} = props;
+ const transactionID = route.params.transactionID ?? -1;
+ const userAction = 'action' in route.params && route.params.action ? route.params.action : CONST.IOU.ACTION.CREATE;
+
+ const shouldUseTransactionDraft = IOUUtils.shouldUseTransactionDraft(userAction);
+
+ const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`);
+ const [transactionDraft] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`);
const isFocused = useIsFocused();
@@ -59,7 +70,8 @@ export default function
);
@@ -67,19 +79,7 @@ export default function , WithFullTransactionOrNotFoundOnyxProps>({
- transaction: {
- key: ({route}) => {
- const transactionID = route.params.transactionID ?? -1;
- const userAction = 'action' in route.params && route.params.action ? route.params.action : CONST.IOU.ACTION.CREATE;
-
- if (IOUUtils.shouldUseTransactionDraft(userAction)) {
- return `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}` as `${typeof ONYXKEYS.COLLECTION.TRANSACTION}${string}`;
- }
- return `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`;
- },
- },
- })(forwardRef(WithFullTransactionOrNotFound));
+ return forwardRef(WithFullTransactionOrNotFound);
}
export type {WithFullTransactionOrNotFoundProps};
diff --git a/src/pages/settings/Profile/CustomStatus/StatusPage.tsx b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx
index 26c2a9092131..c9858738906d 100644
--- a/src/pages/settings/Profile/CustomStatus/StatusPage.tsx
+++ b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx
@@ -1,7 +1,6 @@
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {InteractionManager, View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import EmojiPickerButtonDropdown from '@components/EmojiPicker/EmojiPickerButtonDropdown';
import FormProvider from '@components/Form/FormProvider';
@@ -15,9 +14,8 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
-import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
-import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
@@ -30,21 +28,16 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
import INPUT_IDS from '@src/types/form/SettingsStatusSetForm';
-import type {CustomStatusDraft} from '@src/types/onyx';
-
-type StatusPageOnyxProps = {
- draftStatus: OnyxEntry;
-};
-
-type StatusPageProps = StatusPageOnyxProps & WithCurrentUserPersonalDetailsProps;
const initialEmoji = '💬';
-function StatusPage({draftStatus, currentUserPersonalDetails}: StatusPageProps) {
+function StatusPage() {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
+ const [draftStatus] = useOnyx(ONYXKEYS.CUSTOM_STATUS_DRAFT);
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const formRef = useRef(null);
const [brickRoadIndicator, setBrickRoadIndicator] = useState>();
const currentUserEmojiCode = currentUserPersonalDetails?.status?.emojiCode ?? '';
@@ -97,6 +90,9 @@ function StatusPage({draftStatus, currentUserPersonalDetails}: StatusPageProps)
const navigateBackToPreviousScreen = useCallback(() => Navigation.goBack(), []);
const updateStatus = useCallback(
({emojiCode, statusText}: FormOnyxValues) => {
+ if (navigateBackToPreviousScreenTask.current) {
+ return;
+ }
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const clearAfterTime = draftClearAfter || currentUserClearAfter || CONST.CUSTOM_STATUS_TYPES.NEVER;
const isValid = DateUtils.isTimeAtLeastOneMinuteInFuture({dateTimeString: clearAfterTime});
@@ -118,6 +114,9 @@ function StatusPage({draftStatus, currentUserPersonalDetails}: StatusPageProps)
);
const clearStatus = () => {
+ if (navigateBackToPreviousScreenTask.current) {
+ return;
+ }
User.clearCustomStatus();
User.updateDraftCustomStatus({
text: '',
@@ -229,10 +228,4 @@ function StatusPage({draftStatus, currentUserPersonalDetails}: StatusPageProps)
StatusPage.displayName = 'StatusPage';
-export default withCurrentUserPersonalDetails(
- withOnyx({
- draftStatus: {
- key: () => ONYXKEYS.CUSTOM_STATUS_DRAFT,
- },
- })(StatusPage),
-);
+export default StatusPage;
diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx
index 97de309453d5..46f6ded27cbd 100644
--- a/src/pages/settings/Wallet/PaymentMethodList.tsx
+++ b/src/pages/settings/Wallet/PaymentMethodList.tsx
@@ -186,6 +186,7 @@ function PaymentMethodList({
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const {isOffline} = useNetwork();
+
const [isUserValidated] = useOnyx(ONYXKEYS.USER, {selector: (user) => !!user?.validated});
const [bankAccountList = {}] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
const [cardList = {}] = useOnyx(ONYXKEYS.CARD_LIST);
@@ -319,6 +320,14 @@ function PaymentMethodList({
*/
const renderListEmptyComponent = () => {translate('paymentMethodList.addFirstPaymentMethod')};
+ const onPressItem = useCallback(() => {
+ if (!isUserValidated) {
+ Navigation.navigate(ROUTES.SETTINGS_WALLET_VERIFY_ACCOUNT.getRoute(ROUTES.SETTINGS_ADD_BANK_ACCOUNT));
+ return;
+ }
+ onPress();
+ }, [isUserValidated, onPress]);
+
const renderListFooterComponent = useCallback(
() =>
shouldShowAddBankAccountButton ? (
@@ -333,16 +342,15 @@ function PaymentMethodList({
/>
) : (
),
- [shouldShowAddBankAccountButton, translate, onPress, buttonRef, styles.paymentMethod, listItemStyle, isUserValidated],
+ [shouldShowAddBankAccountButton, onPressItem, translate, onPress, buttonRef, styles.paymentMethod, listItemStyle, isUserValidated],
);
/**
diff --git a/src/pages/settings/Wallet/VerifyAccountPage.tsx b/src/pages/settings/Wallet/VerifyAccountPage.tsx
new file mode 100644
index 000000000000..1fb39304ec41
--- /dev/null
+++ b/src/pages/settings/Wallet/VerifyAccountPage.tsx
@@ -0,0 +1,86 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useCallback, useEffect, useRef} from 'react';
+import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import type {AnimatedTextInputRef} from '@components/RNTextInput';
+import ScreenWrapper from '@components/ScreenWrapper';
+import Text from '@components/Text';
+import ValidateCodeForm from '@components/ValidateCodeActionModal/ValidateCodeForm';
+import useLocalize from '@hooks/useLocalize';
+import useSafePaddingBottomStyle from '@hooks/useSafePaddingBottomStyle';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
+import * as User from '@userActions/User';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+
+type VerifyAccountPageProps = StackScreenProps;
+
+function VerifyAccountPage({route}: VerifyAccountPageProps) {
+ const [account] = useOnyx(ONYXKEYS.ACCOUNT);
+ const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST);
+ const contactMethod = account?.primaryLogin ?? '';
+ const themeStyles = useThemeStyles();
+ const {translate} = useLocalize();
+ const safePaddingBottomStyle = useSafePaddingBottomStyle();
+ const loginInputRef = useRef(null);
+ const loginData = loginList?.[contactMethod];
+ const styles = useThemeStyles();
+ const validateLoginError = ErrorUtils.getEarliestErrorField(loginData, 'validateLogin');
+ const [isUserValidated] = useOnyx(ONYXKEYS.USER, {selector: (user) => !!user?.validated});
+
+ const [validateCodeAction] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE);
+
+ const navigateBackTo = route?.params?.backTo ?? ROUTES.SETTINGS_WALLET;
+
+ useEffect(() => {
+ User.requestValidateCodeAction();
+ return () => User.clearUnvalidatedNewContactMethodAction();
+ }, []);
+
+ const handleSubmitForm = useCallback(
+ (submitCode: string) => {
+ User.validateSecondaryLogin(loginList, contactMethod ?? '', submitCode);
+ },
+ [loginList, contactMethod],
+ );
+
+ useEffect(() => {
+ if (!isUserValidated) {
+ return;
+ }
+ Navigation.navigate(navigateBackTo);
+ }, [isUserValidated, navigateBackTo]);
+
+ return (
+ loginInputRef.current?.focus()}
+ includeSafeAreaPaddingBottom={false}
+ shouldEnableMaxHeight
+ testID={VerifyAccountPage.displayName}
+ >
+ Navigation.goBack(ROUTES.SETTINGS_WALLET)}
+ />
+
+ {translate('contacts.featureRequiresValidate')}
+ User.clearContactMethodErrors(contactMethod, 'validateLogin')}
+ buttonStyles={[styles.justifyContentEnd, styles.flex1, safePaddingBottomStyle]}
+ />
+
+
+ );
+}
+
+VerifyAccountPage.displayName = 'VerifyAccountPage';
+
+export default VerifyAccountPage;
diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx
index 4118d4e267a8..7b9366370349 100644
--- a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx
+++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx
@@ -57,6 +57,7 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) {
const [isLoadingPaymentMethods] = useOnyx(ONYXKEYS.IS_LOADING_PAYMENT_METHODS, {initialValue: true});
const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET);
const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS, {initialValue: {}});
+ const [isUserValidated] = useOnyx(ONYXKEYS.USER, {selector: (user) => !!user?.validated});
const theme = useTheme();
const styles = useThemeStyles();
@@ -497,7 +498,13 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) {
return (