From db53a301d6ebd3e4759617b684cd0b6b0a4539b9 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Thu, 2 Apr 2026 18:23:03 +0100 Subject: [PATCH 1/2] Fix iOS share extension for screenshot preview sharing Screenshot preview items register images via NSItemProviderWriting with specific types (e.g. public.png). The previous code requested the generic "public.data" type which could fail for these providers. This also fixes handleStringData silently dropping file:// strings without calling completion, which caused the DispatchGroup to hang. --- .../ShareViewController.swift | 143 +++++++++++++----- 1 file changed, 101 insertions(+), 42 deletions(-) diff --git a/ios/ShareViewController/ShareViewController.swift b/ios/ShareViewController/ShareViewController.swift index 4ad39131a40c1..b9adb19446a25 100644 --- a/ios/ShareViewController/ShareViewController.swift +++ b/ios/ShareViewController/ShareViewController.swift @@ -9,17 +9,17 @@ class ShareViewController: UIViewController { let APP_GROUP_ID = "group.com.expensify.new" let FILES_DIRECTORY_NAME = "sharedFiles" let READ_FROM_FILE_FILE_NAME = "text_to_read.txt" - + enum FileSaveError: String { case CouldNotLoad case URLError case GroupSharedFolderNotFound } - + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) os_log("viewDidAppear triggered") - + if let content = extensionContext!.inputItems[0] as? NSExtensionItem { os_log("Received NSExtensionItem: %@", content) saveFileToAppGroup(content: content) { error in @@ -30,11 +30,11 @@ class ShareViewController: UIViewController { } } } - + private func saveFileToFolder(folder: URL, filename: String, fileData: NSData) -> URL? { let filePath = folder.appendingPathComponent(filename) os_log("Saving file to: %@", filePath.path) - + do { try fileData.write(to: filePath, options: .completeFileProtection) os_log("File saved successfully at: %@", filePath.path) @@ -44,27 +44,27 @@ class ShareViewController: UIViewController { return nil } } - + private func saveFileToAppGroup(content: NSExtensionItem, completion: @escaping (FileSaveError?) -> Void) { guard let groupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: self.APP_GROUP_ID) else { completion(.GroupSharedFolderNotFound) os_log("Group shared folder not found") return } - + let sharedFileFolder = groupURL.appendingPathComponent(FILES_DIRECTORY_NAME, isDirectory: true) os_log("Shared file folder: %@", sharedFileFolder.path) setupSharedFolder(folder: sharedFileFolder) - + guard let attachments = content.attachments else { completion(.CouldNotLoad) os_log("Could not load attachments") return } - + processAttachments(attachments, in: sharedFileFolder, completion: completion) } - + private func setupSharedFolder(folder: URL) { os_log("Setting up shared folder: %@", folder.path) do { @@ -73,11 +73,11 @@ class ShareViewController: UIViewController { os_log("Failed to create folder: %@, error: %@", folder.path, error.localizedDescription) return } - + do { let filePaths = try FileManager.default.contentsOfDirectory(atPath: folder.path) os_log("Clearing folder with contents: %@", filePaths) - + for filePath in filePaths { try FileManager.default.removeItem(atPath: folder.appendingPathComponent(filePath).path) } @@ -85,11 +85,11 @@ class ShareViewController: UIViewController { os_log("Could not clear temp folder: %@", error.localizedDescription) } } - + private func processAttachments(_ attachments: [NSItemProvider], in folder: URL, completion: @escaping (FileSaveError?) -> Void) { os_log("Processing attachments") let group = DispatchGroup() - + for attachment in attachments { group.enter() os_log("Processing attachment") @@ -101,37 +101,81 @@ class ShareViewController: UIViewController { } } } - + group.notify(queue: .main) { os_log("Finished processing all attachments") self.openMainApp() } } - + private func loadData(for attachment: NSItemProvider, in folder: URL, group: DispatchGroup, completion: @escaping (FileSaveError?) -> Void) { os_log("Loading data for attachment") + os_log("Registered type identifiers: %@", attachment.registeredTypeIdentifiers.joined(separator: ", ")) let isURL = attachment.hasItemConformingToTypeIdentifier("public.url") && !attachment.hasItemConformingToTypeIdentifier("public.file-url") - let typeIdentifier = isURL ? (kUTTypeURL as String) : (kUTTypeData as String) - + + if isURL { + attachment.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { (data, error) in + DispatchQueue.main.async { + if let error = error { + os_log("Sharing error: %@", error.localizedDescription) + completion(.CouldNotLoad) + return + } + if let url = data as? URL { + os_log("Handling URL: %@", url.absoluteString) + self.handleURL(url, folder: folder, completion: completion) + } else { + completion(.CouldNotLoad) + } + } + } + } else if attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) { + os_log("Loading image via public.image type identifier") + attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { (data, error) in + DispatchQueue.main.async { + if let error = error { + os_log("Image loadItem error: %@, falling back to registered type", error.localizedDescription) + self.loadGenericData(for: attachment, in: folder, completion: completion) + return + } + if let image = data as? UIImage { + os_log("Got UIImage directly from loadItem") + self.handleImageData(image, folder: folder, completion: completion) + } else if let imageData = data as? Data, let image = UIImage(data: imageData) { + os_log("Got Data from loadItem, converted to UIImage") + self.handleImageData(image, folder: folder, completion: completion) + } else if let url = data as? URL { + os_log("Got file URL from loadItem: %@", url.path) + self.handleURLData(url as NSURL, folder: folder, completion: completion) + } else { + os_log("Unrecognized image data type, falling back to registered type") + self.loadGenericData(for: attachment, in: folder, completion: completion) + } + } + } + } else { + loadGenericData(for: attachment, in: folder, completion: completion) + } + } + + private func loadGenericData(for attachment: NSItemProvider, in folder: URL, completion: @escaping (FileSaveError?) -> Void) { + // Use the provider's own registered type instead of the generic "public.data". + // Screenshot previews register via NSItemProviderWriting with specific types + // (e.g. public.png) and may fail when asked for the generic "public.data" type. + let typeIdentifier = attachment.registeredTypeIdentifiers.first ?? UTType.data.identifier + os_log("Loading data with type identifier: %@", typeIdentifier) attachment.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { (data, error) in DispatchQueue.main.async { if let error = error { - os_log("Sharing error: %@", error.localizedDescription) + os_log("Data load error: %@", error.localizedDescription) completion(.CouldNotLoad) return } - - if isURL, let url = data as? URL { - os_log("Handling URL: %@", url.absoluteString) - self.handleURL(url, folder: folder, completion: completion) - } else { - os_log("Handling data for attachment") - self.handleData(data, folder: folder, completion: completion) - } + self.handleData(data, folder: folder, completion: completion) } } } - + private func handleURL(_ url: URL, folder: URL, completion: @escaping (FileSaveError?) -> Void) { os_log("Handling URL: %@", url.absoluteString) if let fileData = url.absoluteString.data(using: .utf8) as NSData? { @@ -147,7 +191,7 @@ class ShareViewController: UIViewController { completion(.URLError) } } - + private func handleData(_ data: Any?, folder: URL, completion: @escaping (FileSaveError?) -> Void) { os_log("Handling generic data") guard let data = data else { @@ -155,7 +199,7 @@ class ShareViewController: UIViewController { completion(.CouldNotLoad) return } - + if let dataString = data as? String { os_log("Handling string data: %@", dataString) handleStringData(dataString, folder: folder, completion: completion) @@ -165,19 +209,34 @@ class ShareViewController: UIViewController { } else if let file = data as? UIImage { os_log("Handling image data") handleImageData(file, folder: folder, completion: completion) + } else if let rawData = data as? Data { + os_log("Handling raw Data bytes") + if let image = UIImage(data: rawData) { + handleImageData(image, folder: folder, completion: completion) + } else { + processAndSave(data: rawData, filename: "shared_file", folder: folder, completion: completion) + } } else { os_log("Received data of unhandled type", type: .error) completion(.URLError) } } - + private func handleStringData(_ dataString: String, folder: URL, completion: @escaping (FileSaveError?) -> Void) { - os_log("Handling string data without file prefix") - if !dataString.hasPrefix("file://") { + if dataString.hasPrefix("file://") { + os_log("Handling string data with file:// prefix") + if let url = NSURL(string: dataString) { + handleURLData(url, folder: folder, completion: completion) + } else { + os_log("Invalid file:// URL string") + completion(.URLError) + } + } else { + os_log("Handling string data as text") processAndSave(data: dataString.data(using: .utf8), filename: READ_FROM_FILE_FILE_NAME, folder: folder, completion: completion) } } - + private func handleURLData(_ url: NSURL, folder: URL, completion: @escaping (FileSaveError?) -> Void) { os_log("Handling NSURL data") guard let filename = url.lastPathComponent else { @@ -185,17 +244,17 @@ class ShareViewController: UIViewController { completion(.CouldNotLoad) return } - + let fileData = NSData(contentsOf: url as URL) as Data? processAndSave(data: fileData, filename: filename, folder: folder, completion: completion) } - + private func handleImageData(_ image: UIImage, folder: URL, completion: @escaping (FileSaveError?) -> Void) { os_log("Handling image data") let filename = "shared_image.png" processAndSave(data: image.pngData(), filename: filename, folder: folder, completion: completion) } - + private func processAndSave(data: Data?, filename: String, folder: URL, completion: @escaping (FileSaveError?) -> Void) { os_log("Processing and saving data") guard let fileData = data as NSData? else { @@ -203,7 +262,7 @@ class ShareViewController: UIViewController { completion(.CouldNotLoad) return } - + if saveFileToFolder(folder: folder, filename: filename, fileData: fileData) != nil { os_log("File saved successfully: %@", filename) completion(nil) @@ -212,11 +271,11 @@ class ShareViewController: UIViewController { completion(.CouldNotLoad) } } - + private func openMainApp() { os_log("Attempting to open main app") let url = URL(string: "new-expensify://share/root")! - + if launchApp(customURL: url) { os_log("Main app opened successfully") self.extensionContext!.completeRequest(returningItems: nil, completionHandler: nil) @@ -225,14 +284,14 @@ class ShareViewController: UIViewController { self.extensionContext!.cancelRequest(withError: NSError(domain: "", code: 0, userInfo: nil)) } } - + private func launchApp(customURL: URL?) -> Bool { os_log("Launching app with custom URL") guard let url = customURL else { os_log("Invalid custom URL") return false } - + var responder: UIResponder? = self while responder != nil { if let application = responder as? UIApplication { From 0cbdee47aa4dfe1e1f7ee576065c6da097875249 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Tue, 7 Apr 2026 14:15:22 +0100 Subject: [PATCH 2/2] NSExtensionActivationDictionaryVersion: 2 --- ios/ShareViewController/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ios/ShareViewController/Info.plist b/ios/ShareViewController/Info.plist index b0f55a182ee1f..1410c93e885a3 100644 --- a/ios/ShareViewController/Info.plist +++ b/ios/ShareViewController/Info.plist @@ -20,6 +20,8 @@ NSExtensionActivationRule + NSExtensionActivationDictionaryVersion + 2 NSExtensionActivationSupportsAttachmentsWithMaxCount 1 NSExtensionActivationSupportsFileWithMaxCount