From 8fda09452bec60e1bb00c998593542a4ad937c15 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 19 Oct 2026 20:41:19 -0700 Subject: [PATCH] refactor(share): migrate native bridge to Nitro Generate one shared-file contract for Swift, Kotlin, and TypeScript. Keep native storage internal and preserve the existing Onyx timestamp format. Refs: https://github.com/Expensify/App/issues/104432 --- .../expensify/chat/ExpensifyAppPackage.java | 1 - .../chat/ShareActionHandlerModule.kt | 90 ---------- .../chat/intentHandler/ShareFileObject.kt | 2 + .../chat/intentHandler/TextIntentHandler.kt | 2 +- ios/NewExpensify.xcodeproj/project.pbxproj | 6 - ios/NewExpensify/Info.plist | 2 + ios/RCTShareActionHandlerModule.h | 16 -- ios/RCTShareActionHandlerModule.m | 129 -------------- .../nitro/utils/HybridShareActionHandler.kt | 76 ++++++++ .../ios/share/HybridShareActionHandler.swift | 62 +++++++ modules/ExpensifyNitroUtils/nitro.json | 4 + .../ExpensifyNitroUtils+autolinking.cmake | 2 + .../android/ExpensifyNitroUtilsOnLoad.cpp | 16 ++ .../android/c++/JHybridContactsModuleSpec.cpp | 1 - .../c++/JHybridShareActionHandlerSpec.cpp | 80 +++++++++ .../c++/JHybridShareActionHandlerSpec.hpp | 63 +++++++ .../generated/android/c++/JSharedFile.hpp | 74 ++++++++ .../utils/HybridShareActionHandlerSpec.kt | 57 ++++++ .../com/margelo/nitro/utils/SharedFile.kt | 71 ++++++++ .../ExpensifyNitroUtils-Swift-Cxx-Bridge.cpp | 25 +++ .../ExpensifyNitroUtils-Swift-Cxx-Bridge.hpp | 89 ++++++++++ ...ExpensifyNitroUtils-Swift-Cxx-Umbrella.hpp | 8 + .../ios/ExpensifyNitroUtilsAutolinking.mm | 8 + .../ios/ExpensifyNitroUtilsAutolinking.swift | 12 ++ .../ios/c++/HybridContactsModuleSpecSwift.hpp | 1 - .../c++/HybridShareActionHandlerSpecSwift.cpp | 11 ++ .../c++/HybridShareActionHandlerSpecSwift.hpp | 87 +++++++++ .../Func_void_std__vector_SharedFile_.swift | 46 +++++ .../swift/HybridShareActionHandlerSpec.swift | 55 ++++++ .../HybridShareActionHandlerSpec_cxx.swift | 151 ++++++++++++++++ .../generated/ios/swift/SharedFile.swift | 62 +++++++ .../c++/HybridShareActionHandlerSpec.cpp | 21 +++ .../c++/HybridShareActionHandlerSpec.hpp | 65 +++++++ .../generated/shared/c++/SharedFile.hpp | 100 +++++++++++ modules/ExpensifyNitroUtils/src/index.ts | 5 +- .../src/specs/ShareActionHandler.nitro.ts | 19 ++ .../ShareActionHandlerModule/index.native.ts | 4 + src/libs/ShareActionHandlerModule/index.ts | 23 +-- src/pages/Share/ShareRootPage.tsx | 168 +++++++++--------- src/types/modules/react-native.d.ts | 2 - tests/ui/ShareRootPageTest.tsx | 92 ++++++++++ tests/unit/ShareActionHandlerModuleTest.ts | 52 ++++++ 42 files changed, 1514 insertions(+), 346 deletions(-) delete mode 100644 android/app/src/main/java/com/expensify/chat/ShareActionHandlerModule.kt delete mode 100644 ios/RCTShareActionHandlerModule.h delete mode 100644 ios/RCTShareActionHandlerModule.m create mode 100644 modules/ExpensifyNitroUtils/android/src/main/java/com/margelo/nitro/utils/HybridShareActionHandler.kt create mode 100644 modules/ExpensifyNitroUtils/ios/share/HybridShareActionHandler.swift create mode 100644 modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridShareActionHandlerSpec.cpp create mode 100644 modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridShareActionHandlerSpec.hpp create mode 100644 modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JSharedFile.hpp create mode 100644 modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/HybridShareActionHandlerSpec.kt create mode 100644 modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/SharedFile.kt create mode 100644 modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridShareActionHandlerSpecSwift.cpp create mode 100644 modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridShareActionHandlerSpecSwift.hpp create mode 100644 modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/Func_void_std__vector_SharedFile_.swift create mode 100644 modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridShareActionHandlerSpec.swift create mode 100644 modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridShareActionHandlerSpec_cxx.swift create mode 100644 modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/SharedFile.swift create mode 100644 modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridShareActionHandlerSpec.cpp create mode 100644 modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridShareActionHandlerSpec.hpp create mode 100644 modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/SharedFile.hpp create mode 100644 modules/ExpensifyNitroUtils/src/specs/ShareActionHandler.nitro.ts create mode 100644 src/libs/ShareActionHandlerModule/index.native.ts create mode 100644 tests/ui/ShareRootPageTest.tsx create mode 100644 tests/unit/ShareActionHandlerModuleTest.ts diff --git a/android/app/src/main/java/com/expensify/chat/ExpensifyAppPackage.java b/android/app/src/main/java/com/expensify/chat/ExpensifyAppPackage.java index 73017c34a4e4..b7f7676996f6 100644 --- a/android/app/src/main/java/com/expensify/chat/ExpensifyAppPackage.java +++ b/android/app/src/main/java/com/expensify/chat/ExpensifyAppPackage.java @@ -21,7 +21,6 @@ public List createNativeModules( ReactApplicationContext reactContext) { List modules = new ArrayList<>(); - modules.add(new ShareActionHandlerModule(reactContext)); modules.add(new AppStateTrackerModule(reactContext)); modules.add(new GpsTripServiceModule(reactContext)); diff --git a/android/app/src/main/java/com/expensify/chat/ShareActionHandlerModule.kt b/android/app/src/main/java/com/expensify/chat/ShareActionHandlerModule.kt deleted file mode 100644 index 14258ea744f9..000000000000 --- a/android/app/src/main/java/com/expensify/chat/ShareActionHandlerModule.kt +++ /dev/null @@ -1,90 +0,0 @@ -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 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 - val fileUri = android.net.Uri.fromFile(file) - val fileUriPath = fileUri.toString() - - 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/ShareFileObject.kt b/android/app/src/main/java/com/expensify/chat/intentHandler/ShareFileObject.kt index 352850f1710e..0db43c870f9f 100644 --- a/android/app/src/main/java/com/expensify/chat/intentHandler/ShareFileObject.kt +++ b/android/app/src/main/java/com/expensify/chat/intentHandler/ShareFileObject.kt @@ -5,11 +5,13 @@ import org.json.JSONObject data class ShareFileObject( val content: String, val mimeType: String?, + val isText: Boolean = false, ) { override fun toString(): String { return JSONObject().apply { put("content", content) put("mimeType", mimeType) + put("isText", isText) }.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 index 35aa8ab21719..c0253ad16d58 100644 --- a/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt +++ b/android/app/src/main/java/com/expensify/chat/intentHandler/TextIntentHandler.kt @@ -62,7 +62,7 @@ class TextIntentHandler(private val context: Context) : AbstractIntentHandler() private fun handleTextPlainIntent(intent: Intent, context: Context) { var intentTextContent = intent.getStringExtra(Intent.EXTRA_TEXT) if(intentTextContent != null) { - val shareFileObject = ShareFileObject(intentTextContent, intent.type) + val shareFileObject = ShareFileObject(intentTextContent, intent.type, isText = true) saveToSharedPreferences(IntentHandlerConstants.shareObjectProperty, shareFileObject.toString()) } } diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index 9a9c57fd8ebf..a87111150cc4 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -54,7 +54,6 @@ 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 */; }; - E5A27B912C7F2FA8002C36BF /* 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, ); }; }; @@ -184,8 +183,6 @@ DCF33E34FFEC48128CDD41D4 /* ExpensifyMono-Bold.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyMono-Bold.otf"; path = "../assets/fonts/native/ExpensifyMono-Bold.otf"; sourceTree = ""; }; 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 = ""; }; - 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 = ""; }; @@ -330,8 +327,6 @@ children = ( 6E13A8052F51A21100B2FA13 /* LiveActivityExtension.entitlements */, 47347F002DA56FAC00633001 /* NewExpensify-Bridging-Header.h */, - E5A27B8F2C7F2F51002C36BF /* RCTShareActionHandlerModule.h */, - E5A27B902C7F2FA8002C36BF /* RCTShareActionHandlerModule.m */, 0DFC45922C884D7900B56C91 /* RCTShortcutManagerModule.h */, 0DFC45932C884E0A00B56C91 /* RCTShortcutManagerModule.m */, 499B0DA92BE2A1C000CABFB0 /* PrivacyInfo.xcprivacy */, @@ -891,7 +886,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - E5A27B912C7F2FA8002C36BF /* RCTShareActionHandlerModule.m in Sources */, 18D050E0262400AF000D658B /* BridgingFile.swift in Sources */, 0DFC45942C884E0A00B56C91 /* RCTShortcutManagerModule.m in Sources */, 47347EFF2DA5664A00633001 /* AppDelegate.swift in Sources */, diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 6aadcc93aee3..50263f2333da 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -2,6 +2,8 @@ + ShareExtensionAppGroup + group.com.expensify.new BGTaskSchedulerPermittedIdentifiers com.chat.expensify.backgroundTaskSync diff --git a/ios/RCTShareActionHandlerModule.h b/ios/RCTShareActionHandlerModule.h deleted file mode 100644 index 0dc50ae51a96..000000000000 --- a/ios/RCTShareActionHandlerModule.h +++ /dev/null @@ -1,16 +0,0 @@ -// -// 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 deleted file mode 100644 index 9999fe45368b..000000000000 --- a/ios/RCTShareActionHandlerModule.m +++ /dev/null @@ -1,129 +0,0 @@ -#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 = [fileURL absoluteString]; - } - - 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/modules/ExpensifyNitroUtils/android/src/main/java/com/margelo/nitro/utils/HybridShareActionHandler.kt b/modules/ExpensifyNitroUtils/android/src/main/java/com/margelo/nitro/utils/HybridShareActionHandler.kt new file mode 100644 index 000000000000..55c497c59387 --- /dev/null +++ b/modules/ExpensifyNitroUtils/android/src/main/java/com/margelo/nitro/utils/HybridShareActionHandler.kt @@ -0,0 +1,76 @@ +package com.margelo.nitro.utils + +import android.content.Context +import android.graphics.BitmapFactory +import android.media.MediaMetadataRetriever +import android.net.Uri +import android.webkit.MimeTypeMap +import com.margelo.nitro.NitroModules +import com.margelo.nitro.core.Promise +import org.json.JSONObject +import java.io.File +import java.io.IOException +import java.util.Locale + +/** Reads the same persisted share intent in standalone and hybrid builds. */ +class HybridShareActionHandler : HybridShareActionHandlerSpec() { + override val memorySize: Long = 0L + + override fun processFiles(): Promise> = Promise.parallel { + val context = NitroModules.applicationContext ?: throw IOException("React Native context is unavailable") + val preferences = context.getSharedPreferences("shareActionHandler", Context.MODE_PRIVATE) + val serialized = preferences.getString("shareObject", null) ?: return@parallel emptyArray() + val share = JSONObject(serialized) + val content = share.opt("content") as? String ?: throw IOException("Shared content must be a string") + if (content.isEmpty()) { + throw IOException("Shared content is empty") + } + val file = File(content) + val storedMimeType = share.opt("mimeType") + if (storedMimeType != null && storedMimeType != JSONObject.NULL && storedMimeType !is String) { + throw IOException("Shared MIME type must be a string") + } + var mimeType = (storedMimeType as? String).orEmpty().substringBefore(';').trim().lowercase(Locale.ROOT) + val timestamp = System.currentTimeMillis().toString() + + // Older intent handlers did not distinguish literal text from a text file's path. + val isText = if (share.has("isText")) { + share.opt("isText") as? Boolean ?: throw IOException("Shared text discriminator must be a boolean") + } else { + mimeType == "text/plain" && !file.isAbsolute + } + if (isText) { + return@parallel arrayOf(SharedFile("text", content, "txt", timestamp, null)) + } + + if (!file.isFile || !file.canRead() || file.length() == 0L) { + throw IOException("Shared attachment is missing, unreadable, or empty") + } + if (mimeType.isEmpty() || mimeType == "null" || mimeType.endsWith("/*")) { + mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension.lowercase(Locale.ROOT)) + ?: "application/octet-stream" + } + + val aspectRatio = when { + mimeType.startsWith("image/") -> { + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeFile(content, options) + if (options.outWidth > 0 && options.outHeight > 0) options.outWidth.toDouble() / options.outHeight else null + } + mimeType.startsWith("video/") -> { + val retriever = MediaMetadataRetriever() + try { + retriever.setDataSource(content) + val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toDoubleOrNull() + val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toDoubleOrNull() + if (width != null && height != null && width > 0 && height > 0) width / height else null + } finally { + retriever.release() + } + } + else -> null + } + + arrayOf(SharedFile(file.name, Uri.fromFile(file).toString(), mimeType, timestamp, aspectRatio)) + } +} diff --git a/modules/ExpensifyNitroUtils/ios/share/HybridShareActionHandler.swift b/modules/ExpensifyNitroUtils/ios/share/HybridShareActionHandler.swift new file mode 100644 index 000000000000..633292712382 --- /dev/null +++ b/modules/ExpensifyNitroUtils/ios/share/HybridShareActionHandler.swift @@ -0,0 +1,62 @@ +import AVFoundation +import Foundation +import NitroModules +import UIKit +import UniformTypeIdentifiers + +/** Processes files persisted by the share extension without exposing storage serialization to JS. */ +final class HybridShareActionHandler: HybridShareActionHandlerSpec { + public var memorySize: Int { MemoryLayout.size } + + func processFiles() throws -> Promise<[SharedFile]> { + Promise.async { + // Older hybrid hosts have no configuration key and use the production app group. + let appGroup = Bundle.main.object(forInfoDictionaryKey: "ShareExtensionAppGroup") as? String ?? "group.com.expensify" + let fileManager = FileManager.default + guard let container = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { + throw self.shareError("Shared app group is unavailable") + } + let folder = container.appendingPathComponent("sharedFiles", isDirectory: true) + UserDefaults(suiteName: appGroup)?.removeObject(forKey: "sharedFiles") + + let files: [URL] + do { + files = try fileManager.contentsOfDirectory(at: folder, includingPropertiesForKeys: [.fileSizeKey, .isRegularFileKey]) + } catch let error as NSError where error.domain == NSCocoaErrorDomain && error.code == NSFileReadNoSuchFileError { + return [] + } + + return try files.map { try self.processFile($0) } + } + } + + private func processFile(_ url: URL) throws -> SharedFile { + let attributes = try url.resourceValues(forKeys: [.fileSizeKey, .isRegularFileKey]) + guard attributes.isRegularFile == true, let size = attributes.fileSize, size > 0 else { + throw shareError("Shared attachment is not a non-empty file") + } + let timestamp = String(format: "%.0f", Date().timeIntervalSince1970 * 1000) + let isText = url.lastPathComponent.contains("text_to_read") + let mimeType = isText ? "txt" : UTType(filenameExtension: url.pathExtension)?.preferredMIMEType ?? "application/octet-stream" + let content = isText ? try String(contentsOf: url, encoding: .utf8) : url.absoluteString + var aspectRatio: Double? + + if mimeType.hasPrefix("image/"), let image = UIImage(contentsOfFile: url.path), image.size.height > 0 { + aspectRatio = Double(image.size.width / image.size.height) + } else if mimeType.hasPrefix("video/") { + let asset = AVURLAsset(url: url) + if let track = asset.tracks(withMediaType: .video).first { + let size = track.naturalSize.applying(track.preferredTransform) + if size.height != 0 { + aspectRatio = Double(abs(size.width / size.height)) + } + } + } + + return SharedFile(id: "\(timestamp)_\(url.path)", content: content, mimeType: mimeType, processedAt: timestamp, aspectRatio: aspectRatio) + } + + private func shareError(_ message: String) -> NSError { + NSError(domain: "ShareActionHandler", code: 1, userInfo: [NSLocalizedDescriptionKey: message]) + } +} diff --git a/modules/ExpensifyNitroUtils/nitro.json b/modules/ExpensifyNitroUtils/nitro.json index 9999ffdba114..cd897a0e81a0 100644 --- a/modules/ExpensifyNitroUtils/nitro.json +++ b/modules/ExpensifyNitroUtils/nitro.json @@ -15,6 +15,10 @@ "AppStartTimeModule": { "swift": "HybridAppStartTimeModule", "kotlin": "HybridAppStartTimeModule" + }, + "ShareActionHandler": { + "swift": "HybridShareActionHandler", + "kotlin": "HybridShareActionHandler" } }, "ignorePaths": ["node_modules"] diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtils+autolinking.cmake b/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtils+autolinking.cmake index b09226ad9a7f..46f84f21b84e 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtils+autolinking.cmake +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtils+autolinking.cmake @@ -35,9 +35,11 @@ target_sources( # Shared Nitrogen C++ sources ../nitrogen/generated/shared/c++/HybridAppStartTimeModuleSpec.cpp ../nitrogen/generated/shared/c++/HybridContactsModuleSpec.cpp + ../nitrogen/generated/shared/c++/HybridShareActionHandlerSpec.cpp # Android-specific Nitrogen C++ sources ../nitrogen/generated/android/c++/JHybridAppStartTimeModuleSpec.cpp ../nitrogen/generated/android/c++/JHybridContactsModuleSpec.cpp + ../nitrogen/generated/android/c++/JHybridShareActionHandlerSpec.cpp ) # From node_modules/react-native/ReactAndroid/cmake-utils/folly-flags.cmake diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtilsOnLoad.cpp b/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtilsOnLoad.cpp index ba0c3e861922..2e2d13cd8822 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtilsOnLoad.cpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/ExpensifyNitroUtilsOnLoad.cpp @@ -17,6 +17,7 @@ #include "JHybridAppStartTimeModuleSpec.hpp" #include "JHybridContactsModuleSpec.hpp" +#include "JHybridShareActionHandlerSpec.hpp" #include namespace margelo::nitro::utils { @@ -43,6 +44,14 @@ struct JHybridAppStartTimeModuleSpecImpl: public jni::JavaClassgetJHybridAppStartTimeModuleSpec(); } }; +struct JHybridShareActionHandlerSpecImpl: public jni::JavaClass { + static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/utils/HybridShareActionHandler;"; + static std::shared_ptr create() { + static const auto constructorFn = javaClassStatic()->getConstructor(); + jni::local_ref javaPart = javaClassStatic()->newObject(constructorFn); + return javaPart->getJHybridShareActionHandlerSpec(); + } +}; void registerAllNatives() { using namespace margelo::nitro; @@ -51,6 +60,7 @@ void registerAllNatives() { // Register native JNI methods margelo::nitro::utils::JHybridAppStartTimeModuleSpec::CxxPart::registerNatives(); margelo::nitro::utils::JHybridContactsModuleSpec::CxxPart::registerNatives(); + margelo::nitro::utils::JHybridShareActionHandlerSpec::CxxPart::registerNatives(); // Register Nitro Hybrid Objects HybridObjectRegistry::registerHybridObjectConstructor( @@ -65,6 +75,12 @@ void registerAllNatives() { return JHybridAppStartTimeModuleSpecImpl::create(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "ShareActionHandler", + []() -> std::shared_ptr { + return JHybridShareActionHandlerSpecImpl::create(); + } + ); } } // namespace margelo::nitro::utils diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridContactsModuleSpec.cpp b/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridContactsModuleSpec.cpp index 7d51cb0a1147..c84bafc4c8f0 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridContactsModuleSpec.cpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridContactsModuleSpec.cpp @@ -93,7 +93,6 @@ namespace margelo::nitro::utils { return __promise; }(); } - std::shared_ptr>> JHybridContactsModuleSpec::pick(const std::vector& keys) { static const auto method = _javaPart->javaClassStatic()->getMethod(jni::alias_ref> /* keys */)>("pick"); auto __result = method(_javaPart, [&](auto&& __input) { diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridShareActionHandlerSpec.cpp b/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridShareActionHandlerSpec.cpp new file mode 100644 index 000000000000..9597e30f6233 --- /dev/null +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridShareActionHandlerSpec.cpp @@ -0,0 +1,80 @@ +/// +/// JHybridShareActionHandlerSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#include "JHybridShareActionHandlerSpec.hpp" + +// Forward declaration of `SharedFile` to properly resolve imports. +namespace margelo::nitro::utils { struct SharedFile; } + +#include "SharedFile.hpp" +#include +#include +#include +#include "JSharedFile.hpp" +#include +#include + +namespace margelo::nitro::utils { + + std::shared_ptr JHybridShareActionHandlerSpec::JavaPart::getJHybridShareActionHandlerSpec() { + auto hybridObject = JHybridObject::JavaPart::getJHybridObject(); + auto castHybridObject = std::dynamic_pointer_cast(hybridObject); + if (castHybridObject == nullptr) [[unlikely]] { + throw std::runtime_error("Failed to downcast JHybridObject to JHybridShareActionHandlerSpec!"); + } + return castHybridObject; + } + + jni::local_ref JHybridShareActionHandlerSpec::CxxPart::initHybrid(jni::alias_ref jThis) { + return makeCxxInstance(jThis); + } + + std::shared_ptr JHybridShareActionHandlerSpec::CxxPart::createHybridObject(const jni::local_ref& javaPart) { + auto castJavaPart = jni::dynamic_ref_cast(javaPart); + if (castJavaPart == nullptr) [[unlikely]] { + throw std::runtime_error("Failed to cast JHybridObject::JavaPart to JHybridShareActionHandlerSpec::JavaPart!"); + } + return std::make_shared(castJavaPart); + } + + void JHybridShareActionHandlerSpec::CxxPart::registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", JHybridShareActionHandlerSpec::CxxPart::initHybrid), + }); + } + + // Properties + + + // Methods + std::shared_ptr>> JHybridShareActionHandlerSpec::processFiles() { + static const auto method = _javaPart->javaClassStatic()->getMethod()>("processFiles"); + auto __result = method(_javaPart); + return [&]() { + auto __promise = Promise>::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast>(__boxedResult); + __promise->resolve([&](auto&& __input) { + size_t __size = __input->size(); + std::vector __vector; + __vector.reserve(__size); + for (size_t __i = 0; __i < __size; __i++) { + auto __element = __input->getElement(__i); + __vector.push_back(__element->toCpp()); + } + return __vector; + }(__result)); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + +} // namespace margelo::nitro::utils diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridShareActionHandlerSpec.hpp b/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridShareActionHandlerSpec.hpp new file mode 100644 index 000000000000..f080aa26ed06 --- /dev/null +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JHybridShareActionHandlerSpec.hpp @@ -0,0 +1,63 @@ +/// +/// HybridShareActionHandlerSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include +#include "HybridShareActionHandlerSpec.hpp" + + + + +namespace margelo::nitro::utils { + + using namespace facebook; + + class JHybridShareActionHandlerSpec: public virtual HybridShareActionHandlerSpec, public virtual JHybridObject { + public: + struct JavaPart: public jni::JavaClass { + static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/utils/HybridShareActionHandlerSpec;"; + std::shared_ptr getJHybridShareActionHandlerSpec(); + }; + struct CxxPart: public jni::HybridClass { + static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/utils/HybridShareActionHandlerSpec$CxxPart;"; + static jni::local_ref initHybrid(jni::alias_ref jThis); + static void registerNatives(); + using HybridBase::HybridBase; + protected: + std::shared_ptr createHybridObject(const jni::local_ref& javaPart) override; + }; + + public: + explicit JHybridShareActionHandlerSpec(const jni::local_ref& javaPart): + HybridObject(HybridShareActionHandlerSpec::TAG), + JHybridObject(javaPart), + _javaPart(jni::make_global(javaPart)) {} + ~JHybridShareActionHandlerSpec() override { + // Hermes GC can destroy JS objects on a non-JNI Thread. + jni::ThreadScope::WithClassLoader([&] { _javaPart.reset(); }); + } + + public: + inline const jni::global_ref& getJavaPart() const noexcept { + return _javaPart; + } + + public: + // Properties + + + public: + // Methods + std::shared_ptr>> processFiles() override; + + private: + jni::global_ref _javaPart; + }; + +} // namespace margelo::nitro::utils diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JSharedFile.hpp b/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JSharedFile.hpp new file mode 100644 index 000000000000..ef84d1d05e71 --- /dev/null +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/c++/JSharedFile.hpp @@ -0,0 +1,74 @@ +/// +/// JSharedFile.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "SharedFile.hpp" + +#include +#include + +namespace margelo::nitro::utils { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "SharedFile" and the Kotlin data class "SharedFile". + */ + struct JSharedFile final: public jni::JavaClass { + public: + static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/utils/SharedFile;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct SharedFile by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + SharedFile toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldId = clazz->getField("id"); + jni::local_ref id = this->getFieldValue(fieldId); + static const auto fieldContent = clazz->getField("content"); + jni::local_ref content = this->getFieldValue(fieldContent); + static const auto fieldMimeType = clazz->getField("mimeType"); + jni::local_ref mimeType = this->getFieldValue(fieldMimeType); + static const auto fieldProcessedAt = clazz->getField("processedAt"); + jni::local_ref processedAt = this->getFieldValue(fieldProcessedAt); + static const auto fieldAspectRatio = clazz->getField("aspectRatio"); + jni::local_ref aspectRatio = this->getFieldValue(fieldAspectRatio); + return SharedFile( + id->toStdString(), + content->toStdString(), + mimeType->toStdString(), + processedAt->toStdString(), + aspectRatio != nullptr ? std::make_optional(aspectRatio->value()) : std::nullopt + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const SharedFile& value) { + using JSignature = JSharedFile(jni::alias_ref, jni::alias_ref, jni::alias_ref, jni::alias_ref, jni::alias_ref); + static const auto clazz = javaClassStatic(); + static const auto create = clazz->getStaticMethod("fromCpp"); + return create( + clazz, + jni::make_jstring(value.id), + jni::make_jstring(value.content), + jni::make_jstring(value.mimeType), + jni::make_jstring(value.processedAt), + value.aspectRatio.has_value() ? jni::JDouble::valueOf(value.aspectRatio.value()) : nullptr + ); + } + }; + +} // namespace margelo::nitro::utils diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/HybridShareActionHandlerSpec.kt b/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/HybridShareActionHandlerSpec.kt new file mode 100644 index 000000000000..00d4b329012b --- /dev/null +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/HybridShareActionHandlerSpec.kt @@ -0,0 +1,57 @@ +/// +/// HybridShareActionHandlerSpec.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.utils + +import androidx.annotation.Keep +import com.facebook.jni.HybridData +import com.facebook.proguard.annotations.DoNotStrip +import dalvik.annotation.optimization.FastNative +import com.margelo.nitro.core.Promise +import com.margelo.nitro.core.HybridObject + +/** + * A Kotlin class representing the ShareActionHandler HybridObject. + * Implement this abstract class to create Kotlin-based instances of ShareActionHandler. + */ +@DoNotStrip +@Keep +@Suppress( + "KotlinJniMissingFunction", "unused", + "RedundantSuppression", "RedundantUnitReturnType", "SimpleRedundantLet", + "LocalVariableName", "PropertyName", "PrivatePropertyName", "FunctionName" +) +abstract class HybridShareActionHandlerSpec: HybridObject() { + // Properties + + + // Methods + @DoNotStrip + @Keep + abstract fun processFiles(): Promise> + + // Default implementation of `HybridObject.toString()` + override fun toString(): String { + return "[HybridObject ShareActionHandler]" + } + + // C++ backing class + @DoNotStrip + @Keep + protected open class CxxPart(javaPart: HybridShareActionHandlerSpec): HybridObject.CxxPart(javaPart) { + // C++ JHybridShareActionHandlerSpec::CxxPart::initHybrid(...) + @FastNative + external override fun initHybrid(): HybridData + } + override fun createCxxPart(): CxxPart { + return CxxPart(this) + } + + companion object { + protected const val TAG = "HybridShareActionHandlerSpec" + } +} diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/SharedFile.kt b/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/SharedFile.kt new file mode 100644 index 000000000000..ae140ce08218 --- /dev/null +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/android/kotlin/com/margelo/nitro/utils/SharedFile.kt @@ -0,0 +1,71 @@ +/// +/// SharedFile.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.utils + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip +import java.util.Objects + + +/** + * Represents the JavaScript object/struct "SharedFile". + */ +@DoNotStrip +@Keep +data class SharedFile( + @DoNotStrip + @Keep + val id: String, + @DoNotStrip + @Keep + val content: String, + @DoNotStrip + @Keep + val mimeType: String, + @DoNotStrip + @Keep + val processedAt: String, + @DoNotStrip + @Keep + val aspectRatio: Double? +) { + /* primary constructor */ + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is SharedFile) return false + return Objects.deepEquals(this.id, other.id) + && Objects.deepEquals(this.content, other.content) + && Objects.deepEquals(this.mimeType, other.mimeType) + && Objects.deepEquals(this.processedAt, other.processedAt) + && Objects.deepEquals(this.aspectRatio, other.aspectRatio) + } + + override fun hashCode(): Int { + return arrayOf( + id, + content, + mimeType, + processedAt, + aspectRatio + ).contentDeepHashCode() + } + + companion object { + /** + * Constructor called from C++ + */ + @DoNotStrip + @Keep + @Suppress("unused") + @JvmStatic + private fun fromCpp(id: String, content: String, mimeType: String, processedAt: String, aspectRatio: Double?): SharedFile { + return SharedFile(id, content, mimeType, processedAt, aspectRatio) + } + } +} diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Bridge.cpp b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Bridge.cpp index bad2bd0d8e7a..5e936924bde1 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Bridge.cpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Bridge.cpp @@ -11,6 +11,7 @@ #include "ExpensifyNitroUtils-Swift-Cxx-Umbrella.hpp" #include "HybridAppStartTimeModuleSpecSwift.hpp" #include "HybridContactsModuleSpecSwift.hpp" +#include "HybridShareActionHandlerSpecSwift.hpp" #include namespace margelo::nitro::utils::bridge::swift { @@ -62,5 +63,29 @@ namespace margelo::nitro::utils::bridge::swift { ExpensifyNitroUtils::HybridContactsModuleSpec_cxx& swiftPart = swiftWrapper->getSwiftPart(); return swiftPart.toUnsafe(); } + + // pragma MARK: std::function& /* result */)> + Func_void_std__vector_SharedFile_ create_Func_void_std__vector_SharedFile_(void* NON_NULL swiftClosureWrapper) noexcept { + auto swiftClosure = ExpensifyNitroUtils::Func_void_std__vector_SharedFile_::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](const std::vector& result) mutable -> void { + swiftClosure.call(result); + }; + } + + // pragma MARK: std::shared_ptr + std::shared_ptr create_std__shared_ptr_HybridShareActionHandlerSpec_(void* NON_NULL swiftUnsafePointer) noexcept { + ExpensifyNitroUtils::HybridShareActionHandlerSpec_cxx swiftPart = ExpensifyNitroUtils::HybridShareActionHandlerSpec_cxx::fromUnsafe(swiftUnsafePointer); + return std::make_shared(swiftPart); + } + void* NON_NULL get_std__shared_ptr_HybridShareActionHandlerSpec_(std__shared_ptr_HybridShareActionHandlerSpec_ cppType) { + std::shared_ptr swiftWrapper = std::dynamic_pointer_cast(cppType); + #ifdef NITRO_DEBUG + if (swiftWrapper == nullptr) [[unlikely]] { + throw std::runtime_error("Class \"HybridShareActionHandlerSpec\" is not implemented in Swift!"); + } + #endif + ExpensifyNitroUtils::HybridShareActionHandlerSpec_cxx& swiftPart = swiftWrapper->getSwiftPart(); + return swiftPart.toUnsafe(); + } } // namespace margelo::nitro::utils::bridge::swift diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Bridge.hpp b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Bridge.hpp index d0086d25f93c..bf72056b984c 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Bridge.hpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Bridge.hpp @@ -16,6 +16,10 @@ namespace margelo::nitro::utils { struct Contact; } namespace margelo::nitro::utils { class HybridAppStartTimeModuleSpec; } // Forward declaration of `HybridContactsModuleSpec` to properly resolve imports. namespace margelo::nitro::utils { class HybridContactsModuleSpec; } +// Forward declaration of `HybridShareActionHandlerSpec` to properly resolve imports. +namespace margelo::nitro::utils { class HybridShareActionHandlerSpec; } +// Forward declaration of `SharedFile` to properly resolve imports. +namespace margelo::nitro::utils { struct SharedFile; } // Forward declaration of `StringHolder` to properly resolve imports. namespace margelo::nitro::utils { struct StringHolder; } @@ -24,12 +28,16 @@ namespace margelo::nitro::utils { struct StringHolder; } namespace ExpensifyNitroUtils { class HybridAppStartTimeModuleSpec_cxx; } // Forward declaration of `HybridContactsModuleSpec_cxx` to properly resolve imports. namespace ExpensifyNitroUtils { class HybridContactsModuleSpec_cxx; } +// Forward declaration of `HybridShareActionHandlerSpec_cxx` to properly resolve imports. +namespace ExpensifyNitroUtils { class HybridShareActionHandlerSpec_cxx; } // Include C++ defined types #include "Contact.hpp" #include "ContactFields.hpp" #include "HybridAppStartTimeModuleSpec.hpp" #include "HybridContactsModuleSpec.hpp" +#include "HybridShareActionHandlerSpec.hpp" +#include "SharedFile.hpp" #include "StringHolder.hpp" #include #include @@ -224,5 +232,86 @@ namespace margelo::nitro::utils::bridge::swift { inline Result_std__shared_ptr_Promise_std__vector_Contact____ create_Result_std__shared_ptr_Promise_std__vector_Contact____(const std::exception_ptr& error) noexcept { return Result>>>::withError(error); } + + // pragma MARK: std::optional + /** + * Specialized version of `std::optional`. + */ + using std__optional_double_ = std::optional; + inline std::optional create_std__optional_double_(const double& value) noexcept { + return std::optional(value); + } + inline bool has_value_std__optional_double_(const std::optional& optional) noexcept { + return optional.has_value(); + } + inline double get_std__optional_double_(const std::optional& optional) noexcept { + return optional.value(); + } + + // pragma MARK: std::vector + /** + * Specialized version of `std::vector`. + */ + using std__vector_SharedFile_ = std::vector; + inline std::vector create_std__vector_SharedFile_(size_t size) noexcept { + std::vector vector; + vector.reserve(size); + return vector; + } + + // pragma MARK: std::shared_ptr>> + /** + * Specialized version of `std::shared_ptr>>`. + */ + using std__shared_ptr_Promise_std__vector_SharedFile___ = std::shared_ptr>>; + inline std::shared_ptr>> create_std__shared_ptr_Promise_std__vector_SharedFile___() noexcept { + return Promise>::create(); + } + inline PromiseHolder> wrap_std__shared_ptr_Promise_std__vector_SharedFile___(std::shared_ptr>> promise) noexcept { + return PromiseHolder>(std::move(promise)); + } + + // pragma MARK: std::function& /* result */)> + /** + * Specialized version of `std::function&)>`. + */ + using Func_void_std__vector_SharedFile_ = std::function& /* result */)>; + /** + * Wrapper class for a `std::function& / * result * /)>`, this can be used from Swift. + */ + class Func_void_std__vector_SharedFile__Wrapper final { + public: + explicit Func_void_std__vector_SharedFile__Wrapper(std::function& /* result */)>&& func): _function(std::make_unique& /* result */)>>(std::move(func))) {} + inline void call(std::vector result) const noexcept { + _function->operator()(result); + } + private: + std::unique_ptr& /* result */)>> _function; + } SWIFT_NONCOPYABLE; + Func_void_std__vector_SharedFile_ create_Func_void_std__vector_SharedFile_(void* NON_NULL swiftClosureWrapper) noexcept; + inline Func_void_std__vector_SharedFile__Wrapper wrap_Func_void_std__vector_SharedFile_(Func_void_std__vector_SharedFile_ value) noexcept { + return Func_void_std__vector_SharedFile__Wrapper(std::move(value)); + } + + // pragma MARK: std::shared_ptr + /** + * Specialized version of `std::shared_ptr`. + */ + using std__shared_ptr_HybridShareActionHandlerSpec_ = std::shared_ptr; + std::shared_ptr create_std__shared_ptr_HybridShareActionHandlerSpec_(void* NON_NULL swiftUnsafePointer) noexcept; + void* NON_NULL get_std__shared_ptr_HybridShareActionHandlerSpec_(std__shared_ptr_HybridShareActionHandlerSpec_ cppType); + + // pragma MARK: std::weak_ptr + using std__weak_ptr_HybridShareActionHandlerSpec_ = std::weak_ptr; + inline std__weak_ptr_HybridShareActionHandlerSpec_ weakify_std__shared_ptr_HybridShareActionHandlerSpec_(const std::shared_ptr& strong) noexcept { return strong; } + + // pragma MARK: Result>>> + using Result_std__shared_ptr_Promise_std__vector_SharedFile____ = Result>>>; + inline Result_std__shared_ptr_Promise_std__vector_SharedFile____ create_Result_std__shared_ptr_Promise_std__vector_SharedFile____(const std::shared_ptr>>& value) noexcept { + return Result>>>::withValue(value); + } + inline Result_std__shared_ptr_Promise_std__vector_SharedFile____ create_Result_std__shared_ptr_Promise_std__vector_SharedFile____(const std::exception_ptr& error) noexcept { + return Result>>>::withError(error); + } } // namespace margelo::nitro::utils::bridge::swift diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Umbrella.hpp b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Umbrella.hpp index 85a4a5ae4fe1..7ad1c2713034 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Umbrella.hpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtils-Swift-Cxx-Umbrella.hpp @@ -16,6 +16,10 @@ namespace margelo::nitro::utils { struct Contact; } namespace margelo::nitro::utils { class HybridAppStartTimeModuleSpec; } // Forward declaration of `HybridContactsModuleSpec` to properly resolve imports. namespace margelo::nitro::utils { class HybridContactsModuleSpec; } +// Forward declaration of `HybridShareActionHandlerSpec` to properly resolve imports. +namespace margelo::nitro::utils { class HybridShareActionHandlerSpec; } +// Forward declaration of `SharedFile` to properly resolve imports. +namespace margelo::nitro::utils { struct SharedFile; } // Forward declaration of `StringHolder` to properly resolve imports. namespace margelo::nitro::utils { struct StringHolder; } @@ -24,6 +28,8 @@ namespace margelo::nitro::utils { struct StringHolder; } #include "ContactFields.hpp" #include "HybridAppStartTimeModuleSpec.hpp" #include "HybridContactsModuleSpec.hpp" +#include "HybridShareActionHandlerSpec.hpp" +#include "SharedFile.hpp" #include "StringHolder.hpp" #include #include @@ -48,6 +54,8 @@ namespace margelo::nitro::utils { struct StringHolder; } namespace ExpensifyNitroUtils { class HybridAppStartTimeModuleSpec_cxx; } // Forward declaration of `HybridContactsModuleSpec_cxx` to properly resolve imports. namespace ExpensifyNitroUtils { class HybridContactsModuleSpec_cxx; } +// Forward declaration of `HybridShareActionHandlerSpec_cxx` to properly resolve imports. +namespace ExpensifyNitroUtils { class HybridShareActionHandlerSpec_cxx; } // Include Swift defined types #if __has_include("ExpensifyNitroUtils-Swift.h") diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtilsAutolinking.mm b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtilsAutolinking.mm index dcf15dd6aba0..502a38f9bf54 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtilsAutolinking.mm +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtilsAutolinking.mm @@ -12,6 +12,7 @@ #include "HybridContactsModuleSpecSwift.hpp" #include "HybridAppStartTimeModuleSpecSwift.hpp" +#include "HybridShareActionHandlerSpecSwift.hpp" @interface ExpensifyNitroUtilsAutolinking : NSObject @end @@ -36,6 +37,13 @@ + (void) load { return hybridObject; } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "ShareActionHandler", + []() -> std::shared_ptr { + std::shared_ptr hybridObject = ExpensifyNitroUtils::ExpensifyNitroUtilsAutolinking::createShareActionHandler(); + return hybridObject; + } + ); } @end diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtilsAutolinking.swift b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtilsAutolinking.swift index 5794c7097255..5dc5a0b329e1 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtilsAutolinking.swift +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/ExpensifyNitroUtilsAutolinking.swift @@ -35,4 +35,16 @@ public final class ExpensifyNitroUtilsAutolinking { public static func isAppStartTimeModuleRecyclable() -> Bool { return HybridAppStartTimeModule.self is any RecyclableView.Type } + + public static func createShareActionHandler() -> bridge.std__shared_ptr_HybridShareActionHandlerSpec_ { + let hybridObject = HybridShareActionHandler() + return { () -> bridge.std__shared_ptr_HybridShareActionHandlerSpec_ in + let __cxxWrapped = hybridObject.getCxxWrapper() + return __cxxWrapped.getCxxPart() + }() + } + + public static func isShareActionHandlerRecyclable() -> Bool { + return HybridShareActionHandler.self is any RecyclableView.Type + } } diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.hpp b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.hpp index 8e9655fcef63..85aa2c49222c 100644 --- a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.hpp +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridContactsModuleSpecSwift.hpp @@ -85,7 +85,6 @@ namespace margelo::nitro::utils { auto __value = std::move(__result.value()); return __value; } - inline std::shared_ptr>> pick(const std::vector& keys) override { auto __result = _swiftPart.pick(keys); if (__result.hasError()) [[unlikely]] { diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridShareActionHandlerSpecSwift.cpp b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridShareActionHandlerSpecSwift.cpp new file mode 100644 index 000000000000..dee550a3621b --- /dev/null +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridShareActionHandlerSpecSwift.cpp @@ -0,0 +1,11 @@ +/// +/// HybridShareActionHandlerSpecSwift.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#include "HybridShareActionHandlerSpecSwift.hpp" + +namespace margelo::nitro::utils { +} // namespace margelo::nitro::utils diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridShareActionHandlerSpecSwift.hpp b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridShareActionHandlerSpecSwift.hpp new file mode 100644 index 000000000000..23e47e3bd14e --- /dev/null +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/c++/HybridShareActionHandlerSpecSwift.hpp @@ -0,0 +1,87 @@ +/// +/// HybridShareActionHandlerSpecSwift.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#include "HybridShareActionHandlerSpec.hpp" + +// Forward declaration of `HybridShareActionHandlerSpec_cxx` to properly resolve imports. +namespace ExpensifyNitroUtils { class HybridShareActionHandlerSpec_cxx; } + +// Forward declaration of `SharedFile` to properly resolve imports. +namespace margelo::nitro::utils { struct SharedFile; } + +#include "SharedFile.hpp" +#include +#include +#include +#include + +#include "ExpensifyNitroUtils-Swift-Cxx-Umbrella.hpp" + +namespace margelo::nitro::utils { + + /** + * The C++ part of HybridShareActionHandlerSpec_cxx.swift. + * + * HybridShareActionHandlerSpecSwift (C++) accesses HybridShareActionHandlerSpec_cxx (Swift), and might + * contain some additional bridging code for C++ <> Swift interop. + * + * Since this obviously introduces an overhead, I hope at some point in + * the future, HybridShareActionHandlerSpec_cxx can directly inherit from the C++ class HybridShareActionHandlerSpec + * to simplify the whole structure and memory management. + */ + class HybridShareActionHandlerSpecSwift: public virtual HybridShareActionHandlerSpec { + public: + // Constructor from a Swift instance + explicit HybridShareActionHandlerSpecSwift(const ExpensifyNitroUtils::HybridShareActionHandlerSpec_cxx& swiftPart): + HybridObject(HybridShareActionHandlerSpec::TAG), + _swiftPart(swiftPart) { } + + public: + // Get the Swift part + inline ExpensifyNitroUtils::HybridShareActionHandlerSpec_cxx& getSwiftPart() noexcept { + return _swiftPart; + } + + public: + inline size_t getExternalMemorySize() noexcept override { + return _swiftPart.getMemorySize(); + } + bool equals(const std::shared_ptr& other) override { + if (auto otherCast = std::dynamic_pointer_cast(other)) { + return _swiftPart.equals(otherCast->_swiftPart); + } + return false; + } + void dispose() noexcept override { + _swiftPart.dispose(); + } + std::string toString() override { + return _swiftPart.toString(); + } + + public: + // Properties + + + public: + // Methods + inline std::shared_ptr>> processFiles() override { + auto __result = _swiftPart.processFiles(); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + + private: + ExpensifyNitroUtils::HybridShareActionHandlerSpec_cxx _swiftPart; + }; + +} // namespace margelo::nitro::utils diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/Func_void_std__vector_SharedFile_.swift b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/Func_void_std__vector_SharedFile_.swift new file mode 100644 index 000000000000..79c1d2a0152c --- /dev/null +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/Func_void_std__vector_SharedFile_.swift @@ -0,0 +1,46 @@ +/// +/// Func_void_std__vector_SharedFile_.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Wraps a Swift `(_ value: [SharedFile]) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_std__vector_SharedFile_ { + public typealias bridge = margelo.nitro.utils.bridge.swift + + private let closure: (_ value: [SharedFile]) -> Void + + public init(_ closure: @escaping (_ value: [SharedFile]) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(value: bridge.std__vector_SharedFile_) -> Void { + self.closure(value.map({ __item in __item })) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_std__vector_SharedFile_`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_std__vector_SharedFile_ { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridShareActionHandlerSpec.swift b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridShareActionHandlerSpec.swift new file mode 100644 index 000000000000..44125188fa5d --- /dev/null +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridShareActionHandlerSpec.swift @@ -0,0 +1,55 @@ +/// +/// HybridShareActionHandlerSpec.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +import NitroModules + +/// See ``HybridShareActionHandlerSpec`` +public protocol HybridShareActionHandlerSpec_protocol: HybridObject { + // Properties + + + // Methods + func processFiles() throws -> Promise<[SharedFile]> +} + +public extension HybridShareActionHandlerSpec_protocol { + /// Default implementation of ``HybridObject.toString`` + func toString() -> String { + return "[HybridObject ShareActionHandler]" + } +} + +/// See ``HybridShareActionHandlerSpec`` +open class HybridShareActionHandlerSpec_base { + private weak var cxxWrapper: HybridShareActionHandlerSpec_cxx? = nil + public init() { } + public func getCxxWrapper() -> HybridShareActionHandlerSpec_cxx { + #if DEBUG + guard self is any HybridShareActionHandlerSpec else { + fatalError("`self` is not a `HybridShareActionHandlerSpec`! Did you accidentally inherit from `HybridShareActionHandlerSpec_base` instead of `HybridShareActionHandlerSpec`?") + } + #endif + if let cxxWrapper = self.cxxWrapper { + return cxxWrapper + } else { + let cxxWrapper = HybridShareActionHandlerSpec_cxx(self as! any HybridShareActionHandlerSpec) + self.cxxWrapper = cxxWrapper + return cxxWrapper + } + } +} + +/** + * A Swift base-protocol representing the ShareActionHandler HybridObject. + * Implement this protocol to create Swift-based instances of ShareActionHandler. + * ```swift + * class HybridShareActionHandler : HybridShareActionHandlerSpec { + * // ... + * } + * ``` + */ +public typealias HybridShareActionHandlerSpec = HybridShareActionHandlerSpec_protocol & HybridShareActionHandlerSpec_base diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridShareActionHandlerSpec_cxx.swift b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridShareActionHandlerSpec_cxx.swift new file mode 100644 index 000000000000..634b4b7cd8b5 --- /dev/null +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/HybridShareActionHandlerSpec_cxx.swift @@ -0,0 +1,151 @@ +/// +/// HybridShareActionHandlerSpec_cxx.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * A class implementation that bridges HybridShareActionHandlerSpec over to C++. + * In C++, we cannot use Swift protocols - so we need to wrap it in a class to make it strongly defined. + * + * Also, some Swift types need to be bridged with special handling: + * - Enums need to be wrapped in Structs, otherwise they cannot be accessed bi-directionally (Swift bug: https://github.com/swiftlang/swift/issues/75330) + * - Other HybridObjects need to be wrapped/unwrapped from the Swift TCxx wrapper + * - Throwing methods need to be wrapped with a Result type, as exceptions cannot be propagated to C++ + */ +open class HybridShareActionHandlerSpec_cxx { + /** + * The Swift <> C++ bridge's namespace (`margelo::nitro::utils::bridge::swift`) + * from `ExpensifyNitroUtils-Swift-Cxx-Bridge.hpp`. + * This contains specialized C++ templates, and C++ helper functions that can be accessed from Swift. + */ + public typealias bridge = margelo.nitro.utils.bridge.swift + + /** + * Holds an instance of the `HybridShareActionHandlerSpec` Swift protocol. + */ + private var __implementation: any HybridShareActionHandlerSpec + + /** + * Holds a weak pointer to the C++ class that wraps the Swift class. + */ + private var __cxxPart: bridge.std__weak_ptr_HybridShareActionHandlerSpec_ + + /** + * Create a new `HybridShareActionHandlerSpec_cxx` that wraps the given `HybridShareActionHandlerSpec`. + * All properties and methods bridge to C++ types. + */ + public init(_ implementation: any HybridShareActionHandlerSpec) { + self.__implementation = implementation + self.__cxxPart = .init() + /* no base class */ + } + + /** + * Get the actual `HybridShareActionHandlerSpec` instance this class wraps. + */ + @inline(__always) + public func getHybridShareActionHandlerSpec() -> any HybridShareActionHandlerSpec { + return __implementation + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `HybridShareActionHandlerSpec_cxx`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + public class func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> HybridShareActionHandlerSpec_cxx { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } + + /** + * Gets (or creates) the C++ part of this Hybrid Object. + * The C++ part is a `std::shared_ptr`. + */ + public func getCxxPart() -> bridge.std__shared_ptr_HybridShareActionHandlerSpec_ { + let cachedCxxPart = self.__cxxPart.lock() + if Bool(fromCxx: cachedCxxPart) { + return cachedCxxPart + } else { + let newCxxPart = bridge.create_std__shared_ptr_HybridShareActionHandlerSpec_(self.toUnsafe()) + __cxxPart = bridge.weakify_std__shared_ptr_HybridShareActionHandlerSpec_(newCxxPart) + return newCxxPart + } + } + + + + /** + * Get the memory size of the Swift class (plus size of any other allocations) + * so the JS VM can properly track it and garbage-collect the JS object if needed. + */ + @inline(__always) + public var memorySize: Int { + return MemoryHelper.getSizeOf(self.__implementation) + self.__implementation.memorySize + } + + /** + * Compares this object with the given [other] object for reference equality. + */ + @inline(__always) + public func equals(other: HybridShareActionHandlerSpec_cxx) -> Bool { + return self.__implementation === other.__implementation + } + + /** + * Call dispose() on the Swift class. + * This _may_ be called manually from JS. + */ + @inline(__always) + public func dispose() { + self.__implementation.dispose() + } + + /** + * Call toString() on the Swift class. + */ + @inline(__always) + public func toString() -> String { + return self.__implementation.toString() + } + + // Properties + + + // Methods + @inline(__always) + public final func processFiles() -> bridge.Result_std__shared_ptr_Promise_std__vector_SharedFile____ { + do { + let __result = try self.__implementation.processFiles() + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__vector_SharedFile___ in + let __promise = bridge.create_std__shared_ptr_Promise_std__vector_SharedFile___() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__vector_SharedFile___(__promise) + __result + .then({ __result in __promiseHolder.resolve({ () -> bridge.std__vector_SharedFile_ in + var __vector = bridge.create_std__vector_SharedFile_(__result.count) + for __item in __result { + __vector.push_back(__item) + } + return __vector + }()) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__vector_SharedFile____(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_std__vector_SharedFile____(__exceptionPtr) + } + } +} diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/SharedFile.swift b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/SharedFile.swift new file mode 100644 index 000000000000..ac3c559d3ecf --- /dev/null +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/ios/swift/SharedFile.swift @@ -0,0 +1,62 @@ +/// +/// SharedFile.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `SharedFile`, backed by a C++ struct. + */ +public typealias SharedFile = margelo.nitro.utils.SharedFile + +public extension SharedFile { + private typealias bridge = margelo.nitro.utils.bridge.swift + + /** + * Create a new instance of `SharedFile`. + */ + init(id: String, content: String, mimeType: String, processedAt: String, aspectRatio: Double?) { + self.init(std.string(id), std.string(content), std.string(mimeType), std.string(processedAt), { () -> bridge.std__optional_double_ in + if let __unwrappedValue = aspectRatio { + return bridge.create_std__optional_double_(__unwrappedValue) + } else { + return .init() + } + }()) + } + + @inline(__always) + var id: String { + return String(self.__id) + } + + @inline(__always) + var content: String { + return String(self.__content) + } + + @inline(__always) + var mimeType: String { + return String(self.__mimeType) + } + + @inline(__always) + var processedAt: String { + return String(self.__processedAt) + } + + @inline(__always) + var aspectRatio: Double? { + return { () -> Double? in + if bridge.has_value_std__optional_double_(self.__aspectRatio) { + let __unwrapped = bridge.get_std__optional_double_(self.__aspectRatio) + return __unwrapped + } else { + return nil + } + }() + } +} diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridShareActionHandlerSpec.cpp b/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridShareActionHandlerSpec.cpp new file mode 100644 index 000000000000..47f6f2e6f5b4 --- /dev/null +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridShareActionHandlerSpec.cpp @@ -0,0 +1,21 @@ +/// +/// HybridShareActionHandlerSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#include "HybridShareActionHandlerSpec.hpp" + +namespace margelo::nitro::utils { + + void HybridShareActionHandlerSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("processFiles", &HybridShareActionHandlerSpec::processFiles); + }); + } + +} // namespace margelo::nitro::utils diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridShareActionHandlerSpec.hpp b/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridShareActionHandlerSpec.hpp new file mode 100644 index 000000000000..ba1961f60f7d --- /dev/null +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/HybridShareActionHandlerSpec.hpp @@ -0,0 +1,65 @@ +/// +/// HybridShareActionHandlerSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `SharedFile` to properly resolve imports. +namespace margelo::nitro::utils { struct SharedFile; } + +#include "SharedFile.hpp" +#include +#include + +namespace margelo::nitro::utils { + + using namespace margelo::nitro; + + /** + * An abstract base class for `ShareActionHandler` + * Inherit this class to create instances of `HybridShareActionHandlerSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridShareActionHandler: public HybridShareActionHandlerSpec { + * public: + * HybridShareActionHandler(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridShareActionHandlerSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridShareActionHandlerSpec(): HybridObject(TAG) { } + + // Destructor + ~HybridShareActionHandlerSpec() override = default; + + public: + // Properties + + + public: + // Methods + virtual std::shared_ptr>> processFiles() = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "ShareActionHandler"; + }; + +} // namespace margelo::nitro::utils diff --git a/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/SharedFile.hpp b/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/SharedFile.hpp new file mode 100644 index 000000000000..b9d359d07d8e --- /dev/null +++ b/modules/ExpensifyNitroUtils/nitrogen/generated/shared/c++/SharedFile.hpp @@ -0,0 +1,100 @@ +/// +/// SharedFile.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include +#include + +namespace margelo::nitro::utils { + + /** + * A struct which can be represented as a JavaScript object (SharedFile). + */ + struct SharedFile final { + public: + std::string id SWIFT_PRIVATE; + std::string content SWIFT_PRIVATE; + std::string mimeType SWIFT_PRIVATE; + std::string processedAt SWIFT_PRIVATE; + std::optional aspectRatio SWIFT_PRIVATE; + + public: + SharedFile() = default; + explicit SharedFile(std::string id, std::string content, std::string mimeType, std::string processedAt, std::optional aspectRatio): id(id), content(content), mimeType(mimeType), processedAt(processedAt), aspectRatio(aspectRatio) {} + + public: + friend bool operator==(const SharedFile& lhs, const SharedFile& rhs) = default; + }; + +} // namespace margelo::nitro::utils + +namespace margelo::nitro { + + // C++ SharedFile <> JS SharedFile (object) + template <> + struct JSIConverter final { + static inline margelo::nitro::utils::SharedFile fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return margelo::nitro::utils::SharedFile( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "id"))), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "content"))), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "mimeType"))), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "processedAt"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "aspectRatio"))) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::utils::SharedFile& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "id"), JSIConverter::toJSI(runtime, arg.id)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "content"), JSIConverter::toJSI(runtime, arg.content)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "mimeType"), JSIConverter::toJSI(runtime, arg.mimeType)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "processedAt"), JSIConverter::toJSI(runtime, arg.processedAt)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "aspectRatio"), JSIConverter>::toJSI(runtime, arg.aspectRatio)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!nitro::isPlainObject(runtime, obj)) { + return false; + } + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "id")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "content")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "mimeType")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "processedAt")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "aspectRatio")))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/modules/ExpensifyNitroUtils/src/index.ts b/modules/ExpensifyNitroUtils/src/index.ts index e02e1d8606df..042b78764ce0 100644 --- a/modules/ExpensifyNitroUtils/src/index.ts +++ b/modules/ExpensifyNitroUtils/src/index.ts @@ -2,10 +2,13 @@ import {NitroModules} from 'react-native-nitro-modules'; import type AppStartTimeModule from './specs/AppStartTimeModule.nitro'; import type * as ContactsModuleSpec from './specs/ContactsModule.nitro'; +import type {ShareActionHandler} from './specs/ShareActionHandler.nitro'; const ContactsNitroModule = NitroModules.createHybridObject('ContactsModule'); const AppStartTimeNitroModule = NitroModules.createHybridObject('AppStartTimeModule'); +const ShareActionHandlerNitroModule = NitroModules.createHybridObject('ShareActionHandler'); -export {ContactsNitroModule, AppStartTimeNitroModule}; +export {ContactsNitroModule, AppStartTimeNitroModule, ShareActionHandlerNitroModule}; +export type {ShareActionHandler, SharedFile} from './specs/ShareActionHandler.nitro'; export * from './specs/ContactsModule.nitro'; export type {default as AppStartTimeModule} from './specs/AppStartTimeModule.nitro'; diff --git a/modules/ExpensifyNitroUtils/src/specs/ShareActionHandler.nitro.ts b/modules/ExpensifyNitroUtils/src/specs/ShareActionHandler.nitro.ts new file mode 100644 index 000000000000..177548dc05bf --- /dev/null +++ b/modules/ExpensifyNitroUtils/src/specs/ShareActionHandler.nitro.ts @@ -0,0 +1,19 @@ +/** Defines the shared-file contract implemented by both native platforms. */ +import type {HybridObject} from 'react-native-nitro-modules'; + +type SharedFile = { + id: string; + /** File URL, or the actual content for a plain-text share. */ + content: string; + mimeType: string; + /** Milliseconds since the Unix epoch, matching the persisted ShareTempFile format. */ + processedAt: string; + aspectRatio?: number; +}; + +interface ShareActionHandler extends HybridObject<{ios: 'swift'; android: 'kotlin'}> { + /** Returns an empty array when no share is pending; rejects if pending data cannot be read. */ + processFiles(): Promise; +} + +export type {ShareActionHandler, SharedFile}; diff --git a/src/libs/ShareActionHandlerModule/index.native.ts b/src/libs/ShareActionHandlerModule/index.native.ts new file mode 100644 index 000000000000..5c2130d7d8f5 --- /dev/null +++ b/src/libs/ShareActionHandlerModule/index.native.ts @@ -0,0 +1,4 @@ +/** Uses the same generated shared-file contract on Android and iOS. */ +import {ShareActionHandlerNitroModule} from '@expensify/nitro-utils'; + +export default ShareActionHandlerNitroModule; diff --git a/src/libs/ShareActionHandlerModule/index.ts b/src/libs/ShareActionHandlerModule/index.ts index 205bd89960d9..dc58c96df6cd 100644 --- a/src/libs/ShareActionHandlerModule/index.ts +++ b/src/libs/ShareActionHandlerModule/index.ts @@ -1,21 +1,8 @@ -import {NativeModules} from 'react-native'; +/** Sharing into the app is only supported by native hosts. */ +import type {ShareActionHandler} from '@expensify/nitro-utils'; -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; +const ShareActionHandlerModule: Pick = { + processFiles: () => Promise.resolve([]), }; -export default ShareActionHandler; -export type {ShareActionHandlerModule}; +export default ShareActionHandlerModule; diff --git a/src/pages/Share/ShareRootPage.tsx b/src/pages/Share/ShareRootPage.tsx index 6ec0c6e448d9..ffb39181d1a4 100644 --- a/src/pages/Share/ShareRootPage.tsx +++ b/src/pages/Share/ShareRootPage.tsx @@ -26,7 +26,7 @@ import ROUTES from '@src/ROUTES'; import type {ShareTempFile} from '@src/types/onyx'; import type {FileObject} from '@src/types/utils/Attachment'; -import React, {useCallback, useEffect, useRef, useState} from 'react'; +import React, {useEffect, useRef, useState} from 'react'; import {Alert, AppState, View} from 'react-native'; import getFileSize from './getFileSize'; @@ -50,34 +50,31 @@ function ShareRootPage() { const {validateFiles} = useFilesValidation(addValidatedShareFile); const isTextShared = currentAttachment?.mimeType === 'txt'; - const validateFileIfNecessary = useCallback( - (file: ShareTempFile) => { - if (!file || isTextShared || !shouldValidateFile(file)) { - return; - } + const validateFileIfNecessary = (file: ShareTempFile) => { + if (!file || isTextShared || !shouldValidateFile(file)) { + return; + } - getFileSize(file.content) - .catch((error: unknown) => { - Log.warn('[ShareRootPage] Failed to get file size for validation', {error}); - return undefined; - }) - .then((size) => { - validateFiles( - [ - { - name: file.id, - uri: file.content, - type: file.mimeType, - size, - }, - ], - undefined, - {isValidatingReceipts: false}, - ); - }); - }, - [isTextShared, validateFiles], - ); + getFileSize(file.content) + .catch((error: unknown) => { + Log.warn('[ShareRootPage] Failed to get file size for validation', {error}); + return undefined; + }) + .then((size) => { + validateFiles( + [ + { + name: file.id, + uri: file.content, + type: file.mimeType, + size, + }, + ], + undefined, + {isValidatingReceipts: false}, + ); + }); + }; const appState = useRef(AppState.currentState); const [isFileReady, setIsFileReady] = useState(false); @@ -98,63 +95,74 @@ function ShareRootPage() { showErrorAlert(errorTitle, errorMessage); }, [errorTitle, errorMessage]); - const handleProcessFiles = useCallback(() => { - ShareActionHandler.processFiles((processedFiles) => { - const tempFile = Array.isArray(processedFiles) ? processedFiles.at(0) : (JSON.parse(processedFiles) as ShareTempFile); - if (errorTitle) { - return; - } - if (!tempFile?.mimeType || !shareFileMimeTypes.includes(tempFile?.mimeType)) { - setErrorTitle(translate('attachmentPicker.wrongFileType')); - setErrorMessage(translate('attachmentPicker.notAllowedExtension')); - return; - } - - const isImage = /image\/.*/.test(tempFile?.mimeType); - if (tempFile?.mimeType && tempFile?.mimeType !== 'txt' && !isImage) { - getFileSize(tempFile?.content).then((size) => { - if (size > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) { - setErrorTitle(translate('attachmentPicker.attachmentTooLarge')); - setErrorMessage(translate('attachmentPicker.sizeExceeded')); - } - - if (size < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) { - setErrorTitle(translate('attachmentPicker.attachmentTooSmall')); - setErrorMessage(translate('attachmentPicker.sizeNotMet')); - } - }); - } - - // Skip the standalone corruption check for files that go through `validateFileIfNecessary`, - // because `validateFiles` already runs corruption checks internally. Running both would - // surface a duplicate (and potentially misleading) "corrupt attachment" alert. - if (isImage && !shouldValidateFile(tempFile)) { - const fileObject: FileObject = {name: tempFile.id, uri: tempFile?.content, type: tempFile?.mimeType}; - validateImageForCorruption(fileObject).catch(() => { + const handleProcessFiles = () => { + ShareActionHandler.processFiles() + .then((processedFiles) => { + if (errorTitle) { + return; + } + const tempFile = processedFiles.at(0); + if (!tempFile) { setErrorTitle(translate('attachmentPicker.attachmentError')); setErrorMessage(translate('attachmentPicker.errorWhileSelectingCorruptedAttachment')); - }); - } + return; + } + if (!tempFile?.mimeType || !shareFileMimeTypes.includes(tempFile?.mimeType)) { + setErrorTitle(translate('attachmentPicker.wrongFileType')); + setErrorMessage(translate('attachmentPicker.notAllowedExtension')); + return; + } - const {fileExtension} = splitExtensionFromFileName(tempFile?.content); - if (tempFile) { - if (tempFile.mimeType) { - if (receiptFileFormats.includes(tempFile.mimeType) && fileExtension) { - setIsFileScannable(true); - // Reset the persisted tab to SUBMIT so that defaultSelectedTab takes effect - // even when a previous Share session left the tab on SHARE. - Tab.setSelectedTab(CONST.TAB.SHARE.NAVIGATOR_ID, CONST.TAB.SHARE.SUBMIT); - } else { - setIsFileScannable(false); - } - validateFileIfNecessary(tempFile); - setIsFileReady(true); + const isImage = /image\/.*/.test(tempFile?.mimeType); + if (tempFile?.mimeType && tempFile?.mimeType !== 'txt' && !isImage) { + getFileSize(tempFile?.content).then((size) => { + if (size > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) { + setErrorTitle(translate('attachmentPicker.attachmentTooLarge')); + setErrorMessage(translate('attachmentPicker.sizeExceeded')); + } + + if (size < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) { + setErrorTitle(translate('attachmentPicker.attachmentTooSmall')); + setErrorMessage(translate('attachmentPicker.sizeNotMet')); + } + }); } - addTempShareFile(tempFile); - } - }); - }, [errorTitle, shareFileMimeTypes, translate, receiptFileFormats, validateFileIfNecessary]); + // Skip the standalone corruption check for files that go through `validateFileIfNecessary`, + // because `validateFiles` already runs corruption checks internally. Running both would + // surface a duplicate (and potentially misleading) "corrupt attachment" alert. + if (isImage && !shouldValidateFile(tempFile)) { + const fileObject: FileObject = {name: tempFile.id, uri: tempFile?.content, type: tempFile?.mimeType}; + validateImageForCorruption(fileObject).catch(() => { + setErrorTitle(translate('attachmentPicker.attachmentError')); + setErrorMessage(translate('attachmentPicker.errorWhileSelectingCorruptedAttachment')); + }); + } + + const {fileExtension} = splitExtensionFromFileName(tempFile?.content); + if (tempFile) { + if (tempFile.mimeType) { + if (receiptFileFormats.includes(tempFile.mimeType) && fileExtension) { + setIsFileScannable(true); + // Reset the persisted tab to SUBMIT so that defaultSelectedTab takes effect + // even when a previous Share session left the tab on SHARE. + Tab.setSelectedTab(CONST.TAB.SHARE.NAVIGATOR_ID, CONST.TAB.SHARE.SUBMIT); + } else { + setIsFileScannable(false); + } + validateFileIfNecessary(tempFile); + setIsFileReady(true); + } + + addTempShareFile(tempFile); + } + }) + .catch((error: unknown) => { + Log.warn('[ShareRootPage] Failed to process shared files', {error}); + setErrorTitle(translate('attachmentPicker.attachmentError')); + setErrorMessage(translate('attachmentPicker.errorWhileSelectingCorruptedAttachment')); + }); + }; useEffect(() => { const subscription = AppState.addEventListener('change', (nextAppState) => { diff --git a/src/types/modules/react-native.d.ts b/src/types/modules/react-native.d.ts index e43d30a8631e..f1b66fbf5faf 100644 --- a/src/types/modules/react-native.d.ts +++ b/src/types/modules/react-native.d.ts @@ -1,7 +1,6 @@ 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'; /* eslint-disable @typescript-eslint/consistent-type-definitions */ @@ -67,7 +66,6 @@ declare module 'react-native' { GpsTripServiceModule: GpsTripServiceModule; EnvironmentChecker: EnvironmentCheckerModule; ShortcutManager: ShortcutManagerModule; - ShareActionHandler: ShareActionHandlerModule; TestToolsBridge: TestToolsBridge; PushNotificationBridge?: PushNotificationBridge; } diff --git a/tests/ui/ShareRootPageTest.tsx b/tests/ui/ShareRootPageTest.tsx new file mode 100644 index 000000000000..b3a4a943c808 --- /dev/null +++ b/tests/ui/ShareRootPageTest.tsx @@ -0,0 +1,92 @@ +/** Covers the Share screen's handling of the generated native result and rejected processing. */ +import {render, waitFor} from '@testing-library/react-native'; + +import {addTempShareFile} from '@libs/actions/Share'; +import ShareActionHandler from '@libs/ShareActionHandlerModule'; + +import ShareRootPage from '@pages/Share/ShareRootPage'; + +import React from 'react'; +import {Alert} from 'react-native'; + +jest.mock('@libs/ShareActionHandlerModule', () => ({__esModule: true, default: {processFiles: jest.fn()}})); +jest.mock('@libs/actions/Share', () => ({addTempShareFile: jest.fn(), addValidatedShareFile: jest.fn(), clearShareData: jest.fn()})); +jest.mock('@hooks/useOnyx', () => () => [undefined]); +jest.mock('@hooks/useLocalize', () => () => ({translate: (key: string) => key})); +jest.mock('@hooks/useThemeStyles', () => () => ({})); +jest.mock('@hooks/useFilesValidation', () => () => ({validateFiles: jest.fn()})); +jest.mock('@libs/Navigation/Navigation', () => ({navigate: jest.fn()})); +jest.mock('@libs/Navigation/OnyxTabNavigator', () => ({__esModule: true, default: 'OnyxTabNavigator', TopTab: {Screen: 'Screen'}})); +jest.mock('@libs/actions/Modal', () => ({close: jest.fn()})); +jest.mock('@libs/actions/Tab', () => ({__esModule: true, default: {setSelectedTab: jest.fn()}})); +jest.mock('@libs/ReceiptUtils', () => ({shouldValidateFile: () => false})); +jest.mock('@libs/fileDownload/FileUtils', () => ({splitExtensionFromFileName: () => ({fileExtension: 'jpg'}), validateImageForCorruption: () => Promise.resolve()})); +jest.mock('@libs/Log', () => ({warn: jest.fn()})); +jest.mock('@components/ScreenWrapper', () => 'ScreenWrapper'); +jest.mock('@components/HeaderWithBackButton', () => 'HeaderWithBackButton'); +jest.mock('@components/Skeletons/TabNavigatorSkeleton', () => 'TabNavigatorSkeleton'); +jest.mock('@components/TabSelector/TabSelector', () => 'TabSelector'); +jest.mock('@pages/Share/ShareTab', () => 'ShareTab'); +jest.mock('@pages/Share/SubmitTab', () => 'SubmitTab'); + +describe('ShareRootPage native processing', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(Alert, 'alert').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it.each([ + {id: 'receipt.jpg', content: 'file:///receipt.jpg', mimeType: 'image/jpeg', processedAt: '123', aspectRatio: 1.5}, + {id: 'text', content: 'Shared text', mimeType: 'txt', processedAt: '123'}, + ])('stores a typed $mimeType share', async (file) => { + // Given a file or literal-text record returned by native code. + jest.mocked(ShareActionHandler.processFiles).mockResolvedValueOnce([file]); + + // When the Share screen opens. + render(); + + // Then it stores the record without parsing JSON or changing timestamp/metadata types. + await waitFor(() => expect(addTempShareFile).toHaveBeenCalledWith(file)); + expect(Alert.alert).not.toHaveBeenCalled(); + }); + + it('shows an attachment error when no files are available', async () => { + // Given a Share route with no pending native data. + jest.mocked(ShareActionHandler.processFiles).mockResolvedValueOnce([]); + + // When the screen opens. + render(); + + // Then it reports the missing attachment instead of leaving the loading skeleton indefinitely. + await waitFor(() => expect(Alert.alert).toHaveBeenCalledWith('attachmentPicker.attachmentError', 'attachmentPicker.errorWhileSelectingCorruptedAttachment', expect.any(Array))); + expect(addTempShareFile).not.toHaveBeenCalled(); + }); + + it('shows an attachment error when native processing rejects', async () => { + // Given an unreadable or invalid pending attachment. + jest.mocked(ShareActionHandler.processFiles).mockRejectedValueOnce(new Error('Unreadable attachment')); + + // When the screen opens and native processing fails. + render(); + + // Then the rejection reaches the existing attachment-error UI. + await waitFor(() => expect(Alert.alert).toHaveBeenCalledWith('attachmentPicker.attachmentError', 'attachmentPicker.errorWhileSelectingCorruptedAttachment', expect.any(Array))); + expect(addTempShareFile).not.toHaveBeenCalled(); + }); + + it('rejects unsupported MIME types without persisting them', async () => { + // Given a readable file with a type the Share flow does not support. + jest.mocked(ShareActionHandler.processFiles).mockResolvedValueOnce([{id: 'file', content: 'file:///file', mimeType: 'application/x-unsupported', processedAt: '123'}]); + + // When the screen opens. + render(); + + // Then the existing unsupported-file error is retained. + await waitFor(() => expect(Alert.alert).toHaveBeenCalledWith('attachmentPicker.wrongFileType', 'attachmentPicker.notAllowedExtension', expect.any(Array))); + expect(addTempShareFile).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/unit/ShareActionHandlerModuleTest.ts b/tests/unit/ShareActionHandlerModuleTest.ts new file mode 100644 index 000000000000..bd96e6b7988e --- /dev/null +++ b/tests/unit/ShareActionHandlerModuleTest.ts @@ -0,0 +1,52 @@ +/** Verifies the native adapter exposes generated records without changing the payload. */ + +import ShareActionHandler from '@libs/ShareActionHandlerModule/index.native'; + +import type {SharedFile} from '@expensify/nitro-utils'; + +import {NitroModules} from 'react-native-nitro-modules'; + +jest.mock('react-native-nitro-modules', () => ({ + NitroModules: {createHybridObject: jest.fn(() => ({processFiles: jest.fn()}))}, +})); + +describe('ShareActionHandler native adapter', () => { + const processFiles = jest.spyOn(ShareActionHandler, 'processFiles'); + const createHybridObject = jest.spyOn(NitroModules, 'createHybridObject'); + + it.each([ + {id: 'receipt.jpg', content: 'file:///receipt.jpg', mimeType: 'image/jpeg', processedAt: '123', aspectRatio: 1.5}, + {id: 'text', content: '/literal text, not a file path', mimeType: 'txt', processedAt: '123'}, + ])('preserves the generated payload for $mimeType', async (file) => { + // Given a native result that uses the common record type, including optional metadata. + const files = [file]; + processFiles.mockResolvedValueOnce(files); + + // When the native adapter processes the share. + const result = await ShareActionHandler.processFiles(); + + // Then neither JSON parsing nor platform-specific result conversion is needed. + expect(createHybridObject).toHaveBeenCalledWith('ShareActionHandler'); + expect(result).toBe(files); + }); + + it('preserves an empty result when no share is pending', async () => { + // Given no pending share. + processFiles.mockResolvedValueOnce([]); + + // When the native adapter processes files. + const result = await ShareActionHandler.processFiles(); + + // Then the contract remains an array, not null or an error string. + expect(result).toEqual([]); + }); + + it('propagates native processing failures', async () => { + // Given an attachment that native code cannot read. + const error = new Error('Shared attachment is missing'); + processFiles.mockRejectedValueOnce(error); + + // When processing fails, then the caller receives a rejection rather than a malformed payload. + await expect(ShareActionHandler.processFiles()).rejects.toBe(error); + }); +});