diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml index 60ba16164eb6..04fdb7e80d6a 100644 --- a/.github/workflows/e2ePerformanceTests.yml +++ b/.github/workflows/e2ePerformanceTests.yml @@ -205,22 +205,61 @@ jobs: cat ./mainResults/Host_Machine_Files/\$WORKING_DIRECTORY/debug.log || true cat "./mainResults/Host_Machine_Files/\$WORKING_DIRECTORY/Test spec output.txt" || true + - name: Announce failed workflow in Slack + if: failure() + uses: 8398a7/action-slack@v3 + with: + status: custom + custom_payload: | + { + channel: '#expensify-margelo', + attachments: [{ + color: 'danger', + text: `💥 ${process.env.AS_REPO} E2E Test run failed failed on workflow 💥`, + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + - name: Unzip AWS Device Farm results - if: ${{ always() }} + if: always() run: unzip "Customer Artifacts.zip" - name: Print AWS Device Farm run results - if: ${{ always() }} + if: always() run: cat "./Host_Machine_Files/\$WORKING_DIRECTORY/output.md" - name: Check if test failed, if so post the results and add the DeployBlocker label + id: checkIfRegressionDetected run: | if grep -q '🔴' ./output.md; then + # Create an output to the GH action that the test failed: + echo "performanceRegressionDetected=true" >> "$GITHUB_OUTPUT" + gh pr edit ${{ inputs.PR_NUMBER }} --add-label DeployBlockerCash gh pr comment ${{ inputs.PR_NUMBER }} -F ./output.md gh pr comment ${{ inputs.PR_NUMBER }} -b "@Expensify/mobile-deployers 📣 Please look into this performance regression as it's a deploy blocker." else + echo "performanceRegressionDetected=false" >> "$GITHUB_OUTPUT" echo '✅ no performance regression detected' fi env: GITHUB_TOKEN: ${{ github.token }} + + - name: 'Announce regression in Slack' + if: ${{ steps.checkIfRegressionDetected.outputs.performanceRegressionDetected == 'true' }} + uses: 8398a7/action-slack@v3 + with: + status: custom + custom_payload: | + { + channel: '#newdot-performance', + attachments: [{ + color: 'danger', + text: `🔴 Performance regression detected in PR ${{ inputs.PR_NUMBER }}\nDetected in workflow.`, + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/failureNotifier.yml b/.github/workflows/failureNotifier.yml new file mode 100644 index 000000000000..d2e0ec4f38e5 --- /dev/null +++ b/.github/workflows/failureNotifier.yml @@ -0,0 +1,96 @@ +name: Notify on Workflow Failure + +on: + workflow_run: + workflows: ["Process new code merged to main"] + types: + - completed + +permissions: + issues: write + +jobs: + notifyFailure: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'failure' }} + steps: + - name: Fetch Workflow Run Jobs + id: fetch-workflow-jobs + uses: actions/github-script@v7 + with: + script: | + const runId = "${{ github.event.workflow_run.id }}"; + const jobsData = await github.rest.actions.listJobsForWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId, + }); + return jobsData.data; + + - name: Process Each Failed Job + uses: actions/github-script@v7 + with: + script: | + const jobs = ${{ steps.fetch-workflow-jobs.outputs.result }}; + + const headCommit = "${{ github.event.workflow_run.head_commit.id }}"; + const prData = await github.rest.repos.listPullRequestsAssociatedWithCommit({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: headCommit, + }); + + const pr = prData.data[0]; + const prLink = pr.html_url; + const prAuthor = pr.user.login; + const prMerger = "${{ github.event.workflow_run.actor.login }}"; + + const failureLabel = 'Workflow Failure'; + for (let i = 0; i < jobs.total_count; i++) { + if (jobs.jobs[i].conclusion == 'failure') { + const jobName = jobs.jobs[i].name; + const jobLink = jobs.jobs[i].html_url; + const issues = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + labels: failureLabel, + state: 'open' + }); + const existingIssue = issues.data.find(issue => issue.title.includes(jobName)); + if (!existingIssue) { + const annotations = await github.rest.checks.listAnnotations({ + owner: context.repo.owner, + repo: context.repo.repo, + check_run_id: jobs.jobs[i].id, + }); + let errorMessage = ""; + for(let j = 0; j < annotations.data.length; j++) { + errorMessage += annotations.data[j].annotation_level + ": "; + errorMessage += annotations.data[j].message + "\n"; + } + const issueTitle = `Investigate workflow job failing on main: ${ jobName }`; + const issueBody = `🚨 **Failure Summary** 🚨:\n\n` + + `- **📋 Job Name**: [${ jobName }](${ jobLink })\n` + + `- **🔧 Failure in Workflow**: Process new code merged to main\n` + + `- **🔗 Triggered by PR**: [PR Link](${ prLink })\n` + + `- **👤 PR Author**: @${ prAuthor }\n` + + `- **🤝 Merged by**: @${ prMerger }\n` + + `- **🐛 Error Message**: \n ${errorMessage}\n\n` + + `⚠️ **Action Required** ⚠️:\n\n` + + `🛠️ A recent merge appears to have caused a failure in the job named [${ jobName }](${ jobLink }).\n` + + `This issue has been automatically created and labeled with \`${ failureLabel }\` for investigation. \n\n` + + `👀 **Please look into the following**:\n` + + `1. **Why the PR caused the job to fail?**\n` + + `2. **Address any underlying issues.**\n\n` + + `🐛 We appreciate your help in squashing this bug!`; + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: issueTitle, + body: issueBody, + labels: [failureLabel, 'Daily'], + assignees: [prMerger] + }); + } + } + } diff --git a/.github/workflows/preDeploy.yml b/.github/workflows/preDeploy.yml index 8f9512062e9d..f09865de0194 100644 --- a/.github/workflows/preDeploy.yml +++ b/.github/workflows/preDeploy.yml @@ -1,3 +1,4 @@ +# Reminder: If this workflow's name changes, update the name in the dependent workflow at .github/workflows/failureNotifier.yml. name: Process new code merged to main on: diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js index 6717c1736f65..204f70344b18 100644 --- a/.storybook/webpack.config.js +++ b/.storybook/webpack.config.js @@ -25,7 +25,7 @@ module.exports = ({config}) => { config.resolve.alias = { 'react-native-config': 'react-web-config', 'react-native$': 'react-native-web', - '@react-native-community/netinfo': path.resolve(__dirname, '../__mocks__/@react-native-community/netinfo.js'), + '@react-native-community/netinfo': path.resolve(__dirname, '../__mocks__/@react-native-community/netinfo.ts'), '@react-navigation/native': path.resolve(__dirname, '../__mocks__/@react-navigation/native'), // Module alias support for storybook files, coping from `webpack.common.js` diff --git a/__mocks__/@react-native-community/netinfo.js b/__mocks__/@react-native-community/netinfo.js deleted file mode 100644 index 53a9282ea8db..000000000000 --- a/__mocks__/@react-native-community/netinfo.js +++ /dev/null @@ -1,19 +0,0 @@ -const defaultState = { - type: 'cellular', - isConnected: true, - isInternetReachable: true, - details: { - isConnectionExpensive: true, - cellularGeneration: '3g', - }, -}; - -const RNCNetInfoMock = { - configure: () => {}, - fetch: () => Promise.resolve(defaultState), - refresh: () => Promise.resolve(defaultState), - addEventListener: () => () => {}, - useNetInfo: () => {}, -}; - -export default RNCNetInfoMock; diff --git a/__mocks__/@react-native-community/netinfo.ts b/__mocks__/@react-native-community/netinfo.ts new file mode 100644 index 000000000000..0b7bdc9010a3 --- /dev/null +++ b/__mocks__/@react-native-community/netinfo.ts @@ -0,0 +1,31 @@ +import {NetInfoCellularGeneration, NetInfoStateType} from '@react-native-community/netinfo'; +import type {addEventListener, configure, fetch, NetInfoState, refresh, useNetInfo} from '@react-native-community/netinfo'; + +const defaultState: NetInfoState = { + type: NetInfoStateType.cellular, + isConnected: true, + isInternetReachable: true, + details: { + isConnectionExpensive: true, + cellularGeneration: NetInfoCellularGeneration['3g'], + carrier: 'T-Mobile', + }, +}; + +type NetInfoMock = { + configure: typeof configure; + fetch: typeof fetch; + refresh: typeof refresh; + addEventListener: typeof addEventListener; + useNetInfo: typeof useNetInfo; +}; + +const netInfoMock: NetInfoMock = { + configure: () => {}, + fetch: () => Promise.resolve(defaultState), + refresh: () => Promise.resolve(defaultState), + addEventListener: () => () => {}, + useNetInfo: () => defaultState, +}; + +export default netInfoMock; diff --git a/__mocks__/@react-native-firebase/crashlytics.js b/__mocks__/@react-native-firebase/crashlytics.js deleted file mode 100644 index cc7ff3f55e4a..000000000000 --- a/__mocks__/@react-native-firebase/crashlytics.js +++ /dev/null @@ -1,7 +0,0 @@ -// uses and we need to mock the imported crashlytics module -// due to an error that happens otherwise https://github.com/invertase/react-native-firebase/issues/2475 -export default () => ({ - log: jest.fn(), - recordError: jest.fn(), - setCrashlyticsCollectionEnabled: jest.fn(), -}); diff --git a/__mocks__/@react-native-firebase/crashlytics.ts b/__mocks__/@react-native-firebase/crashlytics.ts new file mode 100644 index 000000000000..2df845ba0c69 --- /dev/null +++ b/__mocks__/@react-native-firebase/crashlytics.ts @@ -0,0 +1,15 @@ +import type {FirebaseCrashlyticsTypes} from '@react-native-firebase/crashlytics'; + +type CrashlyticsModule = Pick; + +type CrashlyticsMock = () => CrashlyticsModule; + +// uses and we need to mock the imported crashlytics module +// due to an error that happens otherwise https://github.com/invertase/react-native-firebase/issues/2475 +const crashlyticsMock: CrashlyticsMock = () => ({ + log: jest.fn(), + recordError: jest.fn(), + setCrashlyticsCollectionEnabled: jest.fn(), +}); + +export default crashlyticsMock; diff --git a/__mocks__/@react-native-firebase/perf.ts b/__mocks__/@react-native-firebase/perf.ts new file mode 100644 index 000000000000..e304b1a1f007 --- /dev/null +++ b/__mocks__/@react-native-firebase/perf.ts @@ -0,0 +1,5 @@ +type PerfMock = () => void; + +const perfMock: PerfMock = () => {}; + +export default perfMock; diff --git a/__mocks__/react-freeze.js b/__mocks__/react-freeze.js deleted file mode 100644 index 51294f40f9ca..000000000000 --- a/__mocks__/react-freeze.js +++ /dev/null @@ -1,6 +0,0 @@ -const Freeze = (props) => props.children; - -export { - // eslint-disable-next-line import/prefer-default-export - Freeze, -}; diff --git a/__mocks__/react-freeze.ts b/__mocks__/react-freeze.ts new file mode 100644 index 000000000000..d87abe01acfb --- /dev/null +++ b/__mocks__/react-freeze.ts @@ -0,0 +1,8 @@ +import type {Freeze as FreezeComponent} from 'react-freeze'; + +const Freeze: typeof FreezeComponent = (props) => props.children as JSX.Element; + +export { + // eslint-disable-next-line import/prefer-default-export + Freeze, +}; diff --git a/__mocks__/react-native-blob-util.js b/__mocks__/react-native-blob-util.js deleted file mode 100644 index ff8b4c56321a..000000000000 --- a/__mocks__/react-native-blob-util.js +++ /dev/null @@ -1 +0,0 @@ -export default {}; diff --git a/__mocks__/react-native-blob-util.ts b/__mocks__/react-native-blob-util.ts new file mode 100644 index 000000000000..bdaa0ae3ded5 --- /dev/null +++ b/__mocks__/react-native-blob-util.ts @@ -0,0 +1,5 @@ +import type RNFetchBlob from 'react-native-blob-util'; + +const ReactNativeBlobUtilMock: Partial = {}; + +export default ReactNativeBlobUtilMock; diff --git a/__mocks__/react-native-dev-menu.js b/__mocks__/react-native-dev-menu.js deleted file mode 100644 index 49cb4c61a209..000000000000 --- a/__mocks__/react-native-dev-menu.js +++ /dev/null @@ -1,3 +0,0 @@ -export default { - addItem: jest.fn(), -}; diff --git a/__mocks__/react-native-dev-menu.ts b/__mocks__/react-native-dev-menu.ts new file mode 100644 index 000000000000..0d35d5c32723 --- /dev/null +++ b/__mocks__/react-native-dev-menu.ts @@ -0,0 +1,11 @@ +import type {addItem} from 'react-native-dev-menu'; + +type ReactNativeDevMenuMock = { + addItem: typeof addItem; +}; + +const reactNativeDevMenuMock: ReactNativeDevMenuMock = { + addItem: jest.fn(), +}; + +export default reactNativeDevMenuMock; diff --git a/__mocks__/react-native-device-info.js b/__mocks__/react-native-device-info.js deleted file mode 100644 index 2ba5b6ef85b3..000000000000 --- a/__mocks__/react-native-device-info.js +++ /dev/null @@ -1,3 +0,0 @@ -import MockDeviceInfo from 'react-native-device-info/jest/react-native-device-info-mock'; - -export default MockDeviceInfo; diff --git a/__mocks__/react-native-device-info.ts b/__mocks__/react-native-device-info.ts new file mode 100644 index 000000000000..bb04f1a176b2 --- /dev/null +++ b/__mocks__/react-native-device-info.ts @@ -0,0 +1,6 @@ +import type DeviceInfoModule from 'react-native-device-info'; +import MockDeviceInfo from 'react-native-device-info/jest/react-native-device-info-mock'; + +const DeviceInfo: typeof DeviceInfoModule = MockDeviceInfo; + +export default DeviceInfo; diff --git a/__mocks__/react-native-localize.js b/__mocks__/react-native-localize.js deleted file mode 100644 index 8d302b7d598b..000000000000 --- a/__mocks__/react-native-localize.js +++ /dev/null @@ -1,3 +0,0 @@ -const mockRNLocalize = require('react-native-localize/mock'); - -module.exports = mockRNLocalize; diff --git a/__mocks__/react-native-localize.ts b/__mocks__/react-native-localize.ts new file mode 100644 index 000000000000..aa0322d6714c --- /dev/null +++ b/__mocks__/react-native-localize.ts @@ -0,0 +1,3 @@ +import mockRNLocalize from 'react-native-localize/mock'; + +module.exports = mockRNLocalize; diff --git a/__mocks__/react-native-pdf.js b/__mocks__/react-native-pdf.js deleted file mode 100644 index 4d179e730903..000000000000 --- a/__mocks__/react-native-pdf.js +++ /dev/null @@ -1,4 +0,0 @@ -export default { - DocumentDir: jest.fn(), - ImageCache: jest.fn(), -}; diff --git a/__mocks__/react-native-reanimated.js b/__mocks__/react-native-reanimated.js deleted file mode 100644 index dfd96838caa7..000000000000 --- a/__mocks__/react-native-reanimated.js +++ /dev/null @@ -1,7 +0,0 @@ -// This mock is required as per setup instructions for react-navigation testing -// https://reactnavigation.org/docs/testing/#mocking-native-modules - -const Reanimated = require('react-native-reanimated/mock'); - -Reanimated.default.call = () => {}; -module.exports = Reanimated; diff --git a/android/app/build.gradle b/android/app/build.gradle index 6ed326f9dee1..ed7b2f568b93 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001043800 - versionName "1.4.38-0" + versionCode 1001043907 + versionName "1.4.39-7" } flavorDimensions "default" diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomAirshipExtender.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomAirshipExtender.java index 0cae0bd2de6d..86529e19401e 100644 --- a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomAirshipExtender.java +++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomAirshipExtender.java @@ -14,8 +14,5 @@ public void onAirshipReady(@NonNull Context context, @NonNull UAirship airship) CustomNotificationProvider notificationProvider = new CustomNotificationProvider(context, airship.getAirshipConfigOptions()); pushManager.setNotificationProvider(notificationProvider); - - NotificationListener notificationListener = airship.getPushManager().getNotificationListener(); - pushManager.setNotificationListener(new CustomNotificationListener(notificationListener, notificationProvider)); } } diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationListener.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationListener.java deleted file mode 100644 index 8149ca118d58..000000000000 --- a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationListener.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.expensify.chat.customairshipextender; - -import androidx.annotation.NonNull; -import com.urbanairship.push.NotificationActionButtonInfo; -import com.urbanairship.push.NotificationInfo; -import com.urbanairship.push.NotificationListener; -import com.urbanairship.push.PushMessage; -import org.jetbrains.annotations.NotNull; - -/** - * Allows us to clear the notification cache when the user dismisses a notification. - */ -public class CustomNotificationListener implements NotificationListener { - private final NotificationListener parent; - private final CustomNotificationProvider provider; - - CustomNotificationListener(NotificationListener parent, CustomNotificationProvider provider) { - this.parent = parent; - this.provider = provider; - } - - @Override - public void onNotificationPosted(@NonNull @NotNull NotificationInfo notificationInfo) { - parent.onNotificationPosted(notificationInfo); - } - - @Override - public boolean onNotificationOpened(@NonNull @NotNull NotificationInfo notificationInfo) { - // The notification is also dismissed when it's tapped so handle that as well - PushMessage message = notificationInfo.getMessage(); - provider.onDismissNotification(message); - - return parent.onNotificationOpened(notificationInfo); - } - - @Override - public boolean onNotificationForegroundAction(@NonNull @NotNull NotificationInfo notificationInfo, @NonNull @NotNull NotificationActionButtonInfo actionButtonInfo) { - return parent.onNotificationForegroundAction(notificationInfo, actionButtonInfo); - } - - @Override - public void onNotificationBackgroundAction(@NonNull @NotNull NotificationInfo notificationInfo, @NonNull @NotNull NotificationActionButtonInfo actionButtonInfo) { - parent.onNotificationBackgroundAction(notificationInfo, actionButtonInfo); - } - - @Override - public void onNotificationDismissed(@NonNull @NotNull NotificationInfo notificationInfo) { - parent.onNotificationDismissed(notificationInfo); - - PushMessage message = notificationInfo.getMessage(); - provider.onDismissNotification(message); - } -} diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java index c60476ad3f0a..c57819d19d03 100644 --- a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java @@ -3,6 +3,7 @@ import static androidx.core.app.NotificationCompat.CATEGORY_MESSAGE; import static androidx.core.app.NotificationCompat.PRIORITY_MAX; +import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.app.NotificationManager; @@ -15,6 +16,8 @@ import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.os.Build; +import android.os.Bundle; +import android.service.notification.StatusBarNotification; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; @@ -26,6 +29,7 @@ import androidx.core.app.NotificationManagerCompat; import androidx.core.app.Person; import androidx.core.graphics.drawable.IconCompat; +import androidx.versionedparcelable.ParcelUtils; import com.urbanairship.AirshipConfigOptions; import com.urbanairship.json.JsonMap; @@ -40,18 +44,14 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; -import java.util.HashMap; +import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import com.expensify.chat.customairshipextender.NotificationCache.NotificationData; -import com.expensify.chat.customairshipextender.NotificationCache.NotificationMessage; - public class CustomNotificationProvider extends ReactNotificationProvider { // Resize icons to 100 dp x 100 dp private static final int MAX_ICON_SIZE_DPS = 100; @@ -73,6 +73,13 @@ public class CustomNotificationProvider extends ReactNotificationProvider { private static final String PAYLOAD_KEY = "payload"; private static final String ONYX_DATA_KEY = "onyxData"; + // Notification extras keys + public static final String EXTRAS_REPORT_ID_KEY = "reportID"; + public static final String EXTRAS_AVATAR_KEY = "avatar"; + public static final String EXTRAS_NAME_KEY = "name"; + public static final String EXTRAS_ACCOUNT_ID_KEY = "accountID"; + + private final ExecutorService executorService = Executors.newCachedThreadPool(); public CustomNotificationProvider(@NonNull Context context, @NonNull AirshipConfigOptions configOptions) { @@ -158,7 +165,7 @@ public Bitmap getCroppedBitmap(Bitmap bitmap) { /** * Applies the message style to the notification builder. It also takes advantage of the - * notification cache to build conversations style notifications. + * android notification API to build conversations style notifications. * * @param builder Notification builder that will receive the message style * @param payload Notification payload, which contains all the data we need to build the notifications. @@ -170,10 +177,9 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil return; } - // Retrieve and check for cached notifications - NotificationData notificationData = NotificationCache.getNotificationData(reportID); - boolean hasExistingNotification = notificationData.messages.size() >= 1; - + // Retrieve and check for existing notifications + StatusBarNotification existingReportNotification = getActiveNotificationByReportId(context, reportID); + boolean hasExistingNotification = existingReportNotification != null; try { JsonMap reportMap = payload.get(ONYX_DATA_KEY).getList().get(1).getMap().get("value").getMap(); String reportId = reportMap.keySet().iterator().next(); @@ -187,31 +193,15 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil String message = alert != null ? alert : messageData.get("message").getList().get(0).getMap().get("text").getString(); String conversationName = payload.get("roomName") == null ? "" : payload.get("roomName").getString(""); - // Retrieve or create the Person object who sent the latest report comment - Person person = notificationData.getPerson(accountID); - Bitmap personIcon = notificationData.getIcon(accountID); - - if (personIcon == null) { - personIcon = fetchIcon(context, avatar); - } + // create the Person object who sent the latest report comment + Bitmap personIcon = fetchIcon(context, avatar); builder.setLargeIcon(personIcon); - // Persist the person and icon to the notification cache - if (person == null) { - IconCompat iconCompat = IconCompat.createWithBitmap(personIcon); - person = new Person.Builder() - .setIcon(iconCompat) - .setKey(accountID) - .setName(name) - .build(); - - notificationData.putPerson(accountID, name, personIcon); - } + Person person = createMessagePersonObject(IconCompat.createWithBitmap(personIcon), accountID, name); - // Despite not using conversation style for the initial notification from each chat, we need to cache it to enable conversation style for future notifications + // Create latest received message object long createdTimeInMillis = getMessageTimeInMillis(messageData.get("created").getString("")); - notificationData.messages.add(new NotificationMessage(accountID, message, createdTimeInMillis)); - + NotificationCompat.MessagingStyle.Message newMessage = new NotificationCompat.MessagingStyle.Message(message, createdTimeInMillis, person); // Conversational styling should be applied to groups chats, rooms, and any 1:1 chats with more than one notification (ensuring the large profile image is always shown) if (!conversationName.isEmpty() || hasExistingNotification) { @@ -220,30 +210,77 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil .setGroupConversation(true) .setConversationTitle(conversationName); + // Add all conversation messages to the notification, including the last one we just received. - for (NotificationMessage cachedMessage : notificationData.messages) { - messagingStyle.addMessage(cachedMessage.text, cachedMessage.time, notificationData.getPerson(cachedMessage.accountID)); + List messages; + if (hasExistingNotification) { + NotificationCompat.MessagingStyle previousStyle = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(existingReportNotification.getNotification()); + messages = previousStyle != null ? previousStyle.getMessages() : new ArrayList<>(List.of(recreatePreviousMessage(existingReportNotification))); + } else { + messages = new ArrayList<>(); + } + + // add the last one message we just received. + messages.add(newMessage); + + for (NotificationCompat.MessagingStyle.Message activeMessage : messages) { + messagingStyle.addMessage(activeMessage); } + builder.setStyle(messagingStyle); } + // save reportID and person info for future merging + builder.addExtras(createMessageExtrasBundle(reportID, person)); + // Clear the previous notification associated to this conversation so it looks like we are // replacing them with this new one we just built. - if (notificationData.prevNotificationID != -1) { - NotificationManagerCompat.from(context).cancel(notificationData.prevNotificationID); + if (hasExistingNotification) { + int previousNotificationID = existingReportNotification.getId(); + NotificationManagerCompat.from(context).cancel(previousNotificationID); } } catch (Exception e) { e.printStackTrace(); } + } + + private Person createMessagePersonObject (IconCompat icon, String key, String name) { + return new Person.Builder().setIcon(icon).setKey(key).setName(name).build(); + } + + private NotificationCompat.MessagingStyle.Message recreatePreviousMessage (StatusBarNotification statusBarNotification) { + // Get previous message + Notification previousNotification = statusBarNotification.getNotification(); + String previousMessage = previousNotification.extras.getString("android.text"); + long time = statusBarNotification.getNotification().when; + // Recreate Person object + IconCompat avatarBitmap = ParcelUtils.getVersionedParcelable(previousNotification.extras, EXTRAS_AVATAR_KEY); + String previousName = previousNotification.extras.getString(EXTRAS_NAME_KEY); + String previousAccountID = previousNotification.extras.getString(EXTRAS_ACCOUNT_ID_KEY); + Person previousPerson = createMessagePersonObject(avatarBitmap, previousAccountID, previousName); + + return new NotificationCompat.MessagingStyle.Message(previousMessage, time, previousPerson); + } - // Store the new notification ID so we can replace the notification if this conversation - // receives more messages - notificationData.prevNotificationID = notificationID; + private Bundle createMessageExtrasBundle(long reportID, Person person) { + Bundle extrasBundle = new Bundle(); + extrasBundle.putLong(EXTRAS_REPORT_ID_KEY, reportID); + ParcelUtils.putVersionedParcelable(extrasBundle, EXTRAS_AVATAR_KEY, person.getIcon()); + extrasBundle.putString(EXTRAS_ACCOUNT_ID_KEY, person.getKey()); + extrasBundle.putString(EXTRAS_NAME_KEY, person.getName().toString()); - NotificationCache.setNotificationData(reportID, notificationData); + return extrasBundle; } + private StatusBarNotification getActiveNotificationByReportId(@NonNull Context context, long reportId) { + List notifications = NotificationManagerCompat.from(context).getActiveNotifications(); + for (StatusBarNotification currentNotification : notifications) { + long associatedReportId = currentNotification.getNotification().extras.getLong("reportID", -1); + if (associatedReportId == reportId) return currentNotification; + } + return null; + } /** * Safely retrieve the message time in milliseconds */ @@ -260,26 +297,6 @@ private long getMessageTimeInMillis(String createdTime) { return Calendar.getInstance().getTimeInMillis(); } - /** - * Remove the notification data from the cache when the user dismisses the notification. - * - * @param message Push notification's message - */ - public void onDismissNotification(PushMessage message) { - try { - JsonMap payload = JsonValue.parseString(message.getExtra(PAYLOAD_KEY)).optMap(); - long reportID = payload.get("reportID").getLong(-1); - - if (reportID == -1) { - return; - } - - NotificationCache.setNotificationData(reportID, null); - } catch (Exception e) { - Log.e(TAG, "Failed to delete conversation cache. SendID=" + message.getSendId(), e); - } - } - private Bitmap fetchIcon(@NonNull Context context, String urlString) { URL parsedUrl = null; try { diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/NotificationCache.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/NotificationCache.java deleted file mode 100644 index 7ddc17d37b4d..000000000000 --- a/android/app/src/main/java/com/expensify/chat/customairshipextender/NotificationCache.java +++ /dev/null @@ -1,193 +0,0 @@ -package com.expensify.chat.customairshipextender; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.util.Base64; - -import androidx.core.app.Person; -import androidx.core.graphics.drawable.IconCompat; - -import com.expensify.chat.MainApplication; -import com.urbanairship.UAirship; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.HashMap; - -public class NotificationCache { - - private static final String CACHE_FILE_NAME = "notification-cache"; - private static HashMap cache = null; - - /* - * Get NotificationData for an existing notification or create a new instance - * if it doesn't exist - */ - public static NotificationData getNotificationData(long reportID) { - if (cache == null) { - cache = readFromInternalStorage(); - } - - NotificationData notificationData = cache.get(Long.toString(reportID)); - - if (notificationData == null) { - notificationData = new NotificationData(); - setNotificationData(reportID, notificationData); - } - - return notificationData; - } - - /* - * Set and persist NotificationData in the cache - */ - public static void setNotificationData(long reportID, NotificationData data) { - if (cache == null) { - cache = readFromInternalStorage(); - } - - cache.put(Long.toString(reportID), data); - writeToInternalStorage(); - } - - private static void writeToInternalStorage() { - Context context = UAirship.getApplicationContext(); - - FileOutputStream fos = null; - ObjectOutputStream oos = null; - try { - File outputFile = new File(context.getFilesDir(), CACHE_FILE_NAME); - fos = new FileOutputStream(outputFile); - oos = new ObjectOutputStream(fos); - oos.writeObject(cache); - } catch (IOException e) { - e.printStackTrace(); - } finally { - try { - if (oos != null) { - oos.close(); - } - if (fos != null) { - fos.close(); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - } - - private static HashMap readFromInternalStorage() { - HashMap result; - Context context = UAirship.getApplicationContext(); - - FileInputStream fis = null; - ObjectInputStream ois = null; - try { - File fileCache = new File(context.getFilesDir(), CACHE_FILE_NAME); - fis = new FileInputStream(fileCache); - ois = new ObjectInputStream(fis); - result = (HashMap) ois.readObject(); - } catch (IOException | ClassNotFoundException e) { - e.printStackTrace(); - result = new HashMap<>(); - } finally { - try { - if (ois != null) { - ois.close(); - } - if (fis != null) { - fis.close(); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - return result; - } - - /** - * A class for caching data for notifications. We use this to track active notifications so we - * can thread related notifications together - */ - public static class NotificationData implements Serializable { - private final HashMap names = new HashMap<>(); - - // A map of accountID => base64 encoded Bitmap - // In order to make Bitmaps serializable, we encode them as base64 strings - private final HashMap icons = new HashMap<>(); - public ArrayList messages = new ArrayList<>(); - - public int prevNotificationID = -1; - - public NotificationData() {} - - public Bitmap getIcon(String accountID) { - return decodeToBitmap(icons.get(accountID)); - } - - public void putIcon(String accountID, Bitmap bitmap) { - icons.put(accountID, encodeToBase64(bitmap)); - } - - public Person getPerson(String accountID) { - if (!names.containsKey(accountID) || !icons.containsKey(accountID)) { - return null; - } - - String name = names.get(accountID); - Bitmap icon = getIcon(accountID); - - return new Person.Builder() - .setIcon(IconCompat.createWithBitmap(icon)) - .setKey(accountID) - .setName(name) - .build(); - } - - public void putPerson(String accountID, String name, Bitmap icon) { - names.put(accountID, name); - putIcon(accountID, icon); - } - - public static String encodeToBase64(Bitmap bitmap) { - if (bitmap == null) { - return ""; - } - - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream); - byte[] byteArray = byteArrayOutputStream.toByteArray(); - return Base64.encodeToString(byteArray, Base64.DEFAULT); - } - - public static Bitmap decodeToBitmap(String base64String) { - if (base64String == null) { - return null; - } - - byte[] decodedBytes = Base64.decode(base64String, Base64.DEFAULT); - return BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.length); - } - } - - public static class NotificationMessage implements Serializable { - public String accountID; - public String text; - public long time; - - NotificationMessage(String accountID, String text, long time) { - this.accountID = accountID; - this.text = text; - this.time = time; - } - } -} diff --git a/assets/images/chatbubble-add.svg b/assets/images/chatbubble-add.svg index 047a43073b3c..48eebf863cc3 100644 --- a/assets/images/chatbubble-add.svg +++ b/assets/images/chatbubble-add.svg @@ -1,13 +1 @@ - - - - - - + \ No newline at end of file diff --git a/assets/images/chatbubble-unread.svg b/assets/images/chatbubble-unread.svg index 9da789510276..492616cf2ab5 100644 --- a/assets/images/chatbubble-unread.svg +++ b/assets/images/chatbubble-unread.svg @@ -1,12 +1 @@ - - - - - - + \ No newline at end of file diff --git a/assets/images/google-meet.svg b/assets/images/google-meet.svg deleted file mode 100644 index 8def88aa6edc..000000000000 --- a/assets/images/google-meet.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/images/home.svg b/assets/images/home.svg index 6b2411407be7..d4e02b723fee 100644 --- a/assets/images/home.svg +++ b/assets/images/home.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/assets/images/olddot-wireframe.svg b/assets/images/olddot-wireframe.svg new file mode 100644 index 000000000000..055059edfd70 --- /dev/null +++ b/assets/images/olddot-wireframe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__gears.svg b/assets/images/simple-illustrations/simple-illustration__gears.svg new file mode 100644 index 000000000000..2798feb4e04d --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__gears.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__lockclosed.svg b/assets/images/simple-illustrations/simple-illustration__lockclosed.svg new file mode 100644 index 000000000000..791500c28032 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__lockclosed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__palmtree.svg b/assets/images/simple-illustrations/simple-illustration__palmtree.svg new file mode 100644 index 000000000000..c67e871dc434 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__palmtree.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__profile.svg b/assets/images/simple-illustrations/simple-illustration__profile.svg new file mode 100644 index 000000000000..085f02822bc0 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__profile.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__qr-code.svg b/assets/images/simple-illustrations/simple-illustration__qr-code.svg new file mode 100644 index 000000000000..7bd460d5f4e9 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__qr-code.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/zoom-icon.svg b/assets/images/zoom-icon.svg deleted file mode 100644 index 81f025aedf79..000000000000 --- a/assets/images/zoom-icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/contributingGuides/NAVIGATION.md b/contributingGuides/NAVIGATION.md index 543b133fe62b..5bb6dfb85851 100644 --- a/contributingGuides/NAVIGATION.md +++ b/contributingGuides/NAVIGATION.md @@ -30,7 +30,7 @@ When creating RHP flows, you have to remember a couple things: - Since you can deeplink to different pages inside the RHP navigator, it is important to provide the possibility for the user to properly navigate back from any page with UP press (`HeaderWithBackButton` component). -- An example can be deeplinking to `/settings/profile/personal-details`. From there, when pressing the UP button, you should navigate to `/settings/profile`, so in order for it to work, you should provide the correct route in `onBackButtonPress` prop of `HeaderWithBackButton` (`Navigation.goBack(ROUTES.SETTINGS_PROFILE)` in this example). +- An example can be deeplinking to `/settings/profile/timezone/select`. From there, when pressing the UP button, you should navigate to `/settings/profile/timezone`, so in order for it to work, you should provide the correct route in `onBackButtonPress` prop of `HeaderWithBackButton` (`Navigation.goBack(ROUTES.SETTINGS_PROFILE)` in this example). - We use a custom `goBack` function to handle the browser and the `react-navigation` history stack. Under the hood, it resolves to either replacing the current screen with the one we navigate to (deeplinking scenario) or just going back if we reached the current page by navigating in App (pops the screen). It ensures the requested behaviors on web, which is navigating back to the place from where you deeplinked when going into the RHP flow by it. diff --git a/desktop/main.js b/desktop/main.js index e53f03530b57..4b38c5d36ab3 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -410,6 +410,14 @@ const mainWindow = () => { browserWindow.webContents.goBack(); }, }, + { + role: 'back', + visible: false, + accelerator: process.platform === 'darwin' ? 'Cmd+Left' : 'Shift+Left', + click: () => { + browserWindow.webContents.goBack(); + }, + }, { id: 'forward', role: 'forward', @@ -418,6 +426,14 @@ const mainWindow = () => { browserWindow.webContents.goForward(); }, }, + { + role: 'forward', + visible: false, + accelerator: process.platform === 'darwin' ? 'Cmd+Right' : 'Shift+Right', + click: () => { + browserWindow.webContents.goForward(); + }, + }, ], }, { diff --git a/docs/articles/expensify-classic/getting-started/Using-The-App.md b/docs/articles/expensify-classic/getting-started/Using-The-App.md deleted file mode 100644 index f1bc31793ba8..000000000000 --- a/docs/articles/expensify-classic/getting-started/Using-The-App.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: Using the app -description: Streamline expense management effortlessly with the Expensify mobile app. Learn how to install, enable push notifications, and use SmartScan to capture, categorize, and track expenses. Versatile for personal and business use, Expensify is a secure and automated solution for managing your finances on the go. ---- - -
-# Overview -The Expensify mobile app is the ultimate expense management solution that makes it effortless to track and submit your receipts and expenses. Use the app to snap a picture of your receipts, categorize and submit expenses, and even review and approve expense reports. -# How to install the Expensify app -To get started with Expensify on your mobile device, you need to download the app: -1. Visit the App Store (iOS) or Google Play Store (Android). -2. Search for "Expensify" and select the official Expensify app. -3. Tap "Download" or "Install." - -Once the app is installed, open it and log in with your Expensify credentials. If you don't have an Expensify account, you can create one during the sign-up process. -# How to enable on push notifications -Push notifications keep you informed about expense approvals, reimbursements, and more. To enable push notifications: -1. Open the Expensify app. -2. Go to "Settings" or "Preferences." -3. Find the "Receive realtime alerts" toggle -4. Toggle realtime alerts on to begin receiving notifications - -# Deep dive -## Using SmartScan on the App -### Capture receipts -1. Open the Expensify mobile app. -2. Tap the green camera button to take a photo of a receipt. -3. The receipt will be SmartScanned automatically. - -If you have multiple receipts tap the Rapid Fire Mode button in the bottom right hand corner to snap multiple pictures. You can also upload an existing photo from your gallery. -### SmartScan analysis -After capturing or uploading a receipt, Expensify's SmartScan technology goes to work. It analyzes the receipt to extract key details such as the merchant's name, transaction date, transaction currency, and total amount spent. SmartScan inputs all the data for you, so you don’t have to type a thing. -### Review and edit -Once SmartScan is finished, you can further categorize and code your expense based on your company’s policy. Review this data to ensure accuracy. If necessary, you can edit or add additional details, such as expense categories, tags, attendees, tax rates, or descriptions. -### Multi-Currency support -For businesses dealing with international expenses, SmartScan can handle multiple currencies and provide accurate exchange rate conversion based on your policies reporting currency. It's essential to set up and configure currency preferences for these scenarios. -### Custom expense categories -SmartScan can automatically categorize expenses based on vendor or merchant. Users can customize these categories to suit their specific accounting needs. This can be particularly useful for tracking expenses across different departments or projects. -### SmartScan outcomes -SmartScan's performance can vary depending on factors such as receipt quality, language, and handwriting. It's important to keep the following variables in mind: -**Receipt quality**: The clarity and condition of a receipt can impact SmartScan's accuracy. For best results, ensure your environment is well-lit and the receipt is straight and free of obstructions. -**Language support**: While SmartScan supports multiple languages, its accuracy may differ from one language to another. Users dealing with non-English receipts should be aware of potential variations in data extraction. -**Handwriting recognition**: Handwritten receipts might pose challenges for SmartScan. In such cases, manual verification may be necessary to ensure accurate data entry. - -{% include faq-begin.md %} - -## Can I use the mobile app for both personal and business expenses? -Yes, you can use Expensify for personal and business expenses. It's versatile and suitable for both individual and corporate use. Check out our personal and business plans [here](https://www.expensify.com/pricing) to see what might be right for you. -## Is it possible to categorize and tag expenses on the mobile app? -Yes, you can categorize and tag expenses on the mobile app. The app allows you to customize categories and tags to help organize and track your spending. -## What should I do if I encounter issues with the mobile app, such as login problems or crashes? -If you experience issues, first make sure you’re using the most recent version of the app. You can also try to restarting the app. If the issue persists, you can start a chat with Concierge in the app or write to [concierge@expensify.com](mailto:concierge@expensify.com). -## Is the mobile app secure for managing sensitive financial information? -Expensify takes security seriously and employs encryption and other security measures to protect your data. It's important to use strong, unique passwords and enable device security features like biometric authentication. -## Can I use the mobile app offline, and will my data sync when I'm back online? -Yes, you can use the mobile app offline to capture receipts and create expenses. The app will sync your data once you have an internet connection. - -{% include faq-end.md %} -
-
- -# Coming soon - - -
\ No newline at end of file diff --git a/docs/assets/images/info.svg b/docs/assets/images/info.svg index 96924fbb6cf7..fbe9b3612667 100644 --- a/docs/assets/images/info.svg +++ b/docs/assets/images/info.svg @@ -1,9 +1 @@ - - - - - - - - - + \ No newline at end of file diff --git a/docs/redirects.csv b/docs/redirects.csv index 2609f6665c8d..674e5c32ab04 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -48,5 +48,7 @@ https://community.expensify.com/discussion/4463/how-to-remove-or-manage-settings https://community.expensify.com/discussion/5793/how-to-connect-your-personal-card-to-import-expenses,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Credit-Cards https://community.expensify.com/discussion/4826/how-to-set-your-annual-subscription-size,https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription https://community.expensify.com/discussion/5667/deep-dive-how-does-the-annual-subscription-billing-work,https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription +https://help.expensify.com/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager,https://help.expensify.com/articles/expensify-classic/expensify-partner-program/Your-Expensify-Partner-Manager https://help.expensify.com/expensify-classic/hubs/getting-started/plan-types,https://use.expensify.com/ https://help.expensify.com/articles/expensify-classic/getting-started/Employees,https://help.expensify.com/articles/expensify-classic/getting-started/Join-your-company's-workspace +https://help.expensify.com/articles/expensify-classic/getting-started/Using-The-App,https://help.expensify.com/articles/expensify-classic/getting-started/Join-your-company's-workspace diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 5b280de0c195..4f571739ecc7 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.38 + 1.4.39 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.38.0 + 1.4.39.7 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 8d17d0c0c572..bc29fdff60c2 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.38 + 1.4.39 CFBundleSignature ???? CFBundleVersion - 1.4.38.0 + 1.4.39.7 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 7af26a2c647b..b54ddc36ddf0 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.38 + 1.4.39 CFBundleVersion - 1.4.38.0 + 1.4.39.7 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 1664c982ce50..2c05bf4175d8 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1178,10 +1178,6 @@ PODS: - React-Core - react-native-launch-arguments (4.0.2): - React - - react-native-live-markdown (0.1.0): - - glog - - RCT-Folly (= 2022.05.16.00) - - React-Core - react-native-netinfo (11.2.1): - React-Core - react-native-pager-view (6.2.2): @@ -1412,6 +1408,10 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core + - RNLiveMarkdown (0.1.5): + - glog + - RCT-Folly (= 2022.05.16.00) + - React-Core - RNLocalize (2.2.6): - React-Core - rnmapbox-maps (10.0.11): @@ -1529,7 +1529,6 @@ DEPENDENCIES: - react-native-image-picker (from `../node_modules/react-native-image-picker`) - react-native-key-command (from `../node_modules/react-native-key-command`) - react-native-launch-arguments (from `../node_modules/react-native-launch-arguments`) - - "react-native-live-markdown (from `../node_modules/@expensify/react-native-live-markdown`)" - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-pager-view (from `../node_modules/react-native-pager-view`) - react-native-pdf (from `../node_modules/react-native-pdf`) @@ -1574,6 +1573,7 @@ DEPENDENCIES: - RNFS (from `../node_modules/react-native-fs`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - "RNGoogleSignin (from `../node_modules/@react-native-google-signin/google-signin`)" + - "RNLiveMarkdown (from `../node_modules/@expensify/react-native-live-markdown`)" - RNLocalize (from `../node_modules/react-native-localize`) - "rnmapbox-maps (from `../node_modules/@rnmapbox/maps`)" - RNPermissions (from `../node_modules/react-native-permissions`) @@ -1725,8 +1725,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-key-command" react-native-launch-arguments: :path: "../node_modules/react-native-launch-arguments" - react-native-live-markdown: - :path: "../node_modules/@expensify/react-native-live-markdown" react-native-netinfo: :path: "../node_modules/@react-native-community/netinfo" react-native-pager-view: @@ -1815,6 +1813,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-gesture-handler" RNGoogleSignin: :path: "../node_modules/@react-native-google-signin/google-signin" + RNLiveMarkdown: + :path: "../node_modules/@expensify/react-native-live-markdown" RNLocalize: :path: "../node_modules/react-native-localize" rnmapbox-maps: @@ -1922,7 +1922,6 @@ SPEC CHECKSUMS: react-native-image-picker: c33d4e79f0a14a2b66e5065e14946ae63749660b react-native-key-command: 5af6ee30ff4932f78da6a2109017549042932aa5 react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d - react-native-live-markdown: 81ac491b913cb8541a06586adcdc159ab1b86fb5 react-native-netinfo: 8a7fd3f7130ef4ad2fb4276d5c9f8d3f28d2df3d react-native-pager-view: 02a5c4962530f7efc10dd51ee9cdabeff5e6c631 react-native-pdf: b4ca3d37a9a86d9165287741c8b2ef4d8940c00e @@ -1967,6 +1966,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 25b969a1ffc806b9f9ad2e170d4a3b049c6af85e RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 + RNLiveMarkdown: 35eeecf7e57eb26fdc279d5d4815982a9a9f7beb RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: 6f638ec002aa6e906a6f766d69cd45f968d98e64 RNPermissions: 9b086c8f05b2e2faa587fdc31f4c5ab4509728aa diff --git a/jest.config.js b/jest.config.js index de7ed4b1f974..b347db593d83 100644 --- a/jest.config.js +++ b/jest.config.js @@ -22,8 +22,8 @@ module.exports = { doNotFake: ['nextTick'], }, testEnvironment: 'jsdom', - setupFiles: ['/jest/setup.js', './node_modules/@react-native-google-signin/google-signin/jest/build/setup.js'], - setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect', '/jest/setupAfterEnv.js', '/tests/perf-test/setupAfterEnv.js'], + setupFiles: ['/jest/setup.ts', './node_modules/@react-native-google-signin/google-signin/jest/build/setup.js'], + setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect', '/jest/setupAfterEnv.ts', '/tests/perf-test/setupAfterEnv.js'], cacheDirectory: '/.jest-cache', moduleNameMapper: { '\\.(lottie)$': '/__mocks__/fileMock.js', diff --git a/jest/setup.js b/jest/setup.ts similarity index 90% rename from jest/setup.js rename to jest/setup.ts index e82bf678941d..68d904fac5be 100644 --- a/jest/setup.js +++ b/jest/setup.ts @@ -1,6 +1,7 @@ import mockClipboard from '@react-native-clipboard/clipboard/jest/clipboard-mock'; import '@shopify/flash-list/jestSetup'; import 'react-native-gesture-handler/jestSetup'; +import mockStorage from 'react-native-onyx/dist/storage/__mocks__'; import * as reanimatedJestUtils from 'react-native-reanimated/src/reanimated2/jestUtils'; import 'setimmediate'; import setupMockImages from './setupMockImages'; @@ -19,7 +20,7 @@ jest.mock('@react-native-clipboard/clipboard', () => mockClipboard); // Mock react-native-onyx storage layer because the SQLite storage layer doesn't work in jest. // Mocking this file in __mocks__ does not work because jest doesn't support mocking files that are not directly used in the testing project, // and we only want to mock the storage layer, not the whole Onyx module. -jest.mock('react-native-onyx/dist/storage', () => require('react-native-onyx/dist/storage/__mocks__')); +jest.mock('react-native-onyx/dist/storage', () => mockStorage); // Turn off the console logs for timing events. They are not relevant for unit tests and create a lot of noise jest.spyOn(console, 'debug').mockImplementation((...params) => { @@ -34,6 +35,6 @@ jest.spyOn(console, 'debug').mockImplementation((...params) => { // This mock is required for mocking file systems when running tests jest.mock('react-native-fs', () => ({ - unlink: jest.fn(() => new Promise((res) => res())), + unlink: jest.fn(() => new Promise((res) => res())), CachesDirectoryPath: jest.fn(), })); diff --git a/jest/setupAfterEnv.js b/jest/setupAfterEnv.ts similarity index 100% rename from jest/setupAfterEnv.js rename to jest/setupAfterEnv.ts diff --git a/jest/setupMockImages.js b/jest/setupMockImages.ts similarity index 87% rename from jest/setupMockImages.js rename to jest/setupMockImages.ts index 10925aca8736..c48797b3c07b 100644 --- a/jest/setupMockImages.js +++ b/jest/setupMockImages.ts @@ -1,14 +1,10 @@ import fs from 'fs'; import path from 'path'; -import _ from 'underscore'; -/** - * @param {String} imagePath - */ -function mockImages(imagePath) { +function mockImages(imagePath: string) { const imageFilenames = fs.readdirSync(path.resolve(__dirname, `../assets/${imagePath}/`)); // eslint-disable-next-line rulesdir/prefer-early-return - _.each(imageFilenames, (fileName) => { + imageFilenames.forEach((fileName) => { if (/\.svg/.test(fileName)) { jest.mock(`../assets/${imagePath}/${fileName}`, () => () => ''); } diff --git a/package-lock.json b/package-lock.json index 1825fa649ae9..1cc630ce5dac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "new.expensify", - "version": "1.4.38-0", + "version": "1.4.39-7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.38-0", + "version": "1.4.39-7", "hasInstallScript": true, "license": "MIT", "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "git+ssh://git@github.com/Expensify/react-native-live-markdown.git#2ed4240336e50abb4a7fa9ff6a3c180f8bc9ce5b", + "@expensify/react-native-live-markdown": "0.1.5", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-getcanonicallocales": "^2.2.0", @@ -96,12 +96,12 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.1", + "react-native-onyx": "2.0.2", "react-native-pager-view": "6.2.2", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", "react-native-permissions": "^3.9.3", - "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#7a407cd4174d9838a944c1c2e1cb4a9737ac69c5", + "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#42b334d0c4e71d225601f72828d3dedd0bc22212", "react-native-plaid-link-sdk": "10.8.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", @@ -113,7 +113,7 @@ "react-native-tab-view": "^3.5.2", "react-native-url-polyfill": "^2.0.0", "react-native-view-shot": "3.8.0", - "react-native-vision-camera": "2.16.5", + "react-native-vision-camera": "2.16.8", "react-native-web": "^0.19.9", "react-native-web-linear-gradient": "^1.1.2", "react-native-webview": "13.6.3", @@ -199,7 +199,7 @@ "css-loader": "^6.7.2", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", - "electron": "^25.9.4", + "electron": "^26.6.8", "electron-builder": "24.6.4", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", @@ -232,6 +232,7 @@ "shellcheck": "^1.1.0", "style-loader": "^2.0.0", "time-analytics-webpack-plugin": "^0.1.17", + "ts-node": "^10.9.2", "type-fest": "^3.12.0", "typescript": "^5.3.2", "wait-port": "^0.2.9", @@ -2887,6 +2888,28 @@ "node": ">=0.1.90" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "devOptional": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "devOptional": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -3351,13 +3374,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.0", - "resolved": "git+ssh://git@github.com/Expensify/react-native-live-markdown.git#2ed4240336e50abb4a7fa9ff6a3c180f8bc9ce5b", - "integrity": "sha512-i+HIsCFL9cdma+saH/KN2llTGqEb2DQttEJKozdm4fvcie9Ce2/q7XNDZo6nIYTbIVXPDLKPDmWLXqXTgLBKDQ==", - "license": "MIT", - "workspaces": [ - "example" - ], + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.5.tgz", + "integrity": "sha512-Z1tduU1C2BDgZNMrDvXtiWUhQoroMasvwucLBdLSypAMB4Kls4G038A/yZEbD00YVXjXv2tBUiqvUmCMuRdlqw==", "engines": { "node": ">= 18.0.0" }, @@ -20161,6 +20180,30 @@ "node": ">=10.13.0" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "devOptional": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "devOptional": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "devOptional": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "devOptional": true + }, "node_modules/@turf/along": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/@turf/along/-/along-6.5.0.tgz", @@ -22780,7 +22823,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/argparse": { @@ -27256,6 +27299,12 @@ "sha.js": "^2.4.8" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "devOptional": true + }, "node_modules/cross-fetch": { "version": "3.1.5", "license": "MIT", @@ -28151,6 +28200,15 @@ "detect-port": "bin/detect-port.js" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "devOptional": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.4.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", @@ -28529,9 +28587,9 @@ } }, "node_modules/electron": { - "version": "25.9.4", - "resolved": "https://registry.npmjs.org/electron/-/electron-25.9.4.tgz", - "integrity": "sha512-5pDU8a7o7ZIPTZHAqjflGMq764Favdsc271KXrAT3oWvFTHs5Ve9+IOt5EUVPrwvC2qRWKpCIEM47WzwkTlENQ==", + "version": "26.6.8", + "resolved": "https://registry.npmjs.org/electron/-/electron-26.6.8.tgz", + "integrity": "sha512-nuzJ5nVButL1jErc97IVb+A6jbContMg5Uuz5fhmZ4NLcygLkSW8FZpnOT7A4k8Saa95xDJOvqGZyQdI/OPNFw==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -39902,6 +39960,12 @@ "semver": "bin/semver" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "devOptional": true + }, "node_modules/make-event-props": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-1.6.1.tgz", @@ -45044,9 +45108,9 @@ } }, "node_modules/react-native-onyx": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.1.tgz", - "integrity": "sha512-o6QNvq91qg8hFXIhmHjBqlNXD/YZxBZSRN8Vkq7xD2NYskzxK2mLqhBdhB8yMMwe6Cd8sVUK4vlZax/JU79xYw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.2.tgz", + "integrity": "sha512-24kcG3ChBXp+uSSCXudFvZTdCnKLRHQRgvTcnh2eA7COtKvbL8ggEJNkglSYmcf5WoDzLgYyWiKvcjcXQnmBvw==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", @@ -45138,8 +45202,8 @@ }, "node_modules/react-native-picker-select": { "version": "8.1.0", - "resolved": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#7a407cd4174d9838a944c1c2e1cb4a9737ac69c5", - "integrity": "sha512-NpXXyK+UuANYOysjUb9pCoq9SookRYPfpOcM4shxOD4+2Fkh7TYt2LBUpAdBicMHmtaR43RWXVQk9pMimOhg2w==", + "resolved": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#42b334d0c4e71d225601f72828d3dedd0bc22212", + "integrity": "sha512-e8TAWVR4AEw2PFGFxlevCBFr1RwvwTqq1M2w9Yi6xNz+d4SbG6tDIcJDNIqt0gyBqvxlL7BuK0G5BjbiZDLKsg==", "license": "MIT", "dependencies": { "lodash.isequal": "^4.5.0" @@ -45329,9 +45393,9 @@ } }, "node_modules/react-native-vision-camera": { - "version": "2.16.5", - "resolved": "https://registry.npmjs.org/react-native-vision-camera/-/react-native-vision-camera-2.16.5.tgz", - "integrity": "sha512-MzXhNd597OyMQSEGhqWI4DufWkdr7PR7U9B30E3gXnln7cnvjMVIp4j3eIW9BIrgvEyUcEeL7nZM5NLhTmO/fA==", + "version": "2.16.8", + "resolved": "https://registry.npmjs.org/react-native-vision-camera/-/react-native-vision-camera-2.16.8.tgz", + "integrity": "sha512-DCYkhxHw0p8dftfYxkfyeECraPOCliNWriVUTe+qit16ejs9fXoW1zXJBq48UbysUTuIOk8QwTn9OSy4jhGvTg==", "peerDependencies": { "react": "*", "react-native": "*" @@ -50519,6 +50583,70 @@ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "devOptional": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "devOptional": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ts-node/node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "devOptional": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ts-object-utils": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/ts-object-utils/-/ts-object-utils-0.0.5.tgz", @@ -50760,7 +50888,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -51510,6 +51638,12 @@ "dev": true, "license": "MIT" }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "devOptional": true + }, "node_modules/v8-to-istanbul": { "version": "9.0.1", "license": "ISC", @@ -53360,6 +53494,15 @@ "fd-slicer": "~1.1.0" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 4a8a7ec5612a..5870241897b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.38-0", + "version": "1.4.39-7", "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.", @@ -19,15 +19,15 @@ "ipad-sm": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (11-inch) (4th generation)\\\" --mode=\\\"DebugDevelopment\\\" --scheme=\\\"New Expensify Dev\\\"\"", "start": "npx react-native start", "web": "scripts/set-pusher-suffix.sh && concurrently npm:web-proxy npm:web-server", - "web-proxy": "node web/proxy.js", + "web-proxy": "ts-node web/proxy.js", "web-server": "webpack-dev-server --open --config config/webpack/webpack.dev.js", "build": "webpack --config config/webpack/webpack.common.js --env envFile=.env.production", "build-staging": "webpack --config config/webpack/webpack.common.js --env envFile=.env.staging", "build-adhoc": "webpack --config config/webpack/webpack.common.js --env envFile=.env.adhoc", - "desktop": "scripts/set-pusher-suffix.sh && node desktop/start.js", + "desktop": "scripts/set-pusher-suffix.sh && ts-node desktop/start.js", "desktop-build": "scripts/build-desktop.sh production", "desktop-build-staging": "scripts/build-desktop.sh staging", - "createDocsRoutes": "node .github/scripts/createDocsRoutes.js", + "createDocsRoutes": "ts-node .github/scripts/createDocsRoutes.js", "desktop-build-adhoc": "scripts/build-desktop.sh adhoc", "ios-build": "fastlane ios build", "android-build": "fastlane android build", @@ -50,16 +50,16 @@ "analyze-packages": "ANALYZE_BUNDLE=true webpack --config config/webpack/webpack.common.js --env envFile=.env.production", "symbolicate:android": "npx metro-symbolicate android/app/build/generated/sourcemaps/react/release/index.android.bundle.map", "symbolicate:ios": "npx metro-symbolicate main.jsbundle.map", - "test:e2e": "node tests/e2e/testRunner.js --development --skipCheckout --skipInstallDeps --buildMode none", - "test:e2e:dev": "node tests/e2e/testRunner.js --development --skipCheckout --config ./config.dev.js --buildMode skip --skipInstallDeps", + "test:e2e": "ts-node tests/e2e/testRunner.js --development --skipCheckout --skipInstallDeps --buildMode none", + "test:e2e:dev": "ts-node tests/e2e/testRunner.js --development --skipCheckout --config ./config.dev.js --buildMode skip --skipInstallDeps", "gh-actions-unused-styles": "./.github/scripts/findUnusedKeys.sh", "workflow-test": "./workflow_tests/scripts/runWorkflowTests.sh", - "workflow-test:generate": "node workflow_tests/utils/preGenerateTest.js", + "workflow-test:generate": "ts-node workflow_tests/utils/preGenerateTest.js", "setup-https": "mkcert -install && mkcert -cert-file config/webpack/certificate.pem -key-file config/webpack/key.pem dev.new.expensify.com localhost 127.0.0.1" }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "git+ssh://git@github.com/Expensify/react-native-live-markdown.git#2ed4240336e50abb4a7fa9ff6a3c180f8bc9ce5b", + "@expensify/react-native-live-markdown": "0.1.5", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-getcanonicallocales": "^2.2.0", @@ -144,12 +144,12 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.1", + "react-native-onyx": "2.0.2", "react-native-pager-view": "6.2.2", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", "react-native-permissions": "^3.9.3", - "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#7a407cd4174d9838a944c1c2e1cb4a9737ac69c5", + "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#42b334d0c4e71d225601f72828d3dedd0bc22212", "react-native-plaid-link-sdk": "10.8.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", @@ -161,7 +161,7 @@ "react-native-tab-view": "^3.5.2", "react-native-url-polyfill": "^2.0.0", "react-native-view-shot": "3.8.0", - "react-native-vision-camera": "2.16.5", + "react-native-vision-camera": "2.16.8", "react-native-web": "^0.19.9", "react-native-web-linear-gradient": "^1.1.2", "react-native-webview": "13.6.3", @@ -247,7 +247,7 @@ "css-loader": "^6.7.2", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", - "electron": "^25.9.4", + "electron": "^26.6.8", "electron-builder": "24.6.4", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", @@ -280,6 +280,7 @@ "shellcheck": "^1.1.0", "style-loader": "^2.0.0", "time-analytics-webpack-plugin": "^0.1.17", + "ts-node": "^10.9.2", "type-fest": "^3.12.0", "typescript": "^5.3.2", "wait-port": "^0.2.9", diff --git a/patches/@react-native+virtualized-lists+0.73.4+001+onStartReched.patch b/patches/@react-native+virtualized-lists+0.73.4+001+onStartReched.patch new file mode 100644 index 000000000000..b183124964f6 --- /dev/null +++ b/patches/@react-native+virtualized-lists+0.73.4+001+onStartReched.patch @@ -0,0 +1,32 @@ +diff --git a/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js b/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js +index 0516679..e338d90 100644 +--- a/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js ++++ b/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js +@@ -1546,7 +1546,7 @@ class VirtualizedList extends StateSafePureComponent { + // Next check if the user just scrolled within the start threshold + // and call onStartReached only once for a given content length, + // and only if onEndReached is not being executed +- else if ( ++ if ( + onStartReached != null && + this.state.cellsAroundViewport.first === 0 && + isWithinStartThreshold && +@@ -1558,13 +1558,11 @@ class VirtualizedList extends StateSafePureComponent { + + // If the user scrolls away from the start or end and back again, + // cause onStartReached or onEndReached to be triggered again +- else { +- this._sentStartForContentLength = isWithinStartThreshold +- ? this._sentStartForContentLength +- : 0; +- this._sentEndForContentLength = isWithinEndThreshold +- ? this._sentEndForContentLength +- : 0; ++ if (!isWithinStartThreshold) { ++ this._sentStartForContentLength = 0; ++ } ++ if (!isWithinEndThreshold) { ++ this._sentEndForContentLength = 0; + } + } + diff --git a/patches/react-native-vision-camera+2.16.5.patch b/patches/react-native-vision-camera+2.16.5.patch deleted file mode 100644 index d08f7c11f5f3..000000000000 --- a/patches/react-native-vision-camera+2.16.5.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessorRuntimeManager.kt b/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessorRuntimeManager.kt -index c0a8b23..653b51e 100644 ---- a/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessorRuntimeManager.kt -+++ b/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/frameprocessor/FrameProcessorRuntimeManager.kt -@@ -40,7 +40,7 @@ class FrameProcessorRuntimeManager(context: ReactApplicationContext, frameProces - val holder = context.catalystInstance.jsCallInvokerHolder as CallInvokerHolderImpl - mScheduler = VisionCameraScheduler(frameProcessorThread) - mContext = WeakReference(context) -- mHybridData = initHybrid(context.javaScriptContextHolder.get(), holder, mScheduler!!) -+ mHybridData = initHybrid(context.javaScriptContextHolder!!.get(), holder, mScheduler!!) - initializeRuntime() - - Log.i(TAG, "Installing JSI Bindings on JS Thread...") diff --git a/patches/react-native-vision-camera+2.16.5+001+fix-boost-dependency.patch b/patches/react-native-vision-camera+2.16.8.patch similarity index 100% rename from patches/react-native-vision-camera+2.16.5+001+fix-boost-dependency.patch rename to patches/react-native-vision-camera+2.16.8.patch diff --git a/patches/react-native-web+0.19.9+003+fix-pointer-events.patch b/patches/react-native-web+0.19.9+003+fix-pointer-events.patch deleted file mode 100644 index a457fbcfe36c..000000000000 --- a/patches/react-native-web+0.19.9+003+fix-pointer-events.patch +++ /dev/null @@ -1,22 +0,0 @@ -diff --git a/node_modules/react-native-web/dist/exports/StyleSheet/compiler/index.js b/node_modules/react-native-web/dist/exports/StyleSheet/compiler/index.js -index bdcecc2..63f1364 100644 ---- a/node_modules/react-native-web/dist/exports/StyleSheet/compiler/index.js -+++ b/node_modules/react-native-web/dist/exports/StyleSheet/compiler/index.js -@@ -353,7 +353,7 @@ function createAtomicRules(identifier, property, value) { - var _block2 = createDeclarationBlock({ - pointerEvents: 'none' - }); -- rules.push(selector + ">*" + _block2); -+ rules.push(selector + " *" + _block2); - } - } else if (value === 'none' || value === 'box-none') { - finalValue = 'none!important'; -@@ -361,7 +361,7 @@ function createAtomicRules(identifier, property, value) { - var _block3 = createDeclarationBlock({ - pointerEvents: 'auto' - }); -- rules.push(selector + ">*" + _block3); -+ rules.push(selector + " *" + _block3); - } - } - var _block4 = createDeclarationBlock({ diff --git a/src/App.js b/src/App.tsx similarity index 93% rename from src/App.js rename to src/App.tsx index b750d12e8c28..7c1ead1d86d3 100644 --- a/src/App.js +++ b/src/App.tsx @@ -1,5 +1,4 @@ import {PortalProvider} from '@gorhom/portal'; -import PropTypes from 'prop-types'; import React from 'react'; import {LogBox} from 'react-native'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; @@ -32,14 +31,11 @@ import * as Session from './libs/actions/Session'; import * as Environment from './libs/Environment/Environment'; import InitialUrlContext from './libs/InitialUrlContext'; import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext'; +import type {Route} from './ROUTES'; -const propTypes = { - /** Initial url that may be passed as deeplink from Hybrid App */ - url: PropTypes.string, -}; - -const defaultProps = { - url: undefined, +type AppProps = { + /** If we have an authToken this is true */ + url?: Route; }; // For easier debugging and development, when we are in web we expose Onyx to the window, so you can more easily set data into Onyx @@ -57,7 +53,7 @@ LogBox.ignoreLogs([ const fill = {flex: 1}; -function App({url}) { +function App({url}: AppProps) { useDefaultDragAndDrop(); OnyxUpdateManager(); return ( @@ -88,6 +84,7 @@ function App({url}) { + {/* @ts-expect-error TODO: Remove this once Expensify (https://github.com/Expensify/App/issues/25231) is migrated to TypeScript. */} @@ -97,8 +94,6 @@ function App({url}) { ); } -App.propTypes = propTypes; -App.defaultProps = defaultProps; App.displayName = 'App'; export default App; diff --git a/src/CONST.ts b/src/CONST.ts index 6c726cde12f7..eae4b8ec7a2b 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -189,6 +189,7 @@ const CONST = { UNIX_EPOCH: '1970-01-01 00:00:00.000', MAX_DATE: '9999-12-31', MIN_DATE: '0001-01-01', + ORDINAL_DAY_OF_MONTH: 'do', }, SMS: { DOMAIN: '@expensify.sms', @@ -462,8 +463,6 @@ const CONST = { EMPTY_ARRAY, EMPTY_OBJECT, USE_EXPENSIFY_URL, - NEW_ZOOM_MEETING_URL: 'https://zoom.us/start/videomeeting', - NEW_GOOGLE_MEET_MEETING_URL: 'https://meet.google.com/new', GOOGLE_MEET_URL_ANDROID: 'https://meet.google.com', GOOGLE_DOC_IMAGE_LINK_MATCH: 'googleusercontent.com', IMAGE_BASE64_MATCH: 'base64', @@ -500,6 +499,7 @@ const CONST = { ADMIN_POLICIES_URL: 'admin_policies', ADMIN_DOMAINS_URL: 'admin_domains', INBOX: 'inbox', + DISMMISSED_REASON: '?dismissedReason=missingFeatures', }, SIGN_IN_FORM_WIDTH: 300, @@ -736,7 +736,6 @@ const CONST = { REPORT_INITIAL_RENDER: 'report_initial_render', SWITCH_REPORT: 'switch_report', SIDEBAR_LOADED: 'sidebar_loaded', - OPEN_SEARCH: 'open_search', LOAD_SEARCH_OPTIONS: 'load_search_options', COLD: 'cold', WARM: 'warm', @@ -1336,9 +1335,9 @@ const CONST = { OWNER_EMAIL_FAKE: '_FAKE_', OWNER_ACCOUNT_ID_FAKE: 0, REIMBURSEMENT_CHOICES: { - REIMBURSEMENT_YES: 'reimburseYes', - REIMBURSEMENT_NO: 'reimburseNo', - REIMBURSEMENT_MANUAL: 'reimburseManual', + REIMBURSEMENT_YES: 'reimburseYes', // Direct + REIMBURSEMENT_NO: 'reimburseNo', // None + REIMBURSEMENT_MANUAL: 'reimburseManual', // Indirect }, ID_FAKE: '_FAKE_', EMPTY: 'EMPTY', @@ -1507,7 +1506,7 @@ const CONST = { GUIDES_CALL_TASK_IDS: { CONCIERGE_DM: 'NewExpensifyConciergeDM', WORKSPACE_INITIAL: 'WorkspaceHome', - WORKSPACE_OVERVIEW: 'WorkspaceOverview', + WORKSPACE_PROFILE: 'WorkspaceProfile', WORKSPACE_CARD: 'WorkspaceCorporateCards', WORKSPACE_REIMBURSE: 'WorkspaceReimburseReceipts', WORKSPACE_BILLS: 'WorkspacePayBills', @@ -1568,6 +1567,10 @@ const CONST = { FORM_CHARACTER_LIMIT: 50, LEGAL_NAMES_CHARACTER_LIMIT: 150, LOGIN_CHARACTER_LIMIT: 254, + + TITLE_CHARACTER_LIMIT: 100, + DESCRIPTION_LIMIT: 500, + WORKSPACE_NAME_CHARACTER_LIMIT: 80, AVATAR_CROP_MODAL: { // The next two constants control what is min and max value of the image crop scale. @@ -3174,6 +3177,14 @@ const CONST = { CHAT_SPLIT: 'newDotSplitChat', }, + MANAGE_TEAMS_CHOICE: { + MULTI_LEVEL: 'multiLevelApproval', + CUSTOM_EXPENSE: 'customExpenseCoding', + CARD_TRACKING: 'companyCardTracking', + ACCOUNTING: 'accountingIntegrations', + RULE: 'ruleEnforcement', + }, + MINI_CONTEXT_MENU_MAX_ITEMS: 4, WORKSPACE_SWITCHER: { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 9fed8b69db79..b46d3db8b60d 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -424,7 +424,7 @@ type OnyxValues = { [ONYXKEYS.WALLET_TERMS]: OnyxTypes.WalletTerms; [ONYXKEYS.BANK_ACCOUNT_LIST]: OnyxTypes.BankAccountList; [ONYXKEYS.FUND_LIST]: OnyxTypes.FundList; - [ONYXKEYS.CARD_LIST]: Record; + [ONYXKEYS.CARD_LIST]: OnyxTypes.CardList; [ONYXKEYS.WALLET_STATEMENT]: OnyxTypes.WalletStatement; [ONYXKEYS.PERSONAL_BANK_ACCOUNT]: OnyxTypes.PersonalBankAccount; [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: OnyxTypes.ReimbursementAccount; @@ -465,7 +465,8 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories; [ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS]: OnyxTypes.PolicyReportFields; [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMembers; - [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: Record; + [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: OnyxTypes.InvitedEmailsToAccountIDs | undefined; + [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT]: string | undefined; [ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report; [ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata; [ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportActions; @@ -547,8 +548,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: OnyxTypes.GetPhysicalCardForm; + [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.GetPhysicalCardForm; [ONYXKEYS.FORMS.REPORT_FIELD_EDIT_FORM]: OnyxTypes.ReportFieldEditForm; [ONYXKEYS.FORMS.REPORT_FIELD_EDIT_FORM_DRAFT]: OnyxTypes.Form; // @ts-expect-error Different values are defined under the same key: ReimbursementAccount and ReimbursementAccountForm diff --git a/src/ROUTES.ts b/src/ROUTES.ts index e987c5b94d7d..5a2ab8cfc7de 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -125,13 +125,12 @@ const ROUTES = { route: 'settings/wallet/card/:domain/activate', getRoute: (domain: string) => `settings/wallet/card/${domain}/activate` as const, }, - SETTINGS_PERSONAL_DETAILS: 'settings/profile/personal-details', - SETTINGS_PERSONAL_DETAILS_LEGAL_NAME: 'settings/profile/personal-details/legal-name', - SETTINGS_PERSONAL_DETAILS_DATE_OF_BIRTH: 'settings/profile/personal-details/date-of-birth', - SETTINGS_PERSONAL_DETAILS_ADDRESS: 'settings/profile/personal-details/address', - SETTINGS_PERSONAL_DETAILS_ADDRESS_COUNTRY: { - route: 'settings/profile/personal-details/address/country', - getRoute: (country: string, backTo?: string) => getUrlWithBackToParam(`settings/profile/personal-details/address/country?country=${country}`, backTo), + SETTINGS_LEGAL_NAME: 'settings/profile/legal-name', + SETTINGS_DATE_OF_BIRTH: 'settings/profile/date-of-birth', + SETTINGS_ADDRESS: 'settings/profile/address', + SETTINGS_ADDRESS_COUNTRY: { + route: 'settings/profile/address/country', + getRoute: (country: string, backTo?: string) => getUrlWithBackToParam(`settings/profile/address/country?country=${country}`, backTo), }, SETTINGS_CONTACT_METHODS: { route: 'settings/profile/contact-methods', @@ -281,10 +280,6 @@ const ROUTES = { route: ':iouType/new/currency/:reportID?', getRoute: (iouType: string, reportID: string, currency: string, backTo: string) => `${iouType}/new/currency/${reportID}?currency=${currency}&backTo=${backTo}` as const, }, - MONEY_REQUEST_DESCRIPTION: { - route: ':iouType/new/description/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/description/${reportID}` as const, - }, MONEY_REQUEST_CATEGORY: { route: ':iouType/new/category/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/category/${reportID}` as const, @@ -347,9 +342,9 @@ const ROUTES = { getUrlWithBackToParam(`create/${iouType}/date/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_DESCRIPTION: { - route: 'create/:iouType/description/:transactionID/:reportID', - getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/description/${transactionID}/${reportID}`, backTo), + route: ':action/:iouType/description/:transactionID/:reportID', + getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`${action}/${iouType}/description/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_DISTANCE: { route: 'create/:iouType/distance/:transactionID/:reportID', @@ -413,6 +408,10 @@ const ROUTES = { NEW_TASK_TITLE: 'new/task/title', NEW_TASK_DESCRIPTION: 'new/task/description', + ONBOARD: 'onboard', + ONBOARD_MANAGE_EXPENSES: 'onboard/manage-expenses', + ONBOARD_EXPENSIFY_CLASSIC: 'onboard/expensify-classic', + TEACHERS_UNITE: 'teachersunite', I_KNOW_A_TEACHER: 'teachersunite/i-know-a-teacher', I_AM_A_TEACHER: 'teachersunite/i-am-a-teacher', @@ -437,17 +436,17 @@ const ROUTES = { route: 'workspace/:policyID/invite-message', getRoute: (policyID: string) => `workspace/${policyID}/invite-message` as const, }, - WORKSPACE_OVERVIEW: { - route: 'workspace/:policyID/overview', - getRoute: (policyID: string) => `workspace/${policyID}/overview` as const, + WORKSPACE_PROFILE: { + route: 'workspace/:policyID/profile', + getRoute: (policyID: string) => `workspace/${policyID}/profile` as const, }, - WORKSPACE_OVERVIEW_CURRENCY: { - route: 'workspace/:policyID/overview/currency', - getRoute: (policyID: string) => `workspace/${policyID}/overview/currency` as const, + WORKSPACE_PROFILE_CURRENCY: { + route: 'workspace/:policyID/profile/currency', + getRoute: (policyID: string) => `workspace/${policyID}/profile/currency` as const, }, - WORKSPACE_OVERVIEW_NAME: { - route: 'workspace/:policyID/overview/name', - getRoute: (policyID: string) => `workspace/${policyID}/overview/name` as const, + WORKSPACE_PROFILE_NAME: { + route: 'workspace/:policyID/profile/name', + getRoute: (policyID: string) => `workspace/${policyID}/profile/name` as const, }, WORKSPACE_AVATAR: { route: 'workspace/:policyID/avatar', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index e2f0e9745561..1d7c77bf129c 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -49,14 +49,10 @@ const SCREENS = { PRONOUNS: 'Settings_Pronouns', TIMEZONE: 'Settings_Timezone', TIMEZONE_SELECT: 'Settings_Timezone_Select', - - PERSONAL_DETAILS: { - INITIAL: 'Settings_PersonalDetails_Initial', - LEGAL_NAME: 'Settings_PersonalDetails_LegalName', - DATE_OF_BIRTH: 'Settings_PersonalDetails_DateOfBirth', - ADDRESS: 'Settings_PersonalDetails_Address', - ADDRESS_COUNTRY: 'Settings_PersonalDetails_Address_Country', - }, + LEGAL_NAME: 'Settings_LegalName', + DATE_OF_BIRTH: 'Settings_DateOfBirth', + ADDRESS: 'Settings_Address', + ADDRESS_COUNTRY: 'Settings_Address_Country', }, PREFERENCES: { @@ -104,6 +100,7 @@ const SCREENS = { PARTICIPANTS: 'Participants', MONEY_REQUEST: 'MoneyRequest', NEW_TASK: 'NewTask', + ONBOARD_ENGAGEMENT: 'Onboard_Engagement', TEACHERS_UNITE: 'TeachersUnite', TASK_DETAILS: 'Task_Details', ENABLE_PAYMENTS: 'EnablePayments', @@ -150,7 +147,6 @@ const SCREENS = { CONFIRMATION: 'Money_Request_Confirmation', CURRENCY: 'Money_Request_Currency', DATE: 'Money_Request_Date', - DESCRIPTION: 'Money_Request_Description', CATEGORY: 'Money_Request_Category', MERCHANT: 'Money_Request_Merchant', WAYPOINT: 'Money_Request_Waypoint', @@ -198,7 +194,7 @@ const SCREENS = { WORKSPACE: { INITIAL: 'Workspace_Initial', - OVERVIEW: 'Workspace_Overview', + PROFILE: 'Workspace_Profile', CARD: 'Workspace_Card', REIMBURSE: 'Workspace_Reimburse', RATE_AND_UNIT: 'Workspace_RateAndUnit', @@ -208,8 +204,8 @@ const SCREENS = { MEMBERS: 'Workspace_Members', INVITE: 'Workspace_Invite', INVITE_MESSAGE: 'Workspace_Invite_Message', - CURRENCY: 'Workspace_Overview_Currency', - NAME: 'Workspace_Overview_Name', + CURRENCY: 'Workspace_Profile_Currency', + NAME: 'Workspace_Profile_Name', }, EDIT_REQUEST: { @@ -230,6 +226,12 @@ const SCREENS = { EDIT_CURRENCY: 'SplitDetails_Edit_Currency', }, + ONBOARD_ENGAGEMENT: { + ROOT: 'Onboard_Engagement_Root', + MANAGE_TEAMS_EXPENSES: 'Manage_Teams_Expenses', + EXPENSIFY_CLASSIC: 'Expenisfy_Classic', + }, + I_KNOW_A_TEACHER: 'I_Know_A_Teacher', INTRO_SCHOOL_PRINCIPAL: 'Intro_School_Principal', I_AM_A_TEACHER: 'I_Am_A_Teacher', diff --git a/src/components/AddressForm.js b/src/components/AddressForm.js index 68d451e5c7c8..aee1b652b22c 100644 --- a/src/components/AddressForm.js +++ b/src/components/AddressForm.js @@ -67,7 +67,7 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS const styles = useThemeStyles(); const {translate} = useLocalize(); const zipSampleFormat = lodashGet(CONST.COUNTRY_ZIP_REGEX_DATA, [country, 'samples'], ''); - const zipFormat = translate('common.zipCodeExampleFormat', {zipSampleFormat}); + const zipFormat = ['common.zipCodeExampleFormat', {zipSampleFormat}]; const isUSAForm = country === CONST.COUNTRY.US; /** @@ -103,7 +103,7 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS if (countrySpecificZipRegex) { if (!countrySpecificZipRegex.test(values.zipPostCode.trim().toUpperCase())) { if (ValidationUtils.isRequiredFulfilled(values.zipPostCode.trim())) { - errors.zipPostCode = ['privatePersonalDetails.error.incorrectZipFormat', {zipFormat: countryZipFormat}]; + errors.zipPostCode = ['privatePersonalDetails.error.incorrectZipFormat', countryZipFormat]; } else { errors.zipPostCode = 'common.error.fieldRequired'; } diff --git a/src/components/AddressSearch/types.ts b/src/components/AddressSearch/types.ts index 8016f1b2ea39..9b4254a9bc45 100644 --- a/src/components/AddressSearch/types.ts +++ b/src/components/AddressSearch/types.ts @@ -1,6 +1,7 @@ import type {RefObject} from 'react'; import type {NativeSyntheticEvent, StyleProp, TextInputFocusEventData, View, ViewStyle} from 'react-native'; import type {Place} from 'react-native-google-places-autocomplete'; +import type {MaybePhraseKey} from '@libs/Localize'; import type Locale from '@src/types/onyx/Locale'; type CurrentLocationButtonProps = { @@ -43,7 +44,7 @@ type AddressSearchProps = { onBlur?: () => void; /** Error text to display */ - errorText?: string; + errorText?: MaybePhraseKey; /** Hint text to display */ hint?: string; diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 78ef72ac3536..ab39e5379230 100755 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -436,7 +436,7 @@ function AttachmentModal({ }, }); } - if (!isOffline) { + if (!isOffline && allowDownload) { menuItems.push({ icon: Expensicons.Download, text: translate('common.download'), diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js index 010d074d1da6..f55db3dd0620 100644 --- a/src/components/AvatarWithImagePicker.js +++ b/src/components/AvatarWithImagePicker.js @@ -421,7 +421,7 @@ function AvatarWithImagePicker({ {errorData.validationError && ( )} diff --git a/src/components/BlockingViews/FullPageNotFoundView.tsx b/src/components/BlockingViews/FullPageNotFoundView.tsx index 8cabf7dce494..5039de3b20b6 100644 --- a/src/components/BlockingViews/FullPageNotFoundView.tsx +++ b/src/components/BlockingViews/FullPageNotFoundView.tsx @@ -7,7 +7,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import variables from '@styles/variables'; import type {TranslationPaths} from '@src/languages/types'; -import ROUTES from '@src/ROUTES'; import BlockingView from './BlockingView'; import ForceFullScreenView from './ForceFullScreenView'; @@ -50,7 +49,7 @@ function FullPageNotFoundView({ titleKey = 'notFound.notHere', subtitleKey = 'notFound.pageNotFound', linkKey = 'notFound.goBackHome', - onBackButtonPress = () => Navigation.goBack(ROUTES.HOME), + onBackButtonPress = () => Navigation.goBack(), shouldShowLink = true, shouldShowBackButton = true, onLinkPress = () => Navigation.dismissModal(), diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx index 6af3a4c6d477..34bc3f7e30c8 100644 --- a/src/components/Breadcrumbs.tsx +++ b/src/components/Breadcrumbs.tsx @@ -38,7 +38,7 @@ function Breadcrumbs({breadcrumbs, style}: BreadcrumbsProps) { const [primaryBreadcrumb, secondaryBreadcrumb] = breadcrumbs; return ( - + {primaryBreadcrumb.type === CONST.BREADCRUMB_TYPE.ROOT ? (
; /** Error text to display */ - errorText?: string; + errorText?: MaybePhraseKey; /** Value for checkbox. This prop is intended to be set by FormProvider only */ value?: boolean; diff --git a/src/components/CommunicationsLink.js b/src/components/CommunicationsLink.js deleted file mode 100644 index 01ae0354a66d..000000000000 --- a/src/components/CommunicationsLink.js +++ /dev/null @@ -1,51 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import useThemeStyles from '@hooks/useThemeStyles'; -import Clipboard from '@libs/Clipboard'; -import ContextMenuItem from './ContextMenuItem'; -import * as Expensicons from './Icon/Expensicons'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; - -const propTypes = { - /** Children to wrap in CommunicationsLink. */ - children: PropTypes.node.isRequired, - - /** Styles to be assigned to Container */ - // eslint-disable-next-line react/forbid-prop-types - containerStyles: PropTypes.arrayOf(PropTypes.object), - - /** Value to be copied or passed via tap. */ - value: PropTypes.string.isRequired, - - ...withLocalizePropTypes, -}; - -const defaultProps = { - containerStyles: [], -}; - -function CommunicationsLink(props) { - const styles = useThemeStyles(); - return ( - - - {props.children} - Clipboard.setString(props.value)} - /> - - - ); -} - -CommunicationsLink.propTypes = propTypes; -CommunicationsLink.defaultProps = defaultProps; -CommunicationsLink.displayName = 'CommunicationsLink'; - -export default withLocalize(CommunicationsLink); diff --git a/src/components/CommunicationsLink.tsx b/src/components/CommunicationsLink.tsx new file mode 100644 index 000000000000..646326e0a632 --- /dev/null +++ b/src/components/CommunicationsLink.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Clipboard from '@libs/Clipboard'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import ContextMenuItem from './ContextMenuItem'; +import * as Expensicons from './Icon/Expensicons'; + +type CommunicationsLinkProps = ChildrenProps & { + /** Styles to be assigned to Container */ + containerStyles?: StyleProp; + + /** Value to be copied or passed via tap. */ + value: string; +}; + +function CommunicationsLink({value, containerStyles, children}: CommunicationsLinkProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + return ( + + + {children} + Clipboard.setString(value)} + /> + + + ); +} + +CommunicationsLink.displayName = 'CommunicationsLink'; + +export default CommunicationsLink; diff --git a/src/components/ConnectBankAccountButton.js b/src/components/ConnectBankAccountButton.js deleted file mode 100644 index f036918d9429..000000000000 --- a/src/components/ConnectBankAccountButton.js +++ /dev/null @@ -1,57 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import Navigation from '@libs/Navigation/Navigation'; -import * as ReimbursementAccount from '@userActions/ReimbursementAccount'; -import Button from './Button'; -import * as Expensicons from './Icon/Expensicons'; -import networkPropTypes from './networkPropTypes'; -import {withNetwork} from './OnyxProvider'; -import Text from './Text'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; - -const propTypes = { - ...withLocalizePropTypes, - - /** Information about the network */ - network: networkPropTypes.isRequired, - - /** PolicyID for navigating to bank account route of that policy */ - policyID: PropTypes.string.isRequired, - - /** Button styles, also applied for offline message wrapper */ - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), -}; - -const defaultProps = { - style: [], -}; - -function ConnectBankAccountButton(props) { - const styles = useThemeStyles(); - const activeRoute = Navigation.getActiveRouteWithoutParams(); - return props.network.isOffline ? ( - - {`${props.translate('common.youAppearToBeOffline')} ${props.translate('common.thisFeatureRequiresInternet')}`} - - ) : ( -