From 88ef5887a91b4a0eace68faec90cd53381436dea Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Sat, 15 Jul 2023 15:05:49 +0530 Subject: [PATCH] feat: You can now share files & text from other apps into SN on iOS (#2358) --- .../ReceiveSharingIntent.m | 14 + .../ReceiveSharingIntent.swift | 147 ++++++++ .../Base.lproj/MainInterface.storyboard | 24 ++ packages/mobile/ios/Share To SN/Info.plist | 34 ++ .../Share To SN/Share To SNDebug.entitlements | 10 + .../ios/Share To SN/ShareViewController.swift | 336 ++++++++++++++++++ .../ios/StandardNotes-Bridging-Header.h | 2 + .../StandardNotes.xcodeproj/project.pbxproj | 215 ++++++++++- .../mobile/ios/StandardNotes/AppDelegate.mm | 42 ++- packages/mobile/ios/StandardNotes/Info.plist | 12 + .../StandardNotesDebug.entitlements | 16 + .../mobile/src/ReceivedSharedItemsHandler.ts | 118 ++++-- 12 files changed, 930 insertions(+), 40 deletions(-) create mode 100644 packages/mobile/ios/ReceiveSharingIntent/ReceiveSharingIntent.m create mode 100644 packages/mobile/ios/ReceiveSharingIntent/ReceiveSharingIntent.swift create mode 100644 packages/mobile/ios/Share To SN/Base.lproj/MainInterface.storyboard create mode 100644 packages/mobile/ios/Share To SN/Info.plist create mode 100644 packages/mobile/ios/Share To SN/Share To SNDebug.entitlements create mode 100644 packages/mobile/ios/Share To SN/ShareViewController.swift create mode 100644 packages/mobile/ios/StandardNotes-Bridging-Header.h create mode 100644 packages/mobile/ios/StandardNotes/StandardNotesDebug.entitlements diff --git a/packages/mobile/ios/ReceiveSharingIntent/ReceiveSharingIntent.m b/packages/mobile/ios/ReceiveSharingIntent/ReceiveSharingIntent.m new file mode 100644 index 000000000..4125b4de1 --- /dev/null +++ b/packages/mobile/ios/ReceiveSharingIntent/ReceiveSharingIntent.m @@ -0,0 +1,14 @@ +#import +#import + + +@interface RCT_EXTERN_MODULE(ReceiveSharingIntent, NSObject) + +RCT_EXTERN_METHOD(getFileNames:(NSString)url + resolver:(RCTPromiseResolveBlock)resolve + rejecter: (RCTPromiseRejectBlock)reject); + +RCT_EXTERN_METHOD(clearFileNames) + + +@end diff --git a/packages/mobile/ios/ReceiveSharingIntent/ReceiveSharingIntent.swift b/packages/mobile/ios/ReceiveSharingIntent/ReceiveSharingIntent.swift new file mode 100644 index 000000000..6d7e5d83f --- /dev/null +++ b/packages/mobile/ios/ReceiveSharingIntent/ReceiveSharingIntent.swift @@ -0,0 +1,147 @@ +import Foundation +import Photos +import MobileCoreServices + +@objc(ReceiveSharingIntent) +class ReceiveSharingIntent: NSObject { + + struct Share: Codable { + var media: [SharedMediaFile] = [] + var text: [String] = [] + var urls: [String] = [] + } + + private var share = Share() + + @objc + func getFileNames(_ url: String, + resolver resolve: RCTPromiseResolveBlock, + rejecter reject: RCTPromiseRejectBlock + ) -> Void { + let fileUrl = URL(string: url) + let json = handleUrl(url: fileUrl); + if(json == "error"){ + let error = NSError(domain: "", code: 400, userInfo: nil) + reject("message", "file type is Invalid", error); + }else if(json == "invalid group name"){ + let error = NSError(domain: "", code: 400, userInfo: nil) + reject("message", "invalid group name. Please check your share extention bundle name is same as `group.mainbundle name` ", error); + }else{ + resolve(json); + } + } + + private func handleUrl(url: URL?) -> String? { + if let url = url { + let appDomain = Bundle.main.bundleIdentifier! + let userDefaults = UserDefaults(suiteName: "group.\(appDomain)") + if let key = url.host?.components(separatedBy: "=").last { + if let mediaJson = userDefaults?.object(forKey: "\(key).media") as? Data { + let mediaSharedArray = decode(data: mediaJson) + let sharedMediaFiles: [SharedMediaFile] = mediaSharedArray.compactMap { + guard let path = getAbsolutePath(for: $0.path) else { + return nil + } + + return SharedMediaFile.init(path: path, fileName: fileNameForPath(path: path), mimeType: mimeTypeForPath(path: path)) + } + self.share.media = sharedMediaFiles + } + if let textSharedArray = userDefaults?.object(forKey: "\(key).text") as? [String] { + self.share.text = textSharedArray + } + if let textSharedArray = userDefaults?.object(forKey: "\(key).url") as? [String] { + self.share.urls = textSharedArray + } + let encodedData = try? JSONEncoder().encode(self.share) + let json = String(data: encodedData!, encoding: .utf8)! + return json + } + return "error" + } + return "invalid group name" + } + + + private func getAbsolutePath(for identifier: String) -> String? { + if (identifier.starts(with: "file://") || identifier.starts(with: "/var/mobile/Media") || identifier.starts(with: "/private/var/mobile")) { + return identifier; + } + let phAsset = PHAsset.fetchAssets(withLocalIdentifiers: [identifier], options: .none).firstObject + if(phAsset == nil) { + return nil + } + let (url, _) = getFullSizeImageURLAndOrientation(for: phAsset!) + return url + } + + private func getFullSizeImageURLAndOrientation(for asset: PHAsset)-> (String?, Int) { + var url: String? = nil + var orientation: Int = 0 + let semaphore = DispatchSemaphore(value: 0) + let options2 = PHContentEditingInputRequestOptions() + options2.isNetworkAccessAllowed = true + asset.requestContentEditingInput(with: options2){(input, info) in + orientation = Int(input?.fullSizeImageOrientation ?? 0) + url = input?.fullSizeImageURL?.path + semaphore.signal() + } + semaphore.wait() + return (url, orientation) + } + + private func decode(data: Data) -> [SharedMediaFile] { + let encodedData = try? JSONDecoder().decode([SharedMediaFile].self, from: data) + return encodedData! + } + + private func toJson(data: [SharedMediaFile]?) -> String? { + if data == nil { + return nil + } + let encodedData = try? JSONEncoder().encode(data) + let json = String(data: encodedData!, encoding: .utf8)! + return json + } + + class SharedMediaFile: Codable { + var path: String; + var fileName: String?; + var mimeType: String?; + + init(path: String, fileName: String?, mimeType: String?) { + self.path = path + self.fileName = fileName + self.mimeType = mimeType + } + } + + @objc + func clearFileNames(){ + print("clearFileNames"); + } + + + @objc + static func requiresMainQueueSetup() -> Bool { + return true + } +} + +func fileNameForPath(path: String) -> String? { + let url = NSURL(fileURLWithPath: path) + return url.lastPathComponent +} + +func mimeTypeForPath(path: String) -> String { + let url = NSURL(fileURLWithPath: path) + let pathExtension = url.pathExtension + + if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension! as NSString, nil)?.takeRetainedValue() { + if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() { + return mimetype as String + } + return "application/octet-stream" + } + return "application/octet-stream" +} diff --git a/packages/mobile/ios/Share To SN/Base.lproj/MainInterface.storyboard b/packages/mobile/ios/Share To SN/Base.lproj/MainInterface.storyboard new file mode 100644 index 000000000..286a50894 --- /dev/null +++ b/packages/mobile/ios/Share To SN/Base.lproj/MainInterface.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/mobile/ios/Share To SN/Info.plist b/packages/mobile/ios/Share To SN/Info.plist new file mode 100644 index 000000000..eb5a80f64 --- /dev/null +++ b/packages/mobile/ios/Share To SN/Info.plist @@ -0,0 +1,34 @@ + + + + + NSExtension + + NSExtensionAttributes + + PHSupportedMediaTypes + + Video + Image + + NSExtensionActivationRule + + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + NSExtensionActivationSupportsImageWithMaxCount + 100 + NSExtensionActivationSupportsMovieWithMaxCount + 100 + NSExtensionActivationSupportsFileWithMaxCount + 100 + + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.share-services + + + diff --git a/packages/mobile/ios/Share To SN/Share To SNDebug.entitlements b/packages/mobile/ios/Share To SN/Share To SNDebug.entitlements new file mode 100644 index 000000000..b013583d4 --- /dev/null +++ b/packages/mobile/ios/Share To SN/Share To SNDebug.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.standardnotes.standardnotes + + + diff --git a/packages/mobile/ios/Share To SN/ShareViewController.swift b/packages/mobile/ios/Share To SN/ShareViewController.swift new file mode 100644 index 000000000..296e8c69b --- /dev/null +++ b/packages/mobile/ios/Share To SN/ShareViewController.swift @@ -0,0 +1,336 @@ +import UIKit +import Social +import MobileCoreServices +import Photos +import UniformTypeIdentifiers + +let hostAppBundleIdentifier = "com.standardnotes.standardnotes" +let shareProtocol = hostAppBundleIdentifier + +class ShareViewController: SLComposeServiceViewController { + let sharedKey = "ShareKey" + var sharedMedia: [SharedMediaFile] = [] + var sharedText: [String] = [] + var sharedURL: [String] = [] + let imageContentType = UTType.image.identifier + let videoContentType = UTType.movie.identifier + let textContentType = UTType.text.identifier + let urlContentType = UTType.url.identifier + let fileURLType = UTType.fileURL.identifier; + + override func isContentValid() -> Bool { + return true + } + + override func viewDidLoad() { + super.viewDidLoad(); + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let content = extensionContext!.inputItems[0] as? NSExtensionItem { + if let contents = content.attachments { + for (index, attachment) in (contents).enumerated() { + if attachment.hasItemConformingToTypeIdentifier(fileURLType) { + handleFiles(content: content, attachment: attachment, index: index) + } else if attachment.hasItemConformingToTypeIdentifier(imageContentType) { + handleImages(content: content, attachment: attachment, index: index) + } else if attachment.hasItemConformingToTypeIdentifier(textContentType) { + handleText(content: content, attachment: attachment, index: index) + } else if attachment.hasItemConformingToTypeIdentifier(urlContentType) { + handleUrl(content: content, attachment: attachment, index: index) + } else if attachment.hasItemConformingToTypeIdentifier(videoContentType) { + handleVideos(content: content, attachment: attachment, index: index) + } + } + } + } + } + + override func didSelectPost() { + print("didSelectPost"); + } + + override func configurationItems() -> [Any]! { + // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here. + return [] + } + + private func handleText (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { + attachment.loadItem(forTypeIdentifier: textContentType, options: nil) { [weak self] data, error in + + if error == nil, let item = data as? String, let this = self { + + this.sharedText.append(item) + + if index == (content.attachments?.count)! - 1 { + this.redirectToHostApp() + } + + } else { + self?.dismissWithError() + } + } + } + + private func handleUrl (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { + attachment.loadItem(forTypeIdentifier: urlContentType, options: nil) { [weak self] data, error in + + if error == nil, let item = data as? URL, let this = self { + + this.sharedURL.append(item.absoluteString) + + if index == (content.attachments?.count)! - 1 { + this.redirectToHostApp() + } + + } else { + self?.dismissWithError() + } + } + } + + private func handleImages (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { + attachment.loadItem(forTypeIdentifier: imageContentType, options: nil) { [weak self] data, error in + + if error == nil, let url = data as? URL, let this = self { + // this.redirectToHostApp(type: .media) + // Always copy + let fileExtension = this.getExtension(from: url, type: .video) + let newName = UUID().uuidString + let newPath = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.\(hostAppBundleIdentifier)")! + .appendingPathComponent("\(newName).\(fileExtension)") + let copied = this.copyFile(at: url, to: newPath) + if(copied) { + this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .image)) + } + + if index == (content.attachments?.count)! - 1 { + this.redirectToHostApp() + } + + } else { + self?.dismissWithError() + } + } + } + + private func handleVideos (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { + attachment.loadItem(forTypeIdentifier: videoContentType, options:nil) { [weak self] data, error in + + if error == nil, let url = data as? URL, let this = self { + + // Always copy + let fileExtension = this.getExtension(from: url, type: .video) + let newName = UUID().uuidString + let newPath = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.\(hostAppBundleIdentifier)")! + .appendingPathComponent("\(newName).\(fileExtension)") + let copied = this.copyFile(at: url, to: newPath) + if(copied) { + guard let sharedFile = this.getSharedMediaFile(forVideo: newPath) else { + return + } + this.sharedMedia.append(sharedFile) + } + + if index == (content.attachments?.count)! - 1 { + this.redirectToHostApp() + } + + } else { + self?.dismissWithError() + } + } + } + + private func handleFiles (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { + attachment.loadItem(forTypeIdentifier: fileURLType, options: nil) { [weak self] data, error in + + if error == nil, let url = data as? URL, let this = self { + + // Always copy + let newName = this.getFileName(from :url) + let newPath = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.\(hostAppBundleIdentifier)")! + .appendingPathComponent("\(newName)") + let copied = this.copyFile(at: url, to: newPath) + if (copied) { + this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .file)) + } + + if index == (content.attachments?.count)! - 1 { + this.redirectToHostApp() + } + + } else { + self?.dismissWithError() + } + } + } + + private func redirectToHostApp() { + let userDefaults = UserDefaults(suiteName: "group.\(hostAppBundleIdentifier)") + userDefaults?.set(self.toData(data: self.sharedMedia), forKey: "\(self.sharedKey).media") + userDefaults?.set(self.sharedText, forKey: "\(self.sharedKey).text") + userDefaults?.set(self.sharedURL, forKey: "\(self.sharedKey).url") + userDefaults?.synchronize() + + let url = URL(string: "\(shareProtocol)://dataUrl=\(sharedKey)") + var responder = self as UIResponder? + let selectorOpenURL = sel_registerName("openURL:") + + while (responder != nil) { + if (responder?.responds(to: selectorOpenURL))! { + let _ = responder?.perform(selectorOpenURL, with: url) + } + responder = responder!.next + } + + extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + } + + private func dismissWithError() { + print("[ERROR] Error loading data!") + let alert = UIAlertController(title: "Error", message: "Error loading data", preferredStyle: .alert) + + let action = UIAlertAction(title: "Error", style: .cancel) { _ in + self.dismiss(animated: true, completion: nil) + } + + alert.addAction(action) + present(alert, animated: true, completion: nil) + extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + } + + private func alertLog(message: String) { + let alert = UIAlertController(title: "Log", message: message, preferredStyle: .alert) + + let action = UIAlertAction(title: "OK", style: .default) + + alert.addAction(action) + present(alert, animated: true, completion: nil) + } + + enum RedirectType { + case media + case text + case file + } + + func getExtension(from url: URL, type: SharedMediaType) -> String { + let parts = url.lastPathComponent.components(separatedBy: ".") + var ex: String? = nil + if (parts.count > 1) { + ex = parts.last + } + + if (ex == nil) { + switch type { + case .image: + ex = "PNG" + case .video: + ex = "MP4" + case .file: + ex = "TXT" + } + } + return ex ?? "Unknown" + } + + func getFileName(from url: URL) -> String { + var name = url.lastPathComponent + + if (name == "") { + name = UUID().uuidString + "." + getExtension(from: url, type: .file) + } + + return name + } + + func copyFile(at srcURL: URL, to dstURL: URL) -> Bool { + do { + if FileManager.default.fileExists(atPath: dstURL.path) { + try FileManager.default.removeItem(at: dstURL) + } + try FileManager.default.copyItem(at: srcURL, to: dstURL) + } catch (let error) { + print("Cannot copy item at \(srcURL) to \(dstURL): \(error)") + return false + } + return true + } + + private func getSharedMediaFile(forVideo: URL) -> SharedMediaFile? { + let asset = AVAsset(url: forVideo) + let duration = (CMTimeGetSeconds(asset.duration) * 1000).rounded() + let thumbnailPath = getThumbnailPath(for: forVideo) + + if FileManager.default.fileExists(atPath: thumbnailPath.path) { + return SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, duration: duration, type: .video) + } + + var saved = false + let assetImgGenerate = AVAssetImageGenerator(asset: asset) + assetImgGenerate.appliesPreferredTrackTransform = true + // let scale = UIScreen.main.scale + assetImgGenerate.maximumSize = CGSize(width: 360, height: 360) + do { + let img = try assetImgGenerate.copyCGImage(at: CMTimeMakeWithSeconds(600, preferredTimescale: Int32(1.0)), 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, duration: duration, type: .video) : nil + + } + + private func getThumbnailPath(for url: URL) -> URL { + let fileName = Data(url.lastPathComponent.utf8).base64EncodedString().replacingOccurrences(of: "==", with: "") + let path = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.\(hostAppBundleIdentifier)")! + .appendingPathComponent("\(fileName).jpg") + return path + } + + class SharedMediaFile: Codable { + var path: String; // can be image, video or url path. It can also be text content + var thumbnail: String?; // video thumbnail + var duration: Double?; // video duration in milliseconds + var type: SharedMediaType; + + + init(path: String, thumbnail: String?, duration: Double?, type: SharedMediaType) { + self.path = path + self.thumbnail = thumbnail + self.duration = duration + self.type = type + } + + // Debug method to print out SharedMediaFile details in the console + func toString() { + print("[SharedMediaFile] \n\tpath: \(self.path)\n\tthumbnail: \(self.thumbnail)\n\tduration: \(self.duration)\n\ttype: \(self.type)") + } + } + + enum SharedMediaType: Int, Codable { + case image + case video + case file + } + + func toData(data: [SharedMediaFile]) -> Data { + let encodedData = try? JSONEncoder().encode(data) + return encodedData! + } +} + +extension Array { + subscript (safe index: UInt) -> Element? { + return Int(index) < count ? self[Int(index)] : nil + } +} diff --git a/packages/mobile/ios/StandardNotes-Bridging-Header.h b/packages/mobile/ios/StandardNotes-Bridging-Header.h new file mode 100644 index 000000000..dea7ff6bf --- /dev/null +++ b/packages/mobile/ios/StandardNotes-Bridging-Header.h @@ -0,0 +1,2 @@ +#import +#import diff --git a/packages/mobile/ios/StandardNotes.xcodeproj/project.pbxproj b/packages/mobile/ios/StandardNotes.xcodeproj/project.pbxproj index ef9814f64..8f7706b9d 100644 --- a/packages/mobile/ios/StandardNotes.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/StandardNotes.xcodeproj/project.pbxproj @@ -8,6 +8,11 @@ /* Begin PBXBuildFile section */ 00E356F31AD99517003FC87E /* StandardNotesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* StandardNotesTests.m */; }; + 07CAB75E2A618128008FE1EF /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07CAB75D2A618128008FE1EF /* ShareViewController.swift */; }; + 07CAB7612A618128008FE1EF /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 07CAB75F2A618128008FE1EF /* MainInterface.storyboard */; }; + 07CAB7652A618128008FE1EF /* Share To SN.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 07CAB75B2A618128008FE1EF /* Share To SN.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 07CAB76E2A618353008FE1EF /* ReceiveSharingIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07CAB76D2A618353008FE1EF /* ReceiveSharingIntent.swift */; }; + 07CAB7702A618385008FE1EF /* ReceiveSharingIntent.m in Sources */ = {isa = PBXBuildFile; fileRef = 07CAB76F2A618385008FE1EF /* ReceiveSharingIntent.m */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 1C2EEB3B45F4EB07AC795C77 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; @@ -34,13 +39,43 @@ remoteGlobalIDString = 13B07F861A680F5B00A75B9A; remoteInfo = StandardNotes; }; + 07CAB7632A618128008FE1EF /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 07CAB75A2A618128008FE1EF; + remoteInfo = "Share To SN"; + }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + 07CAB7662A618128008FE1EF /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 07CAB7652A618128008FE1EF /* Share To SN.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 00E356EE1AD99517003FC87E /* StandardNotesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StandardNotesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 00E356F21AD99517003FC87E /* StandardNotesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = StandardNotesTests.m; sourceTree = ""; }; 04FCB5A3A3387CA3CFC82AA3 /* libPods-StandardNotes-StandardNotesTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-StandardNotes-StandardNotesTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 07CAB75B2A618128008FE1EF /* Share To SN.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Share To SN.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + 07CAB75D2A618128008FE1EF /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; + 07CAB7602A618128008FE1EF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + 07CAB7622A618128008FE1EF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 07CAB76A2A6182C7008FE1EF /* StandardNotesDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = StandardNotesDebug.entitlements; path = StandardNotes/StandardNotesDebug.entitlements; sourceTree = ""; }; + 07CAB76B2A6182D6008FE1EF /* Share To SNDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Share To SNDebug.entitlements"; sourceTree = ""; }; + 07CAB76C2A618353008FE1EF /* StandardNotes-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "StandardNotes-Bridging-Header.h"; sourceTree = ""; }; + 07CAB76D2A618353008FE1EF /* ReceiveSharingIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ReceiveSharingIntent.swift; path = ReceiveSharingIntent/ReceiveSharingIntent.swift; sourceTree = ""; }; + 07CAB76F2A618385008FE1EF /* ReceiveSharingIntent.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = ReceiveSharingIntent.m; path = ReceiveSharingIntent/ReceiveSharingIntent.m; sourceTree = ""; }; 0BB10C8D896AFACECE748F6D /* Pods-StandardNotesDev.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StandardNotesDev.release.xcconfig"; path = "Target Support Files/Pods-StandardNotesDev/Pods-StandardNotesDev.release.xcconfig"; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* StandardNotes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StandardNotes.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = StandardNotes/AppDelegate.h; sourceTree = ""; }; @@ -75,6 +110,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 07CAB7582A618128008FE1EF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -113,6 +155,17 @@ name = "Supporting Files"; sourceTree = ""; }; + 07CAB75C2A618128008FE1EF /* Share To SN */ = { + isa = PBXGroup; + children = ( + 07CAB76B2A6182D6008FE1EF /* Share To SNDebug.entitlements */, + 07CAB75D2A618128008FE1EF /* ShareViewController.swift */, + 07CAB75F2A618128008FE1EF /* MainInterface.storyboard */, + 07CAB7622A618128008FE1EF /* Info.plist */, + ); + path = "Share To SN"; + sourceTree = ""; + }; 0D0B9F98B8FBE9ED9C11F260 /* Pods */ = { isa = PBXGroup; children = ( @@ -129,6 +182,7 @@ 13B07FAE1A68108700A75B9A /* StandardNotes */ = { isa = PBXGroup; children = ( + 07CAB76A2A6182C7008FE1EF /* StandardNotesDebug.entitlements */, D261494428699DCE00B17102 /* Web.bundle */, 13B07FAF1A68108700A75B9A /* AppDelegate.h */, CDC75795292552080019F4AF /* AppDelegate.mm */, @@ -137,6 +191,9 @@ 13B07FB71A68108700A75B9A /* main.m */, CD7D5EC8278005B6005FE1BF /* Info.plist */, CD7D5EC927800608005FE1BF /* LaunchScreen.storyboard */, + 07CAB76D2A618353008FE1EF /* ReceiveSharingIntent.swift */, + 07CAB76C2A618353008FE1EF /* StandardNotes-Bridging-Header.h */, + 07CAB76F2A618385008FE1EF /* ReceiveSharingIntent.m */, ); name = StandardNotes; sourceTree = ""; @@ -166,6 +223,7 @@ 13B07FAE1A68108700A75B9A /* StandardNotes */, 832341AE1AAA6A7D00B99B32 /* Libraries */, 00E356EF1AD99517003FC87E /* StandardNotesTests */, + 07CAB75C2A618128008FE1EF /* Share To SN */, 83CBBA001A601CBA00E9B192 /* Products */, 2D16E6871FA4F8E400B85C8A /* Frameworks */, CD7D5EE127801645005FE1BF /* StandardNotesDev-Info.plist */, @@ -184,6 +242,7 @@ 13B07F961A680F5B00A75B9A /* StandardNotes.app */, 00E356EE1AD99517003FC87E /* StandardNotesTests.xctest */, CD7D5EDF278015D2005FE1BF /* StandardNotesDev.app */, + 07CAB75B2A618128008FE1EF /* Share To SN.appex */, ); name = Products; sourceTree = ""; @@ -212,6 +271,23 @@ productReference = 00E356EE1AD99517003FC87E /* StandardNotesTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + 07CAB75A2A618128008FE1EF /* Share To SN */ = { + isa = PBXNativeTarget; + buildConfigurationList = 07CAB7692A618128008FE1EF /* Build configuration list for PBXNativeTarget "Share To SN" */; + buildPhases = ( + 07CAB7572A618128008FE1EF /* Sources */, + 07CAB7582A618128008FE1EF /* Frameworks */, + 07CAB7592A618128008FE1EF /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Share To SN"; + productName = "Share To SN"; + productReference = 07CAB75B2A618128008FE1EF /* Share To SN.appex */; + productType = "com.apple.product-type.app-extension"; + }; 13B07F861A680F5B00A75B9A /* StandardNotes */ = { isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "StandardNotes" */; @@ -224,10 +300,12 @@ 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, ADF6EC52208176452ADF4217 /* [CP] Embed Pods Frameworks */, BD9D24718C148A593389F498 /* [CP] Copy Pods Resources */, + 07CAB7662A618128008FE1EF /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( + 07CAB7642A618128008FE1EF /* PBXTargetDependency */, ); name = StandardNotes; productName = StandardNotes; @@ -262,14 +340,18 @@ 83CBB9F71A601CBA00E9B192 /* Project object */ = { isa = PBXProject; attributes = { + LastSwiftUpdateCheck = 1420; LastUpgradeCheck = 1210; TargetAttributes = { 00E356ED1AD99517003FC87E = { CreatedOnToolsVersion = 6.2; TestTargetID = 13B07F861A680F5B00A75B9A; }; + 07CAB75A2A618128008FE1EF = { + CreatedOnToolsVersion = 14.2; + }; 13B07F861A680F5B00A75B9A = { - LastSwiftMigration = 1120; + LastSwiftMigration = 1420; }; }; }; @@ -289,6 +371,7 @@ 13B07F861A680F5B00A75B9A /* StandardNotes */, 00E356ED1AD99517003FC87E /* StandardNotesTests */, CD7D5ECB278015D2005FE1BF /* StandardNotesDev */, + 07CAB75A2A618128008FE1EF /* Share To SN */, ); }; /* End PBXProject section */ @@ -301,6 +384,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 07CAB7592A618128008FE1EF /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 07CAB7612A618128008FE1EF /* MainInterface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 13B07F8E1A680F5B00A75B9A /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -570,11 +661,21 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 07CAB7572A618128008FE1EF /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 07CAB75E2A618128008FE1EF /* ShareViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 13B07F871A680F5B00A75B9A /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( CDC75796292552080019F4AF /* AppDelegate.mm in Sources */, + 07CAB76E2A618353008FE1EF /* ReceiveSharingIntent.swift in Sources */, + 07CAB7702A618385008FE1EF /* ReceiveSharingIntent.m in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -596,13 +697,30 @@ target = 13B07F861A680F5B00A75B9A /* StandardNotes */; targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; }; + 07CAB7642A618128008FE1EF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 07CAB75A2A618128008FE1EF /* Share To SN */; + targetProxy = 07CAB7632A618128008FE1EF /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ +/* Begin PBXVariantGroup section */ + 07CAB75F2A618128008FE1EF /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 07CAB7602A618128008FE1EF /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ 00E356F61AD99517003FC87E /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 5915B3231CE7C52662278306 /* Pods-StandardNotes-StandardNotesTests.debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -630,6 +748,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 321F15E603CF0AF8B1769447 /* Pods-StandardNotes-StandardNotesTests.release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; COPY_PHASE_STRIP = NO; INFOPLIST_FILE = StandardNotesTests/Info.plist; @@ -650,13 +769,93 @@ }; name = Release; }; + 07CAB7672A618128008FE1EF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = "Share To SN/Share To SNDebug.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = HKF9BXSN95; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Share To SN/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "Share To SN"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.standardnotes.standardnotes.Share-To-SN"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 07CAB7682A618128008FE1EF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = HKF9BXSN95; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Share To SN/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "Share To SN"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.standardnotes.standardnotes.Share-To-SN"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 66417CEB7622E77D89928FCA /* Pods-StandardNotes.debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = Blue; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = StandardNotes/StandardNotes.entitlements; + CODE_SIGN_ENTITLEMENTS = StandardNotes/StandardNotesDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -676,6 +875,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.standardnotes.standardnotes; PRODUCT_NAME = StandardNotes; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "StandardNotes-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -687,6 +887,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 948EE90E15EA48C27577820B /* Pods-StandardNotes.release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = Blue; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = StandardNotes/StandardNotes.entitlements; @@ -710,6 +911,7 @@ PRODUCT_NAME = StandardNotes; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.standardnotes.standardnotes"; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.standardnotes.standardnotes"; + SWIFT_OBJC_BRIDGING_HEADER = "StandardNotes-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; @@ -916,6 +1118,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 07CAB7692A618128008FE1EF /* Build configuration list for PBXNativeTarget "Share To SN" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 07CAB7672A618128008FE1EF /* Debug */, + 07CAB7682A618128008FE1EF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "StandardNotes" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/packages/mobile/ios/StandardNotes/AppDelegate.mm b/packages/mobile/ios/StandardNotes/AppDelegate.mm index 0c665dadf..a69d300aa 100644 --- a/packages/mobile/ios/StandardNotes/AppDelegate.mm +++ b/packages/mobile/ios/StandardNotes/AppDelegate.mm @@ -1,28 +1,36 @@ #import "AppDelegate.h" #import +#import #import #import @implementation AppDelegate +- (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + options:(NSDictionary *)options +{ + return [RCTLinkingManager application:application openURL:url options:options]; +} + - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - + [self configurePinning]; - + [self disableUrlCache]; - + [self clearWebEditorCache]; - + NSString *CFBundleIdentifier = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleIdentifier"]; - + NSDictionary * initialProperties = @{@"env" : [CFBundleIdentifier isEqualToString:@"com.standardnotes.standardnotes.dev"] ? @"dev" : @"prod"}; - + self.moduleName = @"StandardNotes"; self.initialProps = @{}; - + return [super application:application didFinishLaunchingWithOptions:launchOptions]; } @@ -50,12 +58,12 @@ if(![currentVersion isEqualToString:lastVersionClear]) { // UIWebView [[NSURLCache sharedURLCache] removeAllCachedResponses]; - + // WebKit NSSet *websiteDataTypes = [WKWebsiteDataStore allWebsiteDataTypes]; NSDate *dateFrom = [NSDate dateWithTimeIntervalSince1970:0]; [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes modifiedSince:dateFrom completionHandler:^{}]; - + [[NSUserDefaults standardUserDefaults] setObject:currentVersion forKey:lastVersionClearKey]; } } @@ -65,17 +73,17 @@ NSDictionary *trustKitConfig = @{ kTSKSwizzleNetworkDelegates: @YES, - + // The list of domains we want to pin and their configuration kTSKPinnedDomains: @{ @"standardnotes.org" : @{ kTSKIncludeSubdomains:@YES, - + kTSKEnforcePinning:@YES, - + // Send reports for pin validation failures so we can track them kTSKReportUris: @[@"https://standard.report-uri.com/r/d/hpkp/reportOnly"], - + // The pinned public keys' Subject Public Key Info hashes kTSKPublicKeyHashes : @[ @"Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys=", @@ -90,12 +98,12 @@ }, @"standardnotes.com" : @{ kTSKIncludeSubdomains:@YES, - + kTSKEnforcePinning:@YES, - + // Send reports for pin validation failures so we can track them kTSKReportUris: @[@"https://standard.report-uri.com/r/d/hpkp/reportOnly"], - + // The pinned public keys' Subject Public Key Info hashes kTSKPublicKeyHashes : @[ @"Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys=", @@ -110,7 +118,7 @@ }, } }; - + [TrustKit initSharedInstanceWithConfiguration:trustKitConfig]; } } diff --git a/packages/mobile/ios/StandardNotes/Info.plist b/packages/mobile/ios/StandardNotes/Info.plist index 569f5dff8..8c773ef90 100644 --- a/packages/mobile/ios/StandardNotes/Info.plist +++ b/packages/mobile/ios/StandardNotes/Info.plist @@ -46,6 +46,18 @@ ???? CFBundleVersion $(CURRENT_PROJECT_VERSION) + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + $(PRODUCT_BUNDLE_IDENTIFIER) + + + + LSApplicationQueriesSchemes http diff --git a/packages/mobile/ios/StandardNotes/StandardNotesDebug.entitlements b/packages/mobile/ios/StandardNotes/StandardNotesDebug.entitlements new file mode 100644 index 000000000..11be7598d --- /dev/null +++ b/packages/mobile/ios/StandardNotes/StandardNotesDebug.entitlements @@ -0,0 +1,16 @@ + + + + + com.apple.developer.associated-domains + + webcredentials:app.standardnotes.com + + com.apple.developer.default-data-protection + NSFileProtectionComplete + com.apple.security.application-groups + + group.com.standardnotes.standardnotes + + + diff --git a/packages/mobile/src/ReceivedSharedItemsHandler.ts b/packages/mobile/src/ReceivedSharedItemsHandler.ts index a203834a2..0c0ec520e 100644 --- a/packages/mobile/src/ReceivedSharedItemsHandler.ts +++ b/packages/mobile/src/ReceivedSharedItemsHandler.ts @@ -1,6 +1,6 @@ import { ReactNativeToWebEvent } from '@standardnotes/snjs' import { RefObject } from 'react' -import { AppState, NativeEventSubscription, NativeModules, Platform } from 'react-native' +import { AppState, Linking, NativeEventSubscription, NativeModules, Platform } from 'react-native' import { readFile } from 'react-native-fs' import WebView from 'react-native-webview' const { ReceiveSharingIntent } = NativeModules @@ -13,13 +13,19 @@ type ReceivedItem = { text?: string | null weblink?: string | null subject?: string | null + path?: string | null } -type ReceivedFile = ReceivedItem & { +type ReceivedAndroidFile = ReceivedItem & { contentUri: string mimeType: string } +type ReceivedIosFile = ReceivedItem & { + path: string + mimeType: string +} + type ReceivedWeblink = ReceivedItem & { weblink: string } @@ -28,10 +34,14 @@ type ReceivedText = ReceivedItem & { text: string } -const isReceivedFile = (item: ReceivedItem): item is ReceivedFile => { +const isReceivedAndroidFile = (item: ReceivedItem): item is ReceivedAndroidFile => { return !!item.contentUri && !!item.mimeType } +const isReceivedIosFile = (item: ReceivedItem): item is ReceivedIosFile => { + return !!item.path +} + const isReceivedWeblink = (item: ReceivedItem): item is ReceivedWeblink => { return !!item.weblink } @@ -40,15 +50,16 @@ const isReceivedText = (item: ReceivedItem): item is ReceivedText => { return !!item.text } +const BundleIdentifier = 'com.standardnotes.standardnotes' +const IosUrlToCheckFor = `${BundleIdentifier}://dataUrl` + export class ReceivedSharedItemsHandler { - private appStateEventSub: NativeEventSubscription | null = null + private eventSub: NativeEventSubscription | null = null private receivedItemsQueue: ReceivedItem[] = [] private isApplicationLaunched = false constructor(private webViewRef: RefObject) { - if (Platform.OS === 'android') { - this.registerNativeEventSub() - } + this.registerNativeEventSub() } setIsApplicationLaunched = (isApplicationLaunched: boolean) => { @@ -61,23 +72,29 @@ export class ReceivedSharedItemsHandler { deinit() { this.receivedItemsQueue = [] - this.appStateEventSub?.remove() + this.eventSub?.remove() } private registerNativeEventSub = () => { - this.appStateEventSub = AppState.addEventListener('change', (state) => { - if (state === 'active') { - ReceiveSharingIntent.getFileNames() - .then(async (filesObject: Record) => { - const items = Object.values(filesObject) - this.receivedItemsQueue.push(...items) + if (Platform.OS === 'ios') { + Linking.getInitialURL() + .then((url) => { + if (url && url.startsWith(IosUrlToCheckFor)) { + this.addSharedItemsToQueue(url) + } + }) + .catch(console.error) + this.eventSub = Linking.addEventListener('url', ({ url }) => { + if (url && url.startsWith(IosUrlToCheckFor)) { + this.addSharedItemsToQueue(url) + } + }) + return + } - if (this.isApplicationLaunched) { - this.handleItemsQueue().catch(console.error) - } - }) - .then(() => ReceiveSharingIntent.clearFileNames()) - .catch(console.error) + this.eventSub = AppState.addEventListener('change', (state) => { + if (state === 'active') { + this.addSharedItemsToQueue() } }) } @@ -92,7 +109,7 @@ export class ReceivedSharedItemsHandler { return } - if (isReceivedFile(item)) { + if (isReceivedAndroidFile(item)) { const data = await readFile(item.contentUri, 'base64') const file = { name: item.fileName || item.contentUri, @@ -106,6 +123,20 @@ export class ReceivedSharedItemsHandler { messageData: file, }), ) + } else if (isReceivedIosFile(item)) { + const data = await readFile(item.path, 'base64') + const file = { + name: item.fileName || item.path, + data, + mimeType: item.mimeType, + } + this.webViewRef.current?.postMessage( + JSON.stringify({ + reactNativeEvent: ReactNativeToWebEvent.ReceivedFile, + messageType: 'event', + messageData: file, + }), + ) } else if (isReceivedWeblink(item)) { this.webViewRef.current?.postMessage( JSON.stringify({ @@ -131,4 +162,49 @@ export class ReceivedSharedItemsHandler { this.handleItemsQueue().catch(console.error) } + + private addSharedItemsToQueue = (url?: string) => { + ReceiveSharingIntent.getFileNames(url) + .then(async (received: unknown) => { + if (!received) { + return + } + + if (Platform.OS === 'android') { + const items = Object.values(received as Record) + this.receivedItemsQueue.push(...items) + } else if (typeof received === 'string') { + const parsed: unknown = JSON.parse(received) + if (typeof parsed !== 'object') { + return + } + if (!parsed) { + return + } + if ('media' in parsed && Array.isArray(parsed.media)) { + this.receivedItemsQueue.push(...parsed.media) + } + if ('text' in parsed && Array.isArray(parsed.text)) { + this.receivedItemsQueue.push( + ...parsed.text.map((text: string) => ({ + text: text, + })), + ) + } + if ('urls' in parsed && Array.isArray(parsed.urls)) { + this.receivedItemsQueue.push( + ...parsed.urls.map((url: string) => ({ + weblink: url, + })), + ) + } + } + + if (this.isApplicationLaunched) { + this.handleItemsQueue().catch(console.error) + } + }) + .then(() => ReceiveSharingIntent.clearFileNames()) + .catch(console.error) + } }