From 70234e44fab554a1a21d2897d7f34ddaca52e269 Mon Sep 17 00:00:00 2001 From: Ngoc Le Date: Wed, 12 Aug 2026 14:06:00 +0700 Subject: [PATCH] fix: load Safari shared URLs as typed objects --- package-lock.json | 4 +- package.json | 2 +- plugin/src/__tests__/ios-artifacts.test.ts | 8 +- plugin/src/ios/ShareViewController.swift | 92 ++++++++++------------ 4 files changed, 50 insertions(+), 56 deletions(-) diff --git a/package-lock.json b/package-lock.json index 76826f4..90e108b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "expo-share-content", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "expo-share-content", - "version": "0.1.0", + "version": "0.1.1", "license": "MIT", "dependencies": { "@expo/config-plugins": "~57.0.7", diff --git a/package.json b/package.json index aee26b5..43652d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "expo-share-content", - "version": "0.1.0", + "version": "0.1.1", "description": "Receive text, URLs, images, videos, audio, and files shared to an Expo app.", "type": "commonjs", "main": "build/index.js", diff --git a/plugin/src/__tests__/ios-artifacts.test.ts b/plugin/src/__tests__/ios-artifacts.test.ts index 3e394b6..0a0f901 100644 --- a/plugin/src/__tests__/ios-artifacts.test.ts +++ b/plugin/src/__tests__/ios-artifacts.test.ts @@ -48,9 +48,11 @@ describe('iOS Share Extension artifacts', () => { expect(swift).toContain('completeRequest(returningItems: nil, completionHandler: nil)'); expect(swift).toContain('loadFileRepresentation(forTypeIdentifier:'); expect(swift).toContain('read(upToCount:'); - // Binary/media attachments must stream. Text/URL may load small in-memory values, - // but only after a byte-length guard (loadDataRepresentation path). - expect(swift).toContain('loadDataRepresentation(forTypeIdentifier:'); + // Binary/media attachments must stream. Text/URL use typed object loading so + // ExtensionKit receives an expected value class for cross-process providers. + expect(swift).toContain('loadObject(ofClass: URL.self)'); + expect(swift).toContain('loadObject(ofClass: String.self)'); + expect(swift).not.toContain('loadDataRepresentation(forTypeIdentifier:'); expect(swift).not.toMatch(/loadItem\(forTypeIdentifier:\s*UTType\.(image|movie|audio|fileURL)/); expect(swift).not.toContain('image.pngData()'); expect(swift).not.toContain('copyItem(at: source'); diff --git a/plugin/src/ios/ShareViewController.swift b/plugin/src/ios/ShareViewController.swift index b68128f..e9a5233 100644 --- a/plugin/src/ios/ShareViewController.swift +++ b/plugin/src/ios/ShareViewController.swift @@ -280,76 +280,68 @@ final class ShareViewController: UIViewController { return try await loadFile(provider: provider, typeIdentifier: UTType.fileURL.identifier, itemType: "file", id: id, shareId: shareId) } if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) { - return try await loadBoundedText( - provider: provider, - typeIdentifier: UTType.url.identifier, - id: id, - asURL: true - ) + return try await loadURL(provider: provider, id: id) } if provider.hasItemConformingToTypeIdentifier(UTType.text.identifier) { - return try await loadBoundedText( - provider: provider, - typeIdentifier: UTType.text.identifier, - id: id, - asURL: false - ) + return try await loadText(provider: provider, id: id) } guard let typeIdentifier = provider.registeredTypeIdentifiers.first else { return nil } return try await loadFile(provider: provider, typeIdentifier: typeIdentifier, itemType: "file", id: id, shareId: shareId) } - private func loadBoundedText( - provider: NSItemProvider, - typeIdentifier: String, - id: String, - asURL: Bool - ) async throws -> SharedItem { - let data = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - provider.loadDataRepresentation(forTypeIdentifier: typeIdentifier) { data, error in + private func loadURL(provider: NSItemProvider, id: String) async throws -> SharedItem { + let url = try await withCheckedThrowingContinuation { + (continuation: CheckedContinuation) in + _ = provider.loadObject(ofClass: URL.self) { url, error in if let error { continuation.resume(throwing: error) return } - guard let data else { + guard let url else { continuation.resume(throwing: ShareExtensionError.itemUnavailable) return } - guard data.count <= self.maxTextBytes else { - continuation.resume(throwing: ShareExtensionError.textTooLarge(self.maxTextBytes)) - return - } - continuation.resume(returning: data) + continuation.resume(returning: url) } } - if asURL { - guard - let text = String(data: data, encoding: .utf8) ?? String(data: data, encoding: .utf16), - let url = URL(string: text.trimmingCharacters(in: .whitespacesAndNewlines)), - let scheme = url.scheme?.lowercased(), - ["http", "https"].contains(scheme) - else { - throw ShareExtensionError.itemUnavailable - } - let normalized = url.absoluteString - guard normalized.utf8.count <= maxTextBytes else { - throw ShareExtensionError.textTooLarge(maxTextBytes) - } - return SharedItem( - id: id, - type: "url", - mimeType: "text/uri-list", - text: normalized, - uri: nil, - fileName: nil, - size: nil - ) + guard + let scheme = url.scheme?.lowercased(), + ["http", "https"].contains(scheme) + else { + throw ShareExtensionError.itemUnavailable + } + let normalized = url.absoluteString + guard normalized.utf8.count <= maxTextBytes else { + throw ShareExtensionError.textTooLarge(maxTextBytes) } + return SharedItem( + id: id, + type: "url", + mimeType: "text/uri-list", + text: normalized, + uri: nil, + fileName: nil, + size: nil + ) + } - guard let value = String(data: data, encoding: .utf8) ?? String(data: data, encoding: .utf16) else { - throw ShareExtensionError.itemUnavailable + private func loadText(provider: NSItemProvider, id: String) async throws -> SharedItem { + let value = try await withCheckedThrowingContinuation { + (continuation: CheckedContinuation) in + _ = provider.loadObject(ofClass: String.self) { value, error in + if let error { + continuation.resume(throwing: error) + return + } + guard let value else { + continuation.resume(throwing: ShareExtensionError.itemUnavailable) + return + } + continuation.resume(returning: value) + } } + guard value.utf8.count <= maxTextBytes else { throw ShareExtensionError.textTooLarge(maxTextBytes) }