diff --git a/.eslintignore b/.eslintignore index 26ecb1ae7cc7..aa10a3073f4e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -14,3 +14,4 @@ web/gtm.js src/libs/SearchParser/searchParser.js src/libs/SearchParser/autocompleteParser.js help/_scripts/** +modules/** diff --git a/.eslintrc.changed.js b/.eslintrc.changed.js index c279c3e67a51..a72dd6a9250a 100644 --- a/.eslintrc.changed.js +++ b/.eslintrc.changed.js @@ -6,5 +6,14 @@ module.exports = { }, rules: { 'deprecation/deprecation': 'error', + 'rulesdir/no-default-id-values': 'error', }, + overrides: [ + { + files: ['src/libs/ReportUtils.ts', 'src/libs/actions/IOU.ts', 'src/libs/actions/Report.ts', 'src/libs/actions/Task.ts'], + rules: { + 'rulesdir/no-default-id-values': 'off', + }, + }, + ], }; diff --git a/.github/workflows/testBuildHybrid.yml b/.github/workflows/testBuildHybrid.yml index 42a5f15f8910..c8027f93a2a5 100644 --- a/.github/workflows/testBuildHybrid.yml +++ b/.github/workflows/testBuildHybrid.yml @@ -1,4 +1,4 @@ -name: Build and deploy hybird apps for testing +name: Build and deploy hybrid apps for testing on: workflow_dispatch: diff --git a/.gitmodules b/.gitmodules index 7b3a3d9f9432..59abf2448f1d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "Mobile-Expensify"] path = Mobile-Expensify - url = https://github.com/Expensify/Mobile-Expensify.git + url = git@github.com:Expensify/Mobile-Expensify.git diff --git a/Mobile-Expensify b/Mobile-Expensify index d81135521e25..9854d5bfa2e3 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit d81135521e25add10bfebebf83fa5ec4a67cca13 +Subproject commit 9854d5bfa2e31c702066e161256e0a9655051892 diff --git a/README.md b/README.md index 8f7161b7fe96..fcc5e2b934e9 100644 --- a/README.md +++ b/README.md @@ -456,12 +456,12 @@ You can only build HybridApp if you have been granted access to [`Mobile-Expensi ## Getting started with HybridApp 1. If you haven't, please follow [these instructions](https://github.com/Expensify/App?tab=readme-ov-file#getting-started) to setup the NewDot local environment. -2. Run `git submodule update --init --progress` to download the `Mobile-Expensify` sourcecode. -- If you have access to `Mobile-Expensify` and the command fails with a https-related error add this to your `~/.gitconfig` file: +2. Run `git submodule update --init --progress --depth 100` to download the `Mobile-Expensify` sourcecode. +- If you have access to `Mobile-Expensify` and the command fails, add this to your `~/.gitconfig` file: ``` - [url "ssh://git@github.com/"] - insteadOf = https://github.com/ + [url "https://github.com/"] + insteadOf = ssh://git@github.com/ ``` At this point, the default behavior of some `npm` scripts will change to target HybridApp: @@ -472,7 +472,7 @@ At this point, the default behavior of some `npm` scripts will change to target - `npm run pod-install` - install pods for HybridApp - `npm run clean` - clean native code of HybridApp -If for some reason, you need to target the standalone NewDot application, you can append `*-standalone` to each of these scripts (eg. `npm run ios-standalone` will build NewDot instead of HybridApp). +If for some reason, you need to target the standalone NewDot application, you can append `*-standalone` to each of these scripts (eg. `npm run ios-standalone` will build NewDot instead of HybridApp). The same concept applies to the installation of standalone NewDot node modules. To skip the installation of HybridApp-specific patches and node modules, use `npm run i-standalone` or `npm run install-standalone`. ## Working with HybridApp Day-to-day work with HybridApp shouldn't differ much from the work on the standalone NewDot repo. diff --git a/android/app/build.gradle b/android/app/build.gradle index f75c9d2eb80b..675c8b917e7a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009007606 - versionName "9.0.76-6" + versionCode 1009007702 + versionName "9.0.77-2" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 6d6406551cdd..bef985265d7f 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -10,6 +10,7 @@ # Add any project specific keep options here: -keep class com.expensify.chat.BuildConfig { *; } -keep class com.facebook.** { *; } +-keep class com.margelo.nitro.** { *; } -keep, allowoptimization, allowobfuscation class expo.modules.** { *; } # Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items). diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4a86cc34dccf..c30b16eca797 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ + diff --git a/assets/images/companyCards/card-bofa.svg b/assets/images/companyCards/card-bofa.svg index 3cc7cf1de2cc..c58229f1b242 100644 --- a/assets/images/companyCards/card-bofa.svg +++ b/assets/images/companyCards/card-bofa.svg @@ -1 +1,27 @@ - \ No newline at end of file + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/companyCards/card-capitalone.svg b/assets/images/companyCards/card-capitalone.svg index a7c54c7bf529..9f1402298683 100644 --- a/assets/images/companyCards/card-capitalone.svg +++ b/assets/images/companyCards/card-capitalone.svg @@ -1 +1,23 @@ - \ No newline at end of file + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/companyCards/large/card-bofa-large.svg b/assets/images/companyCards/large/card-bofa-large.svg index a842bc93d80b..c83e06ffb65d 100644 --- a/assets/images/companyCards/large/card-bofa-large.svg +++ b/assets/images/companyCards/large/card-bofa-large.svg @@ -1,6 +1,6 @@ - + - - - - - - - + + + + + + + \ No newline at end of file diff --git a/assets/images/companyCards/large/card-capital_one-large.svg b/assets/images/companyCards/large/card-capital_one-large.svg index b71e209a4c11..20f3bd442d9e 100644 --- a/assets/images/companyCards/large/card-capital_one-large.svg +++ b/assets/images/companyCards/large/card-capital_one-large.svg @@ -1,15 +1,15 @@ - + - - + + \ No newline at end of file diff --git a/assets/images/train.svg b/assets/images/train.svg new file mode 100644 index 000000000000..40d8c9d1af8a --- /dev/null +++ b/assets/images/train.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/articles/expensify-classic/connections/Expensify-API.md b/docs/articles/expensify-classic/connections/Expensify-API.md new file mode 100644 index 000000000000..fba85dcd154a --- /dev/null +++ b/docs/articles/expensify-classic/connections/Expensify-API.md @@ -0,0 +1,231 @@ +--- +title: Expensify API +description: User-sourced tips and tricks for using Expensify’s API. +--- +# Overview +An API (Application Programming Interface) allows two programs to communicate with each other. Expensify's API connects with various software platforms like NetSuite or Xero, and it can also link to other systems that don’t have a pre-made connection, such as [Workday](https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/Workday). + +{% include info.html %} +To begin, review our [Integration Server Manual](https://integrations.expensify.com/Integration-Server/doc/#introduction) thoroughly, as it will be your primary resource. The Expensify API is a self-serve tool, and your internal team is responsible for setting it up and ensuring it meets your needs. We can assist with basic troubleshooting, but the level of support may vary based on the support agent or account manager. It’s important for your team to be familiar with the setup process. +{% include end-info.html %} + +We've compiled answers to some frequently asked questions to help you get started. + +**Should I give your support team my API credentials when I need help?** + +If you’re seeking help with Expensify's API, do not share your partnerUserSecret. If you do, immediately rotate your credentials on [this page](https://www.expensify.com/tools/integrations/). + +**Is there a rate limit?** + +To keep our platform stable and handle high traffic, Expensify limits how many API requests you can send: +- Up to 5 requests every 10 seconds +- Up to 20 requests every 60 seconds + +Sending more requests than allowed may result in an error with status code `429`. + +**What is a Policy ID?** + +This is also known as a Workspace ID. To find your Policy/Workspace ID, +Hover over Settings and click Workspaces. +Click the name of the Workspace. +Copy the ID number from the URL. For example, if the URL is https://www.expensify.com/policy?param={"policyID":"0810E551A5F2A9C2”}, then your workspace ID is 0810E551A5F2A9C2. + +**Can I use the parent type `file` to export workspace/policy data?** + +No. The parent type `file` can only be used to export expense and report data — not policy information. To export policy data (e.g., categories, tags), you must use the `get` type with `inputSettings.type` set to `policy`. + +**Can I use the API to create Domain Groups?** + +No, you cannot create domain groups. You can only assign users to them. + +**I’m exporting expense IDs `${expense.transactionID}` but when I open my CSV in Excel, it’s changing all the IDs and making them look the same. How can I prevent this?** + +Try prepending a non-numeric character like a quote to force Excel to interpret the value as a string and not a number (i.e., `'${expense.transactionID}`). + +**How can we export the person who will approve a report while the reports are still processing?** + +Use the field ${report.managerEmail}. + +**Why won’t my boolean field return any data?** + +Boolean fields won't output values without a string. For example, instead of using `${expense.billable}`, use `${expense.billable?string("Yes", "No")}`. This will display "Yes" if the expense is billable and "No" if it is not. + +**Can I export the reports for just one user?** + +Not in a quick convenient way, as you would need to include the user in your template. The simplest approach is to export data for all users and then apply a filter in your preferred spreadsheet program. + +**Can I create expenses on behalf of users?** + +Yes. However, to access the Expense Creator API on behalf of employees, Expensify needs to verify the following setup: + +Ensure you are properly configured (e.g., Domain Control, Domain Admin, Policy Admin). +Verify you have internal authorization to add data to other accounts within your domain. + +If you need this access, contact concierge@expensify.com and reference this help page. + +## Using Postman + +Many customers use Postman to help them build out their APIs. Below are some guides contributed by our customers. Please note, in all cases, you will need to first generate your authentication credentials, the steps for which can be found [here](https://integrations.expensify.com/Integration-Server/doc/#introduction) and have them ready: + +### Download expenses from a report as a CSV file + +**Step 1: Get the ID of a report you want to export in Expensify** + +Find the ID by opening the expense report and clicking Details at the top right corner of the page. At the top of the menu, the ID is provided as the “Long ID.” + +**Step 3: Export (generate) a "Report" as a CSV file** +{% include info.html %} +For this you'll use the Documentation under [Report Exporter](https://integrations.expensify.com/Integration-Server/doc/#export). +{% include end-info.html %} + +In Postman, set the following: + +- HTTP Action: POST +- URL: https://integrations.expensify.com/Integration-Server/ExpensifyIntegrations +- Your only Parameters ("Params") will be "requestJobDescription", described below +- Body: "x-www-form-encoded", with a key "template", described below + +The requestJobDescription key will have a value like below: + +``` +{ + "type": "file", + "credentials": { + "partnerUserID": "my_user_id", + "partnerUserSecret": "my_user_secret" + }, + "onReceive": { + "immediateResponse": [ + "returnRandomFileName" + ] + }, + "inputSettings": { + "type": "combinedReportData", + "filters": { + "reportIDList": "50352738" + } + }, + "outputSettings": { + "fileExtension": "csv" + } +} +``` +Take the above and replace it with your own partnerUserID, partnerUserSecret, and reportIDList. To download multiple reports, you can use a comma-separated list as the reportIDList, such as "12345,45678,11111". + +The template key will have the value like below: + +``` +<#if addHeader> + Merchant,Amount,Transaction Date<#lt> + +<#list reports as report> + <#list report.transactionList as expense> + <#if expense.modifiedMerchant?has_content> + <#assign merchant = expense.modifiedMerchant> + <#else> + <#assign merchant = expense.merchant> + + <#if expense.convertedAmount?has_content> + <#assign amount = expense.convertedAmount/100> + <#elseif expense.modifiedAmount?has_content> + <#assign amount = expense.modifiedAmount/100> + <#else> + <#assign amount = expense.amount/100> + + <#if expense.modifiedCreated?has_content> + <#assign created = expense.modifiedCreated> + <#else> + <#assign created = expense.created> + + ${merchant},<#t> + ${amount},<#t> + ${created}<#lt> + + +``` + +The template variable determines what information is saved in your CSV file. If you want more columns than merchant, amount, and transaction date, follow the syntax as defined in the export template format documentation. + +**Step 4: Save your generated file name** + +Expensify currently supports only the "onReceive":{"immediateResponse":["returnRandomFileName"]} option in step 3, so you should receive a random filename back from the API like "exportc111111d-a1a1-a1a1-a1a1-d1111111f.csv". You will need to document this filename if you plan on running the download command after this one. + +**Step 5: Download your exported report** + +Set up another API call in almost the same way you did before. You don't need the template key in the Body anymore, so delete that and set the Body type to "none". Then modify your requestJobDescription to read like below, but with your own credentials and file name: + +``` +{ + "type": "download", + "credentials": { + "partnerUserID": "my_user_id", + "partnerUserSecret": "my_user_secret" + }, + "fileName": "exportc111111d-a1a1-a1a1-a1a1-d1111111f.csv", + "fileSystem": "integrationServer" +} +``` + +Click Go and you should see the CSV in the response body. + +*Thank you to our customer Frederico Pettinella who originally wrote and shared this guide.* + +### Use Advanced Employee Updater API with Postman + +1. Create a new request. +2. Select POST as the method. +3. Copy-paste this to the URL section: https://integrations.expensify.com/Integration-Server/ExpensifyIntegrations +4. Do not add anything to "Params", "Authorization", or "Header". Go straight to "Body". +5. Select "x-www-form-urlencoded" and add 2 keys "requestJobDescription" and "data". +6. For "requestJobDescription" copy and paste the following text, and replace the values for "partnerUserID", "partner_UserSecret", and "recipients". Remember that "dry-run"=true means that it's just for testing. Set it to false whenever you are ready to modify that in production. + +``` +{ + "type": "update", + "dry-run" : true, + "credentials": { + "partnerUserID": "aa_api_domain_com", + "partnerUserSecret": "xxx" + }, + "dataSource" : "request", + "inputSettings": { + "type": "employees", + "entity": "generic" + }, + "onFinish":[ + {"actionName": "email", "recipients":"admin1@domain.com"} + ] + }' +For "data" copy-paste the following text and replace values as needed +{ + "Employees":[ + { + "employeeEmail": "user@domain.com", + "managerEmail": "usermanager@domain.com", + "policyID": "1D1BC525C4892584", +"isTerminated": "false", + } +]} +``` + +7. Click SEND. + +This is how it should look on Postman: + +![Image of API credentials request]({{site.url}}/assets/images/ExpensifyHelp-Postman-userID-userSecret-request.png){:width="100%"} + +![Image of API data request]({{site.url}}/assets/images/ExpensifyHelp-Postman-Request-data.png){:width="100%"} + +This is how the value looks inside those keys: + +![Image of API dry run]({{site.url}}/assets/images/ExpensifyHelp-Postman-Successful-dryrun-response.png){:width="100%"} + +Remember that there are 4 [required fields](https://integrations.expensify.com/Integration-Server/doc/employeeUpdater/#api-principles) needed to make this API call to work: + +- employeeEmail +- managerEmail +- employeeID +- policyID + +*Thank you to our customer Raul Hernandez who originally wrote and shared this guide.* + diff --git a/docs/articles/expensify-classic/expenses/Apply-Tax.md b/docs/articles/expensify-classic/expenses/Apply-Tax.md deleted file mode 100644 index 9360962cb2ba..000000000000 --- a/docs/articles/expensify-classic/expenses/Apply-Tax.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: Apply Tax -description: This is article shows you how to apply taxes to your expenses! ---- - - - -# About - -There are two types of tax in Expensify: Simple Tax (i.e. one tax rate) and Complex Tax (i.e. more than one tax rate). This article shows you how to apply both to your expenses! - - -# How-to Apply Tax - -When Tax Tracking is enabled on a Workspace, the default tax rate is selected under **Settings > Workspace > _Workspace Name_ > Tax**, with the default tax rate applied to all expenses automatically. - -There may be multiple tax rates set up within your Workspace, so if the tax on your receipt is different to the default tax that has been applied, you can select the appropriate rate from the tax drop-down on the web expense editor or the mobile app. - -If the tax amount on your receipt is different to the calculated amount or the tax rate doesn’t show up, you can always manually type in the correct tax amount. - - -{% include faq-begin.md %} - -## How do I set up multiple taxes (GST/PST/QST) on indirect connections? -Expenses sometimes have more than one tax applied to them - for example in Canada, expenses can have both a Federal GST and a provincial PST or QST. - -To handle these, you can create a single tax that combines both taxes into a single effective tax rate. For example, if you have a GST of 5% and PST of 7%, adding the two tax rates together gives you an effective tax rate of 12%. - -From the Reports page, you can select Reports and then click **Export To > Tax Report** to generate a CSV containing all the expense information, including the split-out taxes. - -## Why is the tax amount different than I expect? - -In Expensify, tax is *inclusive*, meaning it's already part of the total amount shown. - -To determine the inclusive tax from a total price that already includes tax, you can use the following formula: - -### **Tax amount = (Total price x Tax rate) ÷ (1 + Tax Rate)** - -For example, if an item costs $100 and the tax rate is 20%: -Tax amount = (**$100** x .20) ÷ (1 + .**20**) = **$16.67** -This means the tax amount $16.67 is included in the total. - -If you are simply trying to calculate the price before tax, you can use the formula: - -### **Price before tax = (Total price) ÷ (1 + Tax rate)** - -# Deep Dive - -If you have a receipt that has more than one tax rate (i.e. Complex Tax) on it, then there are two options for handling this in Expensify! - -Many tax authorities do not require the reporting of tax amounts by rate and the easiest approach is to apply the highest rate on the receipt and then modify the tax amount to reflect the amount shown on the receipt if this is less. Please check with your local tax advisor if this approach will be allowed. - -Alternatively, you can apply each specific tax rate by splitting the expense into the components that each rate will be applied to. To do this, click on **Split Expense** and apply the correct tax rate to each part. - -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspaces/Tax-Tracking.md b/docs/articles/expensify-classic/workspaces/Tax-Tracking.md deleted file mode 100644 index c47e5ed51f32..000000000000 --- a/docs/articles/expensify-classic/workspaces/Tax-Tracking.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: Tax -description: How to track expense taxes ---- -# Overview -Expensify’s tax tracking feature allows you to: -- Add tax names, rates, and codes whether you’re connected to an accounting system or not. -- Enable/disable taxes you’d like to make available to users. -- Set a default tax for Workspace currency expenses and, optionally, another default tax (including exempt) for foreign currency expenses which - will automatically apply to all new expenses. - -# How to Enable Tax Tracking -Tax tracking can be enabled in the Tax section of the Workspace settings of any Workspace, whether group or individual. -## If Connected to an Accounting Integration -If your group Workspace is connected to Xero, QuickBooks Online, Sage Intacct, or NetSuite, make sure to first enable tax via the connection configuration page (Settings > Workspaces > Group > [Workspace Name] > Connections > Configure) and then sync the connection. Your tax rates will be imported from the accounting system and indicated by its logo. -## Not Connected to an Accounting Integration -If your Workspace is not connected to an accounting system, go to Settings > Workspaces > Group > [Workspace Name] > Tax to enable tax. - -# Tracking Tax by Expense Category -To set a different tax rate for a specific expense type in the Workspace currency, go to Settings > Workspaces > Group > [Workspace Name] > Categories page. Click "Edit Rules" next to the desired category and set the "Category default tax". This will be applied to new expenses, overriding the default Workspace currency tax rate. diff --git a/docs/articles/expensify-classic/workspaces/Track-Taxes.md b/docs/articles/expensify-classic/workspaces/Track-Taxes.md new file mode 100644 index 000000000000..c75058dc8447 --- /dev/null +++ b/docs/articles/expensify-classic/workspaces/Track-Taxes.md @@ -0,0 +1,76 @@ +--- +title: Track Taxes +description: How to track taxes and apply them to expenses +--- +Expensify's tax tracking allows you to create tax rates and codes for domestic and foreign currencies, and even for different expense categories. Once you've enabled tax tracking, your default tax rate is automatically applied to all expenses. + +# Tax Tracking - Connected to an accounting integration + +If your Workspace is connected to Xero, QuickBooks Online, Sage Intacct, or NetSuite, you can run through the following steps to set up tax tracking: +1. Hover over **Settings**, then click **Workspaces**. +2. Click the desired workspace name. +3. Click the **Connections** tab on the left. +4. Click **Configure**. +5. Click **Sync Connection**. + +Your tax rates will be imported from the accounting system and indicated by its logo. + +# Tax Tracking - Not connected to an accounting integration + +If your Workspace is not connected to an accounting system, you can run through the following steps to set up tax tracking: +1. Hover over **Settings**, then click **Workspaces**. +2. Click the desired workspace name. +3. Click the **Tax** tab on the left. +4. Enable the toggle to allow taxes to be added to expenses. +5. You can modify the existing tax rate, or you can click New Option to add a new tax rate. For each tax rate, you can enable/disable them individually, add a specific name for the rate, add a percent value, and (if desired) add a unique tax code. +6. Once you have your tax codes added, go to the top of the screen to enter the name that taxes will appear as on expenses. You'll also select which of your tax rates you will use as your defaults for expenses submitted under your workspace currency and foreign currency. + +## Track tax by expense category + +You can also set tax rates for specific expense categories: +1. Hover over **Settings**, then click **Workspaces**. +2. Click the desired workspace name. +3. Click the **Categories** tab on the left. +4. Click **Edit** next to the desired category. +5. Click the Default Tax dropdown and select the desired tax rate. + +This rate will be applied to all new expenses under this category, overriding the workspace's default currency tax rate. + +{% include faq-begin.md %} + +## How do I set up multiple taxes (GST/PST/QST) for indirect connections? + +Expenses sometimes have more than one tax applied to them (for example in Canada, expenses can have both a Federal GST and a provincial PST or QST). + +To handle multiple tax rates, you can create a new tax rate that combines both into a single rate. For example, if you have a GST of 5% and PST of 7%, you can add them together and create a new tax rate of 12%. + +From the Reports page, you can generate a CSV containing all the expense information, including the split-out taxes, by going to the Reports tab, clicking **Export To**, and selecting **Tax Report**. + +## How do I handle the taxes for a receipt that includes more than one tax rate? + +If your receipt includes more than one tax rate, there are two ways you can handle the tax rate: + +- Many tax authorities do not require the reporting of tax amounts by rate; therefore, you can apply the highest rate on the receipt and then modify the tax amount on the receipt if necessary. Please check with your tax advisor to determine if this approach is appropriate for you. +- Alternatively, you can apply each specific tax rate by splitting the expense by the applicable expenses for each rate. To do this, open the expense and click **Split Expense**. Then apply the correct tax rate to each. + +## What if my workspace has multiple tax rates? + +You'll have the option to change the tax rate from within the expense as needed. + +## What should I do if the tax amount for my expense does not show up, or is it showing as a different amount than what I expected? + +In Expensify, tax is *inclusive*, meaning it's already part of the total amount shown. If the tax amount doesn't show up on your receipt or is different than the calculated amount, you can manually type in the correct tax amount. + +To determine the inclusive tax from a total price that already includes tax, you can use the following formula: + +**Tax amount = (Total price x Tax rate) ÷ (1 + Tax Rate)** + +For example, if an item costs $100 and the tax rate is 20%: +Tax amount = (**$100** x .20) ÷ (1 + .**20**) = **$16.67** +This means the tax amount of $16.67 is included in the total. + +If you are simply trying to calculate the price before tax, you can use the formula: + +**Price before tax = (Total price) ÷ (1 + Tax rate)** + +{% include faq-end.md %} diff --git a/docs/articles/new-expensify/expenses-&-payments/Export-download-expenses.md b/docs/articles/new-expensify/expenses-&-payments/Export-download-expenses.md index 6f7292245f00..8593ab65205b 100644 --- a/docs/articles/new-expensify/expenses-&-payments/Export-download-expenses.md +++ b/docs/articles/new-expensify/expenses-&-payments/Export-download-expenses.md @@ -15,7 +15,7 @@ The first step to exporting and downloading expenses is finding the data you nee 3. Select your Filters on the top right to filter by credit card used, coding, date range, keyword, expense value and a number of other useful criteria 4. Hit View Results to see all expenses that match your filters - ## Download Expenses +## Download Expenses 1. Select the checkbox to the left of the expenses or select all with the very top checkbox. 2. Click **# selected** at the top-right and select **Download**. diff --git a/docs/assets/images/search-hold-01.png b/docs/assets/images/search-hold-01.png new file mode 100644 index 000000000000..04745c570367 Binary files /dev/null and b/docs/assets/images/search-hold-01.png differ diff --git a/docs/assets/images/search-hold-02.png b/docs/assets/images/search-hold-02.png new file mode 100644 index 000000000000..3c7c39defd66 Binary files /dev/null and b/docs/assets/images/search-hold-02.png differ diff --git a/docs/assets/images/search-hold-03.png b/docs/assets/images/search-hold-03.png new file mode 100644 index 000000000000..81fbddcf5d75 Binary files /dev/null and b/docs/assets/images/search-hold-03.png differ diff --git a/docs/assets/images/search-hold-04.png b/docs/assets/images/search-hold-04.png new file mode 100644 index 000000000000..e5c1b71c0e37 Binary files /dev/null and b/docs/assets/images/search-hold-04.png differ diff --git a/docs/assets/images/search-hold-05.png b/docs/assets/images/search-hold-05.png new file mode 100644 index 000000000000..2d111abecb65 Binary files /dev/null and b/docs/assets/images/search-hold-05.png differ diff --git a/docs/redirects.csv b/docs/redirects.csv index 751e072fb13f..04eba2e6152c 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -385,7 +385,7 @@ https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Vac https://community.expensify.com/discussion/5678/deep-dive-secondary-login-merge-accounts-what-does-this-mean,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Merge-accounts https://community.expensify.com/discussion/5103/how-to-create-and-use-custom-units/,https://help.expensify.com/ https://community.expensify.com/discussion/6530/how-to-set-your-time-zone-for-report-history-comments,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Set-time-zone -https://community.expensify.com/discussion/5651/deep-dive--practices-when-youre-running-into-trouble-receiving-emails-from-expensify,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Set-Notifications +https://community.expensify.com/discussion/5651/deep-dive-best-practices-when-youre-running-into-trouble-receiving-emails-from-expensify,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Set-Notifications https://community.expensify.com/discussion/5793/how-to-connect-your-personal-card-to-import-expenses/,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Personal-Credit-Cards https://community.expensify.com/discussion/5677/deep-dive-security-how-expensify-protects-your-information,https://help.expensify.com/articles/new-expensify/settings/Encryption-and-Data-Security https://community.expensify.com/discussion/4641/how-to-add-a-u-s-deposit-account,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-Personal-US-Bank-Account @@ -608,3 +608,5 @@ https://help.expensify.com/articles/expensify-classic/travel/Edit-or-cancel-trav https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-and-Pay-Bills https://help.expensify.com/articles/expensify-classic/settings/Set-Notifications,https://help.expensify.com/articles/expensify-classic/settings/Email-Notifications https://help.expensify.com/articles/new-expensify/expenses-&-payments/Export-expenses,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Export-download-expenses +https://help.expensify.com/articles/expensify-classic/expenses/Apply-Tax,https://help.expensify.com/articles/expensify-classic/workspaces/Track-Taxes +https://help.expensify.com/articles/expensify-classic/workspaces/Tax-Tracking,https://help.expensify.com/articles/expensify-classic/workspaces/Track-Taxes \ No newline at end of file diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 27de0846d9d3..fe4c07fdac9e 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.76 + 9.0.77 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.76.6 + 9.0.77.2 FullStory OrgId @@ -67,6 +67,8 @@ NSCameraUsageDescription Your camera is used to create chat attachments, documents, and facial capture. + NSContactsUsageDescription + Import contacts from your phone so your favorite people are always a tap away. NSLocationAlwaysAndWhenInUseUsageDescription Your location is used to determine your default currency and timezone. NSLocationWhenInUseUsageDescription diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index d2bfbdefba61..6a92f70d678f 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.76 + 9.0.77 CFBundleSignature ???? CFBundleVersion - 9.0.76.6 + 9.0.77.2 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 0b61137e2127..7ae2ed9457e8 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.76 + 9.0.77 CFBundleVersion - 9.0.76.6 + 9.0.77.2 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile b/ios/Podfile index 41dc5179752d..bdad8a0ec396 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -24,6 +24,7 @@ prepare_react_native_project! setup_permissions([ 'Camera', + 'Contacts', 'LocationAccuracy', 'LocationAlways', 'LocationWhenInUse' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 31ff58598c82..0389642465da 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -28,6 +28,28 @@ PODS: - AppAuth/Core - AppLogs (0.1.0) - boost (1.84.0) + - ContactsModule (0.0.1): + - DoubleConversion + - glog + - hermes-engine + - NitroModules + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - DoubleConversion (1.1.6) - EXAV (14.0.7): - ExpoModulesCore @@ -287,6 +309,29 @@ PODS: - nanopb/encode (= 2.30908.0) - nanopb/decode (2.30908.0) - nanopb/encode (2.30908.0) + - NitroModules (0.18.1): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - Onfido (29.7.2) - onfido-react-native-sdk (10.6.0): - DoubleConversion @@ -1722,7 +1767,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-keyboard-controller (1.14.4): + - react-native-keyboard-controller (1.15.0): - DoubleConversion - glog - hermes-engine @@ -2410,7 +2455,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.207): + - RNLiveMarkdown (0.1.209): - DoubleConversion - glog - hermes-engine @@ -2430,10 +2475,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/newarch (= 0.1.207) + - RNLiveMarkdown/newarch (= 0.1.209) - RNReanimated/worklets - Yoga - - RNLiveMarkdown/newarch (0.1.207): + - RNLiveMarkdown/newarch (0.1.209): - DoubleConversion - glog - hermes-engine @@ -2750,6 +2795,7 @@ DEPENDENCIES: - AirshipServiceExtension - AppLogs (from `../node_modules/react-native-app-logs/AppLogsPod`) - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) + - ContactsModule (from `../modules/ContactsNitroModule`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - EXAV (from `../node_modules/expo-av/ios`) - EXImageLoader (from `../node_modules/expo-image-loader/ios`) @@ -2765,6 +2811,7 @@ DEPENDENCIES: - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - lottie-react-native (from `../node_modules/lottie-react-native`) + - NitroModules (from `../node_modules/react-native-nitro-modules`) - "onfido-react-native-sdk (from `../node_modules/@onfido/react-native-sdk`)" - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) @@ -2915,6 +2962,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-app-logs/AppLogsPod" boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" + ContactsModule: + :path: "../modules/ContactsNitroModule" DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" EXAV: @@ -2946,6 +2995,8 @@ EXTERNAL SOURCES: :tag: hermes-2024-08-15-RNv0.75.1-4b3bf912cc0f705b51b71ce1a5b8bd79b93a451b lottie-react-native: :path: "../node_modules/lottie-react-native" + NitroModules: + :path: "../node_modules/react-native-nitro-modules" onfido-react-native-sdk: :path: "../node_modules/@onfido/react-native-sdk" RCT-Folly: @@ -3160,6 +3211,7 @@ SPEC CHECKSUMS: AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa AppLogs: 3bc4e9b141dbf265b9464409caaa40416a9ee0e0 boost: 26992d1adf73c1c7676360643e687aee6dda994b + ContactsModule: 21671b28654413dc28795d1afc3b12eaffa28ed1 DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5 EXAV: afa491e598334bbbb92a92a2f4dd33d7149ad37f EXImageLoader: ab589d67d6c5f2c33572afea9917304418566334 @@ -3199,6 +3251,7 @@ SPEC CHECKSUMS: MapboxMaps: e76b14f52c54c40b76ddecd04f40448e6f35a864 MapboxMobileEvents: de50b3a4de180dd129c326e09cd12c8adaaa46d6 nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 + NitroModules: ebe2ba2d01dc03c1f82441561fe6062b8c3c4366 Onfido: f3af62ea1c9a419589c133e3e511e5d2c4f3f8af onfido-react-native-sdk: 4ccfdeb10f9ccb4a5799d2555cdbc2a068a42c0d Plaid: c32f22ffce5ec67c9e6147eaf6c4d7d5f8086d89 @@ -3242,7 +3295,7 @@ SPEC CHECKSUMS: react-native-geolocation: b9bd12beaf0ebca61a01514517ca8455bd26fa06 react-native-image-picker: f8a13ff106bcc7eb00c71ce11fdc36aac2a44440 react-native-key-command: aae312752fcdfaa2240be9a015fc41ce54087546 - react-native-keyboard-controller: 97bb7b48fa427c7455afdc8870c2978efd9bfa3a + react-native-keyboard-controller: 3428e4761623fd6a242d9bf3573112f8ebe92238 react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d react-native-netinfo: fb5112b1fa754975485884ae85a3fb6a684f49d5 react-native-pager-view: abc5ef92699233eb726442c7f452cac82f73d0cb @@ -3292,10 +3345,10 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 8781e2529230a1bc3ea8d75e5c3cd071b6c6aed7 RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: 8f9d9b32a25969ddb5f59eb92136b73823bbd141 + RNLiveMarkdown: f19d3c962fba4fb87bb9bc27ce9119216d86d92e RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: 460d6ff97ae49c7d5708c3212c6521697c36a0c4 - RNPermissions: 0b1429b55af59d1d08b75a8be2459f65a8ac3f28 + RNPermissions: 9e5c26aaa982fe00743281f6f47fbdc050ebc58f RNReactNativeHapticFeedback: 73756a3477a5a622fa16862a3ab0d0fc5e5edff5 RNReanimated: d95f865e1e42c34ca56b987e0719a8c72fc02dbc RNScreens: de6e57426ba0e6cbc3fb5b4f496e7f08cb2773c2 @@ -3311,6 +3364,6 @@ SPEC CHECKSUMS: VisionCamera: c95a8ad535f527562be1fb05fb2fd324578e769c Yoga: a1d7895431387402a674fd0d1c04ec85e87909b8 -PODFILE CHECKSUM: 615266329434ea4a994dccf622008a2197313c88 +PODFILE CHECKSUM: e744fa802b4bee097ff8d1977dd8f79d16b21547 COCOAPODS: 1.15.2 diff --git a/modules/ContactsNitroModule/.gitignore b/modules/ContactsNitroModule/.gitignore new file mode 100644 index 000000000000..d3b53dfce541 --- /dev/null +++ b/modules/ContactsNitroModule/.gitignore @@ -0,0 +1,78 @@ +# OSX +# +.DS_Store + +# XDE +.expo/ + +# VSCode +.vscode/ +jsconfig.json + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace + +# Android/IJ +# +.classpath +.cxx +.gradle +.idea +.project +.settings +local.properties +android.iml + +# Cocoapods +# +example/ios/Pods + +# Ruby +example/vendor/ + +# node.js +# +node_modules/ +npm-debug.log +yarn-debug.log +yarn-error.log + +# BUCK +buck-out/ +\.buckd/ +android/app/libs +android/keystores/debug.keystore + +# Yarn +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Expo +.expo/ + +# Turborepo +.turbo/ + +# generated by bob +lib/ diff --git a/modules/ContactsNitroModule/.watchmanconfig b/modules/ContactsNitroModule/.watchmanconfig new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/modules/ContactsNitroModule/.watchmanconfig @@ -0,0 +1 @@ +{} diff --git a/modules/ContactsNitroModule/ContactsModule.podspec b/modules/ContactsNitroModule/ContactsModule.podspec new file mode 100644 index 000000000000..5f0b012c7b52 --- /dev/null +++ b/modules/ContactsNitroModule/ContactsModule.podspec @@ -0,0 +1,29 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +Pod::Spec.new do |s| + s.name = "ContactsModule" + s.version = package["version"] + s.summary = package["description"] + s.homepage = package["homepage"] + s.license = package["license"] + s.authors = package["author"] + + s.platforms = { :ios => min_ios_version_supported } + s.source = { :git => "https://github.com/mrousavy/nitro.git", :tag => "#{s.version}" } + + s.source_files = [ + # Implementation (Swift) + "ios/**/*.{swift}", + # Autolinking/Registration (Objective-C++) + "ios/**/*.{m,mm}", + # Implementation (C++ objects) + "cpp/**/*.{hpp,cpp}", + ] + + load 'nitrogen/generated/ios/ContactsModule+autolinking.rb' + add_nitrogen_files(s) + + install_modules_dependencies(s) +end diff --git a/modules/ContactsNitroModule/android/CMakeLists.txt b/modules/ContactsNitroModule/android/CMakeLists.txt new file mode 100644 index 000000000000..beb0c308df07 --- /dev/null +++ b/modules/ContactsNitroModule/android/CMakeLists.txt @@ -0,0 +1,29 @@ +project(ContactsModule) +cmake_minimum_required(VERSION 3.9.0) + +set (PACKAGE_NAME ContactsModule) +set (CMAKE_VERBOSE_MAKEFILE ON) +set (CMAKE_CXX_STANDARD 20) + +# Define C++ library and add all sources +add_library(${PACKAGE_NAME} SHARED + src/main/cpp/cpp-adapter.cpp +) + +# Add Nitrogen specs :) +include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/ContactsModule+autolinking.cmake) + +# Set up local includes +include_directories( + "src/main/cpp" + "../cpp" +) + +find_library(LOG_LIB log) + +# Link all libraries together +target_link_libraries( + ${PACKAGE_NAME} + ${LOG_LIB} + android # <-- Android core +) diff --git a/modules/ContactsNitroModule/android/build.gradle b/modules/ContactsNitroModule/android/build.gradle new file mode 100644 index 000000000000..0b414c88dea3 --- /dev/null +++ b/modules/ContactsNitroModule/android/build.gradle @@ -0,0 +1,130 @@ +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "com.android.tools.build:gradle:7.2.1" + } +} + +def reactNativeArchitectures() { + def value = rootProject.getProperties().get("reactNativeArchitectures") + return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] +} + +def isNewArchitectureEnabled() { + return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true" +} + +apply plugin: "com.android.library" +apply plugin: 'org.jetbrains.kotlin.android' +apply from: '../nitrogen/generated/android/ContactsModule+autolinking.gradle' + +if (isNewArchitectureEnabled()) { + apply plugin: "com.facebook.react" +} + +def getExtOrDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["ContactsModule_" + name] +} + +def getExtOrIntegerDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ContactsModule_" + name]).toInteger() +} + +android { + namespace "com.margelo.nitro.contacts" + + ndkVersion getExtOrDefault("ndkVersion") + compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") + + defaultConfig { + minSdkVersion getExtOrIntegerDefault("minSdkVersion") + targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") + buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() + + externalNativeBuild { + cmake { + cppFlags "-O2 -frtti -fexceptions -Wall -fstack-protector-all" + arguments "-DANDROID_STL=c++_shared" + abiFilters (*reactNativeArchitectures()) + } + } + } + + externalNativeBuild { + cmake { + path "CMakeLists.txt" + } + } + + packagingOptions { + excludes = [ + "META-INF", + "META-INF/**", + "**/libc++_shared.so", + "**/libfbjni.so", + "**/libjsi.so", + "**/libfolly_json.so", + "**/libfolly_runtime.so", + "**/libglog.so", + "**/libhermes.so", + "**/libhermes-executor-debug.so", + "**/libhermes_executor.so", + "**/libreactnativejni.so", + "**/libturbomodulejsijni.so", + "**/libreact_nativemodule_core.so", + "**/libjscexecutor.so" + ] + } + + buildFeatures { + buildConfig true + prefab true + } + + buildTypes { + release { + minifyEnabled false + } + } + + lintOptions { + disable "GradleCompatible" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + if (isNewArchitectureEnabled()) { + java.srcDirs += [ + // React Codegen files + "${project.buildDir}/generated/source/codegen/java" + ] + } + } + } +} + +repositories { + mavenCentral() + google() +} + + +dependencies { + // For < 0.71, this will be from the local maven repo + // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin + //noinspection GradleDynamicVersion + implementation "com.facebook.react:react-native:+" + + // Add a dependency on NitroModules + implementation project(":react-native-nitro-modules") +} + diff --git a/modules/ContactsNitroModule/android/gradle.properties b/modules/ContactsNitroModule/android/gradle.properties new file mode 100644 index 000000000000..59d3858d1bb9 --- /dev/null +++ b/modules/ContactsNitroModule/android/gradle.properties @@ -0,0 +1,5 @@ +ContactsModule_kotlinVersion=1.9.24 +ContactsModule_minSdkVersion=23 +ContactsModule_targetSdkVersion=34 +ContactsModule_compileSdkVersion=34 +ContactsModule_ndkVersion=26.1.10909125 diff --git a/modules/ContactsNitroModule/android/src/main/AndroidManifest.xml b/modules/ContactsNitroModule/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..a2f47b6057db --- /dev/null +++ b/modules/ContactsNitroModule/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/modules/ContactsNitroModule/android/src/main/cpp/cpp-adapter.cpp b/modules/ContactsNitroModule/android/src/main/cpp/cpp-adapter.cpp new file mode 100644 index 000000000000..7a88410f3e4d --- /dev/null +++ b/modules/ContactsNitroModule/android/src/main/cpp/cpp-adapter.cpp @@ -0,0 +1,6 @@ +#include +#include "ContactsModuleOnLoad.hpp" + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { + return margelo::nitro::contacts::initialize(vm); +} diff --git a/modules/ContactsNitroModule/android/src/main/java/com/margelo/nitro/contacts/ContactsModulePackage.java b/modules/ContactsNitroModule/android/src/main/java/com/margelo/nitro/contacts/ContactsModulePackage.java new file mode 100644 index 000000000000..e8c26844ce86 --- /dev/null +++ b/modules/ContactsNitroModule/android/src/main/java/com/margelo/nitro/contacts/ContactsModulePackage.java @@ -0,0 +1,34 @@ +package com.margelo.nitro.contacts; + +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.module.model.ReactModuleInfoProvider; +import com.facebook.react.TurboReactPackage; +import com.margelo.nitro.core.HybridObject; +import com.margelo.nitro.core.HybridObjectRegistry; + +import java.util.HashMap; +import java.util.function.Supplier; + +public class ContactsModulePackage extends TurboReactPackage { + @Nullable + @Override + public NativeModule getModule(String name, ReactApplicationContext reactContext) { + return null; + } + + @Override + public ReactModuleInfoProvider getReactModuleInfoProvider() { + return () -> { + return new HashMap<>(); + }; + } + + static { + System.loadLibrary("ContactsModule"); + } +} diff --git a/modules/ContactsNitroModule/android/src/main/java/com/margelo/nitro/contacts/HybridContactsModule.kt b/modules/ContactsNitroModule/android/src/main/java/com/margelo/nitro/contacts/HybridContactsModule.kt new file mode 100644 index 000000000000..00feaa7660c2 --- /dev/null +++ b/modules/ContactsNitroModule/android/src/main/java/com/margelo/nitro/contacts/HybridContactsModule.kt @@ -0,0 +1,166 @@ +package com.margelo.nitro.contacts + +import android.Manifest +import android.content.pm.PackageManager +import android.provider.ContactsContract +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import com.facebook.react.bridge.ReactApplicationContext +import com.margelo.nitro.NitroModules +import com.margelo.nitro.core.Promise + +class HybridContactsModule : HybridContactsModuleSpec() { + @Volatile + private var estimatedMemorySize: Long = 0 + + override val memorySize: Long + get() = estimatedMemorySize + + private val context: ReactApplicationContext? = NitroModules.applicationContext + + private fun requestContactPermission(): Boolean { + val currentActivity = context?.currentActivity + return if (currentActivity != null) { + ActivityCompat.requestPermissions( + currentActivity, arrayOf(REQUIRED_PERMISSION), PERMISSION_REQUEST_CODE + ) + true + } else { + false + } + } + + private fun hasPhoneContactsPermission(): Boolean { + return context?.let { + ContextCompat.checkSelfPermission(it, Manifest.permission.READ_CONTACTS) + } == PackageManager.PERMISSION_GRANTED + } + + override fun getAll(keys: Array): Promise> { + return Promise.parallel { + val contacts = mutableListOf() + if (!hasPhoneContactsPermission()) { + requestContactPermission() + return@parallel emptyArray() + } + + context?.contentResolver?.let { resolver -> + val projection = arrayOf( + ContactsContract.Data.MIMETYPE, + ContactsContract.Data.CONTACT_ID, + ContactsContract.Data.DISPLAY_NAME, + ContactsContract.Contacts.PHOTO_URI, + ContactsContract.Contacts.PHOTO_THUMBNAIL_URI, + ContactsContract.Data.DATA1, + ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, + ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, + ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME + ) + + val selection = "${ContactsContract.Data.MIMETYPE} IN (?, ?, ?)" + val selectionArgs = arrayOf( + ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE, + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, + ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE + ) + + val sortOrder = "${ContactsContract.Data.CONTACT_ID} ASC" + + resolver.query( + ContactsContract.Data.CONTENT_URI, + projection, + selection, + selectionArgs, + sortOrder + )?.use { cursor -> + val mimeTypeIndex = cursor.getColumnIndex(ContactsContract.Data.MIMETYPE) + val contactIdIndex = cursor.getColumnIndex(ContactsContract.Data.CONTACT_ID) + val photoUriIndex = cursor.getColumnIndex(ContactsContract.Contacts.PHOTO_URI) + val thumbnailUriIndex = + cursor.getColumnIndex(ContactsContract.Contacts.PHOTO_THUMBNAIL_URI) + val data1Index = cursor.getColumnIndex(ContactsContract.Data.DATA1) + val givenNameIndex = + cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME) + val familyNameIndex = + cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME) + val middleNameIndex = + cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME) + + var currentContact: Contact? = null + var currentContactId: String? = null + val currentPhoneNumbers = mutableListOf() + val currentEmailAddresses = mutableListOf() + + while (cursor.moveToNext()) { + val contactId = cursor.getString(contactIdIndex) + val mimeType = cursor.getString(mimeTypeIndex) + + if (contactId != currentContactId) { + currentContact?.let { contact -> + contacts.add( + contact.copy( + phoneNumbers = currentPhoneNumbers.toTypedArray(), + emailAddresses = currentEmailAddresses.toTypedArray() + ) + ) + } + currentPhoneNumbers.clear() + currentEmailAddresses.clear() + currentContact = Contact( + firstName = "", + lastName = "", + middleName = null, + phoneNumbers = emptyArray(), + emailAddresses = emptyArray(), + imageData = cursor.getString(photoUriIndex) ?: "", + thumbnailImageData = cursor.getString(thumbnailUriIndex) ?: "" + ) + currentContactId = contactId + } + + when (mimeType) { + ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> { + currentContact = currentContact?.copy( + firstName = cursor.getString(givenNameIndex) ?: "", + lastName = cursor.getString(familyNameIndex) ?: "", + middleName = cursor.getString(middleNameIndex) + ) + } + + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> { + cursor.getString(data1Index)?.let { phone -> + currentPhoneNumbers.add(StringHolder(phone)) + } + } + + ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE -> { + cursor.getString(data1Index)?.let { email -> + currentEmailAddresses.add(StringHolder(email)) + } + } + } + } + + // Add the last contact + currentContact?.let { contact -> + contacts.add( + contact.copy( + phoneNumbers = currentPhoneNumbers.toTypedArray(), + emailAddresses = currentEmailAddresses.toTypedArray() + ) + ) + } + } + } + + // Update memory size based on contact count + estimatedMemorySize = contacts.size.toLong() * 1024 // Assume ~1KB per contact + contacts.toTypedArray() + } + } + + companion object { + const val PERMISSION_REQUEST_CODE = 1 + const val REQUIRED_PERMISSION = Manifest.permission.READ_CONTACTS + } +} diff --git a/modules/ContactsNitroModule/babel.config.js b/modules/ContactsNitroModule/babel.config.js new file mode 100644 index 000000000000..3e0218e68fc3 --- /dev/null +++ b/modules/ContactsNitroModule/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['module:@react-native/babel-preset'], +} diff --git a/modules/ContactsNitroModule/ios/HybridContactsModule.swift b/modules/ContactsNitroModule/ios/HybridContactsModule.swift new file mode 100644 index 000000000000..59cc3ea31702 --- /dev/null +++ b/modules/ContactsNitroModule/ios/HybridContactsModule.swift @@ -0,0 +1,72 @@ +import NitroModules +import Contacts +import Foundation + +final class HybridContactsModule: HybridContactsModuleSpec { + public var hybridContext = margelo.nitro.HybridContext() + public var memorySize: Int { MemoryLayout.size } + + private let contactStore = CNContactStore() + private let imageDirectory: URL + private let fieldToKeyDescriptor: [ContactFields: CNKeyDescriptor] = [ + .firstName: CNContactGivenNameKey as CNKeyDescriptor, + .lastName: CNContactFamilyNameKey as CNKeyDescriptor, + .phoneNumbers: CNContactPhoneNumbersKey as CNKeyDescriptor, + .emailAddresses: CNContactEmailAddressesKey as CNKeyDescriptor, + .middleName: CNContactMiddleNameKey as CNKeyDescriptor, + .imageData: CNContactImageDataKey as CNKeyDescriptor, + .thumbnailImageData: CNContactThumbnailImageDataKey as CNKeyDescriptor, + .givenNameKey: CNContactGivenNameKey as CNKeyDescriptor + ] + + init() { + imageDirectory = FileManager.default.temporaryDirectory.appendingPathComponent("ContactImages") + try? FileManager.default.createDirectory(at: imageDirectory, withIntermediateDirectories: true) + } + + func getAll(keys: [ContactFields]) throws -> Promise<[Contact]> { + Promise.async { [unowned self] in + let keysSet = Set(keys) + let keysToFetch = keys.compactMap { self.fieldToKeyDescriptor[$0] } + guard !keysToFetch.isEmpty else { return [] } + + let request = CNContactFetchRequest(keysToFetch: keysToFetch) + var contacts = [Contact]() + contacts.reserveCapacity(1000) + + try self.contactStore.enumerateContacts(with: request) { contact, _ in + contacts.append(self.processContact(contact, keysSet: keysSet)) + } + + return contacts + } + } + + @inline(__always) + private func processContact(_ contact: CNContact, keysSet: Set) -> Contact { + Contact( + firstName: keysSet.contains(.firstName) ? contact.givenName : nil, + lastName: keysSet.contains(.lastName) ? contact.familyName : nil, + middleName: keysSet.contains(.middleName) ? contact.middleName : nil, + phoneNumbers: keysSet.contains(.phoneNumbers) ? contact.phoneNumbers.map { StringHolder(value: $0.value.stringValue) } : nil, + emailAddresses: keysSet.contains(.emailAddresses) ? contact.emailAddresses.map { StringHolder(value: $0.value as String) } : nil, + imageData: keysSet.contains(.imageData) ? getImagePath(for: contact, isThumbnail: false) : nil, + thumbnailImageData: keysSet.contains(.thumbnailImageData) ? getImagePath(for: contact, isThumbnail: true) : nil + ) + } + + @inline(__always) + private func getImagePath(for contact: CNContact, isThumbnail: Bool) -> String? { + let imageData = isThumbnail ? contact.thumbnailImageData : contact.imageData + guard let data = imageData else { return nil } + + let fileName = "\(contact.identifier)_\(isThumbnail ? "thumb" : "full").jpg" + let fileURL = imageDirectory.appendingPathComponent(fileName) + + if !FileManager.default.fileExists(atPath: fileURL.path) { + try? data.write(to: fileURL, options: .atomic) + } + + return fileURL.path + } +} diff --git a/modules/ContactsNitroModule/nitro.json b/modules/ContactsNitroModule/nitro.json new file mode 100644 index 000000000000..426f8486118a --- /dev/null +++ b/modules/ContactsNitroModule/nitro.json @@ -0,0 +1,17 @@ +{ + "cxxNamespace": ["contacts"], + "ios": { + "iosModuleName": "ContactsModule" + }, + "android": { + "androidNamespace": ["contacts"], + "androidCxxLibName": "ContactsModule" + }, + "autolinking": { + "ContactsModule": { + "swift": "HybridContactsModule", + "kotlin": "HybridContactsModule" + } + }, + "ignorePaths": ["node_modules"] +} diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModule+autolinking.cmake b/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModule+autolinking.cmake new file mode 100644 index 000000000000..5478bc224b05 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModule+autolinking.cmake @@ -0,0 +1,59 @@ +# +# ContactsModule+autolinking.cmake +# This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +# https://github.com/mrousavy/nitro +# Copyright © 2024 Marc Rousavy @ Margelo +# + +# This is a CMake file that adds all files generated by Nitrogen +# to the current CMake project. +# +# To use it, add this to your CMakeLists.txt: +# ```cmake +# include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/ContactsModule+autolinking.cmake) +# ``` + +# Add all headers that were generated by Nitrogen +include_directories( + "../nitrogen/generated/shared/c++" + "../nitrogen/generated/android/c++" + "../nitrogen/generated/android/" +) + +# Add all .cpp sources that were generated by Nitrogen +target_sources( + # CMake project name (Android C++ library name) + ContactsModule PRIVATE + # Autolinking Setup + ../nitrogen/generated/android/ContactsModuleOnLoad.cpp + # Shared Nitrogen C++ sources + ../nitrogen/generated/shared/c++/HybridContactsModuleSpec.cpp + # Android-specific Nitrogen C++ sources + ../nitrogen/generated/android/c++/JHybridContactsModuleSpec.cpp +) + +# Add all libraries required by the generated specs +find_package(fbjni REQUIRED) # <-- Used for communication between Java <-> C++ +find_package(ReactAndroid REQUIRED) # <-- Used to set up React Native bindings (e.g. CallInvoker/TurboModule) +find_package(react-native-nitro-modules REQUIRED) # <-- Used to create all HybridObjects and use the Nitro core library + +# Link all libraries together +target_link_libraries( + ContactsModule + fbjni::fbjni # <-- Facebook C++ JNI helpers + ReactAndroid::jsi # <-- RN: JSI + react-native-nitro-modules::NitroModules # <-- NitroModules Core :) +) + +# Link react-native (different prefab between RN 0.75 and RN 0.76) +if(ReactAndroid_VERSION_MINOR GREATER_EQUAL 76) + target_link_libraries( + ContactsModule + ReactAndroid::reactnative # <-- RN: Native Modules umbrella prefab + ) +else() + target_link_libraries( + ContactsModule + ReactAndroid::react_nativemodule_core # <-- RN: TurboModules Core + ) +endif() diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModule+autolinking.gradle b/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModule+autolinking.gradle new file mode 100644 index 000000000000..2d19cd2ced32 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModule+autolinking.gradle @@ -0,0 +1,27 @@ +/// +/// ContactsModule+autolinking.gradle +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +/// This is a Gradle file that adds all files generated by Nitrogen +/// to the current Gradle project. +/// +/// To use it, add this to your build.gradle: +/// ```gradle +/// apply from: '../nitrogen/generated/android/ContactsModule+autolinking.gradle' +/// ``` + +logger.warn("[NitroModules] 🔥 ContactsModule is boosted by nitro!") + +android { + sourceSets { + main { + java.srcDirs += [ + // Nitrogen files + "${project.projectDir}/../nitrogen/generated/android/kotlin" + ] + } + } +} diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModuleOnLoad.cpp b/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModuleOnLoad.cpp new file mode 100644 index 000000000000..156ea811e509 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModuleOnLoad.cpp @@ -0,0 +1,42 @@ +/// +/// ContactsModuleOnLoad.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#include "ContactsModuleOnLoad.hpp" + +#include +#include +#include + +#include "JHybridContactsModuleSpec.hpp" +#include +#include + +namespace margelo::nitro::contacts { + +int initialize(JavaVM* vm) { + using namespace margelo::nitro; + using namespace margelo::nitro::contacts; + using namespace facebook; + + return facebook::jni::initialize(vm, [] { + // Register native JNI methods + margelo::nitro::contacts::JHybridContactsModuleSpec::registerNatives(); + + // Register Nitro Hybrid Objects + HybridObjectRegistry::registerHybridObjectConstructor( + "ContactsModule", + []() -> std::shared_ptr { + static DefaultConstructableObject object("com/margelo/nitro/contacts/HybridContactsModule"); + auto instance = object.create(); + auto globalRef = jni::make_global(instance); + return JNISharedPtr::make_shared_from_jni(globalRef); + } + ); + }); +} + +} // namespace margelo::nitro::contacts diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModuleOnLoad.hpp b/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModuleOnLoad.hpp new file mode 100644 index 000000000000..b71adaca07bf --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModuleOnLoad.hpp @@ -0,0 +1,25 @@ +/// +/// ContactsModuleOnLoad.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#include +#include + +namespace margelo::nitro::contacts { + + /** + * Initializes the native (C++) part of ContactsModule, and autolinks all Hybrid Objects. + * Call this in your `JNI_OnLoad` function (probably inside `cpp-adapter.cpp`). + * Example: + * ```cpp (cpp-adapter.cpp) + * JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { + * return margelo::nitro::contacts::initialize(vm); + * } + * ``` + */ + int initialize(JavaVM* vm); + +} // namespace margelo::nitro::contacts diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModuleOnLoad.kt b/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModuleOnLoad.kt new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModuleOnLoad.kt @@ -0,0 +1 @@ + diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/c++/JContact.hpp b/modules/ContactsNitroModule/nitrogen/generated/android/c++/JContact.hpp new file mode 100644 index 000000000000..bbd5354163a2 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/c++/JContact.hpp @@ -0,0 +1,114 @@ +/// +/// JContact.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "Contact.hpp" + +#include "JStringHolder.hpp" +#include "StringHolder.hpp" +#include +#include +#include + +namespace margelo::nitro::contacts { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "Contact" and the the Kotlin data class "Contact". + */ + struct JContact final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/contacts/Contact;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct Contact by copying all values to C++. + */ + [[maybe_unused]] + Contact toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldFirstName = clazz->getField("firstName"); + jni::local_ref firstName = this->getFieldValue(fieldFirstName); + static const auto fieldLastName = clazz->getField("lastName"); + jni::local_ref lastName = this->getFieldValue(fieldLastName); + static const auto fieldMiddleName = clazz->getField("middleName"); + jni::local_ref middleName = this->getFieldValue(fieldMiddleName); + static const auto fieldPhoneNumbers = clazz->getField>("phoneNumbers"); + jni::local_ref> phoneNumbers = this->getFieldValue(fieldPhoneNumbers); + static const auto fieldEmailAddresses = clazz->getField>("emailAddresses"); + jni::local_ref> emailAddresses = this->getFieldValue(fieldEmailAddresses); + static const auto fieldImageData = clazz->getField("imageData"); + jni::local_ref imageData = this->getFieldValue(fieldImageData); + static const auto fieldThumbnailImageData = clazz->getField("thumbnailImageData"); + jni::local_ref thumbnailImageData = this->getFieldValue(fieldThumbnailImageData); + return Contact( + firstName != nullptr ? std::make_optional(firstName->toStdString()) : std::nullopt, + lastName != nullptr ? std::make_optional(lastName->toStdString()) : std::nullopt, + middleName != nullptr ? std::make_optional(middleName->toStdString()) : std::nullopt, + phoneNumbers != nullptr ? std::make_optional([&]() { + size_t __size = phoneNumbers->size(); + std::vector __vector; + __vector.reserve(__size); + for (size_t __i = 0; __i < __size; __i++) { + auto __element = phoneNumbers->getElement(__i); + __vector.push_back(__element->toCpp()); + } + return __vector; + }()) : std::nullopt, + emailAddresses != nullptr ? std::make_optional([&]() { + size_t __size = emailAddresses->size(); + std::vector __vector; + __vector.reserve(__size); + for (size_t __i = 0; __i < __size; __i++) { + auto __element = emailAddresses->getElement(__i); + __vector.push_back(__element->toCpp()); + } + return __vector; + }()) : std::nullopt, + imageData != nullptr ? std::make_optional(imageData->toStdString()) : std::nullopt, + thumbnailImageData != nullptr ? std::make_optional(thumbnailImageData->toStdString()) : std::nullopt + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const Contact& value) { + return newInstance( + value.firstName.has_value() ? jni::make_jstring(value.firstName.value()) : nullptr, + value.lastName.has_value() ? jni::make_jstring(value.lastName.value()) : nullptr, + value.middleName.has_value() ? jni::make_jstring(value.middleName.value()) : nullptr, + value.phoneNumbers.has_value() ? [&]() { + size_t __size = value.phoneNumbers.value().size(); + jni::local_ref> __array = jni::JArrayClass::newArray(__size); + for (size_t __i = 0; __i < __size; __i++) { + const auto& __element = value.phoneNumbers.value()[__i]; + __array->setElement(__i, *JStringHolder::fromCpp(__element)); + } + return __array; + }() : nullptr, + value.emailAddresses.has_value() ? [&]() { + size_t __size = value.emailAddresses.value().size(); + jni::local_ref> __array = jni::JArrayClass::newArray(__size); + for (size_t __i = 0; __i < __size; __i++) { + const auto& __element = value.emailAddresses.value()[__i]; + __array->setElement(__i, *JStringHolder::fromCpp(__element)); + } + return __array; + }() : nullptr, + value.imageData.has_value() ? jni::make_jstring(value.imageData.value()) : nullptr, + value.thumbnailImageData.has_value() ? jni::make_jstring(value.thumbnailImageData.value()) : nullptr + ); + } + }; + +} // namespace margelo::nitro::contacts diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/c++/JContactFields.hpp b/modules/ContactsNitroModule/nitrogen/generated/android/c++/JContactFields.hpp new file mode 100644 index 000000000000..371b6607d105 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/c++/JContactFields.hpp @@ -0,0 +1,76 @@ +/// +/// JContactFields.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "ContactFields.hpp" + +namespace margelo::nitro::contacts { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ enum "ContactFields" and the the Kotlin enum "ContactFields". + */ + struct JContactFields final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/contacts/ContactFields;"; + + public: + /** + * Convert this Java/Kotlin-based enum to the C++ enum ContactFields. + */ + [[maybe_unused]] + ContactFields toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldOrdinal = clazz->getField("_ordinal"); + int ordinal = this->getFieldValue(fieldOrdinal); + return static_cast(ordinal); + } + + public: + /** + * Create a Java/Kotlin-based enum with the given C++ enum's value. + */ + [[maybe_unused]] + static jni::alias_ref fromCpp(ContactFields value) { + static const auto clazz = javaClassStatic(); + static const auto fieldFIRST_NAME = clazz->getStaticField("FIRST_NAME"); + static const auto fieldLAST_NAME = clazz->getStaticField("LAST_NAME"); + static const auto fieldMIDDLE_NAME = clazz->getStaticField("MIDDLE_NAME"); + static const auto fieldPHONE_NUMBERS = clazz->getStaticField("PHONE_NUMBERS"); + static const auto fieldEMAIL_ADDRESSES = clazz->getStaticField("EMAIL_ADDRESSES"); + static const auto fieldIMAGE_DATA = clazz->getStaticField("IMAGE_DATA"); + static const auto fieldTHUMBNAIL_IMAGE_DATA = clazz->getStaticField("THUMBNAIL_IMAGE_DATA"); + static const auto fieldGIVEN_NAME_KEY = clazz->getStaticField("GIVEN_NAME_KEY"); + + switch (value) { + case ContactFields::FIRST_NAME: + return clazz->getStaticFieldValue(fieldFIRST_NAME); + case ContactFields::LAST_NAME: + return clazz->getStaticFieldValue(fieldLAST_NAME); + case ContactFields::MIDDLE_NAME: + return clazz->getStaticFieldValue(fieldMIDDLE_NAME); + case ContactFields::PHONE_NUMBERS: + return clazz->getStaticFieldValue(fieldPHONE_NUMBERS); + case ContactFields::EMAIL_ADDRESSES: + return clazz->getStaticFieldValue(fieldEMAIL_ADDRESSES); + case ContactFields::IMAGE_DATA: + return clazz->getStaticFieldValue(fieldIMAGE_DATA); + case ContactFields::THUMBNAIL_IMAGE_DATA: + return clazz->getStaticFieldValue(fieldTHUMBNAIL_IMAGE_DATA); + case ContactFields::GIVEN_NAME_KEY: + return clazz->getStaticFieldValue(fieldGIVEN_NAME_KEY); + default: + std::string stringValue = std::to_string(static_cast(value)); + throw std::invalid_argument("Invalid enum value (" + stringValue + "!"); + } + } + }; + +} // namespace margelo::nitro::contacts diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/c++/JHybridContactsModuleSpec.cpp b/modules/ContactsNitroModule/nitrogen/generated/android/c++/JHybridContactsModuleSpec.cpp new file mode 100644 index 000000000000..e0505ee46d36 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/c++/JHybridContactsModuleSpec.cpp @@ -0,0 +1,84 @@ +/// +/// JHybridContactsModuleSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#include "JHybridContactsModuleSpec.hpp" + +// Forward declaration of `Contact` to properly resolve imports. +namespace margelo::nitro::contacts { struct Contact; } +// Forward declaration of `StringHolder` to properly resolve imports. +namespace margelo::nitro::contacts { struct StringHolder; } +// Forward declaration of `ContactFields` to properly resolve imports. +namespace margelo::nitro::contacts { enum class ContactFields; } + +#include +#include +#include "Contact.hpp" +#include +#include "JContact.hpp" +#include +#include +#include "StringHolder.hpp" +#include "JStringHolder.hpp" +#include "ContactFields.hpp" +#include "JContactFields.hpp" + +namespace margelo::nitro::contacts { + + jni::local_ref JHybridContactsModuleSpec::initHybrid(jni::alias_ref jThis) { + return makeCxxInstance(jThis); + } + + void JHybridContactsModuleSpec::registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", JHybridContactsModuleSpec::initHybrid), + }); + } + + size_t JHybridContactsModuleSpec::getExternalMemorySize() noexcept { + static const auto method = _javaPart->getClass()->getMethod("getMemorySize"); + return method(_javaPart); + } + + // Properties + + + // Methods + std::shared_ptr>> JHybridContactsModuleSpec::getAll(const std::vector& keys) { + static const auto method = _javaPart->getClass()->getMethod(jni::alias_ref> /* keys */)>("getAll"); + auto __result = method(_javaPart, [&]() { + size_t __size = keys.size(); + jni::local_ref> __array = jni::JArrayClass::newArray(__size); + for (size_t __i = 0; __i < __size; __i++) { + const auto& __element = keys[__i]; + __array->setElement(__i, *JContactFields::fromCpp(__element)); + } + return __array; + }()); + return [&]() { + auto __promise = Promise>::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast>(__boxedResult); + __promise->resolve([&]() { + size_t __size = __result->size(); + std::vector __vector; + __vector.reserve(__size); + for (size_t __i = 0; __i < __size; __i++) { + auto __element = __result->getElement(__i); + __vector.push_back(__element->toCpp()); + } + return __vector; + }()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + +} // namespace margelo::nitro::contacts diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/c++/JHybridContactsModuleSpec.hpp b/modules/ContactsNitroModule/nitrogen/generated/android/c++/JHybridContactsModuleSpec.hpp new file mode 100644 index 000000000000..6b94d3be37e7 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/c++/JHybridContactsModuleSpec.hpp @@ -0,0 +1,62 @@ +/// +/// HybridContactsModuleSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include +#include "HybridContactsModuleSpec.hpp" + + + + +namespace margelo::nitro::contacts { + + using namespace facebook; + + class JHybridContactsModuleSpec: public jni::HybridClass, + public virtual HybridContactsModuleSpec { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/contacts/HybridContactsModuleSpec;"; + static jni::local_ref initHybrid(jni::alias_ref jThis); + static void registerNatives(); + + protected: + // C++ constructor (called from Java via `initHybrid()`) + explicit JHybridContactsModuleSpec(jni::alias_ref jThis) : + HybridObject(HybridContactsModuleSpec::TAG), + _javaPart(jni::make_global(jThis)) {} + + public: + virtual ~JHybridContactsModuleSpec() { + // Hermes GC can destroy JS objects on a non-JNI Thread. + jni::ThreadScope::WithClassLoader([&] { _javaPart.reset(); }); + } + + public: + size_t getExternalMemorySize() noexcept override; + + public: + inline const jni::global_ref& getJavaPart() const noexcept { + return _javaPart; + } + + public: + // Properties + + + public: + // Methods + std::shared_ptr>> getAll(const std::vector& keys) override; + + private: + friend HybridBase; + using HybridBase::HybridBase; + jni::global_ref _javaPart; + }; + +} // namespace margelo::nitro::contacts diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/c++/JStringHolder.hpp b/modules/ContactsNitroModule/nitrogen/generated/android/c++/JStringHolder.hpp new file mode 100644 index 000000000000..29695fe48d58 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/c++/JStringHolder.hpp @@ -0,0 +1,52 @@ +/// +/// JStringHolder.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "StringHolder.hpp" + +#include + +namespace margelo::nitro::contacts { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "StringHolder" and the the Kotlin data class "StringHolder". + */ + struct JStringHolder final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/contacts/StringHolder;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct StringHolder by copying all values to C++. + */ + [[maybe_unused]] + StringHolder toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldValue = clazz->getField("value"); + jni::local_ref value = this->getFieldValue(fieldValue); + return StringHolder( + value->toStdString() + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const StringHolder& value) { + return newInstance( + jni::make_jstring(value.value) + ); + } + }; + +} // namespace margelo::nitro::contacts diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/Contact.kt b/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/Contact.kt new file mode 100644 index 000000000000..a6d9e59a2b2b --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/Contact.kt @@ -0,0 +1,27 @@ +/// +/// Contact.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.contacts + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* + +/** + * Represents the JavaScript object/struct "Contact". + */ +@DoNotStrip +@Keep +data class Contact( + val firstName: String?, + val lastName: String?, + val middleName: String?, + val phoneNumbers: Array?, + val emailAddresses: Array?, + val imageData: String?, + val thumbnailImageData: String? +) diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/ContactFields.kt b/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/ContactFields.kt new file mode 100644 index 000000000000..841d6c82a32b --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/ContactFields.kt @@ -0,0 +1,31 @@ +/// +/// ContactFields.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.contacts + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + +/** + * Represents the JavaScript enum/union "ContactFields". + */ +@DoNotStrip +@Keep +enum class ContactFields { + FIRST_NAME, + LAST_NAME, + MIDDLE_NAME, + PHONE_NUMBERS, + EMAIL_ADDRESSES, + IMAGE_DATA, + THUMBNAIL_IMAGE_DATA, + GIVEN_NAME_KEY; + + @DoNotStrip + @Keep + private val _ordinal = ordinal +} diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/HybridContactsModuleSpec.kt b/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/HybridContactsModuleSpec.kt new file mode 100644 index 000000000000..63a118b8be57 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/HybridContactsModuleSpec.kt @@ -0,0 +1,64 @@ +/// +/// HybridContactsModuleSpec.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.contacts + +import android.util.Log +import androidx.annotation.Keep +import com.facebook.jni.HybridData +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* + +/** + * A Kotlin class representing the ContactsModule HybridObject. + * Implement this abstract class to create Kotlin-based instances of ContactsModule. + */ +@DoNotStrip +@Keep +@Suppress("RedundantSuppression", "KotlinJniMissingFunction", "PropertyName", "RedundantUnitReturnType", "unused") +abstract class HybridContactsModuleSpec: HybridObject() { + @DoNotStrip + private var mHybridData: HybridData = initHybrid() + + init { + // Pass this `HybridData` through to it's base class, + // to represent inheritance to JHybridObject on C++ side + super.updateNative(mHybridData) + } + + /** + * Call from a child class to initialize HybridData with a child. + */ + override fun updateNative(hybridData: HybridData) { + mHybridData = hybridData + } + + // Properties + + + // Methods + @DoNotStrip + @Keep + abstract fun getAll(keys: Array): Promise> + + private external fun initHybrid(): HybridData + + companion object { + private const val TAG = "HybridContactsModuleSpec" + init { + try { + Log.i(TAG, "Loading ContactsModule C++ library...") + System.loadLibrary("ContactsModule") + Log.i(TAG, "Successfully loaded ContactsModule C++ library!") + } catch (e: Error) { + Log.e(TAG, "Failed to load ContactsModule C++ library! Is it properly installed and linked? " + + "Is the name correct? (see `CMakeLists.txt`, at `add_library(...)`)", e) + throw e + } + } + } +} diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/StringHolder.kt b/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/StringHolder.kt new file mode 100644 index 000000000000..b6af53e53217 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/StringHolder.kt @@ -0,0 +1,21 @@ +/// +/// StringHolder.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.contacts + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* + +/** + * Represents the JavaScript object/struct "StringHolder". + */ +@DoNotStrip +@Keep +data class StringHolder( + val value: String +) diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule+autolinking.rb b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule+autolinking.rb new file mode 100644 index 000000000000..35bc19c47bf7 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule+autolinking.rb @@ -0,0 +1,58 @@ +# +# ContactsModule+autolinking.rb +# This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +# https://github.com/mrousavy/nitro +# Copyright © 2024 Marc Rousavy @ Margelo +# + +# This is a Ruby script that adds all files generated by Nitrogen +# to the given podspec. +# +# To use it, add this to your .podspec: +# ```ruby +# Pod::Spec.new do |spec| +# # ... +# +# # Add all files generated by Nitrogen +# load 'nitrogen/generated/ios/ContactsModule+autolinking.rb' +# add_nitrogen_files(spec) +# end +# ``` + +def add_nitrogen_files(spec) + Pod::UI.puts "[NitroModules] 🔥 ContactsModule is boosted by nitro!" + + spec.dependency "NitroModules" + + current_source_files = Array(spec.attributes_hash['source_files']) + spec.source_files = current_source_files + [ + # Generated cross-platform specs + "nitrogen/generated/shared/**/*.{h,hpp,c,cpp,swift}", + # Generated bridges for the cross-platform specs + "nitrogen/generated/ios/**/*.{h,hpp,c,cpp,mm,swift}", + ] + + current_public_header_files = Array(spec.attributes_hash['public_header_files']) + spec.public_header_files = current_public_header_files + [ + # Generated specs + "nitrogen/generated/shared/**/*.{h,hpp}", + # Swift to C++ bridging helpers + "nitrogen/generated/ios/ContactsModule-Swift-Cxx-Bridge.hpp" + ] + + current_private_header_files = Array(spec.attributes_hash['private_header_files']) + spec.private_header_files = current_private_header_files + [ + # iOS specific specs + "nitrogen/generated/ios/c++/**/*.{h,hpp}", + ] + + current_pod_target_xcconfig = spec.attributes_hash['pod_target_xcconfig'] || {} + spec.pod_target_xcconfig = current_pod_target_xcconfig.merge({ + # Use C++ 20 + "CLANG_CXX_LANGUAGE_STANDARD" => "c++20", + # Enables C++ <-> Swift interop (by default it's only C) + "SWIFT_OBJC_INTEROP_MODE" => "objcxx", + # Enables stricter modular headers + "DEFINES_MODULE" => "YES", + }) +end diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule-Swift-Cxx-Bridge.cpp b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule-Swift-Cxx-Bridge.cpp new file mode 100644 index 000000000000..4746dbadaa18 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule-Swift-Cxx-Bridge.cpp @@ -0,0 +1,33 @@ +/// +/// ContactsModule-Swift-Cxx-Bridge.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#include "ContactsModule-Swift-Cxx-Bridge.hpp" + +// Include C++ implementation defined types +#include "ContactsModule-Swift-Cxx-Umbrella.hpp" +#include "HybridContactsModuleSpecSwift.hpp" +#include + +namespace margelo::nitro::contacts::bridge::swift { + + // pragma MARK: std::shared_ptr + std::shared_ptr create_std__shared_ptr_margelo__nitro__contacts__HybridContactsModuleSpec_(void* _Nonnull swiftUnsafePointer) { + ContactsModule::HybridContactsModuleSpecCxx swiftPart = ContactsModule::HybridContactsModuleSpecCxxUnsafe::fromUnsafe(swiftUnsafePointer); + return HybridContext::getOrCreate(swiftPart); + } + void* _Nonnull get_std__shared_ptr_margelo__nitro__contacts__HybridContactsModuleSpec_(std__shared_ptr_margelo__nitro__contacts__HybridContactsModuleSpec_ cppType) { + std::shared_ptr swiftWrapper = std::dynamic_pointer_cast(cppType); + #ifdef NITRO_DEBUG + if (swiftWrapper == nullptr) [[unlikely]] { + throw std::runtime_error("Class \"HybridContactsModuleSpec\" is not implemented in Swift!"); + } + #endif + ContactsModule::HybridContactsModuleSpecCxx swiftPart = swiftWrapper->getSwiftPart(); + return ContactsModule::HybridContactsModuleSpecCxxUnsafe::toUnsafe(swiftPart); + } + +} // namespace margelo::nitro::contacts::bridge::swift diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule-Swift-Cxx-Bridge.hpp b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule-Swift-Cxx-Bridge.hpp new file mode 100644 index 000000000000..76d584613df2 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule-Swift-Cxx-Bridge.hpp @@ -0,0 +1,167 @@ +/// +/// ContactsModule-Swift-Cxx-Bridge.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#pragma once + +// Forward declarations of C++ defined types +// Forward declaration of `ContactFields` to properly resolve imports. +namespace margelo::nitro::contacts { enum class ContactFields; } +// Forward declaration of `Contact` to properly resolve imports. +namespace margelo::nitro::contacts { struct Contact; } +// Forward declaration of `HybridContactsModuleSpec` to properly resolve imports. +namespace margelo::nitro::contacts { class HybridContactsModuleSpec; } +// Forward declaration of `StringHolder` to properly resolve imports. +namespace margelo::nitro::contacts { struct StringHolder; } + +// Forward declarations of Swift defined types +// Forward declaration of `HybridContactsModuleSpecCxx` to properly resolve imports. +namespace ContactsModule { class HybridContactsModuleSpecCxx; } + +// Include C++ defined types +#include "Contact.hpp" +#include "ContactFields.hpp" +#include "HybridContactsModuleSpec.hpp" +#include "StringHolder.hpp" +#include +#include +#include +#include +#include +#include +#include + +/** + * Contains specialized versions of C++ templated types so they can be accessed from Swift, + * as well as helper functions to interact with those C++ types from Swift. + */ +namespace margelo::nitro::contacts::bridge::swift { + + // pragma MARK: std::optional + /** + * Specialized version of `std::optional`. + */ + using std__optional_std__string_ = std::optional; + inline std::optional create_std__optional_std__string_(const std::string& value) { + return std::optional(value); + } + + // pragma MARK: std::vector + /** + * Specialized version of `std::vector`. + */ + using std__vector_StringHolder_ = std::vector; + inline std::vector create_std__vector_StringHolder_(size_t size) { + std::vector vector; + vector.reserve(size); + return vector; + } + + // pragma MARK: std::optional> + /** + * Specialized version of `std::optional>`. + */ + using std__optional_std__vector_StringHolder__ = std::optional>; + inline std::optional> create_std__optional_std__vector_StringHolder__(const std::vector& value) { + return std::optional>(value); + } + + // pragma MARK: std::vector + /** + * Specialized version of `std::vector`. + */ + using std__vector_Contact_ = std::vector; + inline std::vector create_std__vector_Contact_(size_t size) { + std::vector vector; + vector.reserve(size); + return vector; + } + + // pragma MARK: std::shared_ptr>> + /** + * Specialized version of `std::shared_ptr>>`. + */ + using std__shared_ptr_Promise_std__vector_Contact___ = std::shared_ptr>>; + inline std::shared_ptr>> create_std__shared_ptr_Promise_std__vector_Contact___() { + return Promise>::create(); + } + + // pragma MARK: std::function& /* result */)> + /** + * Specialized version of `std::function&)>`. + */ + using Func_void_std__vector_Contact_ = std::function& /* result */)>; + /** + * Wrapper class for a `std::function& / * result * /)>`, this can be used from Swift. + */ + class Func_void_std__vector_Contact__Wrapper final { + public: + explicit Func_void_std__vector_Contact__Wrapper(const std::function& /* result */)>& func): _function(func) {} + explicit Func_void_std__vector_Contact__Wrapper(std::function& /* result */)>&& func): _function(std::move(func)) {} + inline void call(std::vector result) const { + _function(result); + } + private: + std::function& /* result */)> _function; + }; + inline Func_void_std__vector_Contact_ create_Func_void_std__vector_Contact_(void* _Nonnull closureHolder, void(* _Nonnull call)(void* _Nonnull /* closureHolder */, std::vector), void(* _Nonnull destroy)(void* _Nonnull)) { + std::shared_ptr sharedClosureHolder(closureHolder, destroy); + return Func_void_std__vector_Contact_([sharedClosureHolder, call](const std::vector& result) -> void { + call(sharedClosureHolder.get(), result); + }); + } + inline std::shared_ptr share_Func_void_std__vector_Contact_(const Func_void_std__vector_Contact_& value) { + return std::make_shared(value); + } + + // pragma MARK: std::function + /** + * Specialized version of `std::function`. + */ + using Func_void_std__exception_ptr = std::function; + /** + * Wrapper class for a `std::function`, this can be used from Swift. + */ + class Func_void_std__exception_ptr_Wrapper final { + public: + explicit Func_void_std__exception_ptr_Wrapper(const std::function& func): _function(func) {} + explicit Func_void_std__exception_ptr_Wrapper(std::function&& func): _function(std::move(func)) {} + inline void call(std::exception_ptr error) const { + _function(error); + } + private: + std::function _function; + }; + inline Func_void_std__exception_ptr create_Func_void_std__exception_ptr(void* _Nonnull closureHolder, void(* _Nonnull call)(void* _Nonnull /* closureHolder */, std::exception_ptr), void(* _Nonnull destroy)(void* _Nonnull)) { + std::shared_ptr sharedClosureHolder(closureHolder, destroy); + return Func_void_std__exception_ptr([sharedClosureHolder, call](const std::exception_ptr& error) -> void { + call(sharedClosureHolder.get(), error); + }); + } + inline std::shared_ptr share_Func_void_std__exception_ptr(const Func_void_std__exception_ptr& value) { + return std::make_shared(value); + } + + // pragma MARK: std::vector + /** + * Specialized version of `std::vector`. + */ + using std__vector_ContactFields_ = std::vector; + inline std::vector create_std__vector_ContactFields_(size_t size) { + std::vector vector; + vector.reserve(size); + return vector; + } + + // pragma MARK: std::shared_ptr + /** + * Specialized version of `std::shared_ptr`. + */ + using std__shared_ptr_margelo__nitro__contacts__HybridContactsModuleSpec_ = std::shared_ptr; + std::shared_ptr create_std__shared_ptr_margelo__nitro__contacts__HybridContactsModuleSpec_(void* _Nonnull swiftUnsafePointer); + void* _Nonnull get_std__shared_ptr_margelo__nitro__contacts__HybridContactsModuleSpec_(std__shared_ptr_margelo__nitro__contacts__HybridContactsModuleSpec_ cppType); + +} // namespace margelo::nitro::contacts::bridge::swift diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule-Swift-Cxx-Umbrella.hpp b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule-Swift-Cxx-Umbrella.hpp new file mode 100644 index 000000000000..6f38d7c7e417 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule-Swift-Cxx-Umbrella.hpp @@ -0,0 +1,54 @@ +/// +/// ContactsModule-Swift-Cxx-Umbrella.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#pragma once + +// Forward declarations of C++ defined types +// Forward declaration of `ContactFields` to properly resolve imports. +namespace margelo::nitro::contacts { enum class ContactFields; } +// Forward declaration of `Contact` to properly resolve imports. +namespace margelo::nitro::contacts { struct Contact; } +// Forward declaration of `HybridContactsModuleSpec` to properly resolve imports. +namespace margelo::nitro::contacts { class HybridContactsModuleSpec; } +// Forward declaration of `StringHolder` to properly resolve imports. +namespace margelo::nitro::contacts { struct StringHolder; } + +// Include C++ defined types +#include "Contact.hpp" +#include "ContactFields.hpp" +#include "HybridContactsModuleSpec.hpp" +#include "StringHolder.hpp" +#include +#include +#include +#include +#include + +// C++ helpers for Swift +#include "ContactsModule-Swift-Cxx-Bridge.hpp" + +// Common C++ types used in Swift +#include +#include +#include +#include + +// Forward declarations of Swift defined types +// Forward declaration of `HybridContactsModuleSpecCxx` to properly resolve imports. +namespace ContactsModule { class HybridContactsModuleSpecCxx; } + +// Include Swift defined types +#if __has_include("ContactsModule-Swift.h") +// This header is generated by Xcode/Swift on every app build. +// If it cannot be found, make sure the Swift module's name (= podspec name) is actually "ContactsModule". +#include "ContactsModule-Swift.h" +// Same as above, but used when building with frameworks (`use_frameworks`) +#elif __has_include() +#include +#else +#error ContactsModule's autogenerated Swift header cannot be found! Make sure the Swift module's name (= podspec name) is actually "ContactsModule", and try building the app first. +#endif diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModuleAutolinking.mm b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModuleAutolinking.mm new file mode 100644 index 000000000000..e769fb6a6806 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModuleAutolinking.mm @@ -0,0 +1,33 @@ +/// +/// ContactsModuleAutolinking.mm +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#import +#import +#import "ContactsModule-Swift-Cxx-Umbrella.hpp" +#import + +#include "HybridContactsModuleSpecSwift.hpp" + +@interface ContactsModuleAutolinking : NSObject +@end + +@implementation ContactsModuleAutolinking + ++ (void) load { + using namespace margelo::nitro; + using namespace margelo::nitro::contacts; + + HybridObjectRegistry::registerHybridObjectConstructor( + "ContactsModule", + []() -> std::shared_ptr { + std::shared_ptr hybridObject = ContactsModule::ContactsModuleAutolinking::createContactsModule(); + return hybridObject; + } + ); +} + +@end diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModuleAutolinking.swift b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModuleAutolinking.swift new file mode 100644 index 000000000000..15d9d9b9064e --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModuleAutolinking.swift @@ -0,0 +1,26 @@ +/// +/// ContactsModuleAutolinking.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +public final class ContactsModuleAutolinking { + public typealias bridge = margelo.nitro.contacts.bridge.swift + + /** + * Creates an instance of a Swift class that implements `HybridContactsModuleSpec`, + * and wraps it in a Swift class that can directly interop with C++ (`HybridContactsModuleSpecCxx`) + * + * This is generated by Nitrogen and will initialize the class specified + * in the `"autolinking"` property of `nitro.json` (in this case, `HybridContactsModule`). + */ + public static func createContactsModule() -> bridge.std__shared_ptr_margelo__nitro__contacts__HybridContactsModuleSpec_ { + let hybridObject = HybridContactsModule() + return { () -> bridge.std__shared_ptr_margelo__nitro__contacts__HybridContactsModuleSpec_ in + let __cxxWrapped = HybridContactsModuleSpecCxx(hybridObject) + let __pointer = HybridContactsModuleSpecCxxUnsafe.toUnsafe(__cxxWrapped) + return bridge.create_std__shared_ptr_margelo__nitro__contacts__HybridContactsModuleSpec_(__pointer) + }() + } +} diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.cpp b/modules/ContactsNitroModule/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.cpp new file mode 100644 index 000000000000..71151f3c1883 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.cpp @@ -0,0 +1,11 @@ +/// +/// HybridContactsModuleSpecSwift.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#include "HybridContactsModuleSpecSwift.hpp" + +namespace margelo::nitro::contacts { +} // namespace margelo::nitro::contacts diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.hpp b/modules/ContactsNitroModule/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.hpp new file mode 100644 index 000000000000..dbb4fe829dc2 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.hpp @@ -0,0 +1,82 @@ +/// +/// HybridContactsModuleSpecSwift.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#pragma once + +#include "HybridContactsModuleSpec.hpp" + +// Forward declaration of `HybridContactsModuleSpecCxx` to properly resolve imports. +namespace ContactsModule { class HybridContactsModuleSpecCxx; } + +// Forward declaration of `Contact` to properly resolve imports. +namespace margelo::nitro::contacts { struct Contact; } +// Forward declaration of `StringHolder` to properly resolve imports. +namespace margelo::nitro::contacts { struct StringHolder; } +// Forward declaration of `ContactFields` to properly resolve imports. +namespace margelo::nitro::contacts { enum class ContactFields; } + +#include +#include +#include "Contact.hpp" +#include +#include +#include "StringHolder.hpp" +#include "ContactFields.hpp" + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +#include "ContactsModule-Swift-Cxx-Umbrella.hpp" + +namespace margelo::nitro::contacts { + + /** + * The C++ part of HybridContactsModuleSpecCxx.swift. + * + * HybridContactsModuleSpecSwift (C++) accesses HybridContactsModuleSpecCxx (Swift), and might + * contain some additional bridging code for C++ <> Swift interop. + * + * Since this obviously introduces an overhead, I hope at some point in + * the future, HybridContactsModuleSpecCxx can directly inherit from the C++ class HybridContactsModuleSpec + * to simplify the whole structure and memory management. + */ + class HybridContactsModuleSpecSwift: public virtual HybridContactsModuleSpec { + public: + // Constructor from a Swift instance + explicit HybridContactsModuleSpecSwift(const ContactsModule::HybridContactsModuleSpecCxx& swiftPart): + HybridObject(HybridContactsModuleSpec::TAG), + _swiftPart(swiftPart) { } + + public: + // Get the Swift part + inline ContactsModule::HybridContactsModuleSpecCxx getSwiftPart() noexcept { return _swiftPart; } + + public: + // Get memory pressure + inline size_t getExternalMemorySize() noexcept override { + return _swiftPart.getMemorySize(); + } + + public: + // Properties + + + public: + // Methods + inline std::shared_ptr>> getAll(const std::vector& keys) override { + auto __result = _swiftPart.getAll(keys); + return __result; + } + + private: + ContactsModule::HybridContactsModuleSpecCxx _swiftPart; + }; + +} // namespace margelo::nitro::contacts diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/swift/Contact.swift b/modules/ContactsNitroModule/nitrogen/generated/ios/swift/Contact.swift new file mode 100644 index 000000000000..404d6ba86b25 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/swift/Contact.swift @@ -0,0 +1,251 @@ +/// +/// Contact.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `Contact`, backed by a C++ struct. + */ +public typealias Contact = margelo.nitro.contacts.Contact + +public extension Contact { + private typealias bridge = margelo.nitro.contacts.bridge.swift + + /** + * Create a new instance of `Contact`. + */ + init(firstName: String?, lastName: String?, middleName: String?, phoneNumbers: [StringHolder]?, emailAddresses: [StringHolder]?, imageData: String?, thumbnailImageData: String?) { + self.init({ () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = firstName { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = lastName { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = middleName { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__vector_StringHolder__ in + if let __unwrappedValue = phoneNumbers { + return bridge.create_std__optional_std__vector_StringHolder__({ () -> bridge.std__vector_StringHolder_ in + var __vector = bridge.create_std__vector_StringHolder_(__unwrappedValue.count) + for __item in __unwrappedValue { + __vector.push_back(__item) + } + return __vector + }()) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__vector_StringHolder__ in + if let __unwrappedValue = emailAddresses { + return bridge.create_std__optional_std__vector_StringHolder__({ () -> bridge.std__vector_StringHolder_ in + var __vector = bridge.create_std__vector_StringHolder_(__unwrappedValue.count) + for __item in __unwrappedValue { + __vector.push_back(__item) + } + return __vector + }()) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = imageData { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = thumbnailImageData { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }()) + } + + var firstName: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__firstName.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__firstName = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } + + var lastName: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__lastName.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__lastName = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } + + var middleName: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__middleName.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__middleName = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } + + var phoneNumbers: [StringHolder]? { + @inline(__always) + get { + return { () -> [StringHolder]? in + if let __unwrapped = self.__phoneNumbers.value { + return __unwrapped.map({ __item in __item }) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__phoneNumbers = { () -> bridge.std__optional_std__vector_StringHolder__ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__vector_StringHolder__({ () -> bridge.std__vector_StringHolder_ in + var __vector = bridge.create_std__vector_StringHolder_(__unwrappedValue.count) + for __item in __unwrappedValue { + __vector.push_back(__item) + } + return __vector + }()) + } else { + return .init() + } + }() + } + } + + var emailAddresses: [StringHolder]? { + @inline(__always) + get { + return { () -> [StringHolder]? in + if let __unwrapped = self.__emailAddresses.value { + return __unwrapped.map({ __item in __item }) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__emailAddresses = { () -> bridge.std__optional_std__vector_StringHolder__ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__vector_StringHolder__({ () -> bridge.std__vector_StringHolder_ in + var __vector = bridge.create_std__vector_StringHolder_(__unwrappedValue.count) + for __item in __unwrappedValue { + __vector.push_back(__item) + } + return __vector + }()) + } else { + return .init() + } + }() + } + } + + var imageData: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__imageData.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__imageData = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } + + var thumbnailImageData: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__thumbnailImageData.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__thumbnailImageData = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } +} diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/swift/ContactFields.swift b/modules/ContactsNitroModule/nitrogen/generated/ios/swift/ContactFields.swift new file mode 100644 index 000000000000..ce38940795d9 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/swift/ContactFields.swift @@ -0,0 +1,64 @@ +/// +/// ContactFields.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `ContactFields`, backed by a C++ enum. + */ +public typealias ContactFields = margelo.nitro.contacts.ContactFields + +public extension ContactFields { + /** + * Get a ContactFields for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "FIRST_NAME": + self = .firstName + case "LAST_NAME": + self = .lastName + case "MIDDLE_NAME": + self = .middleName + case "PHONE_NUMBERS": + self = .phoneNumbers + case "EMAIL_ADDRESSES": + self = .emailAddresses + case "IMAGE_DATA": + self = .imageData + case "THUMBNAIL_IMAGE_DATA": + self = .thumbnailImageData + case "GIVEN_NAME_KEY": + self = .givenNameKey + default: + return nil + } + } + + /** + * Get the String value this ContactFields represents. + */ + var stringValue: String { + switch self { + case .firstName: + return "FIRST_NAME" + case .lastName: + return "LAST_NAME" + case .middleName: + return "MIDDLE_NAME" + case .phoneNumbers: + return "PHONE_NUMBERS" + case .emailAddresses: + return "EMAIL_ADDRESSES" + case .imageData: + return "IMAGE_DATA" + case .thumbnailImageData: + return "THUMBNAIL_IMAGE_DATA" + case .givenNameKey: + return "GIVEN_NAME_KEY" + } + } +} diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/swift/HybridContactsModuleSpec.swift b/modules/ContactsNitroModule/nitrogen/generated/ios/swift/HybridContactsModuleSpec.swift new file mode 100644 index 000000000000..611110efca1d --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/swift/HybridContactsModuleSpec.swift @@ -0,0 +1,36 @@ +/// +/// HybridContactsModuleSpec.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +import Foundation +import NitroModules + +/** + * A Swift protocol representing the ContactsModule HybridObject. + * Implement this protocol to create Swift-based instances of ContactsModule. + * + * When implementing this protocol, make sure to initialize `hybridContext` - example: + * ``` + * public class HybridContactsModule : HybridContactsModuleSpec { + * // Initialize HybridContext + * var hybridContext = margelo.nitro.HybridContext() + * + * // Return size of the instance to inform JS GC about memory pressure + * var memorySize: Int { + * return getSizeOf(self) + * } + * + * // ... + * } + * ``` + */ +public protocol HybridContactsModuleSpec: AnyObject, HybridObjectSpec { + // Properties + + + // Methods + func getAll(keys: [ContactFields]) throws -> Promise<[Contact]> +} diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/swift/HybridContactsModuleSpecCxx.swift b/modules/ContactsNitroModule/nitrogen/generated/ios/swift/HybridContactsModuleSpecCxx.swift new file mode 100644 index 000000000000..156cdf86bd7f --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/swift/HybridContactsModuleSpecCxx.swift @@ -0,0 +1,123 @@ +/// +/// HybridContactsModuleSpecCxx.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +import Foundation +import NitroModules + +/** + * Helper class for converting instances of `HybridContactsModuleSpecCxx` from- and to unsafe pointers. + * This is useful to pass Swift classes to C++, without having to strongly type the C++ function signature. + * The actual Swift type can be included in the .cpp file, without having to forward-declare anything in .hpp. + */ +public final class HybridContactsModuleSpecCxxUnsafe { + /** + * Casts a `HybridContactsModuleSpecCxx` instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + public static func toUnsafe(_ instance: HybridContactsModuleSpecCxx) -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(instance).toOpaque() + } + + /** + * Casts an unsafe pointer to a `HybridContactsModuleSpecCxx`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> HybridContactsModuleSpecCxx { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} + +/** + * A class implementation that bridges HybridContactsModuleSpec over to C++. + * In C++, we cannot use Swift protocols - so we need to wrap it in a class to make it strongly defined. + * + * Also, some Swift types need to be bridged with special handling: + * - Enums need to be wrapped in Structs, otherwise they cannot be accessed bi-directionally (Swift bug: https://github.com/swiftlang/swift/issues/75330) + * - Other HybridObjects need to be wrapped/unwrapped from the Swift TCxx wrapper + * - Throwing methods need to be wrapped with a Result type, as exceptions cannot be propagated to C++ + */ +public class HybridContactsModuleSpecCxx { + /** + * The Swift <> C++ bridge's namespace (`margelo::nitro::contacts::bridge::swift`) + * from `ContactsModule-Swift-Cxx-Bridge.hpp`. + * This contains specialized C++ templates, and C++ helper functions that can be accessed from Swift. + */ + public typealias bridge = margelo.nitro.contacts.bridge.swift + + /** + * Holds an instance of the `HybridContactsModuleSpec` Swift protocol. + */ + private var __implementation: any HybridContactsModuleSpec + + /** + * Create a new `HybridContactsModuleSpecCxx` that wraps the given `HybridContactsModuleSpec`. + * All properties and methods bridge to C++ types. + */ + public init(_ implementation: some HybridContactsModuleSpec) { + self.__implementation = implementation + /* no base class */ + } + + /** + * Get the actual `HybridContactsModuleSpec` instance this class wraps. + */ + @inline(__always) + public func getHybridContactsModuleSpec() -> any HybridContactsModuleSpec { + return __implementation + } + + /** + * Contains a (weak) reference to the C++ HybridObject to cache it. + */ + public var hybridContext: margelo.nitro.HybridContext { + @inline(__always) + get { + return self.__implementation.hybridContext + } + @inline(__always) + set { + self.__implementation.hybridContext = newValue + } + } + + /** + * Get the memory size of the Swift class (plus size of any other allocations) + * so the JS VM can properly track it and garbage-collect the JS object if needed. + */ + @inline(__always) + public var memorySize: Int { + return self.__implementation.memorySize + } + + // Properties + + + // Methods + @inline(__always) + public func getAll(keys: bridge.std__vector_ContactFields_) -> bridge.std__shared_ptr_Promise_std__vector_Contact___ { + do { + let __result = try self.__implementation.getAll(keys: keys.map({ __item in __item })) + return { () -> bridge.std__shared_ptr_Promise_std__vector_Contact___ in + let __promise = bridge.create_std__shared_ptr_Promise_std__vector_Contact___() + __result + .then({ __result in __promise.pointee.resolve({ () -> bridge.std__vector_Contact_ in + var __vector = bridge.create_std__vector_Contact_(__result.count) + for __item in __result { + __vector.push_back(__item) + } + return __vector + }()) }) + .catch({ __error in __promise.pointee.reject(__error.toCpp()) }) + return __promise + }() + } catch { + let __message = "\(error.localizedDescription)" + fatalError("Swift errors can currently not be propagated to C++! See https://github.com/swiftlang/swift/issues/75290 (Error: \(__message))") + } + } +} diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/swift/StringHolder.swift b/modules/ContactsNitroModule/nitrogen/generated/ios/swift/StringHolder.swift new file mode 100644 index 000000000000..477279082456 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/swift/StringHolder.swift @@ -0,0 +1,35 @@ +/// +/// StringHolder.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `StringHolder`, backed by a C++ struct. + */ +public typealias StringHolder = margelo.nitro.contacts.StringHolder + +public extension StringHolder { + private typealias bridge = margelo.nitro.contacts.bridge.swift + + /** + * Create a new instance of `StringHolder`. + */ + init(value: String) { + self.init(std.string(value)) + } + + var value: String { + @inline(__always) + get { + return String(self.__value) + } + @inline(__always) + set { + self.__value = std.string(newValue) + } + } +} diff --git a/modules/ContactsNitroModule/nitrogen/generated/shared/c++/Contact.hpp b/modules/ContactsNitroModule/nitrogen/generated/shared/c++/Contact.hpp new file mode 100644 index 000000000000..6e4a5bd0c27e --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/shared/c++/Contact.hpp @@ -0,0 +1,96 @@ +/// +/// Contact.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `StringHolder` to properly resolve imports. +namespace margelo::nitro::contacts { struct StringHolder; } + +#include +#include +#include +#include "StringHolder.hpp" + +namespace margelo::nitro::contacts { + + /** + * A struct which can be represented as a JavaScript object (Contact). + */ + struct Contact { + public: + std::optional firstName SWIFT_PRIVATE; + std::optional lastName SWIFT_PRIVATE; + std::optional middleName SWIFT_PRIVATE; + std::optional> phoneNumbers SWIFT_PRIVATE; + std::optional> emailAddresses SWIFT_PRIVATE; + std::optional imageData SWIFT_PRIVATE; + std::optional thumbnailImageData SWIFT_PRIVATE; + + public: + explicit Contact(std::optional firstName, std::optional lastName, std::optional middleName, std::optional> phoneNumbers, std::optional> emailAddresses, std::optional imageData, std::optional thumbnailImageData): firstName(firstName), lastName(lastName), middleName(middleName), phoneNumbers(phoneNumbers), emailAddresses(emailAddresses), imageData(imageData), thumbnailImageData(thumbnailImageData) {} + }; + +} // namespace margelo::nitro::contacts + +namespace margelo::nitro { + + using namespace margelo::nitro::contacts; + + // C++ Contact <> JS Contact (object) + template <> + struct JSIConverter { + static inline Contact fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return Contact( + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "firstName")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "lastName")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "middleName")), + JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, "phoneNumbers")), + JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, "emailAddresses")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "imageData")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "thumbnailImageData")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const Contact& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "firstName", JSIConverter>::toJSI(runtime, arg.firstName)); + obj.setProperty(runtime, "lastName", JSIConverter>::toJSI(runtime, arg.lastName)); + obj.setProperty(runtime, "middleName", JSIConverter>::toJSI(runtime, arg.middleName)); + obj.setProperty(runtime, "phoneNumbers", JSIConverter>>::toJSI(runtime, arg.phoneNumbers)); + obj.setProperty(runtime, "emailAddresses", JSIConverter>>::toJSI(runtime, arg.emailAddresses)); + obj.setProperty(runtime, "imageData", JSIConverter>::toJSI(runtime, arg.imageData)); + obj.setProperty(runtime, "thumbnailImageData", JSIConverter>::toJSI(runtime, arg.thumbnailImageData)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "firstName"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "lastName"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "middleName"))) return false; + if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, "phoneNumbers"))) return false; + if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, "emailAddresses"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "imageData"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "thumbnailImageData"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/modules/ContactsNitroModule/nitrogen/generated/shared/c++/ContactFields.hpp b/modules/ContactsNitroModule/nitrogen/generated/shared/c++/ContactFields.hpp new file mode 100644 index 000000000000..c3e8c115465e --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/shared/c++/ContactFields.hpp @@ -0,0 +1,102 @@ +/// +/// ContactFields.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::contacts { + + /** + * An enum which can be represented as a JavaScript union (ContactFields). + */ + enum class ContactFields { + FIRST_NAME SWIFT_NAME(firstName) = 0, + LAST_NAME SWIFT_NAME(lastName) = 1, + MIDDLE_NAME SWIFT_NAME(middleName) = 2, + PHONE_NUMBERS SWIFT_NAME(phoneNumbers) = 3, + EMAIL_ADDRESSES SWIFT_NAME(emailAddresses) = 4, + IMAGE_DATA SWIFT_NAME(imageData) = 5, + THUMBNAIL_IMAGE_DATA SWIFT_NAME(thumbnailImageData) = 6, + GIVEN_NAME_KEY SWIFT_NAME(givenNameKey) = 7, + } CLOSED_ENUM; + +} // namespace margelo::nitro::contacts + +namespace margelo::nitro { + + using namespace margelo::nitro::contacts; + + // C++ ContactFields <> JS ContactFields (union) + template <> + struct JSIConverter { + static inline ContactFields fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("FIRST_NAME"): return ContactFields::FIRST_NAME; + case hashString("LAST_NAME"): return ContactFields::LAST_NAME; + case hashString("MIDDLE_NAME"): return ContactFields::MIDDLE_NAME; + case hashString("PHONE_NUMBERS"): return ContactFields::PHONE_NUMBERS; + case hashString("EMAIL_ADDRESSES"): return ContactFields::EMAIL_ADDRESSES; + case hashString("IMAGE_DATA"): return ContactFields::IMAGE_DATA; + case hashString("THUMBNAIL_IMAGE_DATA"): return ContactFields::THUMBNAIL_IMAGE_DATA; + case hashString("GIVEN_NAME_KEY"): return ContactFields::GIVEN_NAME_KEY; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum ContactFields - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, ContactFields arg) { + switch (arg) { + case ContactFields::FIRST_NAME: return JSIConverter::toJSI(runtime, "FIRST_NAME"); + case ContactFields::LAST_NAME: return JSIConverter::toJSI(runtime, "LAST_NAME"); + case ContactFields::MIDDLE_NAME: return JSIConverter::toJSI(runtime, "MIDDLE_NAME"); + case ContactFields::PHONE_NUMBERS: return JSIConverter::toJSI(runtime, "PHONE_NUMBERS"); + case ContactFields::EMAIL_ADDRESSES: return JSIConverter::toJSI(runtime, "EMAIL_ADDRESSES"); + case ContactFields::IMAGE_DATA: return JSIConverter::toJSI(runtime, "IMAGE_DATA"); + case ContactFields::THUMBNAIL_IMAGE_DATA: return JSIConverter::toJSI(runtime, "THUMBNAIL_IMAGE_DATA"); + case ContactFields::GIVEN_NAME_KEY: return JSIConverter::toJSI(runtime, "GIVEN_NAME_KEY"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert ContactFields to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("FIRST_NAME"): + case hashString("LAST_NAME"): + case hashString("MIDDLE_NAME"): + case hashString("PHONE_NUMBERS"): + case hashString("EMAIL_ADDRESSES"): + case hashString("IMAGE_DATA"): + case hashString("THUMBNAIL_IMAGE_DATA"): + case hashString("GIVEN_NAME_KEY"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/modules/ContactsNitroModule/nitrogen/generated/shared/c++/HybridContactsModuleSpec.cpp b/modules/ContactsNitroModule/nitrogen/generated/shared/c++/HybridContactsModuleSpec.cpp new file mode 100644 index 000000000000..eba17de8d910 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/shared/c++/HybridContactsModuleSpec.cpp @@ -0,0 +1,21 @@ +/// +/// HybridContactsModuleSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#include "HybridContactsModuleSpec.hpp" + +namespace margelo::nitro::contacts { + + void HybridContactsModuleSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("getAll", &HybridContactsModuleSpec::getAll); + }); + } + +} // namespace margelo::nitro::contacts diff --git a/modules/ContactsNitroModule/nitrogen/generated/shared/c++/HybridContactsModuleSpec.hpp b/modules/ContactsNitroModule/nitrogen/generated/shared/c++/HybridContactsModuleSpec.hpp new file mode 100644 index 000000000000..6c298086f493 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/shared/c++/HybridContactsModuleSpec.hpp @@ -0,0 +1,68 @@ +/// +/// HybridContactsModuleSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `Contact` to properly resolve imports. +namespace margelo::nitro::contacts { struct Contact; } +// Forward declaration of `ContactFields` to properly resolve imports. +namespace margelo::nitro::contacts { enum class ContactFields; } + +#include +#include +#include "Contact.hpp" +#include "ContactFields.hpp" + +namespace margelo::nitro::contacts { + + using namespace margelo::nitro; + + /** + * An abstract base class for `ContactsModule` + * Inherit this class to create instances of `HybridContactsModuleSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridContactsModule: public HybridContactsModuleSpec { + * public: + * HybridContactsModule(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridContactsModuleSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridContactsModuleSpec(): HybridObject(TAG) { } + + // Destructor + virtual ~HybridContactsModuleSpec() { } + + public: + // Properties + + + public: + // Methods + virtual std::shared_ptr>> getAll(const std::vector& keys) = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "ContactsModule"; + }; + +} // namespace margelo::nitro::contacts diff --git a/modules/ContactsNitroModule/nitrogen/generated/shared/c++/StringHolder.hpp b/modules/ContactsNitroModule/nitrogen/generated/shared/c++/StringHolder.hpp new file mode 100644 index 000000000000..1a666ed1faca --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/shared/c++/StringHolder.hpp @@ -0,0 +1,68 @@ +/// +/// StringHolder.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include + +namespace margelo::nitro::contacts { + + /** + * A struct which can be represented as a JavaScript object (StringHolder). + */ + struct StringHolder { + public: + std::string value SWIFT_PRIVATE; + + public: + explicit StringHolder(std::string value): value(value) {} + }; + +} // namespace margelo::nitro::contacts + +namespace margelo::nitro { + + using namespace margelo::nitro::contacts; + + // C++ StringHolder <> JS StringHolder (object) + template <> + struct JSIConverter { + static inline StringHolder fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return StringHolder( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "value")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const StringHolder& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "value", JSIConverter::toJSI(runtime, arg.value)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "value"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/modules/ContactsNitroModule/package.json b/modules/ContactsNitroModule/package.json new file mode 100644 index 000000000000..6f70882a2193 --- /dev/null +++ b/modules/ContactsNitroModule/package.json @@ -0,0 +1,103 @@ +{ + "name": "contacts-nitro-module", + "version": "0.0.1", + "main": "src/index", + "react-native": "src/index", + "description": "React Native Contacts Module with Nitro optimization", + "source": "src/index", + "files": [ + "src", + "react-native.config.js", + "lib", + "android/build.gradle", + "android/gradle.properties", + "android/CMakeLists.txt", + "android/src", + "ios/**/*.h", + "ios/**/*.m", + "ios/**/*.mm", + "ios/**/*.cpp", + "ios/**/*.swift", + "app.plugin.js", + "*.podspec", + "README.md" + ], + "scripts": { + "postinstall": "tsc || exit 0;", + "typecheck": "tsc --noEmit", + "clean": "del-cli android/build node_modules/**/android/build lib", + "lint": "eslint \"**/*.{js,ts,tsx}\" --fix", + "lint-ci": "eslint \"**/*.{js,ts,tsx}\" -f @jamesacarr/github-actions", + "typescript": "tsc --noEmit false", + "specs-debug": "bun run --filter=\"**\" typescript && bun nitro-codegen --logLevel=\"debug\"", + "specs": "bun nitro-codegen" + }, + "keywords": [ + "react-native", + "nitro" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/mrousavy/nitro.git" + }, + "author": "Marc Rousavy (https://github.com/mrousavy)", + "license": "MIT", + "bugs": { + "url": "https://github.com/mrousavy/nitro/issues" + }, + "homepage": "https://github.com/mrousavy/nitro#readme", + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "devDependencies": { + "@react-native/eslint-config": "^0.75.2", + "@types/jest": "^29.5.12", + "@types/react": "^18.3.4", + "del-cli": "^5.1.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "nitro-codegen": "0.18.1", + "prettier": "^3.3.3", + "react": "^18.3.1", + "react-native": "0.75.2", + "react-native-nitro-modules": "*", + "typescript": "^5.5.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + }, + "eslintConfig": { + "root": true, + "extends": [ + "@react-native", + "prettier" + ], + "plugins": ["prettier"], + "rules": { + "prettier/prettier": [ + "warn", + { + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false + } + ] + } + }, + "eslintIgnore": [ + "node_modules/", + "lib/" + ], + "prettier": { + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false, + "semi": false + } +} diff --git a/modules/ContactsNitroModule/src/ContactsModule.nitro.ts b/modules/ContactsNitroModule/src/ContactsModule.nitro.ts new file mode 100644 index 000000000000..8df839f550e8 --- /dev/null +++ b/modules/ContactsNitroModule/src/ContactsModule.nitro.ts @@ -0,0 +1,29 @@ +import type { HybridObject } from 'react-native-nitro-modules' + +interface StringHolder { + value: string +} + +export interface Contact { + firstName?: string + lastName?: string + middleName?: string + phoneNumbers?: StringHolder[] + emailAddresses?: StringHolder[] + imageData?: string + thumbnailImageData?: string +} +export type ContactFields = + | 'FIRST_NAME' + | 'LAST_NAME' + | 'MIDDLE_NAME' + | 'PHONE_NUMBERS' + | 'EMAIL_ADDRESSES' + | 'IMAGE_DATA' + | 'THUMBNAIL_IMAGE_DATA' + | 'GIVEN_NAME_KEY' + +export interface ContactsModule + extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> { + getAll(keys: ContactFields[]): Promise +} diff --git a/modules/ContactsNitroModule/src/index.ts b/modules/ContactsNitroModule/src/index.ts new file mode 100644 index 000000000000..acc7c7c1fb76 --- /dev/null +++ b/modules/ContactsNitroModule/src/index.ts @@ -0,0 +1,8 @@ +import type { ContactsModule } from './ContactsModule.nitro' +import type { Contact } from './ContactsModule.nitro' +import { NitroModules } from 'react-native-nitro-modules' + +export const ContactsNitroModule = + NitroModules.createHybridObject('ContactsModule') + +export type { Contact } diff --git a/modules/ContactsNitroModule/tsconfig.json b/modules/ContactsNitroModule/tsconfig.json new file mode 100644 index 000000000000..e30dc47ac169 --- /dev/null +++ b/modules/ContactsNitroModule/tsconfig.json @@ -0,0 +1,29 @@ +{ + "include": ["src"], + "compilerOptions": { + "composite": true, + "outDir": "lib", + "rootDir": "src", + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "lib": ["esnext"], + "module": "esnext", + "moduleResolution": "node", + "noEmit": false, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitUseStrict": false, + "noStrictGenericChecks": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "esnext", + "verbatimModuleSyntax": true + } +} diff --git a/package-lock.json b/package-lock.json index 947e3a4cc1bc..497c8bbaeb45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,20 @@ { "name": "new.expensify", - "version": "9.0.76-6", + "version": "9.0.77-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.76-6", + "version": "9.0.77-2", "hasInstallScript": true, "license": "MIT", + "workspaces": [ + "modules/ContactsNitroModule" + ], "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.207", + "@expensify/react-native-live-markdown": "0.1.209", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -46,6 +49,7 @@ "awesome-phonenumber": "^5.4.0", "babel-polyfill": "^6.26.0", "canvas-size": "^1.2.6", + "contacts-nitro-module": "./modules/ContactsNitroModule", "core-js": "^3.32.0", "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", @@ -91,10 +95,11 @@ "react-native-image-picker": "^7.0.3", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#cb392140db4953a283590d7cf93b4d0461baa2a9", "react-native-key-command": "^1.0.8", - "react-native-keyboard-controller": "1.14.4", + "react-native-keyboard-controller": "1.15.0", "react-native-launch-arguments": "^4.0.2", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", + "react-native-nitro-modules": "^0.18.1", "react-native-onyx": "2.0.86", "react-native-pager-view": "6.5.1", "react-native-pdf": "6.7.3", @@ -219,7 +224,7 @@ "electron-builder": "25.0.0", "eslint": "^8.57.0", "eslint-config-airbnb-typescript": "^18.0.0", - "eslint-config-expensify": "^2.0.74", + "eslint-config-expensify": "2.0.75", "eslint-config-prettier": "^9.1.0", "eslint-plugin-deprecation": "^3.0.0", "eslint-plugin-jest": "^28.6.0", @@ -275,10 +280,114 @@ "webpack-dev-server": "^5.0.4", "webpack-merge": "^5.8.0", "xlsx": "file:vendor/xlsx-0.20.3.tgz" + } + }, + "modules/ContactsNitroModule": { + "name": "contacts-nitro-module", + "version": "0.0.1", + "hasInstallScript": true, + "license": "MIT", + "devDependencies": { + "@react-native/eslint-config": "^0.75.2", + "@types/jest": "^29.5.12", + "@types/react": "^18.3.4", + "del-cli": "^5.1.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "nitro-codegen": "0.18.1", + "prettier": "^3.3.3", + "react": "^18.3.1", + "react-native": "0.75.2", + "react-native-nitro-modules": "*", + "typescript": "^5.5.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "modules/ContactsNitroModule/node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "modules/ContactsNitroModule/node_modules/@types/react": { + "version": "18.3.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.17.tgz", + "integrity": "sha512-opAQ5no6LqJNo9TqnxBKsgnkIYHozW9KSTlFVoSUJYh1Fl/sswkEoqIugRSm7tbh6pABtYjGAjW+GOS23j8qbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "modules/ContactsNitroModule/node_modules/eslint-plugin-prettier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" }, "engines": { - "node": "20.18.0", - "npm": "10.8.2" + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "modules/ContactsNitroModule/node_modules/prettier": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "modules/ContactsNitroModule/node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" } }, "node_modules/@actions/core": { @@ -3498,9 +3607,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.207", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.207.tgz", - "integrity": "sha512-8snKeruLuHJCecnwQ+ru6pJhrDeI2Y3EywmXf/keT4aMk2xcW1fyCAr925zikTWANMDghcKkeuR/JqLe2b3rkA==", + "version": "0.1.209", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.209.tgz", + "integrity": "sha512-u+RRY+Jog/llEu9T1v0okSLgRhG5jGlX9H1Je0A8HWv0439XFLnAWSvN2eQ2T7bvT8Yjdj5CcC0hkgJiB9oCQw==", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -7199,6 +7308,19 @@ "node": ">=14" } }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.15.tgz", @@ -9309,6 +9431,83 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@react-native/eslint-config": { + "version": "0.75.4", + "resolved": "https://registry.npmjs.org/@react-native/eslint-config/-/eslint-config-0.75.4.tgz", + "integrity": "sha512-3KBHYwp4HnBdaCFx9KDPvQY+sGrv5fHX2qDkXGKmN3uYBz+zfnMQXTiht6OuBbWULUF0y0o8m+uH1yYAn/V9mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.20.0", + "@babel/eslint-parser": "^7.20.0", + "@react-native/eslint-plugin": "0.75.4", + "@typescript-eslint/eslint-plugin": "^7.1.1", + "@typescript-eslint/parser": "^7.1.1", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-ft-flow": "^2.0.1", + "eslint-plugin-jest": "^27.9.0", + "eslint-plugin-react": "^7.30.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-native": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": ">=8", + "prettier": ">=2" + } + }, + "node_modules/@react-native/eslint-config/node_modules/eslint-config-prettier": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/@react-native/eslint-config/node_modules/eslint-plugin-jest": { + "version": "27.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.9.0.tgz", + "integrity": "sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^5.10.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0 || ^6.0.0 || ^7.0.0", + "eslint": "^7.0.0 || ^8.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/@react-native/eslint-plugin": { + "version": "0.75.4", + "resolved": "https://registry.npmjs.org/@react-native/eslint-plugin/-/eslint-plugin-0.75.4.tgz", + "integrity": "sha512-1kEZzC8UKi3baHnH7tBVCNpF4aoAmT7g7hEa5/rtZ+Z7vcpaxeY6wjNYt3j02Z9n310yX0NKDJox30CqvzEvsg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@react-native/gradle-plugin": { "version": "0.75.2", "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.75.2.tgz", @@ -13105,6 +13304,44 @@ "node": ">=10.13.0" } }, + "node_modules/@ts-morph/common": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.25.0.tgz", + "integrity": "sha512-kMnZz+vGGHi4GoHnLmMhGNjm44kGtKUXGnOvrKmMwAuvNjM/PgKVGfUnL7IDvK7Jb2QQ82jq3Zmp04Gy+r3Dkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^9.0.4", + "path-browserify": "^1.0.1", + "tinyglobby": "^0.2.9" + } + }, + "node_modules/@ts-morph/common/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@ts-morph/common/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "dev": true, @@ -13623,6 +13860,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ms": { "version": "0.7.34", "dev": true, @@ -13664,6 +13908,13 @@ "@types/node": "*" } }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/parse-json": { "version": "4.0.0", "dev": true, @@ -15496,6 +15747,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/asap": { "version": "2.0.6", "license": "MIT" @@ -17290,6 +17551,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/camelcase-keys": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-7.0.2.tgz", + "integrity": "sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.3.0", + "map-obj": "^4.1.0", + "quick-lru": "^5.1.1", + "type-fest": "^1.2.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/camelize": { "version": "1.0.1", "license": "MIT", @@ -17653,6 +17946,13 @@ "node": ">= 0.12.0" } }, + "node_modules/code-block-writer": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "dev": true, + "license": "MIT" + }, "node_modules/collect-v8-coverage": { "version": "1.0.1", "dev": true, @@ -18144,6 +18444,10 @@ "dev": true, "license": "MIT" }, + "node_modules/contacts-nitro-module": { + "resolved": "modules/ContactsNitroModule", + "link": true + }, "node_modules/content-disposition": { "version": "0.5.4", "dev": true, @@ -18858,6 +19162,56 @@ } } }, + "node_modules/decamelize": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-5.0.1.tgz", + "integrity": "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decimal.js": { "version": "10.4.3", "dev": true, @@ -19061,6 +19415,184 @@ "node": ">=6" } }, + "node_modules/del-cli": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/del-cli/-/del-cli-5.1.0.tgz", + "integrity": "sha512-xwMeh2acluWeccsfzE7VLsG3yTr7nWikbfw+xhMnpRrF15pGSkw+3/vJZWlGoE4I86UiLRNHicmKt4tkIX9Jtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "del": "^7.1.0", + "meow": "^10.1.3" + }, + "bin": { + "del": "cli.js", + "del-cli": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del-cli/node_modules/aggregate-error": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-4.0.1.tgz", + "integrity": "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^4.0.0", + "indent-string": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del-cli/node_modules/clean-stack": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-4.2.0.tgz", + "integrity": "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del-cli/node_modules/del": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/del/-/del-7.1.0.tgz", + "integrity": "sha512-v2KyNk7efxhlyHpjEvfyxaAihKKK0nWCuf6ZtqZcFFpQRG0bJ12Qsr0RpvsICMjAAZ8DOVCxrlqpxISlMHC4Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "globby": "^13.1.2", + "graceful-fs": "^4.2.10", + "is-glob": "^4.0.3", + "is-path-cwd": "^3.0.0", + "is-path-inside": "^4.0.0", + "p-map": "^5.5.0", + "rimraf": "^3.0.2", + "slash": "^4.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del-cli/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del-cli/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del-cli/node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del-cli/node_modules/is-path-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-3.0.0.tgz", + "integrity": "sha512-kyiNFFLU0Ampr6SDZitD/DwUo4Zs1nSdnygUBqsu3LooL00Qvb5j+UnvApUn/TTj1J3OuE6BTdQ5rudKmU2ZaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del-cli/node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del-cli/node_modules/p-map": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-5.5.0.tgz", + "integrity": "sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del-cli/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/del/node_modules/array-union": { "version": "1.0.2", "dev": true, @@ -20311,9 +20843,9 @@ } }, "node_modules/eslint-config-expensify": { - "version": "2.0.74", - "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.74.tgz", - "integrity": "sha512-NTA8fPbfkyCBZG+2/xJqB+HYD2D0XP8Sx1IDLWiwe/XJyNEESeqwQVbpA7FUP9sq4Ik2m2LPMf/G/aQHfw88rQ==", + "version": "2.0.75", + "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.75.tgz", + "integrity": "sha512-eSzQpxmVMGGXZSoB7aPZoWh75NC3oStyQnd+1JBFUQMDrdCyWjkMl8UJjzBqp/dOHazmVgLQUS1vDfk5cGXe6Q==", "dev": true, "license": "ISC", "dependencies": { @@ -23403,6 +23935,16 @@ "node": ">=0.10.0" } }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/has": { "version": "1.0.3", "dev": true, @@ -24796,6 +25338,16 @@ "node": ">=6" } }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-plain-object": { "version": "5.0.0", "dev": true, @@ -28446,6 +28998,19 @@ "tmpl": "1.0.5" } }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/map-or-similar": { "version": "1.5.0", "dev": true, @@ -28652,6 +29217,92 @@ "resolved": "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz", "integrity": "sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==" }, + "node_modules/meow": { + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/meow/-/meow-10.1.5.tgz", + "integrity": "sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimist": "^1.2.2", + "camelcase-keys": "^7.0.0", + "decamelize": "^5.0.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.2", + "read-pkg-up": "^8.0.0", + "redent": "^4.0.0", + "trim-newlines": "^4.0.2", + "type-fest": "^1.2.2", + "yargs-parser": "^20.2.9" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/redent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", + "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^5.0.0", + "strip-indent": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/strip-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", + "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -29247,6 +29898,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/minipass": { "version": "3.3.6", "license": "ISC", @@ -29484,6 +30150,37 @@ "version": "1.0.5", "license": "MIT" }, + "node_modules/nitro-codegen": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/nitro-codegen/-/nitro-codegen-0.18.1.tgz", + "integrity": "sha512-gDOHIIFFY89Ibo/Q8Dlzx4Rk9fCaGnby4Er5Dh1xV4J5hMqTfqo2VjG+RxScdUTYy/SKOc0UsB2faQybs5+GDw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "react-native-nitro-modules": "^0.18.1", + "ts-morph": "^24.0.0", + "yargs": "^17.7.2", + "zod": "^3.23.8" + }, + "bin": { + "nitro-codegen": "lib/index.js" + } + }, + "node_modules/nitro-codegen/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/no-case": { "version": "3.0.4", "dev": true, @@ -29717,6 +30414,22 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "license": "MIT", @@ -32122,9 +32835,9 @@ "license": "MIT" }, "node_modules/react-native-keyboard-controller": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/react-native-keyboard-controller/-/react-native-keyboard-controller-1.14.4.tgz", - "integrity": "sha512-hVt9KhK2dxBNtk4xHTnKLeO9Jv7v5h2TZlIeCQkbBLMd5NIJa4ll0GxIpbuutjP1ctPdhXUVpCfQzgXXJOYlzw==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/react-native-keyboard-controller/-/react-native-keyboard-controller-1.15.0.tgz", + "integrity": "sha512-Laqszs0Uciu9MFkHurLwaHs9kftzUueew75HVOndbdcGR3MbKs2MqKdQEg1AgXSHcGoGg5nKafMOLVIoYjK6kA==", "license": "MIT", "dependencies": { "react-native-is-edge-to-edge": "^1.1.6" @@ -32173,6 +32886,17 @@ "react-native": ">=0.65.0" } }, + "node_modules/react-native-nitro-modules": { + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/react-native-nitro-modules/-/react-native-nitro-modules-0.18.2.tgz", + "integrity": "sha512-eHsq1cRfm/Bz1Nq7KctTqxAqhzVSNo0WGX281xARZh+vOq8633Qxn1NHRZ5/Rno2Bla6HOXlUW6RoW0wKM/7kg==", + "hasInstallScript": true, + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-onyx": { "version": "2.0.86", "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.86.tgz", @@ -32974,6 +33698,69 @@ "node": ">=6" } }, + "node_modules/read-pkg": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-6.0.0.tgz", + "integrity": "sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^3.0.2", + "parse-json": "^5.2.0", + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-8.0.0.tgz", + "integrity": "sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^5.0.0", + "read-pkg": "^6.0.0", + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "license": "MIT", @@ -34373,6 +35160,17 @@ "version": "0.0.2", "dev": true }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, "node_modules/spdx-exceptions": { "version": "2.3.0", "dev": true, @@ -35085,6 +35883,23 @@ "dev": true, "license": "MIT" }, + "node_modules/synckit": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/tabbable": { "version": "6.2.0", "license": "MIT" @@ -35518,6 +36333,48 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz", + "integrity": "sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tinyqueue": { "version": "2.0.3", "license": "ISC" @@ -35633,6 +36490,19 @@ "tree-kill": "cli.js" } }, + "node_modules/trim-newlines": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.1.1.tgz", + "integrity": "sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/trim-right": { "version": "1.0.1", "license": "MIT", @@ -35737,12 +36607,15 @@ "node": ">=10" } }, - "node_modules/ts-jest/node_modules/yargs-parser": { - "version": "21.1.1", + "node_modules/ts-morph": { + "version": "24.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-24.0.0.tgz", + "integrity": "sha512-2OAOg/Ob5yx9Et7ZX4CvTCc0UFoZHwLEJ+dpDPSUi5TgwwlTlX47w+iFRrEwzUZwYACjq83cgjS/Da50Ga37uw==", "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.25.0", + "code-block-writer": "^13.0.3" } }, "node_modules/ts-node": { @@ -36518,6 +37391,17 @@ "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", "integrity": "sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==" }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/validate-npm-package-name": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", @@ -37573,18 +38457,20 @@ "node": ">=12" } }, - "node_modules/yargs/node_modules/y18n": { - "version": "5.0.8", + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "license": "ISC", "engines": { - "node": ">=10" + "node": ">=12" } }, - "node_modules/yargs/node_modules/yargs-parser": { - "version": "21.1.1", + "node_modules/yargs/node_modules/y18n": { + "version": "5.0.8", "license": "ISC", "engines": { - "node": ">=12" + "node": ">=10" } }, "node_modules/yauzl": { diff --git a/package.json b/package.json index 02ef81489e01..c67c99e28e2d 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,30 @@ { "name": "new.expensify", - "version": "9.0.76-6", + "version": "9.0.77-2", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", "license": "MIT", "private": true, "scripts": { + "i-standalone": "STANDALONE_NEW_DOT=true npm i", + "install-standalone": "STANDALONE_NEW_DOT=true npm install", "configure-mapbox": "./scripts/setup-mapbox-sdk-walkthrough.sh", "setupNewDotWebForEmulators": "./scripts/setup-newdot-web-emulators.sh", "startAndroidEmulator": "./scripts/start-android.sh", "postinstall": "./scripts/postInstall.sh", "clean": "./scripts/clean.sh", - "clean-standalone": "./scripts/clean.sh --new-dot", + "clean-standalone": "STANDALONE_NEW_DOT=true ./scripts/clean.sh", "android": "./scripts/set-pusher-suffix.sh && ./scripts/run-build.sh --android", - "android-standalone": "./scripts/set-pusher-suffix.sh && ./scripts/run-build.sh --android --new-dot", + "android-standalone": "./scripts/set-pusher-suffix.sh && STANDALONE_NEW_DOT=true ./scripts/run-build.sh --android", "ios": "./scripts/set-pusher-suffix.sh && ./scripts/run-build.sh --ios", - "ios-standalone": "./scripts/set-pusher-suffix.sh && ./scripts/run-build.sh --ios --new-dot", + "ios-standalone": "./scripts/set-pusher-suffix.sh && STANDALONE_NEW_DOT=true ./scripts/run-build.sh --ios", "pod-install": "./scripts/pod-install.sh", - "pod-install-standalone": "./scripts/pod-install.sh --new-dot", + "pod-install-standalone": "STANDALONE_NEW_DOT=true ./scripts/pod-install.sh", "ipad": "concurrently \"./scripts/run-build.sh --ipad\"", - "ipad-standalone": "concurrently \"./scripts/run-build.sh --ipad --new-dot\"", + "ipad-standalone": "concurrently \"STANDALONE_NEW_DOT=true ./scripts/run-build.sh --ipad\"", "ipad-sm": "concurrently \"./scripts/run-build.sh --ipad-sm\"", - "ipad-sm-standalone": "concurrently \"./scripts/run-build.sh --ipad-sm --new-dot\"", + "ipad-sm-standalone": "concurrently \"STANDALONE_NEW_DOT=true ./scripts/run-build.sh --ipad-sm\"", "start": "npx react-native start", "web": "./scripts/set-pusher-suffix.sh && concurrently npm:web-proxy npm:web-server", "web-proxy": "ts-node web/proxy.ts", @@ -42,7 +44,7 @@ "perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure", "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc", "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=0 --cache --cache-location=node_modules/.cache/eslint", - "lint-changed": "NODE_OPTIONS=--max_old_space_size=8192 eslint --max-warnings=0 --config ./.eslintrc.changed.js $(git diff --diff-filter=AM --name-only origin/main HEAD -- \"*.ts\" \"*.tsx\")", + "lint-changed": "NODE_OPTIONS=--max_old_space_size=8192 eslint --max-warnings=0 --config ./.eslintrc.changed.js $(git diff --diff-filter=AM --name-only origin/main HEAD -- \"*.ts\" \"*.tsx\" \":!modules/**\")", "lint-watch": "npx eslint-watch --watch --changed", "shellcheck": "./scripts/shellCheck.sh", "prettier": "prettier --write .", @@ -74,7 +76,7 @@ }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.207", + "@expensify/react-native-live-markdown": "0.1.209", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -109,6 +111,7 @@ "awesome-phonenumber": "^5.4.0", "babel-polyfill": "^6.26.0", "canvas-size": "^1.2.6", + "contacts-nitro-module": "./modules/ContactsNitroModule", "core-js": "^3.32.0", "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", @@ -154,11 +157,12 @@ "react-native-image-picker": "^7.0.3", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#cb392140db4953a283590d7cf93b4d0461baa2a9", "react-native-key-command": "^1.0.8", - "react-native-keyboard-controller": "1.14.4", + "react-native-keyboard-controller": "1.15.0", "react-native-launch-arguments": "^4.0.2", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", "react-native-onyx": "2.0.86", + "react-native-nitro-modules": "^0.18.1", "react-native-pager-view": "6.5.1", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -282,7 +286,7 @@ "electron-builder": "25.0.0", "eslint": "^8.57.0", "eslint-config-airbnb-typescript": "^18.0.0", - "eslint-config-expensify": "^2.0.74", + "eslint-config-expensify": "2.0.75", "eslint-config-prettier": "^9.1.0", "eslint-plugin-deprecation": "^3.0.0", "eslint-plugin-jest": "^28.6.0", @@ -374,17 +378,7 @@ ] } }, - "electronmon": { - "patterns": [ - "!src/**", - "!ios/**", - "!android/**", - "!tests/**", - "*.test.*" - ] - }, - "engines": { - "node": "20.18.0", - "npm": "10.8.2" - } + "workspaces": [ + "modules/ContactsNitroModule" + ] } diff --git a/patches/react-native+0.75.2+026+fix-dropping-mutations-in-transactions.patch b/patches/react-native+0.75.2+026+fix-dropping-mutations-in-transactions.patch new file mode 100644 index 000000000000..974a0d090fb9 --- /dev/null +++ b/patches/react-native+0.75.2+026+fix-dropping-mutations-in-transactions.patch @@ -0,0 +1,67 @@ +diff --git a/node_modules/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp b/node_modules/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp +index 572fb3d..0efa1ed 100644 +--- a/node_modules/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp ++++ b/node_modules/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp +@@ -468,7 +468,7 @@ void Binding::schedulerDidFinishTransaction( + mountingTransaction->getSurfaceId(); + }); + +- if (pendingTransaction != pendingTransactions_.end()) { ++ if (pendingTransaction != pendingTransactions_.end() && pendingTransaction->canMergeWith(*mountingTransaction)) { + pendingTransaction->mergeWith(std::move(*mountingTransaction)); + } else { + pendingTransactions_.push_back(std::move(*mountingTransaction)); +diff --git a/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.cpp b/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.cpp +index d7dd1bc..d95d779 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.cpp ++++ b/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.cpp +@@ -5,6 +5,8 @@ + * LICENSE file in the root directory of this source tree. + */ + ++#include ++ + #include "MountingTransaction.h" + + namespace facebook::react { +@@ -54,4 +56,21 @@ void MountingTransaction::mergeWith(MountingTransaction&& transaction) { + telemetry_ = std::move(transaction.telemetry_); + } + ++bool MountingTransaction::canMergeWith(MountingTransaction& transaction) { ++ std::set deletedTags; ++ for (const auto& mutation : mutations_) { ++ if (mutation.type == ShadowViewMutation::Type::Delete) { ++ deletedTags.insert(mutation.oldChildShadowView.tag); ++ } ++ } ++ ++ for (const auto& mutation : transaction.getMutations()) { ++ if (mutation.type == ShadowViewMutation::Type::Create && deletedTags.contains(mutation.newChildShadowView.tag)) { ++ return false; ++ } ++ } ++ ++ return true; ++} ++ + } // namespace facebook::react +diff --git a/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.h b/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.h +index 277e9f4..38629db 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.h ++++ b/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.h +@@ -85,6 +85,14 @@ class MountingTransaction final { + */ + void mergeWith(MountingTransaction&& transaction); + ++ /* ++ * Checks whether the two transactions can be safely merged. Due to ++ * reordering of mutations during mount, the sequence of ++ * REMOVE -> DELETE | CREATE -> INSERT (2 transactions) may get changed to ++ * INSERT -> REMOVE -> DELETE and the state will diverge from there. ++ */ ++ bool canMergeWith(MountingTransaction& transaction); ++ + private: + SurfaceId surfaceId_; + Number number_; diff --git a/patches/react-native-screens+3.34.0+004+ios-custom-animations-native-transitions.patch b/patches/react-native-screens+3.34.0+004+ios-custom-animations-native-transitions.patch index 62cbf68f458d..52f8d76c4fe1 100644 --- a/patches/react-native-screens+3.34.0+004+ios-custom-animations-native-transitions.patch +++ b/patches/react-native-screens+3.34.0+004+ios-custom-animations-native-transitions.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/react-native-screens/ios/RNSScreenStackAnimator.mm b/node_modules/react-native-screens/ios/RNSScreenStackAnimator.mm -index abb2cf6..fb81d52 100644 +index abb2cf6..c21b3e9 100644 --- a/node_modules/react-native-screens/ios/RNSScreenStackAnimator.mm +++ b/node_modules/react-native-screens/ios/RNSScreenStackAnimator.mm @@ -5,13 +5,14 @@ @@ -32,7 +32,7 @@ index abb2cf6..fb81d52 100644 } @@ -129,6 +130,8 @@ - (void)animateSimplePushWithShadowEnabled:(BOOL)shadowEnabled } - + [UIView animateWithDuration:[self transitionDuration:transitionContext] + delay:0 + options:UIViewAnimationOptionCurveDefaultTransition @@ -66,25 +66,7 @@ index abb2cf6..fb81d52 100644 animations:animationBlock completion:completionBlock]; } else { -@@ -251,6 +260,8 @@ - (void)animateFadeWithTransitionContext:(id; replaceAnimation?: WithDefault; swipeDirection?: WithDefault; - hideKeyboardOnSwipe?: boolean; \ No newline at end of file + hideKeyboardOnSwipe?: boolean; diff --git a/react-native.config.js b/react-native.config.js index 773375378acd..ffbc8f5c65cb 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -1,3 +1,5 @@ +const path = require('path'); +const pak = require('./modules/ContactsNitroModule/package.json'); const iosSourceDir = process.env.PROJECT_ROOT_PATH ? process.env.PROJECT_ROOT_PATH + 'ios' : 'ios'; const androidSourceDir = process.env.PROJECT_ROOT_PATH ? process.env.PROJECT_ROOT_PATH + 'android' : 'android'; @@ -7,4 +9,9 @@ module.exports = { android: {sourceDir: androidSourceDir}, }, assets: ['./assets/fonts/native'], + dependencies: { + [pak.name]: { + root: path.join(__dirname, 'modules', 'ContactsNitroModule'), + }, + }, }; diff --git a/scripts/applyPatches.sh b/scripts/applyPatches.sh index 29e121acc968..9ba8360ea39f 100755 --- a/scripts/applyPatches.sh +++ b/scripts/applyPatches.sh @@ -11,11 +11,13 @@ source "$SCRIPTS_DIR/shellUtils.sh" function patchPackage { # See if we're in the HybridApp repo IS_HYBRID_APP_REPO=$(scripts/is-hybrid-app.sh) + NEW_DOT_FLAG="${STANDALONE_NEW_DOT:-false}" OS="$(uname)" if [[ "$OS" == "Darwin" || "$OS" == "Linux" ]]; then npx patch-package --error-on-fail --color=always - if [[ "$IS_HYBRID_APP_REPO" == "true" ]]; then + if [[ "$IS_HYBRID_APP_REPO" == "true" && "$NEW_DOT_FLAG" == "false" ]]; then + echo -e "\n${GREEN}Applying HybridApp patches!${NC}" npx patch-package --patch-dir 'Mobile-Expensify/patches' --error-on-fail --color=always fi else diff --git a/scripts/clean.sh b/scripts/clean.sh index 1ecd73731b61..fbbfa070d442 100755 --- a/scripts/clean.sh +++ b/scripts/clean.sh @@ -7,7 +7,10 @@ NC='\033[0m' # See if we're in the HybridApp repo IS_HYBRID_APP_REPO=$(scripts/is-hybrid-app.sh) -if [[ "$IS_HYBRID_APP_REPO" == "true" && "$1" != "--new-dot" ]]; then +# See if we should force standalone NewDot build +NEW_DOT_FLAG="${STANDALONE_NEW_DOT:-false}" + +if [[ "$IS_HYBRID_APP_REPO" == "true" && "$NEW_DOT_FLAG" == "false" ]]; then echo -e "${BLUE}Cleaning HybridApp project...${NC}" # Navigate to Mobile-Expensify repository, and clean cd Mobile-Expensify diff --git a/scripts/pod-install.sh b/scripts/pod-install.sh index 8e38f1706d6f..77237bb207b4 100755 --- a/scripts/pod-install.sh +++ b/scripts/pod-install.sh @@ -45,11 +45,9 @@ fi # See if we're in the HybridApp repo IS_HYBRID_APP_REPO=$(scripts/is-hybrid-app.sh) -NEW_DOT_FLAG="false" -if [ "$1" == "--new-dot" ]; then - NEW_DOT_FLAG="true" -fi +# See if we should force standalone NewDot build +NEW_DOT_FLAG="${STANDALONE_NEW_DOT:-false}" if [[ "$IS_HYBRID_APP_REPO" == "true" && "$NEW_DOT_FLAG" == "false" ]]; then echo -e "${BLUE}Executing npm run pod-install for HybridApp...${NC}" diff --git a/scripts/postInstall.sh b/scripts/postInstall.sh index db24f04f8a6c..c2adcadc4f43 100755 --- a/scripts/postInstall.sh +++ b/scripts/postInstall.sh @@ -10,7 +10,11 @@ cd "$ROOT_DIR" || exit 1 # See if we're in the HybridApp repo IS_HYBRID_APP_REPO=$(scripts/is-hybrid-app.sh) -if [[ "$IS_HYBRID_APP_REPO" == "true" ]]; then +# See if we should force standalone NewDot build +NEW_DOT_FLAG="${STANDALONE_NEW_DOT:-false}" + +if [[ "$IS_HYBRID_APP_REPO" == "true" && "$NEW_DOT_FLAG" == "false" ]]; then + echo -e "\n${GREEN}Installing node modules in Mobile-Expensify submodule!${NC}" cd Mobile-Expensify || exit 1 npm i diff --git a/scripts/run-build.sh b/scripts/run-build.sh index 7689aabbbf59..67da6285e1a0 100755 --- a/scripts/run-build.sh +++ b/scripts/run-build.sh @@ -3,8 +3,6 @@ set -e export PROJECT_ROOT_PATH -BUILD="$1" -NEW_DOT_FLAG="false" IOS_MODE="DebugDevelopment" ANDROID_MODE="developmentDebug" SCHEME="New Expensify Dev" @@ -20,31 +18,24 @@ function print_error_and_exit { exit 1 } -# Assign the arguments to variables -if [ "$#" -eq 1 ]; then - BUILD="$1" -elif [ "$#" -eq 2 ]; then - if [ "$1" == "--new-dot" ]; then - BUILD="$2" - NEW_DOT_FLAG="true" - elif [ "$2" == "--new-dot" ]; then - BUILD="$1" - NEW_DOT_FLAG="true" - else - print_error_and_exit - fi -else +# Assign the arguments to variables if arguments are correct +if [ "$#" -ne 1 ] || [[ "$1" != "--ios" && "$1" != "--ipad" && "$1" != "--ipad-sm" && "$1" != "--android" ]]; then print_error_and_exit fi +BUILD="$1" + # See if we're in the HybridApp repo IS_HYBRID_APP_REPO=$(scripts/is-hybrid-app.sh) +# See if we should force standalone NewDot build +NEW_DOT_FLAG="${STANDALONE_NEW_DOT:-false}" + if [[ "$IS_HYBRID_APP_REPO" == "true" && "$NEW_DOT_FLAG" == "false" ]]; then # Set HybridApp-specific arguments IOS_MODE="Debug" ANDROID_MODE="Debug" - SCHEME="Expensify" + SCHEME="Expensify Dev" APP_ID="org.me.mobiexpensifyg" echo -e "\n${GREEN}Starting a HybridApp build!${NC}" diff --git a/src/App.tsx b/src/App.tsx index cc824b78fa4c..52904e0a06c4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,7 +18,6 @@ import KeyboardProvider from './components/KeyboardProvider'; import {LocaleContextProvider} from './components/LocaleContextProvider'; import OnyxProvider from './components/OnyxProvider'; import PopoverContextProvider from './components/PopoverProvider'; -import {ProductTrainingContextProvider} from './components/ProductTrainingContext'; import SafeArea from './components/SafeArea'; import ScrollOffsetContextProvider from './components/ScrollOffsetContextProvider'; import {SearchRouterContextProvider} from './components/Search/SearchRouter/SearchRouterContext'; @@ -96,7 +95,6 @@ function App({url}: AppProps) { VideoPopoverMenuContextProvider, KeyboardProvider, SearchRouterContextProvider, - ProductTrainingContextProvider, ]} > diff --git a/src/CONST.ts b/src/CONST.ts index 8fd7705687cf..0a87f3f31b3b 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -514,6 +514,7 @@ const CONST = { MAX_DATE: '9999-12-31', MIN_DATE: '0001-01-01', ORDINAL_DAY_OF_MONTH: 'do', + MONTH_DAY_YEAR_ORDINAL_FORMAT: 'MMMM do, yyyy', }, SMS: { DOMAIN: '@expensify.sms', @@ -900,13 +901,8 @@ const CONST = { DEEP_DIVE_EXPENSIFY_CARD: 'https://community.expensify.com/discussion/4848/deep-dive-expensify-card-and-quickbooks-online-auto-reconciliation-how-it-works', DEEP_DIVE_ERECEIPTS: 'https://community.expensify.com/discussion/5542/deep-dive-what-are-ereceipts/', DEEP_DIVE_PER_DIEM: 'https://community.expensify.com/discussion/4772/how-to-add-a-single-rate-per-diem', + SET_NOTIFICATION_LINK: 'https://community.expensify.com/discussion/5651/deep-dive--practices-when-youre-running-into-trouble-receiving-emails-from-expensify', GITHUB_URL: 'https://github.com/Expensify/App', - TERMS_URL: `${EXPENSIFY_URL}/terms`, - PRIVACY_URL: `${EXPENSIFY_URL}/privacy`, - LICENSES_URL: `${USE_EXPENSIFY_URL}/licenses`, - ACH_TERMS_URL: `${EXPENSIFY_URL}/achterms`, - WALLET_AGREEMENT_URL: `${EXPENSIFY_URL}/expensify-payments-wallet-terms-of-service`, - BANCORP_WALLET_AGREEMENT_URL: `${EXPENSIFY_URL}/bancorp-bank-wallet-terms-of-service`, HELP_LINK_URL: `${USE_EXPENSIFY_URL}/usa-patriot-act`, ELECTRONIC_DISCLOSURES_URL: `${USE_EXPENSIFY_URL}/esignagreement`, GITHUB_RELEASE_URL: 'https://api.github.com/repos/expensify/app/releases/latest', @@ -951,7 +947,14 @@ const CONST = { EMPLOYEE_TOUR_PRODUCTION: 'https://expensify.navattic.com/35609gb', EMPLOYEE_TOUR_STAGING: 'https://expensify.navattic.com/cf15002s', }, - + OLD_DOT_PUBLIC_URLS: { + TERMS_URL: `${EXPENSIFY_URL}/terms`, + PRIVACY_URL: `${EXPENSIFY_URL}/privacy`, + LICENSES_URL: `${USE_EXPENSIFY_URL}/licenses`, + ACH_TERMS_URL: `${EXPENSIFY_URL}/achterms`, + WALLET_AGREEMENT_URL: `${EXPENSIFY_URL}/expensify-payments-wallet-terms-of-service`, + BANCORP_WALLET_AGREEMENT_URL: `${EXPENSIFY_URL}/bancorp-bank-wallet-terms-of-service`, + }, OLDDOT_URLS: { ADMIN_POLICIES_URL: 'admin_policies', ADMIN_DOMAINS_URL: 'admin_domains', @@ -4492,7 +4495,7 @@ const CONST = { BOOK_TRAVEL_DEMO_URL: 'https://calendly.com/d/ck2z-xsh-q97/expensify-travel-demo-travel-page', TRAVEL_DOT_URL: 'https://travel.expensify.com', STAGING_TRAVEL_DOT_URL: 'https://staging.travel.expensify.com', - TRIP_ID_PATH: (tripID: string) => `trips/${tripID}`, + TRIP_ID_PATH: (tripID?: string) => (tripID ? `trips/${tripID}` : undefined), SPOTNANA_TMC_ID: '8e8e7258-1cf3-48c0-9cd1-fe78a6e31eed', STAGING_SPOTNANA_TMC_ID: '7a290c6e-5328-4107-aff6-e48765845b81', SCREEN_READER_STATES: { @@ -5994,6 +5997,7 @@ const CONST = { CAR: 'car', HOTEL: 'hotel', FLIGHT: 'flight', + TRAIN: 'train', }, DOT_SEPARATOR: '•', @@ -6022,6 +6026,7 @@ const CONST = { DOWNLOADS_PATH: '/Downloads', DOWNLOADS_TIMEOUT: 5000, NEW_EXPENSIFY_PATH: '/New Expensify', + RECEIPTS_UPLOAD_PATH: '/Receipts-Upload', ENVIRONMENT_SUFFIX: { DEV: ' Dev', @@ -6472,22 +6477,19 @@ const CONST = { }, }, + DEVICE_CONTACT: { + FIRST_NAME: 'FIRST_NAME', + LAST_NAME: 'LAST_NAME', + PHONE_NUMBERS: 'PHONE_NUMBERS', + EMAIL_ADDRESSES: 'EMAIL_ADDRESSES', + IMAGE_DATA: 'IMAGE_DATA', + }, + HYBRID_APP: { REORDERING_REACT_NATIVE_ACTIVITY_TO_FRONT: 'reorderingReactNativeActivityToFront', }, MIGRATED_USER_WELCOME_MODAL: 'migratedUserWelcomeModal', - - PRODUCT_TRAINING_TOOLTIP_NAMES: { - CONCEIRGE_LHN_GBR: 'conciergeLHNGBR', - RENAME_SAVED_SEARCH: 'renameSavedSearch', - QUICK_ACTION_BUTTON: 'quickActionButton', - WORKSAPCE_CHAT_CREATE: 'workspaceChatCreate', - SEARCH_FILTER_BUTTON_TOOLTIP: 'filterButtonTooltip', - BOTTOM_NAV_INBOX_TOOLTIP: 'bottomNavInboxTooltip', - LHN_WORKSPACE_CHAT_TOOLTIP: 'workspaceChatLHNTooltip', - GLOBAL_CREATE_TOOLTIP: 'globalCreateTooltip', - }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 5db695243c92..8681c212dc2d 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -117,6 +117,9 @@ const ONYXKEYS = { /** NVP keys */ + /** Boolean flag only true when first set */ + NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER: 'nvp_isFirstTimeNewExpensifyUser', + /** This NVP contains list of at most 5 recent attendees */ NVP_RECENT_ATTENDEES: 'nvp_expensify_recentAttendees', @@ -219,9 +222,18 @@ const ONYXKEYS = { /** The end date (epoch timestamp) of the workspace owner’s grace period after the free trial ends. */ NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END: 'nvp_private_billingGracePeriodEnd', + /** The NVP containing all information related to educational tooltip in workspace chat */ + NVP_WORKSPACE_TOOLTIP: 'workspaceTooltip', + /** The NVP containing the target url to navigate to when deleting a transaction */ NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL: 'nvp_deleteTransactionNavigateBackURL', + /** Whether to show save search rename tooltip */ + SHOULD_SHOW_SAVED_SEARCH_RENAME_TOOLTIP: 'shouldShowSavedSearchRenameTooltip', + + /** Whether to hide gbr tooltip */ + NVP_SHOULD_HIDE_GBR_TOOLTIP: 'nvp_should_hide_gbr_tooltip', + /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', @@ -879,6 +891,7 @@ type OnyxCollectionValuesMapping = { type OnyxValuesMapping = { [ONYXKEYS.ACCOUNT]: OnyxTypes.Account; [ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID]: string; + [ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER]: boolean; // NVP_ONBOARDING is an array for old users. [ONYXKEYS.NVP_ONBOARDING]: Onboarding | []; @@ -1021,7 +1034,9 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_BILLING_FUND_ID]: number; [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: number; [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: number; + [ONYXKEYS.NVP_WORKSPACE_TOOLTIP]: OnyxTypes.WorkspaceTooltip; [ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL]: string | undefined; + [ONYXKEYS.NVP_SHOULD_HIDE_GBR_TOOLTIP]: boolean; [ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS]: OnyxTypes.CancellationDetails[]; [ONYXKEYS.ROOM_MEMBERS_USER_SEARCH_PHRASE]: string; [ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflowOnyx; @@ -1029,6 +1044,7 @@ type OnyxValuesMapping = { [ONYXKEYS.LAST_ROUTE]: string; [ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY]: boolean | undefined; [ONYXKEYS.IS_USING_IMPORTED_STATE]: boolean; + [ONYXKEYS.SHOULD_SHOW_SAVED_SEARCH_RENAME_TOOLTIP]: boolean; [ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES]: Record; [ONYXKEYS.CONCIERGE_REPORT_ID]: string; [ONYXKEYS.SHARE_UNKNOWN_USER_DETAILS]: Participant; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 1fe99320853e..2ba35ef6af5e 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -716,6 +716,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/profile/address', getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/profile/address` as const, backTo), }, + WORKSPACE_PROFILE_PLAN: { + route: 'settings/workspaces/:policyID/profile/plan', + getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/profile/plan` as const, backTo), + }, WORKSPACE_ACCOUNTING: { route: 'settings/workspaces/:policyID/accounting', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting` as const, @@ -979,9 +983,9 @@ const ROUTES = { getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/category/${encodeURIComponent(categoryName)}` as const, }, WORKSPACE_UPGRADE: { - route: 'settings/workspaces/:policyID/upgrade/:featureName', - getRoute: (policyID: string, featureName: string, backTo?: string) => - getUrlWithBackToParam(`settings/workspaces/${policyID}/upgrade/${encodeURIComponent(featureName)}` as const, backTo), + route: 'settings/workspaces/:policyID/upgrade/:featureName?', + getRoute: (policyID: string, featureName?: string, backTo?: string) => + getUrlWithBackToParam(`settings/workspaces/${policyID}/upgrade/${encodeURIComponent(featureName ?? '')}` as const, backTo), }, WORKSPACE_DOWNGRADE: { route: 'settings/workspaces/:policyID/downgrade/', @@ -1162,16 +1166,16 @@ const ROUTES = { }, WORKSPACE_REPORT_FIELDS_LIST_VALUES: { route: 'settings/workspaces/:policyID/reportFields/listValues/:reportFieldID?', - getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/listValues/${encodeURIComponent(reportFieldID ?? '')}` as const, + getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/listValues/${reportFieldID ? encodeURIComponent(reportFieldID) : ''}` as const, }, WORKSPACE_REPORT_FIELDS_ADD_VALUE: { route: 'settings/workspaces/:policyID/reportFields/addValue/:reportFieldID?', - getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/addValue/${encodeURIComponent(reportFieldID ?? '')}` as const, + getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/addValue/${reportFieldID ? encodeURIComponent(reportFieldID) : ''}` as const, }, WORKSPACE_REPORT_FIELDS_VALUE_SETTINGS: { route: 'settings/workspaces/:policyID/reportFields/:valueIndex/:reportFieldID?', getRoute: (policyID: string, valueIndex: number, reportFieldID?: string) => - `settings/workspaces/${policyID}/reportFields/${valueIndex}/${encodeURIComponent(reportFieldID ?? '')}` as const, + `settings/workspaces/${policyID}/reportFields/${valueIndex}/${reportFieldID ? encodeURIComponent(reportFieldID) : ''}` as const, }, WORKSPACE_REPORT_FIELDS_EDIT_VALUE: { route: 'settings/workspaces/:policyID/reportFields/new/:valueIndex/edit', @@ -1375,6 +1379,15 @@ const ROUTES = { TRAVEL_MY_TRIPS: 'travel', TRAVEL_TCS: 'travel/terms', TRACK_TRAINING_MODAL: 'track-training', + TRAVEL_TRIP_SUMMARY: { + route: 'r/:reportID/trip/:transactionID', + getRoute: (reportID: string, transactionID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/trip/${transactionID}`, backTo), + }, + TRAVEL_TRIP_DETAILS: { + route: 'r/:reportID/trip/:transactionID/:reservationIndex', + getRoute: (reportID: string, transactionID: string, reservationIndex: number, backTo?: string) => + getUrlWithBackToParam(`r/${reportID}/trip/${transactionID}/${reservationIndex}`, backTo), + }, ONBOARDING_ROOT: { route: 'onboarding', getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding`, backTo), diff --git a/src/SCREENS.ts b/src/SCREENS.ts index f7f55a8eca13..4d3f20678695 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -27,6 +27,8 @@ const SCREENS = { TRAVEL: { MY_TRIPS: 'Travel_MyTrips', TCS: 'Travel_TCS', + TRIP_SUMMARY: 'Travel_TripSummary', + TRIP_DETAILS: 'Travel_TripDetails', }, SEARCH: { CENTRAL_PANE: 'Search_Central_Pane', @@ -503,6 +505,7 @@ const SCREENS = { TAG_GL_CODE: 'Tag_GL_Code', CURRENCY: 'Workspace_Profile_Currency', ADDRESS: 'Workspace_Profile_Address', + PLAN: 'Workspace_Profile_Plan_Type', WORKFLOWS: 'Workspace_Workflows', WORKFLOWS_PAYER: 'Workspace_Workflows_Payer', WORKFLOWS_APPROVALS_NEW: 'Workspace_Approvals_New', diff --git a/src/components/AddPaymentCard/PaymentCardForm.tsx b/src/components/AddPaymentCard/PaymentCardForm.tsx index 9843996602f1..bcb3e27783e8 100644 --- a/src/components/AddPaymentCard/PaymentCardForm.tsx +++ b/src/components/AddPaymentCard/PaymentCardForm.tsx @@ -48,8 +48,8 @@ function IAcceptTheLabel() { return ( {`${translate('common.iAcceptThe')}`} - {`${translate('common.addCardTermsOfService')}`} {`${translate('common.and')}`} - {` ${translate('common.privacyPolicy')} `} + {`${translate('common.addCardTermsOfService')}`} {`${translate('common.and')}`} + {` ${translate('common.privacyPolicy')} `} ); } diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index c443b1ab8093..c0010af468af 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -7,7 +7,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import CONST from '@src/CONST'; -import type {ReportAction, ReportActions} from '@src/types/onyx'; +import type {Report, ReportAction, ReportActions} from '@src/types/onyx'; import type {Note} from '@src/types/onyx/Report'; /** @@ -20,12 +20,11 @@ function extractAttachments( accountID, parentReportAction, reportActions, - reportID, - }: {privateNotes?: Record; accountID?: number; parentReportAction?: OnyxEntry; reportActions?: OnyxEntry; reportID: string}, + report, + }: {privateNotes?: Record; accountID?: number; parentReportAction?: OnyxEntry; reportActions?: OnyxEntry; report: OnyxEntry}, ) { const targetNote = privateNotes?.[Number(accountID)]?.note ?? ''; const attachments: Attachment[] = []; - const report = ReportUtils.getReport(reportID); const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); // We handle duplicate image sources by considering the first instance as original. Selecting any duplicate diff --git a/src/components/Attachments/AttachmentCarousel/index.native.tsx b/src/components/Attachments/AttachmentCarousel/index.native.tsx index 9aa619eb1cda..68668ccc6ab0 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.native.tsx @@ -34,9 +34,9 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : undefined; let newAttachments: Attachment[] = []; if (type === CONST.ATTACHMENT_TYPE.NOTE && accountID) { - newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {privateNotes: report.privateNotes, accountID, reportID: report.reportID}); + newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {privateNotes: report.privateNotes, accountID, report}); } else { - newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions, reportID: report.reportID}); + newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions, report}); } let newIndex = newAttachments.findIndex(compareImage); @@ -68,7 +68,7 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi } } // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [reportActions, compareImage]); + }, [reportActions, compareImage, report]); /** Updates the page state when the user navigates between attachments */ const updatePage = useCallback( diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index f169416f1812..50caaac3dd81 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -89,9 +89,9 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : undefined; let newAttachments: Attachment[] = []; if (type === CONST.ATTACHMENT_TYPE.NOTE && accountID) { - newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {privateNotes: report.privateNotes, accountID, reportID: report.reportID}); + newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {privateNotes: report.privateNotes, accountID, report}); } else { - newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions: reportActions ?? undefined, reportID: report.reportID}); + newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions: reportActions ?? undefined, report}); } if (isEqual(attachments, newAttachments)) { @@ -130,19 +130,7 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi onNavigate(attachment); } } - }, [ - report.privateNotes, - reportActions, - parentReportActions, - compareImage, - report.parentReportActionID, - attachments, - setDownloadButtonVisibility, - onNavigate, - accountID, - type, - report.reportID, - ]); + }, [reportActions, parentReportActions, compareImage, attachments, setDownloadButtonVisibility, onNavigate, accountID, type, report]); // Scroll position is affected when window width is resized, so we readjust it on width changes useEffect(() => { diff --git a/src/components/ContactPermissionModal/index.native.tsx b/src/components/ContactPermissionModal/index.native.tsx new file mode 100644 index 000000000000..825c8bc4afbe --- /dev/null +++ b/src/components/ContactPermissionModal/index.native.tsx @@ -0,0 +1,73 @@ +import React, {useEffect, useState} from 'react'; +import {InteractionManager} from 'react-native'; +import {RESULTS} from 'react-native-permissions'; +import ConfirmModal from '@components/ConfirmModal'; +import * as Illustrations from '@components/Icon/Illustrations'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getContactPermission} from '@libs/ContactPermission'; +import type {ContactPermissionModalProps} from './types'; + +function ContactPermissionModal({startPermissionFlow, resetPermissionFlow, onDeny, onGrant}: ContactPermissionModalProps) { + const [isModalVisible, setIsModalVisible] = useState(false); + + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + useEffect(() => { + if (!startPermissionFlow) { + return; + } + getContactPermission().then((status) => { + if (status === RESULTS.GRANTED || status === RESULTS.LIMITED) { + return onGrant(); + } + if (status === RESULTS.BLOCKED) { + return; + } + setIsModalVisible(true); + }); + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- We only want to run this effect when startPermissionFlow changes + }, [startPermissionFlow]); + + const handleGrantPermission = () => { + setIsModalVisible(false); + InteractionManager.runAfterInteractions(onGrant); + }; + + const handleDenyPermission = () => { + onDeny(RESULTS.DENIED); + setIsModalVisible(false); + }; + + const handleCloseModal = () => { + setIsModalVisible(false); + resetPermissionFlow(); + }; + + return ( + + ); +} + +ContactPermissionModal.displayName = 'ContactPermissionModal'; + +export default ContactPermissionModal; diff --git a/src/components/ContactPermissionModal/index.tsx b/src/components/ContactPermissionModal/index.tsx new file mode 100644 index 000000000000..3f7e25bac590 --- /dev/null +++ b/src/components/ContactPermissionModal/index.tsx @@ -0,0 +1,10 @@ +import type {ContactPermissionModalProps} from './types'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function ContactPermissionModal(props: ContactPermissionModalProps) { + return null; +} + +ContactPermissionModal.displayName = 'ContactPermissionModal'; + +export default ContactPermissionModal; diff --git a/src/components/ContactPermissionModal/types.ts b/src/components/ContactPermissionModal/types.ts new file mode 100644 index 000000000000..5c831410656f --- /dev/null +++ b/src/components/ContactPermissionModal/types.ts @@ -0,0 +1,19 @@ +import type {PermissionStatus} from 'react-native-permissions'; + +type ContactPermissionModalProps = { + /** A callback to call when the permission has been granted */ + onGrant: () => void; + + /** A callback to call when the permission has been denied */ + onDeny: (permission: PermissionStatus) => void; + + /** Should start the permission flow? */ + startPermissionFlow: boolean; + + /** Reset the permission flow */ + resetPermissionFlow: () => void; +}; + +export default {}; + +export type {ContactPermissionModalProps}; diff --git a/src/components/FloatingActionButton.tsx b/src/components/FloatingActionButton.tsx index e0f0ff4e6dcd..3c831301db8b 100644 --- a/src/components/FloatingActionButton.tsx +++ b/src/components/FloatingActionButton.tsx @@ -5,16 +5,10 @@ import type {GestureResponderEvent, Role, Text, View} from 'react-native'; import {Platform} from 'react-native'; import Animated, {createAnimatedPropAdapter, Easing, interpolateColor, processColor, useAnimatedProps, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import Svg, {Path} from 'react-native-svg'; -import useBottomTabIsFocused from '@hooks/useBottomTabIsFocused'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import getPlatform from '@libs/getPlatform'; import variables from '@styles/variables'; -import CONST from '@src/CONST'; import {PressableWithoutFeedback} from './Pressable'; -import {useProductTrainingContext} from './ProductTrainingContext'; -import EducationalTooltip from './Tooltip/EducationalTooltip'; const AnimatedPath = Animated.createAnimatedComponent(Path); AnimatedPath.displayName = 'AnimatedPath'; @@ -62,14 +56,6 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo const styles = useThemeStyles(); const borderRadius = styles.floatingActionButton.borderRadius; const fabPressable = useRef(null); - const {shouldUseNarrowLayout} = useResponsiveLayout(); - const platform = getPlatform(); - const isNarrowScreenOnWeb = shouldUseNarrowLayout && platform === CONST.PLATFORM.WEB; - const isFocused = useBottomTabIsFocused(); - const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext( - CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.GLOBAL_CREATE_TOOLTIP, - isFocused, - ); const sharedValue = useSharedValue(isActive ? 1 : 0); const buttonRef = ref; @@ -111,45 +97,32 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo }; return ( - { + fabPressable.current = el ?? null; + if (buttonRef && 'current' in buttonRef) { + buttonRef.current = el ?? null; + } }} - shouldUseOverlay - shiftHorizontal={isNarrowScreenOnWeb ? 0 : variables.fabTooltipShiftHorizontal} - renderTooltipContent={renderProductTrainingTooltip} - wrapperStyle={styles.productTrainingTooltipWrapper} - onHideTooltip={hideProductTrainingTooltip} + style={[styles.h100, styles.bottomTabBarItem]} + accessibilityLabel={accessibilityLabel} + onPress={toggleFabAction} + onLongPress={() => {}} + role={role} + shouldUseHapticsOnLongPress={false} > - { - fabPressable.current = el ?? null; - if (buttonRef && 'current' in buttonRef) { - buttonRef.current = el ?? null; - } - }} - style={[styles.h100, styles.bottomTabBarItem]} - accessibilityLabel={accessibilityLabel} - onPress={toggleFabAction} - onLongPress={() => {}} - role={role} - shouldUseHapticsOnLongPress={false} - > - - - - - - - + + + + + + ); } diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index 2c07c48d52b7..b4d097e90994 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -10,7 +10,6 @@ import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeed import SearchButton from '@components/Search/SearchRouter/SearchButton'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; import Tooltip from '@components/Tooltip'; -import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; @@ -26,6 +25,9 @@ import type HeaderWithBackButtonProps from './types'; function HeaderWithBackButton({ icon, iconFill, + iconWidth, + iconHeight, + iconStyles, guidesCallTaskID = '', onBackButtonPress = () => Navigation.goBack(), onCloseButtonPress = () => Navigation.dismissModal(), @@ -46,6 +48,7 @@ function HeaderWithBackButton({ shouldSetModalVisibility = true, shouldShowThreeDotsButton = false, shouldDisableThreeDotsButton = false, + shouldUseHeadlineHeader = false, stepCounter, subtitle = '', title = '', @@ -72,10 +75,6 @@ function HeaderWithBackButton({ const StyleUtils = useStyleUtils(); const [isDownloadButtonActive, temporarilyDisableDownloadButton] = useThrottledButtonState(); const {translate} = useLocalize(); - const {isKeyboardShown} = useKeyboardState(); - - // If the icon is present, the header bar should be taller and use different font. - const isCentralPaneSettings = !!icon; const middleContent = useMemo(() => { if (progressBarPercentage) { @@ -108,14 +107,14 @@ function HeaderWithBackButton({
); }, [ StyleUtils, subTitleLink, - isCentralPaneSettings, + shouldUseHeadlineHeader, policy, progressBarPercentage, report, @@ -140,7 +139,7 @@ function HeaderWithBackButton({ dataSet={{dragArea: false}} style={[ styles.headerBar, - isCentralPaneSettings && styles.headerBarDesktopHeight, + shouldUseHeadlineHeader && styles.headerBarDesktopHeight, shouldShowBorderBottom && styles.borderBottom, // progressBarPercentage can be 0 which would // be falsey, hence using !== undefined explicitly @@ -155,7 +154,7 @@ function HeaderWithBackButton({ { - if (isKeyboardShown) { + if (Keyboard.isVisible()) { Keyboard.dismiss(); } const topmostReportId = Navigation.getTopmostReportId(); @@ -180,9 +179,10 @@ function HeaderWithBackButton({ {!!icon && ( )} {!!policyAvatar && ( diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts index 6eef2b072eee..d2d4ba9e4e0f 100644 --- a/src/components/HeaderWithBackButton/types.ts +++ b/src/components/HeaderWithBackButton/types.ts @@ -38,6 +38,15 @@ type HeaderWithBackButtonProps = Partial & { * */ icon?: IconAsset; + /** Icon Width */ + iconWidth?: number; + + /** Icon Height */ + iconHeight?: number; + + /** Any additional styles to pass to the icon container. */ + iconStyles?: StyleProp; + /** Method to trigger when pressing download button of the header */ onDownloadButtonPress?: () => void; @@ -119,6 +128,9 @@ type HeaderWithBackButtonProps = Partial & { /** Whether we should navigate to report page when the route have a topMostReport */ shouldNavigateToTopMostReport?: boolean; + /** Whether the header should use the headline header style */ + shouldUseHeadlineHeader?: boolean; + /** The fill color for the icon. Can be hex, rgb, rgba, or valid react-native named color such as 'red' or 'blue'. */ iconFill?: string; diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 51db1bc12c8e..4093b44743fe 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -187,6 +187,7 @@ import Task from '@assets/images/task.svg'; import Thread from '@assets/images/thread.svg'; import ThreeDots from '@assets/images/three-dots.svg'; import ThumbsUp from '@assets/images/thumbs-up.svg'; +import Train from '@assets/images/train.svg'; import Transfer from '@assets/images/transfer.svg'; import Trashcan from '@assets/images/trashcan.svg'; import Unlock from '@assets/images/unlock.svg'; @@ -413,5 +414,6 @@ export { Star, QBDSquare, GalleryNotFound, + Train, boltSlash, }; diff --git a/src/components/KeyboardAvoidingView/index.android.tsx b/src/components/KeyboardAvoidingView/index.android.tsx index 4d758511d7ad..ec2dc3bd18d7 100644 --- a/src/components/KeyboardAvoidingView/index.android.tsx +++ b/src/components/KeyboardAvoidingView/index.android.tsx @@ -1,133 +1,15 @@ -import React, {forwardRef, useCallback, useMemo, useState} from 'react'; -import type {LayoutRectangle, View, ViewProps} from 'react-native'; -import {useKeyboardContext, useKeyboardHandler} from 'react-native-keyboard-controller'; -import Reanimated, {interpolate, runOnUI, useAnimatedStyle, useDerivedValue, useSharedValue} from 'react-native-reanimated'; -import {useSafeAreaFrame} from 'react-native-safe-area-context'; -import type {KeyboardAvoidingViewProps} from './types'; - -const useKeyboardAnimation = () => { - const {reanimated} = useKeyboardContext(); - - // calculate it only once on mount, to avoid `SharedValue` reads during a render - const [initialHeight] = useState(() => -reanimated.height.get()); - const [initialProgress] = useState(() => reanimated.progress.get()); - - const heightWhenOpened = useSharedValue(initialHeight); - const height = useSharedValue(initialHeight); - const progress = useSharedValue(initialProgress); - const isClosed = useSharedValue(initialProgress === 0); - - useKeyboardHandler( - { - onStart: (e) => { - 'worklet'; - - progress.set(e.progress); - height.set(e.height); - - if (e.height > 0) { - isClosed.set(false); - heightWhenOpened.set(e.height); - } - }, - onEnd: (e) => { - 'worklet'; - - isClosed.set(e.height === 0); - height.set(e.height); - progress.set(e.progress); - }, - }, - [], - ); - - return {height, progress, heightWhenOpened, isClosed}; -}; - -const defaultLayout: LayoutRectangle = { - x: 0, - y: 0, - width: 0, - height: 0, -}; - -/** - * View that moves out of the way when the keyboard appears by automatically - * adjusting its height, position, or bottom padding. - * - * This `KeyboardAvoidingView` acts as a backward compatible layer for the previous Android behavior (prior to edge-to-edge mode). - * We can use `KeyboardAvoidingView` directly from the `react-native-keyboard-controller` package, but in this case animations are stuttering and it's better to handle as a separate task. +/* + * The KeyboardAvoidingView is only used on ios */ -const KeyboardAvoidingView = forwardRef>( - ({behavior, children, contentContainerStyle, enabled = true, keyboardVerticalOffset = 0, style, onLayout: onLayoutProps, ...props}, ref) => { - const initialFrame = useSharedValue(null); - const frame = useDerivedValue(() => initialFrame.get() ?? defaultLayout); - - const keyboard = useKeyboardAnimation(); - const {height: screenHeight} = useSafeAreaFrame(); - - const relativeKeyboardHeight = useCallback(() => { - 'worklet'; - - const keyboardY = screenHeight - keyboard.heightWhenOpened.get() - keyboardVerticalOffset; - - return Math.max(frame.get().y + frame.get().height - keyboardY, 0); - }, [screenHeight, keyboard.heightWhenOpened, keyboardVerticalOffset, frame]); - - const onLayoutWorklet = useCallback( - (layout: LayoutRectangle) => { - 'worklet'; - - if (keyboard.isClosed.get() || initialFrame.get() === null) { - initialFrame.set(layout); - } - }, - [initialFrame, keyboard.isClosed], - ); - const onLayout = useCallback>( - (e) => { - runOnUI(onLayoutWorklet)(e.nativeEvent.layout); - onLayoutProps?.(e); - }, - [onLayoutProps, onLayoutWorklet], - ); - - const animatedStyle = useAnimatedStyle(() => { - const bottom = interpolate(keyboard.progress.get(), [0, 1], [0, relativeKeyboardHeight()]); - const bottomHeight = enabled ? bottom : 0; - - switch (behavior) { - case 'height': - if (!keyboard.isClosed.get()) { - return { - height: frame.get().height - bottomHeight, - flex: 0, - }; - } - - return {}; - - case 'padding': - return {paddingBottom: bottomHeight}; +import React from 'react'; +import type {KeyboardAvoidingViewProps} from 'react-native-keyboard-controller'; +import {KeyboardAvoidingView as KeyboardAvoidingViewComponent} from 'react-native-keyboard-controller'; - default: - return {}; - } - }, [behavior, enabled, relativeKeyboardHeight]); - const combinedStyles = useMemo(() => [style, animatedStyle], [style, animatedStyle]); +function KeyboardAvoidingView(props: KeyboardAvoidingViewProps) { + // eslint-disable-next-line react/jsx-props-no-spreading + return ; +} - return ( - - {children} - - ); - }, -); +KeyboardAvoidingView.displayName = 'KeyboardAvoidingView'; export default KeyboardAvoidingView; diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index efdd9659c845..c423d3101d92 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -1,5 +1,5 @@ import {useFocusEffect} from '@react-navigation/native'; -import React, {useCallback, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useRef, useState} from 'react'; import type {GestureResponderEvent, ViewStyle} from 'react-native'; import {StyleSheet, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; @@ -11,7 +11,6 @@ import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {useSession} from '@components/OnyxProvider'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; -import {useProductTrainingContext} from '@components/ProductTrainingContext'; import SubscriptAvatar from '@components/SubscriptAvatar'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; @@ -23,6 +22,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; import DomUtils from '@libs/DomUtils'; +import {hasCompletedGuidedSetupFlowSelector} from '@libs/onboardingSelectors'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Parser from '@libs/Parser'; import Performance from '@libs/Performance'; @@ -32,6 +32,7 @@ import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportA import FreeTrial from '@pages/settings/Subscription/FreeTrial'; import variables from '@styles/variables'; import Timing from '@userActions/Timing'; +import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -47,21 +48,18 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${optionItem?.reportID || -1}`); - const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); - const isActiveWorkspaceChat = ReportUtils.isPolicyExpenseChat(report) && report?.isOwnPolicyExpenseChat && activePolicyID === report?.policyID; + const [isFirstTimeNewExpensifyUser] = useOnyx(ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER); + const [isOnboardingCompleted = true] = useOnyx(ONYXKEYS.NVP_ONBOARDING, { + selector: hasCompletedGuidedSetupFlowSelector, + }); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const session = useSession(); - const isOnboardingGuideAssigned = introSelected?.choice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM && !session?.email?.includes('+'); - const shouldShowGetStartedTooltip = isOnboardingGuideAssigned ? ReportUtils.isAdminRoom(report) : ReportUtils.isConciergeChatReport(report); - - const {tooltipToRender, shouldShowTooltip} = useMemo(() => { - const tooltip = shouldShowGetStartedTooltip ? CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.CONCEIRGE_LHN_GBR : CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.LHN_WORKSPACE_CHAT_TOOLTIP; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return {tooltipToRender: tooltip, shouldShowTooltip: shouldUseNarrowLayout ? isScreenFocused : true}; - }, [shouldShowGetStartedTooltip, isScreenFocused, shouldUseNarrowLayout]); + // Guides are assigned for the MANAGE_TEAM onboarding action, except for emails that have a '+'. + const isOnboardingGuideAssigned = introSelected?.choice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM && !session?.email?.includes('+'); + const shouldShowToooltipOnThisReport = isOnboardingGuideAssigned ? ReportUtils.isAdminRoom(report) : ReportUtils.isConciergeChatReport(report); + const [shouldHideGBRTooltip] = useOnyx(ONYXKEYS.NVP_SHOULD_HIDE_GBR_TOOLTIP, {initialValue: true}); - const {shouldShowProductTrainingTooltip, renderProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext(tooltipToRender, shouldShowTooltip); const {translate} = useLocalize(); const [isContextMenuActive, setIsContextMenuActive] = useState(false); @@ -74,6 +72,30 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti }, []), ); + const renderGBRTooltip = useCallback( + () => ( + + + {translate('sidebarScreen.tooltip')} + + ), + [ + styles.alignItemsCenter, + styles.flexRow, + styles.justifyContentCenter, + styles.flexWrap, + styles.textAlignCenter, + styles.gap1, + styles.quickActionTooltipSubtitle, + theme.tooltipHighlightText, + translate, + ], + ); + const isInFocusMode = viewMode === CONST.OPTION_MODE.COMPACT; const sidebarInnerRowStyle = StyleSheet.flatten( isInFocusMode @@ -158,18 +180,17 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti needsOffscreenAlphaCompositing > diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index 6abf72e9e520..1896bc4f5f07 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -66,7 +66,9 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportAct {reportName} )} - {!!workspaceName && {` ${translate('threads.in')} ${workspaceName}`}} + {!!workspaceName && workspaceName !== reportName && ( + {` ${translate('threads.in')} ${workspaceName}`} + )} ); diff --git a/src/components/ProductTrainingContext/TOOLTIPS.ts b/src/components/ProductTrainingContext/TOOLTIPS.ts deleted file mode 100644 index dc2a761a4903..000000000000 --- a/src/components/ProductTrainingContext/TOOLTIPS.ts +++ /dev/null @@ -1,118 +0,0 @@ -import type {ValueOf} from 'type-fest'; -import {dismissProductTraining} from '@libs/actions/Welcome'; -import CONST from '@src/CONST'; -import type {TranslationPaths} from '@src/languages/types'; - -const { - CONCEIRGE_LHN_GBR, - RENAME_SAVED_SEARCH, - WORKSAPCE_CHAT_CREATE, - QUICK_ACTION_BUTTON, - SEARCH_FILTER_BUTTON_TOOLTIP, - BOTTOM_NAV_INBOX_TOOLTIP, - LHN_WORKSPACE_CHAT_TOOLTIP, - GLOBAL_CREATE_TOOLTIP, -} = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES; - -type ProductTrainingTooltipName = ValueOf; - -type ShouldShowConditionProps = { - shouldUseNarrowLayout?: boolean; -}; - -type TooltipData = { - content: Array<{text: TranslationPaths; isBold: boolean}>; - onHideTooltip: () => void; - name: ProductTrainingTooltipName; - priority: number; - shouldShow: (props: ShouldShowConditionProps) => boolean; -}; - -const TOOLTIPS: Record = { - [CONCEIRGE_LHN_GBR]: { - content: [ - {text: 'productTrainingTooltip.conciergeLHNGBR.part1', isBold: false}, - {text: 'productTrainingTooltip.conciergeLHNGBR.part2', isBold: true}, - ], - onHideTooltip: () => dismissProductTraining(CONCEIRGE_LHN_GBR), - name: CONCEIRGE_LHN_GBR, - priority: 1300, - shouldShow: ({shouldUseNarrowLayout}) => !!shouldUseNarrowLayout, - }, - [RENAME_SAVED_SEARCH]: { - content: [ - {text: 'productTrainingTooltip.saveSearchTooltip.part1', isBold: true}, - {text: 'productTrainingTooltip.saveSearchTooltip.part2', isBold: false}, - ], - onHideTooltip: () => dismissProductTraining(RENAME_SAVED_SEARCH), - name: RENAME_SAVED_SEARCH, - priority: 1250, - shouldShow: ({shouldUseNarrowLayout}) => !shouldUseNarrowLayout, - }, - [GLOBAL_CREATE_TOOLTIP]: { - content: [ - {text: 'productTrainingTooltip.globalCreateTooltip.part1', isBold: true}, - {text: 'productTrainingTooltip.globalCreateTooltip.part2', isBold: false}, - {text: 'productTrainingTooltip.globalCreateTooltip.part3', isBold: false}, - ], - onHideTooltip: () => dismissProductTraining(GLOBAL_CREATE_TOOLTIP), - name: GLOBAL_CREATE_TOOLTIP, - priority: 1200, - shouldShow: () => true, - }, - [QUICK_ACTION_BUTTON]: { - content: [ - {text: 'productTrainingTooltip.quickActionButton.part1', isBold: true}, - {text: 'productTrainingTooltip.quickActionButton.part2', isBold: false}, - ], - onHideTooltip: () => dismissProductTraining(QUICK_ACTION_BUTTON), - name: QUICK_ACTION_BUTTON, - priority: 1150, - shouldShow: () => true, - }, - [WORKSAPCE_CHAT_CREATE]: { - content: [ - {text: 'productTrainingTooltip.workspaceChatCreate.part1', isBold: true}, - {text: 'productTrainingTooltip.workspaceChatCreate.part2', isBold: false}, - ], - onHideTooltip: () => dismissProductTraining(WORKSAPCE_CHAT_CREATE), - name: WORKSAPCE_CHAT_CREATE, - priority: 1100, - shouldShow: () => true, - }, - [SEARCH_FILTER_BUTTON_TOOLTIP]: { - content: [ - {text: 'productTrainingTooltip.searchFilterButtonTooltip.part1', isBold: true}, - {text: 'productTrainingTooltip.searchFilterButtonTooltip.part2', isBold: false}, - ], - onHideTooltip: () => dismissProductTraining(SEARCH_FILTER_BUTTON_TOOLTIP), - name: SEARCH_FILTER_BUTTON_TOOLTIP, - priority: 1000, - shouldShow: () => true, - }, - [BOTTOM_NAV_INBOX_TOOLTIP]: { - content: [ - {text: 'productTrainingTooltip.bottomNavInboxTooltip.part1', isBold: true}, - {text: 'productTrainingTooltip.bottomNavInboxTooltip.part2', isBold: false}, - {text: 'productTrainingTooltip.bottomNavInboxTooltip.part3', isBold: false}, - ], - onHideTooltip: () => dismissProductTraining(BOTTOM_NAV_INBOX_TOOLTIP), - name: BOTTOM_NAV_INBOX_TOOLTIP, - priority: 900, - shouldShow: () => true, - }, - [LHN_WORKSPACE_CHAT_TOOLTIP]: { - content: [ - {text: 'productTrainingTooltip.workspaceChatTooltip.part1', isBold: true}, - {text: 'productTrainingTooltip.workspaceChatTooltip.part2', isBold: false}, - {text: 'productTrainingTooltip.workspaceChatTooltip.part3', isBold: false}, - ], - onHideTooltip: () => dismissProductTraining(LHN_WORKSPACE_CHAT_TOOLTIP), - name: LHN_WORKSPACE_CHAT_TOOLTIP, - priority: 800, - shouldShow: () => true, - }, -}; - -export default TOOLTIPS; -export type {ProductTrainingTooltipName}; diff --git a/src/components/ProductTrainingContext/index.tsx b/src/components/ProductTrainingContext/index.tsx deleted file mode 100644 index 7cfcf4d3bfa7..000000000000 --- a/src/components/ProductTrainingContext/index.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import React, {createContext, useCallback, useContext, useEffect, useMemo, useState} from 'react'; -import {View} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; -import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; -import Text from '@components/Text'; -import useLocalize from '@hooks/useLocalize'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {hasCompletedGuidedSetupFlowSelector} from '@libs/onboardingSelectors'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type ChildrenProps from '@src/types/utils/ChildrenProps'; -import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; -import type {ProductTrainingTooltipName} from './TOOLTIPS'; -import TOOLTIPS from './TOOLTIPS'; - -type ProductTrainingContextType = { - shouldRenderTooltip: (tooltipName: ProductTrainingTooltipName) => boolean; - registerTooltip: (tooltipName: ProductTrainingTooltipName) => void; - unregisterTooltip: (tooltipName: ProductTrainingTooltipName) => void; -}; - -const ProductTrainingContext = createContext({ - shouldRenderTooltip: () => false, - registerTooltip: () => {}, - unregisterTooltip: () => {}, -}); - -function ProductTrainingContextProvider({children}: ChildrenProps) { - const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRYNEWDOT); - const hasBeenAddedToNudgeMigration = !!tryNewDot?.nudgeMigration?.timestamp; - const [isOnboardingCompleted = true, isOnboardingCompletedMetadata] = useOnyx(ONYXKEYS.NVP_ONBOARDING, { - selector: hasCompletedGuidedSetupFlowSelector, - }); - const [dismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING); - const {shouldUseNarrowLayout} = useResponsiveLayout(); - - const [activeTooltips, setActiveTooltips] = useState>(new Set()); - - const unregisterTooltip = useCallback( - (tooltipName: ProductTrainingTooltipName) => { - setActiveTooltips((prev) => { - const next = new Set(prev); - next.delete(tooltipName); - return next; - }); - }, - [setActiveTooltips], - ); - - const determineVisibleTooltip = useCallback(() => { - if (activeTooltips.size === 0) { - return null; - } - - const sortedTooltips = Array.from(activeTooltips) - .map((name) => ({ - name, - priority: TOOLTIPS[name]?.priority ?? 0, - })) - .sort((a, b) => b.priority - a.priority); - - const highestPriorityTooltip = sortedTooltips.at(0); - - if (!highestPriorityTooltip) { - return null; - } - - return highestPriorityTooltip.name; - }, [activeTooltips]); - - const shouldTooltipBeVisible = useCallback( - (tooltipName: ProductTrainingTooltipName) => { - if (isLoadingOnyxValue(isOnboardingCompletedMetadata)) { - return false; - } - - const isDismissed = !!dismissedProductTraining?.[tooltipName]; - - if (isDismissed) { - return false; - } - const tooltipConfig = TOOLTIPS[tooltipName]; - - // if hasBeenAddedToNudgeMigration is true, and welcome modal is not dismissed, don't show tooltip - if (hasBeenAddedToNudgeMigration && !dismissedProductTraining?.[CONST.MIGRATED_USER_WELCOME_MODAL]) { - return false; - } - if (isOnboardingCompleted === false) { - return false; - } - - return tooltipConfig.shouldShow({ - shouldUseNarrowLayout, - }); - }, - [dismissedProductTraining, hasBeenAddedToNudgeMigration, isOnboardingCompleted, isOnboardingCompletedMetadata, shouldUseNarrowLayout], - ); - - const registerTooltip = useCallback( - (tooltipName: ProductTrainingTooltipName) => { - const shouldRegister = shouldTooltipBeVisible(tooltipName); - if (!shouldRegister) { - return; - } - setActiveTooltips((prev) => new Set([...prev, tooltipName])); - }, - [shouldTooltipBeVisible], - ); - - const shouldRenderTooltip = useCallback( - (tooltipName: ProductTrainingTooltipName) => { - // First check base conditions - const shouldShow = shouldTooltipBeVisible(tooltipName); - if (!shouldShow) { - return false; - } - const visibleTooltip = determineVisibleTooltip(); - - // If this is the highest priority visible tooltip, show it - if (tooltipName === visibleTooltip) { - return true; - } - - return false; - }, - [shouldTooltipBeVisible, determineVisibleTooltip], - ); - - const contextValue = useMemo( - () => ({ - shouldRenderTooltip, - registerTooltip, - unregisterTooltip, - }), - [shouldRenderTooltip, registerTooltip, unregisterTooltip], - ); - - return {children}; -} - -const useProductTrainingContext = (tooltipName: ProductTrainingTooltipName, shouldShow = true) => { - const context = useContext(ProductTrainingContext); - const styles = useThemeStyles(); - const theme = useTheme(); - const {translate} = useLocalize(); - - if (!context) { - throw new Error('useProductTourContext must be used within a ProductTourProvider'); - } - - const {shouldRenderTooltip, registerTooltip, unregisterTooltip} = context; - - useEffect(() => { - if (shouldShow) { - registerTooltip(tooltipName); - return () => { - unregisterTooltip(tooltipName); - }; - } - return () => {}; - }, [tooltipName, registerTooltip, unregisterTooltip, shouldShow]); - - const renderProductTrainingTooltip = useCallback(() => { - const tooltip = TOOLTIPS[tooltipName]; - return ( - - - - {tooltip.content.map(({text, isBold}) => { - const translatedText = translate(text); - return ( - - {translatedText} - - ); - })} - - - ); - }, [ - styles.alignItemsCenter, - styles.flexRow, - styles.flexWrap, - styles.gap3, - styles.justifyContentCenter, - styles.mw100, - styles.p2, - styles.productTrainingTooltipText, - styles.textAlignCenter, - styles.textBold, - styles.textWrap, - theme.tooltipHighlightText, - tooltipName, - translate, - ]); - - const shouldShowProductTrainingTooltip = useMemo(() => { - return shouldRenderTooltip(tooltipName); - }, [shouldRenderTooltip, tooltipName]); - - const hideProductTrainingTooltip = useCallback(() => { - const tooltip = TOOLTIPS[tooltipName]; - tooltip.onHideTooltip(); - unregisterTooltip(tooltipName); - }, [tooltipName, unregisterTooltip]); - - return { - renderProductTrainingTooltip, - hideProductTrainingTooltip, - shouldShowProductTrainingTooltip, - }; -}; - -export {ProductTrainingContextProvider, useProductTrainingContext}; diff --git a/src/components/ReceiptEmptyState.tsx b/src/components/ReceiptEmptyState.tsx index 046026190a5b..08d83b6962af 100644 --- a/src/components/ReceiptEmptyState.tsx +++ b/src/components/ReceiptEmptyState.tsx @@ -21,13 +21,15 @@ type ReceiptEmptyStateProps = { }; // Returns an SVG icon indicating that the user should attach a receipt -function ReceiptEmptyState({hasError = false, onPress = () => {}, disabled = false, isThumbnail = false}: ReceiptEmptyStateProps) { +function ReceiptEmptyState({hasError = false, onPress, disabled = false, isThumbnail = false}: ReceiptEmptyStateProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const theme = useTheme(); + const Wrapper = onPress ? PressableWithoutFeedback : View; + return ( - {}, disabled = fal /> )} - + ); } diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 72e0a43ed5e5..ba0cda25d59e 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -353,7 +353,6 @@ function MoneyRequestPreviewContent({ images={receiptImages} isHovered={isHovered || isScanning} size={1} - onPress={shouldDisableOnPress ? undefined : onPreviewPressed} /> {isEmptyObject(transaction) && !ReportActionsUtils.isMessageDeleted(action) && action.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ? ( diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index e3d4a8d31cf6..18750bfc7a29 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -33,7 +33,6 @@ import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import Navigation from '@navigation/Navigation'; import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground'; import * as IOU from '@userActions/IOU'; -import * as Link from '@userActions/Link'; import * as Transaction from '@userActions/Transaction'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -82,7 +81,6 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals const session = useSession(); const {isOffline} = useNetwork(); const {translate, toLocaleDigit} = useLocalize(); - const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const parentReportID = report?.parentReportID ?? '-1'; const policyID = report?.policyID ?? '-1'; const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`); @@ -91,7 +89,8 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals }); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); - const targetPolicyID = updatedTransaction?.reportID ? ReportUtils.getReport(updatedTransaction?.reportID)?.policyID : policyID; + const [transactionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${updatedTransaction?.reportID}`); + const targetPolicyID = updatedTransaction?.reportID ? transactionReport?.policyID : policyID; const [policyTagList] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${targetPolicyID}`); const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, { canEvict: false, @@ -187,7 +186,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals const shouldShowAttendees = useMemo(() => TransactionUtils.shouldShowAttendees(iouType, policy), [iouType, policy]); const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest); - const tripID = ReportUtils.getTripIDFromTransactionParentReport(parentReport); + const tripID = ReportUtils.getTripIDFromTransactionParentReportID(parentReport?.parentReportID); const shouldShowViewTripDetails = TransactionUtils.hasReservationList(transaction) && !!tripID; const {getViolationsForField} = useViolations(transactionViolations ?? [], isReceiptBeingScanned || !ReportUtils.isPaidGroupPolicy(report)); @@ -698,10 +697,12 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals { - Link.openTravelDotLink(activePolicyID, CONST.TRIP_ID_PATH(tripID)); + const reservations = transaction?.receipt?.reservationList?.length ?? 0; + if (reservations > 1) { + Navigation.navigate(ROUTES.TRAVEL_TRIP_SUMMARY.getRoute(report?.reportID ?? '-1', transaction?.transactionID ?? '-1', Navigation.getReportRHPActiveRoute())); + } + Navigation.navigate(ROUTES.TRAVEL_TRIP_DETAILS.getRoute(report?.reportID ?? '-1', transaction?.transactionID ?? '-1', 0, Navigation.getReportRHPActiveRoute())); }} /> )} diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index c7af8ce1f614..65cdb4a7d00b 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -503,7 +503,6 @@ function ReportPreview({ images={lastThreeReceipts} total={allTransactions.length} size={CONST.RECEIPT.MAX_REPORT_PREVIEW_RECEIPTS} - onPress={openReportFromPreview} /> diff --git a/src/components/ReportActionItem/TripDetailsView.tsx b/src/components/ReportActionItem/TripDetailsView.tsx index 32cbc5dd853e..a7fdef547bf9 100644 --- a/src/components/ReportActionItem/TripDetailsView.tsx +++ b/src/components/ReportActionItem/TripDetailsView.tsx @@ -11,16 +11,18 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; +import Navigation from '@libs/Navigation/Navigation'; import variables from '@styles/variables'; import * as Expensicons from '@src/components/Icon/Expensicons'; import CONST from '@src/CONST'; import * as ReportUtils from '@src/libs/ReportUtils'; import * as TripReservationUtils from '@src/libs/TripReservationUtils'; +import ROUTES from '@src/ROUTES'; import type {Reservation, ReservationTimeDetails} from '@src/types/onyx/Transaction'; type TripDetailsViewProps = { /** The active tripRoomReportID, used for Onyx subscription */ - tripRoomReportID?: string; + tripRoomReportID: string; /** Whether we should display the horizontal rule below the component */ shouldShowHorizontalRule: boolean; @@ -28,9 +30,12 @@ type TripDetailsViewProps = { type ReservationViewProps = { reservation: Reservation; + transactionID: string; + tripRoomReportID: string; + reservationIndex: number; }; -function ReservationView({reservation}: ReservationViewProps) { +function ReservationView({reservation, transactionID, tripRoomReportID, reservationIndex}: ReservationViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -75,11 +80,14 @@ function ReservationView({reservation}: ReservationViewProps) { const vendor = reservation.vendor ? `${reservation.vendor} • ` : ''; return `${vendor}${reservation.start.location}`; } + if (reservation.type === CONST.RESERVATION_TYPE.TRAIN) { + return reservation.route?.name; + } return reservation.start.address ?? reservation.start.location; }, [reservation]); const titleComponent = () => { - if (reservation.type === CONST.RESERVATION_TYPE.FLIGHT) { + if (reservation.type === CONST.RESERVATION_TYPE.FLIGHT || reservation.type === CONST.RESERVATION_TYPE.TRAIN) { return ( @@ -129,6 +137,7 @@ function ReservationView({reservation}: ReservationViewProps) { iconWidth={20} iconStyles={[StyleUtils.getTripReservationIconContainer(false), styles.mr3]} secondaryIconFill={theme.icon} + onPress={() => Navigation.navigate(ROUTES.TRAVEL_TRIP_DETAILS.getRoute(tripRoomReportID, transactionID, reservationIndex, Navigation.getReportRHPActiveRoute()))} /> ); } @@ -138,7 +147,7 @@ function TripDetailsView({tripRoomReportID, shouldShowHorizontalRule}: TripDetai const {translate} = useLocalize(); const tripTransactions = ReportUtils.getTripTransactions(tripRoomReportID); - const reservations: Reservation[] = TripReservationUtils.getReservationsFromTripTransactions(tripTransactions); + const reservationsData: TripReservationUtils.ReservationData[] = TripReservationUtils.getReservationsFromTripTransactions(tripTransactions); return ( @@ -153,11 +162,18 @@ function TripDetailsView({tripRoomReportID, shouldShowHorizontalRule}: TripDetai <> - {reservations.map((reservation) => ( - - - - ))} + {reservationsData.map(({reservation, transactionID, reservationIndex}) => { + return ( + + + + ); + })} + {title} + + ); + + if (reservation.type === CONST.RESERVATION_TYPE.FLIGHT || reservation.type === CONST.RESERVATION_TYPE.TRAIN) { + const startName = reservation.type === CONST.RESERVATION_TYPE.FLIGHT ? reservation.start.shortName : reservation.start.longName; + const endName = reservation.type === CONST.RESERVATION_TYPE.FLIGHT ? reservation.end.shortName : reservation.end.longName; + + titleComponent = ( - {reservation.start.shortName} + {startName} - {reservation.end.shortName} + {endName} - ) : ( - - {title} - ); + } return ( ; +const renderItem = ({item}: {item: TripReservationUtils.ReservationData}) => ; function TripRoomPreview({action, chatReportID, containerStyles, contextMenuAnchor, isHovered = false, checkIfContextMenuActive = () => {}}: TripRoomPreviewProps) { const styles = useThemeStyles(); @@ -112,31 +117,22 @@ function TripRoomPreview({action, chatReportID, containerStyles, contextMenuAnch const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`); const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReport?.iouReportID}`); - const tripTransactions = ReportUtils.getTripTransactions(chatReport?.iouReportID, 'reportID'); - const reservations: Reservation[] = TripReservationUtils.getReservationsFromTripTransactions(tripTransactions); + const tripTransactions = ReportUtils.getTripTransactions(chatReport?.reportID); + const reservationsData: TripReservationUtils.ReservationData[] = TripReservationUtils.getReservationsFromTripTransactions(tripTransactions); const dateInfo = chatReport?.tripData ? DateUtils.getFormattedDateRange(new Date(chatReport.tripData.startDate), new Date(chatReport.tripData.endDate)) : ''; const {totalDisplaySpend} = ReportUtils.getMoneyRequestSpendBreakdown(chatReport); + const currency = iouReport?.currency ?? chatReport?.currency; const displayAmount = useMemo(() => { if (totalDisplaySpend) { - return CurrencyUtils.convertToDisplayString(totalDisplaySpend, iouReport?.currency); + return CurrencyUtils.convertToDisplayString(totalDisplaySpend, currency); } - // If iouReport is not available, get amount from the action message (Ex: "Domain20821's Workspace owes $33.00" or "paid ₫60" or "paid -₫60 elsewhere") - let displayAmountValue = ''; - const actionMessage = getReportActionText(action) ?? ''; - const splits = actionMessage.split(' '); - - splits.forEach((split) => { - if (!/\d/.test(split)) { - return; - } - - displayAmountValue = split; - }); - - return displayAmountValue; - }, [action, iouReport?.currency, totalDisplaySpend]); + return CurrencyUtils.convertToDisplayString( + tripTransactions.reduce((acc, transaction) => acc + Math.abs(transaction.amount), 0), + currency, + ); + }, [currency, totalDisplaySpend, tripTransactions]); return ( diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index 09315bfb8a8e..bb20b4abae11 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -7,7 +7,6 @@ import {PickerAvoidingView} from 'react-native-picker-select'; import type {EdgeInsets} from 'react-native-safe-area-context'; import useEnvironment from '@hooks/useEnvironment'; import useInitialDimensions from '@hooks/useInitialWindowDimensions'; -import useKeyboardState from '@hooks/useKeyboardState'; import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; @@ -158,18 +157,11 @@ function ScreenWrapper( const {isSmallScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); const {initialHeight} = useInitialDimensions(); const styles = useThemeStyles(); - const keyboardState = useKeyboardState(); const {isDevelopment} = useEnvironment(); const {isOffline} = useNetwork(); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const maxHeight = shouldEnableMaxHeight ? windowHeight : undefined; const minHeight = shouldEnableMinHeight && !Browser.isSafari() ? initialHeight : undefined; - const isKeyboardShown = keyboardState?.isKeyboardShown ?? false; - - const isKeyboardShownRef = useRef(false); - - // eslint-disable-next-line react-compiler/react-compiler - isKeyboardShownRef.current = keyboardState?.isKeyboardShown ?? false; const route = useRoute(); const shouldReturnToOldDot = useMemo(() => { @@ -191,7 +183,7 @@ function ScreenWrapper( PanResponder.create({ onMoveShouldSetPanResponderCapture: (_e, gestureState) => { const isHorizontalSwipe = Math.abs(gestureState.dx) > Math.abs(gestureState.dy); - const shouldDismissKeyboard = shouldDismissKeyboardBeforeClose && isKeyboardShown && Browser.isMobile(); + const shouldDismissKeyboard = shouldDismissKeyboardBeforeClose && Keyboard.isVisible() && Browser.isMobile(); return isHorizontalSwipe && shouldDismissKeyboard; }, @@ -221,7 +213,7 @@ function ScreenWrapper( // described here https://reactnavigation.org/docs/preventing-going-back/#limitations const beforeRemoveSubscription = shouldDismissKeyboardBeforeClose ? navigation.addListener('beforeRemove', () => { - if (!isKeyboardShownRef.current) { + if (!Keyboard.isVisible()) { return; } Keyboard.dismiss(); diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 21a5832052c0..a78845f126d2 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -1,4 +1,3 @@ -import {useIsFocused} from '@react-navigation/native'; import React, {useMemo, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; @@ -9,8 +8,6 @@ import ConfirmModal from '@components/ConfirmModal'; import DecisionModal from '@components/DecisionModal'; import * as Expensicons from '@components/Icon/Expensicons'; import {usePersonalDetails} from '@components/OnyxProvider'; -import {useProductTrainingContext} from '@components/ProductTrainingContext'; -import EducationalTooltip from '@components/Tooltip/EducationalTooltip'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -58,11 +55,6 @@ function SearchPageHeader({queryJSON}: SearchPageHeaderProps) { const [isDeleteExpensesConfirmModalVisible, setIsDeleteExpensesConfirmModalVisible] = useState(false); const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); - const isFocused = useIsFocused(); - const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext( - CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SEARCH_FILTER_BUTTON_TOOLTIP, - isFocused, - ); const {status, hash} = queryJSON; @@ -356,25 +348,12 @@ function SearchPageHeader({queryJSON}: SearchPageHeaderProps) { shouldUseStyleUtilityForAnchorPosition /> ) : ( - -