diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 33a12b929e98..db5c68ac0231 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -104,6 +104,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
createNativeModules(
List modules = new ArrayList<>();
modules.add(new StartupTimer(reactContext));
+ modules.add(new ShareActionHandlerModule(reactContext));
return modules;
}
diff --git a/android/app/src/main/java/com/expensify/chat/MainActivity.kt b/android/app/src/main/java/com/expensify/chat/MainActivity.kt
index 2daebb9b1c00..16d7047fac08 100644
--- a/android/app/src/main/java/com/expensify/chat/MainActivity.kt
+++ b/android/app/src/main/java/com/expensify/chat/MainActivity.kt
@@ -1,18 +1,20 @@
package com.expensify.chat
-import expo.modules.ReactActivityDelegateWrapper
-
+import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.Bundle
+import android.util.Log
import android.view.KeyEvent
import android.view.View
import android.view.WindowInsets
import com.expensify.chat.bootsplash.BootSplash
+import com.expensify.chat.intenthandler.IntentHandlerFactory
import com.expensify.reactnativekeycommand.KeyCommandModule
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
+import expo.modules.ReactActivityDelegateWrapper
import com.oblador.performance.RNPerformance
@@ -52,6 +54,25 @@ class MainActivity : ReactActivity() {
defaultInsets.systemWindowInsetBottom
)
}
+
+ if (intent != null) {
+ handleIntent(intent)
+ }
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+ setIntent(intent) // Must store the new intent unless getIntent() will return the old one
+ handleIntent(intent)
+ }
+
+ private fun handleIntent(intent: Intent) {
+ try {
+ val intenthandler = IntentHandlerFactory.getIntentHandler(this, intent.type, intent.toString())
+ intenthandler?.handle(intent)
+ } catch (exception: Exception) {
+ Log.e("handleIntentException", exception.toString())
+ }
}
/**
diff --git a/android/app/src/main/java/com/expensify/chat/ShareActionHandlerModule.kt b/android/app/src/main/java/com/expensify/chat/ShareActionHandlerModule.kt
new file mode 100644
index 000000000000..dcb94d19a22d
--- /dev/null
+++ b/android/app/src/main/java/com/expensify/chat/ShareActionHandlerModule.kt
@@ -0,0 +1,89 @@
+package com.expensify.chat
+
+import android.content.Context
+import android.graphics.BitmapFactory
+import android.media.MediaMetadataRetriever
+import com.expensify.chat.intenthandler.IntentHandlerConstants
+import com.facebook.react.bridge.Callback
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReactContextBaseJavaModule
+import android.util.Log
+import com.facebook.react.bridge.ReactMethod
+import org.json.JSONObject
+import java.io.File
+
+class ShareActionHandlerModule(reactContext: ReactApplicationContext) :
+ ReactContextBaseJavaModule(reactContext) {
+
+ override fun getName(): String {
+ return "ShareActionHandler"
+ }
+
+ @ReactMethod
+ fun processFiles(callback: Callback) {
+ try {
+ val sharedPreferences = reactApplicationContext.getSharedPreferences(
+ IntentHandlerConstants.preferencesFile,
+ Context.MODE_PRIVATE
+ )
+
+ val shareObjectString = sharedPreferences.getString(IntentHandlerConstants.shareObjectProperty, null)
+ if (shareObjectString == null) {
+ callback.invoke("No data found", null)
+ return
+ }
+
+ val shareObject = JSONObject(shareObjectString)
+ val content = shareObject.optString("content")
+ val mimeType = shareObject.optString("mimeType")
+ val fileUriPath = "file://$content"
+ val timestamp = System.currentTimeMillis()
+
+ val file = File(content)
+ if (!file.exists()) {
+ val textObject = JSONObject().apply {
+ put("id", "text")
+ put("content", content)
+ put("mimeType", "txt")
+ put("processedAt", timestamp)
+ }
+ callback.invoke(textObject.toString())
+ return
+ }
+
+ val identifier = file.name
+ var aspectRatio = 0.0f
+
+ if (mimeType.startsWith("image/")) {
+ val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
+ BitmapFactory.decodeFile(content, options)
+ aspectRatio = if (options.outHeight != 0) options.outWidth.toFloat() / options.outHeight else 1.0f
+ } else if (mimeType.startsWith("video/")) {
+ val retriever = MediaMetadataRetriever()
+ try {
+ retriever.setDataSource(content)
+ val videoWidth = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toFloatOrNull() ?: 1f
+ val videoHeight = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toFloatOrNull() ?: 1f
+ if (videoHeight != 0f) aspectRatio = videoWidth / videoHeight
+ } catch (e: Exception) {
+ Log.e("ShareActionHandlerModule", "Error retrieving video metadata: ${e.message}")
+ } finally {
+ retriever.release()
+ }
+ }
+
+ val fileData = JSONObject().apply {
+ put("id", identifier)
+ put("content", fileUriPath)
+ put("mimeType", mimeType)
+ put("processedAt", timestamp)
+ put("aspectRatio", aspectRatio)
+ }
+
+ callback.invoke(fileData.toString())
+
+ } catch (e: Exception) {
+ callback.invoke(e.toString(), null)
+ }
+ }
+}
diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/AbstractIntentHandler.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/AbstractIntentHandler.kt
new file mode 100644
index 000000000000..a5f554b53a18
--- /dev/null
+++ b/android/app/src/main/java/com/expensify/chat/intentHandler/AbstractIntentHandler.kt
@@ -0,0 +1,19 @@
+package com.expensify.chat.intenthandler
+
+import android.content.Context
+import com.expensify.chat.utils.FileUtils.clearInternalStorageDirectory
+
+abstract class AbstractIntentHandler: IntentHandler {
+ override fun onCompleted() {}
+
+ protected fun clearTemporaryFiles(context: Context) {
+ // Clear data present in the shared preferences
+ val sharedPreferences = context.getSharedPreferences(IntentHandlerConstants.preferencesFile, Context.MODE_PRIVATE)
+ val editor = sharedPreferences.edit()
+ editor.clear()
+ editor.apply()
+
+ // Clear leftover temporary files from previous share attempts
+ clearInternalStorageDirectory(context)
+ }
+}
diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt
new file mode 100644
index 000000000000..0d52c66b1248
--- /dev/null
+++ b/android/app/src/main/java/com/expensify/chat/intentHandler/FileIntentHandler.kt
@@ -0,0 +1,42 @@
+package com.expensify.chat.intenthandler
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import com.expensify.chat.utils.FileUtils
+
+class FileIntentHandler(private val context: Context) : AbstractIntentHandler() {
+ override fun handle(intent: Intent): Boolean {
+ super.clearTemporaryFiles(context)
+ when(intent.action) {
+ Intent.ACTION_SEND -> {
+ handleSingleFileIntent(intent, context)
+ onCompleted()
+ return true
+ }
+ }
+ return false
+ }
+
+ private fun handleSingleFileIntent(intent: Intent, context: Context) {
+ (intent.getParcelableExtra(Intent.EXTRA_STREAM))?.let { fileUri ->
+ val resultingPath: String? = FileUtils.copyUriToStorage(fileUri, context)
+
+ if (resultingPath != null) {
+ val shareFileObject = ShareFileObject(resultingPath, intent.type)
+
+ val sharedPreferences = context.getSharedPreferences(IntentHandlerConstants.preferencesFile, Context.MODE_PRIVATE)
+ val editor = sharedPreferences.edit()
+ editor.putString(IntentHandlerConstants.shareObjectProperty, shareFileObject.toString())
+ editor.apply()
+ }
+ }
+ }
+
+ override fun onCompleted() {
+ val uri: Uri = Uri.parse("new-expensify://share/root")
+ val deepLinkIntent = Intent(Intent.ACTION_VIEW, uri)
+ deepLinkIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ context.startActivity(deepLinkIntent)
+ }
+}
diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/IntentHandler.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/IntentHandler.kt
new file mode 100644
index 000000000000..d698096836b0
--- /dev/null
+++ b/android/app/src/main/java/com/expensify/chat/intentHandler/IntentHandler.kt
@@ -0,0 +1,12 @@
+package com.expensify.chat.intenthandler
+
+import android.content.Intent
+
+object IntentHandlerConstants {
+ const val preferencesFile = "shareActionHandler"
+ const val shareObjectProperty = "shareObject"
+}
+interface IntentHandler {
+ fun handle(intent: Intent): Boolean
+ fun onCompleted()
+}
diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/IntentHandlerFactory.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/IntentHandlerFactory.kt
new file mode 100644
index 000000000000..e024a9172493
--- /dev/null
+++ b/android/app/src/main/java/com/expensify/chat/intentHandler/IntentHandlerFactory.kt
@@ -0,0 +1,15 @@
+package com.expensify.chat.intenthandler
+
+import android.content.Context
+
+object IntentHandlerFactory {
+ fun getIntentHandler(context: Context, mimeType: String?, rest: String?): IntentHandler? {
+ if (mimeType == null) return null
+
+ return when {
+ mimeType.matches(Regex("(image|application|audio|video)/.*")) -> FileIntentHandler(context)
+ mimeType.startsWith("text/") -> TextIntentHandler(context)
+ else -> throw UnsupportedOperationException("Unsupported MIME type: $mimeType")
+ }
+ }
+}
diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/ShareFileObject.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/ShareFileObject.kt
new file mode 100644
index 000000000000..352850f1710e
--- /dev/null
+++ b/android/app/src/main/java/com/expensify/chat/intentHandler/ShareFileObject.kt
@@ -0,0 +1,15 @@
+package com.expensify.chat.intenthandler
+
+import org.json.JSONObject
+
+data class ShareFileObject(
+ val content: String,
+ val mimeType: String?,
+) {
+ override fun toString(): String {
+ return JSONObject().apply {
+ put("content", content)
+ put("mimeType", mimeType)
+ }.toString()
+ }
+}
diff --git a/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt
new file mode 100644
index 000000000000..35aa8ab21719
--- /dev/null
+++ b/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt
@@ -0,0 +1,76 @@
+package com.expensify.chat.intenthandler
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import com.expensify.chat.utils.FileUtils
+
+
+class TextIntentHandler(private val context: Context) : AbstractIntentHandler() {
+ override fun handle(intent: Intent): Boolean {
+ super.clearTemporaryFiles(context)
+ when(intent.action) {
+ Intent.ACTION_SEND -> {
+ handleTextIntent(intent, context)
+ onCompleted()
+ return true
+ }
+ }
+ return false
+ }
+
+ private fun handleTextIntent(intent: Intent, context: Context) {
+ when {
+ intent.type == "text/plain" -> {
+ val extras = intent.extras
+ if (extras != null) {
+ when {
+ extras.containsKey(Intent.EXTRA_STREAM) -> {
+ handleTextFileIntent(intent, context)
+ }
+ extras.containsKey(Intent.EXTRA_TEXT) -> {
+ handleTextPlainIntent(intent, context)
+ }
+ else -> {
+ throw UnsupportedOperationException("Unknown text/plain content")
+ }
+ }
+ }
+ }
+ Regex("text/.*").matches(intent.type ?: "") -> handleTextFileIntent(intent, context)
+ else -> throw UnsupportedOperationException("Unsupported MIME type: ${intent.type}")
+ }
+ }
+
+ private fun saveToSharedPreferences(key: String, value: String) {
+ val sharedPreferences = context.getSharedPreferences(IntentHandlerConstants.preferencesFile, Context.MODE_PRIVATE)
+ val editor = sharedPreferences.edit()
+ editor.putString(key, value)
+ editor.apply()
+ }
+
+ private fun handleTextFileIntent(intent: Intent, context: Context) {
+ (intent.getParcelableExtra(Intent.EXTRA_STREAM))?.let { fileUri ->
+ val resultingPath: String? = FileUtils.copyUriToStorage(fileUri, context)
+ if (resultingPath != null) {
+ val shareFileObject = ShareFileObject(resultingPath, intent.type)
+ saveToSharedPreferences(IntentHandlerConstants.shareObjectProperty, shareFileObject.toString())
+ }
+ }
+ }
+
+ private fun handleTextPlainIntent(intent: Intent, context: Context) {
+ var intentTextContent = intent.getStringExtra(Intent.EXTRA_TEXT)
+ if(intentTextContent != null) {
+ val shareFileObject = ShareFileObject(intentTextContent, intent.type)
+ saveToSharedPreferences(IntentHandlerConstants.shareObjectProperty, shareFileObject.toString())
+ }
+ }
+
+ override fun onCompleted() {
+ val uri: Uri = Uri.parse("new-expensify://share/root")
+ val deepLinkIntent = Intent(Intent.ACTION_VIEW, uri)
+ deepLinkIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ context.startActivity(deepLinkIntent)
+ }
+}
diff --git a/android/app/src/main/java/com/expensify/chat/utils/FileUtils.kt b/android/app/src/main/java/com/expensify/chat/utils/FileUtils.kt
new file mode 100644
index 000000000000..a0f13ddad645
--- /dev/null
+++ b/android/app/src/main/java/com/expensify/chat/utils/FileUtils.kt
@@ -0,0 +1,126 @@
+package com.expensify.chat.utils
+
+import android.content.Context
+import android.net.Uri
+import android.provider.OpenableColumns
+import android.util.Log
+import android.webkit.MimeTypeMap
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+
+object FileUtils {
+ private const val tag = "FileUtils"
+ private const val directoryName = "Expensify"
+
+ private fun getInternalStorageDirectory(context: Context): File {
+ val internalStorageDirectory = File(context.filesDir.absolutePath, directoryName)
+ if (!internalStorageDirectory.exists()) {
+ internalStorageDirectory.mkdirs()
+ }
+ return internalStorageDirectory
+ }
+
+ fun clearInternalStorageDirectory(context: Context) {
+ val internalStorageDirectory = getInternalStorageDirectory(context)
+ if (internalStorageDirectory.exists()) {
+ val files = internalStorageDirectory.listFiles()
+ if (files != null && files.isNotEmpty()) {
+ for (file in files) {
+ file.delete()
+ }
+ } else {
+ Log.i(tag, "No files found to delete in directory: ${internalStorageDirectory.absolutePath}")
+ }
+ }
+ }
+
+ /**
+ * Creates a temporary file in the internal storage.
+ *
+ * @return unique file prefix
+ */
+ fun getUniqueFilePrefix(): String {
+ return System.currentTimeMillis().toString()
+ }
+
+ /**
+ * Synchronous method
+ *
+ * @param fileUri
+ * @param destinationFile
+ * @param context
+ * @throws IOException
+ */
+ @Throws(IOException::class)
+ fun saveFileFromProviderUri(fileUri: Uri, destinationFile: File?, context: Context) {
+ val inputStream: InputStream? = context.contentResolver.openInputStream(fileUri)
+ val outputStream: OutputStream = FileOutputStream(destinationFile)
+ inputStream?.use { input ->
+ outputStream.use { output ->
+ input.copyTo(output)
+ }
+ }
+ }
+
+ /**
+ * Creates a temporary image file in the internal storage.
+ *
+ * @param uri
+ * @param context
+ * @return
+ * @throws IOException
+ */
+ @Throws(IOException::class)
+ fun createTemporaryFile(uri: Uri, context: Context): File {
+
+ val mimeTypeMap = MimeTypeMap.getSingleton()
+
+ val fileExtension = ".${mimeTypeMap.getExtensionFromMimeType(context.contentResolver.getType(uri))}"
+
+ val file: File = File.createTempFile(
+ getUniqueFilePrefix(),
+ fileExtension,
+ getInternalStorageDirectory(context)
+ )
+
+ Log.i(tag, "Created a temporary file at" + file.absolutePath)
+ return file
+ }
+
+ /**
+ * Copy the given Uri to storage
+ *
+ * @param uri
+ * @param context
+ * @return The absolute path of the image
+ */
+ fun copyUriToStorage(fileUri: Uri, context: Context): String? {
+ val fileName = getFileName(context, fileUri) ?: return null
+ val destinationFile = File(getInternalStorageDirectory(context), fileName)
+
+ return try {
+ saveFileFromProviderUri(fileUri, destinationFile, context)
+ destinationFile.absolutePath
+ } catch (ex: IOException) {
+ Log.e(tag, "Couldn't save file from intent", ex)
+ null
+ }
+ }
+
+ private fun getFileName(context: Context, uri: Uri): String? {
+ var name: String? = null
+ val cursor = context.contentResolver.query(uri, null, null, null, null)
+ cursor?.use {
+ if (it.moveToFirst()) {
+ val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
+ if (nameIndex != -1) {
+ name = it.getString(nameIndex)
+ }
+ }
+ }
+ return name
+ }
+}
diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj
index 14866da313d5..35e6eb75c59e 100644
--- a/ios/NewExpensify.xcodeproj/project.pbxproj
+++ b/ios/NewExpensify.xcodeproj/project.pbxproj
@@ -48,7 +48,12 @@
D27CE6B77196EF3EF450EEAC /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 0D3F9E814828D91464DF9D35 /* PrivacyInfo.xcprivacy */; };
DD79042B2792E76D004484B4 /* RCTBootSplash.mm in Sources */ = {isa = PBXBuildFile; fileRef = DD79042A2792E76D004484B4 /* RCTBootSplash.mm */; };
DDCB2E57F334C143AC462B43 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D20D83B0E39BA6D21761E72 /* ExpoModulesProvider.swift */; };
- E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */ = {isa = PBXBuildFile; };
+ E51DC681C7DEE40AEBDDFBFE /* (null) in Frameworks */ = {isa = PBXBuildFile; };
+ E5A27B912C7F2FA8002C36BF /* RCTShareActionHandlerModule.m in Sources */ = {isa = PBXBuildFile; fileRef = E5A27B902C7F2FA8002C36BF /* RCTShareActionHandlerModule.m */; };
+ E5A27B922C7F2FA8002C36BF /* RCTShareActionHandlerModule.m in Sources */ = {isa = PBXBuildFile; fileRef = E5A27B902C7F2FA8002C36BF /* RCTShareActionHandlerModule.m */; };
+ E5A27B9A2C7F427F002C36BF /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5A27B992C7F427F002C36BF /* ShareViewController.swift */; };
+ E5A27B9D2C7F427F002C36BF /* Base in Resources */ = {isa = PBXBuildFile; fileRef = E5A27B9C2C7F427F002C36BF /* Base */; };
+ E5A27BA12C7F427F002C36BF /* ShareViewController.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = E5A27B972C7F427F002C36BF /* ShareViewController.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
E9DF872D2525201700607FDC /* AirshipConfig.plist in Resources */ = {isa = PBXBuildFile; fileRef = E9DF872C2525201700607FDC /* AirshipConfig.plist */; };
ED222ED90E074A5481A854FA /* ExpensifyNeue-BoldItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8B28D84EF339436DBD42A203 /* ExpensifyNeue-BoldItalic.otf */; };
F0C450EA2705020500FD2970 /* colors.json in Resources */ = {isa = PBXBuildFile; fileRef = F0C450E92705020500FD2970 /* colors.json */; };
@@ -70,6 +75,13 @@
remoteGlobalIDString = 7FD73C9A2B23CE9500420AF3;
remoteInfo = NotificationServiceExtension;
};
+ E5A27B9F2C7F427F002C36BF /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = E5A27B962C7F427F002C36BF;
+ remoteInfo = ShareViewController;
+ };
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -80,6 +92,7 @@
dstSubfolderSpec = 13;
files = (
7FD73CA22B23CE9500420AF3 /* NotificationServiceExtension.appex in Embed Foundation Extensions */,
+ E5A27BA12C7F427F002C36BF /* ShareViewController.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
@@ -118,6 +131,7 @@
41D2EDB009CF19BA59C5181C /* Pods-NotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.debug.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.debug.xcconfig"; sourceTree = ""; };
44BF435285B94E5B95F90994 /* ExpensifyNewKansas-Medium.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNewKansas-Medium.otf"; path = "../assets/fonts/native/ExpensifyNewKansas-Medium.otf"; sourceTree = ""; };
46B1FE4DE317D30C25A74C15 /* Pods-NewExpensify.debugdevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.debugdevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.debugdevelopment.xcconfig"; sourceTree = ""; };
+ 46CFEEC62CA1C2E0003D36CC /* ShareViewController.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareViewController.entitlements; sourceTree = ""; };
48E7775E0D42D3E3F53A5B99 /* Pods-NotificationServiceExtension.releaseadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.releaseadhoc.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.releaseadhoc.xcconfig"; sourceTree = ""; };
499B0DA92BE2A1C000CABFB0 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; };
4A39BBFB1A6AA6A0EB08878C /* Pods-NotificationServiceExtension.debugproduction.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.debugproduction.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.debugproduction.xcconfig"; sourceTree = ""; };
@@ -158,6 +172,12 @@
DD7904292792E76D004484B4 /* RCTBootSplash.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RCTBootSplash.h; path = NewExpensify/RCTBootSplash.h; sourceTree = ""; };
DD79042A2792E76D004484B4 /* RCTBootSplash.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = RCTBootSplash.mm; path = NewExpensify/RCTBootSplash.mm; sourceTree = ""; };
E5428460BDBED9E1BA8B3599 /* Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debugdevelopment.xcconfig"; sourceTree = ""; };
+ E5A27B8F2C7F2F51002C36BF /* RCTShareActionHandlerModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTShareActionHandlerModule.h; sourceTree = ""; };
+ E5A27B902C7F2FA8002C36BF /* RCTShareActionHandlerModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTShareActionHandlerModule.m; sourceTree = ""; };
+ E5A27B972C7F427F002C36BF /* ShareViewController.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareViewController.appex; sourceTree = BUILT_PRODUCTS_DIR; };
+ E5A27B992C7F427F002C36BF /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; };
+ E5A27B9C2C7F427F002C36BF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; };
+ E5A27B9E2C7F427F002C36BF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
E704648954784DDFBAADF568 /* ExpensifyMono-Regular.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyMono-Regular.otf"; path = "../assets/fonts/native/ExpensifyMono-Regular.otf"; sourceTree = ""; };
E9DF872C2525201700607FDC /* AirshipConfig.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = AirshipConfig.plist; sourceTree = ""; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
@@ -184,8 +204,8 @@
buildActionMask = 2147483647;
files = (
383643682B6D4AE2005BB9AE /* DeviceCheck.framework in Frameworks */,
- E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */,
- E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */,
+ E51DC681C7DEE40AEBDDFBFE /* (null) in Frameworks */,
+ E51DC681C7DEE40AEBDDFBFE /* (null) in Frameworks */,
AC131FBB2CF634F20010CE80 /* BackgroundTasks.framework in Frameworks */,
8744C5400E24E379441C04A4 /* libPods-NewExpensify.a in Frameworks */,
);
@@ -199,6 +219,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ E5A27B942C7F427F002C36BF /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -295,6 +322,8 @@
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
+ E5A27B8F2C7F2F51002C36BF /* RCTShareActionHandlerModule.h */,
+ E5A27B902C7F2FA8002C36BF /* RCTShareActionHandlerModule.m */,
0DFC45922C884D7900B56C91 /* RCTShortcutManagerModule.h */,
0DFC45932C884E0A00B56C91 /* RCTShortcutManagerModule.m */,
499B0DA92BE2A1C000CABFB0 /* PrivacyInfo.xcprivacy */,
@@ -310,6 +339,7 @@
832341AE1AAA6A7D00B99B32 /* Libraries */,
00E356EF1AD99517003FC87E /* NewExpensifyTests */,
7FD73C9C2B23CE9500420AF3 /* NotificationServiceExtension */,
+ E5A27B982C7F427F002C36BF /* ShareViewController */,
83CBBA001A601CBA00E9B192 /* Products */,
2D16E6871FA4F8E400B85C8A /* Frameworks */,
EC29677F0A49C2946A495A33 /* Pods */,
@@ -327,6 +357,7 @@
13B07F961A680F5B00A75B9A /* New Expensify Dev.app */,
00E356EE1AD99517003FC87E /* NewExpensifyTests.xctest */,
7FD73C9B2B23CE9500420AF3 /* NotificationServiceExtension.appex */,
+ E5A27B972C7F427F002C36BF /* ShareViewController.appex */,
);
name = Products;
sourceTree = "";
@@ -360,6 +391,17 @@
name = Resources;
sourceTree = "";
};
+ E5A27B982C7F427F002C36BF /* ShareViewController */ = {
+ isa = PBXGroup;
+ children = (
+ 46CFEEC62CA1C2E0003D36CC /* ShareViewController.entitlements */,
+ E5A27B992C7F427F002C36BF /* ShareViewController.swift */,
+ E5A27B9B2C7F427F002C36BF /* MainInterface.storyboard */,
+ E5A27B9E2C7F427F002C36BF /* Info.plist */,
+ );
+ path = ShareViewController;
+ sourceTree = "";
+ };
EC29677F0A49C2946A495A33 /* Pods */ = {
isa = PBXGroup;
children = (
@@ -435,6 +477,7 @@
);
dependencies = (
7FD73CA12B23CE9500420AF3 /* PBXTargetDependency */,
+ E5A27BA02C7F427F002C36BF /* PBXTargetDependency */,
);
name = NewExpensify;
productName = NewExpensify;
@@ -459,13 +502,30 @@
productReference = 7FD73C9B2B23CE9500420AF3 /* NotificationServiceExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
+ E5A27B962C7F427F002C36BF /* ShareViewController */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = E5A27BA22C7F4280002C36BF /* Build configuration list for PBXNativeTarget "ShareViewController" */;
+ buildPhases = (
+ E5A27B932C7F427F002C36BF /* Sources */,
+ E5A27B942C7F427F002C36BF /* Frameworks */,
+ E5A27B952C7F427F002C36BF /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = ShareViewController;
+ productName = ShareViewController;
+ productReference = E5A27B972C7F427F002C36BF /* ShareViewController.appex */;
+ productType = "com.apple.product-type.app-extension";
+ };
/* End PBXNativeTarget section */
/* Begin PBXProject section */
83CBB9F71A601CBA00E9B192 /* Project object */ = {
isa = PBXProject;
attributes = {
- LastSwiftUpdateCheck = 1500;
+ LastSwiftUpdateCheck = 1540;
LastUpgradeCheck = 1130;
TargetAttributes = {
00E356ED1AD99517003FC87E = {
@@ -484,6 +544,9 @@
DevelopmentTeam = 368M544MTT;
ProvisioningStyle = Manual;
};
+ E5A27B962C7F427F002C36BF = {
+ CreatedOnToolsVersion = 15.4;
+ };
};
};
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "NewExpensify" */;
@@ -502,6 +565,7 @@
13B07F861A680F5B00A75B9A /* NewExpensify */,
00E356ED1AD99517003FC87E /* NewExpensifyTests */,
7FD73C9A2B23CE9500420AF3 /* NotificationServiceExtension */,
+ E5A27B962C7F427F002C36BF /* ShareViewController */,
);
};
/* End PBXProject section */
@@ -551,6 +615,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ E5A27B952C7F427F002C36BF /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ E5A27B9D2C7F427F002C36BF /* Base in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
@@ -916,6 +988,7 @@
0CDA8E35287DD650004ECBEC /* AppDelegate.mm in Sources */,
7041848626A8E47D00E09F4D /* RCTStartupTimer.m in Sources */,
7F5E81F06BCCF61AD02CEA06 /* ExpoModulesProvider.swift in Sources */,
+ E5A27B922C7F2FA8002C36BF /* RCTShareActionHandlerModule.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -923,6 +996,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ E5A27B912C7F2FA8002C36BF /* RCTShareActionHandlerModule.m in Sources */,
18D050E0262400AF000D658B /* BridgingFile.swift in Sources */,
0DFC45942C884E0A00B56C91 /* RCTShortcutManagerModule.m in Sources */,
0F5E5350263B73FD004CA14F /* EnvironmentChecker.m in Sources */,
@@ -944,6 +1018,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ E5A27B932C7F427F002C36BF /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ E5A27B9A2C7F427F002C36BF /* ShareViewController.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
@@ -957,8 +1039,24 @@
target = 7FD73C9A2B23CE9500420AF3 /* NotificationServiceExtension */;
targetProxy = 7FD73CA02B23CE9500420AF3 /* PBXContainerItemProxy */;
};
+ E5A27BA02C7F427F002C36BF /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = E5A27B962C7F427F002C36BF /* ShareViewController */;
+ targetProxy = E5A27B9F2C7F427F002C36BF /* PBXContainerItemProxy */;
+ };
/* End PBXTargetDependency section */
+/* Begin PBXVariantGroup section */
+ E5A27B9B2C7F427F002C36BF /* MainInterface.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ E5A27B9C2C7F427F002C36BF /* Base */,
+ );
+ name = MainInterface.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
/* Begin XCBuildConfiguration section */
00E356F61AD99517003FC87E /* DebugDevelopment */ = {
isa = XCBuildConfiguration;
@@ -2563,6 +2661,444 @@
};
name = ReleaseAdHoc;
};
+ E5A27BA32C7F4280002C36BF /* DebugDevelopment */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_ENTITLEMENTS = ShareViewController/ShareViewController.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Distribution";
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ CODE_SIGN_STYLE = Manual;
+ CURRENT_PROJECT_VERSION = 1;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ DEVELOPMENT_TEAM = "";
+ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = ShareViewController/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = ShareViewController;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 17.5;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
+ PRODUCT_BUNDLE_IDENTIFIER = com.expensify.chat.dev.ShareViewController;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "(NewApp) Development: Share Extension";
+ SKIP_INSTALL = YES;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = DebugDevelopment;
+ };
+ E5A27BA42C7F4280002C36BF /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_ENTITLEMENTS = ShareViewController/ShareViewController.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Distribution";
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ CODE_SIGN_STYLE = Manual;
+ CURRENT_PROJECT_VERSION = 1;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ DEVELOPMENT_TEAM = "";
+ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = ShareViewController/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = ShareViewController;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 17.5;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
+ PRODUCT_BUNDLE_IDENTIFIER = com.expensify.chat.dev.ShareViewController;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "(NewApp) Development: Share Extension";
+ SKIP_INSTALL = YES;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ E5A27BA52C7F4280002C36BF /* DebugAdHoc */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_ENTITLEMENTS = ShareViewController/ShareViewController.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Distribution";
+ CODE_SIGN_STYLE = Manual;
+ CURRENT_PROJECT_VERSION = 1;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ DEVELOPMENT_TEAM = "";
+ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = ShareViewController/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = ShareViewController;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 17.5;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
+ PRODUCT_BUNDLE_IDENTIFIER = com.expensify.chat.adhoc.ShareViewController;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "(NewApp) AdHoc: Share Extension";
+ SKIP_INSTALL = YES;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = DebugAdHoc;
+ };
+ E5A27BA62C7F4280002C36BF /* DebugProduction */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_ENTITLEMENTS = ShareViewController/ShareViewController.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Distribution";
+ CODE_SIGN_STYLE = Manual;
+ CURRENT_PROJECT_VERSION = 1;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ DEVELOPMENT_TEAM = "";
+ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = ShareViewController/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = ShareViewController;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 17.5;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
+ PRODUCT_BUNDLE_IDENTIFIER = com.chat.expensify.chat.ShareViewController;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "(NewApp) AppStore: Share Extension";
+ SKIP_INSTALL = YES;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = DebugProduction;
+ };
+ E5A27BA72C7F4280002C36BF /* ReleaseDevelopment */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_ENTITLEMENTS = ShareViewController/ShareViewController.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Distribution";
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ CODE_SIGN_STYLE = Manual;
+ COPY_PHASE_STRIP = NO;
+ CURRENT_PROJECT_VERSION = 1;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ DEVELOPMENT_TEAM = "";
+ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT;
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = ShareViewController/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = ShareViewController;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 17.5;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
+ PRODUCT_BUNDLE_IDENTIFIER = com.expensify.chat.dev.ShareViewController;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "(NewApp) Development: Share Extension";
+ SDKROOT = iphoneos;
+ SKIP_INSTALL = YES;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = ReleaseDevelopment;
+ };
+ E5A27BA82C7F4280002C36BF /* ReleaseAdHoc */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_ENTITLEMENTS = ShareViewController/ShareViewController.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Distribution";
+ CODE_SIGN_STYLE = Manual;
+ COPY_PHASE_STRIP = NO;
+ CURRENT_PROJECT_VERSION = 1;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ DEVELOPMENT_TEAM = "";
+ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT;
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = ShareViewController/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = ShareViewController;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 17.5;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
+ PRODUCT_BUNDLE_IDENTIFIER = com.expensify.chat.adhoc.ShareViewController;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "(NewApp) AdHoc: Share Extension";
+ SDKROOT = iphoneos;
+ SKIP_INSTALL = YES;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = ReleaseAdHoc;
+ };
+ E5A27BA92C7F4280002C36BF /* ReleaseProduction */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_ENTITLEMENTS = ShareViewController/ShareViewController.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Distribution";
+ CODE_SIGN_STYLE = Manual;
+ COPY_PHASE_STRIP = NO;
+ CURRENT_PROJECT_VERSION = 1;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ DEVELOPMENT_TEAM = "";
+ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 368M544MTT;
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = ShareViewController/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = ShareViewController;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 17.5;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
+ PRODUCT_BUNDLE_IDENTIFIER = com.chat.expensify.chat.ShareViewController;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "(NewApp) AppStore: Share Extension";
+ SDKROOT = iphoneos;
+ SKIP_INSTALL = YES;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = ReleaseProduction;
+ };
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -2622,6 +3158,20 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = DebugDevelopment;
};
+ E5A27BA22C7F4280002C36BF /* Build configuration list for PBXNativeTarget "ShareViewController" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ E5A27BA32C7F4280002C36BF /* DebugDevelopment */,
+ E5A27BA42C7F4280002C36BF /* Debug */,
+ E5A27BA52C7F4280002C36BF /* DebugAdHoc */,
+ E5A27BA62C7F4280002C36BF /* DebugProduction */,
+ E5A27BA72C7F4280002C36BF /* ReleaseDevelopment */,
+ E5A27BA82C7F4280002C36BF /* ReleaseAdHoc */,
+ E5A27BA92C7F4280002C36BF /* ReleaseProduction */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = DebugDevelopment;
+ };
/* End XCConfigurationList section */
};
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
diff --git a/ios/NewExpensify.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/NewExpensify.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 000000000000..0c67376ebacb
--- /dev/null
+++ b/ios/NewExpensify.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/ios/NewExpensify/Chat.entitlements b/ios/NewExpensify/Chat.entitlements
index 165745a3c1a0..831c5b2d5c8f 100644
--- a/ios/NewExpensify/Chat.entitlements
+++ b/ios/NewExpensify/Chat.entitlements
@@ -16,5 +16,9 @@
com.apple.developer.usernotifications.communication
+ com.apple.security.application-groups
+
+ group.com.expensify.new
+
diff --git a/ios/RCTShareActionHandlerModule.h b/ios/RCTShareActionHandlerModule.h
new file mode 100644
index 000000000000..0dc50ae51a96
--- /dev/null
+++ b/ios/RCTShareActionHandlerModule.h
@@ -0,0 +1,16 @@
+//
+// RCTShareActionHandlerModule.h
+// NewExpensify
+//
+// Created by Bartek Krasoń on 28/08/2024.
+//
+
+#ifndef RCTShareActionHandlerModule_h
+#define RCTShareActionHandlerModule_h
+
+#import
+
+@interface RCTShareActionHandlerModule : NSObject
+@end
+
+#endif /* RCTShareActionHandlerModule_h */
diff --git a/ios/RCTShareActionHandlerModule.m b/ios/RCTShareActionHandlerModule.m
new file mode 100644
index 000000000000..e058c304e3f1
--- /dev/null
+++ b/ios/RCTShareActionHandlerModule.m
@@ -0,0 +1,129 @@
+#import
+#import
+#import "RCTShareActionHandlerModule.h"
+#import
+#import
+
+NSString *const ShareExtensionGroupId = @"group.com.expensify.new";
+NSString *const ShareExtensionFilesKey = @"sharedFiles";
+
+@implementation RCTShareActionHandlerModule
+
+RCT_EXPORT_MODULE(ShareActionHandler);
+
+RCT_EXPORT_METHOD(processFiles:(RCTResponseSenderBlock)callback) {
+ RCTLogInfo(@"Processing share extension files");
+ NSArray *fileFinalPaths = [self fileListFromSharedContainer];
+ NSArray *processedFiles = [self processFileList:fileFinalPaths];
+ callback(@[processedFiles]);
+}
+
+- (NSArray *)fileListFromSharedContainer {
+ NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:ShareExtensionGroupId];
+ NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:ShareExtensionGroupId];
+ if (groupURL == NULL) {
+ NSLog(@"Missing app group url");
+ return @[];
+ }
+ NSURL *sharedFilesFolderPathURL = [groupURL URLByAppendingPathComponent:ShareExtensionFilesKey];
+ NSString *sharedFilesFolderPath = [sharedFilesFolderPathURL path];
+ [defaults removeObjectForKey:ShareExtensionFilesKey];
+ [defaults synchronize];
+
+ NSError *error = nil;
+ NSArray *fileSrcPath = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:sharedFilesFolderPath error:&error];
+ if (fileSrcPath.count == 0) {
+ NSLog(@"Failed to find files in 'sharedFilesFolderPath' %@", sharedFilesFolderPath);
+ return @[];
+ }
+
+ NSMutableArray *fileFinalPaths = [NSMutableArray array];
+ for (NSString *source in fileSrcPath) {
+ if (source == NULL) {
+ NSLog(@"Invalid file");
+ continue;
+ }
+ NSString *srcFileAbsolutePath = [sharedFilesFolderPath stringByAppendingPathComponent:source];
+ [fileFinalPaths addObject:srcFileAbsolutePath];
+ }
+
+ return fileFinalPaths;
+}
+
+- (NSArray *)processFileList:(NSArray *)filePaths {
+ NSMutableArray *fileObjectsArray = [[NSMutableArray alloc] init];
+ for (NSString *filePath in filePaths) {
+ NSDictionary *fileDict = [self processSingleFile:filePath];
+ if (fileDict) {
+ [fileObjectsArray addObject:fileDict];
+ }
+ }
+ return fileObjectsArray;
+}
+
+- (NSDictionary *)processSingleFile:(NSString *)filePath {
+ NSString *extension = [filePath pathExtension];
+ NSString *fileName = [filePath lastPathComponent];
+ NSString *fileContent, *mimeType;
+ CGFloat aspectRatio = 1.0;
+ BOOL isTextToReadFromFile = [fileName containsString:@"text_to_read"];
+ NSDictionary *dict;
+ NSError *error;
+
+ if (isTextToReadFromFile) {
+ fileContent = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:&error];
+ if (error) {
+ NSLog(@"Failed to read file: %@, error: %@", filePath, error);
+ return nil;
+ }
+ mimeType = @"txt";
+ } else {
+ UTType *type = [UTType typeWithFilenameExtension:extension];
+ mimeType = type.preferredMIMEType ?: @"application/octet-stream";
+ NSURL *fileURL = [NSURL fileURLWithPath:filePath];
+ if ([mimeType hasPrefix:@"video/"]) {
+ aspectRatio = [self videoAspectRatio:fileURL];
+ } else if ([mimeType hasPrefix:@"image/"]) {
+ aspectRatio = [self imageAspectRatio:fileURL];
+ }
+ fileContent = [@"file://" stringByAppendingString:filePath];
+ }
+
+ NSString *identifier = [self uniqueIdentifierForFilePath:filePath];
+ NSString *timestamp = [NSString stringWithFormat:@"%.0f", ([[NSDate date] timeIntervalSince1970] * 1000)];
+
+ dict = @{
+ @"id": identifier,
+ @"content": fileContent,
+ @"mimeType": mimeType,
+ @"processedAt": timestamp,
+ @"aspectRatio": @(aspectRatio)
+ };
+ return dict;
+}
+
+- (CGFloat)videoAspectRatio:(NSURL *)url {
+ AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
+ AVAssetTrack *track = [[asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
+ if (track) {
+ CGSize size = CGSizeApplyAffineTransform(track.naturalSize, track.preferredTransform);
+ return size.height != 0 ? fabs(size.width / size.height) : 1;
+ }
+ return 1;
+}
+
+- (CGFloat)imageAspectRatio:(NSURL *)url {
+ UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
+ if (image) {
+ return image.size.height != 0 ? image.size.width / image.size.height : 1;
+ }
+ return 1;
+}
+
+- (NSString *)uniqueIdentifierForFilePath:(NSString *)filePath {
+ NSTimeInterval timestampInterval = [[NSDate date] timeIntervalSince1970] * 1000;
+ NSString *timestamp = [NSString stringWithFormat:@"%.0f", timestampInterval];
+ return [NSString stringWithFormat:@"%@_%@", timestamp, filePath];
+}
+
+@end
diff --git a/ios/ShareViewController/Base.lproj/MainInterface.storyboard b/ios/ShareViewController/Base.lproj/MainInterface.storyboard
new file mode 100644
index 000000000000..286a50894d87
--- /dev/null
+++ b/ios/ShareViewController/Base.lproj/MainInterface.storyboard
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/ShareViewController/Info.plist b/ios/ShareViewController/Info.plist
new file mode 100644
index 000000000000..e0acd460a1bd
--- /dev/null
+++ b/ios/ShareViewController/Info.plist
@@ -0,0 +1,32 @@
+
+
+
+
+
+ NSExtension
+
+ NSExtensionAttributes
+
+ NSExtensionActivationRule
+
+ NSExtensionActivationSupportsAttachmentsWithMaxCount
+ 1
+ NSExtensionActivationSupportsMovieWithMaxCount
+ 1
+ NSExtensionActivationSupportsImageWithMaxCount
+ 1
+ NSExtensionActivationSupportsFileWithMaxCount
+ 1
+ NSExtensionActivationSupportsText
+ 1
+ NSExtensionActivationSupportsWebURLWithMaxCount
+ 1
+
+
+ NSExtensionMainStoryboard
+ MainInterface
+ NSExtensionPointIdentifier
+ com.apple.share-services
+
+
+
diff --git a/ios/ShareViewController/ShareViewController.entitlements b/ios/ShareViewController/ShareViewController.entitlements
new file mode 100644
index 000000000000..f52d3207d6e3
--- /dev/null
+++ b/ios/ShareViewController/ShareViewController.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.application-groups
+
+ group.com.expensify.new
+
+
+
diff --git a/ios/ShareViewController/ShareViewController.swift b/ios/ShareViewController/ShareViewController.swift
new file mode 100644
index 000000000000..4ad39131a40c
--- /dev/null
+++ b/ios/ShareViewController/ShareViewController.swift
@@ -0,0 +1,247 @@
+import Intents
+import MobileCoreServices
+import Social
+import UIKit
+import UniformTypeIdentifiers
+import os
+
+class ShareViewController: UIViewController {
+ let APP_GROUP_ID = "group.com.expensify.new"
+ let FILES_DIRECTORY_NAME = "sharedFiles"
+ let READ_FROM_FILE_FILE_NAME = "text_to_read.txt"
+
+ enum FileSaveError: String {
+ case CouldNotLoad
+ case URLError
+ case GroupSharedFolderNotFound
+ }
+
+ override func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+ os_log("viewDidAppear triggered")
+
+ if let content = extensionContext!.inputItems[0] as? NSExtensionItem {
+ os_log("Received NSExtensionItem: %@", content)
+ saveFileToAppGroup(content: content) { error in
+ guard error == nil else {
+ os_log("Sharing error: %@", error!.rawValue)
+ return
+ }
+ }
+ }
+ }
+
+ private func saveFileToFolder(folder: URL, filename: String, fileData: NSData) -> URL? {
+ let filePath = folder.appendingPathComponent(filename)
+ os_log("Saving file to: %@", filePath.path)
+
+ do {
+ try fileData.write(to: filePath, options: .completeFileProtection)
+ os_log("File saved successfully at: %@", filePath.path)
+ return filePath
+ } catch {
+ os_log("Unexpected saveFileToFolder error: %@", error.localizedDescription)
+ return nil
+ }
+ }
+
+ private func saveFileToAppGroup(content: NSExtensionItem, completion: @escaping (FileSaveError?) -> Void) {
+ guard let groupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: self.APP_GROUP_ID) else {
+ completion(.GroupSharedFolderNotFound)
+ os_log("Group shared folder not found")
+ return
+ }
+
+ let sharedFileFolder = groupURL.appendingPathComponent(FILES_DIRECTORY_NAME, isDirectory: true)
+ os_log("Shared file folder: %@", sharedFileFolder.path)
+ setupSharedFolder(folder: sharedFileFolder)
+
+ guard let attachments = content.attachments else {
+ completion(.CouldNotLoad)
+ os_log("Could not load attachments")
+ return
+ }
+
+ processAttachments(attachments, in: sharedFileFolder, completion: completion)
+ }
+
+ private func setupSharedFolder(folder: URL) {
+ os_log("Setting up shared folder: %@", folder.path)
+ do {
+ try FileManager.default.createDirectory(atPath: folder.path, withIntermediateDirectories: true, attributes: nil)
+ } catch {
+ os_log("Failed to create folder: %@, error: %@", folder.path, error.localizedDescription)
+ return
+ }
+
+ do {
+ let filePaths = try FileManager.default.contentsOfDirectory(atPath: folder.path)
+ os_log("Clearing folder with contents: %@", filePaths)
+
+ for filePath in filePaths {
+ try FileManager.default.removeItem(atPath: folder.appendingPathComponent(filePath).path)
+ }
+ } catch {
+ os_log("Could not clear temp folder: %@", error.localizedDescription)
+ }
+ }
+
+ private func processAttachments(_ attachments: [NSItemProvider], in folder: URL, completion: @escaping (FileSaveError?) -> Void) {
+ os_log("Processing attachments")
+ let group = DispatchGroup()
+
+ for attachment in attachments {
+ group.enter()
+ os_log("Processing attachment")
+ loadData(for: attachment, in: folder, group: group) { error in
+ group.leave()
+ if let error = error {
+ os_log("Error loading attachment: %@", error.rawValue)
+ completion(error)
+ }
+ }
+ }
+
+ group.notify(queue: .main) {
+ os_log("Finished processing all attachments")
+ self.openMainApp()
+ }
+ }
+
+ private func loadData(for attachment: NSItemProvider, in folder: URL, group: DispatchGroup, completion: @escaping (FileSaveError?) -> Void) {
+ os_log("Loading data for attachment")
+ let isURL = attachment.hasItemConformingToTypeIdentifier("public.url") && !attachment.hasItemConformingToTypeIdentifier("public.file-url")
+ let typeIdentifier = isURL ? (kUTTypeURL as String) : (kUTTypeData as String)
+
+ attachment.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { (data, error) in
+ DispatchQueue.main.async {
+ if let error = error {
+ os_log("Sharing error: %@", error.localizedDescription)
+ completion(.CouldNotLoad)
+ return
+ }
+
+ if isURL, let url = data as? URL {
+ os_log("Handling URL: %@", url.absoluteString)
+ self.handleURL(url, folder: folder, completion: completion)
+ } else {
+ os_log("Handling data for attachment")
+ self.handleData(data, folder: folder, completion: completion)
+ }
+ }
+ }
+ }
+
+ private func handleURL(_ url: URL, folder: URL, completion: @escaping (FileSaveError?) -> Void) {
+ os_log("Handling URL: %@", url.absoluteString)
+ if let fileData = url.absoluteString.data(using: .utf8) as NSData? {
+ if let fileFinalPath = saveFileToFolder(folder: folder, filename: READ_FROM_FILE_FILE_NAME, fileData: fileData) {
+ os_log("URL saved to: %@", fileFinalPath.path)
+ completion(nil)
+ } else {
+ os_log("Could not save URL")
+ completion(.CouldNotLoad)
+ }
+ } else {
+ os_log("URL error encountered")
+ completion(.URLError)
+ }
+ }
+
+ private func handleData(_ data: Any?, folder: URL, completion: @escaping (FileSaveError?) -> Void) {
+ os_log("Handling generic data")
+ guard let data = data else {
+ os_log("Data is nil", type: .error)
+ completion(.CouldNotLoad)
+ return
+ }
+
+ if let dataString = data as? String {
+ os_log("Handling string data: %@", dataString)
+ handleStringData(dataString, folder: folder, completion: completion)
+ } else if let url = data as? NSURL {
+ os_log("Handling URL data: %@", url)
+ handleURLData(url, folder: folder, completion: completion)
+ } else if let file = data as? UIImage {
+ os_log("Handling image data")
+ handleImageData(file, folder: folder, completion: completion)
+ } else {
+ os_log("Received data of unhandled type", type: .error)
+ completion(.URLError)
+ }
+ }
+
+ private func handleStringData(_ dataString: String, folder: URL, completion: @escaping (FileSaveError?) -> Void) {
+ os_log("Handling string data without file prefix")
+ if !dataString.hasPrefix("file://") {
+ processAndSave(data: dataString.data(using: .utf8), filename: READ_FROM_FILE_FILE_NAME, folder: folder, completion: completion)
+ }
+ }
+
+ private func handleURLData(_ url: NSURL, folder: URL, completion: @escaping (FileSaveError?) -> Void) {
+ os_log("Handling NSURL data")
+ guard let filename = url.lastPathComponent else {
+ os_log("Could not get last path component")
+ completion(.CouldNotLoad)
+ return
+ }
+
+ let fileData = NSData(contentsOf: url as URL) as Data?
+ processAndSave(data: fileData, filename: filename, folder: folder, completion: completion)
+ }
+
+ private func handleImageData(_ image: UIImage, folder: URL, completion: @escaping (FileSaveError?) -> Void) {
+ os_log("Handling image data")
+ let filename = "shared_image.png"
+ processAndSave(data: image.pngData(), filename: filename, folder: folder, completion: completion)
+ }
+
+ private func processAndSave(data: Data?, filename: String, folder: URL, completion: @escaping (FileSaveError?) -> Void) {
+ os_log("Processing and saving data")
+ guard let fileData = data as NSData? else {
+ os_log("Failed to convert data", type: .error)
+ completion(.CouldNotLoad)
+ return
+ }
+
+ if saveFileToFolder(folder: folder, filename: filename, fileData: fileData) != nil {
+ os_log("File saved successfully: %@", filename)
+ completion(nil)
+ } else {
+ os_log("Failed to save file: %@", filename)
+ completion(.CouldNotLoad)
+ }
+ }
+
+ private func openMainApp() {
+ os_log("Attempting to open main app")
+ let url = URL(string: "new-expensify://share/root")!
+
+ if launchApp(customURL: url) {
+ os_log("Main app opened successfully")
+ self.extensionContext!.completeRequest(returningItems: nil, completionHandler: nil)
+ } else {
+ os_log("Failed to open main app")
+ self.extensionContext!.cancelRequest(withError: NSError(domain: "", code: 0, userInfo: nil))
+ }
+ }
+
+ private func launchApp(customURL: URL?) -> Bool {
+ os_log("Launching app with custom URL")
+ guard let url = customURL else {
+ os_log("Invalid custom URL")
+ return false
+ }
+
+ var responder: UIResponder? = self
+ while responder != nil {
+ if let application = responder as? UIApplication {
+ application.open(url, options: [:], completionHandler: nil)
+ os_log("Application opened with URL: %@", url.absoluteString)
+ return true
+ }
+ responder = responder?.next
+ }
+ return false
+ }
+}
diff --git a/src/CONST.ts b/src/CONST.ts
index 0ef02fcbd328..8ab86cfc594a 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1799,7 +1799,69 @@ const CONST = {
PNG: 'image/png',
WEBP: 'image/webp',
JPEG: 'image/jpeg',
+ JPG: 'image/jpg',
+ GIF: 'image/gif',
+ TIF: 'image/tif',
+ TIFF: 'image/tiff',
},
+
+ RECEIPT_ALLOWED_FILE_TYPES: {
+ PNG: 'image/png',
+ WEBP: 'image/webp',
+ JPEG: 'image/jpeg',
+ JPG: 'image/jpg',
+ GIF: 'image/gif',
+ TIF: 'image/tif',
+ TIFF: 'image/tiff',
+ IMG: 'image/*',
+ HTML: 'text/html',
+ XML: 'text/xml',
+ RTF: 'application/rtf',
+ PDF: 'application/pdf',
+ OFFICE: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ MSWORD: 'application/msword',
+ ZIP: 'application/zip',
+ RFC822: 'message/rfc822',
+ },
+
+ SHARE_FILE_MIMETYPE: {
+ JPG: 'image/jpg',
+ JPEG: 'image/jpeg',
+ GIF: 'image/gif',
+ PNG: 'image/png',
+ WEBP: 'image/webp',
+ TIF: 'image/tif',
+ TIFF: 'image/tiff',
+ IMG: 'image/*',
+ PDF: 'application/pdf',
+ MSWORD: 'application/msword',
+ OFFICE: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ RTF: 'application/rtf',
+ ZIP: 'application/zip',
+ RFC822: 'message/rfc822',
+ TEXT: 'text/plain',
+ HTML: 'text/html',
+ XML: 'text/xml',
+ MPEG: 'audio/mpeg',
+ AAC: 'audio/aac',
+ FLAC: 'audio/flac',
+ WAV: 'audio/wav',
+ XWAV: 'audio/x-wav',
+ MP3: 'audio/mp3',
+ VORBIS: 'audio/vorbis',
+ XVORBIS: 'audio/x-vorbis',
+ OPUS: 'audio/opus',
+ MP4: 'video/mp4',
+ MP2T: 'video/mp2t',
+ WEBM: 'video/webm',
+ AVC: 'video/avc',
+ HEVC: 'video/hevc',
+ XVND8: 'video/x-vnd.on2.vp8',
+ XVND9: 'video/x-vnd.on2.vp9',
+ AV01: 'video/av01',
+ TXT: 'txt',
+ },
+
ATTACHMENT_TYPE: {
REPORT: 'r',
NOTE: 'n',
@@ -4950,6 +5012,11 @@ const CONST = {
NEW_ROOM: 'room',
RECEIPT_TAB_ID: 'ReceiptTab',
IOU_REQUEST_TYPE: 'iouRequestType',
+ SHARE: {
+ NAVIGATOR_ID: 'ShareNavigatorID',
+ SHARE: 'ShareTab',
+ SUBMIT: 'SubmitTab',
+ },
},
TAB_REQUEST: {
MANUAL: 'manual',
diff --git a/src/NAVIGATORS.ts b/src/NAVIGATORS.ts
index 0bf4059cfbd2..853e2591b499 100644
--- a/src/NAVIGATORS.ts
+++ b/src/NAVIGATORS.ts
@@ -15,5 +15,6 @@ export default {
SETTINGS_SPLIT_NAVIGATOR: 'SettingsSplitNavigator',
WORKSPACE_SPLIT_NAVIGATOR: 'WorkspaceSplitNavigator',
SEARCH_FULLSCREEN_NAVIGATOR: 'SearchFullscreenNavigator',
+ SHARE_MODAL_NAVIGATOR: 'ShareModalNavigator',
PUBLIC_RIGHT_MODAL_NAVIGATOR: 'PublicRightModalNavigator',
} as const;
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index db7b18d865e0..8b9abd213c81 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -4,7 +4,7 @@ import type {OnboardingCompanySize} from './CONST';
import type Platform from './libs/getPlatform/types';
import type * as FormTypes from './types/form';
import type * as OnyxTypes from './types/onyx';
-import type {Attendee} from './types/onyx/IOU';
+import type {Attendee, Participant} from './types/onyx/IOU';
import type Onboarding from './types/onyx/Onboarding';
import type AssertTypesEqual from './types/utils/AssertTypesEqual';
import type DeepValueOf from './types/utils/DeepValueOf';
@@ -464,6 +464,12 @@ const ONYXKEYS = {
/** The user's Concierge reportID */
CONCIERGE_REPORT_ID: 'conciergeReportID',
+ /** The details of unknown user while sharing a file - we don't know if they exist */
+ SHARE_UNKNOWN_USER_DETAILS: 'shareUnknownUserDetails',
+
+ /** Temporary file to be shared from outside the app */
+ SHARE_TEMP_FILE: 'shareTempFile',
+
/** Corpay fields to be used in the bank account creation setup */
CORPAY_FIELDS: 'corpayFields',
@@ -1081,6 +1087,8 @@ type OnyxValuesMapping = {
[ONYXKEYS.IS_USING_IMPORTED_STATE]: boolean;
[ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES]: Record;
[ONYXKEYS.CONCIERGE_REPORT_ID]: string;
+ [ONYXKEYS.SHARE_UNKNOWN_USER_DETAILS]: Participant;
+ [ONYXKEYS.SHARE_TEMP_FILE]: OnyxTypes.ShareTempFile;
[ONYXKEYS.CORPAY_FIELDS]: OnyxTypes.CorpayFields;
[ONYXKEYS.PRESERVED_USER_SESSION]: OnyxTypes.Session;
[ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING]: OnyxTypes.DismissedProductTraining;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index e722caeed905..c50a327d687a 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -1691,6 +1691,16 @@ const ROUTES = {
route: 'referral/:contentType',
getRoute: (contentType: string, backTo?: string) => getUrlWithBackToParam(`referral/${contentType}`, backTo),
},
+ SHARE_ROOT: 'share/root',
+ SHARE_DETAILS: {
+ route: 'share/share-details/:reportOrAccountID',
+ getRoute: (reportOrAccountID: string) => `share/share-details/${reportOrAccountID}` as const,
+ },
+ SHARE_SUBMIT_DETAILS: {
+ route: 'share/submit-details/:reportOrAccountID',
+ getRoute: (reportOrAccountID: string) => `share/submit-details/${reportOrAccountID}` as const,
+ },
+
PROCESS_MONEY_REQUEST_HOLD: {
route: 'hold-expense-educational',
getRoute: (backTo?: string) => getUrlWithBackToParam('hold-expense-educational', backTo),
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 526bcbaec712..f743eb2b6a3c 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -677,6 +677,11 @@ const SCREENS = {
REIMBURSEMENT_ACCOUNT: 'ReimbursementAccount',
REFERRAL_DETAILS: 'Referral_Details',
KEYBOARD_SHORTCUTS: 'KeyboardShortcuts',
+ SHARE: {
+ ROOT: 'Share_Root',
+ SHARE_DETAILS: 'Share_Details',
+ SUBMIT_DETAILS: 'Submit_Details',
+ },
TRANSACTION_RECEIPT: 'TransactionReceipt',
FEATURE_TRAINING_ROOT: 'FeatureTraining_Root',
RESTRICTED_ACTION_ROOT: 'RestrictedAction_Root',
diff --git a/src/components/AttachmentPreview.tsx b/src/components/AttachmentPreview.tsx
new file mode 100644
index 000000000000..c845833d8df7
--- /dev/null
+++ b/src/components/AttachmentPreview.tsx
@@ -0,0 +1,106 @@
+import {Str} from 'expensify-common';
+import {ResizeMode, Video} from 'expo-av';
+import React, {useState} from 'react';
+import {View} from 'react-native';
+import useThemeStyles from '@hooks/useThemeStyles';
+import variables from '@styles/variables';
+import {checkIsFileImage} from './Attachments/AttachmentView';
+import DefaultAttachmentView from './Attachments/AttachmentView/DefaultAttachmentView';
+import Icon from './Icon';
+import * as Expensicons from './Icon/Expensicons';
+import ImageView from './ImageView';
+import PDFThumbnail from './PDFThumbnail';
+import {PressableWithFeedback} from './Pressable';
+
+type AttachmentPreviewProps = {
+ /** Source for file. */
+ source: string;
+
+ /** Media's aspect ratio to calculate the thumbnail */
+ aspectRatio: number | undefined;
+
+ /** Function to call when pressing thumbnail */
+ onPress: () => void;
+
+ /** The attachment load error callback */
+ onLoadError?: () => void;
+};
+
+function AttachmentPreview({source, aspectRatio = 1, onPress, onLoadError}: AttachmentPreviewProps) {
+ const styles = useThemeStyles();
+
+ const fileName = source.split('/').pop() ?? undefined;
+ const fillStyle = aspectRatio < 1 ? styles.h100 : styles.w100;
+ const [isEncryptedPDF, setIsEncryptedPDF] = useState(false);
+
+ if (typeof source === 'string' && Str.isVideo(source)) {
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+ const isFileImage = checkIsFileImage(source, fileName);
+
+ if (isFileImage) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (typeof source === 'string' && Str.isPDF(source) && !isEncryptedPDF) {
+ return (
+ setIsEncryptedPDF(true)}
+ />
+ );
+ }
+
+ return ;
+}
+
+AttachmentPreview.displayName = 'AttachmentPreview';
+
+export default AttachmentPreview;
diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx
index 93729a8e0da3..3a76c3f5b663 100644
--- a/src/components/Attachments/AttachmentView/index.tsx
+++ b/src/components/Attachments/AttachmentView/index.tsx
@@ -1,13 +1,13 @@
import {Str} from 'expensify-common';
import React, {memo, useEffect, useState} from 'react';
-import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native';
+import type {GestureResponderEvent, ImageURISource, StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import type {Attachment, AttachmentSource} from '@components/Attachments/types';
import DistanceEReceipt from '@components/DistanceEReceipt';
import EReceipt from '@components/EReceipt';
import Icon from '@components/Icon';
-import * as Expensicons from '@components/Icon/Expensicons';
+import {Gallery} from '@components/Icon/Expensicons';
import PerDiemEReceipt from '@components/PerDiemEReceipt';
import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
@@ -85,6 +85,13 @@ type AttachmentViewProps = Attachment & {
reportID?: string;
};
+function checkIsFileImage(source: string | number | ImageURISource | ImageURISource[], fileName: string | undefined) {
+ const isSourceImage = typeof source === 'number' || (typeof source === 'string' && Str.isImage(source));
+ const isFileNameImage = fileName && Str.isImage(fileName);
+
+ return isSourceImage || isFileNameImage;
+}
+
function AttachmentView({
source,
previewSource,
@@ -229,9 +236,7 @@ function AttachmentView({
// For this check we use both source and file.name since temporary file source is a blob
// both PDFs and images will appear as images when pasted into the text field.
// We also check for numeric source since this is how static images (used for preview) are represented in RN.
- const isSourceImage = typeof source === 'number' || (typeof source === 'string' && Str.isImage(source));
- const isFileNameImage = file?.name && Str.isImage(file.name);
- const isFileImage = isSourceImage || isFileNameImage;
+ const isFileImage = checkIsFileImage(source, file?.name);
if (isFileImage) {
if (imageError && (typeof fallbackSource === 'number' || typeof fallbackSource === 'function')) {
@@ -258,7 +263,7 @@ function AttachmentView({
<>
void;
+
+ /** The PDF password callback */
+ onPDFPassword?: () => void;
};
type MoneyRequestConfirmationListItem = Participant | OptionData;
@@ -212,6 +218,8 @@ function MoneyRequestConfirmationList({
shouldPlaySound = true,
isConfirmed,
isConfirming,
+ onPDFLoadError,
+ onPDFPassword,
}: MoneyRequestConfirmationListProps) {
const [policyCategoriesReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`);
const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`);
@@ -1062,6 +1070,8 @@ function MoneyRequestConfirmationList({
transaction={transaction}
transactionID={transactionID}
unit={unit}
+ onPDFLoadError={onPDFLoadError}
+ onPDFPassword={onPDFPassword}
/>
);
diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx
index e8b23b164b9e..e728b7adc494 100644
--- a/src/components/MoneyRequestConfirmationListFooter.tsx
+++ b/src/components/MoneyRequestConfirmationListFooter.tsx
@@ -186,6 +186,12 @@ type MoneyRequestConfirmationListFooterProps = {
/** The unit */
unit: Unit | undefined;
+
+ /** The PDF load error callback */
+ onPDFLoadError?: () => void;
+
+ /** The PDF password callback */
+ onPDFPassword?: () => void;
};
function MoneyRequestConfirmationListFooter({
@@ -234,6 +240,8 @@ function MoneyRequestConfirmationListFooter({
transaction,
transactionID,
unit,
+ onPDFLoadError,
+ onPDFPassword,
}: MoneyRequestConfirmationListFooterProps) {
const styles = useThemeStyles();
const {translate, toLocaleDigit} = useLocalize();
@@ -708,6 +716,8 @@ function MoneyRequestConfirmationListFooter({
) : (
@@ -751,13 +761,15 @@ function MoneyRequestConfirmationListFooter({
translate,
shouldDisplayReceipt,
resolvedReceiptImage,
+ onPDFLoadError,
+ onPDFPassword,
isThumbnail,
resolvedThumbnail,
receiptThumbnail,
fileExtension,
isDistanceRequest,
- reportID,
transactionID,
+ reportID,
],
);
diff --git a/src/components/Share/ShareTabParticipantsSelector.tsx b/src/components/Share/ShareTabParticipantsSelector.tsx
new file mode 100644
index 000000000000..713b1be409d0
--- /dev/null
+++ b/src/components/Share/ShareTabParticipantsSelector.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import {saveUnknownUserDetails} from '@libs/actions/Share';
+import Navigation from '@libs/Navigation/Navigation';
+import MoneyRequestParticipantsSelector from '@pages/iou/request/MoneyRequestParticipantsSelector';
+import {getOptimisticChatReport, saveReportDraft} from '@userActions/Report';
+import CONST from '@src/CONST';
+import type ROUTES from '@src/ROUTES';
+
+type ShareTabParticipantsSelectorProps = {
+ detailsPageRouteObject: typeof ROUTES.SHARE_SUBMIT_DETAILS | typeof ROUTES.SHARE_DETAILS;
+};
+
+export default function ShareTabParticipantsSelector({detailsPageRouteObject}: ShareTabParticipantsSelectorProps) {
+ return (
+ {
+ const participant = value.at(0);
+ let reportID = participant?.reportID ?? CONST.DEFAULT_NUMBER_ID;
+ const accountID = participant?.accountID;
+ if (accountID && !reportID) {
+ saveUnknownUserDetails(participant);
+ const optimisticReport = getOptimisticChatReport(accountID);
+ reportID = optimisticReport.reportID;
+
+ saveReportDraft(reportID, optimisticReport).then(() => {
+ Navigation.navigate(detailsPageRouteObject.getRoute(reportID.toString()));
+ });
+ } else {
+ Navigation.navigate(detailsPageRouteObject.getRoute(reportID.toString()));
+ }
+ }}
+ action="create"
+ />
+ );
+}
diff --git a/src/components/Skeletons/TabNavigatorSkeleton.tsx b/src/components/Skeletons/TabNavigatorSkeleton.tsx
new file mode 100644
index 000000000000..e6def0a2026a
--- /dev/null
+++ b/src/components/Skeletons/TabNavigatorSkeleton.tsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import {View} from 'react-native';
+import {Rect} from 'react-native-svg';
+import SkeletonViewContentLoader from '@components/SkeletonViewContentLoader';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+
+function TabNavigatorSkeleton() {
+ const styles = useThemeStyles();
+ const theme = useTheme();
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+TabNavigatorSkeleton.displayName = 'TabNavigatorSkeleton';
+
+export default TabNavigatorSkeleton;
diff --git a/src/components/TabSelector/TabSelector.tsx b/src/components/TabSelector/TabSelector.tsx
index db819738adf1..daa728e87978 100644
--- a/src/components/TabSelector/TabSelector.tsx
+++ b/src/components/TabSelector/TabSelector.tsx
@@ -41,6 +41,10 @@ function getIconAndTitle(route: string, translate: LocaleContextProps['translate
return {icon: Expensicons.Hashtag, title: translate('tabSelector.room')};
case CONST.TAB_REQUEST.DISTANCE:
return {icon: Expensicons.Car, title: translate('common.distance')};
+ case CONST.TAB.SHARE.SHARE:
+ return {icon: Expensicons.UploadAlt, title: translate('common.share')};
+ case CONST.TAB.SHARE.SUBMIT:
+ return {icon: Expensicons.Receipt, title: translate('common.submit')};
case CONST.TAB_REQUEST.PER_DIEM:
return {icon: Expensicons.CalendarSolid, title: translate('common.perDiem')};
default:
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 9955d6957ea8..50fa7a6e86eb 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1137,6 +1137,10 @@ const translations = {
rates: 'Rates',
submitsTo: ({name}: SubmitsToParams) => `Submits to ${name}`,
},
+ share: {
+ shareToExpensify: 'Share to Expensify',
+ messageInputLabel: 'Message',
+ },
notificationPreferencesPage: {
header: 'Notification preferences',
label: 'Notify me about new messages',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 6f8d22970367..ebeea2f0f288 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1134,6 +1134,10 @@ const translations = {
rates: 'Tasas',
submitsTo: ({name}: SubmitsToParams) => `Se envía a ${name}`,
},
+ share: {
+ shareToExpensify: 'Compartir para Expensify',
+ messageInputLabel: 'Mensaje',
+ },
notificationPreferencesPage: {
header: 'Preferencias de avisos',
label: 'Avisar sobre nuevos mensajes',
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index abeae7e42701..892ec2d6ce65 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -58,6 +58,7 @@ import type ReactComponentModule from '@src/types/utils/ReactComponentModule';
import createRootStackNavigator from './createRootStackNavigator';
import {reportsSplitsWithEnteringAnimation, workspaceSplitsWithoutEnteringAnimation} from './createRootStackNavigator/GetStateForActionHandlers';
import defaultScreenOptions from './defaultScreenOptions';
+import {ShareModalStackNavigator} from './ModalStackNavigators';
import ExplanationModalNavigator from './Navigators/ExplanationModalNavigator';
import FeatureTrainingModalNavigator from './Navigators/FeatureTrainingModalNavigator';
import LeftModalNavigator from './Navigators/LeftModalNavigator';
@@ -580,6 +581,12 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
options={rootNavigatorScreenOptions.fullScreen}
component={DesktopSignInRedirectPage}
/>
+
require('../../../../pages/RestrictedAction/Workspace/WorkspaceRestrictedActionPage').default,
});
+const ShareModalStackNavigator = createModalStackNavigator({
+ [SCREENS.SHARE.ROOT]: () => require('@pages/Share/ShareRootPage').default,
+ [SCREENS.SHARE.SHARE_DETAILS]: () => require('@pages/Share/ShareDetailsPage').default,
+ [SCREENS.SHARE.SUBMIT_DETAILS]: () => require('@pages/Share/SubmitDetailsPage').default,
+});
+
const MissingPersonalDetailsModalStackNavigator = createModalStackNavigator({
[SCREENS.MISSING_PERSONAL_DETAILS_ROOT]: () => require('../../../../pages/MissingPersonalDetails').default,
});
@@ -781,6 +788,7 @@ export {
SearchReportModalStackNavigator,
RestrictedActionModalStackNavigator,
SearchAdvancedFiltersModalStackNavigator,
+ ShareModalStackNavigator,
SearchSavedSearchModalStackNavigator,
MissingPersonalDetailsModalStackNavigator,
DebugModalStackNavigator,
diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts
index bb796c74eee1..a23f27b527b9 100644
--- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts
+++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts
@@ -15,6 +15,7 @@ const MODAL_ROUTES_TO_DISMISS: string[] = [
NAVIGATORS.RIGHT_MODAL_NAVIGATOR,
NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR,
NAVIGATORS.FEATURE_TRANING_MODAL_NAVIGATOR,
+ NAVIGATORS.SHARE_MODAL_NAVIGATOR,
SCREENS.NOT_FOUND,
SCREENS.ATTACHMENTS,
SCREENS.TRANSACTION_RECEIPT,
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index f56b71e6519e..45983270edfc 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -1659,6 +1659,14 @@ const config: LinkingOptions['config'] = {
},
},
},
+ [NAVIGATORS.SHARE_MODAL_NAVIGATOR]: {
+ initialRouteName: SCREENS.SHARE.ROOT,
+ screens: {
+ [SCREENS.SHARE.ROOT]: {path: ROUTES.SHARE_ROOT},
+ [SCREENS.SHARE.SHARE_DETAILS]: {path: ROUTES.SHARE_DETAILS.route},
+ [SCREENS.SHARE.SUBMIT_DETAILS]: {path: ROUTES.SHARE_SUBMIT_DETAILS.route},
+ },
+ },
},
};
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 9d2a12f8d70c..dd029783650a 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1794,6 +1794,12 @@ type SharedScreensParamList = {
};
};
+type ShareNavigatorParamList = {
+ [SCREENS.SHARE.ROOT]: undefined;
+ [SCREENS.SHARE.SHARE_DETAILS]: {reportOrAccountID: string};
+ [SCREENS.SHARE.SUBMIT_DETAILS]: {reportOrAccountID: string};
+};
+
type PublicScreensParamList = SharedScreensParamList & {
[SCREENS.UNLINK_LOGIN]: {
accountID?: string;
@@ -1855,6 +1861,7 @@ type AuthScreensParamList = SharedScreensParamList & {
isFromReviewDuplicates?: string;
};
[SCREENS.CONNECTION_COMPLETE]: undefined;
+ [NAVIGATORS.SHARE_MODAL_NAVIGATOR]: NavigatorScreenParams;
[SCREENS.BANK_CONNECTION_COMPLETE]: undefined;
};
@@ -1997,6 +2004,7 @@ export type {
ReportSettingsNavigatorParamList,
ReportsSplitNavigatorParamList,
RestrictedActionParamList,
+ ShareNavigatorParamList,
RightModalNavigatorParamList,
RoomMembersNavigatorParamList,
RootNavigatorParamList,
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index d2fbe6a3322c..e299236b932d 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -27,7 +27,7 @@ import type {
TransactionViolation,
} from '@src/types/onyx';
import type {Attendee, Participant} from '@src/types/onyx/IOU';
-import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
+import type {Icon, PendingAction} from '@src/types/onyx/OnyxCommon';
import type DeepValueOf from '@src/types/utils/DeepValueOf';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import Timing from './actions/Timing';
@@ -155,13 +155,13 @@ type OptionTree = {
tooltipText: string;
isDisabled: boolean;
isSelected: boolean;
- pendingAction?: OnyxCommon.PendingAction;
+ pendingAction?: PendingAction;
} & Option;
type PayeePersonalDetails = {
text: string;
alternateText: string;
- icons: OnyxCommon.Icon[];
+ icons: Icon[];
descriptiveText: string;
login: string;
accountID: number;
@@ -234,8 +234,8 @@ type MemberForList = {
isDisabled: boolean;
accountID?: number;
login: string;
- icons?: OnyxCommon.Icon[];
- pendingAction?: OnyxCommon.PendingAction;
+ icons?: Icon[];
+ pendingAction?: PendingAction;
reportID: string;
};
@@ -251,7 +251,14 @@ type Options = {
selfDMChat?: OptionData | undefined;
};
-type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean; showPersonalDetails?: boolean};
+type PreviewConfig = {
+ showChatPreviewLine?: boolean;
+ forcePolicyNamePreview?: boolean;
+ showPersonalDetails?: boolean;
+ isDisabled?: boolean | null;
+ selected?: boolean;
+ isSelected?: boolean;
+};
type FilterUserToInviteConfig = Pick & {
canInviteUser?: boolean;
@@ -422,7 +429,7 @@ Onyx.connect({
* @param defaultValues {login: accountID} In workspace invite page, when new user is added we pass available data to opt in
* @returns Returns avatar data for a list of user accountIDs
*/
-function getAvatarsForAccountIDs(accountIDs: number[], personalDetails: OnyxEntry, defaultValues: Record = {}): OnyxCommon.Icon[] {
+function getAvatarsForAccountIDs(accountIDs: number[], personalDetails: OnyxEntry, defaultValues: Record = {}): Icon[] {
const reversedDefaultValues: Record = {};
Object.entries(defaultValues).forEach((item) => {
@@ -586,6 +593,24 @@ function getAlternateText(option: OptionData, {showChatPreviewLine = false, forc
: formatPhoneNumber(option.participantsList && option.participantsList.length > 0 ? option.participantsList.at(0)?.login ?? '' : '');
}
+/**
+ * Searches for a match when provided with a value
+ */
+function isSearchStringMatch(searchValue: string, searchText?: string | null, participantNames = new Set(), isReportChatRoom = false): boolean {
+ const searchWords = new Set(searchValue.replace(/,/g, ' ').split(' '));
+ const valueToSearch = searchText?.replace(new RegExp(/ /g), '');
+ let matching = true;
+ searchWords.forEach((word) => {
+ // if one of the word is not matching, we don't need to check further
+ if (!matching) {
+ return;
+ }
+ const matchRegex = new RegExp(Str.escapeForRegExp(word), 'i');
+ matching = matchRegex.test(valueToSearch ?? '') || (!isReportChatRoom && participantNames.has(word));
+ });
+ return matching;
+}
+
function isSearchStringMatchUserDetails(personalDetail: PersonalDetails, searchValue: string) {
let memberDetails = '';
if (personalDetail.login) {
@@ -753,7 +778,7 @@ function createOption(
reportActions: ReportActions,
config?: PreviewConfig,
): OptionData {
- const {showChatPreviewLine = false, forcePolicyNamePreview = false, showPersonalDetails = false} = config ?? {};
+ const {showChatPreviewLine = false, forcePolicyNamePreview = false, showPersonalDetails = false, selected, isSelected, isDisabled} = config ?? {};
const result: OptionData = {
text: undefined,
alternateText: undefined,
@@ -786,6 +811,9 @@ function createOption(
isOptimisticPersonalDetail: false,
lastMessageText: '',
lastVisibleActionCreated: undefined,
+ selected,
+ isSelected,
+ isDisabled,
};
const personalDetailMap = getPersonalDetailsForAccountIDs(accountIDs, personalDetails);
@@ -919,6 +947,43 @@ function getReportOption(participant: Participant): OptionData {
return option;
}
+/**
+ * Get the display option for a given report.
+ */
+function getReportDisplayOption(report: OnyxEntry, unknownUserDetails: OnyxEntry): OptionData {
+ const visibleParticipantAccountIDs = getParticipantsAccountIDsForDisplay(report, true);
+
+ const option = createOption(
+ visibleParticipantAccountIDs,
+ allPersonalDetails ?? {},
+ !isEmptyObject(report) ? report : undefined,
+ {},
+ {
+ showChatPreviewLine: false,
+ forcePolicyNamePreview: false,
+ },
+ );
+
+ // Update text & alternateText because createOption returns workspace name only if report is owned by the user
+ if (option.isSelfDM) {
+ option.alternateText = translateLocal('reportActionsView.yourSpace');
+ } else if (option.isInvoiceRoom) {
+ option.text = getReportName(report);
+ option.alternateText = translateLocal('workspace.common.invoices');
+ } else if (unknownUserDetails && !option.text) {
+ option.text = unknownUserDetails.text ?? unknownUserDetails.login;
+ option.alternateText = unknownUserDetails.login;
+ option.participantsList = [{...unknownUserDetails, displayName: unknownUserDetails.login, accountID: unknownUserDetails.accountID ?? CONST.DEFAULT_NUMBER_ID}];
+ } else if (report?.ownerAccountID !== 0 || !option.text) {
+ option.text = getPolicyName({report});
+ option.alternateText = translateLocal('workspace.common.workspace');
+ }
+ option.isDisabled = true;
+ option.selected = false;
+ option.isSelected = false;
+
+ return option;
+}
/**
* Get the option for a policy expense report.
*/
@@ -948,24 +1013,6 @@ function getPolicyExpenseReportOption(participant: Participant | OptionData): Op
return option;
}
-/**
- * Searches for a match when provided with a value
- */
-function isSearchStringMatch(searchValue: string, searchText?: string | null, participantNames = new Set(), isChatRoom = false): boolean {
- const searchWords = new Set(searchValue.replace(/,/g, ' ').split(' '));
- const valueToSearch = searchText?.replace(new RegExp(/ /g), '');
- let matching = true;
- searchWords.forEach((word) => {
- // if one of the word is not matching, we don't need to check further
- if (!matching) {
- return;
- }
- const matchRegex = new RegExp(Str.escapeForRegExp(word), 'i');
- matching = matchRegex.test(valueToSearch ?? '') || (!isChatRoom && participantNames.has(word));
- });
- return matching;
-}
-
/**
* Checks if the given userDetails is currentUser or not.
* Note: We can't migrate this off of using logins because this is used to check if you're trying to start a chat with
@@ -1842,8 +1889,8 @@ function formatSectionsFromSearchTerm(
title: undefined,
data: shouldGetOptionDetails
? selectedOptions.map((participant) => {
- const isPolicyExpenseChat = participant.isPolicyExpenseChat ?? false;
- return isPolicyExpenseChat ? getPolicyExpenseReportOption(participant) : getParticipantsOption(participant, personalDetails);
+ const isReportPolicyExpenseChat = participant.isPolicyExpenseChat ?? false;
+ return isReportPolicyExpenseChat ? getPolicyExpenseReportOption(participant) : getParticipantsOption(participant, personalDetails);
})
: selectedOptions,
shouldShow: selectedOptions.length > 0,
@@ -1868,8 +1915,8 @@ function formatSectionsFromSearchTerm(
title: undefined,
data: shouldGetOptionDetails
? selectedParticipantsWithoutDetails.map((participant) => {
- const isPolicyExpenseChat = participant.isPolicyExpenseChat ?? false;
- return isPolicyExpenseChat ? getPolicyExpenseReportOption(participant) : getParticipantsOption(participant, personalDetails);
+ const isReportPolicyExpenseChat = participant.isPolicyExpenseChat ?? false;
+ return isReportPolicyExpenseChat ? getPolicyExpenseReportOption(participant) : getParticipantsOption(participant, personalDetails);
})
: selectedParticipantsWithoutDetails,
shouldShow: selectedParticipantsWithoutDetails.length > 0,
@@ -2206,6 +2253,7 @@ export {
shouldUseBoldText,
getAttendeeOptions,
getAlternateText,
+ getReportDisplayOption,
hasReportErrors,
combineOrderingOfReportsAndPersonalDetails,
filterWorkspaceChats,
diff --git a/src/libs/ShareActionHandlerModule/index.ts b/src/libs/ShareActionHandlerModule/index.ts
new file mode 100644
index 000000000000..205bd89960d9
--- /dev/null
+++ b/src/libs/ShareActionHandlerModule/index.ts
@@ -0,0 +1,21 @@
+import {NativeModules} from 'react-native';
+
+const {ShareActionHandler} = NativeModules;
+
+// Type for the content of a share action
+type ShareActionContent = {
+ id: string;
+ content: string;
+ mimeType: string;
+ processedAt: string;
+ aspectRatio: number;
+};
+
+// Type for the ShareActionHandler module
+type ShareActionHandlerModule = {
+ // Method to process files, which takes a callback function
+ processFiles(callback: (array: ShareActionContent[]) => void): void;
+};
+
+export default ShareActionHandler;
+export type {ShareActionHandlerModule};
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 660926815b25..7ea897a2532c 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -1160,6 +1160,17 @@ function openReport(
}
}
+/**
+ * This will return an optimistic report object for a given user we want to create a chat with without saving it, when the only thing we know about recipient is his accountID. *
+ * @param accountID accountID of the user that the optimistic chat report is created with.
+ */
+function getOptimisticChatReport(accountID: number): OptimisticChatReport {
+ return buildOptimisticChatReport({
+ participantList: [accountID, currentUserAccountID],
+ notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS,
+ });
+}
+
/**
* This will find an existing chat, or create a new one if none exists, for the given user or set of users. It will then navigate to this chat.
*
@@ -1513,6 +1524,11 @@ function togglePinnedState(reportID: string | undefined, isPinnedChat: boolean)
API.write(WRITE_COMMANDS.TOGGLE_PINNED_CHAT, parameters, {optimisticData});
}
+/** Saves the report draft to Onyx */
+function saveReportDraft(reportID: string, report: Report) {
+ return Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${reportID}`, report);
+}
+
/**
* Saves the comment left by the user as they are typing. By saving this data the user can switch between chats, close
* tab, refresh etc without worrying about loosing what they typed out.
@@ -5164,6 +5180,8 @@ export {
updateReportName,
updateRoomVisibility,
updateWriteCapability,
+ getOptimisticChatReport,
+ saveReportDraft,
prepareOnboardingOnyxData,
dismissChangePolicyModal,
changeReportPolicy,
diff --git a/src/libs/actions/Share.ts b/src/libs/actions/Share.ts
new file mode 100644
index 000000000000..6341b30766e6
--- /dev/null
+++ b/src/libs/actions/Share.ts
@@ -0,0 +1,35 @@
+import Onyx from 'react-native-onyx';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {ShareTempFile} from '@src/types/onyx';
+import type {Participant} from '@src/types/onyx/IOU';
+
+/**
+Function for clearing old saved data before at the start of share-extension flow
+ */
+function clearShareData() {
+ Onyx.multiSet({
+ [ONYXKEYS.SHARE_TEMP_FILE]: null,
+ [ONYXKEYS.SHARE_UNKNOWN_USER_DETAILS]: null,
+ });
+}
+
+/**
+Function storing natively shared file's properties for processing across share-extension screens
+
+function addTempShareFile(file: ShareTempFile) {
+ * @param file shared file's object with additional props
+ */
+function addTempShareFile(file: ShareTempFile) {
+ Onyx.merge(ONYXKEYS.SHARE_TEMP_FILE, file);
+}
+
+/**
+Function storing selected user's details for the duration of share-extension flow, if account doesn't exist
+
+ * @param user selected user's details
+ */
+function saveUnknownUserDetails(user: Participant) {
+ Onyx.merge(ONYXKEYS.SHARE_UNKNOWN_USER_DETAILS, user);
+}
+
+export {addTempShareFile, saveUnknownUserDetails, clearShareData};
diff --git a/src/pages/Share/ShareDetailsPage.tsx b/src/pages/Share/ShareDetailsPage.tsx
new file mode 100644
index 000000000000..03ed527d5d58
--- /dev/null
+++ b/src/pages/Share/ShareDetailsPage.tsx
@@ -0,0 +1,182 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useMemo, useState} from 'react';
+import {SafeAreaView, View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
+import AttachmentModal from '@components/AttachmentModal';
+import AttachmentPreview from '@components/AttachmentPreview';
+import Button from '@components/Button';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import {FallbackAvatar} from '@components/Icon/Expensicons';
+import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
+import Text from '@components/Text';
+import TextInput from '@components/TextInput';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {addAttachment, addComment, getCurrentUserAccountID, openReport} from '@libs/actions/Report';
+import {canUseTouchScreen} from '@libs/DeviceCapabilities';
+import {getFileName, readFileAsync} from '@libs/fileDownload/FileUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import type {ShareNavigatorParamList} from '@libs/Navigation/types';
+import {getReportDisplayOption} from '@libs/OptionsListUtils';
+import {getReportOrDraftReport, isDraftReport} from '@libs/ReportUtils';
+import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
+import UserListItem from '@src/components/SelectionList/UserListItem';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import type {Report as ReportType} from '@src/types/onyx';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import {showErrorAlert} from './ShareRootPage';
+
+type ShareDetailsPageProps = StackScreenProps;
+
+function ShareDetailsPage({
+ route: {
+ params: {reportOrAccountID},
+ },
+}: ShareDetailsPageProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const [unknownUserDetails] = useOnyx(ONYXKEYS.SHARE_UNKNOWN_USER_DETAILS);
+ const [currentAttachment] = useOnyx(ONYXKEYS.SHARE_TEMP_FILE);
+ const isTextShared = currentAttachment?.mimeType === 'txt';
+ const [message, setMessage] = useState(isTextShared ? currentAttachment?.content ?? '' : '');
+
+ const report: OnyxEntry = getReportOrDraftReport(reportOrAccountID);
+ const displayReport = useMemo(() => getReportDisplayOption(report, unknownUserDetails), [report, unknownUserDetails]);
+ if (isEmptyObject(report)) {
+ return ;
+ }
+
+ const isDraft = isDraftReport(reportOrAccountID);
+ const currentUserID = getCurrentUserAccountID();
+ const shouldShowAttachment = !isTextShared;
+
+ const fileName = currentAttachment?.content.split('/').pop();
+
+ const handleShare = () => {
+ if (!currentAttachment) {
+ return;
+ }
+
+ if (isTextShared) {
+ addComment(report.reportID, message);
+ const routeToNavigate = ROUTES.REPORT_WITH_ID.getRoute(reportOrAccountID);
+ Navigation.navigate(routeToNavigate);
+ return;
+ }
+
+ readFileAsync(
+ currentAttachment.content,
+ getFileName(currentAttachment.content),
+ (file) => {
+ if (isDraft) {
+ openReport(
+ report.reportID,
+ '',
+ displayReport.participantsList?.filter((u) => u.accountID !== currentUserID).map((u) => u.login ?? '') ?? [],
+ report,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ );
+ }
+ if (report.reportID) {
+ addAttachment(report.reportID, file, message);
+ }
+
+ const routeToNavigate = ROUTES.REPORT_WITH_ID.getRoute(reportOrAccountID);
+ Navigation.navigate(routeToNavigate, {forceReplace: true});
+ },
+ () => {},
+ );
+ };
+
+ return (
+
+
+
+ {!!report && (
+ <>
+
+ {translate('common.to')}
+
+ {}}
+ pressableStyle={[styles.flexRow]}
+ shouldSyncFocus={false}
+ isDisabled
+ />
+ >
+ )}
+
+
+
+
+
+
+
+ {shouldShowAttachment && (
+ <>
+
+ {translate('common.attachment')}
+
+
+
+ {({show}) => (
+ {
+ showErrorAlert(translate('attachmentPicker.attachmentError'), translate('attachmentPicker.errorWhileSelectingCorruptedAttachment'));
+ }}
+ />
+ )}
+
+
+ >
+ )}
+
+
+
+
+
+
+
+ );
+}
+
+ShareDetailsPage.displayName = 'ShareDetailsPage';
+export default ShareDetailsPage;
diff --git a/src/pages/Share/ShareRootPage.tsx b/src/pages/Share/ShareRootPage.tsx
new file mode 100644
index 000000000000..de8da74f2fa3
--- /dev/null
+++ b/src/pages/Share/ShareRootPage.tsx
@@ -0,0 +1,140 @@
+import React, {useCallback, useEffect, useRef, useState} from 'react';
+import {Alert, AppState, View} from 'react-native';
+import type {FileObject} from '@components/AttachmentModal';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import TabNavigatorSkeleton from '@components/Skeletons/TabNavigatorSkeleton';
+import TabSelector from '@components/TabSelector/TabSelector';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {addTempShareFile, clearShareData} from '@libs/actions/Share';
+import {canUseTouchScreen} from '@libs/DeviceCapabilities';
+import {splitExtensionFromFileName, validateImageForCorruption} from '@libs/fileDownload/FileUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import OnyxTabNavigator, {TopTab} from '@libs/Navigation/OnyxTabNavigator';
+import ShareActionHandler from '@libs/ShareActionHandlerModule';
+import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
+import type {ShareTempFile} from '@src/types/onyx';
+import ShareTab from './ShareTab';
+import SubmitTab from './SubmitTab';
+
+function showErrorAlert(title: string, message: string) {
+ Alert.alert(title, message, [
+ {
+ onPress: () => {
+ Navigation.navigate(ROUTES.HOME);
+ },
+ },
+ ]);
+ Navigation.navigate(ROUTES.HOME);
+}
+
+function ShareRootPage() {
+ const appState = useRef(AppState.currentState);
+ const [isFileReady, setIsFileReady] = useState(false);
+
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const [isFileScannable, setIsFileScannable] = useState(false);
+ const receiptFileFormats = Object.values(CONST.RECEIPT_ALLOWED_FILE_TYPES) as string[];
+ const shareFileMimetypes = Object.values(CONST.SHARE_FILE_MIMETYPE) as string[];
+
+ const handleProcessFiles = useCallback(() => {
+ ShareActionHandler.processFiles((processedFiles) => {
+ const tempFile = Array.isArray(processedFiles) ? processedFiles.at(0) : (JSON.parse(processedFiles) as ShareTempFile);
+ if (!tempFile?.mimeType || !shareFileMimetypes.includes(tempFile?.mimeType)) {
+ showErrorAlert(translate('attachmentPicker.wrongFileType'), translate('attachmentPicker.notAllowedExtension'));
+ return;
+ }
+
+ const fileRegexp = /image\/.*/;
+ if (fileRegexp.test(tempFile?.mimeType)) {
+ const fileObject: FileObject = {name: tempFile.id, uri: tempFile?.content, type: tempFile?.mimeType};
+ validateImageForCorruption(fileObject)
+ .then(() => {
+ if (fileObject.size && fileObject.size > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
+ showErrorAlert(translate('attachmentPicker.attachmentTooLarge'), translate('attachmentPicker.sizeExceeded'));
+ }
+
+ if (fileObject.size && fileObject.size < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
+ showErrorAlert(translate('attachmentPicker.attachmentTooSmall'), translate('attachmentPicker.sizeNotMet'));
+ }
+
+ return true;
+ })
+ .catch(() => {
+ showErrorAlert(translate('attachmentPicker.attachmentError'), translate('attachmentPicker.errorWhileSelectingCorruptedAttachment'));
+ });
+ }
+
+ const {fileExtension} = splitExtensionFromFileName(tempFile?.content);
+ if (tempFile) {
+ if (tempFile.mimeType) {
+ if (receiptFileFormats.includes(tempFile.mimeType) && fileExtension) {
+ setIsFileScannable(true);
+ } else {
+ setIsFileScannable(false);
+ }
+ setIsFileReady(true);
+ }
+
+ addTempShareFile(tempFile);
+ }
+ });
+ }, [receiptFileFormats, shareFileMimetypes, translate]);
+
+ useEffect(() => {
+ const subscription = AppState.addEventListener('change', (nextAppState) => {
+ if (appState.current.match(/inactive|background/) && nextAppState === 'active') {
+ handleProcessFiles();
+ }
+
+ appState.current = nextAppState;
+ });
+
+ return () => {
+ subscription.remove();
+ };
+ }, [handleProcessFiles]);
+
+ useEffect(() => {
+ clearShareData();
+ handleProcessFiles();
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+
+
+ Navigation.navigate(ROUTES.HOME)}
+ />
+ {isFileReady ? (
+
+ {() => }
+ {isFileScannable && {() => }}
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
+ShareRootPage.displayName = 'ShareRootPage';
+
+export default ShareRootPage;
+
+export {showErrorAlert};
diff --git a/src/pages/Share/ShareTab.tsx b/src/pages/Share/ShareTab.tsx
new file mode 100644
index 000000000000..461a01943862
--- /dev/null
+++ b/src/pages/Share/ShareTab.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ShareTabParticipantsSelector from '@components/Share/ShareTabParticipantsSelector';
+import ROUTES from '@src/ROUTES';
+
+function ShareTab() {
+ return ;
+}
+
+export default ShareTab;
diff --git a/src/pages/Share/SubmitDetailsPage.tsx b/src/pages/Share/SubmitDetailsPage.tsx
new file mode 100644
index 000000000000..70642eb8120b
--- /dev/null
+++ b/src/pages/Share/SubmitDetailsPage.tsx
@@ -0,0 +1,191 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useEffect, useState} from 'react';
+import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
+import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import LocationPermissionModal from '@components/LocationPermissionModal';
+import MoneyRequestConfirmationList from '@components/MoneyRequestConfirmationList';
+import ScreenWrapper from '@components/ScreenWrapper';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import type {GpsPoint} from '@libs/actions/IOU';
+import {getIOURequestPolicyID, getMoneyRequestParticipantsFromReport, initMoneyRequest, requestMoney, updateLastLocationPermissionPrompt} from '@libs/actions/IOU';
+import DateUtils from '@libs/DateUtils';
+import {getFileName, readFileAsync} from '@libs/fileDownload/FileUtils';
+import getCurrentPosition from '@libs/getCurrentPosition';
+import Log from '@libs/Log';
+import Navigation from '@libs/Navigation/Navigation';
+import type {ShareNavigatorParamList} from '@libs/Navigation/types';
+import {getParticipantsOption, getReportOption} from '@libs/OptionsListUtils';
+import {getReportOrDraftReport} from '@libs/ReportUtils';
+import {getDefaultTaxCode} from '@libs/TransactionUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type SCREENS from '@src/SCREENS';
+import type {Report as ReportType} from '@src/types/onyx';
+import type {Participant} from '@src/types/onyx/IOU';
+import type {Receipt} from '@src/types/onyx/Transaction';
+import {showErrorAlert} from './ShareRootPage';
+
+type ShareDetailsPageProps = StackScreenProps;
+function SubmitDetailsPage({
+ route: {
+ params: {reportOrAccountID},
+ },
+}: ShareDetailsPageProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const [currentAttachment] = useOnyx(ONYXKEYS.SHARE_TEMP_FILE);
+ const [unknownUserDetails] = useOnyx(ONYXKEYS.SHARE_UNKNOWN_USER_DETAILS);
+ const [personalDetails] = useOnyx(`${ONYXKEYS.PERSONAL_DETAILS_LIST}`);
+ const report: OnyxEntry = getReportOrDraftReport(reportOrAccountID);
+ const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`);
+ const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`);
+ const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${getIOURequestPolicyID(transaction, report)}`);
+ const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${getIOURequestPolicyID(transaction, report)}`);
+ const [lastLocationPermissionPrompt] = useOnyx(ONYXKEYS.NVP_LAST_LOCATION_PERMISSION_PROMPT);
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
+ const [startLocationPermissionFlow, setStartLocationPermissionFlow] = useState(false);
+
+ useEffect(() => {
+ initMoneyRequest(reportOrAccountID, policy, false, CONST.IOU.REQUEST_TYPE.SCAN, CONST.IOU.REQUEST_TYPE.SCAN);
+ }, [reportOrAccountID, policy]);
+
+ const selectedParticipants = unknownUserDetails ? [unknownUserDetails] : getMoneyRequestParticipantsFromReport(report);
+ const participants = selectedParticipants.map((participant) => (participant?.accountID ? getParticipantsOption(participant, personalDetails) : getReportOption(participant)));
+
+ const trimmedComment = transaction?.comment?.comment?.trim() ?? '';
+ const transactionAmount = transaction?.amount ?? 0;
+ const transactionTaxAmount = transaction?.taxAmount ?? 0;
+ const defaultTaxCode = getDefaultTaxCode(policy, transaction);
+ const transactionTaxCode = (transaction?.taxCode ? transaction?.taxCode : defaultTaxCode) ?? '';
+
+ const finishRequestAndNavigate = (participant: Participant, receipt: Receipt, gpsPoints?: GpsPoint) => {
+ if (!transaction) {
+ return;
+ }
+ requestMoney({
+ report,
+ participantParams: {payeeEmail: currentUserPersonalDetails.login, payeeAccountID: currentUserPersonalDetails.accountID, participant},
+ policyParams: {policy, policyTagList: policyTags, policyCategories},
+ gpsPoints,
+ action: CONST.IOU.TYPE.CREATE,
+ transactionParams: {
+ attendees: transaction.attendees,
+ amount: transactionAmount,
+ currency: transaction.currency,
+ comment: trimmedComment,
+ receipt,
+ category: transaction.category,
+ tag: transaction.tag,
+ taxCode: transactionTaxCode,
+ taxAmount: transactionTaxAmount,
+ billable: transaction.billable,
+ merchant: transaction.merchant ?? '',
+ created: transaction.created,
+ actionableWhisperReportActionID: transaction.actionableWhisperReportActionID,
+ linkedTrackedExpenseReportAction: transaction.linkedTrackedExpenseReportAction,
+ linkedTrackedExpenseReportID: transaction.linkedTrackedExpenseReportID,
+ },
+ });
+ };
+
+ const onSuccess = (file: File, locationPermissionGranted?: boolean) => {
+ const participant = selectedParticipants.at(0);
+ if (!participant) {
+ return;
+ }
+
+ const receipt: Receipt = file;
+ receipt.state = file && CONST.IOU.RECEIPT_STATE.SCANREADY;
+ if (locationPermissionGranted) {
+ getCurrentPosition(
+ (successData) => {
+ finishRequestAndNavigate(participant, receipt, {
+ lat: successData.coords.latitude,
+ long: successData.coords.longitude,
+ });
+ },
+ (errorData) => {
+ Log.info('[SubmitDetailsPage] getCurrentPosition failed', false, errorData);
+ finishRequestAndNavigate(participant, receipt);
+ },
+ {
+ maximumAge: CONST.GPS.MAX_AGE,
+ timeout: CONST.GPS.TIMEOUT,
+ },
+ );
+ return;
+ }
+ finishRequestAndNavigate(participant, receipt);
+ };
+
+ const onConfirm = (gpsRequired?: boolean) => {
+ const shouldStartLocationPermissionFlow =
+ gpsRequired &&
+ (!lastLocationPermissionPrompt ||
+ (DateUtils.isValidDateString(lastLocationPermissionPrompt ?? '') &&
+ DateUtils.getDifferenceInDaysFromNow(new Date(lastLocationPermissionPrompt ?? '')) > CONST.IOU.LOCATION_PERMISSION_PROMPT_THRESHOLD_DAYS));
+
+ if (shouldStartLocationPermissionFlow) {
+ setStartLocationPermissionFlow(true);
+ return;
+ }
+
+ readFileAsync(
+ currentAttachment?.content ?? '',
+ getFileName(currentAttachment?.content ?? 'shared_image.png'),
+ (file) => onSuccess(file, shouldStartLocationPermissionFlow),
+ () => {},
+ currentAttachment?.mimeType ?? 'image/jpeg',
+ );
+ };
+
+ return (
+
+
+ Navigation.goBack()}
+ />
+ setStartLocationPermissionFlow(false)}
+ onGrant={onConfirm}
+ onDeny={() => {
+ updateLastLocationPermissionPrompt();
+ setStartLocationPermissionFlow(false);
+ onConfirm(false);
+ }}
+ />
+
+ onConfirm(true)}
+ receiptPath={currentAttachment?.content}
+ receiptFilename={getFileName(currentAttachment?.content ?? '')}
+ reportID={reportOrAccountID}
+ shouldShowSmartScanFields={false}
+ onPDFLoadError={() => {
+ showErrorAlert(translate('attachmentPicker.attachmentError'), translate('attachmentPicker.errorWhileSelectingCorruptedAttachment'));
+ }}
+ onPDFPassword={() => {
+ showErrorAlert(translate('attachmentPicker.attachmentError'), translate('attachmentPicker.protectedPDFNotSupported'));
+ }}
+ />
+
+
+
+ );
+}
+
+SubmitDetailsPage.displayName = 'SubmitDetailsPage';
+
+export default SubmitDetailsPage;
diff --git a/src/pages/Share/SubmitTab.tsx b/src/pages/Share/SubmitTab.tsx
new file mode 100644
index 000000000000..c12c8f8863d6
--- /dev/null
+++ b/src/pages/Share/SubmitTab.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ShareTabParticipantsSelector from '@components/Share/ShareTabParticipantsSelector';
+import ROUTES from '@src/ROUTES';
+
+function SubmitTab() {
+ return ;
+}
+
+export default SubmitTab;
diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx
index a29cf2090931..9cf40dbbf153 100644
--- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx
+++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx
@@ -49,7 +49,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';
type MoneyRequestParticipantsSelectorProps = {
/** Callback to request parent modal to go to next step, which should be split */
- onFinish: (value?: string) => void;
+ onFinish?: (value?: string) => void;
/** Callback to add participants in MoneyRequestModal */
onParticipantsAdded: (value: Participant[]) => void;
@@ -64,7 +64,14 @@ type MoneyRequestParticipantsSelectorProps = {
action: IOUAction;
};
-function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onFinish, onParticipantsAdded, iouType, action}: MoneyRequestParticipantsSelectorProps) {
+function MoneyRequestParticipantsSelector({
+ participants = CONST.EMPTY_ARRAY,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ onFinish = (_value?: string) => {},
+ onParticipantsAdded,
+ iouType,
+ action,
+}: MoneyRequestParticipantsSelectorProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');
diff --git a/src/styles/utils/spacing.ts b/src/styles/utils/spacing.ts
index 526ae3a9b18d..90f42cc56eb9 100644
--- a/src/styles/utils/spacing.ts
+++ b/src/styles/utils/spacing.ts
@@ -621,6 +621,10 @@ export default {
paddingTop: 20,
},
+ pt6: {
+ paddingTop: 24,
+ },
+
pt8: {
paddingTop: 32,
},
diff --git a/src/types/modules/react-native.d.ts b/src/types/modules/react-native.d.ts
index 0b09824509b5..5acd3ae24879 100644
--- a/src/types/modules/react-native.d.ts
+++ b/src/types/modules/react-native.d.ts
@@ -3,6 +3,7 @@ import type {TargetedEvent} from 'react-native';
import type {BootSplashModule} from '@libs/BootSplash/types';
import type {EnvironmentCheckerModule} from '@libs/Environment/betaChecker/types';
import type {NavBarButtonStyle, NavigationBarType} from '@libs/NavBarManager/types';
+import type {ShareActionHandlerModule} from '@libs/ShareActionHandlerModule';
import type {ShortcutManagerModule} from '@libs/ShortcutManager';
import type StartupTimer from '@libs/StartupTimer/types';
@@ -44,6 +45,7 @@ declare module 'react-native' {
RNNavBarManager: RNNavBarManagerModule;
EnvironmentChecker: EnvironmentCheckerModule;
ShortcutManager: ShortcutManagerModule;
+ ShareActionHandler: ShareActionHandlerModule;
}
namespace Animated {
diff --git a/src/types/onyx/ShareTempFile.ts b/src/types/onyx/ShareTempFile.ts
new file mode 100644
index 000000000000..54a0de272c49
--- /dev/null
+++ b/src/types/onyx/ShareTempFile.ts
@@ -0,0 +1,22 @@
+/** Model of the file shared from the external source */
+type ShareTempFile = {
+ /** ID of the share file */
+ id: string;
+
+ /** Path to the file copy in the app folder, or text content */
+ content: string;
+
+ /** Mime type of the file */
+ mimeType?: string;
+
+ /** ID of the report this share file is, or will be attached to */
+ reportID?: string;
+
+ /** Timestamp of when the share attempt started */
+ processedAt?: string;
+
+ /** Aspect ratio of the image */
+ aspectRatio?: number;
+};
+
+export default ShareTempFile;
diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts
index 46d6a2108fc7..c50150c4a456 100644
--- a/src/types/onyx/index.ts
+++ b/src/types/onyx/index.ts
@@ -95,6 +95,7 @@ import type SearchResults from './SearchResults';
import type SecurityGroup from './SecurityGroup';
import type SelectedTabRequest from './SelectedTabRequest';
import type Session from './Session';
+import type ShareTempFile from './ShareTempFile';
import type SidePane from './SidePane';
import type StripeCustomerID from './StripeCustomerID';
import type Task from './Task';
@@ -246,6 +247,7 @@ export type {
Onboarding,
OnboardingPurpose,
ValidateMagicCodeAction,
+ ShareTempFile,
CorpayFields,
CorpayFormField,
JoinablePolicies,