From 9b517d4bf2bdb162615aceb318049e12c3105fa5 Mon Sep 17 00:00:00 2001 From: Tomasz Lesniakiewicz Date: Thu, 21 Nov 2024 16:14:13 +0100 Subject: [PATCH 01/66] fix: remove duplicates when report is same as policy --- src/components/ParentNavigationSubtitle.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index 997106f3e649..9edf73e5f36f 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -61,7 +61,9 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportAct {reportName} )} - {!!workspaceName && {` ${translate('threads.in')} ${workspaceName}`}} + {workspaceName && workspaceName !== reportName && ( + {` ${translate('threads.in')} ${workspaceName}`} + )} ); From ee7c1f5a65fae14e2cee8cda31ff631b446b42f7 Mon Sep 17 00:00:00 2001 From: Tomasz Lesniakiewicz Date: Thu, 21 Nov 2024 19:17:07 +0100 Subject: [PATCH 02/66] fix lint --- src/components/ParentNavigationSubtitle.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index 9edf73e5f36f..c80e8a8caf1a 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -61,7 +61,7 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportAct {reportName} )} - {workspaceName && workspaceName !== reportName && ( + {!!workspaceName && workspaceName !== reportName && ( {` ${translate('threads.in')} ${workspaceName}`} )} From 034d7e140ca63d15edfac6698ad9f0af3d4886ba Mon Sep 17 00:00:00 2001 From: Rachael Hopkins <32854888+RachCHopkins@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:06:46 +1300 Subject: [PATCH 03/66] Create expensify-api.md --- .../connections/expensify-api.md | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 docs/articles/expensify-classic/connections/expensify-api.md diff --git a/docs/articles/expensify-classic/connections/expensify-api.md b/docs/articles/expensify-classic/connections/expensify-api.md new file mode 100644 index 000000000000..52e86a3c9f14 --- /dev/null +++ b/docs/articles/expensify-classic/connections/expensify-api.md @@ -0,0 +1,227 @@ +--- +title: Expensify API +description: User-sourced tips and tricks for using Expensify’s API. +--- +# Overview +An API (Application Programming Interface) allows two programs to communicate with each other. Expensify's API connects with various software platforms like NetSuite or Xero, and it can also link to other systems that don’t have a pre-made connection, such as [Workday](https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/Workday). + +{% include info.html %} +To begin, review our [Integration Server Manual](https://integrations.expensify.com/Integration-Server/doc/#introduction) thoroughly, as it will be your primary resource. The Expensify API is a self-serve tool, and your internal team is responsible for setting it up and ensuring it meets your needs. We can assist with basic troubleshooting, but the level of support may vary based on the support agent or account manager. It’s important for your team to be familiar with the setup process. +{% include end-info.html %} + +We've compiled answers to some frequently asked questions to help you get started. + +**Should I give your support team my API credentials when I need help?** + +If you’re seeking help with Expensify's API, do not share your partnerUserSecret. If you do, immediately rotate your credentials on [this page](https://www.expensify.com/tools/integrations/). + +**Is there a rate limit?** + +Yes, the rate limit is currently 50 requests per minute. If you exceed this limit, you'll receive an error message. + +**What is a Policy ID?** + +This is also known as a Workspace ID. To find your Policy/Workspace ID, +Hover over Settings and click Workspaces. +Click the name of the Workspace. +Copy the ID number from the URL. For example, if the URL is https://www.expensify.com/policy?param={"policyID":"0810E551A5F2A9C2”}, then your workspace ID is 0810E551A5F2A9C2. + +**Can I use the parent type `file` to export workspace/policy data?** + +No. The parent type `file` can only be used to export expense and report data — not policy information. To export policy data (e.g., categories, tags), you must use the `get` type with `inputSettings.type` set to `policy`. + +**Can I use the API to create Domain Groups?** + +No, you cannot create domain groups. You can only assign users to them. + +**I’m exporting expense IDs `${expense.transactionID}` but when I open my CSV in Excel, it’s changing all the IDs and making them look the same. How can I prevent this?** + +Try prepending a non-numeric character like a quote to force Excel to interpret the value as a string and not a number (i.e., `'${expense.transactionID}`). + +**How can we export the person who will approve a report while the reports are still processing?** + +Use the field ${report.managerEmail}. + +**Why won’t my boolean field return any data?** + +Boolean fields won't output values without a string. For example, instead of using `${expense.billable}`, use `${expense.billable?string("Yes", "No")}`. This will display "Yes" if the expense is billable and "No" if it is not. + +**Can I export the reports for just one user?** + +Not in a quick convenient way, as you would need to include the user in your template. The simplest approach is to export data for all users and then apply a filter in your preferred spreadsheet program. + +**Can I create expenses on behalf of users?** + +Yes. However, to access the Expense Creator API on behalf of employees, Expensify needs to verify the following setup: + +Ensure you are properly configured (e.g., Domain Control, Domain Admin, Policy Admin). +Verify you have internal authorization to add data to other accounts within your domain. + +If you need this access, contact concierge@expensify.com and reference this help page. + +## Using Postman + +Many customers use Postman to help them build out their APIs. Below are some guides contributed by our customers. Please note, in all cases, you will need to first generate your authentication credentials, the steps for which can be found [here](https://integrations.expensify.com/Integration-Server/doc/#introduction) and have them ready: + +### Download expenses from a report as a CSV file + +**Step 1: Get the ID of a report you want to export in Expensify** + +Find the ID by opening the expense report and clicking Details at the top right corner of the page. At the top of the menu, the ID is provided as the “Long ID.” + +**Step 3: Export (generate) a "Report" as a CSV file** +{% include info.html %} +For this you'll use the Documentation under [Report Exporter](https://integrations.expensify.com/Integration-Server/doc/#export). +{% include end-info.html %} + +In Postman, set the following: + +- HTTP Action: POST +- URL: https://integrations.expensify.com/Integration-Server/ExpensifyIntegrations +- Your only Parameters ("Params") will be "requestJobDescription", described below +- Body: "x-www-form-encoded", with a key "template", described below + +The requestJobDescription key will have a value like below: + +``` +{ + "type": "file", + "credentials": { + "partnerUserID": "my_user_id", + "partnerUserSecret": "my_user_secret" + }, + "onReceive": { + "immediateResponse": [ + "returnRandomFileName" + ] + }, + "inputSettings": { + "type": "combinedReportData", + "filters": { + "reportIDList": "50352738" + } + }, + "outputSettings": { + "fileExtension": "csv" + } +} +``` +Take the above and replace it with your own partnerUserID, partnerUserSecret, and reportIDList. To download multiple reports, you can use a comma-separated list as the reportIDList, such as "12345,45678,11111". + +The template key will have the value like below: + +``` +<#if addHeader> + Merchant,Amount,Transaction Date<#lt> + +<#list reports as report> + <#list report.transactionList as expense> + <#if expense.modifiedMerchant?has_content> + <#assign merchant = expense.modifiedMerchant> + <#else> + <#assign merchant = expense.merchant> + + <#if expense.convertedAmount?has_content> + <#assign amount = expense.convertedAmount/100> + <#elseif expense.modifiedAmount?has_content> + <#assign amount = expense.modifiedAmount/100> + <#else> + <#assign amount = expense.amount/100> + + <#if expense.modifiedCreated?has_content> + <#assign created = expense.modifiedCreated> + <#else> + <#assign created = expense.created> + + ${merchant},<#t> + ${amount},<#t> + ${created}<#lt> + + +``` + +The template variable determines what information is saved in your CSV file. If you want more columns than merchant, amount, and transaction date, follow the syntax as defined in the export template format documentation. + +**Step 4: Save your generated file name** + +Expensify currently supports only the "onReceive":{"immediateResponse":["returnRandomFileName"]} option in step 3, so you should receive a random filename back from the API like "exportc111111d-a1a1-a1a1-a1a1-d1111111f.csv". You will need to document this filename if you plan on running the download command after this one. + +**Step 5: Download your exported report** + +Set up another API call in almost the same way you did before. You don't need the template key in the Body anymore, so delete that and set the Body type to "none". Then modify your requestJobDescription to read like below, but with your own credentials and file name: + +``` +{ + "type": "download", + "credentials": { + "partnerUserID": "my_user_id", + "partnerUserSecret": "my_user_secret" + }, + "fileName": "exportc111111d-a1a1-a1a1-a1a1-d1111111f.csv", + "fileSystem": "integrationServer" +} +``` + +Click Go and you should see the CSV in the response body. + +*Thank you to our customer Frederico Pettinella who originally wrote and shared this guide.* + +### Use Advanced Employee Updater API with Postman + +1. Create a new request. +2. Select POST as the method. +3. Copy-paste this to the URL section: https://integrations.expensify.com/Integration-Server/ExpensifyIntegrations +4. Do not add anything to "Params", "Authorization", or "Header". Go straight to "Body". +5. Select "x-www-form-urlencoded" and add 2 keys "requestJobDescription" and "data". +6. For "requestJobDescription" copy and paste the following text, and replace the values for "partnerUserID", "partner_UserSecret", and "recipients". Remember that "dry-run"=true means that it's just for testing. Set it to false whenever you are ready to modify that in production. + +``` +{ + "type": "update", + "dry-run" : true, + "credentials": { + "partnerUserID": "aa_api_domain_com", + "partnerUserSecret": "xxx" + }, + "dataSource" : "request", + "inputSettings": { + "type": "employees", + "entity": "generic" + }, + "onFinish":[ + {"actionName": "email", "recipients":"admin1@domain.com"} + ] + }' +For "data" copy-paste the following text and replace values as needed +{ + "Employees":[ + { + "employeeEmail": "user@domain.com", + "managerEmail": "usermanager@domain.com", + "policyID": "1D1BC525C4892584", +"isTerminated": "false", + } +]} +``` + +7. Click SEND. + +This is how it should look on Postman: + +![Image of API credentials request]({{site.url}}/assets/images/ExpensifyHelp-Postman-userID-userSecret-request.png){:width="100%"} + +![Image of API data request]({{site.url}}/assets/images/ExpensifyHelp-Postman-Request-data.png){:width="100%"} + +This is how the value looks inside those keys: + +![Image of API dry run]({{site.url}}/assets/images/ExpensifyHelp-Postman-Successful-dryrun-response.png){:width="100%"} + +Remember that there are 4 [required fields](https://integrations.expensify.com/Integration-Server/doc/employeeUpdater/#api-principles) needed to make this API call to work: + +- employeeEmail +- managerEmail +- employeeID +- policyID + +*Thank you to our customer Raul Hernandez who originally wrote and shared this guide.* + From 8e49e1ee3cb193c8fa75cd843be0afb003af8882 Mon Sep 17 00:00:00 2001 From: Rachael Hopkins <32854888+RachCHopkins@users.noreply.github.com> Date: Tue, 3 Dec 2024 17:26:07 +1300 Subject: [PATCH 04/66] Rename expensify-api.md to Expensify-API.md --- .../connections/{expensify-api.md => Expensify-API.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/articles/expensify-classic/connections/{expensify-api.md => Expensify-API.md} (100%) diff --git a/docs/articles/expensify-classic/connections/expensify-api.md b/docs/articles/expensify-classic/connections/Expensify-API.md similarity index 100% rename from docs/articles/expensify-classic/connections/expensify-api.md rename to docs/articles/expensify-classic/connections/Expensify-API.md From 27aeb7f7c5fb91984ec3fbbd0bc2c7d4747e2531 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 5 Dec 2024 04:46:19 +0530 Subject: [PATCH 05/66] fix: Report fields - Initial value field name is Date for text report field. Signed-off-by: krishna2323 --- src/pages/workspace/reportFields/ReportFieldsSettingsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/reportFields/ReportFieldsSettingsPage.tsx b/src/pages/workspace/reportFields/ReportFieldsSettingsPage.tsx index 799c42ade8d0..401ca93fa9f9 100644 --- a/src/pages/workspace/reportFields/ReportFieldsSettingsPage.tsx +++ b/src/pages/workspace/reportFields/ReportFieldsSettingsPage.tsx @@ -113,7 +113,7 @@ function ReportFieldsSettingsPage({ style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} title={WorkspaceReportFieldUtils.getReportFieldInitialValue(reportField)} - description={translate('common.date')} + description={isDateFieldType ? translate('common.date') : translate('common.initialValue')} shouldShowRightIcon={!isDateFieldType && !hasAccountingConnections} interactive={!isDateFieldType && !hasAccountingConnections} onPress={() => Navigation.navigate(ROUTES.WORKSPACE_EDIT_REPORT_FIELDS_INITIAL_VALUE.getRoute(policyID, reportFieldID))} From 86c486bcf6d9a50affe640d82700aefdbd9d678f Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 6 Dec 2024 12:49:54 +0100 Subject: [PATCH 06/66] Add poor connection simulation --- src/components/TestToolMenu.tsx | 10 ++++++ src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/actions/Network.ts | 55 ++++++++++++++++++++++++++++++++- src/types/onyx/Network.ts | 6 ++++ 5 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/components/TestToolMenu.tsx b/src/components/TestToolMenu.tsx index e81405d026b4..12b6da2c4c9c 100644 --- a/src/components/TestToolMenu.tsx +++ b/src/components/TestToolMenu.tsx @@ -74,6 +74,16 @@ function TestToolMenu({network}: TestToolMenuProps) { accessibilityLabel="Force offline" isOn={!!network?.shouldForceOffline} onToggle={() => Network.setShouldForceOffline(!network?.shouldForceOffline)} + disabled={isUsingImportedState || network?.shouldSimulatePoorConnection} + /> + + + {/* When toggled the app will randomly change internet connection every 2-5 seconds */} + + Network.simulatePoorConnection(!network?.shouldSimulatePoorConnection, network?.poorConnectionTimeoutID)} disabled={isUsingImportedState} /> diff --git a/src/languages/en.ts b/src/languages/en.ts index d79695ed8b48..8dfa9c43f358 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1231,6 +1231,7 @@ const translations = { testingPreferences: 'Testing preferences', useStagingServer: 'Use Staging Server', forceOffline: 'Force offline', + simulatePoorConnection: 'Simulate poor internet connection', simulatFailingNetworkRequests: 'Simulate failing network requests', authenticationStatus: 'Authentication status', deviceCredentials: 'Device credentials', diff --git a/src/languages/es.ts b/src/languages/es.ts index 5ce47db18d35..049749be10fd 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1230,6 +1230,7 @@ const translations = { testingPreferences: 'Preferencias para Tests', useStagingServer: 'Usar servidor “staging”', forceOffline: 'Forzar desconexión', + simulatePoorConnection: 'Simular una conexión a internet deficiente', simulatFailingNetworkRequests: 'Simular fallos en solicitudes de red', authenticationStatus: 'Estado de autenticación', deviceCredentials: 'Credenciales del dispositivo', diff --git a/src/libs/actions/Network.ts b/src/libs/actions/Network.ts index d8a87aff551d..638eef84aff6 100644 --- a/src/libs/actions/Network.ts +++ b/src/libs/actions/Network.ts @@ -1,8 +1,28 @@ import Onyx from 'react-native-onyx'; import Log from '@libs/Log'; import type {NetworkStatus} from '@libs/NetworkConnection'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +let isPoorConnectionSimulated: boolean | undefined; +Onyx.connect({ + key: ONYXKEYS.NETWORK, + callback: (value) => { + if (!value) { + return; + } + + // Starts random network status change when shouldSimulatePoorConnection is turned into true + // or after app restart if shouldSimulatePoorConnection is true already + if (!isPoorConnectionSimulated && !!value.shouldSimulatePoorConnection) { + clearTimeout(value.poorConnectionTimeoutID); + setRandomNetworkStatus(true); + } + + isPoorConnectionSimulated = !!value.shouldSimulatePoorConnection; + }, +}); + function setIsOffline(isOffline: boolean, reason = '') { if (reason) { let textToLog = '[Network] Client is'; @@ -32,4 +52,37 @@ function setShouldFailAllRequests(shouldFailAllRequests: boolean) { Onyx.merge(ONYXKEYS.NETWORK, {shouldFailAllRequests}); } -export {setIsOffline, setShouldForceOffline, setShouldFailAllRequests, setTimeSkew, setNetWorkStatus}; +function setPoorConnectionTimeoutID(poorConnectionTimeoutID: NodeJS.Timeout | undefined) { + Onyx.merge(ONYXKEYS.NETWORK, {poorConnectionTimeoutID}); +} + +function setRandomNetworkStatus(initialCall = false) { + // The check to ensure no new timeouts are scheduled after poor connection simulation is stopped + if (!isPoorConnectionSimulated && !initialCall) { + setShouldForceOffline(false); + return; + } + + const statuses = [CONST.NETWORK.NETWORK_STATUS.OFFLINE, CONST.NETWORK.NETWORK_STATUS.ONLINE]; + const randomStatus = statuses[Math.floor(Math.random() * statuses.length)]; + const randomInterval = Math.random() * (5000 - 2000) + 2000; // random interval between 2-5 seconds + Log.info(`[NetworkConnection] Set connection status "${randomStatus}" for ${randomInterval} sec`); + + setShouldForceOffline(randomStatus === CONST.NETWORK.NETWORK_STATUS.OFFLINE); + + const timeoutID = setTimeout(setRandomNetworkStatus, randomInterval); + + setPoorConnectionTimeoutID(timeoutID); +} + +function simulatePoorConnection(shouldSimulatePoorConnection: boolean, poorConnectionTimeoutID: NodeJS.Timeout | undefined) { + if (!shouldSimulatePoorConnection) { + clearTimeout(poorConnectionTimeoutID); + setPoorConnectionTimeoutID(undefined); + setShouldForceOffline(false); + } + + Onyx.merge(ONYXKEYS.NETWORK, {shouldSimulatePoorConnection}); +} + +export {setIsOffline, setShouldForceOffline, setShouldFailAllRequests, setTimeSkew, setNetWorkStatus, simulatePoorConnection}; diff --git a/src/types/onyx/Network.ts b/src/types/onyx/Network.ts index 680c6c468c00..d834da50cab7 100644 --- a/src/types/onyx/Network.ts +++ b/src/types/onyx/Network.ts @@ -8,6 +8,12 @@ type Network = { /** Should the network be forced offline */ shouldForceOffline?: boolean; + /** Whether we should simulate poor connection */ + shouldSimulatePoorConnection?: boolean; + + /** Poor connection timeout id */ + poorConnectionTimeoutID?: NodeJS.Timeout; + /** Whether we should fail all network requests */ shouldFailAllRequests?: boolean; From 43330f8a5b33f8a50e6872bbc2cea01c4e4772ef Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 6 Dec 2024 12:55:24 +0100 Subject: [PATCH 07/66] Lint fix --- src/components/TestToolMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TestToolMenu.tsx b/src/components/TestToolMenu.tsx index 12b6da2c4c9c..6b020657116e 100644 --- a/src/components/TestToolMenu.tsx +++ b/src/components/TestToolMenu.tsx @@ -74,7 +74,7 @@ function TestToolMenu({network}: TestToolMenuProps) { accessibilityLabel="Force offline" isOn={!!network?.shouldForceOffline} onToggle={() => Network.setShouldForceOffline(!network?.shouldForceOffline)} - disabled={isUsingImportedState || network?.shouldSimulatePoorConnection} + disabled={!!isUsingImportedState || network?.shouldSimulatePoorConnection} /> From 7c12b42064ecb87ce796bc8a5dae6b43b9920e63 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Mon, 9 Dec 2024 10:28:44 +0700 Subject: [PATCH 08/66] fix: link briefly shown before magic code is requested --- src/libs/actions/Link.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/Link.ts b/src/libs/actions/Link.ts index 0250ea7b84a1..8d367c7eeec2 100644 --- a/src/libs/actions/Link.ts +++ b/src/libs/actions/Link.ts @@ -183,10 +183,12 @@ function openLink(href: string, environmentURL: string, isAttachment = false) { Navigation.navigate(internalNewExpensifyPath as Route); return; } + const OLD_DOT_PUBLIC_URLS: string[] = [CONST.TERMS_URL, CONST.PRIVACY_URL]; // If we are handling an old dot Expensify link we need to open it with openOldDotLink() so we can navigate to it with the user already logged in. // As attachments also use expensify.com we don't want it working the same as links. - if (internalExpensifyPath && !isAttachment) { + const isPublicOldDotURL = OLD_DOT_PUBLIC_URLS.includes(href); + if (internalExpensifyPath && !isAttachment && !isPublicOldDotURL) { openOldDotLink(internalExpensifyPath); return; } From 5e52653975b8eeb83dfa441ea46498858c6f7f7f Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 9 Dec 2024 09:38:59 +0100 Subject: [PATCH 09/66] Log connection changes --- src/libs/actions/Network.ts | 30 ++++++++++++++++++++++++++++++ src/types/onyx/Network.ts | 13 +++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/libs/actions/Network.ts b/src/libs/actions/Network.ts index 638eef84aff6..6c85edf8e048 100644 --- a/src/libs/actions/Network.ts +++ b/src/libs/actions/Network.ts @@ -1,10 +1,13 @@ +import {differenceInHours} from 'date-fns/differenceInHours'; import Onyx from 'react-native-onyx'; import Log from '@libs/Log'; import type {NetworkStatus} from '@libs/NetworkConnection'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {ConnectionChanges} from '@src/types/onyx/Network'; let isPoorConnectionSimulated: boolean | undefined; +let connectionChanges: ConnectionChanges | undefined; Onyx.connect({ key: ONYXKEYS.NETWORK, callback: (value) => { @@ -20,9 +23,33 @@ Onyx.connect({ } isPoorConnectionSimulated = !!value.shouldSimulatePoorConnection; + connectionChanges = value.connectionChanges; }, }); +function trackConnectionChanges() { + if (!connectionChanges?.startTime) { + Onyx.merge(ONYXKEYS.NETWORK, {connectionChanges: {startTime: new Date().getTime(), amount: 1}}); + return; + } + + const diffInHours = differenceInHours(new Date(), connectionChanges.startTime); + const newAmount = (connectionChanges.amount ?? 0) + 1; + + if (diffInHours < 1) { + Onyx.merge(ONYXKEYS.NETWORK, {connectionChanges: {amount: newAmount}}); + return; + } + + Log.info( + `[NetworkConnection] Connection has changed ${newAmount} time(s) for the last ${diffInHours} hour(s). Poor connection simulation is turned ${ + isPoorConnectionSimulated ? 'on' : 'off' + }`, + ); + + Onyx.merge(ONYXKEYS.NETWORK, {connectionChanges: {startTime: new Date().getTime(), amount: 0}}); +} + function setIsOffline(isOffline: boolean, reason = '') { if (reason) { let textToLog = '[Network] Client is'; @@ -30,6 +57,9 @@ function setIsOffline(isOffline: boolean, reason = '') { textToLog += ` because: ${reason}`; Log.info(textToLog); } + + trackConnectionChanges(); + Onyx.merge(ONYXKEYS.NETWORK, {isOffline}); } diff --git a/src/types/onyx/Network.ts b/src/types/onyx/Network.ts index d834da50cab7..74fb1202a8a2 100644 --- a/src/types/onyx/Network.ts +++ b/src/types/onyx/Network.ts @@ -1,5 +1,14 @@ import type {NetworkStatus} from '@libs/NetworkConnection'; +/** The value where connection changes are tracked */ +type ConnectionChanges = { + /** Amount of connection changes */ + amount?: number; + + /** Start time in milliseconds */ + startTime?: number; +}; + /** Model of network state */ type Network = { /** Is the network currently offline or not */ @@ -14,6 +23,9 @@ type Network = { /** Poor connection timeout id */ poorConnectionTimeoutID?: NodeJS.Timeout; + /** The value where connection changes are tracked */ + connectionChanges?: ConnectionChanges; + /** Whether we should fail all network requests */ shouldFailAllRequests?: boolean; @@ -25,3 +37,4 @@ type Network = { }; export default Network; +export type {ConnectionChanges}; From ede313c65c7da4bf361de60f317e0739ab068b0d Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 10 Dec 2024 17:03:50 +0100 Subject: [PATCH 10/66] fix: clear previous data from Expensify Card flow --- src/libs/actions/Card.ts | 4 ++-- .../expensifyCard/issueNew/AssigneeStep.tsx | 1 - .../expensifyCard/issueNew/ConfirmationStep.tsx | 12 ++---------- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 1c60d49e9170..c8dce813c895 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -392,8 +392,8 @@ function clearIssueNewCardFlow() { }); } -function clearIssueNewCardError(issueNewCard: IssueNewCardFlowData) { - Onyx.set(ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD, {...issueNewCard, errors: null}); +function clearIssueNewCardError() { + Onyx.merge(ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD, {errors: null}); } function updateExpensifyCardLimit(workspaceAccountID: number, cardID: number, newLimit: number, newAvailableSpend: number, oldLimit?: number, oldAvailableSpend?: number) { diff --git a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx index 769532e49351..a1328645d447 100644 --- a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx @@ -62,7 +62,6 @@ function AssigneeStep({policy}: AssigneeStepProps) { return; } Navigation.goBack(); - Card.clearIssueNewCardFlow(); }; const shouldShowSearchInput = policy?.employeeList && Object.keys(policy.employeeList).length >= MINIMUM_MEMBER_TO_SHOW_SEARCH; diff --git a/src/pages/workspace/expensifyCard/issueNew/ConfirmationStep.tsx b/src/pages/workspace/expensifyCard/issueNew/ConfirmationStep.tsx index c65ae8957dbe..8521da690fe2 100644 --- a/src/pages/workspace/expensifyCard/issueNew/ConfirmationStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/ConfirmationStep.tsx @@ -58,7 +58,6 @@ function ConfirmationStep({policyID, backTo}: ConfirmationStepProps) { return; } Navigation.navigate(backTo ?? ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policyID ?? '-1')); - Card.clearIssueNewCardFlow(); }, [backTo, policyID, isSuccessful]); const submit = (validateCode: string) => { @@ -142,15 +141,8 @@ function ConfirmationStep({policyID, backTo}: ConfirmationStepProps) { sendValidateCode={() => User.requestValidateCodeAction()} validateError={validateError} hasMagicCodeBeenSent={validateCodeSent} - clearError={() => { - Card.clearIssueNewCardError(issueNewCard); - }} - onClose={() => { - if (validateError) { - Card.clearIssueNewCardError(issueNewCard); - } - setIsValidateCodeActionModalVisible(false); - }} + clearError={() => Card.clearIssueNewCardError()} + onClose={() => setIsValidateCodeActionModalVisible(false)} isVisible={isValidateCodeActionModalVisible} title={translate('cardPage.validateCardTitle')} descriptionPrimary={translate('cardPage.enterMagicCode', {contactMethod: account?.primaryLogin ?? ''})} From 2b9fb2df463c445de4efe8cef5c18e60f15d09be Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 10 Dec 2024 17:18:23 +0100 Subject: [PATCH 11/66] Move the logic to NetworkConnection file --- src/components/TestToolMenu.tsx | 3 +- src/libs/NetworkConnection.ts | 70 +++++++++++++++++++++++++++++ src/libs/actions/Network.ts | 79 +++------------------------------ 3 files changed, 77 insertions(+), 75 deletions(-) diff --git a/src/components/TestToolMenu.tsx b/src/components/TestToolMenu.tsx index 6b020657116e..ea21a383d0cd 100644 --- a/src/components/TestToolMenu.tsx +++ b/src/components/TestToolMenu.tsx @@ -4,6 +4,7 @@ import {useOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ApiUtils from '@libs/ApiUtils'; +import NetworkConnection from '@libs/NetworkConnection'; import * as Network from '@userActions/Network'; import * as Session from '@userActions/Session'; import * as User from '@userActions/User'; @@ -83,7 +84,7 @@ function TestToolMenu({network}: TestToolMenuProps) { Network.simulatePoorConnection(!network?.shouldSimulatePoorConnection, network?.poorConnectionTimeoutID)} + onToggle={() => NetworkConnection.simulatePoorConnection(!network?.shouldSimulatePoorConnection, network?.poorConnectionTimeoutID)} disabled={isUsingImportedState} /> diff --git a/src/libs/NetworkConnection.ts b/src/libs/NetworkConnection.ts index cb9faae31ddd..e03f81abc508 100644 --- a/src/libs/NetworkConnection.ts +++ b/src/libs/NetworkConnection.ts @@ -1,4 +1,5 @@ import NetInfo from '@react-native-community/netinfo'; +import {differenceInHours} from 'date-fns/differenceInHours'; import isBoolean from 'lodash/isBoolean'; import throttle from 'lodash/throttle'; import Onyx from 'react-native-onyx'; @@ -6,6 +7,7 @@ import type {ValueOf} from 'type-fest'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {ConnectionChanges} from '@src/types/onyx/Network'; import * as NetworkActions from './actions/Network'; import AppStateMonitor from './AppStateMonitor'; import Log from './Log'; @@ -51,6 +53,7 @@ const triggerReconnectionCallbacks = throttle( * then all of the reconnection callbacks are triggered */ function setOfflineStatus(isCurrentlyOffline: boolean, reason = ''): void { + trackConnectionChanges(); NetworkActions.setIsOffline(isCurrentlyOffline, reason); // When reconnecting, ie, going from offline to online, all the reconnection callbacks @@ -64,12 +67,25 @@ function setOfflineStatus(isCurrentlyOffline: boolean, reason = ''): void { // Update the offline status in response to changes in shouldForceOffline let shouldForceOffline = false; +let isPoorConnectionSimulated: boolean | undefined; +let connectionChanges: ConnectionChanges | undefined; Onyx.connect({ key: ONYXKEYS.NETWORK, callback: (network) => { if (!network) { return; } + + // Starts random network status change when shouldSimulatePoorConnection is turned into true + // or after app restart if shouldSimulatePoorConnection is true already + if (!isPoorConnectionSimulated && !!network.shouldSimulatePoorConnection) { + clearTimeout(network.poorConnectionTimeoutID); + setRandomNetworkStatus(true); + } + + isPoorConnectionSimulated = !!network.shouldSimulatePoorConnection; + connectionChanges = network.connectionChanges; + const currentShouldForceOffline = !!network.shouldForceOffline; if (currentShouldForceOffline === shouldForceOffline) { return; @@ -104,6 +120,59 @@ Onyx.connect({ }, }); +/** Sets online/offline connection randomly every 2-5 seconds */ +function setRandomNetworkStatus(initialCall = false) { + // The check to ensure no new timeouts are scheduled after poor connection simulation is stopped + if (!isPoorConnectionSimulated && !initialCall) { + setOfflineStatus(false); + return; + } + + const statuses = [CONST.NETWORK.NETWORK_STATUS.OFFLINE, CONST.NETWORK.NETWORK_STATUS.ONLINE]; + const randomStatus = statuses[Math.floor(Math.random() * statuses.length)]; + const randomInterval = Math.random() * (5000 - 2000) + 2000; // random interval between 2-5 seconds + Log.info(`[NetworkConnection] Set connection status "${randomStatus}" for ${randomInterval} sec`); + + setOfflineStatus(randomStatus === CONST.NETWORK.NETWORK_STATUS.OFFLINE); + + const timeoutID = setTimeout(setRandomNetworkStatus, randomInterval); + NetworkActions.setPoorConnectionTimeoutID(timeoutID); +} + +function simulatePoorConnection(shouldSimulatePoorConnection: boolean, poorConnectionTimeoutID: NodeJS.Timeout | undefined) { + if (!shouldSimulatePoorConnection) { + clearTimeout(poorConnectionTimeoutID); + NetworkActions.setPoorConnectionTimeoutID(undefined); + setOfflineStatus(false); + } + + NetworkActions.setShouldSimulatePoorConnection(shouldSimulatePoorConnection); +} + +/** Tracks how many times the connection has changed within the time period */ +function trackConnectionChanges() { + if (!connectionChanges?.startTime) { + NetworkActions.setConnectionChanges({startTime: new Date().getTime(), amount: 1}); + return; + } + + const diffInHours = differenceInHours(new Date(), connectionChanges.startTime); + const newAmount = (connectionChanges.amount ?? 0) + 1; + + if (diffInHours < 1) { + NetworkActions.setConnectionChanges({amount: newAmount}); + return; + } + + Log.info( + `[NetworkConnection] Connection has changed ${newAmount} time(s) for the last ${diffInHours} hour(s). Poor connection simulation is turned ${ + isPoorConnectionSimulated ? 'on' : 'off' + }`, + ); + + NetworkActions.setConnectionChanges({startTime: new Date().getTime(), amount: 0}); +} + /** * Set up the event listener for NetInfo to tell whether the user has * internet connectivity or not. This is more reliable than the Pusher @@ -227,5 +296,6 @@ export default { triggerReconnectionCallbacks, recheckNetworkConnection, subscribeToNetInfo, + simulatePoorConnection, }; export type {NetworkStatus}; diff --git a/src/libs/actions/Network.ts b/src/libs/actions/Network.ts index 6c85edf8e048..6d75ec85aa0f 100644 --- a/src/libs/actions/Network.ts +++ b/src/libs/actions/Network.ts @@ -1,55 +1,9 @@ -import {differenceInHours} from 'date-fns/differenceInHours'; import Onyx from 'react-native-onyx'; import Log from '@libs/Log'; import type {NetworkStatus} from '@libs/NetworkConnection'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ConnectionChanges} from '@src/types/onyx/Network'; -let isPoorConnectionSimulated: boolean | undefined; -let connectionChanges: ConnectionChanges | undefined; -Onyx.connect({ - key: ONYXKEYS.NETWORK, - callback: (value) => { - if (!value) { - return; - } - - // Starts random network status change when shouldSimulatePoorConnection is turned into true - // or after app restart if shouldSimulatePoorConnection is true already - if (!isPoorConnectionSimulated && !!value.shouldSimulatePoorConnection) { - clearTimeout(value.poorConnectionTimeoutID); - setRandomNetworkStatus(true); - } - - isPoorConnectionSimulated = !!value.shouldSimulatePoorConnection; - connectionChanges = value.connectionChanges; - }, -}); - -function trackConnectionChanges() { - if (!connectionChanges?.startTime) { - Onyx.merge(ONYXKEYS.NETWORK, {connectionChanges: {startTime: new Date().getTime(), amount: 1}}); - return; - } - - const diffInHours = differenceInHours(new Date(), connectionChanges.startTime); - const newAmount = (connectionChanges.amount ?? 0) + 1; - - if (diffInHours < 1) { - Onyx.merge(ONYXKEYS.NETWORK, {connectionChanges: {amount: newAmount}}); - return; - } - - Log.info( - `[NetworkConnection] Connection has changed ${newAmount} time(s) for the last ${diffInHours} hour(s). Poor connection simulation is turned ${ - isPoorConnectionSimulated ? 'on' : 'off' - }`, - ); - - Onyx.merge(ONYXKEYS.NETWORK, {connectionChanges: {startTime: new Date().getTime(), amount: 0}}); -} - function setIsOffline(isOffline: boolean, reason = '') { if (reason) { let textToLog = '[Network] Client is'; @@ -58,8 +12,6 @@ function setIsOffline(isOffline: boolean, reason = '') { Log.info(textToLog); } - trackConnectionChanges(); - Onyx.merge(ONYXKEYS.NETWORK, {isOffline}); } @@ -86,33 +38,12 @@ function setPoorConnectionTimeoutID(poorConnectionTimeoutID: NodeJS.Timeout | un Onyx.merge(ONYXKEYS.NETWORK, {poorConnectionTimeoutID}); } -function setRandomNetworkStatus(initialCall = false) { - // The check to ensure no new timeouts are scheduled after poor connection simulation is stopped - if (!isPoorConnectionSimulated && !initialCall) { - setShouldForceOffline(false); - return; - } - - const statuses = [CONST.NETWORK.NETWORK_STATUS.OFFLINE, CONST.NETWORK.NETWORK_STATUS.ONLINE]; - const randomStatus = statuses[Math.floor(Math.random() * statuses.length)]; - const randomInterval = Math.random() * (5000 - 2000) + 2000; // random interval between 2-5 seconds - Log.info(`[NetworkConnection] Set connection status "${randomStatus}" for ${randomInterval} sec`); - - setShouldForceOffline(randomStatus === CONST.NETWORK.NETWORK_STATUS.OFFLINE); - - const timeoutID = setTimeout(setRandomNetworkStatus, randomInterval); - - setPoorConnectionTimeoutID(timeoutID); +function setShouldSimulatePoorConnection(shouldSimulatePoorConnection: boolean) { + Onyx.merge(ONYXKEYS.NETWORK, {shouldSimulatePoorConnection}); } -function simulatePoorConnection(shouldSimulatePoorConnection: boolean, poorConnectionTimeoutID: NodeJS.Timeout | undefined) { - if (!shouldSimulatePoorConnection) { - clearTimeout(poorConnectionTimeoutID); - setPoorConnectionTimeoutID(undefined); - setShouldForceOffline(false); - } - - Onyx.merge(ONYXKEYS.NETWORK, {shouldSimulatePoorConnection}); +function setConnectionChanges(connectionChanges: ConnectionChanges) { + Onyx.merge(ONYXKEYS.NETWORK, {connectionChanges}); } -export {setIsOffline, setShouldForceOffline, setShouldFailAllRequests, setTimeSkew, setNetWorkStatus, simulatePoorConnection}; +export {setIsOffline, setShouldForceOffline, setConnectionChanges, setShouldSimulatePoorConnection, setPoorConnectionTimeoutID, setShouldFailAllRequests, setTimeSkew, setNetWorkStatus}; From 714a2ecf0ba7564d20bcaffaece6db9299683fb2 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 10 Dec 2024 17:44:15 +0100 Subject: [PATCH 12/66] Make it possible to turn on only one out of three network test preferences per time --- src/components/TestToolMenu.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/TestToolMenu.tsx b/src/components/TestToolMenu.tsx index ea21a383d0cd..5277ba539374 100644 --- a/src/components/TestToolMenu.tsx +++ b/src/components/TestToolMenu.tsx @@ -75,7 +75,7 @@ function TestToolMenu({network}: TestToolMenuProps) { accessibilityLabel="Force offline" isOn={!!network?.shouldForceOffline} onToggle={() => Network.setShouldForceOffline(!network?.shouldForceOffline)} - disabled={!!isUsingImportedState || network?.shouldSimulatePoorConnection} + disabled={!!isUsingImportedState || !!network?.shouldSimulatePoorConnection || network?.shouldFailAllRequests} /> @@ -85,7 +85,7 @@ function TestToolMenu({network}: TestToolMenuProps) { accessibilityLabel="Simulate poor internet connection" isOn={!!network?.shouldSimulatePoorConnection} onToggle={() => NetworkConnection.simulatePoorConnection(!network?.shouldSimulatePoorConnection, network?.poorConnectionTimeoutID)} - disabled={isUsingImportedState} + disabled={!!isUsingImportedState || !!network?.shouldFailAllRequests || network?.shouldForceOffline} /> @@ -95,6 +95,7 @@ function TestToolMenu({network}: TestToolMenuProps) { accessibilityLabel="Simulate failing network requests" isOn={!!network?.shouldFailAllRequests} onToggle={() => Network.setShouldFailAllRequests(!network?.shouldFailAllRequests)} + disabled={!!network?.shouldForceOffline || network?.shouldSimulatePoorConnection} /> From 2be5a976bb14757089de5438a8c5bd002e107e29 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 11 Dec 2024 12:14:56 +0100 Subject: [PATCH 13/66] Set the correct connection after simulation is turned off --- src/components/TestToolMenu.tsx | 3 +-- src/libs/NetworkConnection.ts | 23 ++++++++++++----------- src/libs/actions/Network.ts | 6 +++++- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/components/TestToolMenu.tsx b/src/components/TestToolMenu.tsx index 5277ba539374..9e438c0b9688 100644 --- a/src/components/TestToolMenu.tsx +++ b/src/components/TestToolMenu.tsx @@ -4,7 +4,6 @@ import {useOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ApiUtils from '@libs/ApiUtils'; -import NetworkConnection from '@libs/NetworkConnection'; import * as Network from '@userActions/Network'; import * as Session from '@userActions/Session'; import * as User from '@userActions/User'; @@ -84,7 +83,7 @@ function TestToolMenu({network}: TestToolMenuProps) { NetworkConnection.simulatePoorConnection(!network?.shouldSimulatePoorConnection, network?.poorConnectionTimeoutID)} + onToggle={() => Network.setShouldSimulatePoorConnection(!network?.shouldSimulatePoorConnection, network?.poorConnectionTimeoutID)} disabled={!!isUsingImportedState || !!network?.shouldFailAllRequests || network?.shouldForceOffline} /> diff --git a/src/libs/NetworkConnection.ts b/src/libs/NetworkConnection.ts index e03f81abc508..69b79502fa17 100644 --- a/src/libs/NetworkConnection.ts +++ b/src/libs/NetworkConnection.ts @@ -83,6 +83,18 @@ Onyx.connect({ setRandomNetworkStatus(true); } + // Stops random network status change when shouldSimulatePoorConnection is turned into false + if (isPoorConnectionSimulated && !network.shouldSimulatePoorConnection) { + NetInfo.fetch().then((state) => { + const isInternetUnreachable = !state.isInternetReachable; + const stringifiedState = JSON.stringify(state); + setOfflineStatus(isInternetUnreachable || !isServerUp, 'NetInfo checked if the internet is reachable'); + Log.info( + `[NetworkStatus] The poor connection simulation mode was turned off. Getting the device network status from NetInfo. Network state: ${stringifiedState}. Setting the offline status to: ${isInternetUnreachable}.`, + ); + }); + } + isPoorConnectionSimulated = !!network.shouldSimulatePoorConnection; connectionChanges = network.connectionChanges; @@ -139,16 +151,6 @@ function setRandomNetworkStatus(initialCall = false) { NetworkActions.setPoorConnectionTimeoutID(timeoutID); } -function simulatePoorConnection(shouldSimulatePoorConnection: boolean, poorConnectionTimeoutID: NodeJS.Timeout | undefined) { - if (!shouldSimulatePoorConnection) { - clearTimeout(poorConnectionTimeoutID); - NetworkActions.setPoorConnectionTimeoutID(undefined); - setOfflineStatus(false); - } - - NetworkActions.setShouldSimulatePoorConnection(shouldSimulatePoorConnection); -} - /** Tracks how many times the connection has changed within the time period */ function trackConnectionChanges() { if (!connectionChanges?.startTime) { @@ -296,6 +298,5 @@ export default { triggerReconnectionCallbacks, recheckNetworkConnection, subscribeToNetInfo, - simulatePoorConnection, }; export type {NetworkStatus}; diff --git a/src/libs/actions/Network.ts b/src/libs/actions/Network.ts index 6d75ec85aa0f..52efb08f5e2e 100644 --- a/src/libs/actions/Network.ts +++ b/src/libs/actions/Network.ts @@ -38,7 +38,11 @@ function setPoorConnectionTimeoutID(poorConnectionTimeoutID: NodeJS.Timeout | un Onyx.merge(ONYXKEYS.NETWORK, {poorConnectionTimeoutID}); } -function setShouldSimulatePoorConnection(shouldSimulatePoorConnection: boolean) { +function setShouldSimulatePoorConnection(shouldSimulatePoorConnection: boolean, poorConnectionTimeoutID: NodeJS.Timeout | undefined) { + if (!shouldSimulatePoorConnection) { + clearTimeout(poorConnectionTimeoutID); + Onyx.merge(ONYXKEYS.NETWORK, {shouldSimulatePoorConnection, poorConnectionTimeoutID: undefined}); + } Onyx.merge(ONYXKEYS.NETWORK, {shouldSimulatePoorConnection}); } From 3e6f41997dcf946d0499c8367ff0e61478193e83 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 11 Dec 2024 12:16:57 +0100 Subject: [PATCH 14/66] Update comment --- src/libs/NetworkConnection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/NetworkConnection.ts b/src/libs/NetworkConnection.ts index 69b79502fa17..1febaee4fb88 100644 --- a/src/libs/NetworkConnection.ts +++ b/src/libs/NetworkConnection.ts @@ -83,7 +83,7 @@ Onyx.connect({ setRandomNetworkStatus(true); } - // Stops random network status change when shouldSimulatePoorConnection is turned into false + // Fetch the NetInfo state to set the correct offline status when shouldSimulatePoorConnection is turned into false if (isPoorConnectionSimulated && !network.shouldSimulatePoorConnection) { NetInfo.fetch().then((state) => { const isInternetUnreachable = !state.isInternetReachable; From 4f0f128e32fd062a14769213aa258b47f325a061 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 11 Dec 2024 12:20:34 +0100 Subject: [PATCH 15/66] Clean up --- src/libs/NetworkConnection.ts | 1 - src/libs/actions/Network.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/libs/NetworkConnection.ts b/src/libs/NetworkConnection.ts index 1febaee4fb88..c5d22e19dc28 100644 --- a/src/libs/NetworkConnection.ts +++ b/src/libs/NetworkConnection.ts @@ -136,7 +136,6 @@ Onyx.connect({ function setRandomNetworkStatus(initialCall = false) { // The check to ensure no new timeouts are scheduled after poor connection simulation is stopped if (!isPoorConnectionSimulated && !initialCall) { - setOfflineStatus(false); return; } diff --git a/src/libs/actions/Network.ts b/src/libs/actions/Network.ts index 52efb08f5e2e..5bfcd411975f 100644 --- a/src/libs/actions/Network.ts +++ b/src/libs/actions/Network.ts @@ -11,7 +11,6 @@ function setIsOffline(isOffline: boolean, reason = '') { textToLog += ` because: ${reason}`; Log.info(textToLog); } - Onyx.merge(ONYXKEYS.NETWORK, {isOffline}); } From e91d5e431a8dd78dfc24d82cf439780138918683 Mon Sep 17 00:00:00 2001 From: cretadn22 Date: Wed, 11 Dec 2024 23:13:00 +0700 Subject: [PATCH 16/66] hide pending transaction --- src/libs/SearchUIUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index c51eadf2c637..41a42968364a 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -294,8 +294,10 @@ function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTr ) { return CONST.SEARCH.ACTION_TYPES.PAY; } + const hasOnlyPendingTransactions = + allReportTransactions.length > 0 && allReportTransactions.every((t) => TransactionUtils.isExpensifyCardTransaction(t) && TransactionUtils.isPending(t)); - if (IOU.canApproveIOU(report, policy) && ReportUtils.isAllowedToApproveExpenseReport(report, undefined, policy)) { + if (IOU.canApproveIOU(report, policy) && ReportUtils.isAllowedToApproveExpenseReport(report, undefined, policy) && !hasOnlyPendingTransactions) { return CONST.SEARCH.ACTION_TYPES.APPROVE; } From e2ed84639d4ea98e89410970c57e8bf1f6431107 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 11 Dec 2024 18:42:03 +0100 Subject: [PATCH 17/66] Applying reviewer feedback --- src/libs/NetworkConnection.ts | 42 ++++++++++++++++++++--------------- src/libs/actions/Network.ts | 1 + 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/libs/NetworkConnection.ts b/src/libs/NetworkConnection.ts index c5d22e19dc28..d5e0b356d7ac 100644 --- a/src/libs/NetworkConnection.ts +++ b/src/libs/NetworkConnection.ts @@ -7,6 +7,7 @@ import type {ValueOf} from 'type-fest'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type Network from '@src/types/onyx/Network'; import type {ConnectionChanges} from '@src/types/onyx/Network'; import * as NetworkActions from './actions/Network'; import AppStateMonitor from './AppStateMonitor'; @@ -76,24 +77,7 @@ Onyx.connect({ return; } - // Starts random network status change when shouldSimulatePoorConnection is turned into true - // or after app restart if shouldSimulatePoorConnection is true already - if (!isPoorConnectionSimulated && !!network.shouldSimulatePoorConnection) { - clearTimeout(network.poorConnectionTimeoutID); - setRandomNetworkStatus(true); - } - - // Fetch the NetInfo state to set the correct offline status when shouldSimulatePoorConnection is turned into false - if (isPoorConnectionSimulated && !network.shouldSimulatePoorConnection) { - NetInfo.fetch().then((state) => { - const isInternetUnreachable = !state.isInternetReachable; - const stringifiedState = JSON.stringify(state); - setOfflineStatus(isInternetUnreachable || !isServerUp, 'NetInfo checked if the internet is reachable'); - Log.info( - `[NetworkStatus] The poor connection simulation mode was turned off. Getting the device network status from NetInfo. Network state: ${stringifiedState}. Setting the offline status to: ${isInternetUnreachable}.`, - ); - }); - } + simulatePoorConnection(network); isPoorConnectionSimulated = !!network.shouldSimulatePoorConnection; connectionChanges = network.connectionChanges; @@ -132,6 +116,28 @@ Onyx.connect({ }, }); +/** Controls poor connection simulation */ +function simulatePoorConnection(network: Network) { + // Starts random network status change when shouldSimulatePoorConnection is turned into true + // or after app restart if shouldSimulatePoorConnection is true already + if (!isPoorConnectionSimulated && !!network.shouldSimulatePoorConnection) { + clearTimeout(network.poorConnectionTimeoutID); + setRandomNetworkStatus(true); + } + + // Fetch the NetInfo state to set the correct offline status when shouldSimulatePoorConnection is turned into false + if (isPoorConnectionSimulated && !network.shouldSimulatePoorConnection) { + NetInfo.fetch().then((state) => { + const isInternetUnreachable = !state.isInternetReachable; + const stringifiedState = JSON.stringify(state); + setOfflineStatus(isInternetUnreachable || !isServerUp, 'NetInfo checked if the internet is reachable'); + Log.info( + `[NetworkStatus] The poor connection simulation mode was turned off. Getting the device network status from NetInfo. Network state: ${stringifiedState}. Setting the offline status to: ${isInternetUnreachable}.`, + ); + }); + } +} + /** Sets online/offline connection randomly every 2-5 seconds */ function setRandomNetworkStatus(initialCall = false) { // The check to ensure no new timeouts are scheduled after poor connection simulation is stopped diff --git a/src/libs/actions/Network.ts b/src/libs/actions/Network.ts index 5bfcd411975f..f2228a008dad 100644 --- a/src/libs/actions/Network.ts +++ b/src/libs/actions/Network.ts @@ -41,6 +41,7 @@ function setShouldSimulatePoorConnection(shouldSimulatePoorConnection: boolean, if (!shouldSimulatePoorConnection) { clearTimeout(poorConnectionTimeoutID); Onyx.merge(ONYXKEYS.NETWORK, {shouldSimulatePoorConnection, poorConnectionTimeoutID: undefined}); + return; } Onyx.merge(ONYXKEYS.NETWORK, {shouldSimulatePoorConnection}); } From fee0ad557bad82dbd1d25c751a5d5e85a3e17293 Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Thu, 12 Dec 2024 05:42:22 +0000 Subject: [PATCH 18/66] Update BOFA and Capital One images Signed-off-by: GitHub --- assets/images/companyCards/card-bofa.svg | 28 ++++++++++++++++++- .../images/companyCards/card-capital_one.svg | 24 +++++++++++++++- .../companyCards/large/card-bofa-large.svg | 22 ++++++--------- .../large/card-capital_one-large.svg | 12 ++++---- 4 files changed, 65 insertions(+), 21 deletions(-) diff --git a/assets/images/companyCards/card-bofa.svg b/assets/images/companyCards/card-bofa.svg index 3cc7cf1de2cc..c58229f1b242 100644 --- a/assets/images/companyCards/card-bofa.svg +++ b/assets/images/companyCards/card-bofa.svg @@ -1 +1,27 @@ - \ No newline at end of file + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/companyCards/card-capital_one.svg b/assets/images/companyCards/card-capital_one.svg index 64e79b8745db..9f1402298683 100644 --- a/assets/images/companyCards/card-capital_one.svg +++ b/assets/images/companyCards/card-capital_one.svg @@ -1 +1,23 @@ - \ No newline at end of file + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/companyCards/large/card-bofa-large.svg b/assets/images/companyCards/large/card-bofa-large.svg index a842bc93d80b..c83e06ffb65d 100644 --- a/assets/images/companyCards/large/card-bofa-large.svg +++ b/assets/images/companyCards/large/card-bofa-large.svg @@ -1,6 +1,6 @@ - + - - - - - - - + + + + + + + \ No newline at end of file diff --git a/assets/images/companyCards/large/card-capital_one-large.svg b/assets/images/companyCards/large/card-capital_one-large.svg index b71e209a4c11..20f3bd442d9e 100644 --- a/assets/images/companyCards/large/card-capital_one-large.svg +++ b/assets/images/companyCards/large/card-capital_one-large.svg @@ -1,15 +1,15 @@ - + - - + + \ No newline at end of file From 60606f69bfbe432020be4675c6c92cd5cb16ce94 Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Thu, 12 Dec 2024 05:43:57 +0000 Subject: [PATCH 19/66] Push correct image Signed-off-by: GitHub --- .../images/companyCards/card-capital_one.svg | 24 +------------------ .../images/companyCards/card-capitalone.svg | 24 ++++++++++++++++++- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/assets/images/companyCards/card-capital_one.svg b/assets/images/companyCards/card-capital_one.svg index 9f1402298683..64e79b8745db 100644 --- a/assets/images/companyCards/card-capital_one.svg +++ b/assets/images/companyCards/card-capital_one.svg @@ -1,23 +1 @@ - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/companyCards/card-capitalone.svg b/assets/images/companyCards/card-capitalone.svg index a7c54c7bf529..9f1402298683 100644 --- a/assets/images/companyCards/card-capitalone.svg +++ b/assets/images/companyCards/card-capitalone.svg @@ -1 +1,23 @@ - \ No newline at end of file + + + + + + + + + + \ No newline at end of file From b07ab52309648937174f4912bac4a26c4c04677b Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 12 Dec 2024 14:09:43 +0700 Subject: [PATCH 20/66] Move to Const.ts --- src/CONST.ts | 9 ++++++++- src/libs/actions/Link.ts | 4 +--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 15554719ca9d..0689a656f428 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -949,7 +949,14 @@ const CONST = { EMPLOYEE_TOUR_PRODUCTION: 'https://expensify.navattic.com/35609gb', EMPLOYEE_TOUR_STAGING: 'https://expensify.navattic.com/cf15002s', }, - + OLD_DOT_PUBLIC_URLS: { + TERMS_URL: `${EXPENSIFY_URL}/terms`, + PRIVACY_URL: `${EXPENSIFY_URL}/privacy`, + LICENSES_URL: `${USE_EXPENSIFY_URL}/licenses`, + ACH_TERMS_URL: `${EXPENSIFY_URL}/achterms`, + WALLET_AGREEMENT_URL: `${EXPENSIFY_URL}/expensify-payments-wallet-terms-of-service`, + BANCORP_WALLET_AGREEMENT_URL: `${EXPENSIFY_URL}/bancorp-bank-wallet-terms-of-service`, + }, OLDDOT_URLS: { ADMIN_POLICIES_URL: 'admin_policies', ADMIN_DOMAINS_URL: 'admin_domains', diff --git a/src/libs/actions/Link.ts b/src/libs/actions/Link.ts index 8d367c7eeec2..cee639f4c4a3 100644 --- a/src/libs/actions/Link.ts +++ b/src/libs/actions/Link.ts @@ -183,11 +183,9 @@ function openLink(href: string, environmentURL: string, isAttachment = false) { Navigation.navigate(internalNewExpensifyPath as Route); return; } - const OLD_DOT_PUBLIC_URLS: string[] = [CONST.TERMS_URL, CONST.PRIVACY_URL]; - // If we are handling an old dot Expensify link we need to open it with openOldDotLink() so we can navigate to it with the user already logged in. // As attachments also use expensify.com we don't want it working the same as links. - const isPublicOldDotURL = OLD_DOT_PUBLIC_URLS.includes(href); + const isPublicOldDotURL = (Object.values(CONST.OLD_DOT_PUBLIC_URLS) as string[]).includes(href); if (internalExpensifyPath && !isAttachment && !isPublicOldDotURL) { openOldDotLink(internalExpensifyPath); return; From 9ddd2ca5df994e8955790a4d36c327fb0820c981 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Thu, 12 Dec 2024 15:46:09 +0700 Subject: [PATCH 21/66] fix test tool text is cropped --- src/styles/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/index.ts b/src/styles/index.ts index a09796000b36..9468a7af4da2 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -611,7 +611,7 @@ const styles = (theme: ThemeColors) => ...flex.justifyContentBetween, ...flex.alignItemsCenter, ...sizing.mnw120, - height: 64, + minHeight: 64, }, buttonSmall: { From 2a8952de6fc7cc4a0d0b406ae3a044136df2e9de Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 12 Dec 2024 12:27:25 +0100 Subject: [PATCH 22/66] fix: revert removing clearIssueNewCardFlow from Confirmation step and assignee step, remove the one in the IssueNewCardPage --- src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx | 1 + .../workspace/expensifyCard/issueNew/ConfirmationStep.tsx | 1 + .../workspace/expensifyCard/issueNew/IssueNewCardPage.tsx | 6 ------ 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx index a1328645d447..769532e49351 100644 --- a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx @@ -62,6 +62,7 @@ function AssigneeStep({policy}: AssigneeStepProps) { return; } Navigation.goBack(); + Card.clearIssueNewCardFlow(); }; const shouldShowSearchInput = policy?.employeeList && Object.keys(policy.employeeList).length >= MINIMUM_MEMBER_TO_SHOW_SEARCH; diff --git a/src/pages/workspace/expensifyCard/issueNew/ConfirmationStep.tsx b/src/pages/workspace/expensifyCard/issueNew/ConfirmationStep.tsx index 8521da690fe2..e0db6935b0a1 100644 --- a/src/pages/workspace/expensifyCard/issueNew/ConfirmationStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/ConfirmationStep.tsx @@ -58,6 +58,7 @@ function ConfirmationStep({policyID, backTo}: ConfirmationStepProps) { return; } Navigation.navigate(backTo ?? ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policyID ?? '-1')); + Card.clearIssueNewCardFlow(); }, [backTo, policyID, isSuccessful]); const submit = (validateCode: string) => { diff --git a/src/pages/workspace/expensifyCard/issueNew/IssueNewCardPage.tsx b/src/pages/workspace/expensifyCard/issueNew/IssueNewCardPage.tsx index 2c9fbd1d193e..27653df9f9b0 100644 --- a/src/pages/workspace/expensifyCard/issueNew/IssueNewCardPage.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/IssueNewCardPage.tsx @@ -34,12 +34,6 @@ function IssueNewCardPage({policy, route}: IssueNewCardPageProps) { Card.startIssueNewCardFlow(policyID); }, [policyID]); - useEffect(() => { - return () => { - Card.clearIssueNewCardFlow(); - }; - }, []); - const getCurrentStep = () => { switch (currentStep) { case CONST.EXPENSIFY_CARD.STEP.ASSIGNEE: From a2aeb0fe759c6cd55a5a7c894cba21cde9d01f6f Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 12 Dec 2024 13:49:05 +0100 Subject: [PATCH 23/66] use reportName for invoice report --- src/libs/ReportUtils.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a30de7b97198..0b848eb06cbd 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4066,11 +4066,7 @@ function getReportName( } if (isInvoiceReport(report)) { - if (!isInvoiceRoom(getReport(report?.chatReportID ?? ''))) { - return report?.reportName ?? getMoneyRequestReportName(report, policy, invoiceReceiverPolicy); - } - - formattedName = getMoneyRequestReportName(report, policy, invoiceReceiverPolicy); + formattedName = report?.reportName ?? getMoneyRequestReportName(report, policy, invoiceReceiverPolicy); } if (isInvoiceRoom(report)) { From 69ed2acbc13372e4cecc607a5af0f419cce578e5 Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Thu, 12 Dec 2024 15:06:58 -0800 Subject: [PATCH 24/66] QBD Setup Flow UI Improvement --- .../qbd/QuickBooksDesktopSetupPage.tsx | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/pages/workspace/accounting/qbd/QuickBooksDesktopSetupPage.tsx b/src/pages/workspace/accounting/qbd/QuickBooksDesktopSetupPage.tsx index 5e90c3db4ed1..a71d73798758 100644 --- a/src/pages/workspace/accounting/qbd/QuickBooksDesktopSetupPage.tsx +++ b/src/pages/workspace/accounting/qbd/QuickBooksDesktopSetupPage.tsx @@ -1,11 +1,10 @@ import React, {useCallback, useEffect, useState} from 'react'; -import {View} from 'react-native'; +import {ActivityIndicator, View} from 'react-native'; import Computer from '@assets/images/laptop-with-second-screen-sync.svg'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import Button from '@components/Button'; import CopyTextToClipboard from '@components/CopyTextToClipboard'; import FixedFooter from '@components/FixedFooter'; -import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Icon from '@components/Icon'; import * as Illustrations from '@components/Icon/Illustrations'; @@ -16,6 +15,7 @@ import TextLink from '@components/TextLink'; import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {setConnectionError} from '@libs/actions/connections'; import * as QuickbooksDesktop from '@libs/actions/connections/QuickbooksDesktop'; @@ -31,10 +31,10 @@ type RequireQuickBooksDesktopModalProps = PlatformStackScreenProps(''); const hasResultOfFetchingSetupLink = !!codatSetupLink || hasError; @@ -42,20 +42,19 @@ function RequireQuickBooksDesktopModal({route}: RequireQuickBooksDesktopModalPro const ContentWrapper = hasResultOfFetchingSetupLink ? ({children}: React.PropsWithChildren) => children : FullPageOfflineBlockingView; const fetchSetupLink = useCallback(() => { - setIsLoading(true); setHasError(false); // eslint-disable-next-line rulesdir/no-thenable-actions-in-views QuickbooksDesktop.getQuickbooksDesktopCodatSetupLink(policyID).then((response) => { - if (response?.jsonCode) { - if (response.jsonCode === CONST.JSON_CODE.SUCCESS) { - setCodatSetupLink(String(response?.setupUrl ?? '')); - } else { - setConnectionError(policyID, CONST.POLICY.CONNECTIONS.NAME.QBD, translate('workspace.qbd.setupPage.setupErrorTitle')); - setHasError(true); - } + if (!response?.jsonCode) { + return; } - setIsLoading(false); + if (response.jsonCode === CONST.JSON_CODE.SUCCESS) { + setCodatSetupLink(String(response?.setupUrl ?? '')); + } else { + setConnectionError(policyID, CONST.POLICY.CONNECTIONS.NAME.QBD, translate('workspace.qbd.setupPage.setupErrorTitle')); + setHasError(true); + } }); }, [policyID, translate]); @@ -77,8 +76,7 @@ function RequireQuickBooksDesktopModal({route}: RequireQuickBooksDesktopModalPro }, }); - const shouldShowLoading = isLoading || !hasResultOfFetchingSetupLink; - const shouldShowError = !shouldShowLoading && hasError; + const shouldShowError = hasError; return ( Navigation.dismissModal()} /> - {shouldShowLoading && } {shouldShowError && ( )} - {!shouldShowLoading && !shouldShowError && ( + {!shouldShowError && ( @@ -122,10 +119,17 @@ function RequireQuickBooksDesktopModal({route}: RequireQuickBooksDesktopModalPro {translate('workspace.qbd.setupPage.title')} {translate('workspace.qbd.setupPage.body')} - + {!hasResultOfFetchingSetupLink ? ( + + ) : ( + + )}