diff --git a/apps/tlon-mobile/ios/ShareExtension/ShareViewController.swift b/apps/tlon-mobile/ios/ShareExtension/ShareViewController.swift index 3a0eea5933..f455957d87 100644 --- a/apps/tlon-mobile/ios/ShareExtension/ShareViewController.swift +++ b/apps/tlon-mobile/ios/ShareExtension/ShareViewController.swift @@ -4,6 +4,7 @@ * inspired by : * - https://ajith-ab.github.io/react-native-receive-sharing-intent/docs/ios#create-share-extension */ +import AVFoundation import ImageIO import MobileCoreServices import Photos @@ -36,6 +37,8 @@ class ShareViewController: UIViewController { let propertyListType: String = UTType.propertyList.identifier let fileURLType: String = UTType.fileURL.identifier let pdfContentType: String = UTType.pdf.identifier + private let maxVideoSizeBytes = 150 * 1024 * 1024 + private let maxVideoSizeLabel = "150 MB" override func viewDidLoad() { super.viewDidLoad() @@ -304,6 +307,7 @@ class ShareViewController: UIViewController { let fileExtension = self.getExtension(from: url, type: .video) let fileSize = self.getFileSize(from: url) let mimeType = url.mimeType(ext: fileExtension) + if self.dismissIfVideoTooLarge(fileSize: fileSize) { return } let newName = "\(UUID().uuidString).\(fileExtension)" let newPath = FileManager.default .containerURL( @@ -311,13 +315,12 @@ class ShareViewController: UIViewController { .appendingPathComponent(newName) let copied = self.copyFile(at: url, to: newPath) if copied { - guard - let sharedFile = self.getSharedMediaFile( - forVideo: newPath, fileName: fileName, fileSize: fileSize, mimeType: mimeType) - else { - return - } + let sharedFile = self.getSharedMediaFile( + forVideo: newPath, fileName: fileName, fileSize: fileSize, mimeType: mimeType) self.sharedMedia.append(sharedFile) + } else { + self.dismissWithError(message: "Could not prepare shared video.") + return } // If this is the last item, save imagesData in userDefaults and redirect to host app @@ -375,6 +378,9 @@ class ShareViewController: UIViewController { let fileExtension = self.getExtension(from: url, type: .file) let fileSize = self.getFileSize(from: url) let mimeType = url.mimeType(ext: fileExtension) + if mimeType.hasPrefix("video/") && dismissIfVideoTooLarge(fileSize: fileSize) { + return + } let newName = "\(UUID().uuidString).\(fileExtension)" let newPath = FileManager.default .containerURL( @@ -397,11 +403,11 @@ class ShareViewController: UIViewController { } } - private func dismissWithError(message: String? = nil) { + private func dismissWithError(message: String = "Something went wrong.") { DispatchQueue.main.async { - NSLog("[ERROR] Error loading application ! \(message!)") + NSLog("[ERROR] Share extension failed: \(message)") let alert = UIAlertController( - title: "Error", message: "Error loading application: \(message!)", preferredStyle: .alert) + title: "Unable to share", message: message, preferredStyle: .alert) let action = UIAlertAction(title: "OK", style: .cancel) { _ in self.dismiss(animated: true, completion: nil) @@ -493,19 +499,31 @@ class ShareViewController: UIViewController { return true } + private func isVideoTooLarge(fileSize: Int?) -> Bool { + return (fileSize ?? 0) > maxVideoSizeBytes + } + + private func dismissIfVideoTooLarge(fileSize: Int?) -> Bool { + guard isVideoTooLarge(fileSize: fileSize) else { + return false + } + dismissWithError(message: "Videos must be under \(maxVideoSizeLabel).") + return true + } + private func getSharedMediaFile(forVideo: URL, fileName: String, fileSize: Int?, mimeType: String) - -> SharedMediaFile? + -> SharedMediaFile { let asset = AVAsset(url: forVideo) let thumbnailPath = getThumbnailPath(for: forVideo) - let duration = (CMTimeGetSeconds(asset.duration) * 1000).rounded() + let durationSeconds = CMTimeGetSeconds(asset.duration) + let duration: Double? = + durationSeconds.isFinite ? (durationSeconds * 1000).rounded() : nil var trackWidth: Int? = nil var trackHeight: Int? = nil - // get video info - let track = asset.tracks(withMediaType: AVMediaType.video).first ?? nil - if track != nil { - let size = track!.naturalSize.applying(track!.preferredTransform) + if let track = asset.tracks(withMediaType: AVMediaType.video).first { + let size = track.naturalSize.applying(track.preferredTransform) trackWidth = abs(Int(size.width)) trackHeight = abs(Int(size.height)) } @@ -522,19 +540,21 @@ class ShareViewController: UIViewController { assetImgGenerate.appliesPreferredTrackTransform = true assetImgGenerate.maximumSize = CGSize(width: 360, height: 360) do { + let captureTime = + durationSeconds.isFinite && durationSeconds > 0 + ? min(0.1, max(durationSeconds - 0.01, 0)) : 0 let img = try assetImgGenerate.copyCGImage( - at: CMTimeMakeWithSeconds(600, preferredTimescale: Int32(1.0)), actualTime: nil) + at: CMTimeMakeWithSeconds(captureTime, preferredTimescale: Int32(600)), actualTime: nil) try UIImage.pngData(UIImage(cgImage: img))()?.write(to: thumbnailPath) saved = true } catch { saved = false } - return saved - ? SharedMediaFile( - path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, fileName: fileName, - fileSize: fileSize, width: trackWidth, height: trackHeight, duration: duration, - mimeType: mimeType, type: .video) : nil + return SharedMediaFile( + path: forVideo.absoluteString, thumbnail: saved ? thumbnailPath.absoluteString : nil, + fileName: fileName, fileSize: fileSize, width: trackWidth, height: trackHeight, + duration: duration, mimeType: mimeType, type: .video) } private func getThumbnailPath(for url: URL) -> URL { diff --git a/packages/app/ui/components/Channel/index.tsx b/packages/app/ui/components/Channel/index.tsx index 46aac4e17e..766c6f2f2e 100644 --- a/packages/app/ui/components/Channel/index.tsx +++ b/packages/app/ui/components/Channel/index.tsx @@ -30,7 +30,7 @@ import { useRef, useState, } from 'react'; -import { Platform } from 'react-native'; +import { Alert, Platform } from 'react-native'; import { AnimatePresence, View, @@ -129,7 +129,7 @@ const uploadIntentFromShareIntentFile = ( type: 'fileUri', localUri, name: file.fileName || localUri.split('/').pop(), - size: file.size ?? 0, + size: file.size ?? -1, mimeType: file.mimeType ?? undefined, voiceMemo: false, }; @@ -201,6 +201,7 @@ function usePrefillDraftFromShareIntent({ if (errorMessage) { shareIntentLogger.log(`Unable to attach shared file: ${errorMessage}`); + Alert.alert('Unable to attach', errorMessage); } if (!uploadIntent && !sharedText) return; diff --git a/packages/app/ui/contexts/attachmentRules.test.ts b/packages/app/ui/contexts/attachmentRules.test.ts index ea73743011..f2c171ba80 100644 --- a/packages/app/ui/contexts/attachmentRules.test.ts +++ b/packages/app/ui/contexts/attachmentRules.test.ts @@ -3,7 +3,7 @@ import { expect, test } from 'vitest'; import { VIDEO_COMPOSITION_ERROR, - VIDEO_VALIDATION_ERROR, + VIDEO_TYPE_ERROR, canAddAttachment, inferAllowedVideoMimeType, } from './attachmentRules'; @@ -52,31 +52,6 @@ test('rejects video mixed with non-text media', () => { }); }); -test('rejects unknown-size local video', () => { - expect(canAddAttachment([], makeVideo({ size: -1 }))).toEqual({ - ok: false, - reason: VIDEO_VALIDATION_ERROR, - kind: 'validation', - }); -}); - -test('rejects unknown-size remote video', () => { - expect( - canAddAttachment( - [], - makeVideo({ - localFile: 'https://cdn.example.com/clip.mp4', - size: -1, - mimeType: undefined, - }) - ) - ).toEqual({ - ok: false, - reason: VIDEO_VALIDATION_ERROR, - kind: 'validation', - }); -}); - test('allows fallback to supported extension when MIME type is unsupported', () => { expect(canAddAttachment([], makeVideo({ mimeType: 'video/avi' }))).toEqual({ ok: true, @@ -88,7 +63,7 @@ test('rejects unsupported extension when MIME is missing', () => { canAddAttachment([], makeVideo({ mimeType: undefined, name: 'clip.bin' })) ).toEqual({ ok: false, - reason: VIDEO_VALIDATION_ERROR, + reason: VIDEO_TYPE_ERROR, kind: 'validation', }); }); diff --git a/packages/app/ui/contexts/attachmentRules.ts b/packages/app/ui/contexts/attachmentRules.ts index 8f4a92af02..c14bd532c2 100644 --- a/packages/app/ui/contexts/attachmentRules.ts +++ b/packages/app/ui/contexts/attachmentRules.ts @@ -1,9 +1,14 @@ import { Attachment } from '@tloncorp/shared'; +import { isWeb } from 'tamagui'; -const MAX_VIDEO_SIZE_BYTES = 200 * 1024 * 1024; +export const MAX_VIDEO_SIZE_BYTES = 150 * 1024 * 1024; +export const MAX_VIDEO_SIZE_LABEL = '150 MB'; export const VIDEO_COMPOSITION_ERROR = 'Video posts support one video and optional text only.'; -export const VIDEO_VALIDATION_ERROR = 'Unsupported video attachment'; +export const VIDEO_SIZE_UNKNOWN_ERROR = + 'We could not read this video. Try downloading it to your device first.'; +export const VIDEO_SIZE_LIMIT_ERROR = `Videos must be under ${MAX_VIDEO_SIZE_LABEL}.`; +export const VIDEO_TYPE_ERROR = 'Video posts support MP4, MOV, and WebM files.'; const VIDEO_EXTENSION_TO_MIME = { mp4: 'video/mp4', mov: 'video/quicktime', @@ -59,19 +64,24 @@ export function inferAllowedVideoMimeType({ ]; } -export function validateVideoSource({ +export function getVideoValidationError({ mimeType, size, name, uri, -}: VideoCandidate): boolean { - if (size == null || size < 0) { - return false; +}: VideoCandidate): string | null { + if (!isWeb) { + if (size == null || size < 0) { + return VIDEO_SIZE_UNKNOWN_ERROR; + } + if (size > MAX_VIDEO_SIZE_BYTES) { + return VIDEO_SIZE_LIMIT_ERROR; + } } - if (size > MAX_VIDEO_SIZE_BYTES) { - return false; + if (inferAllowedVideoMimeType({ mimeType, name, uri }) == null) { + return VIDEO_TYPE_ERROR; } - return inferAllowedVideoMimeType({ mimeType, name, uri }) != null; + return null; } function getVideoName(video: Extract): string { @@ -94,33 +104,24 @@ export function canAddAttachment( nextAttachment: Attachment ): AttachmentValidationResult { if (nextAttachment.type === 'video') { - const name = getVideoName(nextAttachment); - if ( - !validateVideoSource({ - mimeType: nextAttachment.mimeType, - size: nextAttachment.size, - name, - uri: - typeof nextAttachment.localFile === 'string' - ? nextAttachment.localFile - : undefined, - }) - ) { + const validationError = getVideoValidationError({ + mimeType: nextAttachment.mimeType, + size: nextAttachment.size, + name: getVideoName(nextAttachment), + uri: + typeof nextAttachment.localFile === 'string' + ? nextAttachment.localFile + : undefined, + }); + if (validationError) { return { ok: false, - reason: VIDEO_VALIDATION_ERROR, + reason: validationError, kind: 'validation', }; } - } - const hasVideo = prev.some((attachment) => attachment.type === 'video'); - const hasOtherNonVideoAttachment = prev.some( - (attachment) => attachment.type !== 'video' - ); - - if (nextAttachment.type === 'video') { - if (hasOtherNonVideoAttachment) { + if (prev.some((attachment) => attachment.type !== 'video')) { return { ok: false, reason: VIDEO_COMPOSITION_ERROR, @@ -130,7 +131,7 @@ export function canAddAttachment( return { ok: true }; } - if (hasVideo) { + if (prev.some((attachment) => attachment.type === 'video')) { return { ok: false, reason: VIDEO_COMPOSITION_ERROR, diff --git a/packages/app/utils/filepicker.test.ts b/packages/app/utils/filepicker.test.ts index 835908101d..fd89dfdaf1 100644 --- a/packages/app/utils/filepicker.test.ts +++ b/packages/app/utils/filepicker.test.ts @@ -1,10 +1,7 @@ import type { ImagePickerAsset } from 'expo-image-picker'; import { expect, test, vi } from 'vitest'; -import { - isLikelyVideoSource, - validateVideoSource, -} from '../ui/contexts/attachmentRules'; +import { isLikelyVideoSource } from '../ui/contexts/attachmentRules'; import { getVideoPreviewData } from '../ui/utils/videoPreviewData'; import { imagePickerAssetToUploadIntent, @@ -21,8 +18,7 @@ vi.mock('../ui/utils/videoPreviewData', () => ({ vi.mock('../ui/contexts/attachmentRules', () => ({ isLikelyVideoSource: vi.fn(() => false), - validateVideoSource: vi.fn(() => true), - VIDEO_VALIDATION_ERROR: 'Unsupported video attachment', + getVideoValidationError: vi.fn(() => null), })); vi.mock('./images', () => ({ @@ -80,6 +76,64 @@ test('builds a video upload intent from a normalized picker asset', () => { }); }); +test('treats picker assets with video metadata as videos when type is missing', () => { + vi.mocked(isLikelyVideoSource).mockReturnValueOnce(true); + + const uploadIntent = imagePickerAssetToUploadIntent( + makeAsset({ + fileName: 'large-clip.mov', + fileSize: 160 * 1024 * 1024, + mimeType: 'video/quicktime', + type: null, + uri: 'file:///tmp/large-clip.mov', + width: 1920, + height: 1080, + duration: 2500, + }) + ); + + expect(uploadIntent).toEqual({ + type: 'fileUri', + localUri: 'file:///tmp/large-clip.mov', + name: 'large-clip.mov', + size: 160 * 1024 * 1024, + mimeType: 'video/quicktime', + video: { + width: 1920, + height: 1080, + duration: 2.5, + }, + }); +}); + +test('treats picker assets with duration as videos when type and mime are missing', () => { + const uploadIntent = imagePickerAssetToUploadIntent( + makeAsset({ + fileName: null, + fileSize: 160 * 1024 * 1024, + mimeType: undefined, + type: null, + uri: 'file:///tmp/asset', + width: 1920, + height: 1080, + duration: 2500, + }) + ); + + expect(uploadIntent).toEqual({ + type: 'fileUri', + localUri: 'file:///tmp/asset', + name: undefined, + size: 160 * 1024 * 1024, + mimeType: undefined, + video: { + width: 1920, + height: 1080, + duration: 2.5, + }, + }); +}); + test('uses web File and treats duration as seconds when image picker provides a File object', () => { const file = new File(['video-bytes'], 'clip.mp4', { type: 'video/mp4' }); @@ -137,7 +191,6 @@ test('drops non-positive picker video metadata during upload intent conversion', test('normalizeUploadIntent keeps supported quicktime videos with known size', async () => { vi.mocked(isLikelyVideoSource).mockReturnValueOnce(true); - vi.mocked(validateVideoSource).mockReturnValueOnce(true); vi.mocked(getVideoPreviewData).mockResolvedValueOnce({}); const result = await normalizeUploadIntent({ diff --git a/packages/app/utils/filepicker.ts b/packages/app/utils/filepicker.ts index 8a80a48f0f..c551fa1027 100644 --- a/packages/app/utils/filepicker.ts +++ b/packages/app/utils/filepicker.ts @@ -3,9 +3,8 @@ import * as DocumentPicker from 'expo-document-picker'; import type { ImagePickerAsset } from 'expo-image-picker'; import { - VIDEO_VALIDATION_ERROR, + getVideoValidationError, isLikelyVideoSource, - validateVideoSource, } from '../ui/contexts/attachmentRules'; import { getVideoPreviewData } from '../ui/utils/videoPreviewData'; import type { VideoPreviewData } from '../ui/utils/videoPreviewTypes'; @@ -80,10 +79,23 @@ function asVideoMetadata( return metadata && typeof metadata === 'object' ? metadata : undefined; } +function isImagePickerVideoAsset(asset: ImagePickerAsset): boolean { + return ( + asset.type === 'video' || + asset.type === 'pairedVideo' || + positiveNumberOrUndefined(asset.duration) != null || + isLikelyVideoSource({ + mimeType: asset.mimeType ?? undefined, + name: asset.fileName ?? undefined, + uri: asset.uri, + }) + ); +} + export function imagePickerAssetToUploadIntent( asset: ImagePickerAsset ): Attachment.UploadIntent { - if (asset.type === 'video') { + if (isImagePickerVideoAsset(asset)) { const assetFile = getAssetFile(asset); const video = imagePickerAssetVideoMetadata(asset, assetFile); @@ -202,18 +214,26 @@ export async function normalizeUploadIntent( }; } - if (!isLikelyVideoSource({ mimeType, name, uri })) { + const existingVideo = asVideoMetadata(uploadIntent.video); + const isVideoIntent = + existingVideo != null || isLikelyVideoSource({ mimeType, name, uri }); + if (!isVideoIntent) { return { uploadIntent, errorMessage: null }; } - if (!validateVideoSource({ mimeType, size, name, uri })) { + const videoValidationError = getVideoValidationError({ + mimeType, + size, + name, + uri, + }); + if (videoValidationError) { return { uploadIntent: null, - errorMessage: VIDEO_VALIDATION_ERROR, + errorMessage: videoValidationError, }; } - const existingVideo = asVideoMetadata(uploadIntent.video); const existingWidth = positiveNumberOrUndefined(existingVideo?.width); const existingHeight = positiveNumberOrUndefined(existingVideo?.height); const existingDuration = positiveNumberOrUndefined(existingVideo?.duration); @@ -302,7 +322,7 @@ export async function pickFile(acceptedTypes: string[] = ['*/*']): Promise<{ type: 'fileUri', name: res.name, localUri: res.uri, - size: res.size ?? -1, // not sure when size would be undefined, but it's in the types... + size: res.size ?? -1, mimeType: res.mimeType, } : { diff --git a/patches/expo-share-intent@5.1.1.patch b/patches/expo-share-intent@5.1.1.patch index 0f17e3e5a0..bd34a712e4 100644 --- a/patches/expo-share-intent@5.1.1.patch +++ b/patches/expo-share-intent@5.1.1.patch @@ -1,10 +1,18 @@ diff --git a/plugin/build/ios/ShareExtensionViewController.swift b/plugin/build/ios/ShareExtensionViewController.swift -index 0911a9e80f9556353e47324a7244daee704aaf71..d295f7713cc13d79bd6020f87e9b69180e7d1b8b 100644 +index 0911a9e80f9556353e47324a7244daee704aaf71..c171ff47371595b698e70e1b73c6f5ac08fc294b 100644 --- a/plugin/build/ios/ShareExtensionViewController.swift +++ b/plugin/build/ios/ShareExtensionViewController.swift -@@ -10,9 +10,21 @@ import Social +@@ -4,15 +4,29 @@ + * inspired by : + * - https://ajith-ab.github.io/react-native-receive-sharing-intent/docs/ios#create-share-extension + */ ++import AVFoundation ++import ImageIO + import MobileCoreServices + import Photos + import Social import UIKit - + class ShareViewController: UIViewController { - let hostAppGroupIdentifier = "" - let shareProtocol = "" @@ -27,20 +35,212 @@ index 0911a9e80f9556353e47324a7244daee704aaf71..d295f7713cc13d79bd6020f87e9b6918 var sharedMedia: [SharedMediaFile] = [] var sharedWebUrl: [WebUrl] = [] var sharedText: [String] = [] -@@ -28,6 +40,7 @@ class ShareViewController: UIViewController { - +@@ -22,12 +36,13 @@ class ShareViewController: UIViewController { + let urlContentType: String = UTType.url.identifier + let propertyListType: String = UTType.propertyList.identifier + let fileURLType: String = UTType.fileURL.identifier +- let pkpassContentType: String = "com.apple.pkpass" + let pdfContentType: String = UTType.pdf.identifier +- let vcardContentType: String = "public.vcard" ++ private let maxVideoSizeBytes = 150 * 1024 * 1024 ++ private let maxVideoSizeLabel = "150 MB" + override func viewDidLoad() { super.viewDidLoad() + setupLoadingIndicator() } - + override func viewDidAppear(_ animated: Bool) { -@@ -315,14 +328,7 @@ class ShareViewController: UIViewController { +@@ -45,12 +60,8 @@ class ShareViewController: UIViewController { + await handleImages(content: content, attachment: attachment, index: index) + } else if attachment.hasItemConformingToTypeIdentifier(videoContentType) { + await handleVideos(content: content, attachment: attachment, index: index) +- } else if attachment.hasItemConformingToTypeIdentifier(vcardContentType) { +- await handleVCard(content: content, attachment: attachment, index: index) + } else if attachment.hasItemConformingToTypeIdentifier(fileURLType) { + await handleFiles(content: content, attachment: attachment, index: index) +- } else if attachment.hasItemConformingToTypeIdentifier(pkpassContentType) { +- await handlePkPass(content: content, attachment: attachment, index: index) + } else if attachment.hasItemConformingToTypeIdentifier(pdfContentType) { + await handlePdf(content: content, attachment: attachment, index: index) + } else if attachment.hasItemConformingToTypeIdentifier(propertyListType) { +@@ -67,33 +78,6 @@ class ShareViewController: UIViewController { + } + } + +- private func handleVCard(content: NSExtensionItem, attachment: NSItemProvider, index: Int) async { +- Task.detached { +- do { +- if let url = try? await attachment.loadItem(forTypeIdentifier: self.vcardContentType) as? URL { +- // ensure a .vcf file extension so mime resolves properly +- let tmp = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".vcf") +- _ = self.copyFile(at: url, to: tmp) +- Task { @MainActor in +- await self.handleFileURL(content: content, url: tmp, index: index) +- } +- } else if let data = try? await attachment.loadItem(forTypeIdentifier: self.vcardContentType) as? Data { +- let tmp = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".vcf") +- try data.write(to: tmp) +- Task { @MainActor in +- await self.handleFileURL(content: content, url: tmp, index: index) +- } +- } else { +- NSLog("[ERROR] Cannot load vcard content !\(String(describing: content))") +- await self.dismissWithError(message: "Cannot load vCard content \(String(describing: content))") +- } +- } catch { +- NSLog("[ERROR] handleVCard exception: \(error.localizedDescription)") +- await self.dismissWithError(message: "vCard error: \(error.localizedDescription)") +- } +- } +- } +- + private func handleText(content: NSExtensionItem, attachment: NSItemProvider, index: Int) async { + Task.detached { + if let item = try! await attachment.loadItem(forTypeIdentifier: self.textContentType) +@@ -182,83 +166,36 @@ class ShareViewController: UIViewController { + } + } + +- private func handlePkPass(content: NSExtensionItem, attachment: NSItemProvider, index: Int) async { +- Task.detached { +- NSLog("[DEBUG] Attempting to handle pkpass file for item \(index)") +- NSLog("[DEBUG] Available type identifiers: \(attachment.registeredTypeIdentifiers)") +- +- do { +- if let url = try await attachment.loadItem(forTypeIdentifier: self.pkpassContentType) as? URL { +- NSLog("[DEBUG] Successfully loaded pkpass as URL: \(url.absoluteString)") +- NSLog("[DEBUG] URL path: \(url.path), isFileURL: \(url.isFileURL)") +- await self.handleFileURL(content: content, url: url, index: index) +- +- } else if let data = try await attachment.loadItem(forTypeIdentifier: self.pkpassContentType) as? Data { +- NSLog("[DEBUG] Successfully loaded pkpass as Data, size: \(data.count) bytes") +- let tempFileName = UUID().uuidString + ".pkpass" +- let tempFileURL = FileManager.default.temporaryDirectory.appendingPathComponent(tempFileName) +- +- // Writing data to a file is I/O, keep it off the main thread. +- try data.write(to: tempFileURL) +- NSLog("[DEBUG] Saved pkpass data to temporary file: \(tempFileURL.path)") +- +- // Handle the newly created temporary file URL. +- await self.handleFileURL(content: content, url: tempFileURL, index: index) +- +- } else { +- // If it's neither URL nor Data, it's unexpected for pkpassContentType. +- NSLog("[ERROR] Cannot load pkpass content: Item was neither URL nor Data for type \(self.pkpassContentType). Attachment: \(attachment)") +- // Ensure dismissWithError runs on the main thread if it interacts with UI +- Task { @MainActor in +- self.dismissWithError(message: "Cannot load pkpass content (unexpected data type).") +- } +- } +- } catch { +- // Catch errors from loadItem or data.write +- NSLog("[ERROR] Exception when handling pkpass: \(error.localizedDescription)") +- // Ensure dismissWithError runs on the main thread if it interacts with UI +- Task { @MainActor in +- self.dismissWithError(message: "Error processing pkpass: \(error.localizedDescription)") +- } +- } +- } +- } +- +- +- private func handleImages(content: NSExtensionItem, attachment: NSItemProvider, index: Int) async { ++ private func handleImages(content: NSExtensionItem, attachment: NSItemProvider, index: Int) async ++ { + Task.detached { +- do { +- let item = try await attachment.loadItem(forTypeIdentifier: self.imageContentType) +- ++ if let item = try? await attachment.loadItem(forTypeIdentifier: self.imageContentType) { + Task { @MainActor in ++ + var url: URL? = nil +- + if let dataURL = item as? URL { + url = dataURL + } else if let imageData = item as? UIImage { + url = self.saveScreenshot(imageData) +- if url == nil { +- NSLog("[ERROR] handleImages: saveScreenshot returned nil") +- } +- } else if let data = item as? Data { +- if let image = UIImage(data: data) { +- url = self.saveScreenshot(image) +- } else { +- NSLog("[ERROR] handleImages: Failed to create UIImage from Data") ++ } else if let imageData = item as? Data { ++ guard let imageExtension = self.getImageDataExtension(imageData) else { ++ NSLog("[ERROR] Cannot identify image data type !\(String(describing: item))") ++ self.dismissWithError(message: "Could not read shared image") ++ return + } +- } else { +- NSLog("[ERROR] handleImages: Item is unexpected type: \(type(of: item))") ++ ++ url = self.saveImageData(imageData, fileExtension: imageExtension) + } + +- guard let safeURL = url else { +- NSLog("[ERROR] handleImages: Failed to get URL for image item") +- self.dismissWithError(message: "Failed to process image") ++ guard url != nil else { ++ NSLog("[ERROR] Cannot load image URL content !\(String(describing: item))") ++ self.dismissWithError(message: "Cannot load image URL content") + return + } + + var pixelWidth: Int? = nil + var pixelHeight: Int? = nil +- if let imageSource = CGImageSourceCreateWithURL(safeURL as CFURL, nil) { ++ if let imageSource = CGImageSourceCreateWithURL(url! as CFURL, nil) { + if let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) + as Dictionary? + { +@@ -279,18 +216,16 @@ class ShareViewController: UIViewController { + } + + // Always copy +- let fileName = self.getFileName(from: safeURL, type: .image) +- let fileExtension = self.getExtension(from: safeURL, type: .image) +- let fileSize = self.getFileSize(from: safeURL) +- let mimeType = safeURL.mimeType(ext: fileExtension) ++ let fileName = self.getFileName(from: url!, type: .image) ++ let fileExtension = self.getExtension(from: url!, type: .image) ++ let fileSize = self.getFileSize(from: url!) ++ let mimeType = url!.mimeType(ext: fileExtension) + let newName = "\(UUID().uuidString).\(fileExtension)" + let newPath = FileManager.default + .containerURL( + forSecurityApplicationGroupIdentifier: self.hostAppGroupIdentifier)! + .appendingPathComponent(newName) +- +- let copied = self.copyFile(at: safeURL, to: newPath) +- ++ let copied = self.copyFile(at: url!, to: newPath) + if copied { + self.sharedMedia.append( + SharedMediaFile( +@@ -306,52 +241,56 @@ class ShareViewController: UIViewController { + userDefaults?.synchronize() + self.redirectToHostApp(type: .media) + } ++ + } +- } catch { +- NSLog("[ERROR] handleImages: Exception loading image item: \(error)") +- await self.dismissWithError(message: "Cannot load image content: \(error.localizedDescription)") ++ } else { ++ NSLog("[ERROR] Cannot load image content !\(String(describing: content))") ++ await self.dismissWithError( ++ message: "Cannot load image content \(String(describing: content))") + } + } } - + private func documentDirectoryPath() -> URL? { - let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) -- +- - if let firstPath = paths.first { - _ = FileManager.default.fileExists(atPath: firstPath.path) - return firstPath @@ -49,12 +249,122 @@ index 0911a9e80f9556353e47324a7244daee704aaf71..d295f7713cc13d79bd6020f87e9b6918 - } + return FileManager.default.temporaryDirectory } - + private func saveScreenshot(_ image: UIImage) -> URL? { -@@ -504,30 +510,32 @@ class ShareViewController: UIViewController { +- guard let screenshotData = image.pngData() else { ++ var screenshotURL: URL? = nil ++ if let screenshotData = image.pngData(), ++ let screenshotPath = documentDirectoryPath()?.appendingPathComponent("screenshot.png") ++ { ++ try? screenshotData.write(to: screenshotPath) ++ screenshotURL = screenshotPath ++ } ++ return screenshotURL ++ } ++ ++ private func getImageDataExtension(_ data: Data) -> String? { ++ guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil), ++ CGImageSourceGetCount(imageSource) > 0, ++ let imageType = CGImageSourceGetType(imageSource), ++ let imageExtension = UTType(imageType as String)?.preferredFilenameExtension ++ else { + return nil + } +- +- // Try using the app group container instead of documents directory +- guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: self.hostAppGroupIdentifier) else { ++ ++ return imageExtension ++ } ++ ++ private func saveImageData(_ data: Data, fileExtension: String) -> URL? { ++ guard ++ let imagePath = documentDirectoryPath()?.appendingPathComponent( ++ "\(UUID().uuidString).\(fileExtension)") ++ else { + return nil + } +- +- let fileName = "screenshot_\(UUID().uuidString).png" +- let screenshotPath = containerURL.appendingPathComponent(fileName) +- +- do { +- try screenshotData.write(to: screenshotPath) + +- let fileExists = FileManager.default.fileExists(atPath: screenshotPath.path) +- +- if fileExists { +- let attributes = try? FileManager.default.attributesOfItem(atPath: screenshotPath.path) +- _ = attributes?[.size] as? Int ?? 0 +- } +- +- return screenshotPath ++ do { ++ try data.write(to: imagePath) ++ return imagePath + } catch { +- NSLog("[ERROR] saveScreenshot: Failed to write screenshot: \(error)") +- NSLog("[ERROR] saveScreenshot: Error details: \(error.localizedDescription)") ++ NSLog("Cannot save image data: \(error)") + return nil + } + } +@@ -368,6 +307,7 @@ class ShareViewController: UIViewController { + let fileExtension = self.getExtension(from: url, type: .video) + let fileSize = self.getFileSize(from: url) + let mimeType = url.mimeType(ext: fileExtension) ++ if self.dismissIfVideoTooLarge(fileSize: fileSize) { return } + let newName = "\(UUID().uuidString).\(fileExtension)" + let newPath = FileManager.default + .containerURL( +@@ -375,13 +315,12 @@ class ShareViewController: UIViewController { + .appendingPathComponent(newName) + let copied = self.copyFile(at: url, to: newPath) + if copied { +- guard +- let sharedFile = self.getSharedMediaFile( +- forVideo: newPath, fileName: fileName, fileSize: fileSize, mimeType: mimeType) +- else { +- return +- } ++ let sharedFile = self.getSharedMediaFile( ++ forVideo: newPath, fileName: fileName, fileSize: fileSize, mimeType: mimeType) + self.sharedMedia.append(sharedFile) ++ } else { ++ self.dismissWithError(message: "Could not prepare shared video.") ++ return + } + + // If this is the last item, save imagesData in userDefaults and redirect to host app +@@ -439,6 +378,9 @@ class ShareViewController: UIViewController { + let fileExtension = self.getExtension(from: url, type: .file) + let fileSize = self.getFileSize(from: url) + let mimeType = url.mimeType(ext: fileExtension) ++ if mimeType.hasPrefix("video/") && dismissIfVideoTooLarge(fileSize: fileSize) { ++ return ++ } + let newName = "\(UUID().uuidString).\(fileExtension)" + let newPath = FileManager.default + .containerURL( +@@ -461,11 +403,11 @@ class ShareViewController: UIViewController { + } + } + +- private func dismissWithError(message: String? = nil) { ++ private func dismissWithError(message: String = "Something went wrong.") { + DispatchQueue.main.async { +- NSLog("[ERROR] Error loading application ! \(message!)") ++ NSLog("[ERROR] Share extension failed: \(message)") + let alert = UIAlertController( +- title: "Error", message: "Error loading application: \(message!)", preferredStyle: .alert) ++ title: "Unable to share", message: message, preferredStyle: .alert) + + let action = UIAlertAction(title: "OK", style: .cancel) { _ in + self.dismiss(animated: true, completion: nil) +@@ -504,30 +446,32 @@ class ShareViewController: UIViewController { case file } - + + private func setupLoadingIndicator() { + view.backgroundColor = UIColor.systemBackground.withAlphaComponent(0.92) + loadingIndicator.translatesAutoresizingMaskIntoConstraints = false @@ -87,7 +397,7 @@ index 0911a9e80f9556353e47324a7244daee704aaf71..d295f7713cc13d79bd6020f87e9b6918 - return ex ?? "Unknown" + return ex ?? "" } - + func getFileName(from url: URL, type: SharedMediaType) -> String { var name = url.lastPathComponent if name == "" { @@ -97,3 +407,85 @@ index 0911a9e80f9556353e47324a7244daee704aaf71..d295f7713cc13d79bd6020f87e9b6918 } return name } +@@ -555,19 +499,31 @@ class ShareViewController: UIViewController { + return true + } + ++ private func isVideoTooLarge(fileSize: Int?) -> Bool { ++ return (fileSize ?? 0) > maxVideoSizeBytes ++ } ++ ++ private func dismissIfVideoTooLarge(fileSize: Int?) -> Bool { ++ guard isVideoTooLarge(fileSize: fileSize) else { ++ return false ++ } ++ dismissWithError(message: "Videos must be under \(maxVideoSizeLabel).") ++ return true ++ } ++ + private func getSharedMediaFile(forVideo: URL, fileName: String, fileSize: Int?, mimeType: String) +- -> SharedMediaFile? ++ -> SharedMediaFile + { + let asset = AVAsset(url: forVideo) + let thumbnailPath = getThumbnailPath(for: forVideo) +- let duration = (CMTimeGetSeconds(asset.duration) * 1000).rounded() ++ let durationSeconds = CMTimeGetSeconds(asset.duration) ++ let duration: Double? = ++ durationSeconds.isFinite ? (durationSeconds * 1000).rounded() : nil + var trackWidth: Int? = nil + var trackHeight: Int? = nil + +- // get video info +- let track = asset.tracks(withMediaType: AVMediaType.video).first ?? nil +- if track != nil { +- let size = track!.naturalSize.applying(track!.preferredTransform) ++ if let track = asset.tracks(withMediaType: AVMediaType.video).first { ++ let size = track.naturalSize.applying(track.preferredTransform) + trackWidth = abs(Int(size.width)) + trackHeight = abs(Int(size.height)) + } +@@ -584,19 +540,21 @@ class ShareViewController: UIViewController { + assetImgGenerate.appliesPreferredTrackTransform = true + assetImgGenerate.maximumSize = CGSize(width: 360, height: 360) + do { ++ let captureTime = ++ durationSeconds.isFinite && durationSeconds > 0 ++ ? min(0.1, max(durationSeconds - 0.01, 0)) : 0 + let img = try assetImgGenerate.copyCGImage( +- at: CMTimeMakeWithSeconds(600, preferredTimescale: Int32(1.0)), actualTime: nil) ++ at: CMTimeMakeWithSeconds(captureTime, preferredTimescale: Int32(600)), actualTime: nil) + try UIImage.pngData(UIImage(cgImage: img))()?.write(to: thumbnailPath) + saved = true + } catch { + saved = false + } + +- return saved +- ? SharedMediaFile( +- path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, fileName: fileName, +- fileSize: fileSize, width: trackWidth, height: trackHeight, duration: duration, +- mimeType: mimeType, type: .video) : nil ++ return SharedMediaFile( ++ path: forVideo.absoluteString, thumbnail: saved ? thumbnailPath.absoluteString : nil, ++ fileName: fileName, fileSize: fileSize, width: trackWidth, height: trackHeight, ++ duration: duration, mimeType: mimeType, type: .video) + } + + private func getThumbnailPath(for url: URL) -> URL { +@@ -711,7 +669,6 @@ internal let mimeTypes = [ + "cco": "application/x-cocoa", + "jardiff": "application/x-java-archive-diff", + "jnlp": "application/x-java-jnlp-file", +- "pkpass": "application/vnd.apple.pkpass", + "run": "application/x-makeself", + "pl": "application/x-perl", + "pm": "application/x-perl", +@@ -757,7 +714,6 @@ internal let mimeTypes = [ + "asf": "video/x-ms-asf", + "wmv": "video/x-ms-wmv", + "avi": "video/x-msvideo", +- "vcf": "text/vcard", + ] + + extension URL { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e516c360e..25fd7db4b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,7 +68,7 @@ patchedDependencies: hash: czhrjgdiv3v6vqlm6qvuorhdtu path: patches/expo-image-picker@17.0.10.patch expo-share-intent@5.1.1: - hash: 54xal4ok47f55asg22kiivljna + hash: j7b7xa6uwefxi6or5m4cezogta path: patches/expo-share-intent@5.1.1.patch expo-task-manager@14.0.9: hash: j25h3syph5nqno7flr2dkg62oa @@ -379,7 +379,7 @@ importers: version: 15.0.8(expo@54.0.33(@babel/core@7.29.0)(graphql@16.6.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)) expo-share-intent: specifier: 5.1.1 - version: 5.1.1(patch_hash=54xal4ok47f55asg22kiivljna)(expo-constants@18.0.13(expo@54.0.33(@babel/core@7.29.0)(graphql@16.6.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)))(expo-linking@8.0.11(expo@54.0.33(@babel/core@7.29.0)(graphql@16.6.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(expo@54.0.33(@babel/core@7.29.0)(graphql@16.6.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 5.1.1(patch_hash=j7b7xa6uwefxi6or5m4cezogta)(expo-constants@18.0.13(expo@54.0.33(@babel/core@7.29.0)(graphql@16.6.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)))(expo-linking@8.0.11(expo@54.0.33(@babel/core@7.29.0)(graphql@16.6.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(expo@54.0.33(@babel/core@7.29.0)(graphql@16.6.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) expo-sms: specifier: ~14.0.8 version: 14.0.8(expo@54.0.33(@babel/core@7.29.0)(graphql@16.6.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) @@ -23376,7 +23376,7 @@ snapshots: expo-server@1.0.5: {} - ? expo-share-intent@5.1.1(patch_hash=54xal4ok47f55asg22kiivljna)(expo-constants@18.0.13(expo@54.0.33(@babel/core@7.29.0)(graphql@16.6.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)))(expo-linking@8.0.11(expo@54.0.33(@babel/core@7.29.0)(graphql@16.6.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(expo@54.0.33(@babel/core@7.29.0)(graphql@16.6.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + ? expo-share-intent@5.1.1(patch_hash=j7b7xa6uwefxi6or5m4cezogta)(expo-constants@18.0.13(expo@54.0.33(@babel/core@7.29.0)(graphql@16.6.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)))(expo-linking@8.0.11(expo@54.0.33(@babel/core@7.29.0)(graphql@16.6.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(expo@54.0.33(@babel/core@7.29.0)(graphql@16.6.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) : dependencies: '@expo/config-plugins': 10.1.2 expo: 54.0.33(@babel/core@7.29.0)(graphql@16.6.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=uszx554kwcqz53utcvfh7ypuii)(@babel/core@7.29.0)(@react-native-community/cli@13.6.9(encoding@0.1.13))(@react-native/metro-config@0.81.5(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) diff --git a/scripts/regenerate-expo-share-intent-patch.mjs b/scripts/regenerate-expo-share-intent-patch.mjs index 597eb956a7..874abe9fa6 100644 --- a/scripts/regenerate-expo-share-intent-patch.mjs +++ b/scripts/regenerate-expo-share-intent-patch.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node // Run this after editing apps/tlon-mobile/ios/ShareExtension/ShareViewController.swift -// to regenerate patches/expo-share-intent@3.2.3.patch from a clean package copy. +// to regenerate patches/expo-share-intent@5.1.1.patch from a clean package copy. import { spawnSync } from 'node:child_process'; import { @@ -15,7 +15,7 @@ import { fileURLToPath } from 'node:url'; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const packageName = 'expo-share-intent'; -const packageVersion = '3.2.3'; +const packageVersion = '5.1.1'; const editDir = path.join(repoRoot, '.tmp', `${packageName}-patch`); const appControllerPath = path.join( repoRoot, @@ -26,9 +26,10 @@ const packageControllerPath = path.join( 'plugin/build/ios/ShareExtensionViewController.swift' ); -function run(command, args) { +function run(command, args, options = {}) { const result = spawnSync(command, args, { cwd: repoRoot, + ...options, stdio: 'inherit', }); @@ -75,7 +76,9 @@ try { writeFileSync(packageControllerPath, packageControllerSource()); console.log('Generating pnpm patch'); - run('pnpm', ['patch-commit', editDir]); + run('pnpm', ['patch-commit', editDir], { + env: { ...process.env, npm_config_ignore_scripts: 'true' }, + }); } finally { rmSync(editDir, { recursive: true, force: true }); }