From 3e9270b7ebc438ba15497a946ac0b5105d7e5cc3 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 7 Jul 2023 15:36:01 -0700 Subject: [PATCH 001/151] Make the same files without crazy git diff --- package-lock.json | 13 +++ package.json | 1 + scripts/AggregateGitHubDataFromUpwork.js | 105 +++++++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 scripts/AggregateGitHubDataFromUpwork.js diff --git a/package-lock.json b/package-lock.json index bc8aade90b76..cfe6e401c585 100644 --- a/package-lock.json +++ b/package-lock.json @@ -142,6 +142,7 @@ "concurrently": "^5.3.0", "copy-webpack-plugin": "^6.4.1", "css-loader": "^6.7.2", + "csv-writer": "^1.6.0", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", "electron": "22.3.14", @@ -21382,6 +21383,12 @@ "version": "3.1.1", "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/currently-unhandled": { "version": "0.4.1", "dev": true, @@ -58148,6 +58155,12 @@ "csstype": { "version": "3.1.1" }, + "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 + }, "currently-unhandled": { "version": "0.4.1", "dev": true, diff --git a/package.json b/package.json index 9ccdca875ddc..42c8f197dac1 100644 --- a/package.json +++ b/package.json @@ -179,6 +179,7 @@ "concurrently": "^5.3.0", "copy-webpack-plugin": "^6.4.1", "css-loader": "^6.7.2", + "csv-writer": "^1.6.0", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", "electron": "22.3.14", diff --git a/scripts/AggregateGitHubDataFromUpwork.js b/scripts/AggregateGitHubDataFromUpwork.js new file mode 100644 index 000000000000..21d991e1a545 --- /dev/null +++ b/scripts/AggregateGitHubDataFromUpwork.js @@ -0,0 +1,105 @@ +/* + * To run this script from the root of E/App: + * + * node ./scripts/AggregateGitHubDataFromUpwork.js + */ + +/* eslint-disable no-console, @lwc/lwc/no-async-await, no-restricted-syntax, no-await-in-loop */ +const _ = require('underscore'); +const fs = require('fs'); +const {GitHub, getOctokitOptions} = require('@actions/github/lib/utils'); +const {throttling} = require('@octokit/plugin-throttling'); +const {paginateRest} = require('@octokit/plugin-paginate-rest'); +const createCsvWriter = require('csv-writer').createObjectCsvWriter; + +const csvWriter = createCsvWriter({ + path: 'output.csv', + header: [ + {id: 'number', title: 'number'}, + {id: 'title', title: 'title'}, + {id: 'labels', title: 'labels'}, + {id: 'type', title: 'type'}, + ], +}); + +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'); +} + +// Get filepath for csv +const filepath = process.argv[2]; + +// Get data from csv +let issues = _.filter(fs.readFileSync(filepath).toString().split('\n'), (issue) => !_.isEmpty(issue)); + +// Skip header row +issues = issues.slice(1); + +// Get GitHub token +const token = process.argv[3].trim(); +const Octokit = GitHub.plugin(throttling, paginateRest); +const octokit = new Octokit( + getOctokitOptions(token, { + throttle: { + onRateLimit: (retryAfter, options) => { + 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, options) => { + // does not retry, only logs a warning + console.warn(`Abuse detected for request ${options.method} ${options.url}`); + }, + }, + }), +).rest; + +function getType(labels) { + if (_.contains(labels, 'Bug')) { + return 'bug'; + } + if (_.contains(labels, 'NewFeature')) { + return 'feature'; + } + return 'other'; +} + +async function getGitHubData() { + const gitHubData = []; + for (const issueNumber of issues) { + const num = issueNumber.trim(); + console.info(`Fetching ${num}`); + const result = await octokit.issues + .get({ + owner: 'Expensify', + repo: 'App', + issue_number: num, + }) + .catch(() => { + console.warn(`Error getting issue ${num}`); + }); + if (result) { + const issue = result.data; + const labels = _.map(issue.labels, (label) => label.name); + gitHubData.push({ + number: issue.number, + title: issue.title, + labels, + type: getType(labels), + }); + } + } + return gitHubData; +} + +getGitHubData() + .then((gitHubData) => csvWriter.writeRecords(gitHubData)) + .then(() => console.info('Done ✅ Wrote file to output.csv')); From cb1be523c0d5e8f139a29bd482add784decac732 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 9 Apr 2024 11:42:57 -0700 Subject: [PATCH 002/151] Add project check --- scripts/AggregateGitHubDataFromUpwork.js | 28 ++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/scripts/AggregateGitHubDataFromUpwork.js b/scripts/AggregateGitHubDataFromUpwork.js index 21d991e1a545..9409e430c5ac 100644 --- a/scripts/AggregateGitHubDataFromUpwork.js +++ b/scripts/AggregateGitHubDataFromUpwork.js @@ -60,7 +60,7 @@ const octokit = new Octokit( }, }, }), -).rest; +); function getType(labels) { if (_.contains(labels, 'Bug')) { @@ -77,7 +77,7 @@ async function getGitHubData() { for (const issueNumber of issues) { const num = issueNumber.trim(); console.info(`Fetching ${num}`); - const result = await octokit.issues + const result = await octokit.rest.issues .get({ owner: 'Expensify', repo: 'App', @@ -89,11 +89,35 @@ async function getGitHubData() { if (result) { const issue = result.data; const labels = _.map(issue.labels, (label) => label.name); + const type = getType(labels); + let capSWProjects = []; + if (type === 'NewFeature') { + // eslint-disable-next-line rulesdir/prefer-underscore-method + capSWProjects = await octokit + .graphql( + ` + { + repository(owner: "Expensify", name: "App") { + issue(number: 39322) { + projectsV2(last: 30) { + nodes { + title + } + } + } + } + } + `, + ) + .repository.issue.projectsV2.nodes.map((node) => node.title) + .join(','); + } gitHubData.push({ number: issue.number, title: issue.title, labels, type: getType(labels), + capSWProjects, }); } } From 97b6c82050ab3abc1e0372ececfb37b2b2893c44 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 9 Jul 2024 09:47:07 -0700 Subject: [PATCH 003/151] Update package-lock.json --- package-lock.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/package-lock.json b/package-lock.json index 36cb2639ec4f..276408dcbee1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -209,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.1", @@ -23198,6 +23199,12 @@ "integrity": "sha512-erCk7tyU3yLWAhk6wvKxnyPtftuy/6Ak622gOO7BCJ05+TYffnPCJF905wmOQm+BpkX54OdAl8pveJwUdpnCXQ==", "dev": true }, + "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", "license": "MIT" From e38859753d3628edb3a47f4bd6f19a86891bb6bb Mon Sep 17 00:00:00 2001 From: Eduardo Date: Thu, 22 Aug 2024 17:16:55 +0200 Subject: [PATCH 004/151] Updating reconnectApp from the persisted requests queue --- src/libs/actions/PersistedRequests.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/PersistedRequests.ts b/src/libs/actions/PersistedRequests.ts index 851e53876508..310ae2ea3e0a 100644 --- a/src/libs/actions/PersistedRequests.ts +++ b/src/libs/actions/PersistedRequests.ts @@ -1,10 +1,12 @@ import isEqual from 'lodash/isEqual'; import Onyx from 'react-native-onyx'; +import {WRITE_COMMANDS} from '@libs/API/types'; import Log from '@libs/Log'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Request} from '@src/types/onyx'; let persistedRequests: Request[] = []; +const keepLastInstance: string[] = [WRITE_COMMANDS.RECONNECT_APP]; Onyx.connect({ key: ONYXKEYS.PERSISTED_REQUESTS, @@ -23,9 +25,22 @@ function getLength(): number { } function save(requestToPersist: Request) { - const requests = [...persistedRequests, requestToPersist]; - persistedRequests = requests; - Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, requests).then(() => { + if (keepLastInstance.includes(requestToPersist.command)) { + // Find the index of an existing request with the same command + const index = persistedRequests.findIndex((request) => request.command === requestToPersist.command); + + if (index !== -1) { + // If found, update the existing request with the new one + persistedRequests[index] = requestToPersist; + } else { + // If not found, add the new request + persistedRequests.push(requestToPersist); + } + } else { + // If the command is not in the keepLastInstance array, add the new request as usual + persistedRequests = [...persistedRequests, requestToPersist]; + } + Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, persistedRequests).then(() => { Log.info(`[SequentialQueue] '${requestToPersist.command}' command queued. Queue length is ${getLength()}`); }); } From 615a8babb3b6f8eb42620aa7a81baded8c55a72f Mon Sep 17 00:00:00 2001 From: Eduardo Date: Thu, 29 Aug 2024 19:02:57 +0200 Subject: [PATCH 005/151] Applying comments + unit tests --- src/libs/Network/SequentialQueue.ts | 6 +++--- src/libs/actions/App.ts | 8 +++----- src/libs/actions/PersistedRequests.ts | 2 -- src/types/onyx/Request.ts | 29 ++++++++++++++++----------- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index 9fd65602eca2..57bfdc176fe3 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -199,11 +199,11 @@ function push(newRequest: OnyxRequest) { const {checkAndFixConflictingRequest} = newRequest; if (checkAndFixConflictingRequest) { - const {conflictAction} = checkAndFixConflictingRequest(requests, newRequest); + const {conflictAction} = checkAndFixConflictingRequest(requests); - if (conflictAction.type === 'save') { + if (conflictAction.type === 'push') { PersistedRequests.save(newRequest); - } else { + } else if (conflictAction.type === 'replace') { PersistedRequests.update(conflictAction.index, newRequest); } } else { diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 7f7fc95ae5d4..331c39c35137 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -275,21 +275,19 @@ function reconnectApp(updateIDFrom: OnyxEntry = 0) { } API.write(WRITE_COMMANDS.RECONNECT_APP, params, getOnyxDataForOpenOrReconnect(), { - checkAndFixConflictingRequest: (persistedRequests, newRequest) => { + checkAndFixConflictingRequest: (persistedRequests) => { const index = persistedRequests.findIndex((request) => request.command === WRITE_COMMANDS.RECONNECT_APP); if (index === -1) { return { - request: newRequest, conflictAction: { - type: 'save', + type: 'push', }, }; } return { - request: newRequest, conflictAction: { - type: 'update', + type: 'replace', index, }, }; diff --git a/src/libs/actions/PersistedRequests.ts b/src/libs/actions/PersistedRequests.ts index 18d66ee9ccb7..50a273e07a97 100644 --- a/src/libs/actions/PersistedRequests.ts +++ b/src/libs/actions/PersistedRequests.ts @@ -1,12 +1,10 @@ import isEqual from 'lodash/isEqual'; import Onyx from 'react-native-onyx'; -import {WRITE_COMMANDS} from '@libs/API/types'; import Log from '@libs/Log'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Request} from '@src/types/onyx'; let persistedRequests: Request[] = []; -const keepLastInstance: string[] = [WRITE_COMMANDS.RECONNECT_APP]; Onyx.connect({ key: ONYXKEYS.PERSISTED_REQUESTS, diff --git a/src/types/onyx/Request.ts b/src/types/onyx/Request.ts index 0b583422f738..7f17ae6b9d72 100644 --- a/src/types/onyx/Request.ts +++ b/src/types/onyx/Request.ts @@ -55,9 +55,6 @@ type RequestData = { shouldSkipWebProxy?: boolean; }; -/** Model of requests sent to the API */ -type Request = RequestData & OnyxData & RequestConflictResolver; - /** * Model of a conflict request that has to be updated, in the request queue. */ @@ -65,7 +62,7 @@ type ConflictRequestUpdate = { /** * The action to take in case of a conflict. */ - type: 'update'; + type: 'replace'; /** * The index of the request in the queue to update. @@ -74,28 +71,33 @@ type ConflictRequestUpdate = { }; /** - * Model of a conflict request that has to be saved, in the request queue. + * Model of a conflict request that has to be saved at the end the request queue. */ type ConflictRequestSave = { /** * The action to take in case of a conflict. */ - type: 'save'; + type: 'push'; }; /** - * An object that has the request and the action to take in case of a conflict. + * Model of a conflict request that no need to be updated or saved, in the request queue. */ -type ConflictActionData = { +type ConflictRequestNoAction = { /** - * The request that is conflicting with the new request. + * The action to take in case of a conflict. */ - request: Request; + type: 'noAction'; +}; +/** + * An object that has the request and the action to take in case of a conflict. + */ +type ConflictActionData = { /** * The action to take in case of a conflict. */ - conflictAction: ConflictRequestUpdate | ConflictRequestSave; + conflictAction: ConflictRequestUpdate | ConflictRequestSave | ConflictRequestNoAction; }; /** @@ -106,9 +108,12 @@ type RequestConflictResolver = { /** * A function that checks if a new request conflicts with any existing requests in the queue. */ - checkAndFixConflictingRequest?: (persistedRequest: Request[], request: Request) => ConflictActionData; + checkAndFixConflictingRequest?: (persistedRequest: Request[]) => ConflictActionData; }; +/** Model of requests sent to the API */ +type Request = RequestData & OnyxData & RequestConflictResolver; + /** * An object used to describe how a request can be paginated. */ From 8b505ab4985fb0e555e329048171f107b731936a Mon Sep 17 00:00:00 2001 From: Eduardo Date: Thu, 29 Aug 2024 19:33:27 +0200 Subject: [PATCH 006/151] Adding comment --- src/libs/Network/SequentialQueue.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index 57bfdc176fe3..c0173df024af 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -195,6 +195,7 @@ function isPaused(): boolean { NetworkStore.onReconnection(flush); function push(newRequest: OnyxRequest) { + // If a request is already being processed, ignore it when looking for potentially conflicting requests const requests = PersistedRequests.getAll().filter((persistedRequest) => persistedRequest !== currentRequest); const {checkAndFixConflictingRequest} = newRequest; From be72fec72f0797d8c390e861a4f0b229c46795df Mon Sep 17 00:00:00 2001 From: Eduardo Date: Fri, 30 Aug 2024 15:30:31 +0200 Subject: [PATCH 007/151] Adding test to Sequential Queue --- tests/unit/SequentialQueueTest.ts | 147 ++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 tests/unit/SequentialQueueTest.ts diff --git a/tests/unit/SequentialQueueTest.ts b/tests/unit/SequentialQueueTest.ts new file mode 100644 index 000000000000..4a0c4a540e57 --- /dev/null +++ b/tests/unit/SequentialQueueTest.ts @@ -0,0 +1,147 @@ +import Onyx from 'react-native-onyx'; +import * as PersistedRequests from '@userActions/PersistedRequests'; +import ONYXKEYS from '@src/ONYXKEYS'; +import * as SequentialQueue from '../../src/libs/Network/SequentialQueue'; +import type Request from '../../src/types/onyx/Request'; +import * as TestHelper from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +const request: Request = { + command: 'ReconnectApp', + successData: [{key: 'userMetadata', onyxMethod: 'set', value: {accountID: 1234}}], + failureData: [{key: 'userMetadata', onyxMethod: 'set', value: {}}], +}; + +describe('SequentialQueue', () => { + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + }); + }); + beforeEach(() => { + global.fetch = TestHelper.getGlobalFetchMock(); + return Onyx.clear().then(waitForBatchedUpdates); + }); + + it('should push one request and persist one', () => { + SequentialQueue.push(request); + expect(PersistedRequests.getLength()).toBe(1); + }); + + it('should push two requests and persist two', () => { + SequentialQueue.push(request); + SequentialQueue.push(request); + expect(PersistedRequests.getLength()).toBe(2); + }); + + it('should push two requests with conflict resolution and replace', () => { + SequentialQueue.push(request); + const requestWithConflictResolution: Request = { + command: 'ReconnectApp', + data: {accountID: 56789}, + checkAndFixConflictingRequest: (persistedRequests) => { + // should be one instance of ReconnectApp, get the index to replace it later + const index = persistedRequests.findIndex((r) => r.command === 'ReconnectApp'); + if (index === -1) { + return {conflictAction: {type: 'push'}}; + } + + return { + conflictAction: {type: 'replace', index}, + }; + }, + }; + SequentialQueue.push(requestWithConflictResolution); + expect(PersistedRequests.getLength()).toBe(1); + }); + + it('should push two requests with conflict resolution and push', () => { + SequentialQueue.push(request); + const requestWithConflictResolution: Request = { + command: 'ReconnectApp', + data: {accountID: 56789}, + checkAndFixConflictingRequest: () => { + return {conflictAction: {type: 'push'}}; + }, + }; + SequentialQueue.push(requestWithConflictResolution); + expect(PersistedRequests.getLength()).toBe(2); + }); + + it('should push two requests with conflict resolution and noAction', () => { + SequentialQueue.push(request); + const requestWithConflictResolution: Request = { + command: 'ReconnectApp', + data: {accountID: 56789}, + checkAndFixConflictingRequest: () => { + return {conflictAction: {type: 'noAction'}}; + }, + }; + SequentialQueue.push(requestWithConflictResolution); + expect(PersistedRequests.getLength()).toBe(1); + }); + + it('should add a new request even if a similar one is ongoing', async () => { + // .push at the end flush the queue + SequentialQueue.push(request); + + // wait for Onyx.connect execute the callback and start processing the queue + await Promise.resolve(); + + const requestWithConflictResolution: Request = { + command: 'ReconnectApp', + data: {accountID: 56789}, + checkAndFixConflictingRequest: (persistedRequests) => { + // should be one instance of ReconnectApp, get the index to replace it later + const index = persistedRequests.findIndex((r) => r.command === 'ReconnectApp'); + if (index === -1) { + return {conflictAction: {type: 'push'}}; + } + + return { + conflictAction: {type: 'replace', index}, + }; + }, + }; + + SequentialQueue.push(requestWithConflictResolution); + expect(PersistedRequests.getLength()).toBe(2); + }); + + it.only('should replace request request in queue while a similar one is ongoing', async () => { + // .push at the end flush the queue + SequentialQueue.push(request); + + // wait for Onyx.connect execute the callback and start processing the queue + await Promise.resolve(); + + const conflicyResolver = (persistedRequests: Request[]) => { + // should be one instance of ReconnectApp, get the index to replace it later + const index = persistedRequests.findIndex((r) => r.command === 'ReconnectApp'); + if (index === -1) { + return {conflictAction: {type: 'push'}}; + } + + return { + conflictAction: {type: 'replace', index}, + }; + }; + + const requestWithConflictResolution: Request = { + command: 'ReconnectApp', + data: {accountID: 56789}, + checkAndFixConflictingRequest: conflicyResolver, + }; + + const requestWithConflictResolution2: Request = { + command: 'ReconnectApp', + data: {accountID: 56789}, + checkAndFixConflictingRequest: conflicyResolver, + }; + + SequentialQueue.push(requestWithConflictResolution); + SequentialQueue.push(requestWithConflictResolution2); + + expect(PersistedRequests.getLength()).toBe(2); + }); +}); From 6e40d666187c1237e56c2241348c8af9d498637d Mon Sep 17 00:00:00 2001 From: Eduardo Date: Fri, 30 Aug 2024 15:42:49 +0200 Subject: [PATCH 008/151] Fixed TS error --- src/types/onyx/Request.ts | 2 +- tests/unit/SequentialQueueTest.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/types/onyx/Request.ts b/src/types/onyx/Request.ts index 7f17ae6b9d72..5d37cc559a4d 100644 --- a/src/types/onyx/Request.ts +++ b/src/types/onyx/Request.ts @@ -141,4 +141,4 @@ type PaginatedRequest = Request & }; export default Request; -export type {OnyxData, RequestType, PaginationConfig, PaginatedRequest, RequestConflictResolver}; +export type {OnyxData, RequestType, PaginationConfig, PaginatedRequest, RequestConflictResolver, ConflictActionData}; diff --git a/tests/unit/SequentialQueueTest.ts b/tests/unit/SequentialQueueTest.ts index 4a0c4a540e57..1a3149039306 100644 --- a/tests/unit/SequentialQueueTest.ts +++ b/tests/unit/SequentialQueueTest.ts @@ -3,6 +3,7 @@ import * as PersistedRequests from '@userActions/PersistedRequests'; import ONYXKEYS from '@src/ONYXKEYS'; import * as SequentialQueue from '../../src/libs/Network/SequentialQueue'; import type Request from '../../src/types/onyx/Request'; +import type {ConflictActionData} from '../../src/types/onyx/Request'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -115,7 +116,7 @@ describe('SequentialQueue', () => { // wait for Onyx.connect execute the callback and start processing the queue await Promise.resolve(); - const conflicyResolver = (persistedRequests: Request[]) => { + const conflicyResolver = (persistedRequests: Request[]): ConflictActionData => { // should be one instance of ReconnectApp, get the index to replace it later const index = persistedRequests.findIndex((r) => r.command === 'ReconnectApp'); if (index === -1) { From 31bf2867b7a9182a593be9d2f15d1ccab83a828b Mon Sep 17 00:00:00 2001 From: Eduardo Date: Fri, 30 Aug 2024 16:41:47 +0200 Subject: [PATCH 009/151] removed the .only from test --- tests/unit/SequentialQueueTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/SequentialQueueTest.ts b/tests/unit/SequentialQueueTest.ts index 1a3149039306..9fb780828113 100644 --- a/tests/unit/SequentialQueueTest.ts +++ b/tests/unit/SequentialQueueTest.ts @@ -109,7 +109,7 @@ describe('SequentialQueue', () => { expect(PersistedRequests.getLength()).toBe(2); }); - it.only('should replace request request in queue while a similar one is ongoing', async () => { + it('should replace request request in queue while a similar one is ongoing', async () => { // .push at the end flush the queue SequentialQueue.push(request); From 45bf5a61c160656f114c19bdbdd8c0e08be40af4 Mon Sep 17 00:00:00 2001 From: Eduardo Date: Tue, 10 Sep 2024 09:28:34 +0200 Subject: [PATCH 010/151] Fixes and moving the ongoing request to PersistedRequests --- .../LHNOptionsList/LHNOptionsList.tsx | 2 +- src/hooks/useReportIDs.tsx | 6 +- src/libs/Network/SequentialQueue.ts | 14 +-- src/libs/OptionsListUtils.ts | 2 +- src/libs/ReportActionsUtils.ts | 37 ++++++-- src/libs/SidebarUtils.ts | 2 +- src/libs/actions/PersistedRequests.ts | 88 +++++++++++++++---- src/types/onyx/Request.ts | 12 +-- tests/unit/APITest.ts | 16 ++-- tests/unit/SequentialQueueTest.ts | 41 ++++++++- 10 files changed, 169 insertions(+), 51 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index a734890a1f38..321cf4d66765 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -137,7 +137,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio : '-1'; const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; const hasDraftComment = DraftCommentUtils.isValidDraftComment(draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`]); - const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions); + const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions, false, reportID); const lastReportAction = sortedReportActions[0]; // Get the transaction for the last report action diff --git a/src/hooks/useReportIDs.tsx b/src/hooks/useReportIDs.tsx index b7d84cb25196..0ac82a61171b 100644 --- a/src/hooks/useReportIDs.tsx +++ b/src/hooks/useReportIDs.tsx @@ -37,8 +37,9 @@ const ReportIDsContext = createContext({ * This function (and the few below it), narrow down the data from Onyx to just the properties that we want to trigger a re-render of the component. This helps minimize re-rendering * and makes the entire component more performant because it's not re-rendering when a bunch of properties change which aren't ever used in the UI. */ -const reportActionsSelector = (reportActions: OnyxEntry): ReportActionsSelector => - (reportActions && +const reportActionsSelector = (reportActions: OnyxEntry): ReportActionsSelector => { + console.log('reportActions', reportActions); + return (reportActions && Object.values(reportActions) .filter(Boolean) .map((reportAction) => { @@ -58,6 +59,7 @@ const reportActionsSelector = (reportActions: OnyxEntry originalMessage, }; })) as ReportActionsSelector; +}; const policySelector = (policy: OnyxEntry): PolicySelector => (policy && { diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index 26051657eb82..c1b79b015e12 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -24,7 +24,7 @@ let isReadyPromise = new Promise((resolve) => { resolveIsReadyPromise?.(); let isSequentialQueueRunning = false; -let currentRequest: OnyxRequest | null = null; +// let currentRequest: OnyxRequest | null = null; let currentRequestPromise: Promise | null = null; let isQueuePaused = false; @@ -80,9 +80,9 @@ function process(): Promise { return Promise.resolve(); } - const requestToProcess = persistedRequests[0]; - - currentRequest = requestToProcess; + const requestToProcess = PersistedRequests.processNextRequest(); // persistedRequests[0]; + console.log('next process requestToProcess', {...requestToProcess}); + // currentRequest = requestToProcess; // Set the current request to a promise awaiting its processing so that getCurrentRequest can be used to take some action after the current request has processed. currentRequestPromise = Request.processWithMiddleware(requestToProcess, true) .then((response) => { @@ -162,7 +162,7 @@ function flush() { if (NetworkStore.isOffline() || PersistedRequests.getAll().length === 0) { resolveIsReadyPromise?.(); } - currentRequest = null; + // currentRequest = null; currentRequestPromise = null; // The queue can be paused when we sync the data with backend so we should only update the Onyx data when the queue is empty @@ -202,10 +202,11 @@ NetworkStore.onReconnection(flush); function push(newRequest: OnyxRequest) { // If a request is already being processed, ignore it when looking for potentially conflicting requests - const requests = PersistedRequests.getAll().filter((persistedRequest) => persistedRequest !== currentRequest); + const requests = PersistedRequests.getAll(); //.filter((persistedRequest) => persistedRequest !== currentRequest); const {checkAndFixConflictingRequest} = newRequest; if (checkAndFixConflictingRequest) { + console.log('ReconnectApp checkAndFixConflictingRequest', {...requests}); const {conflictAction} = checkAndFixConflictingRequest(requests); if (conflictAction.type === 'push') { @@ -218,6 +219,7 @@ function push(newRequest: OnyxRequest) { PersistedRequests.save(newRequest); } + const final = PersistedRequests.getAll(); // If we are offline we don't need to trigger the queue to empty as it will happen when we come back online if (NetworkStore.isOffline()) { return; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index acc9d4bdefc5..a1be5bcdfaff 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -297,7 +297,7 @@ Onyx.connect({ Object.entries(allReportActions).forEach((reportActions) => { const reportID = reportActions[0].split('_')[1]; const reportActionsArray = Object.values(reportActions[1] ?? {}); - let sortedReportActions = ReportActionUtils.getSortedReportActions(reportActionsArray, true); + let sortedReportActions = ReportActionUtils.getSortedReportActions(reportActionsArray, true, reportID); allSortedReportActions[reportID] = sortedReportActions; // If the report is a one-transaction report and has , we need to return the combined reportActions so that the LHN can display modifications diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 4d126cf9cbf4..414b4a7e1a20 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -49,6 +49,7 @@ type MemberChangeMessageRoomReferenceElement = { type MemberChangeMessageElement = MessageTextElement | MemberChangeMessageUserMentionElement | MemberChangeMessageRoomReferenceElement; let allReportActions: OnyxCollection; +const cachedSortedReportActions = new Map(); Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, waitForCollectionCallback: true, @@ -56,11 +57,25 @@ Onyx.connect({ if (!actions) { return; } - + // console.log('allReportActions', actions); allReportActions = actions; }, }); +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + callback: (actions) => { + if (!actions) { + return; + } + // console.log('LOOKING FOR REPORT changed>', actions); + if (cachedSortedReportActions.has(actions.reportID)) { + // console.log('LOOKING FOR REPORT changed> DELETING', actions.reportID); + cachedSortedReportActions.delete(actions.reportID); + } + }, +}); + let isNetworkOffline = false; Onyx.connect({ key: ONYXKEYS.NETWORK, @@ -364,11 +379,16 @@ function isTransactionThread(parentReportAction: OnyxInputOrEntry) * This gives us a stable order even in the case of multiple reportActions created on the same millisecond * */ -function getSortedReportActions(reportActions: ReportAction[] | null, shouldSortInDescendingOrder = false): ReportAction[] { +function getSortedReportActions(reportActions: ReportAction[] | null, shouldSortInDescendingOrder = false, reportID: string | undefined = undefined): ReportAction[] { if (!Array.isArray(reportActions)) { throw new Error(`ReportActionsUtils.getSortedReportActions requires an array, received ${typeof reportActions}`); } + // console.log('getSortedReportActions - INITIAL reportActions', reportActions); + if (reportID && cachedSortedReportActions.has(reportID)) { + // console.log('getSortedReportActions - CACHED sortedActions', cachedSortedReportActions.get(reportID)); + return cachedSortedReportActions.get(reportID) ?? []; + } const invertedMultiplier = shouldSortInDescendingOrder ? -1 : 1; const sortedActions = reportActions?.filter(Boolean).sort((first, second) => { @@ -391,7 +411,10 @@ function getSortedReportActions(reportActions: ReportAction[] | null, shouldSort // will be consistent across all users and devices return (first.reportActionID < second.reportActionID ? -1 : 1) * invertedMultiplier; }); - + // console.log('getSortedReportActions - FINAL sortedActions', sortedActions); + if (reportID) { + cachedSortedReportActions.set(reportID, sortedActions); + } return sortedActions; } @@ -441,7 +464,7 @@ function getCombinedReportActions( return actionType !== CONST.IOU.REPORT_ACTION_TYPE.CREATE && actionType !== CONST.IOU.REPORT_ACTION_TYPE.TRACK; }); - return getSortedReportActions(filteredReportActions, true); + return getSortedReportActions(filteredReportActions, true, report?.reportID); } /** @@ -731,7 +754,7 @@ function getLastVisibleAction(reportID: string, actionsToMerge: Record shouldReportActionBeVisibleAsLastAction(action)); - const sortedReportActions = getSortedReportActions(visibleReportActions, true); + const sortedReportActions = getSortedReportActions(visibleReportActions, true, reportID); if (sortedReportActions.length === 0) { return undefined; } @@ -784,7 +807,7 @@ function filterOutDeprecatedReportActions(reportActions: OnyxEntry | ReportAction[], shouldIncludeInvisibleActions = false): ReportAction[] { +function getSortedReportActionsForDisplay(reportActions: OnyxEntry | ReportAction[], shouldIncludeInvisibleActions = false, reportID: string | undefined): ReportAction[] { let filteredReportActions: ReportAction[] = []; if (!reportActions) { return []; @@ -799,7 +822,7 @@ function getSortedReportActionsForDisplay(reportActions: OnyxEntry replaceBaseURLInPolicyChangeLogAction(reportAction)); - return getSortedReportActions(baseURLAdjustedReportActions, true); + return getSortedReportActions(baseURLAdjustedReportActions, true, reportID); } /** diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 0e8447635098..52adafc0839b 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -41,7 +41,7 @@ Onyx.connect({ } const reportID = CollectionUtils.extractCollectionItemID(key); - const actionsArray: ReportAction[] = ReportActionsUtils.getSortedReportActions(Object.values(actions)); + const actionsArray: ReportAction[] = ReportActionsUtils.getSortedReportActions(Object.values(actions), false, reportID); // The report is only visible if it is the last action not deleted that // does not match a closed or created state. diff --git a/src/libs/actions/PersistedRequests.ts b/src/libs/actions/PersistedRequests.ts index 50a273e07a97..43675ecf8feb 100644 --- a/src/libs/actions/PersistedRequests.ts +++ b/src/libs/actions/PersistedRequests.ts @@ -5,58 +5,112 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Request} from '@src/types/onyx'; let persistedRequests: Request[] = []; +let ongoingRequest: Request | null = null; Onyx.connect({ key: ONYXKEYS.PERSISTED_REQUESTS, - callback: (val) => (persistedRequests = val ?? []), + callback: (val) => { + console.log('ReconnectApp PERSISTED_REQUESTS val', {...val}, ongoingRequest); + // it has the ongoingRequest in here? + persistedRequests = val ?? []; + + if (ongoingRequest && persistedRequests.length > 0) { + const elem = {...persistedRequests}[0]; + console.log('First persistedRequests', elem, ' are equals: ', isEqual(elem, ongoingRequest)); + // here we try to remove the first element from the persistedRequests if it is the same as ongoingRequest + if (isEqual(elem, ongoingRequest)) { + console.log('First persistedRequests is equal to ongoingRequest'); + persistedRequests = persistedRequests.slice(1); + } + } + }, }); /** * This promise is only used by tests. DO NOT USE THIS PROMISE IN THE APPLICATION CODE */ function clear() { + ongoingRequest = null; return Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, []); } function getLength(): number { - return persistedRequests.length; + // Making it backwards compatible with the old implementation + return persistedRequests.length + (ongoingRequest ? 1 : 0); } function save(requestToPersist: Request) { // If the command is not in the keepLastInstance array, add the new request as usual - persistedRequests = [...persistedRequests, requestToPersist]; - - Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, persistedRequests).then(() => { + const requests = [...persistedRequests, requestToPersist]; + persistedRequests = requests; + Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, requests).then(() => { Log.info(`[SequentialQueue] '${requestToPersist.command}' command queued. Queue length is ${getLength()}`); }); } function remove(requestToRemove: Request) { - /** - * We only remove the first matching request because the order of requests matters. - * If we were to remove all matching requests, we can end up with a final state that is different than what the user intended. - */ - const requests = [...persistedRequests]; - const index = requests.findIndex((persistedRequest) => isEqual(persistedRequest, requestToRemove)); - if (index === -1) { - return; + console.log('remove requestToRemove - init>', {...requestToRemove}); + if (isEqual(ongoingRequest, requestToRemove)) { + console.log('remove ongoingRequest', {...ongoingRequest}); + ongoingRequest = null; + } else { + /** + * We only remove the first matching request because the order of requests matters. + * If we were to remove all matching requests, we can end up with a final state that is different than what the user intended. + */ + const requests = [...persistedRequests]; + const index = requests.findIndex((persistedRequest) => isEqual(persistedRequest, requestToRemove)); + console.log('current queue: ', requests, 'remove index', index); + if (index === -1) { + return; + } + requests.splice(index, 1); + persistedRequests = requests; } - requests.splice(index, 1); - persistedRequests = requests; - Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, requests).then(() => { + Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, persistedRequests).then(() => { Log.info(`[SequentialQueue] '${requestToRemove.command}' removed from the queue. Queue length is ${getLength()}`); }); } function update(oldRequestIndex: number, newRequest: Request) { + console.log(`${newRequest.command} oldRequestIndex`, oldRequestIndex); const requests = [...persistedRequests]; + console.log(`${newRequest.command} before requests`, {...requests}); requests.splice(oldRequestIndex, 1, newRequest); + console.log(`${newRequest.command} after requests`, {...requests}); persistedRequests = requests; + console.log(`${newRequest.command} persistedRequests`, {...persistedRequests}); Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, requests); } +function updateOngoingRequest(newRequest: Request) { + ongoingRequest = newRequest; +} + +function processNextRequest(): Request { + // You must handle the case where there are no requests to process + if (persistedRequests.length === 0) { + throw new Error('No requests to process'); + } + + // At least for now, you must handle the case where there is an ongoing request + if (ongoingRequest) { + throw new Error('There is already an ongoing request'); + } + ongoingRequest = persistedRequests[0]; + persistedRequests = persistedRequests.slice(1); + // We don't need to update Onyx persistedRequests just in case the ongoingRequest fails + // we want to keep trying if the user closes the app + return ongoingRequest; +} + function getAll(): Request[] { + console.log('getAll persistedRequests', {...persistedRequests}); return persistedRequests; } -export {clear, save, getAll, remove, update, getLength}; +function getOngoingRequest(): Request | null { + return ongoingRequest; +} + +export {clear, save, getAll, remove, update, getLength, getOngoingRequest, processNextRequest, updateOngoingRequest}; diff --git a/src/types/onyx/Request.ts b/src/types/onyx/Request.ts index 5d37cc559a4d..739e69669c40 100644 --- a/src/types/onyx/Request.ts +++ b/src/types/onyx/Request.ts @@ -56,9 +56,9 @@ type RequestData = { }; /** - * Model of a conflict request that has to be updated, in the request queue. + * Model of a conflict request that has to be replaced in the request queue. */ -type ConflictRequestUpdate = { +type ConflictRequestReplace = { /** * The action to take in case of a conflict. */ @@ -71,9 +71,9 @@ type ConflictRequestUpdate = { }; /** - * Model of a conflict request that has to be saved at the end the request queue. + * Model of a conflict request that has to be enqueued at the end of request queue. */ -type ConflictRequestSave = { +type ConflictRequestPush = { /** * The action to take in case of a conflict. */ @@ -81,7 +81,7 @@ type ConflictRequestSave = { }; /** - * Model of a conflict request that no need to be updated or saved, in the request queue. + * Model of a conflict request that does not need to be updated or saved in the request queue. */ type ConflictRequestNoAction = { /** @@ -97,7 +97,7 @@ type ConflictActionData = { /** * The action to take in case of a conflict. */ - conflictAction: ConflictRequestUpdate | ConflictRequestSave | ConflictRequestNoAction; + conflictAction: ConflictRequestReplace | ConflictRequestPush | ConflictRequestNoAction; }; /** diff --git a/tests/unit/APITest.ts b/tests/unit/APITest.ts index 74be6c742f51..400d460f0bd5 100644 --- a/tests/unit/APITest.ts +++ b/tests/unit/APITest.ts @@ -43,6 +43,7 @@ const originalXHR = HttpUtils.xhr; beforeEach(() => { global.fetch = TestHelper.getGlobalFetchMock(); HttpUtils.xhr = originalXHR; + MainQueue.clear(); HttpUtils.cancelPendingRequests(); PersistedRequests.clear(); @@ -166,23 +167,23 @@ describe('APITests', () => { .then(waitForBatchedUpdates) .then(() => { // Then requests should remain persisted until the xhr call is resolved - expect(PersistedRequests.getAll().length).toEqual(2); + expect(PersistedRequests.getAll().length).toEqual(1); xhrCalls[0].resolve({jsonCode: CONST.JSON_CODE.SUCCESS}); return waitForBatchedUpdates(); }) .then(waitForBatchedUpdates) .then(() => { - expect(PersistedRequests.getAll().length).toEqual(1); - expect(PersistedRequests.getAll()).toEqual([expect.objectContaining({command: 'mock command', data: expect.objectContaining({param2: 'value2'})})]); + expect(PersistedRequests.getAll().length).toEqual(0); + expect(PersistedRequests.getOngoingRequest()).toEqual(expect.objectContaining({command: 'mock command', data: expect.objectContaining({param2: 'value2'})})); // When a request fails it should be retried xhrCalls[1].reject(new Error(CONST.ERROR.FAILED_TO_FETCH)); return waitForBatchedUpdates(); }) .then(() => { - expect(PersistedRequests.getAll().length).toEqual(1); - expect(PersistedRequests.getAll()).toEqual([expect.objectContaining({command: 'mock command', data: expect.objectContaining({param2: 'value2'})})]); + expect(PersistedRequests.getAll().length).toEqual(0); + expect(PersistedRequests.getOngoingRequest()).toEqual(expect.objectContaining({command: 'mock command', data: expect.objectContaining({param2: 'value2'})})); // We need to advance past the request throttle back off timer because the request won't be retried until then return new Promise((resolve) => { @@ -191,7 +192,7 @@ describe('APITests', () => { }) .then(() => { // Finally, after it succeeds the queue should be empty - xhrCalls[2].resolve({jsonCode: CONST.JSON_CODE.SUCCESS}); + xhrCalls[1].resolve({jsonCode: CONST.JSON_CODE.SUCCESS}); return waitForBatchedUpdates(); }) .then(() => { @@ -366,6 +367,7 @@ describe('APITests', () => { test('several actions made while offline will get added in the order they are created when we need to reauthenticate', () => { // Given offline state where all requests will eventualy succeed without issue and assumed to be valid credentials const xhr = jest.spyOn(HttpUtils, 'xhr').mockResolvedValueOnce({jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED}).mockResolvedValue({jsonCode: CONST.JSON_CODE.SUCCESS}); + xhr.mockClear(); return Onyx.multiSet({ [ONYXKEYS.NETWORK]: {isOffline: true}, @@ -551,7 +553,7 @@ describe('APITests', () => { // THEN the queue should be stopped and there should be no more requests to run expect(SequentialQueue.isRunning()).toBe(false); expect(PersistedRequests.getAll().length).toBe(0); - + console.log('CALLS: ', xhr.mock.calls); // And our Write request should run before our non persistable one in a blocking way const firstRequest = xhr.mock.calls[0]; const [firstRequestCommandName] = firstRequest; diff --git a/tests/unit/SequentialQueueTest.ts b/tests/unit/SequentialQueueTest.ts index 9fb780828113..edae1df5062e 100644 --- a/tests/unit/SequentialQueueTest.ts +++ b/tests/unit/SequentialQueueTest.ts @@ -54,6 +54,10 @@ describe('SequentialQueue', () => { }; SequentialQueue.push(requestWithConflictResolution); expect(PersistedRequests.getLength()).toBe(1); + // We know there is only one request in the queue, so we can get the first one and verify + // that the persisted request is the second one. + const persistedRequest = PersistedRequests.getAll()[0]; + expect(persistedRequest?.data?.accountID).toBe(56789); }); it('should push two requests with conflict resolution and push', () => { @@ -116,7 +120,7 @@ describe('SequentialQueue', () => { // wait for Onyx.connect execute the callback and start processing the queue await Promise.resolve(); - const conflicyResolver = (persistedRequests: Request[]): ConflictActionData => { + const conflictResolver = (persistedRequests: Request[]): ConflictActionData => { // should be one instance of ReconnectApp, get the index to replace it later const index = persistedRequests.findIndex((r) => r.command === 'ReconnectApp'); if (index === -1) { @@ -131,13 +135,13 @@ describe('SequentialQueue', () => { const requestWithConflictResolution: Request = { command: 'ReconnectApp', data: {accountID: 56789}, - checkAndFixConflictingRequest: conflicyResolver, + checkAndFixConflictingRequest: conflictResolver, }; const requestWithConflictResolution2: Request = { command: 'ReconnectApp', data: {accountID: 56789}, - checkAndFixConflictingRequest: conflicyResolver, + checkAndFixConflictingRequest: conflictResolver, }; SequentialQueue.push(requestWithConflictResolution); @@ -145,4 +149,35 @@ describe('SequentialQueue', () => { expect(PersistedRequests.getLength()).toBe(2); }); + + it('should replace request request in queue while a similar one is ongoing and keep the same index', async () => { + SequentialQueue.push({command: 'OpenReport'}); + SequentialQueue.push(request); + + const requestWithConflictResolution: Request = { + command: 'ReconnectApp', + data: {accountID: 56789}, + checkAndFixConflictingRequest: (persistedRequests) => { + // should be one instance of ReconnectApp, get the index to replace it later + const index = persistedRequests.findIndex((r) => r.command === 'ReconnectApp'); + if (index === -1) { + return {conflictAction: {type: 'push'}}; + } + + return { + conflictAction: {type: 'replace', index}, + }; + }, + }; + + SequentialQueue.push(requestWithConflictResolution); + SequentialQueue.push({command: 'AddComment'}); + SequentialQueue.push({command: 'OpenReport'}); + + expect(PersistedRequests.getLength()).toBe(4); + const persistedRequests = PersistedRequests.getAll(); + // We know ReconnectApp is at index 1 in the queue, so we can get it to verify + // that was replaced by the new request. + expect(persistedRequests[1]?.data?.accountID).toBe(56789); + }); }); From 3620da6b3a014890cfd1e92f2e553ab8f4ed512a Mon Sep 17 00:00:00 2001 From: Eduardo Date: Tue, 10 Sep 2024 21:14:09 +0200 Subject: [PATCH 011/151] Fixed APITest --- src/libs/Network/SequentialQueue.ts | 10 +++++++--- src/libs/actions/PersistedRequests.ts | 14 +++++++++++++- tests/unit/APITest.ts | 12 ++++++------ 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index c1b79b015e12..a7ad79c4c281 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -63,6 +63,7 @@ function flushOnyxUpdatesQueue() { * requests to our backend is evenly distributed and it gradually decreases with time, which helps the servers catch up. */ function process(): Promise { + console.log('PROCESS'); // When the queue is paused, return early. This prevents any new requests from happening. The queue will be flushed again when the queue is unpaused. if (isQueuePaused) { Log.info('[SequentialQueue] Unable to process. Queue is paused.'); @@ -73,7 +74,7 @@ function process(): Promise { Log.info('[SequentialQueue] Unable to process. We are offline.'); return Promise.resolve(); } - + console.log('process -> checkking getAll'); const persistedRequests = PersistedRequests.getAll(); if (persistedRequests.length === 0) { Log.info('[SequentialQueue] Unable to process. No requests to process.'); @@ -99,14 +100,18 @@ function process(): Promise { .catch((error: RequestError) => { // On sign out we cancel any in flight requests from the user. Since that user is no longer signed in their requests should not be retried. // Duplicate records don't need to be retried as they just mean the record already exists on the server + console.log('CATCHING 1st error', error); if (error.name === CONST.ERROR.REQUEST_CANCELLED || error.message === CONST.ERROR.DUPLICATE_RECORD) { + console.log('CATCHING 1st error inside 1st IF -> remove clear RETURN'); PersistedRequests.remove(requestToProcess); RequestThrottle.clear(); return process(); } + PersistedRequests.rollbackOngoingRequest(); return RequestThrottle.sleep() .then(process) .catch(() => { + console.log('CATCHING 2nd error', error); Onyx.update(requestToProcess.failureData ?? []); PersistedRequests.remove(requestToProcess); RequestThrottle.clear(); @@ -140,7 +145,7 @@ function flush() { Log.info('[SequentialQueue] Unable to flush. Client is not the leader.'); return; } - + console.log('flushing -> isSequentialQueueRunning true'); isSequentialQueueRunning = true; // Reset the isReadyPromise so that the queue will be flushed as soon as the request is finished @@ -219,7 +224,6 @@ function push(newRequest: OnyxRequest) { PersistedRequests.save(newRequest); } - const final = PersistedRequests.getAll(); // If we are offline we don't need to trigger the queue to empty as it will happen when we come back online if (NetworkStore.isOffline()) { return; diff --git a/src/libs/actions/PersistedRequests.ts b/src/libs/actions/PersistedRequests.ts index 43675ecf8feb..6e2fac339550 100644 --- a/src/libs/actions/PersistedRequests.ts +++ b/src/libs/actions/PersistedRequests.ts @@ -104,6 +104,18 @@ function processNextRequest(): Request { return ongoingRequest; } +function rollbackOngoingRequest() { + if (!ongoingRequest) { + return; + } + + // Prepend ongoingRequest to persistedRequests + persistedRequests = [ongoingRequest, ...persistedRequests]; + + // Clear the ongoingRequest + ongoingRequest = null; +} + function getAll(): Request[] { console.log('getAll persistedRequests', {...persistedRequests}); return persistedRequests; @@ -113,4 +125,4 @@ function getOngoingRequest(): Request | null { return ongoingRequest; } -export {clear, save, getAll, remove, update, getLength, getOngoingRequest, processNextRequest, updateOngoingRequest}; +export {clear, save, getAll, remove, update, getLength, getOngoingRequest, processNextRequest, updateOngoingRequest, rollbackOngoingRequest}; diff --git a/tests/unit/APITest.ts b/tests/unit/APITest.ts index 400d460f0bd5..63d38fd7079f 100644 --- a/tests/unit/APITest.ts +++ b/tests/unit/APITest.ts @@ -182,21 +182,23 @@ describe('APITests', () => { return waitForBatchedUpdates(); }) .then(() => { - expect(PersistedRequests.getAll().length).toEqual(0); - expect(PersistedRequests.getOngoingRequest()).toEqual(expect.objectContaining({command: 'mock command', data: expect.objectContaining({param2: 'value2'})})); - + // The ongoingRequest it is moving back to the persistedRequests queue + expect(PersistedRequests.getAll().length).toEqual(1); + expect(PersistedRequests.getAll()).toEqual([expect.objectContaining({command: 'mock command', data: expect.objectContaining({param2: 'value2'})})]); // We need to advance past the request throttle back off timer because the request won't be retried until then return new Promise((resolve) => { setTimeout(resolve, CONST.NETWORK.MAX_RANDOM_RETRY_WAIT_TIME_MS); }).then(waitForBatchedUpdates); }) .then(() => { + // A new promise is created after the back off timer // Finally, after it succeeds the queue should be empty - xhrCalls[1].resolve({jsonCode: CONST.JSON_CODE.SUCCESS}); + xhrCalls[2].resolve({jsonCode: CONST.JSON_CODE.SUCCESS}); return waitForBatchedUpdates(); }) .then(() => { expect(PersistedRequests.getAll().length).toEqual(0); + expect(PersistedRequests.getOngoingRequest()).toBeNull(); }) ); }); @@ -367,7 +369,6 @@ describe('APITests', () => { test('several actions made while offline will get added in the order they are created when we need to reauthenticate', () => { // Given offline state where all requests will eventualy succeed without issue and assumed to be valid credentials const xhr = jest.spyOn(HttpUtils, 'xhr').mockResolvedValueOnce({jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED}).mockResolvedValue({jsonCode: CONST.JSON_CODE.SUCCESS}); - xhr.mockClear(); return Onyx.multiSet({ [ONYXKEYS.NETWORK]: {isOffline: true}, @@ -553,7 +554,6 @@ describe('APITests', () => { // THEN the queue should be stopped and there should be no more requests to run expect(SequentialQueue.isRunning()).toBe(false); expect(PersistedRequests.getAll().length).toBe(0); - console.log('CALLS: ', xhr.mock.calls); // And our Write request should run before our non persistable one in a blocking way const firstRequest = xhr.mock.calls[0]; const [firstRequestCommandName] = firstRequest; From d07d9126f2df2c10e1cf883f671cfea84e32f641 Mon Sep 17 00:00:00 2001 From: Eduardo Date: Wed, 11 Sep 2024 16:07:48 +0200 Subject: [PATCH 012/151] Fixed tests and code --- .../LHNOptionsList/LHNOptionsList.tsx | 2 +- .../Middleware/HandleUnusedOptimisticID.ts | 25 +++++--- src/libs/Network/SequentialQueue.ts | 17 ++---- src/libs/OptionsListUtils.ts | 2 +- src/libs/ReportActionsUtils.ts | 35 ++--------- src/libs/SidebarUtils.ts | 2 +- src/libs/actions/PersistedRequests.ts | 45 ++++++-------- tests/unit/PersistedRequests.ts | 61 +++++++++++++++++++ 8 files changed, 110 insertions(+), 79 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 04cba45a0bef..624e8f18e69e 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -139,7 +139,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio : '-1'; const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; const hasDraftComment = DraftCommentUtils.isValidDraftComment(draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`]); - const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions, false, reportID); + const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions); const lastReportAction = sortedReportActions[0]; // Get the transaction for the last report action diff --git a/src/libs/Middleware/HandleUnusedOptimisticID.ts b/src/libs/Middleware/HandleUnusedOptimisticID.ts index db726855d075..30707769a05c 100644 --- a/src/libs/Middleware/HandleUnusedOptimisticID.ts +++ b/src/libs/Middleware/HandleUnusedOptimisticID.ts @@ -34,14 +34,23 @@ const handleUnusedOptimisticID: Middleware = (requestResponse, request, isFromSe return; } const oldReportID = request.data?.reportID; - const offset = isFromSequentialQueue ? 1 : 0; - PersistedRequests.getAll() - .slice(offset) - .forEach((persistedRequest, index) => { - const persistedRequestClone = _.clone(persistedRequest); - persistedRequestClone.data = deepReplaceKeysAndValues(persistedRequest.data, oldReportID as string, preexistingReportID); - PersistedRequests.update(index + offset, persistedRequestClone); - }); + + if (isFromSequentialQueue) { + const ongoingRequest = PersistedRequests.getOngoingRequest(); + console.log('ongoingRequest', ongoingRequest); + console.log('oldReportID', oldReportID, 'preexistingReportID', preexistingReportID); + if (ongoingRequest && ongoingRequest.data?.reportID === oldReportID) { + const ongoingRequestClone = _.clone(ongoingRequest); + ongoingRequestClone.data = deepReplaceKeysAndValues(ongoingRequest.data, oldReportID as string, preexistingReportID); + PersistedRequests.updateOngoingRequest(ongoingRequestClone); + } + } + + PersistedRequests.getAll().forEach((persistedRequest, index) => { + const persistedRequestClone = _.clone(persistedRequest); + persistedRequestClone.data = deepReplaceKeysAndValues(persistedRequest.data, oldReportID as string, preexistingReportID); + PersistedRequests.update(index, persistedRequestClone); + }); }); return response; }); diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index a7ad79c4c281..4d0f8c6c7b5d 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -63,7 +63,6 @@ function flushOnyxUpdatesQueue() { * requests to our backend is evenly distributed and it gradually decreases with time, which helps the servers catch up. */ function process(): Promise { - console.log('PROCESS'); // When the queue is paused, return early. This prevents any new requests from happening. The queue will be flushed again when the queue is unpaused. if (isQueuePaused) { Log.info('[SequentialQueue] Unable to process. Queue is paused.'); @@ -74,16 +73,15 @@ function process(): Promise { Log.info('[SequentialQueue] Unable to process. We are offline.'); return Promise.resolve(); } - console.log('process -> checkking getAll'); + const persistedRequests = PersistedRequests.getAll(); if (persistedRequests.length === 0) { Log.info('[SequentialQueue] Unable to process. No requests to process.'); return Promise.resolve(); } - const requestToProcess = PersistedRequests.processNextRequest(); // persistedRequests[0]; - console.log('next process requestToProcess', {...requestToProcess}); - // currentRequest = requestToProcess; + const requestToProcess = PersistedRequests.processNextRequest(); + // Set the current request to a promise awaiting its processing so that getCurrentRequest can be used to take some action after the current request has processed. currentRequestPromise = Request.processWithMiddleware(requestToProcess, true) .then((response) => { @@ -93,6 +91,7 @@ function process(): Promise { Log.info("[SequentialQueue] Handled 'shouldPauseQueue' in response. Pausing the queue."); pause(); } + PersistedRequests.remove(requestToProcess); RequestThrottle.clear(); return process(); @@ -100,9 +99,7 @@ function process(): Promise { .catch((error: RequestError) => { // On sign out we cancel any in flight requests from the user. Since that user is no longer signed in their requests should not be retried. // Duplicate records don't need to be retried as they just mean the record already exists on the server - console.log('CATCHING 1st error', error); if (error.name === CONST.ERROR.REQUEST_CANCELLED || error.message === CONST.ERROR.DUPLICATE_RECORD) { - console.log('CATCHING 1st error inside 1st IF -> remove clear RETURN'); PersistedRequests.remove(requestToProcess); RequestThrottle.clear(); return process(); @@ -111,7 +108,6 @@ function process(): Promise { return RequestThrottle.sleep() .then(process) .catch(() => { - console.log('CATCHING 2nd error', error); Onyx.update(requestToProcess.failureData ?? []); PersistedRequests.remove(requestToProcess); RequestThrottle.clear(); @@ -145,7 +141,7 @@ function flush() { Log.info('[SequentialQueue] Unable to flush. Client is not the leader.'); return; } - console.log('flushing -> isSequentialQueueRunning true'); + isSequentialQueueRunning = true; // Reset the isReadyPromise so that the queue will be flushed as soon as the request is finished @@ -207,11 +203,10 @@ NetworkStore.onReconnection(flush); function push(newRequest: OnyxRequest) { // If a request is already being processed, ignore it when looking for potentially conflicting requests - const requests = PersistedRequests.getAll(); //.filter((persistedRequest) => persistedRequest !== currentRequest); + const requests = PersistedRequests.getAll(); const {checkAndFixConflictingRequest} = newRequest; if (checkAndFixConflictingRequest) { - console.log('ReconnectApp checkAndFixConflictingRequest', {...requests}); const {conflictAction} = checkAndFixConflictingRequest(requests); if (conflictAction.type === 'push') { diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 048f40257f07..f191c1d06532 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -299,7 +299,7 @@ Onyx.connect({ Object.entries(allReportActions).forEach((reportActions) => { const reportID = reportActions[0].split('_')[1]; const reportActionsArray = Object.values(reportActions[1] ?? {}); - let sortedReportActions = ReportActionUtils.getSortedReportActions(reportActionsArray, true, reportID); + let sortedReportActions = ReportActionUtils.getSortedReportActions(reportActionsArray, true); allSortedReportActions[reportID] = sortedReportActions; // If the report is a one-transaction report and has , we need to return the combined reportActions so that the LHN can display modifications diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index e653bcccbebb..673fafe90fd2 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -49,7 +49,6 @@ type MemberChangeMessageRoomReferenceElement = { type MemberChangeMessageElement = MessageTextElement | MemberChangeMessageUserMentionElement | MemberChangeMessageRoomReferenceElement; let allReportActions: OnyxCollection; -const cachedSortedReportActions = new Map(); Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, waitForCollectionCallback: true, @@ -62,20 +61,6 @@ Onyx.connect({ }, }); -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - callback: (actions) => { - if (!actions) { - return; - } - // console.log('LOOKING FOR REPORT changed>', actions); - if (cachedSortedReportActions.has(actions.reportID)) { - // console.log('LOOKING FOR REPORT changed> DELETING', actions.reportID); - cachedSortedReportActions.delete(actions.reportID); - } - }, -}); - let isNetworkOffline = false; Onyx.connect({ key: ONYXKEYS.NETWORK, @@ -379,16 +364,11 @@ function isTransactionThread(parentReportAction: OnyxInputOrEntry) * This gives us a stable order even in the case of multiple reportActions created on the same millisecond * */ -function getSortedReportActions(reportActions: ReportAction[] | null, shouldSortInDescendingOrder = false, reportID: string | undefined = undefined): ReportAction[] { +function getSortedReportActions(reportActions: ReportAction[] | null, shouldSortInDescendingOrder = false): ReportAction[] { if (!Array.isArray(reportActions)) { throw new Error(`ReportActionsUtils.getSortedReportActions requires an array, received ${typeof reportActions}`); } - // console.log('getSortedReportActions - INITIAL reportActions', reportActions); - if (reportID && cachedSortedReportActions.has(reportID)) { - // console.log('getSortedReportActions - CACHED sortedActions', cachedSortedReportActions.get(reportID)); - return cachedSortedReportActions.get(reportID) ?? []; - } const invertedMultiplier = shouldSortInDescendingOrder ? -1 : 1; const sortedActions = reportActions?.filter(Boolean).sort((first, second) => { @@ -411,10 +391,7 @@ function getSortedReportActions(reportActions: ReportAction[] | null, shouldSort // will be consistent across all users and devices return (first.reportActionID < second.reportActionID ? -1 : 1) * invertedMultiplier; }); - // console.log('getSortedReportActions - FINAL sortedActions', sortedActions); - if (reportID) { - cachedSortedReportActions.set(reportID, sortedActions); - } + return sortedActions; } @@ -465,7 +442,7 @@ function getCombinedReportActions( return actionType !== CONST.IOU.REPORT_ACTION_TYPE.CREATE && actionType !== CONST.IOU.REPORT_ACTION_TYPE.TRACK; }); - return getSortedReportActions(filteredReportActions, true, report?.reportID); + return getSortedReportActions(filteredReportActions, true); } /** @@ -755,7 +732,7 @@ function getLastVisibleAction(reportID: string, actionsToMerge: Record shouldReportActionBeVisibleAsLastAction(action)); - const sortedReportActions = getSortedReportActions(visibleReportActions, true, reportID); + const sortedReportActions = getSortedReportActions(visibleReportActions, true); if (sortedReportActions.length === 0) { return undefined; } @@ -808,7 +785,7 @@ function filterOutDeprecatedReportActions(reportActions: OnyxEntry | ReportAction[], shouldIncludeInvisibleActions = false, reportID: string | undefined): ReportAction[] { +function getSortedReportActionsForDisplay(reportActions: OnyxEntry | ReportAction[], shouldIncludeInvisibleActions = false): ReportAction[] { let filteredReportActions: ReportAction[] = []; if (!reportActions) { return []; @@ -823,7 +800,7 @@ function getSortedReportActionsForDisplay(reportActions: OnyxEntry replaceBaseURLInPolicyChangeLogAction(reportAction)); - return getSortedReportActions(baseURLAdjustedReportActions, true, reportID); + return getSortedReportActions(baseURLAdjustedReportActions, true); } /** diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 404f665e4aae..4bd7e2714e25 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -41,7 +41,7 @@ Onyx.connect({ } const reportID = CollectionUtils.extractCollectionItemID(key); - const actionsArray: ReportAction[] = ReportActionsUtils.getSortedReportActions(Object.values(actions), false, reportID); + const actionsArray: ReportAction[] = ReportActionsUtils.getSortedReportActions(Object.values(actions)); // The report is only visible if it is the last action not deleted that // does not match a closed or created state. diff --git a/src/libs/actions/PersistedRequests.ts b/src/libs/actions/PersistedRequests.ts index 6e2fac339550..49ed6bc6473a 100644 --- a/src/libs/actions/PersistedRequests.ts +++ b/src/libs/actions/PersistedRequests.ts @@ -10,16 +10,14 @@ let ongoingRequest: Request | null = null; Onyx.connect({ key: ONYXKEYS.PERSISTED_REQUESTS, callback: (val) => { - console.log('ReconnectApp PERSISTED_REQUESTS val', {...val}, ongoingRequest); // it has the ongoingRequest in here? persistedRequests = val ?? []; if (ongoingRequest && persistedRequests.length > 0) { - const elem = {...persistedRequests}[0]; - console.log('First persistedRequests', elem, ' are equals: ', isEqual(elem, ongoingRequest)); - // here we try to remove the first element from the persistedRequests if it is the same as ongoingRequest - if (isEqual(elem, ongoingRequest)) { - console.log('First persistedRequests is equal to ongoingRequest'); + const nextRequestToProcess = persistedRequests[0]; + // We try to remove the next request from the persistedRequests if it is the same as ongoingRequest + // so we don't process it twice. + if (isEqual(nextRequestToProcess, ongoingRequest)) { persistedRequests = persistedRequests.slice(1); } } @@ -49,37 +47,29 @@ function save(requestToPersist: Request) { } function remove(requestToRemove: Request) { - console.log('remove requestToRemove - init>', {...requestToRemove}); - if (isEqual(ongoingRequest, requestToRemove)) { - console.log('remove ongoingRequest', {...ongoingRequest}); - ongoingRequest = null; - } else { - /** - * We only remove the first matching request because the order of requests matters. - * If we were to remove all matching requests, we can end up with a final state that is different than what the user intended. - */ - const requests = [...persistedRequests]; - const index = requests.findIndex((persistedRequest) => isEqual(persistedRequest, requestToRemove)); - console.log('current queue: ', requests, 'remove index', index); - if (index === -1) { - return; - } - requests.splice(index, 1); - persistedRequests = requests; + ongoingRequest = null; + /** + * We only remove the first matching request because the order of requests matters. + * If we were to remove all matching requests, we can end up with a final state that is different than what the user intended. + */ + const requests = [...persistedRequests]; + const index = requests.findIndex((persistedRequest) => isEqual(persistedRequest, requestToRemove)); + + if (index === -1) { + return; } + requests.splice(index, 1); + persistedRequests = requests; + Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, persistedRequests).then(() => { Log.info(`[SequentialQueue] '${requestToRemove.command}' removed from the queue. Queue length is ${getLength()}`); }); } function update(oldRequestIndex: number, newRequest: Request) { - console.log(`${newRequest.command} oldRequestIndex`, oldRequestIndex); const requests = [...persistedRequests]; - console.log(`${newRequest.command} before requests`, {...requests}); requests.splice(oldRequestIndex, 1, newRequest); - console.log(`${newRequest.command} after requests`, {...requests}); persistedRequests = requests; - console.log(`${newRequest.command} persistedRequests`, {...persistedRequests}); Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, requests); } @@ -117,7 +107,6 @@ function rollbackOngoingRequest() { } function getAll(): Request[] { - console.log('getAll persistedRequests', {...persistedRequests}); return persistedRequests; } diff --git a/tests/unit/PersistedRequests.ts b/tests/unit/PersistedRequests.ts index 670625f65f97..3a814ca28c2f 100644 --- a/tests/unit/PersistedRequests.ts +++ b/tests/unit/PersistedRequests.ts @@ -1,5 +1,9 @@ +import Onyx from 'react-native-onyx'; import * as PersistedRequests from '../../src/libs/actions/PersistedRequests'; +import ONYXKEYS from '../../src/ONYXKEYS'; import type Request from '../../src/types/onyx/Request'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; const request: Request = { command: 'OpenReport', @@ -7,13 +11,22 @@ const request: Request = { failureData: [{key: 'reportMetadata_2', onyxMethod: 'merge', value: {}}], }; +beforeAll(() => + Onyx.init({ + keys: ONYXKEYS, + safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS], + }), +); + beforeEach(() => { + wrapOnyxWithWaitForBatchedUpdates(Onyx); PersistedRequests.clear(); PersistedRequests.save(request); }); afterEach(() => { PersistedRequests.clear(); + Onyx.clear(); }); describe('PersistedRequests', () => { @@ -26,4 +39,52 @@ describe('PersistedRequests', () => { PersistedRequests.remove(request); expect(PersistedRequests.getAll().length).toBe(0); }); + + it('when process the next request, queue should be empty', () => { + const nextRequest = PersistedRequests.processNextRequest(); + expect(PersistedRequests.getAll().length).toBe(0); + expect(nextRequest).toEqual(request); + }); + + it('when onyx persist the request, it should remove from the list the ongoing request', () => { + const request2: Request = { + command: 'AddComment', + successData: [{key: 'reportMetadata_3', onyxMethod: 'merge', value: {}}], + failureData: [{key: 'reportMetadata_4', onyxMethod: 'merge', value: {}}], + }; + PersistedRequests.save(request2); + PersistedRequests.processNextRequest(); + return waitForBatchedUpdates().then(() => { + expect(PersistedRequests.getAll().length).toBe(1); + expect(PersistedRequests.getAll()[0]).toEqual(request2); + }); + }); + + it('update the request at the given index with new data', () => { + const newRequest: Request = { + command: 'OpenReport', + successData: [{key: 'reportMetadata_1', onyxMethod: 'set', value: {}}], + failureData: [{key: 'reportMetadata_2', onyxMethod: 'set', value: {}}], + }; + PersistedRequests.update(0, newRequest); + expect(PersistedRequests.getAll()[0]).toEqual(newRequest); + }); + + it('update the ongoing request with new data', () => { + const newRequest: Request = { + command: 'OpenReport', + successData: [{key: 'reportMetadata_1', onyxMethod: 'set', value: {}}], + failureData: [{key: 'reportMetadata_2', onyxMethod: 'set', value: {}}], + }; + PersistedRequests.updateOngoingRequest(newRequest); + expect(PersistedRequests.getOngoingRequest()).toEqual(newRequest); + }); + + it('when removing a request should update the persistedRequests queue and clear the ongoing request', () => { + PersistedRequests.processNextRequest(); + expect(PersistedRequests.getOngoingRequest()).toEqual(request); + PersistedRequests.remove(request); + expect(PersistedRequests.getOngoingRequest()).toBeNull(); + expect(PersistedRequests.getAll().length).toBe(0); + }); }); From 7170e14a130af8b1d0ef15acb818c7e340331cd9 Mon Sep 17 00:00:00 2001 From: Eduardo Date: Wed, 11 Sep 2024 19:26:46 +0200 Subject: [PATCH 013/151] Fixed lint issues --- src/hooks/useReportIDs.tsx | 6 ++---- src/libs/Middleware/HandleUnusedOptimisticID.ts | 2 -- tests/unit/SequentialQueueTest.ts | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/hooks/useReportIDs.tsx b/src/hooks/useReportIDs.tsx index 0ac82a61171b..b7d84cb25196 100644 --- a/src/hooks/useReportIDs.tsx +++ b/src/hooks/useReportIDs.tsx @@ -37,9 +37,8 @@ const ReportIDsContext = createContext({ * This function (and the few below it), narrow down the data from Onyx to just the properties that we want to trigger a re-render of the component. This helps minimize re-rendering * and makes the entire component more performant because it's not re-rendering when a bunch of properties change which aren't ever used in the UI. */ -const reportActionsSelector = (reportActions: OnyxEntry): ReportActionsSelector => { - console.log('reportActions', reportActions); - return (reportActions && +const reportActionsSelector = (reportActions: OnyxEntry): ReportActionsSelector => + (reportActions && Object.values(reportActions) .filter(Boolean) .map((reportAction) => { @@ -59,7 +58,6 @@ const reportActionsSelector = (reportActions: OnyxEntry originalMessage, }; })) as ReportActionsSelector; -}; const policySelector = (policy: OnyxEntry): PolicySelector => (policy && { diff --git a/src/libs/Middleware/HandleUnusedOptimisticID.ts b/src/libs/Middleware/HandleUnusedOptimisticID.ts index 30707769a05c..eaf827af8858 100644 --- a/src/libs/Middleware/HandleUnusedOptimisticID.ts +++ b/src/libs/Middleware/HandleUnusedOptimisticID.ts @@ -37,8 +37,6 @@ const handleUnusedOptimisticID: Middleware = (requestResponse, request, isFromSe if (isFromSequentialQueue) { const ongoingRequest = PersistedRequests.getOngoingRequest(); - console.log('ongoingRequest', ongoingRequest); - console.log('oldReportID', oldReportID, 'preexistingReportID', preexistingReportID); if (ongoingRequest && ongoingRequest.data?.reportID === oldReportID) { const ongoingRequestClone = _.clone(ongoingRequest); ongoingRequestClone.data = deepReplaceKeysAndValues(ongoingRequest.data, oldReportID as string, preexistingReportID); diff --git a/tests/unit/SequentialQueueTest.ts b/tests/unit/SequentialQueueTest.ts index edae1df5062e..2338e8f7c65f 100644 --- a/tests/unit/SequentialQueueTest.ts +++ b/tests/unit/SequentialQueueTest.ts @@ -150,7 +150,7 @@ describe('SequentialQueue', () => { expect(PersistedRequests.getLength()).toBe(2); }); - it('should replace request request in queue while a similar one is ongoing and keep the same index', async () => { + it('should replace request request in queue while a similar one is ongoing and keep the same index', () => { SequentialQueue.push({command: 'OpenReport'}); SequentialQueue.push(request); From 85c7680df409f5ea15a4dce8d636666fbb2e68b4 Mon Sep 17 00:00:00 2001 From: daledah Date: Mon, 16 Sep 2024 03:47:51 +0700 Subject: [PATCH 014/151] fix: requires validate when opening bank account --- src/languages/en.ts | 2 + src/languages/es.ts | 2 + src/pages/AddPersonalBankAccountPage.tsx | 104 ++++++++++++------ .../settings/Wallet/PaymentMethodList.tsx | 6 +- 4 files changed, 75 insertions(+), 39 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index f36db113a2aa..d60558eb0b6c 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -995,6 +995,8 @@ export default { contacts: { contactMethod: 'Contact method', contactMethods: 'Contact methods', + featureRequiresValidate: 'This feature requires you to validate your account.', + validateAccount: 'Validate your account', helpTextBeforeEmail: 'Add more ways for people to find you, and forward receipts to ', helpTextAfterEmail: ' from multiple email addresses.', pleaseVerify: 'Please verify this contact method', diff --git a/src/languages/es.ts b/src/languages/es.ts index 8a18c6c002e0..24a39b526d75 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -997,6 +997,8 @@ export default { contacts: { contactMethod: 'Método de contacto', contactMethods: 'Métodos de contacto', + featureRequiresValidate: 'Esta función requiere que valides tu cuenta.', + validateAccount: 'Valida tu cuenta', helpTextBeforeEmail: 'Añade más formas de que la gente te encuentre y reenvía los recibos a ', helpTextAfterEmail: ' desde varias direcciones de correo electrónico.', pleaseVerify: 'Por favor, verifica este método de contacto', diff --git a/src/pages/AddPersonalBankAccountPage.tsx b/src/pages/AddPersonalBankAccountPage.tsx index 04cce885bd07..4f0edb630f59 100644 --- a/src/pages/AddPersonalBankAccountPage.tsx +++ b/src/pages/AddPersonalBankAccountPage.tsx @@ -8,13 +8,17 @@ import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; +import ValidateCodeActionModal from '@components/ValidateCodeActionModal'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; import getPlaidOAuthReceivedRedirectURI from '@libs/getPlaidOAuthReceivedRedirectURI'; import Navigation from '@libs/Navigation/Navigation'; import * as BankAccounts from '@userActions/BankAccounts'; import * as PaymentMethods from '@userActions/PaymentMethods'; +import * as User from '@userActions/User'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; import type {PersonalBankAccount, PlaidData} from '@src/types/onyx'; @@ -31,8 +35,23 @@ function AddPersonalBankAccountPage({personalBankAccount, plaidData}: AddPersona const {translate} = useLocalize(); const [selectedPlaidAccountId, setSelectedPlaidAccountId] = useState(''); const [isUserValidated] = useOnyx(ONYXKEYS.USER, {selector: (user) => !!user?.validated}); + const [isValidateCodeActionModalVisible, setIsValidateCodeActionModalVisible] = useState(!isUserValidated); const shouldShowSuccess = personalBankAccount?.shouldShowSuccess ?? false; + const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST); + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const primaryLogin = account?.primaryLogin; + const loginData = loginList?.[primaryLogin ?? '']; + const validateLoginError = ErrorUtils.getEarliestErrorField(loginData, 'validateLogin'); + + const handleSubmitForm = useCallback( + (submitCode: string) => { + User.validateSecondaryLogin(loginList, primaryLogin ?? '', submitCode); + Navigation.navigate(ROUTES.SETTINGS_ADD_BANK_ACCOUNT); + }, + [loginList, primaryLogin], + ); + const submitBankAccountForm = useCallback(() => { const bankAccounts = plaidData?.bankAccounts ?? []; const selectedPlaidBankAccount = bankAccounts.find((bankAccount) => bankAccount.plaidAccountID === selectedPlaidAccountId); @@ -67,43 +86,58 @@ function AddPersonalBankAccountPage({personalBankAccount, plaidData}: AddPersona shouldShowOfflineIndicator={false} testID={AddPersonalBankAccountPage.displayName} > - - - {shouldShowSuccess ? ( - exitFlow(true)} + {isUserValidated && ( + + - ) : ( - 0} - submitButtonText={translate('common.saveAndContinue')} - scrollContextEnabled - onSubmit={submitBankAccountForm} - validate={BankAccounts.validatePlaidSelection} - style={[styles.mh5, styles.flex1]} - > - Navigation.goBack()} - receivedRedirectURI={getPlaidOAuthReceivedRedirectURI()} - selectedPlaidAccountID={selectedPlaidAccountId} + {shouldShowSuccess ? ( + exitFlow(true)} /> - - )} - + ) : ( + 0} + submitButtonText={translate('common.saveAndContinue')} + scrollContextEnabled + onSubmit={submitBankAccountForm} + validate={BankAccounts.validatePlaidSelection} + style={[styles.mh5, styles.flex1]} + > + Navigation.goBack()} + receivedRedirectURI={getPlaidOAuthReceivedRedirectURI()} + selectedPlaidAccountID={selectedPlaidAccountId} + /> + + )} + + )} + { + setIsValidateCodeActionModalVisible(false); + exitFlow(); + }} + handleSubmitForm={handleSubmitForm} + clearError={() => {}} + /> ); } diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index 5f77ac33becf..0206c368d0c0 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -4,7 +4,7 @@ import React, {useCallback, useMemo} from 'react'; import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; import {FlatList, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {useOnyx, withOnyx} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import type {SvgProps} from 'react-native-svg/lib/typescript/ReactNativeSVG'; import type {ValueOf} from 'type-fest'; import type {RenderSuggestionMenuItemProps} from '@components/AutoCompleteSuggestions/types'; @@ -198,7 +198,6 @@ function PaymentMethodList({ const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const [isUserValidated] = useOnyx(ONYXKEYS.USER, {selector: (user) => !!user?.validated}); const getDescriptionForPolicyDomainCard = (domainName: string): string => { // A domain name containing a policyID indicates that this is a workspace feed @@ -332,11 +331,10 @@ function PaymentMethodList({ wrapperStyle={[styles.paymentMethod, listItemStyle]} hoverAndPressStyle={styles.hoveredComponentBG} ref={buttonRef} - disabled={!isUserValidated} /> ), - [onPress, translate, styles.paymentMethod, styles.hoveredComponentBG, listItemStyle, buttonRef, isUserValidated], + [onPress, translate, styles.paymentMethod, styles.hoveredComponentBG, listItemStyle, buttonRef], ); /** From b56aebd5581d8052dd76c5f1aa352eac1ee469b6 Mon Sep 17 00:00:00 2001 From: daledah Date: Mon, 16 Sep 2024 16:47:09 +0700 Subject: [PATCH 015/151] fix: migrate withFullTransactionOrNotFound to useOnyx --- .../step/withFullTransactionOrNotFound.tsx | 34 ++++++------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx index 491c37c9a402..b4984287955a 100644 --- a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx +++ b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx @@ -2,8 +2,7 @@ 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 type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import getComponentDisplayName from '@libs/getComponentDisplayName'; import * as IOUUtils from '@libs/IOUUtils'; @@ -11,12 +10,6 @@ import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; -import type {Transaction} from '@src/types/onyx'; - -type WithFullTransactionOrNotFoundOnyxProps = { - /** Indicates whether the report data is loading */ - transaction: OnyxEntry; -}; type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.CREATE @@ -40,12 +33,18 @@ type MoneyRequestRouteName = type Route = RouteProp; -type WithFullTransactionOrNotFoundProps = WithFullTransactionOrNotFoundOnyxProps & {route: Route}; +type WithFullTransactionOrNotFoundProps = {route: Route}; export default function , TRef>(WrappedComponent: ComponentType>) { // eslint-disable-next-line rulesdir/no-negated-variables function WithFullTransactionOrNotFound(props: TProps, ref: ForwardedRef) { - const transactionID = props.transaction?.transactionID; + const transactionID = props.route.params.transactionID ?? -1; + const userAction = 'action' in props.route.params && props.route.params.action ? props.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(); @@ -60,6 +59,7 @@ export default function ); @@ -67,19 +67,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}; From a372b9e179759bb198fa920c2891a236f6a17422 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Mon, 16 Sep 2024 16:59:01 +0700 Subject: [PATCH 016/151] migrate AccessOrNotFoundWrapper from withOnyx to useOnyx --- .../workspace/AccessOrNotFoundWrapper.tsx | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx index 1a6f59830506..9e64421eafcd 100644 --- a/src/pages/workspace/AccessOrNotFoundWrapper.tsx +++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx @@ -1,7 +1,7 @@ /* eslint-disable rulesdir/no-negated-variables */ import React, {useEffect, useState} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import type {FullPageNotFoundViewProps} from '@components/BlockingViews/FullPageNotFoundView'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -54,7 +54,7 @@ type AccessOrNotFoundWrapperOnyxProps = { isLoadingReportData: OnyxEntry; }; -type AccessOrNotFoundWrapperProps = AccessOrNotFoundWrapperOnyxProps & { +type AccessOrNotFoundWrapperProps = { /** The children to render */ children: ((props: AccessOrNotFoundWrapperOnyxProps) => React.ReactNode) | React.ReactNode; @@ -103,12 +103,15 @@ function PageNotFoundFallback({policyID, shouldShowFullScreenFallback, fullPageN } function AccessOrNotFoundWrapper({accessVariants = [], fullPageNotFoundViewProps, shouldBeBlocked, ...props}: AccessOrNotFoundWrapperProps) { - const {policy, policyID, report, iouType, allPolicies, featureName, isLoadingReportData} = props; + const {policyID, reportID, iouType, allPolicies, featureName} = props; + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const [isLoadingReportData] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA, {initialValue: true}); const {login = ''} = useCurrentUserPersonalDetails(); const isPolicyIDInRoute = !!policyID?.length; const isMoneyRequest = !!iouType && IOUUtils.isValidMoneyRequestType(iouType); const isFromGlobalCreate = isEmptyObject(report?.reportID); - const pendingField = featureName ? props.policy?.pendingFields?.[featureName] : undefined; + const pendingField = featureName ? policy?.pendingFields?.[featureName] : undefined; useEffect(() => { if (!isPolicyIDInRoute || !isEmptyObject(policy)) { @@ -160,19 +163,9 @@ function AccessOrNotFoundWrapper({accessVariants = [], fullPageNotFoundViewProps ); } - return callOrReturn(props.children, props); + return callOrReturn(props.children, {report, policy, isLoadingReportData}); } export type {AccessVariant}; -export default withOnyx({ - report: { - key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - }, - policy: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - }, - isLoadingReportData: { - key: ONYXKEYS.IS_LOADING_REPORT_DATA, - }, -})(AccessOrNotFoundWrapper); +export default AccessOrNotFoundWrapper; From b9d19f0edbae89173d3b6922ea79c4d212e31547 Mon Sep 17 00:00:00 2001 From: Eduardo Date: Mon, 16 Sep 2024 15:46:47 +0200 Subject: [PATCH 017/151] Tests to App.reconnectApp + some minors --- src/libs/Network/SequentialQueue.ts | 2 ++ tests/actions/SessionTest.ts | 38 +++++++++++++++++++++++++++++ tests/unit/PersistedRequests.ts | 1 + 3 files changed, 41 insertions(+) diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index 08f55bab5aea..1502b822463e 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -214,6 +214,8 @@ function push(newRequest: OnyxRequest) { PersistedRequests.save(newRequest); } else if (conflictAction.type === 'replace') { PersistedRequests.update(conflictAction.index, newRequest); + } else { + Log.info(`[SequentialQueue] No action performed to command ${newRequest.command} and it will be ignored.`); } } else { // Add request to Persisted Requests so that it can be retried if it fails diff --git a/tests/actions/SessionTest.ts b/tests/actions/SessionTest.ts index 62d6a54b20b5..f36efa8e4cd8 100644 --- a/tests/actions/SessionTest.ts +++ b/tests/actions/SessionTest.ts @@ -7,6 +7,7 @@ import HttpUtils from '@libs/HttpUtils'; import PushNotification from '@libs/Notification/PushNotification'; // This lib needs to be imported, but it has nothing to export since all it contains is an Onyx connection import '@libs/Notification/PushNotification/subscribePushNotification'; +import * as PersistedRequests from '@userActions/PersistedRequests'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Credentials, Session} from '@src/types/onyx'; @@ -105,4 +106,41 @@ describe('Session', () => { TestHelper.signInWithTestUser() .then(TestHelper.signOutTestUser) .then(() => expect(PushNotification.deregister).toBeCalled())); + + test('ReconnectApp should push request to the queue', () => { + return TestHelper.signInWithTestUser() + .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: true})) + .then(() => { + App.confirmReadyToOpenApp(); + App.reconnectApp(); + }) + .then(waitForBatchedUpdates) + .then(() => { + expect(PersistedRequests.getAll().length).toBe(1); + Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); + }) + .then(waitForBatchedUpdates) + .then(() => { + expect(PersistedRequests.getAll().length).toBe(0); + }); + }); + test('ReconnectApp should replace same requests from the queue', () => { + return TestHelper.signInWithTestUser() + .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: true})) + .then(() => { + App.confirmReadyToOpenApp(); + App.reconnectApp(); + App.reconnectApp(); + App.reconnectApp(); + App.reconnectApp(); + }) + .then(waitForBatchedUpdates) + .then(() => { + expect(PersistedRequests.getAll().length).toBe(1); + }) + .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false})) + .then(() => { + expect(PersistedRequests.getAll().length).toBe(0); + }); + }); }); diff --git a/tests/unit/PersistedRequests.ts b/tests/unit/PersistedRequests.ts index 3a814ca28c2f..476b3f963951 100644 --- a/tests/unit/PersistedRequests.ts +++ b/tests/unit/PersistedRequests.ts @@ -47,6 +47,7 @@ describe('PersistedRequests', () => { }); it('when onyx persist the request, it should remove from the list the ongoing request', () => { + expect(PersistedRequests.getAll().length).toBe(1); const request2: Request = { command: 'AddComment', successData: [{key: 'reportMetadata_3', onyxMethod: 'merge', value: {}}], From 0e1343e1884c9c0f9512ec8821f705c72ddd891e Mon Sep 17 00:00:00 2001 From: daledah Date: Tue, 17 Sep 2024 10:16:10 +0700 Subject: [PATCH 018/151] fix: lint --- .../step/withFullTransactionOrNotFound.tsx | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx index b4984287955a..a05da39c062b 100644 --- a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx +++ b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx @@ -3,6 +3,7 @@ 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 FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import getComponentDisplayName from '@libs/getComponentDisplayName'; import * as IOUUtils from '@libs/IOUUtils'; @@ -10,6 +11,12 @@ import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; +import type {Transaction} from '@src/types/onyx'; + +type WithFullTransactionOrNotFoundOnyxProps = { + /** Indicates whether the report data is loading */ + transaction: OnyxEntry; +}; type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.CREATE @@ -33,13 +40,16 @@ type MoneyRequestRouteName = type Route = RouteProp; -type WithFullTransactionOrNotFoundProps = {route: Route}; +type WithFullTransactionOrNotFoundProps = WithFullTransactionOrNotFoundOnyxProps & { + route: Route; +}; export default function , TRef>(WrappedComponent: ComponentType>) { // eslint-disable-next-line rulesdir/no-negated-variables - function WithFullTransactionOrNotFound(props: TProps, ref: ForwardedRef) { - const transactionID = props.route.params.transactionID ?? -1; - const userAction = 'action' in props.route.params && props.route.params.action ? props.route.params.action : CONST.IOU.ACTION.CREATE; + 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); @@ -58,8 +68,9 @@ export default function ); From dc6aa40a0aa77b870366a793b74ad69166543c4b Mon Sep 17 00:00:00 2001 From: Eduardo Date: Tue, 17 Sep 2024 13:04:20 +0200 Subject: [PATCH 019/151] Cleanup --- src/libs/Network/SequentialQueue.ts | 2 - src/libs/ReportActionsUtils.ts | 1 - tests/actions/SessionTest.ts | 185 ++++++++++++++-------------- 3 files changed, 94 insertions(+), 94 deletions(-) diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index 1502b822463e..82093e3eb456 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -25,7 +25,6 @@ let isReadyPromise = new Promise((resolve) => { resolveIsReadyPromise?.(); let isSequentialQueueRunning = false; -// let currentRequest: OnyxRequest | null = null; let currentRequestPromise: Promise | null = null; let isQueuePaused = false; @@ -164,7 +163,6 @@ function flush() { if (NetworkStore.isOffline() || PersistedRequests.getAll().length === 0) { resolveIsReadyPromise?.(); } - // currentRequest = null; currentRequestPromise = null; // The queue can be paused when we sync the data with backend so we should only update the Onyx data when the queue is empty diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 422904e8b5a8..c78406df139e 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -56,7 +56,6 @@ Onyx.connect({ if (!actions) { return; } - // console.log('allReportActions', actions); allReportActions = actions; }, }); diff --git a/tests/actions/SessionTest.ts b/tests/actions/SessionTest.ts index f36efa8e4cd8..e46806bef99e 100644 --- a/tests/actions/SessionTest.ts +++ b/tests/actions/SessionTest.ts @@ -3,6 +3,7 @@ import Onyx from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import * as App from '@libs/actions/App'; import OnyxUpdateManager from '@libs/actions/OnyxUpdateManager'; +import {WRITE_COMMANDS} from '@libs/API/types'; import HttpUtils from '@libs/HttpUtils'; import PushNotification from '@libs/Notification/PushNotification'; // This lib needs to be imported, but it has nothing to export since all it contains is an Onyx connection @@ -29,7 +30,7 @@ OnyxUpdateManager(); beforeEach(() => Onyx.clear().then(waitForBatchedUpdates)); describe('Session', () => { - test('Authenticate is called with saved credentials when a session expires', () => { + test('Authenticate is called with saved credentials when a session expires', async () => { // Given a test user and set of authToken with subscriptions to session and credentials const TEST_USER_LOGIN = 'test@testguy.com'; const TEST_USER_ACCOUNT_ID = 1; @@ -49,98 +50,100 @@ describe('Session', () => { }); // When we sign in with the test user - return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, 'Password1', TEST_INITIAL_AUTH_TOKEN) - .then(waitForBatchedUpdates) - .then(() => { - // Then our re-authentication credentials should be generated and our session data - // have the correct information + initial authToken. - expect(credentials?.login).toBe(TEST_USER_LOGIN); - expect(credentials?.autoGeneratedLogin).not.toBeUndefined(); - expect(credentials?.autoGeneratedPassword).not.toBeUndefined(); - expect(session?.authToken).toBe(TEST_INITIAL_AUTH_TOKEN); - expect(session?.accountID).toBe(TEST_USER_ACCOUNT_ID); - expect(session?.email).toBe(TEST_USER_LOGIN); - - // At this point we have an authToken. To simulate it expiring we'll just make another - // request and mock the response so it returns 407. Once this happens we should attempt - // to Re-Authenticate with the stored credentials. Our next call will be to Authenticate - // so we will mock that response with a new authToken and then verify that Onyx has our - // data. - (HttpUtils.xhr as jest.MockedFunction) - - // This will make the call to OpenApp below return with an expired session code - .mockImplementationOnce(() => - Promise.resolve({ - jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED, - }), - ) - - // The next call should be Authenticate since we are reauthenticating - .mockImplementationOnce(() => - Promise.resolve({ - jsonCode: CONST.JSON_CODE.SUCCESS, - accountID: TEST_USER_ACCOUNT_ID, - authToken: TEST_REFRESHED_AUTH_TOKEN, - email: TEST_USER_LOGIN, - }), - ); - - // When we attempt to fetch the initial app data via the API - App.confirmReadyToOpenApp(); - App.openApp(); - return waitForBatchedUpdates(); - }) - .then(() => { - // Then it should fail and reauthenticate the user adding the new authToken to the session - // data in Onyx - expect(session?.authToken).toBe(TEST_REFRESHED_AUTH_TOKEN); - }); + await TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, 'Password1', TEST_INITIAL_AUTH_TOKEN); + await waitForBatchedUpdates(); + + // Then our re-authentication credentials should be generated and our session data + // have the correct information + initial authToken. + expect(credentials?.login).toBe(TEST_USER_LOGIN); + expect(credentials?.autoGeneratedLogin).not.toBeUndefined(); + expect(credentials?.autoGeneratedPassword).not.toBeUndefined(); + expect(session?.authToken).toBe(TEST_INITIAL_AUTH_TOKEN); + expect(session?.accountID).toBe(TEST_USER_ACCOUNT_ID); + expect(session?.email).toBe(TEST_USER_LOGIN); + + // At this point we have an authToken. To simulate it expiring we'll just make another + // request and mock the response so it returns 407. Once this happens we should attempt + // to Re-Authenticate with the stored credentials. Our next call will be to Authenticate + // so we will mock that response with a new authToken and then verify that Onyx has our + // data. + (HttpUtils.xhr as jest.MockedFunction) + + // This will make the call to OpenApp below return with an expired session code + .mockImplementationOnce(() => + Promise.resolve({ + jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED, + }), + ) + + // The next call should be Authenticate since we are reauthenticating + .mockImplementationOnce(() => + Promise.resolve({ + jsonCode: CONST.JSON_CODE.SUCCESS, + accountID: TEST_USER_ACCOUNT_ID, + authToken: TEST_REFRESHED_AUTH_TOKEN, + email: TEST_USER_LOGIN, + }), + ); + + // When we attempt to fetch the initial app data via the API + App.confirmReadyToOpenApp(); + App.openApp(); + await waitForBatchedUpdates(); + + // Then it should fail and reauthenticate the user adding the new authToken to the session + // data in Onyx + expect(session?.authToken).toBe(TEST_REFRESHED_AUTH_TOKEN); }); - test('Push notifications are subscribed after signing in', () => - TestHelper.signInWithTestUser() - .then(waitForBatchedUpdates) - .then(() => expect(PushNotification.register).toBeCalled())); - - test('Push notifications are unsubscribed after signing out', () => - TestHelper.signInWithTestUser() - .then(TestHelper.signOutTestUser) - .then(() => expect(PushNotification.deregister).toBeCalled())); - - test('ReconnectApp should push request to the queue', () => { - return TestHelper.signInWithTestUser() - .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: true})) - .then(() => { - App.confirmReadyToOpenApp(); - App.reconnectApp(); - }) - .then(waitForBatchedUpdates) - .then(() => { - expect(PersistedRequests.getAll().length).toBe(1); - Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); - }) - .then(waitForBatchedUpdates) - .then(() => { - expect(PersistedRequests.getAll().length).toBe(0); - }); + test('Push notifications are subscribed after signing in', async () => { + await TestHelper.signInWithTestUser(); + await waitForBatchedUpdates(); + expect(PushNotification.register).toBeCalled(); + }); + + test('Push notifications are unsubscribed after signing out', async () => { + await TestHelper.signInWithTestUser(); + await TestHelper.signOutTestUser(); + expect(PushNotification.deregister).toBeCalled(); + }); + + test('ReconnectApp should push request to the queue', async () => { + await TestHelper.signInWithTestUser(); + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); + + App.confirmReadyToOpenApp(); + App.reconnectApp(); + + await waitForBatchedUpdates(); + + expect(PersistedRequests.getAll().length).toBe(1); + expect(PersistedRequests.getAll()[0].command).toBe(WRITE_COMMANDS.RECONNECT_APP); + + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); + + await waitForBatchedUpdates(); + + expect(PersistedRequests.getAll().length).toBe(0); }); - test('ReconnectApp should replace same requests from the queue', () => { - return TestHelper.signInWithTestUser() - .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: true})) - .then(() => { - App.confirmReadyToOpenApp(); - App.reconnectApp(); - App.reconnectApp(); - App.reconnectApp(); - App.reconnectApp(); - }) - .then(waitForBatchedUpdates) - .then(() => { - expect(PersistedRequests.getAll().length).toBe(1); - }) - .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false})) - .then(() => { - expect(PersistedRequests.getAll().length).toBe(0); - }); + + test('ReconnectApp should replace same requests from the queue', async () => { + await TestHelper.signInWithTestUser(); + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); + + App.confirmReadyToOpenApp(); + App.reconnectApp(); + App.reconnectApp(); + App.reconnectApp(); + App.reconnectApp(); + + await waitForBatchedUpdates(); + + expect(PersistedRequests.getAll().length).toBe(1); + expect(PersistedRequests.getAll()[0].command).toBe(WRITE_COMMANDS.RECONNECT_APP); + + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); + + expect(PersistedRequests.getAll().length).toBe(0); }); }); From b6b893be8635b1b8ec27770f3022eaa5a215179c Mon Sep 17 00:00:00 2001 From: Eduardo Date: Tue, 17 Sep 2024 15:30:38 +0200 Subject: [PATCH 020/151] Fixed some deprecated eslints --- src/libs/ReportActionsUtils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 3f9436d1db2e..3dd6925614f2 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -213,9 +213,9 @@ function isActionOfType( function getOriginalMessage(reportAction: OnyxInputOrEntry>): OriginalMessage | undefined { if (!Array.isArray(reportAction?.message)) { - return reportAction?.message ?? reportAction?.originalMessage; + return reportAction?.message ?? getOriginalMessage(reportAction); } - return reportAction.originalMessage; + return getOriginalMessage(reportAction); } function isExportIntegrationAction(reportAction: OnyxInputOrEntry): boolean { @@ -591,7 +591,7 @@ function isReportActionDeprecated(reportAction: OnyxEntry, key: st // HACK ALERT: We're temporarily filtering out any reportActions keyed by sequenceNumber // to prevent bugs during the migration from sequenceNumber -> reportActionID - if (String(reportAction.sequenceNumber) === key) { + if (String(reportAction.reportActionID) === key) { Log.info('Front-end filtered out reportAction keyed by sequenceNumber!', false, reportAction); return true; } @@ -1696,7 +1696,7 @@ function isCardIssuedAction(reportAction: OnyxEntry) { } function getCardIssuedMessage(reportAction: OnyxEntry, shouldRenderHTML = false) { - const assigneeAccountID = (reportAction?.originalMessage as IssueNewCardOriginalMessage)?.assigneeAccountID; + const assigneeAccountID = (getOriginalMessage(reportAction) as IssueNewCardOriginalMessage)?.assigneeAccountID ?? -1; const assigneeDetails = PersonalDetailsUtils.getPersonalDetailsByIDs([assigneeAccountID], currentUserAccountID ?? -1)[0]; const assignee = shouldRenderHTML ? `` : assigneeDetails?.firstName ?? assigneeDetails.login ?? ''; From 0acddecd37b3ff86ce0175a6fa00be9cc838eabe Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Tue, 17 Sep 2024 15:36:26 +0200 Subject: [PATCH 021/151] add SingleIconListItem --- .../Search/SingleIconListItem.tsx | 75 +++++++++++++++++++ src/components/SelectionList/types.ts | 8 +- src/components/TextWithTooltip/types.ts | 2 +- src/styles/index.ts | 7 ++ 4 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 src/components/SelectionList/Search/SingleIconListItem.tsx diff --git a/src/components/SelectionList/Search/SingleIconListItem.tsx b/src/components/SelectionList/Search/SingleIconListItem.tsx new file mode 100644 index 000000000000..e67ca6db39a1 --- /dev/null +++ b/src/components/SelectionList/Search/SingleIconListItem.tsx @@ -0,0 +1,75 @@ +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 ListItemWithSingleIcon = {singleIcon: IconAsset} & ListItem; + +type SingleIconListItemProps = { + item: TItem; + isFocused?: boolean; + showTooltip?: boolean; + onSelectRow: (item: TItem) => void; + onFocus?: () => void; +}; + +function SingleIconListItem({item, isFocused, showTooltip, onSelectRow, onFocus}: SingleIconListItemProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + + return ( + + <> + {!!item.singleIcon && ( + + )} + + + {!!item.alternateText && ( + + )} + + {!!item.rightElement && item.rightElement} + + + ); +} + +SingleIconListItem.displayName = 'SingleIconListItem'; + +export default SingleIconListItem; +export type {ListItemWithSingleIcon, SingleIconListItemProps}; diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 7bdbb03f2101..ae5101ce8406 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -1,5 +1,6 @@ import type {MutableRefObject, ReactElement, ReactNode} from 'react'; import type {GestureResponderEvent, InputModeOptions, LayoutChangeEvent, SectionListData, StyleProp, TextInput, TextStyle, ViewStyle} from 'react-native'; +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'; @@ -13,6 +14,7 @@ import type ChatListItem from './ChatListItem'; import type InviteMemberListItem from './InviteMemberListItem'; import type RadioListItem from './RadioListItem'; import type ReportListItem from './Search/ReportListItem'; +import type SingleIconListItem from './Search/SingleIconListItem'; import type TransactionListItem from './Search/TransactionListItem'; import type TableListItem from './TableListItem'; import type UserListItem from './UserListItem'; @@ -30,7 +32,7 @@ type CommonListItemProps = { isDisabled?: boolean | null; /** Whether this item should show Tooltip */ - showTooltip: boolean; + showTooltip?: boolean; /** Whether to use the Checkbox (multiple selection) instead of the Checkmark (single selection) */ canSelectMultiple?: boolean; @@ -305,7 +307,9 @@ type ValidListItem = | typeof InviteMemberListItem | typeof TransactionListItem | typeof ReportListItem - | typeof ChatListItem; + | typeof ChatListItem + | typeof SingleIconListItem + | typeof SearchRouterItem; type Section = { /** Title of the section */ 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/styles/index.ts b/src/styles/index.ts index 44362ab75ded..752026d1c935 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -4650,6 +4650,13 @@ const styles = (theme: ThemeColors) => borderRadius: 8, }, + singleIconListItemStyle: { + alignItems: 'center', + flexDirection: 'row', + paddingHorizontal: 16, + paddingVertical: 16, + }, + selectionListStickyHeader: { backgroundColor: theme.appBG, }, From 682a8ec3422d90896a4dea97d3ef99d5fe3ded18 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Tue, 17 Sep 2024 15:38:34 +0200 Subject: [PATCH 022/151] add SearchRouterList --- .../Search/SearchRouter/SearchRouter.tsx | 40 ++++++++++++++- .../Search/SearchRouter/SearchRouterList.tsx | 49 +++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 src/components/Search/SearchRouter/SearchRouterList.tsx diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 73a86f95719e..665b40f2b641 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -1,18 +1,23 @@ import debounce from 'lodash/debounce'; -import React, {useCallback, useState} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; import Modal from '@components/Modal'; +import {useOptionsList} from '@components/OptionListContextProvider'; import type {SearchQueryJSON} from '@components/Search/types'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as SearchUtils from '@libs/SearchUtils'; import Navigation from '@navigation/Navigation'; 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 = 200; @@ -76,6 +81,35 @@ function SearchRouter() { const modalType = isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.CENTERED : CONST.MODAL.MODAL_TYPE.POPOVER; const isFullWidth = isSmallScreenWidth; + const mockedRecentSearches = [ + { + name: 'Big agree', + query: '123', + }, + { + name: 'GIF', + query: '123', + }, + { + name: 'Greg', + query: '123', + }, + ]; + + const {options, areOptionsInitialized} = useOptionsList({ + shouldInitialize: true, + }); + + const [betas] = useOnyx(`${ONYXKEYS.BETAS}`); + + const searchOptions = useMemo(() => { + if (!areOptionsInitialized) { + return []; + } + const optionList = OptionsListUtils.getSearchOptions(options, '', betas ?? []); + return optionList.recentReports.slice(0, 5); + }, [areOptionsInitialized, betas, options]); + return ( + diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx new file mode 100644 index 000000000000..45bc6eeab742 --- /dev/null +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import * as Expensicons from '@components/Icon/Expensicons'; +import SelectionList from '@components/SelectionList'; +import SingleIconListItem from '@components/SelectionList/Search/SingleIconListItem'; +import type {ListItemWithSingleIcon, SingleIconListItemProps} from '@components/SelectionList/Search/SingleIconListItem'; +import type {UserListItemProps} from '@components/SelectionList/types'; +import UserListItem from '@components/SelectionList/UserListItem'; +import type {OptionData} from '@libs/ReportUtils'; + +type RecentSearchObject = { + name: string; + query: string; +}; + +type SearchRouterListProps = { + recentSearches: RecentSearchObject[]; + recentReports: OptionData[]; +}; + +function SearchRouterItem(props: UserListItemProps | SingleIconListItemProps) { + if ('item' in props && props.item.reportID) { + // eslint-disable-next-line react/jsx-props-no-spreading + return )} />; + } + // eslint-disable-next-line react/jsx-props-no-spreading + return )} />; +} + +function SearchRouterList({recentSearches, recentReports}: SearchRouterListProps) { + const recentSearchesData = recentSearches.map(({name, query}) => ({ + text: name, + singleIcon: Expensicons.History, + query, + })); + + return ( + + sections={[ + {title: 'Recent searches', data: recentSearchesData}, + {title: 'Recent chats', data: recentReports}, + ]} + onSelectRow={() => {}} + ListItem={SearchRouterItem} + /> + ); +} + +export default SearchRouterList; +export {SearchRouterItem}; From 1fcd87d327be21c085ab1194d5d24164e5fcedef Mon Sep 17 00:00:00 2001 From: Eduardo Date: Tue, 17 Sep 2024 17:15:15 +0200 Subject: [PATCH 023/151] Rolling back change --- src/libs/ReportActionsUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 3dd6925614f2..3d0f3e1989a6 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -213,9 +213,9 @@ function isActionOfType( function getOriginalMessage(reportAction: OnyxInputOrEntry>): OriginalMessage | undefined { if (!Array.isArray(reportAction?.message)) { - return reportAction?.message ?? getOriginalMessage(reportAction); + return reportAction?.message ?? reportAction?.originalMessage; } - return getOriginalMessage(reportAction); + return reportAction?.originalMessage; } function isExportIntegrationAction(reportAction: OnyxInputOrEntry): boolean { From f7310b12002725990f130ebd597f41d13bf1d7aa Mon Sep 17 00:00:00 2001 From: daledah Date: Wed, 18 Sep 2024 00:28:27 +0700 Subject: [PATCH 024/151] fix: add Verify account page --- src/ROUTES.ts | 1 + src/SCREENS.ts | 1 + .../ModalStackNavigators/index.tsx | 1 + .../CENTRAL_PANE_TO_RHP_MAPPING.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 4 + src/pages/AddPersonalBankAccountPage.tsx | 104 ++++++------------ .../AddBankAccount/SetupMethod.tsx | 11 +- .../settings/Wallet/PaymentMethodList.tsx | 16 ++- .../settings/Wallet/VerifyAccountPage.tsx | 73 ++++++++++++ 9 files changed, 131 insertions(+), 81 deletions(-) create mode 100644 src/pages/settings/Wallet/VerifyAccountPage.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 27504998c49c..6c3491117b46 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -151,6 +151,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, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 8168afba89ab..05bccc82079e 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/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index b41b58530a6b..5e125f30c9ce 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -211,6 +211,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Wallet/TransferBalancePage').default, [SCREENS.SETTINGS.WALLET.CHOOSE_TRANSFER_ACCOUNT]: () => require('../../../../pages/settings/Wallet/ChooseTransferAccountPage').default, [SCREENS.SETTINGS.WALLET.ENABLE_PAYMENTS]: () => require('../../../../pages/EnablePayments/EnablePayments').default, + [SCREENS.SETTINGS.WALLET.VERIFY_ACCOUNT]: () => require('../../../../pages/settings/Wallet/VerifyAccountPage').default, [SCREENS.SETTINGS.ADD_DEBIT_CARD]: () => require('../../../../pages/settings/Wallet/AddDebitCardPage').default, [SCREENS.SETTINGS.ADD_BANK_ACCOUNT]: () => require('../../../../pages/AddPersonalBankAccountPage').default, [SCREENS.SETTINGS.PROFILE.STATUS]: () => require('../../../../pages/settings/Profile/CustomStatus/StatusPage').default, diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts index 609162bedd13..495554dae58f 100755 --- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts @@ -37,6 +37,7 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = SCREENS.SETTINGS.WALLET.CARD_ACTIVATE, SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD, SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS, + SCREENS.SETTINGS.WALLET.VERIFY_ACCOUNT, ], [SCREENS.SETTINGS.SECURITY]: [ SCREENS.SETTINGS.TWO_FACTOR_AUTH, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 2ca2db10a1a7..44db290fc3e5 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -161,6 +161,10 @@ const config: LinkingOptions['config'] = { path: ROUTES.SETTINGS_CLOSE, exact: true, }, + [SCREENS.SETTINGS.WALLET.VERIFY_ACCOUNT]: { + path: ROUTES.SETTINGS_WALLET_VERIFY_ACCOUNT.route, + exact: true, + }, [SCREENS.SETTINGS.WALLET.DOMAIN_CARD]: { path: ROUTES.SETTINGS_WALLET_DOMAINCARD.route, exact: true, diff --git a/src/pages/AddPersonalBankAccountPage.tsx b/src/pages/AddPersonalBankAccountPage.tsx index 4f0edb630f59..04cce885bd07 100644 --- a/src/pages/AddPersonalBankAccountPage.tsx +++ b/src/pages/AddPersonalBankAccountPage.tsx @@ -8,17 +8,13 @@ import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; -import ValidateCodeActionModal from '@components/ValidateCodeActionModal'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ErrorUtils from '@libs/ErrorUtils'; import getPlaidOAuthReceivedRedirectURI from '@libs/getPlaidOAuthReceivedRedirectURI'; import Navigation from '@libs/Navigation/Navigation'; import * as BankAccounts from '@userActions/BankAccounts'; import * as PaymentMethods from '@userActions/PaymentMethods'; -import * as User from '@userActions/User'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; import type {PersonalBankAccount, PlaidData} from '@src/types/onyx'; @@ -35,23 +31,8 @@ function AddPersonalBankAccountPage({personalBankAccount, plaidData}: AddPersona const {translate} = useLocalize(); const [selectedPlaidAccountId, setSelectedPlaidAccountId] = useState(''); const [isUserValidated] = useOnyx(ONYXKEYS.USER, {selector: (user) => !!user?.validated}); - const [isValidateCodeActionModalVisible, setIsValidateCodeActionModalVisible] = useState(!isUserValidated); const shouldShowSuccess = personalBankAccount?.shouldShowSuccess ?? false; - const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST); - const [account] = useOnyx(ONYXKEYS.ACCOUNT); - const primaryLogin = account?.primaryLogin; - const loginData = loginList?.[primaryLogin ?? '']; - const validateLoginError = ErrorUtils.getEarliestErrorField(loginData, 'validateLogin'); - - const handleSubmitForm = useCallback( - (submitCode: string) => { - User.validateSecondaryLogin(loginList, primaryLogin ?? '', submitCode); - Navigation.navigate(ROUTES.SETTINGS_ADD_BANK_ACCOUNT); - }, - [loginList, primaryLogin], - ); - const submitBankAccountForm = useCallback(() => { const bankAccounts = plaidData?.bankAccounts ?? []; const selectedPlaidBankAccount = bankAccounts.find((bankAccount) => bankAccount.plaidAccountID === selectedPlaidAccountId); @@ -86,58 +67,43 @@ function AddPersonalBankAccountPage({personalBankAccount, plaidData}: AddPersona shouldShowOfflineIndicator={false} testID={AddPersonalBankAccountPage.displayName} > - {isUserValidated && ( - - + + {shouldShowSuccess ? ( + exitFlow(true)} /> - {shouldShowSuccess ? ( - exitFlow(true)} + ) : ( + 0} + submitButtonText={translate('common.saveAndContinue')} + scrollContextEnabled + onSubmit={submitBankAccountForm} + validate={BankAccounts.validatePlaidSelection} + style={[styles.mh5, styles.flex1]} + > + Navigation.goBack()} + receivedRedirectURI={getPlaidOAuthReceivedRedirectURI()} + selectedPlaidAccountID={selectedPlaidAccountId} /> - ) : ( - 0} - submitButtonText={translate('common.saveAndContinue')} - scrollContextEnabled - onSubmit={submitBankAccountForm} - validate={BankAccounts.validatePlaidSelection} - style={[styles.mh5, styles.flex1]} - > - Navigation.goBack()} - receivedRedirectURI={getPlaidOAuthReceivedRedirectURI()} - selectedPlaidAccountID={selectedPlaidAccountId} - /> - - )} - - )} - { - setIsValidateCodeActionModalVisible(false); - exitFlow(); - }} - handleSubmitForm={handleSubmitForm} - clearError={() => {}} - /> + + )} + ); } diff --git a/src/pages/EnablePayments/AddBankAccount/SetupMethod.tsx b/src/pages/EnablePayments/AddBankAccount/SetupMethod.tsx index ed06d1aa1ed6..1f477998f4f4 100644 --- a/src/pages/EnablePayments/AddBankAccount/SetupMethod.tsx +++ b/src/pages/EnablePayments/AddBankAccount/SetupMethod.tsx @@ -15,12 +15,8 @@ import * as BankAccounts from '@userActions/BankAccounts'; import * as Link from '@userActions/Link'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {User} from '@src/types/onyx'; type SetupMethodOnyxProps = { - /** The user's data */ - user: OnyxEntry; - /** Whether Plaid is disabled */ isPlaidDisabled: OnyxEntry; }; @@ -29,7 +25,7 @@ type SetupMethodProps = SetupMethodOnyxProps; const plaidDesktopMessage = getPlaidDesktopMessage(); -function SetupMethod({isPlaidDisabled, user}: SetupMethodProps) { +function SetupMethod({isPlaidDisabled}: SetupMethodProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -52,7 +48,7 @@ function SetupMethod({isPlaidDisabled, user}: SetupMethodProps) { icon={Expensicons.Bank} text={translate('bankAccount.addBankAccount')} onPress={() => BankAccounts.openPersonalBankAccountSetupWithPlaid()} - isDisabled={!!isPlaidDisabled || !user?.validated} + isDisabled={!!isPlaidDisabled} style={[styles.mt4, styles.mb2]} iconStyles={styles.buttonCTAIcon} shouldShowRightIcon @@ -70,7 +66,4 @@ export default withOnyx({ isPlaidDisabled: { key: ONYXKEYS.IS_PLAID_DISABLED, }, - user: { - key: ONYXKEYS.USER, - }, })(SetupMethod); diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index 790157f5c3eb..56b8abf4c062 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -4,7 +4,7 @@ import React, {useCallback, useMemo} from 'react'; import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; import {FlatList, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import type {SvgProps} from 'react-native-svg/lib/typescript/ReactNativeSVG'; import type {ValueOf} from 'type-fest'; import type {RenderSuggestionMenuItemProps} from '@components/AutoCompleteSuggestions/types'; @@ -199,6 +199,8 @@ function PaymentMethodList({ const {translate} = useLocalize(); const {isOffline} = useNetwork(); + const [isUserValidated] = useOnyx(ONYXKEYS.USER, {selector: (user) => !!user?.validated}); + const getDescriptionForPolicyDomainCard = (domainName: string): string => { // A domain name containing a policyID indicates that this is a workspace feed const policyID = domainName.match(CONST.REGEX.EXPENSIFY_POLICY_DOMAIN_NAME)?.[1]; @@ -322,17 +324,25 @@ 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( () => ( ), - [onPress, translate, styles.paymentMethod, listItemStyle, buttonRef], + [translate, styles.paymentMethod, listItemStyle, buttonRef, onPressItem], ); /** diff --git a/src/pages/settings/Wallet/VerifyAccountPage.tsx b/src/pages/settings/Wallet/VerifyAccountPage.tsx new file mode 100644 index 000000000000..03ac87af8bc0 --- /dev/null +++ b/src/pages/settings/Wallet/VerifyAccountPage.tsx @@ -0,0 +1,73 @@ +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 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 [pendingContactAction] = useOnyx(ONYXKEYS.PENDING_CONTACT_ACTION); + const contactMethod = account?.primaryLogin ?? ''; + const themeStyles = useThemeStyles(); + const {translate} = useLocalize(); + const loginInputRef = useRef(null); + const loginData = loginList?.[pendingContactAction?.contactMethod ?? contactMethod]; + const validateLoginError = ErrorUtils.getEarliestErrorField(loginData, 'validateLogin'); + + const [validateCodeAction] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE); + + const navigateBackTo = route?.params?.backTo ?? ROUTES.SETTINGS_WALLET; + + useEffect(() => () => User.clearUnvalidatedNewContactMethodAction(), []); + + const handleSubmitForm = useCallback( + (submitCode: string) => { + User.validateSecondaryLogin(loginList, contactMethod ?? '', submitCode); + Navigation.navigate(navigateBackTo); + }, + [loginList, contactMethod, navigateBackTo], + ); + + return ( + loginInputRef.current?.focus()} + includeSafeAreaPaddingBottom={false} + shouldEnableMaxHeight + testID={VerifyAccountPage.displayName} + > + Navigation.goBack(ROUTES.SETTINGS_ADD_BANK_ACCOUNT)} + /> + + {translate('contacts.featureRequiresValidate')} + {}} + /> + + + ); +} + +VerifyAccountPage.displayName = 'VerifyAccountPage'; + +export default VerifyAccountPage; From 3b21e417a06e8aa863f75fb2c29d13191077fa13 Mon Sep 17 00:00:00 2001 From: Eduardo Date: Tue, 17 Sep 2024 21:45:17 +0200 Subject: [PATCH 025/151] disable deprecation fields --- .eslintrc.js | 3 ++- src/libs/ReportActionsUtils.ts | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index cb1219533278..c9d11b97d7dc 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -108,7 +108,7 @@ module.exports = { 'plugin:you-dont-need-lodash-underscore/all', 'plugin:prettier/recommended', ], - plugins: ['@typescript-eslint', 'jsdoc', 'you-dont-need-lodash-underscore', 'react-native-a11y', 'react', 'testing-library', 'eslint-plugin-react-compiler'], + plugins: ['@typescript-eslint', 'jsdoc', 'you-dont-need-lodash-underscore', 'react-native-a11y', 'react', 'testing-library', 'eslint-plugin-react-compiler', 'deprecation'], ignorePatterns: ['lib/**'], parser: '@typescript-eslint/parser', parserOptions: { @@ -254,6 +254,7 @@ module.exports = { }, }, ], + 'deprecation/deprecation': 'off', }, overrides: [ diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 3d0f3e1989a6..1bed7fcac480 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -213,8 +213,10 @@ function isActionOfType( function getOriginalMessage(reportAction: OnyxInputOrEntry>): OriginalMessage | undefined { if (!Array.isArray(reportAction?.message)) { + // eslint-disable-next-line deprecation/deprecation return reportAction?.message ?? reportAction?.originalMessage; } + // eslint-disable-next-line deprecation/deprecation return reportAction?.originalMessage; } @@ -1752,6 +1754,7 @@ export { getNumberOfMoneyRequests, getOneTransactionThreadReportID, getOriginalMessage, + // eslint-disable-next-line deprecation/deprecation getParentReportAction, getRemovedFromApprovalChainMessage, getReportAction, From a5b24fee747c3a67b7b0ef164c8a253656abc14f Mon Sep 17 00:00:00 2001 From: daledah Date: Wed, 18 Sep 2024 15:51:02 +0700 Subject: [PATCH 026/151] fix: navigate when add bank account --- src/pages/AddPersonalBankAccountPage.tsx | 5 +++-- src/pages/settings/Wallet/VerifyAccountPage.tsx | 11 +++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/pages/AddPersonalBankAccountPage.tsx b/src/pages/AddPersonalBankAccountPage.tsx index 04cce885bd07..1aef2f3be99a 100644 --- a/src/pages/AddPersonalBankAccountPage.tsx +++ b/src/pages/AddPersonalBankAccountPage.tsx @@ -15,6 +15,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as BankAccounts from '@userActions/BankAccounts'; import * as PaymentMethods from '@userActions/PaymentMethods'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; import type {PersonalBankAccount, PlaidData} from '@src/types/onyx'; @@ -52,7 +53,7 @@ function AddPersonalBankAccountPage({personalBankAccount, plaidData}: AddPersona } else if (shouldContinue && onSuccessFallbackRoute) { PaymentMethods.continueSetup(onSuccessFallbackRoute); } else { - Navigation.goBack(); + Navigation.navigate(ROUTES.SETTINGS_WALLET); } }, [personalBankAccount], @@ -97,7 +98,7 @@ function AddPersonalBankAccountPage({personalBankAccount, plaidData}: AddPersona text={translate('walletPage.chooseAccountBody')} plaidData={plaidData} isDisplayedInWalletFlow - onExitPlaid={() => Navigation.goBack()} + onExitPlaid={() => Navigation.navigate(ROUTES.SETTINGS_WALLET)} receivedRedirectURI={getPlaidOAuthReceivedRedirectURI()} selectedPlaidAccountID={selectedPlaidAccountId} /> diff --git a/src/pages/settings/Wallet/VerifyAccountPage.tsx b/src/pages/settings/Wallet/VerifyAccountPage.tsx index 03ac87af8bc0..8d6e2246d935 100644 --- a/src/pages/settings/Wallet/VerifyAccountPage.tsx +++ b/src/pages/settings/Wallet/VerifyAccountPage.tsx @@ -29,6 +29,7 @@ function VerifyAccountPage({route}: VerifyAccountPageProps) { const loginInputRef = useRef(null); const loginData = loginList?.[pendingContactAction?.contactMethod ?? contactMethod]; const validateLoginError = ErrorUtils.getEarliestErrorField(loginData, 'validateLogin'); + const [isUserValidated] = useOnyx(ONYXKEYS.USER, {selector: (user) => !!user?.validated}); const [validateCodeAction] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE); @@ -39,11 +40,17 @@ function VerifyAccountPage({route}: VerifyAccountPageProps) { const handleSubmitForm = useCallback( (submitCode: string) => { User.validateSecondaryLogin(loginList, contactMethod ?? '', submitCode); - Navigation.navigate(navigateBackTo); }, - [loginList, contactMethod, navigateBackTo], + [loginList, contactMethod], ); + useEffect(() => { + if (!isUserValidated) { + return; + } + Navigation.navigate(navigateBackTo); + }, [isUserValidated, navigateBackTo]); + return ( loginInputRef.current?.focus()} From 56327f6697f21c8ca20f8ffed826b363b7be058d Mon Sep 17 00:00:00 2001 From: daledah Date: Wed, 18 Sep 2024 16:19:53 +0700 Subject: [PATCH 027/151] fix: request code on open page --- .../EnablePayments/AddBankAccount/SetupMethod.tsx | 12 ++++++++++-- src/pages/settings/Wallet/VerifyAccountPage.tsx | 9 ++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/pages/EnablePayments/AddBankAccount/SetupMethod.tsx b/src/pages/EnablePayments/AddBankAccount/SetupMethod.tsx index 1f477998f4f4..ca1c41925369 100644 --- a/src/pages/EnablePayments/AddBankAccount/SetupMethod.tsx +++ b/src/pages/EnablePayments/AddBankAccount/SetupMethod.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import Button from '@components/Button'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -11,6 +11,7 @@ import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import getPlaidDesktopMessage from '@libs/getPlaidDesktopMessage'; +import Navigation from '@libs/Navigation/Navigation'; import * as BankAccounts from '@userActions/BankAccounts'; import * as Link from '@userActions/Link'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -28,6 +29,7 @@ const plaidDesktopMessage = getPlaidDesktopMessage(); function SetupMethod({isPlaidDisabled}: SetupMethodProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const [isUserValidated] = useOnyx(ONYXKEYS.USER, {selector: (user) => !!user?.validated}); return ( @@ -47,7 +49,13 @@ function SetupMethod({isPlaidDisabled}: SetupMethodProps) { - )} - {/** + + {isHidden ? translate('moderation.revealMessage') : translate('moderation.hideMessage')} + + + )} + {/** These are the actionable buttons that appear at the bottom of a Concierge message for example: Invite a user mentioned but not a member of the room https://github.com/Expensify/App/issues/32741 */} - {actionableItemButtons.length > 0 && ( - - )} - - ) : ( - - )} - - + {actionableItemButtons.length > 0 && ( + + )} + + ) : ( + + )} + + + ); } const numberOfThreadReplies = action.childVisibleActionCount ?? 0; @@ -895,100 +897,98 @@ function ReportActionItem({ const displayNamesWithTooltips = isWhisper ? ReportUtils.getDisplayNamesWithTooltips(whisperedToPersonalDetails, isMultipleParticipant) : []; return ( - - shouldUseNarrowLayout && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} - onPressOut={() => ControlSelection.unblock()} - onSecondaryInteraction={showPopover} - preventDefaultContextMenu={draftMessage === undefined && !hasErrors} - withoutFocusOnSecondaryInteraction - accessibilityLabel={translate('accessibilityHints.chatMessage')} - accessible + shouldUseNarrowLayout && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + onPressOut={() => ControlSelection.unblock()} + onSecondaryInteraction={showPopover} + preventDefaultContextMenu={draftMessage === undefined && !hasErrors} + withoutFocusOnSecondaryInteraction + accessibilityLabel={translate('accessibilityHints.chatMessage')} + accessible + > + - - {(hovered) => ( - - {shouldDisplayNewMarker && (!shouldUseThreadDividerLine || !isFirstVisibleReportAction) && } - {shouldDisplayContextMenu && ( - + {(hovered) => ( + + {shouldDisplayNewMarker && (!shouldUseThreadDividerLine || !isFirstVisibleReportAction) && } + {shouldDisplayContextMenu && ( + + )} + - { - const transactionID = ReportActionsUtils.isMoneyRequestAction(action) ? ReportActionsUtils.getOriginalMessage(action)?.IOUTransactionID : undefined; - if (transactionID) { - Transaction.clearError(transactionID); - } - ReportActions.clearAllRelatedReportActionErrors(reportID, action); - }} - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - pendingAction={ - draftMessage !== undefined ? undefined : action.pendingAction ?? (action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : undefined) + > + { + const transactionID = ReportActionsUtils.isMoneyRequestAction(action) ? ReportActionsUtils.getOriginalMessage(action)?.IOUTransactionID : undefined; + if (transactionID) { + Transaction.clearError(transactionID); } - shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(action, reportID)} - errors={linkedTransactionRouteError ?? ErrorUtils.getLatestErrorMessageField(action as ErrorUtils.OnyxDataWithErrors)} - errorRowStyles={[styles.ml10, styles.mr2]} - needsOffscreenAlphaCompositing={ReportActionsUtils.isMoneyRequestAction(action)} - shouldDisableStrikeThrough - > - {isWhisper && ( - - - - - - {translate('reportActionContextMenu.onlyVisible')} -   - - + {isWhisper && ( + + + - )} - {renderReportActionItem(!!hovered || !!isReportActionLinked, isWhisper, hasErrors)} - - + + {translate('reportActionContextMenu.onlyVisible')} +   + + + + )} + {renderReportActionItem(!!hovered || !!isReportActionLinked, isWhisper, hasErrors)} + - )} - - - - - - + + )} + + + + + ); } From e074dfdefae8e2fd718a1788898152e0f97b9a50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Wed, 2 Oct 2024 12:47:39 +0200 Subject: [PATCH 117/151] update dev instructions --- tests/e2e/README.md | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 1f590a474ad5..a47d9d8e8631 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -41,18 +41,16 @@ npm run android 3. We need to modify the app entry to point to the one for the tests. Therefore rename `./index.js` to `./appIndex.js` temporarily. -4. Create a new `./index.js` with the following content: -```js -require('./src/libs/E2E/reactNativeLaunchingTest'); -``` - -5. In `./src/libs/E2E/reactNativeLaunchingTest.ts` change the main app import to the new `./appIndex.js` file: -```diff -- import '../../../index'; -+ import '../../../appIndex'; -``` - -6. You can now run the tests. This command will invoke the test runner: +4. Temporarily add to the `package.json` a `main` field pointing to the e2e entry file: + + ```diff + { + "private": true, ++ "main": "src/libs/E2E/reactNativeEntry.ts" + } + ``` + +5. You can now run the tests. This command will invoke the test runner: ```sh npm run test:e2e:dev From 0877308b05a3aa48055fe910296977afc0b5f51f Mon Sep 17 00:00:00 2001 From: Eduardo Date: Wed, 2 Oct 2024 13:17:40 +0200 Subject: [PATCH 118/151] Fixed some eslint issues --- src/libs/actions/PersistedRequests.ts | 3 ++- tests/actions/SessionTest.ts | 4 ++-- tests/unit/PersistedRequests.ts | 4 ++-- tests/unit/SequentialQueueTest.ts | 8 ++++---- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/libs/actions/PersistedRequests.ts b/src/libs/actions/PersistedRequests.ts index 77d6e3884380..fc14e8c2303b 100644 --- a/src/libs/actions/PersistedRequests.ts +++ b/src/libs/actions/PersistedRequests.ts @@ -13,7 +13,7 @@ Onyx.connect({ persistedRequests = val ?? []; if (ongoingRequest && persistedRequests.length > 0) { - const nextRequestToProcess = persistedRequests[0]; + const nextRequestToProcess = persistedRequests.at(0); // We try to remove the next request from the persistedRequests if it is the same as ongoingRequest // so we don't process it twice. @@ -35,6 +35,7 @@ Onyx.connect({ */ function clear() { ongoingRequest = null; + Onyx.set(ONYXKEYS.PERSISTED_ONGOING_REQUESTS, null); return Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, []); } diff --git a/tests/actions/SessionTest.ts b/tests/actions/SessionTest.ts index e46806bef99e..51dc775da359 100644 --- a/tests/actions/SessionTest.ts +++ b/tests/actions/SessionTest.ts @@ -118,7 +118,7 @@ describe('Session', () => { await waitForBatchedUpdates(); expect(PersistedRequests.getAll().length).toBe(1); - expect(PersistedRequests.getAll()[0].command).toBe(WRITE_COMMANDS.RECONNECT_APP); + expect(PersistedRequests.getAll().at(0)?.command).toBe(WRITE_COMMANDS.RECONNECT_APP); await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); @@ -140,7 +140,7 @@ describe('Session', () => { await waitForBatchedUpdates(); expect(PersistedRequests.getAll().length).toBe(1); - expect(PersistedRequests.getAll()[0].command).toBe(WRITE_COMMANDS.RECONNECT_APP); + expect(PersistedRequests.getAll().at(0)?.command).toBe(WRITE_COMMANDS.RECONNECT_APP); await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); diff --git a/tests/unit/PersistedRequests.ts b/tests/unit/PersistedRequests.ts index 476b3f963951..7d3a7288ed90 100644 --- a/tests/unit/PersistedRequests.ts +++ b/tests/unit/PersistedRequests.ts @@ -57,7 +57,7 @@ describe('PersistedRequests', () => { PersistedRequests.processNextRequest(); return waitForBatchedUpdates().then(() => { expect(PersistedRequests.getAll().length).toBe(1); - expect(PersistedRequests.getAll()[0]).toEqual(request2); + expect(PersistedRequests.getAll().at(0)).toEqual(request2); }); }); @@ -68,7 +68,7 @@ describe('PersistedRequests', () => { failureData: [{key: 'reportMetadata_2', onyxMethod: 'set', value: {}}], }; PersistedRequests.update(0, newRequest); - expect(PersistedRequests.getAll()[0]).toEqual(newRequest); + expect(PersistedRequests.getAll().at(0)).toEqual(newRequest); }); it('update the ongoing request with new data', () => { diff --git a/tests/unit/SequentialQueueTest.ts b/tests/unit/SequentialQueueTest.ts index 8651d7e95e33..4b5c026eb8f4 100644 --- a/tests/unit/SequentialQueueTest.ts +++ b/tests/unit/SequentialQueueTest.ts @@ -57,7 +57,7 @@ describe('SequentialQueue', () => { expect(PersistedRequests.getLength()).toBe(1); // We know there is only one request in the queue, so we can get the first one and verify // that the persisted request is the second one. - const persistedRequest = PersistedRequests.getAll()[0]; + const persistedRequest = PersistedRequests.getAll().at(0); expect(persistedRequest?.data?.accountID).toBe(56789); }); @@ -179,7 +179,7 @@ describe('SequentialQueue', () => { const persistedRequests = PersistedRequests.getAll(); // We know ReconnectApp is at index 1 in the queue, so we can get it to verify // that was replaced by the new request. - expect(persistedRequests[1]?.data?.accountID).toBe(56789); + expect(persistedRequests.at(1)?.data?.accountID).toBe(56789); }); // need to test a rance condition between processing the next request and then pushing a new request with conflict resolver @@ -223,8 +223,8 @@ describe('SequentialQueue', () => { // We know ReconnectApp is at index 9 in the queue, so we can get it to verify // that was replaced by the new request. - expect(persistedRequests[9]?.command).toBe('ReconnectApp-replaced'); - expect(persistedRequests[9]?.data?.accountID).toBe(56789); + expect(persistedRequests.at(9)?.command).toBe('ReconnectApp-replaced'); + expect(persistedRequests.at(9)?.data?.accountID).toBe(56789); }); // I need to test now when moving the request from the queue to the ongoing request the PERSISTED_REQUESTS is decreased and PERSISTED_ONGOING_REQUESTS has the new request From c9198e591712f71241609f748e0303d70967819a Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:36:52 +0200 Subject: [PATCH 119/151] add completePaymentOnboarding into AddPaymentMethodMenu --- src/components/AddPaymentMethodMenu.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/AddPaymentMethodMenu.tsx b/src/components/AddPaymentMethodMenu.tsx index 5621c031f959..f21f3e119fb0 100644 --- a/src/components/AddPaymentMethodMenu.tsx +++ b/src/components/AddPaymentMethodMenu.tsx @@ -15,6 +15,7 @@ import * as Expensicons from './Icon/Expensicons'; import type {PaymentMethod} from './KYCWall/types'; import type BaseModalProps from './Modal/types'; import PopoverMenu from './PopoverMenu'; +import { completePaymentOnboarding } from '@libs/actions/IOU'; type AddPaymentMethodMenuOnyxProps = { /** Session info for the currently logged-in user. */ @@ -80,6 +81,7 @@ function AddPaymentMethodMenu({ return; } + completePaymentOnboarding(CONST.PAYMENT_SELECTED.PBA); onItemSelected(CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT); }, [isPersonalOnlyOption, isVisible, onItemSelected]); @@ -108,7 +110,8 @@ function AddPaymentMethodMenu({ text: translate('common.personalBankAccount'), icon: Expensicons.Bank, onSelected: () => { - onItemSelected(CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT); + completePaymentOnboarding(CONST.PAYMENT_SELECTED.PBA); + onItemSelected(CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT); }, }, ] @@ -118,7 +121,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); + }, }, ] : []), From 53dee66ea2157e86b7e8b1a017eb60d385e820a7 Mon Sep 17 00:00:00 2001 From: Ted Harris Date: Wed, 2 Oct 2024 12:45:37 +0100 Subject: [PATCH 120/151] Create Finland-per-diem.csv Adding this to the guide because I had to do it for Logitech anyway. --- docs/assets/Files/Finland-per-diem.csv | 1071 ++++++++++++++++++++++++ 1 file changed, 1071 insertions(+) create mode 100644 docs/assets/Files/Finland-per-diem.csv 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 From b2842ca9af89ae65716a48ac771a4b225595239e Mon Sep 17 00:00:00 2001 From: Ted Harris Date: Wed, 2 Oct 2024 12:48:04 +0100 Subject: [PATCH 121/151] Update Enable-per-diem-expenses.md --- .../expensify-classic/workspaces/Enable-per-diem-expenses.md | 1 + 1 file changed, 1 insertion(+) 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. From bd074ad4e05bd5d130f2febe4ed06b52b1d1a8d9 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:51:10 +0200 Subject: [PATCH 122/151] fix prettier --- src/components/AddPaymentMethodMenu.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/AddPaymentMethodMenu.tsx b/src/components/AddPaymentMethodMenu.tsx index f21f3e119fb0..50c7d530a3c2 100644 --- a/src/components/AddPaymentMethodMenu.tsx +++ b/src/components/AddPaymentMethodMenu.tsx @@ -4,6 +4,7 @@ import type {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} 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'; @@ -15,7 +16,6 @@ import * as Expensicons from './Icon/Expensicons'; import type {PaymentMethod} from './KYCWall/types'; import type BaseModalProps from './Modal/types'; import PopoverMenu from './PopoverMenu'; -import { completePaymentOnboarding } from '@libs/actions/IOU'; type AddPaymentMethodMenuOnyxProps = { /** Session info for the currently logged-in user. */ @@ -110,8 +110,8 @@ function AddPaymentMethodMenu({ text: translate('common.personalBankAccount'), icon: Expensicons.Bank, onSelected: () => { - completePaymentOnboarding(CONST.PAYMENT_SELECTED.PBA); - onItemSelected(CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT); + completePaymentOnboarding(CONST.PAYMENT_SELECTED.PBA); + onItemSelected(CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT); }, }, ] @@ -122,8 +122,8 @@ function AddPaymentMethodMenu({ text: translate('common.businessBankAccount'), icon: Expensicons.Building, onSelected: () => { - completePaymentOnboarding(CONST.PAYMENT_SELECTED.BBA); - onItemSelected(CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT); + completePaymentOnboarding(CONST.PAYMENT_SELECTED.BBA); + onItemSelected(CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT); }, }, ] From 8016c3c77654b948daa68a7bf8a4d52331faa278 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:57:44 +0200 Subject: [PATCH 123/151] refactor AddPaymentMethodMenu into useOnyx --- src/components/AddPaymentMethodMenu.tsx | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/components/AddPaymentMethodMenu.tsx b/src/components/AddPaymentMethodMenu.tsx index 50c7d530a3c2..934083faab17 100644 --- a/src/components/AddPaymentMethodMenu.tsx +++ b/src/components/AddPaymentMethodMenu.tsx @@ -2,7 +2,7 @@ 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'; @@ -17,12 +17,7 @@ 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; @@ -59,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. @@ -146,8 +141,4 @@ function AddPaymentMethodMenu({ AddPaymentMethodMenu.displayName = 'AddPaymentMethodMenu'; -export default withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, -})(AddPaymentMethodMenu); +export default AddPaymentMethodMenu; From d425f25b00dbb0ed0707bfa2937aed4762dcb23e Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:24:23 +0200 Subject: [PATCH 124/151] fix eslint errors --- src/components/AddPaymentMethodMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AddPaymentMethodMenu.tsx b/src/components/AddPaymentMethodMenu.tsx index 934083faab17..0057438e3913 100644 --- a/src/components/AddPaymentMethodMenu.tsx +++ b/src/components/AddPaymentMethodMenu.tsx @@ -10,7 +10,7 @@ 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'; From 1be218652113e09ef2a967bdde894a09afa4da3c Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 2 Oct 2024 19:38:26 +0530 Subject: [PATCH 125/151] Fix name of component --- src/pages/MissingPersonalDetails/substeps/Confirmation.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/MissingPersonalDetails/substeps/Confirmation.tsx b/src/pages/MissingPersonalDetails/substeps/Confirmation.tsx index 8cc570ea0e04..b5c27912cd6a 100644 --- a/src/pages/MissingPersonalDetails/substeps/Confirmation.tsx +++ b/src/pages/MissingPersonalDetails/substeps/Confirmation.tsx @@ -14,7 +14,7 @@ import INPUT_IDS from '@src/types/form/PersonalDetailsForm'; const PERSONAL_DETAILS_STEP_INDEXES = CONST.MISSING_PERSONAL_DETAILS_INDEXES.MAPPING; -function Confirmation({personalDetailsValues: values, onNext, onMove}: CustomSubStepProps) { +function ConfirmationStep({personalDetailsValues: values, onNext, onMove}: CustomSubStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const {isOffline} = useNetwork(); @@ -77,6 +77,6 @@ function Confirmation({personalDetailsValues: values, onNext, onMove}: CustomSub ); } -Confirmation.displayName = 'ConfirmationStep'; +ConfirmationStep.displayName = 'ConfirmationStep'; -export default Confirmation; +export default ConfirmationStep; From 48e49236733d7f5a9ffce8feda21ec157bcbce15 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Wed, 2 Oct 2024 15:32:34 +0100 Subject: [PATCH 126/151] don't show just track it unless on CombinedTrackSubmit --- src/pages/iou/request/step/IOURequestStepParticipants.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index 7449042141f3..b27e61df743c 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -46,7 +46,7 @@ function IOURequestStepParticipants({ const {translate} = useLocalize(); const styles = useThemeStyles(); const isFocused = useIsFocused(); - const {canUseP2PDistanceRequests} = usePermissions(iouType); + const {canUseP2PDistanceRequests, canUseCombinedTrackSubmit} = usePermissions(iouType); // We need to set selectedReportID if user has navigated back from confirmation page and navigates to confirmation page with already selected participant const selectedReportID = useRef(participants?.length === 1 ? participants.at(0)?.reportID ?? reportID : reportID); @@ -76,7 +76,7 @@ function IOURequestStepParticipants({ }, [iouType, translate, isSplitRequest, action]); const selfDMReportID = useMemo(() => ReportUtils.findSelfDMReportID(), []); - const shouldDisplayTrackExpenseButton = !!selfDMReportID; + const shouldDisplayTrackExpenseButton = !!selfDMReportID && canUseCombinedTrackSubmit; const receiptFilename = transaction?.filename; const receiptPath = transaction?.receipt?.source; From 9e424cbb2dae76e322a732ff5d81fa69dc52f082 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 2 Oct 2024 20:12:20 +0530 Subject: [PATCH 127/151] Fix lint --- src/components/ReportActionItem/IssueCardMessage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/IssueCardMessage.tsx b/src/components/ReportActionItem/IssueCardMessage.tsx index c8e0bcf165bd..c1f4e534a3c1 100644 --- a/src/components/ReportActionItem/IssueCardMessage.tsx +++ b/src/components/ReportActionItem/IssueCardMessage.tsx @@ -24,7 +24,7 @@ function IssueCardMessage({action}: IssueCardMessageProps) { 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 || From 45ce5f5c9794bbb4bb40b8aefcc6632d95b65469 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Wed, 2 Oct 2024 15:46:17 +0100 Subject: [PATCH 128/151] fix :) --- src/pages/iou/request/step/IOURequestStepParticipants.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index b27e61df743c..e8f02f0c1975 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -46,7 +46,7 @@ function IOURequestStepParticipants({ const {translate} = useLocalize(); const styles = useThemeStyles(); const isFocused = useIsFocused(); - const {canUseP2PDistanceRequests, canUseCombinedTrackSubmit} = usePermissions(iouType); + const {canUseP2PDistanceRequests} = usePermissions(iouType); // We need to set selectedReportID if user has navigated back from confirmation page and navigates to confirmation page with already selected participant const selectedReportID = useRef(participants?.length === 1 ? participants.at(0)?.reportID ?? reportID : reportID); @@ -76,7 +76,7 @@ function IOURequestStepParticipants({ }, [iouType, translate, isSplitRequest, action]); const selfDMReportID = useMemo(() => ReportUtils.findSelfDMReportID(), []); - const shouldDisplayTrackExpenseButton = !!selfDMReportID && canUseCombinedTrackSubmit; + const shouldDisplayTrackExpenseButton = !!selfDMReportID && action === CONST.IOU.ACTION.CREATE; const receiptFilename = transaction?.filename; const receiptPath = transaction?.receipt?.source; From d9642490826031116f6850a556cf4a9ee7281bb0 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Wed, 2 Oct 2024 20:56:35 +0530 Subject: [PATCH 129/151] fixes the issue --- src/components/SelectionList/Search/ReportListItem.tsx | 4 ++-- src/components/SelectionList/Search/TransactionListItem.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/SelectionList/Search/ReportListItem.tsx b/src/components/SelectionList/Search/ReportListItem.tsx index 9b6cf6045b17..91213e046bb0 100644 --- a/src/components/SelectionList/Search/ReportListItem.tsx +++ b/src/components/SelectionList/Search/ReportListItem.tsx @@ -86,10 +86,10 @@ function ReportListItem({ styles.pv1half, styles.ph0, styles.overflowHidden, - item.isSelected && styles.activeComponentBG, - isFocused && styles.sidebarLinkActive, // Removing some of the styles because they are added to the parent OpacityView via animatedHighlightStyle {backgroundColor: 'unset'}, + item.isSelected && styles.activeComponentBG, + isFocused && styles.sidebarLinkActive, styles.mh0, ]; diff --git a/src/components/SelectionList/Search/TransactionListItem.tsx b/src/components/SelectionList/Search/TransactionListItem.tsx index a9bbdf1d1a65..e42609bdeb15 100644 --- a/src/components/SelectionList/Search/TransactionListItem.tsx +++ b/src/components/SelectionList/Search/TransactionListItem.tsx @@ -31,10 +31,10 @@ function TransactionListItem({ styles.selectionListPressableItemWrapper, styles.pv3, styles.ph3, - item.isSelected && styles.activeComponentBG, - isFocused && styles.sidebarLinkActive, // Removing some of the styles because they are added to the parent OpacityView via animatedHighlightStyle {backgroundColor: 'unset'}, + item.isSelected && styles.activeComponentBG, + isFocused && styles.sidebarLinkActive, styles.mh0, ]; From 524eb8b275cacbb14d9aee65cb66911230aac3f6 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak Date: Wed, 2 Oct 2024 18:19:30 +0200 Subject: [PATCH 130/151] focus list item if it exists --- src/components/Search/SearchRouter/SearchRouter.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 76fc42a4f5a9..b3f147b7ac28 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -50,9 +50,7 @@ function SearchRouter() { return Object.values(recentSearches ?? {}).sort((a, b) => b.timestamp.localeCompare(a.timestamp)); }, [recentSearches]); - const {options, areOptionsInitialized} = useOptionsList({ - shouldInitialize: true, - }); + const {options, areOptionsInitialized} = useOptionsList(); const searchOptions = useMemo(() => { if (!areOptionsInitialized) { return {recentReports: [], personalDetails: [], userToInvite: null, currentUserOption: null, categoryOptions: [], tagOptions: [], taxRatesOptions: []}; @@ -91,6 +89,15 @@ function SearchRouter() { 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]); + const contextualReportData = contextualReportID ? searchOptions.recentReports?.find((option) => option.reportID === contextualReportID) : undefined; const clearUserQuery = () => { From 7caca60c5dc977a94d8272a871a84d0324a10b39 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Wed, 2 Oct 2024 09:29:04 -0700 Subject: [PATCH 131/151] ensure that round trips do not display the duplicate waypoint error incorrectly --- src/libs/TransactionUtils/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 9fbed928423f..0db771eaa96b 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -646,7 +646,10 @@ function getValidWaypoints(waypoints: WaypointCollection | undefined, reArrangeI let waypointIndex = -1; return waypointValues.reduce((acc, currentWaypoint, index) => { - const previousWaypoint = waypointValues.at(lastWaypointIndex); + // Array.at(-1) returns the last element of the array + // If a user does a round trip, the last waypoint will be the same as the first waypoint + // We want to avoid comparing them as this will result in an incorrect duplicate waypoint error. + const previousWaypoint = lastWaypointIndex !== -1 ? waypointValues.at(lastWaypointIndex) : undefined; // Check if the waypoint has a valid address if (!waypointHasValidAddress(currentWaypoint)) { From c301698782eb2c337938e1fe327e070de9c6ee49 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Wed, 2 Oct 2024 17:00:11 +0000 Subject: [PATCH 132/151] Update version to 9.0.43-2 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 64b903fdbe24..150d243a185c 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 1009004302 + versionName "9.0.43-2" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index a62749c0629e..56189182168b 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.43.1 + 9.0.43.2 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 16d99226f854..eb194441e455 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.43.1 + 9.0.43.2 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 16c7823a1cf8..1b093f940673 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.43 CFBundleVersion - 9.0.43.1 + 9.0.43.2 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 657c49656503..bc4b22a37d48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.43-1", + "version": "9.0.43-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.43-1", + "version": "9.0.43-2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 41db1e2e46b4..4cd5726a4f82 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.43-1", + "version": "9.0.43-2", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 3a53c2d9fb8c8d2d63858b3ef45834b303d7e960 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Wed, 2 Oct 2024 22:36:48 +0530 Subject: [PATCH 133/151] fixes issue on android --- src/components/SelectionList/Search/ReportListItem.tsx | 2 +- src/components/SelectionList/Search/TransactionListItem.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/SelectionList/Search/ReportListItem.tsx b/src/components/SelectionList/Search/ReportListItem.tsx index 91213e046bb0..f4457662b86a 100644 --- a/src/components/SelectionList/Search/ReportListItem.tsx +++ b/src/components/SelectionList/Search/ReportListItem.tsx @@ -87,7 +87,7 @@ function ReportListItem({ styles.ph0, styles.overflowHidden, // Removing some of the styles because they are added to the parent OpacityView via animatedHighlightStyle - {backgroundColor: 'unset'}, + {backgroundColor: 'transparent'}, item.isSelected && styles.activeComponentBG, isFocused && styles.sidebarLinkActive, styles.mh0, diff --git a/src/components/SelectionList/Search/TransactionListItem.tsx b/src/components/SelectionList/Search/TransactionListItem.tsx index e42609bdeb15..76fe7eb429a8 100644 --- a/src/components/SelectionList/Search/TransactionListItem.tsx +++ b/src/components/SelectionList/Search/TransactionListItem.tsx @@ -32,7 +32,7 @@ function TransactionListItem({ styles.pv3, styles.ph3, // Removing some of the styles because they are added to the parent OpacityView via animatedHighlightStyle - {backgroundColor: 'unset'}, + {backgroundColor: 'transparent'}, item.isSelected && styles.activeComponentBG, isFocused && styles.sidebarLinkActive, styles.mh0, From 4d7a154c3c101b45e011eac49e077628a85205c3 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Wed, 2 Oct 2024 22:38:47 +0530 Subject: [PATCH 134/151] fixes issue on android --- src/components/SelectionList/Search/ReportListItem.tsx | 4 ++-- src/components/SelectionList/Search/TransactionListItem.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/SelectionList/Search/ReportListItem.tsx b/src/components/SelectionList/Search/ReportListItem.tsx index f4457662b86a..2cc3e7d5b2a4 100644 --- a/src/components/SelectionList/Search/ReportListItem.tsx +++ b/src/components/SelectionList/Search/ReportListItem.tsx @@ -86,8 +86,8 @@ function ReportListItem({ styles.pv1half, styles.ph0, styles.overflowHidden, - // Removing some of the styles because they are added to the parent OpacityView via animatedHighlightStyle - {backgroundColor: 'transparent'}, + // Removing background style because they are added to the parent OpacityView via animatedHighlightStyle + styles.bgTransparent, item.isSelected && styles.activeComponentBG, isFocused && styles.sidebarLinkActive, styles.mh0, diff --git a/src/components/SelectionList/Search/TransactionListItem.tsx b/src/components/SelectionList/Search/TransactionListItem.tsx index 76fe7eb429a8..9259583c9f9d 100644 --- a/src/components/SelectionList/Search/TransactionListItem.tsx +++ b/src/components/SelectionList/Search/TransactionListItem.tsx @@ -31,8 +31,8 @@ function TransactionListItem({ styles.selectionListPressableItemWrapper, styles.pv3, styles.ph3, - // Removing some of the styles because they are added to the parent OpacityView via animatedHighlightStyle - {backgroundColor: 'transparent'}, + // Removing background style because they are added to the parent OpacityView via animatedHighlightStyle + styles.bgTransparent, item.isSelected && styles.activeComponentBG, isFocused && styles.sidebarLinkActive, styles.mh0, From 8d2b0a65521084bafe79496faa6b945b8bf861ea Mon Sep 17 00:00:00 2001 From: Gandalf Date: Thu, 3 Oct 2024 00:03:55 +0530 Subject: [PATCH 135/151] Update TaxIdBusiness.tsx --- .../BusinessInfo/substeps/TaxIdBusiness.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ReimbursementAccount/BusinessInfo/substeps/TaxIdBusiness.tsx b/src/pages/ReimbursementAccount/BusinessInfo/substeps/TaxIdBusiness.tsx index 2c55a0b8c251..9cc2e87fd95d 100644 --- a/src/pages/ReimbursementAccount/BusinessInfo/substeps/TaxIdBusiness.tsx +++ b/src/pages/ReimbursementAccount/BusinessInfo/substeps/TaxIdBusiness.tsx @@ -48,7 +48,7 @@ function TaxIdBusiness({reimbursementAccount, onNext, isEditing}: TaxIdBusinessP const handleSubmit = useReimbursementAccountStepFormSubmit({ fieldIds: STEP_FIELDS, onNext, - shouldSaveDraft: isEditing, + shouldSaveDraft: true, }); return ( From ae698fcece1233509aea5682668126a4169d3171 Mon Sep 17 00:00:00 2001 From: Gandalf Date: Thu, 3 Oct 2024 00:19:28 +0530 Subject: [PATCH 136/151] Update WebsiteBusiness.tsx --- .../BusinessInfo/substeps/WebsiteBusiness.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ReimbursementAccount/BusinessInfo/substeps/WebsiteBusiness.tsx b/src/pages/ReimbursementAccount/BusinessInfo/substeps/WebsiteBusiness.tsx index 00ce90ddeb05..3d8fcd944f4f 100644 --- a/src/pages/ReimbursementAccount/BusinessInfo/substeps/WebsiteBusiness.tsx +++ b/src/pages/ReimbursementAccount/BusinessInfo/substeps/WebsiteBusiness.tsx @@ -47,7 +47,7 @@ function WebsiteBusiness({onNext, isEditing}: SubStepProps) { BankAccounts.addBusinessWebsiteForDraft((values as {website: string})?.website); onNext(); }, - shouldSaveDraft: isEditing, + shouldSaveDraft: true, }); return ( From 840299e0e19f00e9097180cedd3ce2bfc5c972e7 Mon Sep 17 00:00:00 2001 From: Gandalf Date: Thu, 3 Oct 2024 00:19:53 +0530 Subject: [PATCH 137/151] Update TaxIdBusiness.tsx --- .../BusinessInfo/substeps/TaxIdBusiness.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ReimbursementAccount/BusinessInfo/substeps/TaxIdBusiness.tsx b/src/pages/ReimbursementAccount/BusinessInfo/substeps/TaxIdBusiness.tsx index 9cc2e87fd95d..2c55a0b8c251 100644 --- a/src/pages/ReimbursementAccount/BusinessInfo/substeps/TaxIdBusiness.tsx +++ b/src/pages/ReimbursementAccount/BusinessInfo/substeps/TaxIdBusiness.tsx @@ -48,7 +48,7 @@ function TaxIdBusiness({reimbursementAccount, onNext, isEditing}: TaxIdBusinessP const handleSubmit = useReimbursementAccountStepFormSubmit({ fieldIds: STEP_FIELDS, onNext, - shouldSaveDraft: true, + shouldSaveDraft: isEditing, }); return ( From 71416cd4f99f137e74a11e2f2386ca1d6e45e3ed Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Wed, 2 Oct 2024 13:02:39 -0600 Subject: [PATCH 138/151] Update issue template to have a proper expand/collapse for screenshots --- .github/ISSUE_TEMPLATE/Standard.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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) From ab43c3a9f0d6d5e8b23840e2dba67051da273da1 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Wed, 2 Oct 2024 19:26:52 +0000 Subject: [PATCH 139/151] Update version to 9.0.43-3 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 150d243a185c..7290fd893c87 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 1009004302 - versionName "9.0.43-2" + versionCode 1009004303 + versionName "9.0.43-3" // 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/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 56189182168b..f5fd176d9f5c 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@
CFBundleVersion - 9.0.43.2 + 9.0.43.3 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index eb194441e455..5f54e72f6ce7 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.43.2 + 9.0.43.3 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 1b093f940673..1b0b23f34d94 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.43 CFBundleVersion - 9.0.43.2 + 9.0.43.3 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index bc4b22a37d48..43ffeaa46b13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.43-2", + "version": "9.0.43-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.43-2", + "version": "9.0.43-3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 4cd5726a4f82..4538e1492ac4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.43-2", + "version": "9.0.43-3", "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.", From 5185906e3a8ab41dc40627c21aea1928161303f9 Mon Sep 17 00:00:00 2001 From: situchan Date: Wed, 2 Oct 2024 12:45:07 -0700 Subject: [PATCH 140/151] fix wrong last message in LHN after message deletion --- src/libs/SidebarUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index d00446f6da1c..0496bc66fe5b 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -51,6 +51,7 @@ Onyx.connect({ const reportAction = reportActionsForDisplay.at(-1); if (!reportAction) { + delete visibleReportActionItems[reportID]; return; } visibleReportActionItems[reportID] = reportAction; From b585a9deb256453f66f9c58d8e7bdc4ebec0934e Mon Sep 17 00:00:00 2001 From: situchan Date: Wed, 2 Oct 2024 13:17:36 -0700 Subject: [PATCH 141/151] fix more --- src/libs/OptionsListUtils.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index cf0b31ef3267..90320b4a9ea1 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -317,11 +317,11 @@ Onyx.connect({ const firstReportAction = sortedReportActions.at(0); if (!firstReportAction) { - return; + delete lastReportActions[reportID]; + } else { + lastReportActions[reportID] = firstReportAction; } - lastReportActions[reportID] = firstReportAction; - // The report is only visible if it is the last action not deleted that // does not match a closed or created state. const reportActionsForDisplay = sortedReportActions.filter( @@ -334,6 +334,7 @@ Onyx.connect({ ); const reportActionForDisplay = reportActionsForDisplay.at(0); if (!reportActionForDisplay) { + delete lastVisibleReportActions[reportID]; return; } lastVisibleReportActions[reportID] = reportActionForDisplay; From 7992d213cae59eff0b6706b8871586e8d586df0a Mon Sep 17 00:00:00 2001 From: OSBotify Date: Wed, 2 Oct 2024 20:39:20 +0000 Subject: [PATCH 142/151] Update version to 9.0.43-4 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 7290fd893c87..85a08214c6a3 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 1009004303 - versionName "9.0.43-3" + versionCode 1009004304 + versionName "9.0.43-4" // 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/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index f5fd176d9f5c..0990a4cd4d10 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.43.3 + 9.0.43.4 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 5f54e72f6ce7..f2246d05d27b 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.43.3 + 9.0.43.4 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 1b0b23f34d94..c662d94364de 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.43 CFBundleVersion - 9.0.43.3 + 9.0.43.4 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 43ffeaa46b13..609ea6e16171 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.43-3", + "version": "9.0.43-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.43-3", + "version": "9.0.43-4", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 4538e1492ac4..dae51b758a4c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.43-3", + "version": "9.0.43-4", "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.", From d053555283ed45175b964efa630959a519db7e18 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Wed, 2 Oct 2024 15:14:10 -0700 Subject: [PATCH 143/151] ensure we only access emojiCode for indices greater than 0 --- src/libs/EmojiUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 493837ca023b..7c042bbefe67 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -462,7 +462,7 @@ const getPreferredSkinToneIndex = (value: OnyxEntry): number => */ const getPreferredEmojiCode = (emoji: Emoji, preferredSkinTone: OnyxEntry): string => { if (emoji.types && typeof preferredSkinTone === 'number') { - const emojiCodeWithSkinTone = emoji.types.at(preferredSkinTone); + const emojiCodeWithSkinTone = preferredSkinTone >= 0 ? emoji.types.at(preferredSkinTone) : undefined; // Note: it can happen that preferredSkinTone has a outdated format, // so it makes sense to check if we actually got a valid emoji code back From f475642a182c37e95a9d271119d4ca6290d8da11 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Wed, 2 Oct 2024 22:40:18 +0000 Subject: [PATCH 144/151] Update version to 9.0.43-5 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 85a08214c6a3..07b2a557d339 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 1009004304 - versionName "9.0.43-4" + versionCode 1009004305 + versionName "9.0.43-5" // 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/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 0990a4cd4d10..ac237507861a 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.43.4 + 9.0.43.5 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index f2246d05d27b..4fcfa8000138 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.43.4 + 9.0.43.5 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index c662d94364de..aea283903ca3 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.43 CFBundleVersion - 9.0.43.4 + 9.0.43.5 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 609ea6e16171..d14689a59ae4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.43-4", + "version": "9.0.43-5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.43-4", + "version": "9.0.43-5", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index dae51b758a4c..dba6c39a5dff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.43-4", + "version": "9.0.43-5", "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.", From 333dba728b7db5f5dec4c1d17db4637634fd5c94 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Wed, 2 Oct 2024 17:28:00 -0700 Subject: [PATCH 145/151] manually revert changes --- .../NotificationService.swift | 6 --- ios/Podfile | 1 - ios/Podfile.lock | 38 ++----------------- jest/setup.ts | 3 -- package-lock.json | 13 ------- package.json | 1 - src/libs/Log.ts | 18 --------- 7 files changed, 4 insertions(+), 76 deletions(-) diff --git a/ios/NotificationServiceExtension/NotificationService.swift b/ios/NotificationServiceExtension/NotificationService.swift index b3c56a36619d..806d14d4c786 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) 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 d14689a59ae4..e7cc5bbdc58e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", @@ -34389,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 dba6c39a5dff..923dfdcd88ea 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/libs/Log.ts b/src/libs/Log.ts index b9d1b246425e..72673b8d3f79 100644 --- a/src/libs/Log.ts +++ b/src/libs/Log.ts @@ -3,7 +3,6 @@ /* eslint-disable rulesdir/no-api-in-views */ import {Logger} from 'expensify-common'; -import AppLogs from 'react-native-app-logs'; import Onyx from 'react-native-onyx'; import type {Merge} from 'type-fest'; import CONST from '@src/CONST'; @@ -83,21 +82,4 @@ const Log = new Logger({ }); timeout = setTimeout(() => Log.info('Flushing logs older than 10 minutes', true, {}, true), 10 * 60 * 1000); -AppLogs.configureAppGroupName('group.com.expensify.new'); -AppLogs.registerHandler({ - filter: '[NotificationService]', - handler: ({filter, logs}) => { - logs.forEach((log) => { - // Both native and JS logs are captured by the filter so we replace the filter before logging to avoid an infinite loop - const message = `[PushNotification] ${log.message.replace(filter, 'NotificationService -')}`; - - if (log.level === 'error') { - Log.hmmm(message); - } else { - Log.info(message); - } - }); - }, -}); - export default Log; From e00c18d20e422e196b5a6851a2fc4e349059b839 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Wed, 2 Oct 2024 17:34:15 -0700 Subject: [PATCH 146/151] revert --- ios/NotificationServiceExtension/NotificationService.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/NotificationServiceExtension/NotificationService.swift b/ios/NotificationServiceExtension/NotificationService.swift index 806d14d4c786..e489cb368d17 100644 --- a/ios/NotificationServiceExtension/NotificationService.swift +++ b/ios/NotificationServiceExtension/NotificationService.swift @@ -42,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 { @@ -212,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 } } From e02df29fc8ff05da2a242775fe972cf2bc5b817a Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 3 Oct 2024 02:04:23 +0000 Subject: [PATCH 147/151] Update version to 9.0.43-6 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 07b2a557d339..13dbe966b4a7 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 1009004305 - versionName "9.0.43-5" + versionCode 1009004306 + versionName "9.0.43-6" // 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/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index ac237507861a..153a5341b932 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.43.5 + 9.0.43.6 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 4fcfa8000138..c2cff756771d 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.43.5 + 9.0.43.6 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index aea283903ca3..eff1be3acd96 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.43 CFBundleVersion - 9.0.43.5 + 9.0.43.6 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index e7cc5bbdc58e..671834c4b975 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.43-5", + "version": "9.0.43-6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.43-5", + "version": "9.0.43-6", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 923dfdcd88ea..1be7cbd1cdbe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.43-5", + "version": "9.0.43-6", "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.", From e61ee7fbe60acc12879b9d5cc6ef92c4a0b98caa Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 3 Oct 2024 03:17:30 +0000 Subject: [PATCH 148/151] Update version to 9.0.44-0 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 4 ++-- ios/NewExpensifyTests/Info.plist | 4 ++-- ios/NotificationServiceExtension/Info.plist | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 13dbe966b4a7..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 1009004306 - versionName "9.0.43-6" + 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/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 153a5341b932..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.6 + 9.0.44.0 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index c2cff756771d..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.6 + 9.0.44.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index eff1be3acd96..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.6 + 9.0.44.0 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 671834c4b975..33baf6a35084 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.43-6", + "version": "9.0.44-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.43-6", + "version": "9.0.44-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 1be7cbd1cdbe..527a293a6a9e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.43-6", + "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.", From 52c3de16dee277c3695ab70d9b2d33afcfdf2e35 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 3 Oct 2024 11:16:59 +0700 Subject: [PATCH 149/151] add fallback reportId as -1 --- src/pages/home/report/ReportActionItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 428ea082b42e..3a2fb427e630 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -393,7 +393,7 @@ function ReportActionItem({ const attachmentContextValue = useMemo(() => ({reportID, type: CONST.ATTACHMENT_TYPE.REPORT}), [reportID]); - const mentionReportContextValue = useMemo(() => ({currentReportID: report?.reportID ?? ''}), [report?.reportID]); + const mentionReportContextValue = useMemo(() => ({currentReportID: report?.reportID ?? '-1'}), [report?.reportID]); const actionableItemButtons: ActionableItem[] = useMemo(() => { if (ReportActionsUtils.isActionableAddPaymentCard(action) && shouldRenderAddPaymentCard()) { From 87649dc38e0732367e5a6e4a912c417b2d38b15d Mon Sep 17 00:00:00 2001 From: Christina Dobrzynski <51066321+Christinadobrzyn@users.noreply.github.com> Date: Thu, 3 Oct 2024 17:11:11 +0800 Subject: [PATCH 150/151] Update and rename Connect-Personal-US-Bank-Account.md to Connect-Personal-Bank-Account.md Removed the "US" from the article title and article path. --- ...onal-US-Bank-Account.md => Connect-Personal-Bank-Account.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/{Connect-Personal-US-Bank-Account.md => Connect-Personal-Bank-Account.md} (97%) 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 ---
From 12e60e367380b087833f8b3470d621b7537853a8 Mon Sep 17 00:00:00 2001 From: Christina Dobrzynski <51066321+Christinadobrzyn@users.noreply.github.com> Date: Thu, 3 Oct 2024 17:15:17 +0800 Subject: [PATCH 151/151] Update redirects.csv --- docs/redirects.csv | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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