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>
+#if>
+<#list reports as report>
+ <#list report.transactionList as expense>
+ <#if expense.modifiedMerchant?has_content>
+ <#assign merchant = expense.modifiedMerchant>
+ <#else>
+ <#assign merchant = expense.merchant>
+ #if>
+ <#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>
+ <#if expense.modifiedCreated?has_content>
+ <#assign created = expense.modifiedCreated>
+ <#else>
+ <#assign created = expense.created>
+ #if>
+ ${merchant},<#t>
+ ${amount},<#t>
+ ${created}<#lt>
+ #list>
+#list>
+```
+
+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