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/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 142d919a7a18..a859703ae719 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ + diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 381b10533eef..094609e14d6c 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -50,6 +50,8 @@ ITSAppUsesNonExemptEncryption + NSContactsUsageDescription + Import contacts from your phone so your favorite people are always a tap away. LSApplicationQueriesSchemes venmo 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 18eba3d79c27..65c7b896b8e3 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 @@ -2729,6 +2774,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`) @@ -2744,6 +2790,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`) @@ -2894,6 +2941,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: @@ -2925,6 +2974,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: @@ -3139,6 +3190,7 @@ SPEC CHECKSUMS: AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa AppLogs: 3bc4e9b141dbf265b9464409caaa40416a9ee0e0 boost: 26992d1adf73c1c7676360643e687aee6dda994b + ContactsModule: 21671b28654413dc28795d1afc3b12eaffa28ed1 DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5 EXAV: afa491e598334bbbb92a92a2f4dd33d7149ad37f EXImageLoader: ab589d67d6c5f2c33572afea9917304418566334 @@ -3178,6 +3230,7 @@ SPEC CHECKSUMS: MapboxMaps: e76b14f52c54c40b76ddecd04f40448e6f35a864 MapboxMobileEvents: de50b3a4de180dd129c326e09cd12c8adaaa46d6 nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 + NitroModules: ebe2ba2d01dc03c1f82441561fe6062b8c3c4366 Onfido: f3af62ea1c9a419589c133e3e511e5d2c4f3f8af onfido-react-native-sdk: 4ccfdeb10f9ccb4a5799d2555cdbc2a068a42c0d Plaid: c32f22ffce5ec67c9e6147eaf6c4d7d5f8086d89 @@ -3274,7 +3327,7 @@ SPEC CHECKSUMS: RNLiveMarkdown: 8338447b39fcd86596c74b9e0e9509e365a2dd3b RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: 460d6ff97ae49c7d5708c3212c6521697c36a0c4 - RNPermissions: 0b1429b55af59d1d08b75a8be2459f65a8ac3f28 + RNPermissions: 9e5c26aaa982fe00743281f6f47fbdc050ebc58f RNReactNativeHapticFeedback: 73756a3477a5a622fa16862a3ab0d0fc5e5edff5 RNReanimated: 03ba2447d5a7789e2843df2ee05108d93b6441d6 RNScreens: de6e57426ba0e6cbc3fb5b4f496e7f08cb2773c2 @@ -3290,6 +3343,6 @@ SPEC CHECKSUMS: VisionCamera: c95a8ad535f527562be1fb05fb2fd324578e769c Yoga: a1d7895431387402a674fd0d1c04ec85e87909b8 -PODFILE CHECKSUM: 615266329434ea4a994dccf622008a2197313c88 +PODFILE CHECKSUM: e744fa802b4bee097ff8d1977dd8f79d16b21547 COCOAPODS: 1.15.2 diff --git a/modules/ContactsNitroModule/.gitignore b/modules/ContactsNitroModule/.gitignore new file mode 100644 index 000000000000..d3b53dfce541 --- /dev/null +++ b/modules/ContactsNitroModule/.gitignore @@ -0,0 +1,78 @@ +# OSX +# +.DS_Store + +# XDE +.expo/ + +# VSCode +.vscode/ +jsconfig.json + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace + +# Android/IJ +# +.classpath +.cxx +.gradle +.idea +.project +.settings +local.properties +android.iml + +# Cocoapods +# +example/ios/Pods + +# Ruby +example/vendor/ + +# node.js +# +node_modules/ +npm-debug.log +yarn-debug.log +yarn-error.log + +# BUCK +buck-out/ +\.buckd/ +android/app/libs +android/keystores/debug.keystore + +# Yarn +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Expo +.expo/ + +# Turborepo +.turbo/ + +# generated by bob +lib/ diff --git a/modules/ContactsNitroModule/.watchmanconfig b/modules/ContactsNitroModule/.watchmanconfig new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/modules/ContactsNitroModule/.watchmanconfig @@ -0,0 +1 @@ +{} diff --git a/modules/ContactsNitroModule/ContactsModule.podspec b/modules/ContactsNitroModule/ContactsModule.podspec new file mode 100644 index 000000000000..5f0b012c7b52 --- /dev/null +++ b/modules/ContactsNitroModule/ContactsModule.podspec @@ -0,0 +1,29 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +Pod::Spec.new do |s| + s.name = "ContactsModule" + s.version = package["version"] + s.summary = package["description"] + s.homepage = package["homepage"] + s.license = package["license"] + s.authors = package["author"] + + s.platforms = { :ios => min_ios_version_supported } + s.source = { :git => "https://github.com/mrousavy/nitro.git", :tag => "#{s.version}" } + + s.source_files = [ + # Implementation (Swift) + "ios/**/*.{swift}", + # Autolinking/Registration (Objective-C++) + "ios/**/*.{m,mm}", + # Implementation (C++ objects) + "cpp/**/*.{hpp,cpp}", + ] + + load 'nitrogen/generated/ios/ContactsModule+autolinking.rb' + add_nitrogen_files(s) + + install_modules_dependencies(s) +end diff --git a/modules/ContactsNitroModule/android/CMakeLists.txt b/modules/ContactsNitroModule/android/CMakeLists.txt new file mode 100644 index 000000000000..beb0c308df07 --- /dev/null +++ b/modules/ContactsNitroModule/android/CMakeLists.txt @@ -0,0 +1,29 @@ +project(ContactsModule) +cmake_minimum_required(VERSION 3.9.0) + +set (PACKAGE_NAME ContactsModule) +set (CMAKE_VERBOSE_MAKEFILE ON) +set (CMAKE_CXX_STANDARD 20) + +# Define C++ library and add all sources +add_library(${PACKAGE_NAME} SHARED + src/main/cpp/cpp-adapter.cpp +) + +# Add Nitrogen specs :) +include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/ContactsModule+autolinking.cmake) + +# Set up local includes +include_directories( + "src/main/cpp" + "../cpp" +) + +find_library(LOG_LIB log) + +# Link all libraries together +target_link_libraries( + ${PACKAGE_NAME} + ${LOG_LIB} + android # <-- Android core +) diff --git a/modules/ContactsNitroModule/android/build.gradle b/modules/ContactsNitroModule/android/build.gradle new file mode 100644 index 000000000000..0b414c88dea3 --- /dev/null +++ b/modules/ContactsNitroModule/android/build.gradle @@ -0,0 +1,130 @@ +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "com.android.tools.build:gradle:7.2.1" + } +} + +def reactNativeArchitectures() { + def value = rootProject.getProperties().get("reactNativeArchitectures") + return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] +} + +def isNewArchitectureEnabled() { + return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true" +} + +apply plugin: "com.android.library" +apply plugin: 'org.jetbrains.kotlin.android' +apply from: '../nitrogen/generated/android/ContactsModule+autolinking.gradle' + +if (isNewArchitectureEnabled()) { + apply plugin: "com.facebook.react" +} + +def getExtOrDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["ContactsModule_" + name] +} + +def getExtOrIntegerDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ContactsModule_" + name]).toInteger() +} + +android { + namespace "com.margelo.nitro.contacts" + + ndkVersion getExtOrDefault("ndkVersion") + compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") + + defaultConfig { + minSdkVersion getExtOrIntegerDefault("minSdkVersion") + targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") + buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() + + externalNativeBuild { + cmake { + cppFlags "-O2 -frtti -fexceptions -Wall -fstack-protector-all" + arguments "-DANDROID_STL=c++_shared" + abiFilters (*reactNativeArchitectures()) + } + } + } + + externalNativeBuild { + cmake { + path "CMakeLists.txt" + } + } + + packagingOptions { + excludes = [ + "META-INF", + "META-INF/**", + "**/libc++_shared.so", + "**/libfbjni.so", + "**/libjsi.so", + "**/libfolly_json.so", + "**/libfolly_runtime.so", + "**/libglog.so", + "**/libhermes.so", + "**/libhermes-executor-debug.so", + "**/libhermes_executor.so", + "**/libreactnativejni.so", + "**/libturbomodulejsijni.so", + "**/libreact_nativemodule_core.so", + "**/libjscexecutor.so" + ] + } + + buildFeatures { + buildConfig true + prefab true + } + + buildTypes { + release { + minifyEnabled false + } + } + + lintOptions { + disable "GradleCompatible" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + if (isNewArchitectureEnabled()) { + java.srcDirs += [ + // React Codegen files + "${project.buildDir}/generated/source/codegen/java" + ] + } + } + } +} + +repositories { + mavenCentral() + google() +} + + +dependencies { + // For < 0.71, this will be from the local maven repo + // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin + //noinspection GradleDynamicVersion + implementation "com.facebook.react:react-native:+" + + // Add a dependency on NitroModules + implementation project(":react-native-nitro-modules") +} + diff --git a/modules/ContactsNitroModule/android/gradle.properties b/modules/ContactsNitroModule/android/gradle.properties new file mode 100644 index 000000000000..59d3858d1bb9 --- /dev/null +++ b/modules/ContactsNitroModule/android/gradle.properties @@ -0,0 +1,5 @@ +ContactsModule_kotlinVersion=1.9.24 +ContactsModule_minSdkVersion=23 +ContactsModule_targetSdkVersion=34 +ContactsModule_compileSdkVersion=34 +ContactsModule_ndkVersion=26.1.10909125 diff --git a/modules/ContactsNitroModule/android/src/main/AndroidManifest.xml b/modules/ContactsNitroModule/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..a2f47b6057db --- /dev/null +++ b/modules/ContactsNitroModule/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/modules/ContactsNitroModule/android/src/main/cpp/cpp-adapter.cpp b/modules/ContactsNitroModule/android/src/main/cpp/cpp-adapter.cpp new file mode 100644 index 000000000000..7a88410f3e4d --- /dev/null +++ b/modules/ContactsNitroModule/android/src/main/cpp/cpp-adapter.cpp @@ -0,0 +1,6 @@ +#include +#include "ContactsModuleOnLoad.hpp" + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { + return margelo::nitro::contacts::initialize(vm); +} diff --git a/modules/ContactsNitroModule/android/src/main/java/com/margelo/nitro/contacts/ContactsModulePackage.java b/modules/ContactsNitroModule/android/src/main/java/com/margelo/nitro/contacts/ContactsModulePackage.java new file mode 100644 index 000000000000..e8c26844ce86 --- /dev/null +++ b/modules/ContactsNitroModule/android/src/main/java/com/margelo/nitro/contacts/ContactsModulePackage.java @@ -0,0 +1,34 @@ +package com.margelo.nitro.contacts; + +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.module.model.ReactModuleInfoProvider; +import com.facebook.react.TurboReactPackage; +import com.margelo.nitro.core.HybridObject; +import com.margelo.nitro.core.HybridObjectRegistry; + +import java.util.HashMap; +import java.util.function.Supplier; + +public class ContactsModulePackage extends TurboReactPackage { + @Nullable + @Override + public NativeModule getModule(String name, ReactApplicationContext reactContext) { + return null; + } + + @Override + public ReactModuleInfoProvider getReactModuleInfoProvider() { + return () -> { + return new HashMap<>(); + }; + } + + static { + System.loadLibrary("ContactsModule"); + } +} diff --git a/modules/ContactsNitroModule/android/src/main/java/com/margelo/nitro/contacts/HybridContactsModule.kt b/modules/ContactsNitroModule/android/src/main/java/com/margelo/nitro/contacts/HybridContactsModule.kt new file mode 100644 index 000000000000..00feaa7660c2 --- /dev/null +++ b/modules/ContactsNitroModule/android/src/main/java/com/margelo/nitro/contacts/HybridContactsModule.kt @@ -0,0 +1,166 @@ +package com.margelo.nitro.contacts + +import android.Manifest +import android.content.pm.PackageManager +import android.provider.ContactsContract +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import com.facebook.react.bridge.ReactApplicationContext +import com.margelo.nitro.NitroModules +import com.margelo.nitro.core.Promise + +class HybridContactsModule : HybridContactsModuleSpec() { + @Volatile + private var estimatedMemorySize: Long = 0 + + override val memorySize: Long + get() = estimatedMemorySize + + private val context: ReactApplicationContext? = NitroModules.applicationContext + + private fun requestContactPermission(): Boolean { + val currentActivity = context?.currentActivity + return if (currentActivity != null) { + ActivityCompat.requestPermissions( + currentActivity, arrayOf(REQUIRED_PERMISSION), PERMISSION_REQUEST_CODE + ) + true + } else { + false + } + } + + private fun hasPhoneContactsPermission(): Boolean { + return context?.let { + ContextCompat.checkSelfPermission(it, Manifest.permission.READ_CONTACTS) + } == PackageManager.PERMISSION_GRANTED + } + + override fun getAll(keys: Array): Promise> { + return Promise.parallel { + val contacts = mutableListOf() + if (!hasPhoneContactsPermission()) { + requestContactPermission() + return@parallel emptyArray() + } + + context?.contentResolver?.let { resolver -> + val projection = arrayOf( + ContactsContract.Data.MIMETYPE, + ContactsContract.Data.CONTACT_ID, + ContactsContract.Data.DISPLAY_NAME, + ContactsContract.Contacts.PHOTO_URI, + ContactsContract.Contacts.PHOTO_THUMBNAIL_URI, + ContactsContract.Data.DATA1, + ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, + ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, + ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME + ) + + val selection = "${ContactsContract.Data.MIMETYPE} IN (?, ?, ?)" + val selectionArgs = arrayOf( + ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE, + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, + ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE + ) + + val sortOrder = "${ContactsContract.Data.CONTACT_ID} ASC" + + resolver.query( + ContactsContract.Data.CONTENT_URI, + projection, + selection, + selectionArgs, + sortOrder + )?.use { cursor -> + val mimeTypeIndex = cursor.getColumnIndex(ContactsContract.Data.MIMETYPE) + val contactIdIndex = cursor.getColumnIndex(ContactsContract.Data.CONTACT_ID) + val photoUriIndex = cursor.getColumnIndex(ContactsContract.Contacts.PHOTO_URI) + val thumbnailUriIndex = + cursor.getColumnIndex(ContactsContract.Contacts.PHOTO_THUMBNAIL_URI) + val data1Index = cursor.getColumnIndex(ContactsContract.Data.DATA1) + val givenNameIndex = + cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME) + val familyNameIndex = + cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME) + val middleNameIndex = + cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME) + + var currentContact: Contact? = null + var currentContactId: String? = null + val currentPhoneNumbers = mutableListOf() + val currentEmailAddresses = mutableListOf() + + while (cursor.moveToNext()) { + val contactId = cursor.getString(contactIdIndex) + val mimeType = cursor.getString(mimeTypeIndex) + + if (contactId != currentContactId) { + currentContact?.let { contact -> + contacts.add( + contact.copy( + phoneNumbers = currentPhoneNumbers.toTypedArray(), + emailAddresses = currentEmailAddresses.toTypedArray() + ) + ) + } + currentPhoneNumbers.clear() + currentEmailAddresses.clear() + currentContact = Contact( + firstName = "", + lastName = "", + middleName = null, + phoneNumbers = emptyArray(), + emailAddresses = emptyArray(), + imageData = cursor.getString(photoUriIndex) ?: "", + thumbnailImageData = cursor.getString(thumbnailUriIndex) ?: "" + ) + currentContactId = contactId + } + + when (mimeType) { + ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> { + currentContact = currentContact?.copy( + firstName = cursor.getString(givenNameIndex) ?: "", + lastName = cursor.getString(familyNameIndex) ?: "", + middleName = cursor.getString(middleNameIndex) + ) + } + + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> { + cursor.getString(data1Index)?.let { phone -> + currentPhoneNumbers.add(StringHolder(phone)) + } + } + + ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE -> { + cursor.getString(data1Index)?.let { email -> + currentEmailAddresses.add(StringHolder(email)) + } + } + } + } + + // Add the last contact + currentContact?.let { contact -> + contacts.add( + contact.copy( + phoneNumbers = currentPhoneNumbers.toTypedArray(), + emailAddresses = currentEmailAddresses.toTypedArray() + ) + ) + } + } + } + + // Update memory size based on contact count + estimatedMemorySize = contacts.size.toLong() * 1024 // Assume ~1KB per contact + contacts.toTypedArray() + } + } + + companion object { + const val PERMISSION_REQUEST_CODE = 1 + const val REQUIRED_PERMISSION = Manifest.permission.READ_CONTACTS + } +} diff --git a/modules/ContactsNitroModule/babel.config.js b/modules/ContactsNitroModule/babel.config.js new file mode 100644 index 000000000000..3e0218e68fc3 --- /dev/null +++ b/modules/ContactsNitroModule/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['module:@react-native/babel-preset'], +} diff --git a/modules/ContactsNitroModule/ios/HybridContactsModule.swift b/modules/ContactsNitroModule/ios/HybridContactsModule.swift new file mode 100644 index 000000000000..59cc3ea31702 --- /dev/null +++ b/modules/ContactsNitroModule/ios/HybridContactsModule.swift @@ -0,0 +1,72 @@ +import NitroModules +import Contacts +import Foundation + +final class HybridContactsModule: HybridContactsModuleSpec { + public var hybridContext = margelo.nitro.HybridContext() + public var memorySize: Int { MemoryLayout.size } + + private let contactStore = CNContactStore() + private let imageDirectory: URL + private let fieldToKeyDescriptor: [ContactFields: CNKeyDescriptor] = [ + .firstName: CNContactGivenNameKey as CNKeyDescriptor, + .lastName: CNContactFamilyNameKey as CNKeyDescriptor, + .phoneNumbers: CNContactPhoneNumbersKey as CNKeyDescriptor, + .emailAddresses: CNContactEmailAddressesKey as CNKeyDescriptor, + .middleName: CNContactMiddleNameKey as CNKeyDescriptor, + .imageData: CNContactImageDataKey as CNKeyDescriptor, + .thumbnailImageData: CNContactThumbnailImageDataKey as CNKeyDescriptor, + .givenNameKey: CNContactGivenNameKey as CNKeyDescriptor + ] + + init() { + imageDirectory = FileManager.default.temporaryDirectory.appendingPathComponent("ContactImages") + try? FileManager.default.createDirectory(at: imageDirectory, withIntermediateDirectories: true) + } + + func getAll(keys: [ContactFields]) throws -> Promise<[Contact]> { + Promise.async { [unowned self] in + let keysSet = Set(keys) + let keysToFetch = keys.compactMap { self.fieldToKeyDescriptor[$0] } + guard !keysToFetch.isEmpty else { return [] } + + let request = CNContactFetchRequest(keysToFetch: keysToFetch) + var contacts = [Contact]() + contacts.reserveCapacity(1000) + + try self.contactStore.enumerateContacts(with: request) { contact, _ in + contacts.append(self.processContact(contact, keysSet: keysSet)) + } + + return contacts + } + } + + @inline(__always) + private func processContact(_ contact: CNContact, keysSet: Set) -> Contact { + Contact( + firstName: keysSet.contains(.firstName) ? contact.givenName : nil, + lastName: keysSet.contains(.lastName) ? contact.familyName : nil, + middleName: keysSet.contains(.middleName) ? contact.middleName : nil, + phoneNumbers: keysSet.contains(.phoneNumbers) ? contact.phoneNumbers.map { StringHolder(value: $0.value.stringValue) } : nil, + emailAddresses: keysSet.contains(.emailAddresses) ? contact.emailAddresses.map { StringHolder(value: $0.value as String) } : nil, + imageData: keysSet.contains(.imageData) ? getImagePath(for: contact, isThumbnail: false) : nil, + thumbnailImageData: keysSet.contains(.thumbnailImageData) ? getImagePath(for: contact, isThumbnail: true) : nil + ) + } + + @inline(__always) + private func getImagePath(for contact: CNContact, isThumbnail: Bool) -> String? { + let imageData = isThumbnail ? contact.thumbnailImageData : contact.imageData + guard let data = imageData else { return nil } + + let fileName = "\(contact.identifier)_\(isThumbnail ? "thumb" : "full").jpg" + let fileURL = imageDirectory.appendingPathComponent(fileName) + + if !FileManager.default.fileExists(atPath: fileURL.path) { + try? data.write(to: fileURL, options: .atomic) + } + + return fileURL.path + } +} diff --git a/modules/ContactsNitroModule/nitro.json b/modules/ContactsNitroModule/nitro.json new file mode 100644 index 000000000000..426f8486118a --- /dev/null +++ b/modules/ContactsNitroModule/nitro.json @@ -0,0 +1,17 @@ +{ + "cxxNamespace": ["contacts"], + "ios": { + "iosModuleName": "ContactsModule" + }, + "android": { + "androidNamespace": ["contacts"], + "androidCxxLibName": "ContactsModule" + }, + "autolinking": { + "ContactsModule": { + "swift": "HybridContactsModule", + "kotlin": "HybridContactsModule" + } + }, + "ignorePaths": ["node_modules"] +} diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModule+autolinking.cmake b/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModule+autolinking.cmake new file mode 100644 index 000000000000..5478bc224b05 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModule+autolinking.cmake @@ -0,0 +1,59 @@ +# +# ContactsModule+autolinking.cmake +# This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +# https://github.com/mrousavy/nitro +# Copyright © 2024 Marc Rousavy @ Margelo +# + +# This is a CMake file that adds all files generated by Nitrogen +# to the current CMake project. +# +# To use it, add this to your CMakeLists.txt: +# ```cmake +# include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/ContactsModule+autolinking.cmake) +# ``` + +# Add all headers that were generated by Nitrogen +include_directories( + "../nitrogen/generated/shared/c++" + "../nitrogen/generated/android/c++" + "../nitrogen/generated/android/" +) + +# Add all .cpp sources that were generated by Nitrogen +target_sources( + # CMake project name (Android C++ library name) + ContactsModule PRIVATE + # Autolinking Setup + ../nitrogen/generated/android/ContactsModuleOnLoad.cpp + # Shared Nitrogen C++ sources + ../nitrogen/generated/shared/c++/HybridContactsModuleSpec.cpp + # Android-specific Nitrogen C++ sources + ../nitrogen/generated/android/c++/JHybridContactsModuleSpec.cpp +) + +# Add all libraries required by the generated specs +find_package(fbjni REQUIRED) # <-- Used for communication between Java <-> C++ +find_package(ReactAndroid REQUIRED) # <-- Used to set up React Native bindings (e.g. CallInvoker/TurboModule) +find_package(react-native-nitro-modules REQUIRED) # <-- Used to create all HybridObjects and use the Nitro core library + +# Link all libraries together +target_link_libraries( + ContactsModule + fbjni::fbjni # <-- Facebook C++ JNI helpers + ReactAndroid::jsi # <-- RN: JSI + react-native-nitro-modules::NitroModules # <-- NitroModules Core :) +) + +# Link react-native (different prefab between RN 0.75 and RN 0.76) +if(ReactAndroid_VERSION_MINOR GREATER_EQUAL 76) + target_link_libraries( + ContactsModule + ReactAndroid::reactnative # <-- RN: Native Modules umbrella prefab + ) +else() + target_link_libraries( + ContactsModule + ReactAndroid::react_nativemodule_core # <-- RN: TurboModules Core + ) +endif() diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModule+autolinking.gradle b/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModule+autolinking.gradle new file mode 100644 index 000000000000..2d19cd2ced32 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModule+autolinking.gradle @@ -0,0 +1,27 @@ +/// +/// ContactsModule+autolinking.gradle +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +/// This is a Gradle file that adds all files generated by Nitrogen +/// to the current Gradle project. +/// +/// To use it, add this to your build.gradle: +/// ```gradle +/// apply from: '../nitrogen/generated/android/ContactsModule+autolinking.gradle' +/// ``` + +logger.warn("[NitroModules] 🔥 ContactsModule is boosted by nitro!") + +android { + sourceSets { + main { + java.srcDirs += [ + // Nitrogen files + "${project.projectDir}/../nitrogen/generated/android/kotlin" + ] + } + } +} diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModuleOnLoad.cpp b/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModuleOnLoad.cpp new file mode 100644 index 000000000000..156ea811e509 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModuleOnLoad.cpp @@ -0,0 +1,42 @@ +/// +/// ContactsModuleOnLoad.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#include "ContactsModuleOnLoad.hpp" + +#include +#include +#include + +#include "JHybridContactsModuleSpec.hpp" +#include +#include + +namespace margelo::nitro::contacts { + +int initialize(JavaVM* vm) { + using namespace margelo::nitro; + using namespace margelo::nitro::contacts; + using namespace facebook; + + return facebook::jni::initialize(vm, [] { + // Register native JNI methods + margelo::nitro::contacts::JHybridContactsModuleSpec::registerNatives(); + + // Register Nitro Hybrid Objects + HybridObjectRegistry::registerHybridObjectConstructor( + "ContactsModule", + []() -> std::shared_ptr { + static DefaultConstructableObject object("com/margelo/nitro/contacts/HybridContactsModule"); + auto instance = object.create(); + auto globalRef = jni::make_global(instance); + return JNISharedPtr::make_shared_from_jni(globalRef); + } + ); + }); +} + +} // namespace margelo::nitro::contacts diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModuleOnLoad.hpp b/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModuleOnLoad.hpp new file mode 100644 index 000000000000..b71adaca07bf --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModuleOnLoad.hpp @@ -0,0 +1,25 @@ +/// +/// ContactsModuleOnLoad.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#include +#include + +namespace margelo::nitro::contacts { + + /** + * Initializes the native (C++) part of ContactsModule, and autolinks all Hybrid Objects. + * Call this in your `JNI_OnLoad` function (probably inside `cpp-adapter.cpp`). + * Example: + * ```cpp (cpp-adapter.cpp) + * JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { + * return margelo::nitro::contacts::initialize(vm); + * } + * ``` + */ + int initialize(JavaVM* vm); + +} // namespace margelo::nitro::contacts diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModuleOnLoad.kt b/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModuleOnLoad.kt new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/ContactsModuleOnLoad.kt @@ -0,0 +1 @@ + diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/c++/JContact.hpp b/modules/ContactsNitroModule/nitrogen/generated/android/c++/JContact.hpp new file mode 100644 index 000000000000..bbd5354163a2 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/c++/JContact.hpp @@ -0,0 +1,114 @@ +/// +/// JContact.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "Contact.hpp" + +#include "JStringHolder.hpp" +#include "StringHolder.hpp" +#include +#include +#include + +namespace margelo::nitro::contacts { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "Contact" and the the Kotlin data class "Contact". + */ + struct JContact final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/contacts/Contact;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct Contact by copying all values to C++. + */ + [[maybe_unused]] + Contact toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldFirstName = clazz->getField("firstName"); + jni::local_ref firstName = this->getFieldValue(fieldFirstName); + static const auto fieldLastName = clazz->getField("lastName"); + jni::local_ref lastName = this->getFieldValue(fieldLastName); + static const auto fieldMiddleName = clazz->getField("middleName"); + jni::local_ref middleName = this->getFieldValue(fieldMiddleName); + static const auto fieldPhoneNumbers = clazz->getField>("phoneNumbers"); + jni::local_ref> phoneNumbers = this->getFieldValue(fieldPhoneNumbers); + static const auto fieldEmailAddresses = clazz->getField>("emailAddresses"); + jni::local_ref> emailAddresses = this->getFieldValue(fieldEmailAddresses); + static const auto fieldImageData = clazz->getField("imageData"); + jni::local_ref imageData = this->getFieldValue(fieldImageData); + static const auto fieldThumbnailImageData = clazz->getField("thumbnailImageData"); + jni::local_ref thumbnailImageData = this->getFieldValue(fieldThumbnailImageData); + return Contact( + firstName != nullptr ? std::make_optional(firstName->toStdString()) : std::nullopt, + lastName != nullptr ? std::make_optional(lastName->toStdString()) : std::nullopt, + middleName != nullptr ? std::make_optional(middleName->toStdString()) : std::nullopt, + phoneNumbers != nullptr ? std::make_optional([&]() { + size_t __size = phoneNumbers->size(); + std::vector __vector; + __vector.reserve(__size); + for (size_t __i = 0; __i < __size; __i++) { + auto __element = phoneNumbers->getElement(__i); + __vector.push_back(__element->toCpp()); + } + return __vector; + }()) : std::nullopt, + emailAddresses != nullptr ? std::make_optional([&]() { + size_t __size = emailAddresses->size(); + std::vector __vector; + __vector.reserve(__size); + for (size_t __i = 0; __i < __size; __i++) { + auto __element = emailAddresses->getElement(__i); + __vector.push_back(__element->toCpp()); + } + return __vector; + }()) : std::nullopt, + imageData != nullptr ? std::make_optional(imageData->toStdString()) : std::nullopt, + thumbnailImageData != nullptr ? std::make_optional(thumbnailImageData->toStdString()) : std::nullopt + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const Contact& value) { + return newInstance( + value.firstName.has_value() ? jni::make_jstring(value.firstName.value()) : nullptr, + value.lastName.has_value() ? jni::make_jstring(value.lastName.value()) : nullptr, + value.middleName.has_value() ? jni::make_jstring(value.middleName.value()) : nullptr, + value.phoneNumbers.has_value() ? [&]() { + size_t __size = value.phoneNumbers.value().size(); + jni::local_ref> __array = jni::JArrayClass::newArray(__size); + for (size_t __i = 0; __i < __size; __i++) { + const auto& __element = value.phoneNumbers.value()[__i]; + __array->setElement(__i, *JStringHolder::fromCpp(__element)); + } + return __array; + }() : nullptr, + value.emailAddresses.has_value() ? [&]() { + size_t __size = value.emailAddresses.value().size(); + jni::local_ref> __array = jni::JArrayClass::newArray(__size); + for (size_t __i = 0; __i < __size; __i++) { + const auto& __element = value.emailAddresses.value()[__i]; + __array->setElement(__i, *JStringHolder::fromCpp(__element)); + } + return __array; + }() : nullptr, + value.imageData.has_value() ? jni::make_jstring(value.imageData.value()) : nullptr, + value.thumbnailImageData.has_value() ? jni::make_jstring(value.thumbnailImageData.value()) : nullptr + ); + } + }; + +} // namespace margelo::nitro::contacts diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/c++/JContactFields.hpp b/modules/ContactsNitroModule/nitrogen/generated/android/c++/JContactFields.hpp new file mode 100644 index 000000000000..371b6607d105 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/c++/JContactFields.hpp @@ -0,0 +1,76 @@ +/// +/// JContactFields.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "ContactFields.hpp" + +namespace margelo::nitro::contacts { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ enum "ContactFields" and the the Kotlin enum "ContactFields". + */ + struct JContactFields final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/contacts/ContactFields;"; + + public: + /** + * Convert this Java/Kotlin-based enum to the C++ enum ContactFields. + */ + [[maybe_unused]] + ContactFields toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldOrdinal = clazz->getField("_ordinal"); + int ordinal = this->getFieldValue(fieldOrdinal); + return static_cast(ordinal); + } + + public: + /** + * Create a Java/Kotlin-based enum with the given C++ enum's value. + */ + [[maybe_unused]] + static jni::alias_ref fromCpp(ContactFields value) { + static const auto clazz = javaClassStatic(); + static const auto fieldFIRST_NAME = clazz->getStaticField("FIRST_NAME"); + static const auto fieldLAST_NAME = clazz->getStaticField("LAST_NAME"); + static const auto fieldMIDDLE_NAME = clazz->getStaticField("MIDDLE_NAME"); + static const auto fieldPHONE_NUMBERS = clazz->getStaticField("PHONE_NUMBERS"); + static const auto fieldEMAIL_ADDRESSES = clazz->getStaticField("EMAIL_ADDRESSES"); + static const auto fieldIMAGE_DATA = clazz->getStaticField("IMAGE_DATA"); + static const auto fieldTHUMBNAIL_IMAGE_DATA = clazz->getStaticField("THUMBNAIL_IMAGE_DATA"); + static const auto fieldGIVEN_NAME_KEY = clazz->getStaticField("GIVEN_NAME_KEY"); + + switch (value) { + case ContactFields::FIRST_NAME: + return clazz->getStaticFieldValue(fieldFIRST_NAME); + case ContactFields::LAST_NAME: + return clazz->getStaticFieldValue(fieldLAST_NAME); + case ContactFields::MIDDLE_NAME: + return clazz->getStaticFieldValue(fieldMIDDLE_NAME); + case ContactFields::PHONE_NUMBERS: + return clazz->getStaticFieldValue(fieldPHONE_NUMBERS); + case ContactFields::EMAIL_ADDRESSES: + return clazz->getStaticFieldValue(fieldEMAIL_ADDRESSES); + case ContactFields::IMAGE_DATA: + return clazz->getStaticFieldValue(fieldIMAGE_DATA); + case ContactFields::THUMBNAIL_IMAGE_DATA: + return clazz->getStaticFieldValue(fieldTHUMBNAIL_IMAGE_DATA); + case ContactFields::GIVEN_NAME_KEY: + return clazz->getStaticFieldValue(fieldGIVEN_NAME_KEY); + default: + std::string stringValue = std::to_string(static_cast(value)); + throw std::invalid_argument("Invalid enum value (" + stringValue + "!"); + } + } + }; + +} // namespace margelo::nitro::contacts diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/c++/JHybridContactsModuleSpec.cpp b/modules/ContactsNitroModule/nitrogen/generated/android/c++/JHybridContactsModuleSpec.cpp new file mode 100644 index 000000000000..e0505ee46d36 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/c++/JHybridContactsModuleSpec.cpp @@ -0,0 +1,84 @@ +/// +/// JHybridContactsModuleSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#include "JHybridContactsModuleSpec.hpp" + +// Forward declaration of `Contact` to properly resolve imports. +namespace margelo::nitro::contacts { struct Contact; } +// Forward declaration of `StringHolder` to properly resolve imports. +namespace margelo::nitro::contacts { struct StringHolder; } +// Forward declaration of `ContactFields` to properly resolve imports. +namespace margelo::nitro::contacts { enum class ContactFields; } + +#include +#include +#include "Contact.hpp" +#include +#include "JContact.hpp" +#include +#include +#include "StringHolder.hpp" +#include "JStringHolder.hpp" +#include "ContactFields.hpp" +#include "JContactFields.hpp" + +namespace margelo::nitro::contacts { + + jni::local_ref JHybridContactsModuleSpec::initHybrid(jni::alias_ref jThis) { + return makeCxxInstance(jThis); + } + + void JHybridContactsModuleSpec::registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", JHybridContactsModuleSpec::initHybrid), + }); + } + + size_t JHybridContactsModuleSpec::getExternalMemorySize() noexcept { + static const auto method = _javaPart->getClass()->getMethod("getMemorySize"); + return method(_javaPart); + } + + // Properties + + + // Methods + std::shared_ptr>> JHybridContactsModuleSpec::getAll(const std::vector& keys) { + static const auto method = _javaPart->getClass()->getMethod(jni::alias_ref> /* keys */)>("getAll"); + auto __result = method(_javaPart, [&]() { + size_t __size = keys.size(); + jni::local_ref> __array = jni::JArrayClass::newArray(__size); + for (size_t __i = 0; __i < __size; __i++) { + const auto& __element = keys[__i]; + __array->setElement(__i, *JContactFields::fromCpp(__element)); + } + return __array; + }()); + return [&]() { + auto __promise = Promise>::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast>(__boxedResult); + __promise->resolve([&]() { + size_t __size = __result->size(); + std::vector __vector; + __vector.reserve(__size); + for (size_t __i = 0; __i < __size; __i++) { + auto __element = __result->getElement(__i); + __vector.push_back(__element->toCpp()); + } + return __vector; + }()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + +} // namespace margelo::nitro::contacts diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/c++/JHybridContactsModuleSpec.hpp b/modules/ContactsNitroModule/nitrogen/generated/android/c++/JHybridContactsModuleSpec.hpp new file mode 100644 index 000000000000..6b94d3be37e7 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/c++/JHybridContactsModuleSpec.hpp @@ -0,0 +1,62 @@ +/// +/// HybridContactsModuleSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include +#include "HybridContactsModuleSpec.hpp" + + + + +namespace margelo::nitro::contacts { + + using namespace facebook; + + class JHybridContactsModuleSpec: public jni::HybridClass, + public virtual HybridContactsModuleSpec { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/contacts/HybridContactsModuleSpec;"; + static jni::local_ref initHybrid(jni::alias_ref jThis); + static void registerNatives(); + + protected: + // C++ constructor (called from Java via `initHybrid()`) + explicit JHybridContactsModuleSpec(jni::alias_ref jThis) : + HybridObject(HybridContactsModuleSpec::TAG), + _javaPart(jni::make_global(jThis)) {} + + public: + virtual ~JHybridContactsModuleSpec() { + // Hermes GC can destroy JS objects on a non-JNI Thread. + jni::ThreadScope::WithClassLoader([&] { _javaPart.reset(); }); + } + + public: + size_t getExternalMemorySize() noexcept override; + + public: + inline const jni::global_ref& getJavaPart() const noexcept { + return _javaPart; + } + + public: + // Properties + + + public: + // Methods + std::shared_ptr>> getAll(const std::vector& keys) override; + + private: + friend HybridBase; + using HybridBase::HybridBase; + jni::global_ref _javaPart; + }; + +} // namespace margelo::nitro::contacts diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/c++/JStringHolder.hpp b/modules/ContactsNitroModule/nitrogen/generated/android/c++/JStringHolder.hpp new file mode 100644 index 000000000000..29695fe48d58 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/c++/JStringHolder.hpp @@ -0,0 +1,52 @@ +/// +/// JStringHolder.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "StringHolder.hpp" + +#include + +namespace margelo::nitro::contacts { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "StringHolder" and the the Kotlin data class "StringHolder". + */ + struct JStringHolder final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/contacts/StringHolder;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct StringHolder by copying all values to C++. + */ + [[maybe_unused]] + StringHolder toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldValue = clazz->getField("value"); + jni::local_ref value = this->getFieldValue(fieldValue); + return StringHolder( + value->toStdString() + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const StringHolder& value) { + return newInstance( + jni::make_jstring(value.value) + ); + } + }; + +} // namespace margelo::nitro::contacts diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/Contact.kt b/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/Contact.kt new file mode 100644 index 000000000000..a6d9e59a2b2b --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/Contact.kt @@ -0,0 +1,27 @@ +/// +/// Contact.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.contacts + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* + +/** + * Represents the JavaScript object/struct "Contact". + */ +@DoNotStrip +@Keep +data class Contact( + val firstName: String?, + val lastName: String?, + val middleName: String?, + val phoneNumbers: Array?, + val emailAddresses: Array?, + val imageData: String?, + val thumbnailImageData: String? +) diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/ContactFields.kt b/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/ContactFields.kt new file mode 100644 index 000000000000..841d6c82a32b --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/ContactFields.kt @@ -0,0 +1,31 @@ +/// +/// ContactFields.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.contacts + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + +/** + * Represents the JavaScript enum/union "ContactFields". + */ +@DoNotStrip +@Keep +enum class ContactFields { + FIRST_NAME, + LAST_NAME, + MIDDLE_NAME, + PHONE_NUMBERS, + EMAIL_ADDRESSES, + IMAGE_DATA, + THUMBNAIL_IMAGE_DATA, + GIVEN_NAME_KEY; + + @DoNotStrip + @Keep + private val _ordinal = ordinal +} diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/HybridContactsModuleSpec.kt b/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/HybridContactsModuleSpec.kt new file mode 100644 index 000000000000..63a118b8be57 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/HybridContactsModuleSpec.kt @@ -0,0 +1,64 @@ +/// +/// HybridContactsModuleSpec.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.contacts + +import android.util.Log +import androidx.annotation.Keep +import com.facebook.jni.HybridData +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* + +/** + * A Kotlin class representing the ContactsModule HybridObject. + * Implement this abstract class to create Kotlin-based instances of ContactsModule. + */ +@DoNotStrip +@Keep +@Suppress("RedundantSuppression", "KotlinJniMissingFunction", "PropertyName", "RedundantUnitReturnType", "unused") +abstract class HybridContactsModuleSpec: HybridObject() { + @DoNotStrip + private var mHybridData: HybridData = initHybrid() + + init { + // Pass this `HybridData` through to it's base class, + // to represent inheritance to JHybridObject on C++ side + super.updateNative(mHybridData) + } + + /** + * Call from a child class to initialize HybridData with a child. + */ + override fun updateNative(hybridData: HybridData) { + mHybridData = hybridData + } + + // Properties + + + // Methods + @DoNotStrip + @Keep + abstract fun getAll(keys: Array): Promise> + + private external fun initHybrid(): HybridData + + companion object { + private const val TAG = "HybridContactsModuleSpec" + init { + try { + Log.i(TAG, "Loading ContactsModule C++ library...") + System.loadLibrary("ContactsModule") + Log.i(TAG, "Successfully loaded ContactsModule C++ library!") + } catch (e: Error) { + Log.e(TAG, "Failed to load ContactsModule C++ library! Is it properly installed and linked? " + + "Is the name correct? (see `CMakeLists.txt`, at `add_library(...)`)", e) + throw e + } + } + } +} diff --git a/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/StringHolder.kt b/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/StringHolder.kt new file mode 100644 index 000000000000..b6af53e53217 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/android/kotlin/com/margelo/nitro/contacts/StringHolder.kt @@ -0,0 +1,21 @@ +/// +/// StringHolder.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.contacts + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.* + +/** + * Represents the JavaScript object/struct "StringHolder". + */ +@DoNotStrip +@Keep +data class StringHolder( + val value: String +) diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule+autolinking.rb b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule+autolinking.rb new file mode 100644 index 000000000000..35bc19c47bf7 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule+autolinking.rb @@ -0,0 +1,58 @@ +# +# ContactsModule+autolinking.rb +# This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +# https://github.com/mrousavy/nitro +# Copyright © 2024 Marc Rousavy @ Margelo +# + +# This is a Ruby script that adds all files generated by Nitrogen +# to the given podspec. +# +# To use it, add this to your .podspec: +# ```ruby +# Pod::Spec.new do |spec| +# # ... +# +# # Add all files generated by Nitrogen +# load 'nitrogen/generated/ios/ContactsModule+autolinking.rb' +# add_nitrogen_files(spec) +# end +# ``` + +def add_nitrogen_files(spec) + Pod::UI.puts "[NitroModules] 🔥 ContactsModule is boosted by nitro!" + + spec.dependency "NitroModules" + + current_source_files = Array(spec.attributes_hash['source_files']) + spec.source_files = current_source_files + [ + # Generated cross-platform specs + "nitrogen/generated/shared/**/*.{h,hpp,c,cpp,swift}", + # Generated bridges for the cross-platform specs + "nitrogen/generated/ios/**/*.{h,hpp,c,cpp,mm,swift}", + ] + + current_public_header_files = Array(spec.attributes_hash['public_header_files']) + spec.public_header_files = current_public_header_files + [ + # Generated specs + "nitrogen/generated/shared/**/*.{h,hpp}", + # Swift to C++ bridging helpers + "nitrogen/generated/ios/ContactsModule-Swift-Cxx-Bridge.hpp" + ] + + current_private_header_files = Array(spec.attributes_hash['private_header_files']) + spec.private_header_files = current_private_header_files + [ + # iOS specific specs + "nitrogen/generated/ios/c++/**/*.{h,hpp}", + ] + + current_pod_target_xcconfig = spec.attributes_hash['pod_target_xcconfig'] || {} + spec.pod_target_xcconfig = current_pod_target_xcconfig.merge({ + # Use C++ 20 + "CLANG_CXX_LANGUAGE_STANDARD" => "c++20", + # Enables C++ <-> Swift interop (by default it's only C) + "SWIFT_OBJC_INTEROP_MODE" => "objcxx", + # Enables stricter modular headers + "DEFINES_MODULE" => "YES", + }) +end diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule-Swift-Cxx-Bridge.cpp b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule-Swift-Cxx-Bridge.cpp new file mode 100644 index 000000000000..4746dbadaa18 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule-Swift-Cxx-Bridge.cpp @@ -0,0 +1,33 @@ +/// +/// ContactsModule-Swift-Cxx-Bridge.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#include "ContactsModule-Swift-Cxx-Bridge.hpp" + +// Include C++ implementation defined types +#include "ContactsModule-Swift-Cxx-Umbrella.hpp" +#include "HybridContactsModuleSpecSwift.hpp" +#include + +namespace margelo::nitro::contacts::bridge::swift { + + // pragma MARK: std::shared_ptr + std::shared_ptr create_std__shared_ptr_margelo__nitro__contacts__HybridContactsModuleSpec_(void* _Nonnull swiftUnsafePointer) { + ContactsModule::HybridContactsModuleSpecCxx swiftPart = ContactsModule::HybridContactsModuleSpecCxxUnsafe::fromUnsafe(swiftUnsafePointer); + return HybridContext::getOrCreate(swiftPart); + } + void* _Nonnull get_std__shared_ptr_margelo__nitro__contacts__HybridContactsModuleSpec_(std__shared_ptr_margelo__nitro__contacts__HybridContactsModuleSpec_ cppType) { + std::shared_ptr swiftWrapper = std::dynamic_pointer_cast(cppType); + #ifdef NITRO_DEBUG + if (swiftWrapper == nullptr) [[unlikely]] { + throw std::runtime_error("Class \"HybridContactsModuleSpec\" is not implemented in Swift!"); + } + #endif + ContactsModule::HybridContactsModuleSpecCxx swiftPart = swiftWrapper->getSwiftPart(); + return ContactsModule::HybridContactsModuleSpecCxxUnsafe::toUnsafe(swiftPart); + } + +} // namespace margelo::nitro::contacts::bridge::swift diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule-Swift-Cxx-Bridge.hpp b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule-Swift-Cxx-Bridge.hpp new file mode 100644 index 000000000000..76d584613df2 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule-Swift-Cxx-Bridge.hpp @@ -0,0 +1,167 @@ +/// +/// ContactsModule-Swift-Cxx-Bridge.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#pragma once + +// Forward declarations of C++ defined types +// Forward declaration of `ContactFields` to properly resolve imports. +namespace margelo::nitro::contacts { enum class ContactFields; } +// Forward declaration of `Contact` to properly resolve imports. +namespace margelo::nitro::contacts { struct Contact; } +// Forward declaration of `HybridContactsModuleSpec` to properly resolve imports. +namespace margelo::nitro::contacts { class HybridContactsModuleSpec; } +// Forward declaration of `StringHolder` to properly resolve imports. +namespace margelo::nitro::contacts { struct StringHolder; } + +// Forward declarations of Swift defined types +// Forward declaration of `HybridContactsModuleSpecCxx` to properly resolve imports. +namespace ContactsModule { class HybridContactsModuleSpecCxx; } + +// Include C++ defined types +#include "Contact.hpp" +#include "ContactFields.hpp" +#include "HybridContactsModuleSpec.hpp" +#include "StringHolder.hpp" +#include +#include +#include +#include +#include +#include +#include + +/** + * Contains specialized versions of C++ templated types so they can be accessed from Swift, + * as well as helper functions to interact with those C++ types from Swift. + */ +namespace margelo::nitro::contacts::bridge::swift { + + // pragma MARK: std::optional + /** + * Specialized version of `std::optional`. + */ + using std__optional_std__string_ = std::optional; + inline std::optional create_std__optional_std__string_(const std::string& value) { + return std::optional(value); + } + + // pragma MARK: std::vector + /** + * Specialized version of `std::vector`. + */ + using std__vector_StringHolder_ = std::vector; + inline std::vector create_std__vector_StringHolder_(size_t size) { + std::vector vector; + vector.reserve(size); + return vector; + } + + // pragma MARK: std::optional> + /** + * Specialized version of `std::optional>`. + */ + using std__optional_std__vector_StringHolder__ = std::optional>; + inline std::optional> create_std__optional_std__vector_StringHolder__(const std::vector& value) { + return std::optional>(value); + } + + // pragma MARK: std::vector + /** + * Specialized version of `std::vector`. + */ + using std__vector_Contact_ = std::vector; + inline std::vector create_std__vector_Contact_(size_t size) { + std::vector vector; + vector.reserve(size); + return vector; + } + + // pragma MARK: std::shared_ptr>> + /** + * Specialized version of `std::shared_ptr>>`. + */ + using std__shared_ptr_Promise_std__vector_Contact___ = std::shared_ptr>>; + inline std::shared_ptr>> create_std__shared_ptr_Promise_std__vector_Contact___() { + return Promise>::create(); + } + + // pragma MARK: std::function& /* result */)> + /** + * Specialized version of `std::function&)>`. + */ + using Func_void_std__vector_Contact_ = std::function& /* result */)>; + /** + * Wrapper class for a `std::function& / * result * /)>`, this can be used from Swift. + */ + class Func_void_std__vector_Contact__Wrapper final { + public: + explicit Func_void_std__vector_Contact__Wrapper(const std::function& /* result */)>& func): _function(func) {} + explicit Func_void_std__vector_Contact__Wrapper(std::function& /* result */)>&& func): _function(std::move(func)) {} + inline void call(std::vector result) const { + _function(result); + } + private: + std::function& /* result */)> _function; + }; + inline Func_void_std__vector_Contact_ create_Func_void_std__vector_Contact_(void* _Nonnull closureHolder, void(* _Nonnull call)(void* _Nonnull /* closureHolder */, std::vector), void(* _Nonnull destroy)(void* _Nonnull)) { + std::shared_ptr sharedClosureHolder(closureHolder, destroy); + return Func_void_std__vector_Contact_([sharedClosureHolder, call](const std::vector& result) -> void { + call(sharedClosureHolder.get(), result); + }); + } + inline std::shared_ptr share_Func_void_std__vector_Contact_(const Func_void_std__vector_Contact_& value) { + return std::make_shared(value); + } + + // pragma MARK: std::function + /** + * Specialized version of `std::function`. + */ + using Func_void_std__exception_ptr = std::function; + /** + * Wrapper class for a `std::function`, this can be used from Swift. + */ + class Func_void_std__exception_ptr_Wrapper final { + public: + explicit Func_void_std__exception_ptr_Wrapper(const std::function& func): _function(func) {} + explicit Func_void_std__exception_ptr_Wrapper(std::function&& func): _function(std::move(func)) {} + inline void call(std::exception_ptr error) const { + _function(error); + } + private: + std::function _function; + }; + inline Func_void_std__exception_ptr create_Func_void_std__exception_ptr(void* _Nonnull closureHolder, void(* _Nonnull call)(void* _Nonnull /* closureHolder */, std::exception_ptr), void(* _Nonnull destroy)(void* _Nonnull)) { + std::shared_ptr sharedClosureHolder(closureHolder, destroy); + return Func_void_std__exception_ptr([sharedClosureHolder, call](const std::exception_ptr& error) -> void { + call(sharedClosureHolder.get(), error); + }); + } + inline std::shared_ptr share_Func_void_std__exception_ptr(const Func_void_std__exception_ptr& value) { + return std::make_shared(value); + } + + // pragma MARK: std::vector + /** + * Specialized version of `std::vector`. + */ + using std__vector_ContactFields_ = std::vector; + inline std::vector create_std__vector_ContactFields_(size_t size) { + std::vector vector; + vector.reserve(size); + return vector; + } + + // pragma MARK: std::shared_ptr + /** + * Specialized version of `std::shared_ptr`. + */ + using std__shared_ptr_margelo__nitro__contacts__HybridContactsModuleSpec_ = std::shared_ptr; + std::shared_ptr create_std__shared_ptr_margelo__nitro__contacts__HybridContactsModuleSpec_(void* _Nonnull swiftUnsafePointer); + void* _Nonnull get_std__shared_ptr_margelo__nitro__contacts__HybridContactsModuleSpec_(std__shared_ptr_margelo__nitro__contacts__HybridContactsModuleSpec_ cppType); + +} // namespace margelo::nitro::contacts::bridge::swift diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule-Swift-Cxx-Umbrella.hpp b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule-Swift-Cxx-Umbrella.hpp new file mode 100644 index 000000000000..6f38d7c7e417 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModule-Swift-Cxx-Umbrella.hpp @@ -0,0 +1,54 @@ +/// +/// ContactsModule-Swift-Cxx-Umbrella.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#pragma once + +// Forward declarations of C++ defined types +// Forward declaration of `ContactFields` to properly resolve imports. +namespace margelo::nitro::contacts { enum class ContactFields; } +// Forward declaration of `Contact` to properly resolve imports. +namespace margelo::nitro::contacts { struct Contact; } +// Forward declaration of `HybridContactsModuleSpec` to properly resolve imports. +namespace margelo::nitro::contacts { class HybridContactsModuleSpec; } +// Forward declaration of `StringHolder` to properly resolve imports. +namespace margelo::nitro::contacts { struct StringHolder; } + +// Include C++ defined types +#include "Contact.hpp" +#include "ContactFields.hpp" +#include "HybridContactsModuleSpec.hpp" +#include "StringHolder.hpp" +#include +#include +#include +#include +#include + +// C++ helpers for Swift +#include "ContactsModule-Swift-Cxx-Bridge.hpp" + +// Common C++ types used in Swift +#include +#include +#include +#include + +// Forward declarations of Swift defined types +// Forward declaration of `HybridContactsModuleSpecCxx` to properly resolve imports. +namespace ContactsModule { class HybridContactsModuleSpecCxx; } + +// Include Swift defined types +#if __has_include("ContactsModule-Swift.h") +// This header is generated by Xcode/Swift on every app build. +// If it cannot be found, make sure the Swift module's name (= podspec name) is actually "ContactsModule". +#include "ContactsModule-Swift.h" +// Same as above, but used when building with frameworks (`use_frameworks`) +#elif __has_include() +#include +#else +#error ContactsModule's autogenerated Swift header cannot be found! Make sure the Swift module's name (= podspec name) is actually "ContactsModule", and try building the app first. +#endif diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModuleAutolinking.mm b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModuleAutolinking.mm new file mode 100644 index 000000000000..e769fb6a6806 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModuleAutolinking.mm @@ -0,0 +1,33 @@ +/// +/// ContactsModuleAutolinking.mm +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#import +#import +#import "ContactsModule-Swift-Cxx-Umbrella.hpp" +#import + +#include "HybridContactsModuleSpecSwift.hpp" + +@interface ContactsModuleAutolinking : NSObject +@end + +@implementation ContactsModuleAutolinking + ++ (void) load { + using namespace margelo::nitro; + using namespace margelo::nitro::contacts; + + HybridObjectRegistry::registerHybridObjectConstructor( + "ContactsModule", + []() -> std::shared_ptr { + std::shared_ptr hybridObject = ContactsModule::ContactsModuleAutolinking::createContactsModule(); + return hybridObject; + } + ); +} + +@end diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModuleAutolinking.swift b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModuleAutolinking.swift new file mode 100644 index 000000000000..15d9d9b9064e --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/ContactsModuleAutolinking.swift @@ -0,0 +1,26 @@ +/// +/// ContactsModuleAutolinking.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +public final class ContactsModuleAutolinking { + public typealias bridge = margelo.nitro.contacts.bridge.swift + + /** + * Creates an instance of a Swift class that implements `HybridContactsModuleSpec`, + * and wraps it in a Swift class that can directly interop with C++ (`HybridContactsModuleSpecCxx`) + * + * This is generated by Nitrogen and will initialize the class specified + * in the `"autolinking"` property of `nitro.json` (in this case, `HybridContactsModule`). + */ + public static func createContactsModule() -> bridge.std__shared_ptr_margelo__nitro__contacts__HybridContactsModuleSpec_ { + let hybridObject = HybridContactsModule() + return { () -> bridge.std__shared_ptr_margelo__nitro__contacts__HybridContactsModuleSpec_ in + let __cxxWrapped = HybridContactsModuleSpecCxx(hybridObject) + let __pointer = HybridContactsModuleSpecCxxUnsafe.toUnsafe(__cxxWrapped) + return bridge.create_std__shared_ptr_margelo__nitro__contacts__HybridContactsModuleSpec_(__pointer) + }() + } +} diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.cpp b/modules/ContactsNitroModule/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.cpp new file mode 100644 index 000000000000..71151f3c1883 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.cpp @@ -0,0 +1,11 @@ +/// +/// HybridContactsModuleSpecSwift.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#include "HybridContactsModuleSpecSwift.hpp" + +namespace margelo::nitro::contacts { +} // namespace margelo::nitro::contacts diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.hpp b/modules/ContactsNitroModule/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.hpp new file mode 100644 index 000000000000..dbb4fe829dc2 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.hpp @@ -0,0 +1,82 @@ +/// +/// HybridContactsModuleSpecSwift.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#pragma once + +#include "HybridContactsModuleSpec.hpp" + +// Forward declaration of `HybridContactsModuleSpecCxx` to properly resolve imports. +namespace ContactsModule { class HybridContactsModuleSpecCxx; } + +// Forward declaration of `Contact` to properly resolve imports. +namespace margelo::nitro::contacts { struct Contact; } +// Forward declaration of `StringHolder` to properly resolve imports. +namespace margelo::nitro::contacts { struct StringHolder; } +// Forward declaration of `ContactFields` to properly resolve imports. +namespace margelo::nitro::contacts { enum class ContactFields; } + +#include +#include +#include "Contact.hpp" +#include +#include +#include "StringHolder.hpp" +#include "ContactFields.hpp" + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +#include "ContactsModule-Swift-Cxx-Umbrella.hpp" + +namespace margelo::nitro::contacts { + + /** + * The C++ part of HybridContactsModuleSpecCxx.swift. + * + * HybridContactsModuleSpecSwift (C++) accesses HybridContactsModuleSpecCxx (Swift), and might + * contain some additional bridging code for C++ <> Swift interop. + * + * Since this obviously introduces an overhead, I hope at some point in + * the future, HybridContactsModuleSpecCxx can directly inherit from the C++ class HybridContactsModuleSpec + * to simplify the whole structure and memory management. + */ + class HybridContactsModuleSpecSwift: public virtual HybridContactsModuleSpec { + public: + // Constructor from a Swift instance + explicit HybridContactsModuleSpecSwift(const ContactsModule::HybridContactsModuleSpecCxx& swiftPart): + HybridObject(HybridContactsModuleSpec::TAG), + _swiftPart(swiftPart) { } + + public: + // Get the Swift part + inline ContactsModule::HybridContactsModuleSpecCxx getSwiftPart() noexcept { return _swiftPart; } + + public: + // Get memory pressure + inline size_t getExternalMemorySize() noexcept override { + return _swiftPart.getMemorySize(); + } + + public: + // Properties + + + public: + // Methods + inline std::shared_ptr>> getAll(const std::vector& keys) override { + auto __result = _swiftPart.getAll(keys); + return __result; + } + + private: + ContactsModule::HybridContactsModuleSpecCxx _swiftPart; + }; + +} // namespace margelo::nitro::contacts diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/swift/Contact.swift b/modules/ContactsNitroModule/nitrogen/generated/ios/swift/Contact.swift new file mode 100644 index 000000000000..404d6ba86b25 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/swift/Contact.swift @@ -0,0 +1,251 @@ +/// +/// Contact.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `Contact`, backed by a C++ struct. + */ +public typealias Contact = margelo.nitro.contacts.Contact + +public extension Contact { + private typealias bridge = margelo.nitro.contacts.bridge.swift + + /** + * Create a new instance of `Contact`. + */ + init(firstName: String?, lastName: String?, middleName: String?, phoneNumbers: [StringHolder]?, emailAddresses: [StringHolder]?, imageData: String?, thumbnailImageData: String?) { + self.init({ () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = firstName { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = lastName { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = middleName { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__vector_StringHolder__ in + if let __unwrappedValue = phoneNumbers { + return bridge.create_std__optional_std__vector_StringHolder__({ () -> bridge.std__vector_StringHolder_ in + var __vector = bridge.create_std__vector_StringHolder_(__unwrappedValue.count) + for __item in __unwrappedValue { + __vector.push_back(__item) + } + return __vector + }()) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__vector_StringHolder__ in + if let __unwrappedValue = emailAddresses { + return bridge.create_std__optional_std__vector_StringHolder__({ () -> bridge.std__vector_StringHolder_ in + var __vector = bridge.create_std__vector_StringHolder_(__unwrappedValue.count) + for __item in __unwrappedValue { + __vector.push_back(__item) + } + return __vector + }()) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = imageData { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = thumbnailImageData { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }()) + } + + var firstName: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__firstName.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__firstName = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } + + var lastName: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__lastName.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__lastName = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } + + var middleName: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__middleName.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__middleName = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } + + var phoneNumbers: [StringHolder]? { + @inline(__always) + get { + return { () -> [StringHolder]? in + if let __unwrapped = self.__phoneNumbers.value { + return __unwrapped.map({ __item in __item }) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__phoneNumbers = { () -> bridge.std__optional_std__vector_StringHolder__ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__vector_StringHolder__({ () -> bridge.std__vector_StringHolder_ in + var __vector = bridge.create_std__vector_StringHolder_(__unwrappedValue.count) + for __item in __unwrappedValue { + __vector.push_back(__item) + } + return __vector + }()) + } else { + return .init() + } + }() + } + } + + var emailAddresses: [StringHolder]? { + @inline(__always) + get { + return { () -> [StringHolder]? in + if let __unwrapped = self.__emailAddresses.value { + return __unwrapped.map({ __item in __item }) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__emailAddresses = { () -> bridge.std__optional_std__vector_StringHolder__ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__vector_StringHolder__({ () -> bridge.std__vector_StringHolder_ in + var __vector = bridge.create_std__vector_StringHolder_(__unwrappedValue.count) + for __item in __unwrappedValue { + __vector.push_back(__item) + } + return __vector + }()) + } else { + return .init() + } + }() + } + } + + var imageData: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__imageData.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__imageData = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } + + var thumbnailImageData: String? { + @inline(__always) + get { + return { () -> String? in + if let __unwrapped = self.__thumbnailImageData.value { + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) + set { + self.__thumbnailImageData = { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } + }() + } + } +} diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/swift/ContactFields.swift b/modules/ContactsNitroModule/nitrogen/generated/ios/swift/ContactFields.swift new file mode 100644 index 000000000000..ce38940795d9 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/swift/ContactFields.swift @@ -0,0 +1,64 @@ +/// +/// ContactFields.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `ContactFields`, backed by a C++ enum. + */ +public typealias ContactFields = margelo.nitro.contacts.ContactFields + +public extension ContactFields { + /** + * Get a ContactFields for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "FIRST_NAME": + self = .firstName + case "LAST_NAME": + self = .lastName + case "MIDDLE_NAME": + self = .middleName + case "PHONE_NUMBERS": + self = .phoneNumbers + case "EMAIL_ADDRESSES": + self = .emailAddresses + case "IMAGE_DATA": + self = .imageData + case "THUMBNAIL_IMAGE_DATA": + self = .thumbnailImageData + case "GIVEN_NAME_KEY": + self = .givenNameKey + default: + return nil + } + } + + /** + * Get the String value this ContactFields represents. + */ + var stringValue: String { + switch self { + case .firstName: + return "FIRST_NAME" + case .lastName: + return "LAST_NAME" + case .middleName: + return "MIDDLE_NAME" + case .phoneNumbers: + return "PHONE_NUMBERS" + case .emailAddresses: + return "EMAIL_ADDRESSES" + case .imageData: + return "IMAGE_DATA" + case .thumbnailImageData: + return "THUMBNAIL_IMAGE_DATA" + case .givenNameKey: + return "GIVEN_NAME_KEY" + } + } +} diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/swift/HybridContactsModuleSpec.swift b/modules/ContactsNitroModule/nitrogen/generated/ios/swift/HybridContactsModuleSpec.swift new file mode 100644 index 000000000000..611110efca1d --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/swift/HybridContactsModuleSpec.swift @@ -0,0 +1,36 @@ +/// +/// HybridContactsModuleSpec.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +import Foundation +import NitroModules + +/** + * A Swift protocol representing the ContactsModule HybridObject. + * Implement this protocol to create Swift-based instances of ContactsModule. + * + * When implementing this protocol, make sure to initialize `hybridContext` - example: + * ``` + * public class HybridContactsModule : HybridContactsModuleSpec { + * // Initialize HybridContext + * var hybridContext = margelo.nitro.HybridContext() + * + * // Return size of the instance to inform JS GC about memory pressure + * var memorySize: Int { + * return getSizeOf(self) + * } + * + * // ... + * } + * ``` + */ +public protocol HybridContactsModuleSpec: AnyObject, HybridObjectSpec { + // Properties + + + // Methods + func getAll(keys: [ContactFields]) throws -> Promise<[Contact]> +} diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/swift/HybridContactsModuleSpecCxx.swift b/modules/ContactsNitroModule/nitrogen/generated/ios/swift/HybridContactsModuleSpecCxx.swift new file mode 100644 index 000000000000..156cdf86bd7f --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/swift/HybridContactsModuleSpecCxx.swift @@ -0,0 +1,123 @@ +/// +/// HybridContactsModuleSpecCxx.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +import Foundation +import NitroModules + +/** + * Helper class for converting instances of `HybridContactsModuleSpecCxx` from- and to unsafe pointers. + * This is useful to pass Swift classes to C++, without having to strongly type the C++ function signature. + * The actual Swift type can be included in the .cpp file, without having to forward-declare anything in .hpp. + */ +public final class HybridContactsModuleSpecCxxUnsafe { + /** + * Casts a `HybridContactsModuleSpecCxx` instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + public static func toUnsafe(_ instance: HybridContactsModuleSpecCxx) -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(instance).toOpaque() + } + + /** + * Casts an unsafe pointer to a `HybridContactsModuleSpecCxx`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> HybridContactsModuleSpecCxx { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} + +/** + * A class implementation that bridges HybridContactsModuleSpec over to C++. + * In C++, we cannot use Swift protocols - so we need to wrap it in a class to make it strongly defined. + * + * Also, some Swift types need to be bridged with special handling: + * - Enums need to be wrapped in Structs, otherwise they cannot be accessed bi-directionally (Swift bug: https://github.com/swiftlang/swift/issues/75330) + * - Other HybridObjects need to be wrapped/unwrapped from the Swift TCxx wrapper + * - Throwing methods need to be wrapped with a Result type, as exceptions cannot be propagated to C++ + */ +public class HybridContactsModuleSpecCxx { + /** + * The Swift <> C++ bridge's namespace (`margelo::nitro::contacts::bridge::swift`) + * from `ContactsModule-Swift-Cxx-Bridge.hpp`. + * This contains specialized C++ templates, and C++ helper functions that can be accessed from Swift. + */ + public typealias bridge = margelo.nitro.contacts.bridge.swift + + /** + * Holds an instance of the `HybridContactsModuleSpec` Swift protocol. + */ + private var __implementation: any HybridContactsModuleSpec + + /** + * Create a new `HybridContactsModuleSpecCxx` that wraps the given `HybridContactsModuleSpec`. + * All properties and methods bridge to C++ types. + */ + public init(_ implementation: some HybridContactsModuleSpec) { + self.__implementation = implementation + /* no base class */ + } + + /** + * Get the actual `HybridContactsModuleSpec` instance this class wraps. + */ + @inline(__always) + public func getHybridContactsModuleSpec() -> any HybridContactsModuleSpec { + return __implementation + } + + /** + * Contains a (weak) reference to the C++ HybridObject to cache it. + */ + public var hybridContext: margelo.nitro.HybridContext { + @inline(__always) + get { + return self.__implementation.hybridContext + } + @inline(__always) + set { + self.__implementation.hybridContext = newValue + } + } + + /** + * Get the memory size of the Swift class (plus size of any other allocations) + * so the JS VM can properly track it and garbage-collect the JS object if needed. + */ + @inline(__always) + public var memorySize: Int { + return self.__implementation.memorySize + } + + // Properties + + + // Methods + @inline(__always) + public func getAll(keys: bridge.std__vector_ContactFields_) -> bridge.std__shared_ptr_Promise_std__vector_Contact___ { + do { + let __result = try self.__implementation.getAll(keys: keys.map({ __item in __item })) + return { () -> bridge.std__shared_ptr_Promise_std__vector_Contact___ in + let __promise = bridge.create_std__shared_ptr_Promise_std__vector_Contact___() + __result + .then({ __result in __promise.pointee.resolve({ () -> bridge.std__vector_Contact_ in + var __vector = bridge.create_std__vector_Contact_(__result.count) + for __item in __result { + __vector.push_back(__item) + } + return __vector + }()) }) + .catch({ __error in __promise.pointee.reject(__error.toCpp()) }) + return __promise + }() + } catch { + let __message = "\(error.localizedDescription)" + fatalError("Swift errors can currently not be propagated to C++! See https://github.com/swiftlang/swift/issues/75290 (Error: \(__message))") + } + } +} diff --git a/modules/ContactsNitroModule/nitrogen/generated/ios/swift/StringHolder.swift b/modules/ContactsNitroModule/nitrogen/generated/ios/swift/StringHolder.swift new file mode 100644 index 000000000000..477279082456 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/ios/swift/StringHolder.swift @@ -0,0 +1,35 @@ +/// +/// StringHolder.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `StringHolder`, backed by a C++ struct. + */ +public typealias StringHolder = margelo.nitro.contacts.StringHolder + +public extension StringHolder { + private typealias bridge = margelo.nitro.contacts.bridge.swift + + /** + * Create a new instance of `StringHolder`. + */ + init(value: String) { + self.init(std.string(value)) + } + + var value: String { + @inline(__always) + get { + return String(self.__value) + } + @inline(__always) + set { + self.__value = std.string(newValue) + } + } +} diff --git a/modules/ContactsNitroModule/nitrogen/generated/shared/c++/Contact.hpp b/modules/ContactsNitroModule/nitrogen/generated/shared/c++/Contact.hpp new file mode 100644 index 000000000000..6e4a5bd0c27e --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/shared/c++/Contact.hpp @@ -0,0 +1,96 @@ +/// +/// Contact.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `StringHolder` to properly resolve imports. +namespace margelo::nitro::contacts { struct StringHolder; } + +#include +#include +#include +#include "StringHolder.hpp" + +namespace margelo::nitro::contacts { + + /** + * A struct which can be represented as a JavaScript object (Contact). + */ + struct Contact { + public: + std::optional firstName SWIFT_PRIVATE; + std::optional lastName SWIFT_PRIVATE; + std::optional middleName SWIFT_PRIVATE; + std::optional> phoneNumbers SWIFT_PRIVATE; + std::optional> emailAddresses SWIFT_PRIVATE; + std::optional imageData SWIFT_PRIVATE; + std::optional thumbnailImageData SWIFT_PRIVATE; + + public: + explicit Contact(std::optional firstName, std::optional lastName, std::optional middleName, std::optional> phoneNumbers, std::optional> emailAddresses, std::optional imageData, std::optional thumbnailImageData): firstName(firstName), lastName(lastName), middleName(middleName), phoneNumbers(phoneNumbers), emailAddresses(emailAddresses), imageData(imageData), thumbnailImageData(thumbnailImageData) {} + }; + +} // namespace margelo::nitro::contacts + +namespace margelo::nitro { + + using namespace margelo::nitro::contacts; + + // C++ Contact <> JS Contact (object) + template <> + struct JSIConverter { + static inline Contact fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return Contact( + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "firstName")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "lastName")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "middleName")), + JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, "phoneNumbers")), + JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, "emailAddresses")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "imageData")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "thumbnailImageData")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const Contact& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "firstName", JSIConverter>::toJSI(runtime, arg.firstName)); + obj.setProperty(runtime, "lastName", JSIConverter>::toJSI(runtime, arg.lastName)); + obj.setProperty(runtime, "middleName", JSIConverter>::toJSI(runtime, arg.middleName)); + obj.setProperty(runtime, "phoneNumbers", JSIConverter>>::toJSI(runtime, arg.phoneNumbers)); + obj.setProperty(runtime, "emailAddresses", JSIConverter>>::toJSI(runtime, arg.emailAddresses)); + obj.setProperty(runtime, "imageData", JSIConverter>::toJSI(runtime, arg.imageData)); + obj.setProperty(runtime, "thumbnailImageData", JSIConverter>::toJSI(runtime, arg.thumbnailImageData)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "firstName"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "lastName"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "middleName"))) return false; + if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, "phoneNumbers"))) return false; + if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, "emailAddresses"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "imageData"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "thumbnailImageData"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/modules/ContactsNitroModule/nitrogen/generated/shared/c++/ContactFields.hpp b/modules/ContactsNitroModule/nitrogen/generated/shared/c++/ContactFields.hpp new file mode 100644 index 000000000000..c3e8c115465e --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/shared/c++/ContactFields.hpp @@ -0,0 +1,102 @@ +/// +/// ContactFields.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::contacts { + + /** + * An enum which can be represented as a JavaScript union (ContactFields). + */ + enum class ContactFields { + FIRST_NAME SWIFT_NAME(firstName) = 0, + LAST_NAME SWIFT_NAME(lastName) = 1, + MIDDLE_NAME SWIFT_NAME(middleName) = 2, + PHONE_NUMBERS SWIFT_NAME(phoneNumbers) = 3, + EMAIL_ADDRESSES SWIFT_NAME(emailAddresses) = 4, + IMAGE_DATA SWIFT_NAME(imageData) = 5, + THUMBNAIL_IMAGE_DATA SWIFT_NAME(thumbnailImageData) = 6, + GIVEN_NAME_KEY SWIFT_NAME(givenNameKey) = 7, + } CLOSED_ENUM; + +} // namespace margelo::nitro::contacts + +namespace margelo::nitro { + + using namespace margelo::nitro::contacts; + + // C++ ContactFields <> JS ContactFields (union) + template <> + struct JSIConverter { + static inline ContactFields fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("FIRST_NAME"): return ContactFields::FIRST_NAME; + case hashString("LAST_NAME"): return ContactFields::LAST_NAME; + case hashString("MIDDLE_NAME"): return ContactFields::MIDDLE_NAME; + case hashString("PHONE_NUMBERS"): return ContactFields::PHONE_NUMBERS; + case hashString("EMAIL_ADDRESSES"): return ContactFields::EMAIL_ADDRESSES; + case hashString("IMAGE_DATA"): return ContactFields::IMAGE_DATA; + case hashString("THUMBNAIL_IMAGE_DATA"): return ContactFields::THUMBNAIL_IMAGE_DATA; + case hashString("GIVEN_NAME_KEY"): return ContactFields::GIVEN_NAME_KEY; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum ContactFields - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, ContactFields arg) { + switch (arg) { + case ContactFields::FIRST_NAME: return JSIConverter::toJSI(runtime, "FIRST_NAME"); + case ContactFields::LAST_NAME: return JSIConverter::toJSI(runtime, "LAST_NAME"); + case ContactFields::MIDDLE_NAME: return JSIConverter::toJSI(runtime, "MIDDLE_NAME"); + case ContactFields::PHONE_NUMBERS: return JSIConverter::toJSI(runtime, "PHONE_NUMBERS"); + case ContactFields::EMAIL_ADDRESSES: return JSIConverter::toJSI(runtime, "EMAIL_ADDRESSES"); + case ContactFields::IMAGE_DATA: return JSIConverter::toJSI(runtime, "IMAGE_DATA"); + case ContactFields::THUMBNAIL_IMAGE_DATA: return JSIConverter::toJSI(runtime, "THUMBNAIL_IMAGE_DATA"); + case ContactFields::GIVEN_NAME_KEY: return JSIConverter::toJSI(runtime, "GIVEN_NAME_KEY"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert ContactFields to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("FIRST_NAME"): + case hashString("LAST_NAME"): + case hashString("MIDDLE_NAME"): + case hashString("PHONE_NUMBERS"): + case hashString("EMAIL_ADDRESSES"): + case hashString("IMAGE_DATA"): + case hashString("THUMBNAIL_IMAGE_DATA"): + case hashString("GIVEN_NAME_KEY"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/modules/ContactsNitroModule/nitrogen/generated/shared/c++/HybridContactsModuleSpec.cpp b/modules/ContactsNitroModule/nitrogen/generated/shared/c++/HybridContactsModuleSpec.cpp new file mode 100644 index 000000000000..eba17de8d910 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/shared/c++/HybridContactsModuleSpec.cpp @@ -0,0 +1,21 @@ +/// +/// HybridContactsModuleSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#include "HybridContactsModuleSpec.hpp" + +namespace margelo::nitro::contacts { + + void HybridContactsModuleSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("getAll", &HybridContactsModuleSpec::getAll); + }); + } + +} // namespace margelo::nitro::contacts diff --git a/modules/ContactsNitroModule/nitrogen/generated/shared/c++/HybridContactsModuleSpec.hpp b/modules/ContactsNitroModule/nitrogen/generated/shared/c++/HybridContactsModuleSpec.hpp new file mode 100644 index 000000000000..6c298086f493 --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/shared/c++/HybridContactsModuleSpec.hpp @@ -0,0 +1,68 @@ +/// +/// HybridContactsModuleSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `Contact` to properly resolve imports. +namespace margelo::nitro::contacts { struct Contact; } +// Forward declaration of `ContactFields` to properly resolve imports. +namespace margelo::nitro::contacts { enum class ContactFields; } + +#include +#include +#include "Contact.hpp" +#include "ContactFields.hpp" + +namespace margelo::nitro::contacts { + + using namespace margelo::nitro; + + /** + * An abstract base class for `ContactsModule` + * Inherit this class to create instances of `HybridContactsModuleSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridContactsModule: public HybridContactsModuleSpec { + * public: + * HybridContactsModule(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridContactsModuleSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridContactsModuleSpec(): HybridObject(TAG) { } + + // Destructor + virtual ~HybridContactsModuleSpec() { } + + public: + // Properties + + + public: + // Methods + virtual std::shared_ptr>> getAll(const std::vector& keys) = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "ContactsModule"; + }; + +} // namespace margelo::nitro::contacts diff --git a/modules/ContactsNitroModule/nitrogen/generated/shared/c++/StringHolder.hpp b/modules/ContactsNitroModule/nitrogen/generated/shared/c++/StringHolder.hpp new file mode 100644 index 000000000000..1a666ed1faca --- /dev/null +++ b/modules/ContactsNitroModule/nitrogen/generated/shared/c++/StringHolder.hpp @@ -0,0 +1,68 @@ +/// +/// StringHolder.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © 2024 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include + +namespace margelo::nitro::contacts { + + /** + * A struct which can be represented as a JavaScript object (StringHolder). + */ + struct StringHolder { + public: + std::string value SWIFT_PRIVATE; + + public: + explicit StringHolder(std::string value): value(value) {} + }; + +} // namespace margelo::nitro::contacts + +namespace margelo::nitro { + + using namespace margelo::nitro::contacts; + + // C++ StringHolder <> JS StringHolder (object) + template <> + struct JSIConverter { + static inline StringHolder fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return StringHolder( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "value")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const StringHolder& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "value", JSIConverter::toJSI(runtime, arg.value)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "value"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/modules/ContactsNitroModule/package.json b/modules/ContactsNitroModule/package.json new file mode 100644 index 000000000000..6f70882a2193 --- /dev/null +++ b/modules/ContactsNitroModule/package.json @@ -0,0 +1,103 @@ +{ + "name": "contacts-nitro-module", + "version": "0.0.1", + "main": "src/index", + "react-native": "src/index", + "description": "React Native Contacts Module with Nitro optimization", + "source": "src/index", + "files": [ + "src", + "react-native.config.js", + "lib", + "android/build.gradle", + "android/gradle.properties", + "android/CMakeLists.txt", + "android/src", + "ios/**/*.h", + "ios/**/*.m", + "ios/**/*.mm", + "ios/**/*.cpp", + "ios/**/*.swift", + "app.plugin.js", + "*.podspec", + "README.md" + ], + "scripts": { + "postinstall": "tsc || exit 0;", + "typecheck": "tsc --noEmit", + "clean": "del-cli android/build node_modules/**/android/build lib", + "lint": "eslint \"**/*.{js,ts,tsx}\" --fix", + "lint-ci": "eslint \"**/*.{js,ts,tsx}\" -f @jamesacarr/github-actions", + "typescript": "tsc --noEmit false", + "specs-debug": "bun run --filter=\"**\" typescript && bun nitro-codegen --logLevel=\"debug\"", + "specs": "bun nitro-codegen" + }, + "keywords": [ + "react-native", + "nitro" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/mrousavy/nitro.git" + }, + "author": "Marc Rousavy (https://github.com/mrousavy)", + "license": "MIT", + "bugs": { + "url": "https://github.com/mrousavy/nitro/issues" + }, + "homepage": "https://github.com/mrousavy/nitro#readme", + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "devDependencies": { + "@react-native/eslint-config": "^0.75.2", + "@types/jest": "^29.5.12", + "@types/react": "^18.3.4", + "del-cli": "^5.1.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "nitro-codegen": "0.18.1", + "prettier": "^3.3.3", + "react": "^18.3.1", + "react-native": "0.75.2", + "react-native-nitro-modules": "*", + "typescript": "^5.5.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + }, + "eslintConfig": { + "root": true, + "extends": [ + "@react-native", + "prettier" + ], + "plugins": ["prettier"], + "rules": { + "prettier/prettier": [ + "warn", + { + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false + } + ] + } + }, + "eslintIgnore": [ + "node_modules/", + "lib/" + ], + "prettier": { + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false, + "semi": false + } +} diff --git a/modules/ContactsNitroModule/src/ContactsModule.nitro.ts b/modules/ContactsNitroModule/src/ContactsModule.nitro.ts new file mode 100644 index 000000000000..8df839f550e8 --- /dev/null +++ b/modules/ContactsNitroModule/src/ContactsModule.nitro.ts @@ -0,0 +1,29 @@ +import type { HybridObject } from 'react-native-nitro-modules' + +interface StringHolder { + value: string +} + +export interface Contact { + firstName?: string + lastName?: string + middleName?: string + phoneNumbers?: StringHolder[] + emailAddresses?: StringHolder[] + imageData?: string + thumbnailImageData?: string +} +export type ContactFields = + | 'FIRST_NAME' + | 'LAST_NAME' + | 'MIDDLE_NAME' + | 'PHONE_NUMBERS' + | 'EMAIL_ADDRESSES' + | 'IMAGE_DATA' + | 'THUMBNAIL_IMAGE_DATA' + | 'GIVEN_NAME_KEY' + +export interface ContactsModule + extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> { + getAll(keys: ContactFields[]): Promise +} diff --git a/modules/ContactsNitroModule/src/index.ts b/modules/ContactsNitroModule/src/index.ts new file mode 100644 index 000000000000..acc7c7c1fb76 --- /dev/null +++ b/modules/ContactsNitroModule/src/index.ts @@ -0,0 +1,8 @@ +import type { ContactsModule } from './ContactsModule.nitro' +import type { Contact } from './ContactsModule.nitro' +import { NitroModules } from 'react-native-nitro-modules' + +export const ContactsNitroModule = + NitroModules.createHybridObject('ContactsModule') + +export type { Contact } diff --git a/modules/ContactsNitroModule/tsconfig.json b/modules/ContactsNitroModule/tsconfig.json new file mode 100644 index 000000000000..e30dc47ac169 --- /dev/null +++ b/modules/ContactsNitroModule/tsconfig.json @@ -0,0 +1,29 @@ +{ + "include": ["src"], + "compilerOptions": { + "composite": true, + "outDir": "lib", + "rootDir": "src", + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "lib": ["esnext"], + "module": "esnext", + "moduleResolution": "node", + "noEmit": false, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitUseStrict": false, + "noStrictGenericChecks": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "esnext", + "verbatimModuleSyntax": true + } +} diff --git a/package-lock.json b/package-lock.json index ab29133c143e..b69384f322e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "9.0.74-0", "hasInstallScript": true, "license": "MIT", + "workspaces": [ + "modules/ContactsNitroModule" + ], "dependencies": { "@dotlottie/react-player": "^1.6.3", "@expensify/react-native-live-markdown": "0.1.187", @@ -46,6 +49,7 @@ "awesome-phonenumber": "^5.4.0", "babel-polyfill": "^6.26.0", "canvas-size": "^1.2.6", + "contacts-nitro-module": "./modules/ContactsNitroModule", "core-js": "^3.32.0", "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", @@ -95,6 +99,7 @@ "react-native-launch-arguments": "^4.0.2", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", + "react-native-nitro-modules": "^0.18.1", "react-native-onyx": "2.0.82", "react-native-pager-view": "6.5.1", "react-native-pdf": "6.7.3", @@ -275,10 +280,145 @@ "webpack-dev-server": "^5.0.4", "webpack-merge": "^5.8.0", "xlsx": "file:vendor/xlsx-0.20.3.tgz" + } + }, + "modules/ContactsNitroModule": { + "name": "contacts-nitro-module", + "version": "0.0.1", + "hasInstallScript": true, + "license": "MIT", + "devDependencies": { + "@react-native/eslint-config": "^0.75.2", + "@types/jest": "^29.5.12", + "@types/react": "^18.3.4", + "del-cli": "^5.1.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "nitro-codegen": "0.18.1", + "prettier": "^3.3.3", + "react": "^18.3.1", + "react-native": "0.75.2", + "react-native-nitro-modules": "*", + "typescript": "^5.5.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "modules/ContactsNitroModule/node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "modules/ContactsNitroModule/node_modules/@types/react": { + "version": "18.3.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", + "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "modules/ContactsNitroModule/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "modules/ContactsNitroModule/node_modules/eslint-plugin-prettier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "modules/ContactsNitroModule/node_modules/nitro-codegen": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/nitro-codegen/-/nitro-codegen-0.18.1.tgz", + "integrity": "sha512-gDOHIIFFY89Ibo/Q8Dlzx4Rk9fCaGnby4Er5Dh1xV4J5hMqTfqo2VjG+RxScdUTYy/SKOc0UsB2faQybs5+GDw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "react-native-nitro-modules": "^0.18.1", + "ts-morph": "^24.0.0", + "yargs": "^17.7.2", + "zod": "^3.23.8" + }, + "bin": { + "nitro-codegen": "lib/index.js" + } + }, + "modules/ContactsNitroModule/node_modules/prettier": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.1.tgz", + "integrity": "sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" }, "engines": { - "node": "20.18.0", - "npm": "10.8.2" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "modules/ContactsNitroModule/node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" } }, "node_modules/@actions/core": { @@ -7197,6 +7337,19 @@ "node": ">=14" } }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.15.tgz", @@ -9307,6 +9460,83 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@react-native/eslint-config": { + "version": "0.75.4", + "resolved": "https://registry.npmjs.org/@react-native/eslint-config/-/eslint-config-0.75.4.tgz", + "integrity": "sha512-3KBHYwp4HnBdaCFx9KDPvQY+sGrv5fHX2qDkXGKmN3uYBz+zfnMQXTiht6OuBbWULUF0y0o8m+uH1yYAn/V9mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.20.0", + "@babel/eslint-parser": "^7.20.0", + "@react-native/eslint-plugin": "0.75.4", + "@typescript-eslint/eslint-plugin": "^7.1.1", + "@typescript-eslint/parser": "^7.1.1", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-ft-flow": "^2.0.1", + "eslint-plugin-jest": "^27.9.0", + "eslint-plugin-react": "^7.30.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-native": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": ">=8", + "prettier": ">=2" + } + }, + "node_modules/@react-native/eslint-config/node_modules/eslint-config-prettier": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/@react-native/eslint-config/node_modules/eslint-plugin-jest": { + "version": "27.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.9.0.tgz", + "integrity": "sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^5.10.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0 || ^6.0.0 || ^7.0.0", + "eslint": "^7.0.0 || ^8.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/@react-native/eslint-plugin": { + "version": "0.75.4", + "resolved": "https://registry.npmjs.org/@react-native/eslint-plugin/-/eslint-plugin-0.75.4.tgz", + "integrity": "sha512-1kEZzC8UKi3baHnH7tBVCNpF4aoAmT7g7hEa5/rtZ+Z7vcpaxeY6wjNYt3j02Z9n310yX0NKDJox30CqvzEvsg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@react-native/gradle-plugin": { "version": "0.75.2", "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.75.2.tgz", @@ -13103,6 +13333,44 @@ "node": ">=10.13.0" } }, + "node_modules/@ts-morph/common": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.25.0.tgz", + "integrity": "sha512-kMnZz+vGGHi4GoHnLmMhGNjm44kGtKUXGnOvrKmMwAuvNjM/PgKVGfUnL7IDvK7Jb2QQ82jq3Zmp04Gy+r3Dkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^9.0.4", + "path-browserify": "^1.0.1", + "tinyglobby": "^0.2.9" + } + }, + "node_modules/@ts-morph/common/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@ts-morph/common/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "dev": true, @@ -13621,6 +13889,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ms": { "version": "0.7.34", "dev": true, @@ -13662,6 +13937,13 @@ "@types/node": "*" } }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/parse-json": { "version": "4.0.0", "dev": true, @@ -15494,6 +15776,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/asap": { "version": "2.0.6", "license": "MIT" @@ -17288,6 +17580,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/camelcase-keys": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-7.0.2.tgz", + "integrity": "sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.3.0", + "map-obj": "^4.1.0", + "quick-lru": "^5.1.1", + "type-fest": "^1.2.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/camelize": { "version": "1.0.1", "license": "MIT", @@ -17647,6 +17971,13 @@ "node": ">= 0.12.0" } }, + "node_modules/code-block-writer": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "dev": true, + "license": "MIT" + }, "node_modules/collect-v8-coverage": { "version": "1.0.1", "dev": true, @@ -18138,6 +18469,10 @@ "dev": true, "license": "MIT" }, + "node_modules/contacts-nitro-module": { + "resolved": "modules/ContactsNitroModule", + "link": true + }, "node_modules/content-disposition": { "version": "0.5.4", "dev": true, @@ -18852,6 +19187,56 @@ } } }, + "node_modules/decamelize": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-5.0.1.tgz", + "integrity": "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decimal.js": { "version": "10.4.3", "dev": true, @@ -19055,6 +19440,184 @@ "node": ">=6" } }, + "node_modules/del-cli": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/del-cli/-/del-cli-5.1.0.tgz", + "integrity": "sha512-xwMeh2acluWeccsfzE7VLsG3yTr7nWikbfw+xhMnpRrF15pGSkw+3/vJZWlGoE4I86UiLRNHicmKt4tkIX9Jtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "del": "^7.1.0", + "meow": "^10.1.3" + }, + "bin": { + "del": "cli.js", + "del-cli": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del-cli/node_modules/aggregate-error": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-4.0.1.tgz", + "integrity": "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^4.0.0", + "indent-string": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del-cli/node_modules/clean-stack": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-4.2.0.tgz", + "integrity": "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del-cli/node_modules/del": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/del/-/del-7.1.0.tgz", + "integrity": "sha512-v2KyNk7efxhlyHpjEvfyxaAihKKK0nWCuf6ZtqZcFFpQRG0bJ12Qsr0RpvsICMjAAZ8DOVCxrlqpxISlMHC4Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "globby": "^13.1.2", + "graceful-fs": "^4.2.10", + "is-glob": "^4.0.3", + "is-path-cwd": "^3.0.0", + "is-path-inside": "^4.0.0", + "p-map": "^5.5.0", + "rimraf": "^3.0.2", + "slash": "^4.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del-cli/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del-cli/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del-cli/node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del-cli/node_modules/is-path-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-3.0.0.tgz", + "integrity": "sha512-kyiNFFLU0Ampr6SDZitD/DwUo4Zs1nSdnygUBqsu3LooL00Qvb5j+UnvApUn/TTj1J3OuE6BTdQ5rudKmU2ZaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del-cli/node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del-cli/node_modules/p-map": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-5.5.0.tgz", + "integrity": "sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del-cli/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/del/node_modules/array-union": { "version": "1.0.2", "dev": true, @@ -23387,6 +23950,16 @@ "node": ">=0.10.0" } }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/has": { "version": "1.0.3", "dev": true, @@ -24778,6 +25351,16 @@ "node": ">=6" } }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-plain-object": { "version": "5.0.0", "dev": true, @@ -28422,6 +29005,19 @@ "tmpl": "1.0.5" } }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/map-or-similar": { "version": "1.5.0", "dev": true, @@ -28628,6 +29224,92 @@ "resolved": "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz", "integrity": "sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==" }, + "node_modules/meow": { + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/meow/-/meow-10.1.5.tgz", + "integrity": "sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimist": "^1.2.2", + "camelcase-keys": "^7.0.0", + "decamelize": "^5.0.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.2", + "read-pkg-up": "^8.0.0", + "redent": "^4.0.0", + "trim-newlines": "^4.0.2", + "type-fest": "^1.2.2", + "yargs-parser": "^20.2.9" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/redent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", + "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^5.0.0", + "strip-indent": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/strip-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", + "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -29223,6 +29905,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/minipass": { "version": "3.3.6", "license": "ISC", @@ -29693,6 +30390,22 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "license": "MIT", @@ -32149,6 +32862,17 @@ "react-native": ">=0.65.0" } }, + "node_modules/react-native-nitro-modules": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/react-native-nitro-modules/-/react-native-nitro-modules-0.18.1.tgz", + "integrity": "sha512-F1PA92N8Qv/0I3gKnUFU/eP2C16TSSWwuWuUJnVXX4pCrZztP6BHSvRAZj9WpwxytoKICjwgeVk8K//kvZDZAg==", + "hasInstallScript": true, + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-onyx": { "version": "2.0.82", "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.82.tgz", @@ -32948,6 +33672,69 @@ "node": ">=6" } }, + "node_modules/read-pkg": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-6.0.0.tgz", + "integrity": "sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^3.0.2", + "parse-json": "^5.2.0", + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-8.0.0.tgz", + "integrity": "sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^5.0.0", + "read-pkg": "^6.0.0", + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "license": "MIT", @@ -34345,6 +35132,17 @@ "version": "0.0.2", "dev": true }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, "node_modules/spdx-exceptions": { "version": "2.3.0", "dev": true, @@ -35057,6 +35855,23 @@ "dev": true, "license": "MIT" }, + "node_modules/synckit": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/tabbable": { "version": "6.2.0", "license": "MIT" @@ -35490,6 +36305,48 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz", + "integrity": "sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tinyqueue": { "version": "2.0.3", "license": "ISC" @@ -35605,6 +36462,19 @@ "tree-kill": "cli.js" } }, + "node_modules/trim-newlines": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.1.1.tgz", + "integrity": "sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/trim-right": { "version": "1.0.1", "license": "MIT", @@ -35709,12 +36579,15 @@ "node": ">=10" } }, - "node_modules/ts-jest/node_modules/yargs-parser": { - "version": "21.1.1", + "node_modules/ts-morph": { + "version": "24.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-24.0.0.tgz", + "integrity": "sha512-2OAOg/Ob5yx9Et7ZX4CvTCc0UFoZHwLEJ+dpDPSUi5TgwwlTlX47w+iFRrEwzUZwYACjq83cgjS/Da50Ga37uw==", "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.25.0", + "code-block-writer": "^13.0.3" } }, "node_modules/ts-node": { @@ -36490,6 +37363,17 @@ "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", "integrity": "sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==" }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/validate-npm-package-name": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", @@ -37545,18 +38429,20 @@ "node": ">=12" } }, - "node_modules/yargs/node_modules/y18n": { - "version": "5.0.8", + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "license": "ISC", "engines": { - "node": ">=10" + "node": ">=12" } }, - "node_modules/yargs/node_modules/yargs-parser": { - "version": "21.1.1", + "node_modules/yargs/node_modules/y18n": { + "version": "5.0.8", "license": "ISC", "engines": { - "node": ">=12" + "node": ">=10" } }, "node_modules/yauzl": { diff --git a/package.json b/package.json index 4cd6fc679f37..8907fae70710 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure", "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc", "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=0 --cache --cache-location=node_modules/.cache/eslint", - "lint-changed": "NODE_OPTIONS=--max_old_space_size=8192 eslint --max-warnings=0 --config ./.eslintrc.changed.js $(git diff --diff-filter=AM --name-only origin/main HEAD -- \"*.ts\" \"*.tsx\")", + "lint-changed": "NODE_OPTIONS=--max_old_space_size=8192 eslint --max-warnings=0 --config ./.eslintrc.changed.js $(git diff --diff-filter=AM --name-only origin/main HEAD -- \"*.ts\" \"*.tsx\" \":!modules/**\")", "lint-watch": "npx eslint-watch --watch --changed", "shellcheck": "./scripts/shellCheck.sh", "prettier": "prettier --write .", @@ -109,6 +109,7 @@ "awesome-phonenumber": "^5.4.0", "babel-polyfill": "^6.26.0", "canvas-size": "^1.2.6", + "contacts-nitro-module": "./modules/ContactsNitroModule", "core-js": "^3.32.0", "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", @@ -158,6 +159,7 @@ "react-native-launch-arguments": "^4.0.2", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", + "react-native-nitro-modules": "^0.18.1", "react-native-onyx": "2.0.82", "react-native-pager-view": "6.5.1", "react-native-pdf": "6.7.3", @@ -374,17 +376,7 @@ ] } }, - "electronmon": { - "patterns": [ - "!src/**", - "!ios/**", - "!android/**", - "!tests/**", - "*.test.*" - ] - }, - "engines": { - "node": "20.18.0", - "npm": "10.8.2" - } + "workspaces": [ + "modules/ContactsNitroModule" + ] } diff --git a/react-native.config.js b/react-native.config.js index a8c2436688e4..5e6da118d7c7 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -1,7 +1,15 @@ +const path = require('path'); +const pak = require('./modules/ContactsNitroModule/package.json'); + module.exports = { project: { ios: {sourceDir: process.env.PROJECT_ROOT_PATH + 'ios'}, android: {sourceDir: process.env.PROJECT_ROOT_PATH + 'android'}, }, assets: ['./assets/fonts/native'], + dependencies: { + [pak.name]: { + root: path.join(__dirname, 'modules', 'ContactsNitroModule'), + }, + }, }; diff --git a/src/CONST.ts b/src/CONST.ts index 2d38d26d8820..2be5259d6ca3 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -6399,6 +6399,14 @@ const CONST = { }, }, + DEVICE_CONTACT: { + FIRST_NAME: 'FIRST_NAME', + LAST_NAME: 'LAST_NAME', + PHONE_NUMBERS: 'PHONE_NUMBERS', + EMAIL_ADDRESSES: 'EMAIL_ADDRESSES', + IMAGE_DATA: 'IMAGE_DATA', + }, + HYBRID_APP: { REORDERING_REACT_NATIVE_ACTIVITY_TO_FRONT: 'reorderingReactNativeActivityToFront', }, diff --git a/src/components/ContactPermissionModal/index.native.tsx b/src/components/ContactPermissionModal/index.native.tsx new file mode 100644 index 000000000000..825c8bc4afbe --- /dev/null +++ b/src/components/ContactPermissionModal/index.native.tsx @@ -0,0 +1,73 @@ +import React, {useEffect, useState} from 'react'; +import {InteractionManager} from 'react-native'; +import {RESULTS} from 'react-native-permissions'; +import ConfirmModal from '@components/ConfirmModal'; +import * as Illustrations from '@components/Icon/Illustrations'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getContactPermission} from '@libs/ContactPermission'; +import type {ContactPermissionModalProps} from './types'; + +function ContactPermissionModal({startPermissionFlow, resetPermissionFlow, onDeny, onGrant}: ContactPermissionModalProps) { + const [isModalVisible, setIsModalVisible] = useState(false); + + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + useEffect(() => { + if (!startPermissionFlow) { + return; + } + getContactPermission().then((status) => { + if (status === RESULTS.GRANTED || status === RESULTS.LIMITED) { + return onGrant(); + } + if (status === RESULTS.BLOCKED) { + return; + } + setIsModalVisible(true); + }); + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- We only want to run this effect when startPermissionFlow changes + }, [startPermissionFlow]); + + const handleGrantPermission = () => { + setIsModalVisible(false); + InteractionManager.runAfterInteractions(onGrant); + }; + + const handleDenyPermission = () => { + onDeny(RESULTS.DENIED); + setIsModalVisible(false); + }; + + const handleCloseModal = () => { + setIsModalVisible(false); + resetPermissionFlow(); + }; + + return ( + + ); +} + +ContactPermissionModal.displayName = 'ContactPermissionModal'; + +export default ContactPermissionModal; diff --git a/src/components/ContactPermissionModal/index.tsx b/src/components/ContactPermissionModal/index.tsx new file mode 100644 index 000000000000..3f7e25bac590 --- /dev/null +++ b/src/components/ContactPermissionModal/index.tsx @@ -0,0 +1,10 @@ +import type {ContactPermissionModalProps} from './types'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function ContactPermissionModal(props: ContactPermissionModalProps) { + return null; +} + +ContactPermissionModal.displayName = 'ContactPermissionModal'; + +export default ContactPermissionModal; diff --git a/src/components/ContactPermissionModal/types.ts b/src/components/ContactPermissionModal/types.ts new file mode 100644 index 000000000000..5c831410656f --- /dev/null +++ b/src/components/ContactPermissionModal/types.ts @@ -0,0 +1,19 @@ +import type {PermissionStatus} from 'react-native-permissions'; + +type ContactPermissionModalProps = { + /** A callback to call when the permission has been granted */ + onGrant: () => void; + + /** A callback to call when the permission has been denied */ + onDeny: (permission: PermissionStatus) => void; + + /** Should start the permission flow? */ + startPermissionFlow: boolean; + + /** Reset the permission flow */ + resetPermissionFlow: () => void; +}; + +export default {}; + +export type {ContactPermissionModalProps}; diff --git a/src/languages/en.ts b/src/languages/en.ts index 7088d9df8a51..578282a3ba39 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -492,6 +492,13 @@ const translations = { allowPermission: 'allow location access in settings', tryAgain: 'and try again.', }, + contact: { + importContacts: 'Import contacts', + importContactsTitle: 'Import your contacts', + importContactsText: 'Import contacts from your phone so your favorite people are always a tap away.', + importContactsExplanation: 'so your favorite people are always a tap away.', + importContactsNativeText: 'Just one more step! Give us the green light to import your contacts.', + }, anonymousReportFooter: { logoTagline: 'Join the discussion.', }, diff --git a/src/languages/es.ts b/src/languages/es.ts index 47e11bf716ac..a411cb081f87 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -487,6 +487,13 @@ const translations = { allowPermission: 'habilita el permiso de ubicación en la configuración', tryAgain: 'e inténtalo de nuevo.', }, + contact: { + importContacts: 'Importar contactos', + importContactsTitle: 'Importa tus contactos', + importContactsText: 'Importa contactos desde tu teléfono para que tus personas favoritas siempre estén a un toque de distancia.', + importContactsExplanation: 'para que tus personas favoritas estén siempre a un toque de distancia.', + importContactsNativeText: '¡Solo un paso más! Danos luz verde para importar tus contactos.', + }, anonymousReportFooter: { logoTagline: 'Únete a la discusión.', }, diff --git a/src/libs/ContactImport/index.native.tsx b/src/libs/ContactImport/index.native.tsx new file mode 100644 index 000000000000..564bbba6805f --- /dev/null +++ b/src/libs/ContactImport/index.native.tsx @@ -0,0 +1,32 @@ +import {ContactsNitroModule} from 'contacts-nitro-module'; +import type {Contact} from 'contacts-nitro-module'; +import {RESULTS} from 'react-native-permissions'; +import type {PermissionStatus} from 'react-native-permissions'; +import {requestContactPermission} from '@libs/ContactPermission'; +import CONST from '@src/CONST'; +import type {ContactImportResult} from './types'; + +function contactImport(): Promise { + let permissionStatus: PermissionStatus = RESULTS.UNAVAILABLE; + + return requestContactPermission() + .then((response: PermissionStatus) => { + permissionStatus = response; + if (response === RESULTS.GRANTED) { + return ContactsNitroModule.getAll([ + CONST.DEVICE_CONTACT.FIRST_NAME, + CONST.DEVICE_CONTACT.LAST_NAME, + CONST.DEVICE_CONTACT.PHONE_NUMBERS, + CONST.DEVICE_CONTACT.EMAIL_ADDRESSES, + CONST.DEVICE_CONTACT.IMAGE_DATA, + ]); + } + return [] as Contact[]; + }) + .then((deviceContacts) => ({ + contactList: Array.isArray(deviceContacts) ? deviceContacts : [], + permissionStatus, + })); +} + +export default contactImport; diff --git a/src/libs/ContactImport/index.tsx b/src/libs/ContactImport/index.tsx new file mode 100644 index 000000000000..b3fe3dc27868 --- /dev/null +++ b/src/libs/ContactImport/index.tsx @@ -0,0 +1,11 @@ +import {RESULTS} from 'react-native-permissions'; +import type {ContactImportResult} from './types'; + +const contactImport = (): Promise => { + return Promise.resolve({ + contactList: [], + permissionStatus: RESULTS.UNAVAILABLE, + }); +}; + +export default contactImport; diff --git a/src/libs/ContactImport/types.ts b/src/libs/ContactImport/types.ts new file mode 100644 index 000000000000..8247bdf8e2ec --- /dev/null +++ b/src/libs/ContactImport/types.ts @@ -0,0 +1,20 @@ +import type {PermissionStatus} from 'react-native-permissions'; + +type StringHolder = { + value: string; +}; + +type ContactImportResult = { + contactList: DeviceContact[] | []; + permissionStatus: PermissionStatus; +}; + +type DeviceContact = { + firstName?: string; + lastName?: string; + emailAddresses?: StringHolder[]; + phoneNumbers?: Array<{value: string}>; + imageData?: string; +}; + +export type {StringHolder, ContactImportResult, DeviceContact}; diff --git a/src/libs/ContactPermission/index.android.ts b/src/libs/ContactPermission/index.android.ts new file mode 100644 index 000000000000..8ef0eace7135 --- /dev/null +++ b/src/libs/ContactPermission/index.android.ts @@ -0,0 +1,11 @@ +import {check, PERMISSIONS, request} from 'react-native-permissions'; + +function requestContactPermission() { + return request(PERMISSIONS.ANDROID.READ_CONTACTS); +} + +function getContactPermission() { + return check(PERMISSIONS.ANDROID.READ_CONTACTS); +} + +export {requestContactPermission, getContactPermission}; diff --git a/src/libs/ContactPermission/index.ios.ts b/src/libs/ContactPermission/index.ios.ts new file mode 100644 index 000000000000..590373c9bd7d --- /dev/null +++ b/src/libs/ContactPermission/index.ios.ts @@ -0,0 +1,11 @@ +import {check, PERMISSIONS, request} from 'react-native-permissions'; + +function requestContactPermission() { + return request(PERMISSIONS.IOS.CONTACTS); +} + +function getContactPermission() { + return check(PERMISSIONS.IOS.CONTACTS); +} + +export {requestContactPermission, getContactPermission}; diff --git a/src/libs/ContactPermission/index.ts b/src/libs/ContactPermission/index.ts new file mode 100644 index 000000000000..0216a5022dda --- /dev/null +++ b/src/libs/ContactPermission/index.ts @@ -0,0 +1,16 @@ +import {RESULTS} from 'react-native-permissions'; +import type {PermissionStatus} from 'react-native-permissions'; + +function requestContactPermission(): Promise { + return new Promise((resolve) => { + resolve(RESULTS.GRANTED); + }); +} + +function getContactPermission(): Promise { + return new Promise((resolve) => { + resolve(RESULTS.GRANTED); + }); +} + +export {requestContactPermission, getContactPermission}; diff --git a/src/libs/ContactUtils.ts b/src/libs/ContactUtils.ts new file mode 100644 index 000000000000..5769e6cde2a4 --- /dev/null +++ b/src/libs/ContactUtils.ts @@ -0,0 +1,50 @@ +import CONST from '@src/CONST'; +import type {PersonalDetails} from '@src/types/onyx'; +import type {DeviceContact, StringHolder} from './ContactImport/types'; +import * as OptionsListUtils from './OptionsListUtils'; +import {getAvatarForContact} from './RandomAvatarUtils'; + +function sortEmailObjects(emails?: StringHolder[]): string[] { + if (!emails?.length) { + return []; + } + + const expensifyDomain = CONST.EMAIL.EXPENSIFY_EMAIL_DOMAIN.toLowerCase(); + + return emails + .filter((email) => email?.value) + .map((email) => email.value) + .sort((a, b) => { + const isExpensifyA = a.toLowerCase().includes(expensifyDomain); + const isExpensifyB = b.toLowerCase().includes(expensifyDomain); + + // Prioritize Expensify emails, then sort alphabetically + return isExpensifyA !== isExpensifyB ? Number(isExpensifyB) - Number(isExpensifyA) : a.localeCompare(b); + }); +} + +const getContacts = (deviceContacts: DeviceContact[] | []): Array> => { + return deviceContacts + .map((contact) => { + const email = sortEmailObjects(contact?.emailAddresses ?? [])?.at(0) ?? ''; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const avatarSource = (contact?.imageData || getAvatarForContact(`${contact?.firstName}${email}${contact?.lastName}`)) ?? ''; + const phoneNumber = contact.phoneNumbers?.[0]?.value ?? ''; + const firstName = contact?.firstName ?? ''; + const lastName = contact?.lastName ?? ''; + + return OptionsListUtils.getUserToInviteContactOption({ + selectedOptions: [], + optionsToExclude: [], + searchValue: email || phoneNumber || firstName || '', + firstName, + lastName, + email, + phone: phoneNumber, + avatar: avatarSource, + }); + }) + .filter((contact): contact is OptionsListUtils.SearchOption => contact !== null); +}; + +export default getContacts; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index fc8c16754cb4..9b3891d91177 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -115,6 +115,11 @@ type GetUserToInviteConfig = { searchValue: string; optionsToExclude?: Array>; reportActions?: ReportActions; + firstName?: string; + lastName?: string; + email?: string; + phone?: string; + avatar?: UserUtils.AvatarSource; shouldAcceptName?: boolean; } & Pick; @@ -351,25 +356,29 @@ function getParticipantsOption(participant: ReportUtils.OptionData | Participant const detail = getPersonalDetailsForAccountIDs([participant.accountID ?? -1], personalDetails)[participant.accountID ?? -1]; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const login = detail?.login || participant.login || ''; - const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(detail, LocalePhoneNumber.formatPhoneNumber(login) || participant.text); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const displayName = participant?.displayName || PersonalDetailsUtils.getDisplayNameOrDefault(detail, LocalePhoneNumber.formatPhoneNumber(login) || participant.text); return { keyForList: String(detail?.accountID), login, accountID: detail?.accountID ?? -1, text: displayName, - firstName: detail?.firstName ?? '', - lastName: detail?.lastName ?? '', + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + firstName: (detail?.firstName || ('firstName' in participant ? participant.firstName : '')) ?? '', + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + lastName: (detail?.lastName || ('lastName' in participant ? participant.lastName : '')) ?? '', alternateText: LocalePhoneNumber.formatPhoneNumber(login) || displayName, icons: [ { - source: detail?.avatar ?? FallbackAvatar, + source: ('avatar' in participant ? participant.avatar : detail?.avatar) ?? FallbackAvatar, name: login, type: CONST.ICON_TYPE_AVATAR, id: detail?.accountID, }, ], - phoneNumber: detail?.phoneNumber ?? '', + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + phoneNumber: (detail?.phoneNumber || participant?.phoneNumber) ?? '', selected: participant.selected, isSelected: participant.selected, searchText: participant.searchText ?? undefined, @@ -1083,6 +1092,101 @@ function getUserToInviteOption({ return userToInvite; } +function getUserToInviteContactOption({ + searchValue, + optionsToExclude = [], + selectedOptions = [], + firstName, + lastName, + email, + phone, + avatar, +}: GetUserToInviteConfig): SearchOption | null { + // If email is provided, use it as the primary identifier + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const effectiveSearchValue = email || searchValue; + + // Handle phone number parsing for either provided phone or searchValue + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const phoneToCheck = phone || searchValue; + const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(phoneToCheck))); + + const isCurrentUserLogin = isCurrentUser({login: effectiveSearchValue} as PersonalDetails); + const isInSelectedOption = selectedOptions.some((option) => 'login' in option && option.login === effectiveSearchValue); + + // Validate email (either provided email or searchValue) + const isValidEmail = Str.isValidEmail(effectiveSearchValue) && !Str.isDomainEmail(effectiveSearchValue) && !Str.endsWith(effectiveSearchValue, CONST.SMS.DOMAIN); + + const isValidPhoneNumber = parsedPhoneNumber.possible && Str.isValidE164Phone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? '')); + + const isInOptionToExclude = + optionsToExclude.findIndex((optionToExclude) => 'login' in optionToExclude && optionToExclude.login === PhoneNumber.addSMSDomainIfPhoneNumber(effectiveSearchValue).toLowerCase()) !== + -1; + + if (!effectiveSearchValue || isCurrentUserLogin || isInSelectedOption || (!isValidEmail && !isValidPhoneNumber) || isInOptionToExclude) { + return null; + } + + // Generates an optimistic account ID for new users not yet saved in Onyx + const optimisticAccountID = UserUtils.generateAccountID(effectiveSearchValue); + + // Construct display name if firstName/lastName are provided + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const displayName = firstName && lastName ? `${firstName} ${lastName}` : firstName || lastName || effectiveSearchValue; + + // Create the base user details that will be used in both item and participantsList + const userDetails = { + accountID: optimisticAccountID, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + avatar: avatar || FallbackAvatar, + firstName: firstName ?? '', + lastName: lastName ?? '', + displayName, + login: effectiveSearchValue, + pronouns: '', + phoneNumber: phone ?? '', + validated: true, + }; + + const userToInvite = { + item: userDetails, + text: displayName, + alternateText: displayName !== effectiveSearchValue ? effectiveSearchValue : undefined, + brickRoadIndicator: null, + icons: [ + { + source: userDetails.avatar, + type: CONST.ICON_TYPE_AVATAR, + name: effectiveSearchValue, + id: optimisticAccountID, + }, + ], + tooltipText: null, + participantsList: [userDetails], + accountID: optimisticAccountID, + login: effectiveSearchValue, + reportID: '', + phoneNumber: phone ?? '', + hasDraftComment: false, + keyForList: optimisticAccountID.toString(), + isDefaultRoom: false, + isPinned: false, + isWaitingOnBankAccount: false, + isIOUReportOwner: false, + iouReportAmount: 0, + isChatRoom: false, + shouldShowSubscript: false, + isPolicyExpenseChat: false, + isOwnPolicyExpenseChat: false, + isExpenseReport: false, + lastMessageText: '', + isBold: true, + isOptimisticAccount: true, + }; + + return userToInvite; +} + /** * Options are reports and personal details. This function filters out the options that are not valid to be displayed. */ @@ -1830,6 +1934,7 @@ export { getFirstKeyForList, canCreateOptimisticPersonalDetailOption, getUserToInviteOption, + getUserToInviteContactOption, getPersonalDetailSearchTerms, getCurrentUserSearchTerms, getEmptyOptions, @@ -1839,4 +1944,4 @@ export { hasReportErrors, }; -export type {Section, SectionBase, MemberForList, Options, OptionList, SearchOption, PayeePersonalDetails, Option, OptionTree}; +export type {MemberForList, Options, OptionList, SearchOption, PayeePersonalDetails, Option, OptionTree, GetUserToInviteConfig, Section, SectionBase}; diff --git a/src/libs/RandomAvatarUtils.ts b/src/libs/RandomAvatarUtils.ts new file mode 100644 index 000000000000..2aad23b13583 --- /dev/null +++ b/src/libs/RandomAvatarUtils.ts @@ -0,0 +1,99 @@ +import type {FC} from 'react'; +import type {SvgProps} from 'react-native-svg'; +import Avatar1 from '@assets/images/avatars/user/default-avatar_1.svg'; +import Avatar2 from '@assets/images/avatars/user/default-avatar_2.svg'; +import Avatar3 from '@assets/images/avatars/user/default-avatar_3.svg'; +import Avatar4 from '@assets/images/avatars/user/default-avatar_4.svg'; +import Avatar5 from '@assets/images/avatars/user/default-avatar_5.svg'; +import Avatar6 from '@assets/images/avatars/user/default-avatar_6.svg'; +import Avatar7 from '@assets/images/avatars/user/default-avatar_7.svg'; +import Avatar8 from '@assets/images/avatars/user/default-avatar_8.svg'; +import Avatar9 from '@assets/images/avatars/user/default-avatar_9.svg'; +import Avatar10 from '@assets/images/avatars/user/default-avatar_10.svg'; +import Avatar11 from '@assets/images/avatars/user/default-avatar_11.svg'; +import Avatar12 from '@assets/images/avatars/user/default-avatar_12.svg'; +import Avatar13 from '@assets/images/avatars/user/default-avatar_13.svg'; +import Avatar14 from '@assets/images/avatars/user/default-avatar_14.svg'; +import Avatar15 from '@assets/images/avatars/user/default-avatar_15.svg'; +import Avatar16 from '@assets/images/avatars/user/default-avatar_16.svg'; +import Avatar17 from '@assets/images/avatars/user/default-avatar_17.svg'; +import Avatar18 from '@assets/images/avatars/user/default-avatar_18.svg'; +import Avatar19 from '@assets/images/avatars/user/default-avatar_19.svg'; +import Avatar20 from '@assets/images/avatars/user/default-avatar_20.svg'; +import Avatar21 from '@assets/images/avatars/user/default-avatar_21.svg'; +import Avatar22 from '@assets/images/avatars/user/default-avatar_22.svg'; +import Avatar23 from '@assets/images/avatars/user/default-avatar_23.svg'; +import Avatar24 from '@assets/images/avatars/user/default-avatar_24.svg'; + +type AvatarComponent = FC; +type AvatarArray = readonly AvatarComponent[]; + +const avatars: AvatarArray = [ + Avatar1, + Avatar2, + Avatar3, + Avatar4, + Avatar5, + Avatar6, + Avatar7, + Avatar8, + Avatar9, + Avatar10, + Avatar11, + Avatar12, + Avatar13, + Avatar14, + Avatar15, + Avatar16, + Avatar17, + Avatar18, + Avatar19, + Avatar20, + Avatar21, + Avatar22, + Avatar23, + Avatar24, +] as const; + +const AVATAR_LENGTH: number = avatars.length; +const DEFAULT_AVATAR: AvatarComponent = Avatar1; + +// Prime numbers for better distribution +const MULTIPLIER = AVATAR_LENGTH + 7; // First prime after length +const OFFSET = AVATAR_LENGTH - 11; // First prime before length + +/** + * Generate a deterministic avatar based on multiple letters from the name. + * Uses a rolling hash of the first 5 letters (or available letters if name is shorter) + * for better distribution while maintaining deterministic results. + * + * @example + * // These will always return the same avatar for the same name + * const avatar1 = getAvatarForContact("Jonathan") // Uses 'Jonat' for hash + * const avatar2 = getAvatarForContact("Jane") // Uses 'Jane' for hash + * const avatar3 = getAvatarForContact("J") // Uses 'J' for hash + * + * @param name - Contact name or null/undefined + * @returns Avatar component + */ +const getAvatarForContact = (name?: string | null): AvatarComponent => { + if (!name?.length) { + return DEFAULT_AVATAR; + } + + // Take up to first 8 characters, or all if name is shorter + const chars = name.slice(0, 8); + + // Create a rolling hash from the characters + let hash = 0; + for (let i = 0; i < chars.length; i++) { + const charCode = chars.charCodeAt(i); + // Use position-based multiplier for better distribution + hash = (hash * MULTIPLIER + charCode * (i + 1) + OFFSET) % AVATAR_LENGTH; + } + + return avatars.at(Math.abs(hash)) ?? DEFAULT_AVATAR; +}; + +export type {AvatarComponent}; +export {getAvatarForContact}; diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 07e34d9692b1..6147ee3e8d47 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -1,10 +1,14 @@ import lodashIsEqual from 'lodash/isEqual'; import lodashPick from 'lodash/pick'; import lodashReject from 'lodash/reject'; -import React, {memo, useCallback, useEffect, useMemo} from 'react'; +import React, {memo, useCallback, useEffect, useMemo, useState} from 'react'; import type {GestureResponderEvent} from 'react-native'; +import {InteractionManager, Linking, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import {RESULTS} from 'react-native-permissions'; +import type {PermissionStatus} from 'react-native-permissions'; import Button from '@components/Button'; +import ContactPermissionModal from '@components/ContactPermissionModal'; import EmptySelectionListContent from '@components/EmptySelectionListContent'; import FormHelpMessage from '@components/FormHelpMessage'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -14,6 +18,7 @@ import {useOptionsList} from '@components/OptionListContextProvider'; import ReferralProgramCTA from '@components/ReferralProgramCTA'; import SelectionList from '@components/SelectionList'; import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem'; +import Text from '@components/Text'; import useDebouncedState from '@hooks/useDebouncedState'; import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners'; import useLocalize from '@hooks/useLocalize'; @@ -21,11 +26,16 @@ import useNetwork from '@hooks/useNetwork'; import usePolicy from '@hooks/usePolicy'; import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionStatus'; import useThemeStyles from '@hooks/useThemeStyles'; +import contactImport from '@libs/ContactImport'; +import type {ContactImportResult} from '@libs/ContactImport/types'; +import {getContactPermission} from '@libs/ContactPermission'; +import getContacts from '@libs/ContactUtils'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import saveLastRoute from '@libs/saveLastRoute'; import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import * as Policy from '@userActions/Policy/Policy'; import * as Report from '@userActions/Report'; @@ -33,6 +43,7 @@ import type {IOUAction, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {PersonalDetails} from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; type MoneyRequestParticipantsSelectorProps = { @@ -69,6 +80,9 @@ function MoneyRequestParticipantsSelector({ }: MoneyRequestParticipantsSelectorProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const [softPermissionModalVisible, setSoftPermissionModalVisible] = useState(false); + const [contactPermissionState, setContactPermissionState] = useState(RESULTS.UNAVAILABLE); + const showImportContacts = !(contactPermissionState === RESULTS.GRANTED || contactPermissionState === RESULTS.LIMITED); const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); const referralContentType = iouType === CONST.IOU.TYPE.PAY ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.PAY_SOMEONE : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SUBMIT_EXPENSE; const {isOffline} = useNetwork(); @@ -83,6 +97,8 @@ function MoneyRequestParticipantsSelector({ const {options, areOptionsInitialized} = useOptionsList({ shouldInitialize: didScreenTransitionEnd, }); + const [contacts, setContacts] = useState>>([]); + const [textInputAutoFocus, setTextInputAutoFocus] = useState(softPermissionModalVisible); const cleanSearchTerm = useMemo(() => debouncedSearchTerm.trim().toLowerCase(), [debouncedSearchTerm]); const offlineMessage: string = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; @@ -108,7 +124,7 @@ function MoneyRequestParticipantsSelector({ const optionList = OptionsListUtils.getValidOptions( { reports: options.reports, - personalDetails: options.personalDetails, + personalDetails: options.personalDetails.concat(contacts), }, { betas, @@ -132,7 +148,7 @@ function MoneyRequestParticipantsSelector({ ...optionList, ...orderedOptions, }; - }, [action, areOptionsInitialized, betas, didScreenTransitionEnd, iouType, isCategorizeOrShareAction, options.personalDetails, options.reports, participants]); + }, [action, contacts, areOptionsInitialized, betas, didScreenTransitionEnd, iouType, isCategorizeOrShareAction, options.personalDetails, options.reports, participants]); const chatOptions = useMemo(() => { if (!areOptionsInitialized) { @@ -156,6 +172,16 @@ function MoneyRequestParticipantsSelector({ return newOptions; }, [areOptionsInitialized, defaultOptions, debouncedSearchTerm, participants, isPaidGroupPolicy, isCategorizeOrShareAction, action]); + const inputHelperText = useMemo( + () => + OptionsListUtils.getHeaderMessage( + (chatOptions.personalDetails ?? []).length + (chatOptions.recentReports ?? []).length !== 0, + !!chatOptions?.userToInvite, + debouncedSearchTerm.trim(), + participants.some((participant) => OptionsListUtils.getPersonalDetailSearchTerms(participant).join(' ').toLowerCase().includes(cleanSearchTerm)), + ), + [chatOptions.personalDetails, chatOptions.recentReports, chatOptions?.userToInvite, cleanSearchTerm, debouncedSearchTerm, participants], + ); /** * Returns the sections needed for the OptionsSelector * @returns {Array} @@ -203,12 +229,10 @@ function MoneyRequestParticipantsSelector({ }); } - const headerMessage = OptionsListUtils.getHeaderMessage( - (chatOptions.personalDetails ?? []).length + (chatOptions.recentReports ?? []).length !== 0, - !!chatOptions?.userToInvite, - debouncedSearchTerm.trim(), - participants.some((participant) => OptionsListUtils.getPersonalDetailSearchTerms(participant).join(' ').toLowerCase().includes(cleanSearchTerm)), - ); + let headerMessage = ''; + if (!showImportContacts) { + headerMessage = inputHelperText; + } return [newSections, headerMessage]; }, [ @@ -221,19 +245,38 @@ function MoneyRequestParticipantsSelector({ chatOptions.userToInvite, personalDetails, translate, - cleanSearchTerm, + showImportContacts, + inputHelperText, ]); + const handleContactImport = useCallback(() => { + contactImport().then(({contactList, permissionStatus}: ContactImportResult) => { + setContactPermissionState(permissionStatus); + const usersFromContact = getContacts(contactList); + setContacts(usersFromContact); + }); + }, []); + + useEffect(() => { + setSoftPermissionModalVisible(true); + getContactPermission().then((status) => { + setContactPermissionState(status); + if (status !== RESULTS.BLOCKED && status !== RESULTS.UNAVAILABLE) { + setSoftPermissionModalVisible(true); + } + }); + }, []); + /** * Adds a single participant to the expense * * @param {Object} option */ const addSingleParticipant = useCallback( - (option: Participant) => { + (option: Participant & OptionsListUtils.Option) => { const newParticipants: Participant[] = [ { - ...lodashPick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText', 'policyID'), + ...lodashPick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText', 'policyID', 'text', 'phoneNumber'), selected: true, iouType, }, @@ -343,21 +386,57 @@ function MoneyRequestParticipantsSelector({ const shouldShowReferralBanner = !isDismissed && iouType !== CONST.IOU.TYPE.INVOICE && !shouldShowListEmptyContent; + const goToSettings = useCallback(() => { + Linking.openSettings(); + // In the case of ios, the App reloads when we update contact permission from settings + // we are saving last route so we can navigate to it after app reload + saveLastRoute(); + }, []); + const headerContent = useMemo(() => { - if (!shouldDisplayTrackExpenseButton) { - return; - } + const importContacts = + showImportContacts && inputHelperText ? ( + + + {`${translate('common.noResultsFound')}. `} + + {translate('contact.importContactsTitle')} + {' '} + {translate('contact.importContactsExplanation')} + + + ) : null; // We only display the track expense button if the user is coming from the combined submit/track flow. - return ( + const expenseButton = shouldDisplayTrackExpenseButton ? ( + ) : null; + + return ( + <> + {importContacts} + {expenseButton} + ); - }, [shouldDisplayTrackExpenseButton, translate, onTrackExpensePress]); + }, [showImportContacts, inputHelperText, shouldDisplayTrackExpenseButton, translate, onTrackExpensePress, styles, goToSettings]); + + const handleSoftPermissionDeny = useCallback(() => { + setSoftPermissionModalVisible(false); + }, []); + + const handleSoftPermissionGrant = useCallback(() => { + setSoftPermissionModalVisible(false); + InteractionManager.runAfterInteractions(handleContactImport); + setTextInputAutoFocus(true); + }, [handleContactImport]); const footerContent = useMemo(() => { if (isDismissed && !shouldShowSplitBillErrorMessage && !participants.length) { @@ -432,27 +511,58 @@ function MoneyRequestParticipantsSelector({ [isIOUSplit, addParticipantToSelection, addSingleParticipant], ); + const listFooterComponent = useMemo(() => { + if (!showImportContacts) { + return null; + } + return ( + + ); + }, [goToSettings, showImportContacts, styles.mb3, translate]); + + const EmptySelectionListContentWithPermission = useMemo(() => { + return ; + }, [iouType]); + return ( - } - headerMessage={header} - showLoadingPlaceholder={showLoadingPlaceholder} - canSelectMultiple={isIOUSplit && isAllowedToSplit} - isLoadingNewOptions={!!isSearchingForReports} - shouldShowListEmptyContent={shouldShowListEmptyContent} - /> + <> + {softPermissionModalVisible && ( + + )} + + ); } diff --git a/tsconfig.json b/tsconfig.json index 869627446f09..f1d7d0282c55 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,9 +21,30 @@ "@pages/*": ["./src/pages/*"], "@styles/*": ["./src/styles/*"], "@src/*": ["./src/*"], - "@userActions/*": ["./src/libs/actions/*"] + "@userActions/*": ["./src/libs/actions/*"], + "contacts-nitro-module": ["./modules/ContactsNitroModule"], + "contacts-nitro-module/*": ["./modules/ContactsNitroModule/*"] } }, - "include": ["src", "desktop", "web", "website", "docs", "assets", "config", "tests", "jest", "__mocks__", ".github/**/*", ".storybook/**/*", "scripts"], + "include": [ + "src", + "desktop", + "web", + "website", + "docs", + "assets", + "config", + "tests", + "jest", + "__mocks__", + ".github/**/*", + ".storybook/**/*", + "scripts", + "modules/index.ts", + "**/*.nitro/*.ts", + "**/*.nitro/*.tsx", + "modules/ContactsNitroModule/lib/**/*", + "modules/ContactsNitroModule/lib/*.d.ts" + ], "exclude": ["**/node_modules/*", "**/dist/*", ".github/actions/**/index.js", "**/docs/*"] }