From 3d498687823be607d43c25988e82a7c395663e9f Mon Sep 17 00:00:00 2001 From: Chris Jansen Date: Fri, 29 Dec 2023 12:22:34 +0100 Subject: [PATCH 1/4] Rewrite app group handling --- ios/Constants.swift | 1 + ios/Modules/ShareMenu.swift | 28 +++-- ios/ShareViewController.swift | 230 ++++++++++++++++++---------------- 3 files changed, 142 insertions(+), 117 deletions(-) diff --git a/ios/Constants.swift b/ios/Constants.swift index 08385b79..fd1d2b6d 100644 --- a/ios/Constants.swift +++ b/ios/Constants.swift @@ -32,6 +32,7 @@ public let USER_DEFAULTS_EXTRA_DATA_KEY = "ShareMenuUserDefaultsExtraData" public let URL_SCHEME_INFO_PLIST_KEY = "AppURLScheme" public let HOST_APP_IDENTIFIER_INFO_PLIST_KEY = "HostAppBundleIdentifier" public let HOST_URL_SCHEME_INFO_PLIST_KEY = "HostAppURLScheme" +public let APP_GROUP_KEY = "AppGroup" public let REACT_SHARE_VIEW_BACKGROUND_COLOR_KEY = "ReactShareViewBackgroundColor" public let COLOR_RED_KEY = "Red" diff --git a/ios/Modules/ShareMenu.swift b/ios/Modules/ShareMenu.swift index 74badda6..bc94b9a1 100644 --- a/ios/Modules/ShareMenu.swift +++ b/ios/Modules/ShareMenu.swift @@ -83,30 +83,42 @@ class ShareMenu: RCTEventEmitter { } guard let scheme = url.scheme, scheme == targetUrlScheme else { return } - guard let bundleId = Bundle.main.bundleIdentifier else { return } - guard let userDefaults = UserDefaults(suiteName: "group.\(bundleId)") else { + guard let group = getGroup() else { print("Error: \(NO_APP_GROUP_ERROR)") return } - let extraData = userDefaults.object(forKey: USER_DEFAULTS_EXTRA_DATA_KEY) as? [String:Any] + let userDefaults = UserDefaults(suiteName: group) + let extraData = userDefaults?.object(forKey: USER_DEFAULTS_EXTRA_DATA_KEY) as? [String:Any] - if let data = userDefaults.object(forKey: USER_DEFAULTS_KEY) as? [[String:String]] { + if let data = userDefaults?.object(forKey: USER_DEFAULTS_KEY) as? [[String:String]] { sharedData = data dispatchEvent(with: data, and: extraData) - userDefaults.removeObject(forKey: USER_DEFAULTS_KEY) + userDefaults?.removeObject(forKey: USER_DEFAULTS_KEY) } } + + func getGroup() -> String? { + guard let group = Bundle.main.object(forInfoDictionaryKey: APP_GROUP_KEY) as? String else { + guard let bundleId = Bundle.main.bundleIdentifier else { + return nil + } + return "group.\(bundleId)" + } + + return group + } @objc(getSharedText:) func getSharedText(callback: RCTResponseSenderBlock) { var data = [DATA_KEY: sharedData] as [String: Any] - if let bundleId = Bundle.main.bundleIdentifier, let userDefaults = UserDefaults(suiteName: "group.\(bundleId)") { - data[EXTRA_DATA_KEY] = userDefaults.object(forKey: USER_DEFAULTS_EXTRA_DATA_KEY) as? [String: Any] - } else { + guard let group = getGroup() else { print("Error: \(NO_APP_GROUP_ERROR)") + return } + let userDefaults = UserDefaults(suiteName: group) + data[EXTRA_DATA_KEY] = userDefaults?.object(forKey: USER_DEFAULTS_EXTRA_DATA_KEY) as? [String: Any] callback([data as Any]) sharedData = [] diff --git a/ios/ShareViewController.swift b/ios/ShareViewController.swift index 12d8c92d..577c5059 100644 --- a/ios/ShareViewController.swift +++ b/ios/ShareViewController.swift @@ -14,10 +14,58 @@ import UIKit import Social import RNShareMenu +enum ShareViewControllerError: Error { + case missingKey(key: String) + case unknownGroup(name: String) + case fileAccessError(debugDescription: String) +} + class ShareViewController: SLComposeServiceViewController { var hostAppId: String? - var hostAppUrlScheme: String? - var sharedItems: [Any] = [] + var sharedItems: [[String: String]] = [] + + internal func getStringFromPlist(key: String) throws -> String { + guard let entry = Bundle.main.object(forInfoDictionaryKey: key) as? String else { + if #available(iOSApplicationExtension 14.0, *) { + Logger().error("Error: You haven't defined '\(key)' in your Share Extension's Info.plist") + } + throw ShareViewControllerError.missingKey(key: key) + } + + return entry + } + + func getAppGroupName() throws -> String { + do { + return try getStringFromPlist(key: APP_GROUP_KEY) + } + catch { + return try "group." + getStringFromPlist(key: HOST_APP_IDENTIFIER_INFO_PLIST_KEY) + } + } + + func getUrlScheme() throws -> String { + return try getStringFromPlist(key: HOST_URL_SCHEME_INFO_PLIST_KEY) + } + + func getUserDefaultsInstance() throws -> UserDefaults { + let appGroup = try getAppGroupName() + guard let userDefaults = UserDefaults(suiteName: appGroup) else { + throw ShareViewControllerError.unknownGroup(name: appGroup) + } + + return userDefaults + } + + func getGroupFileManagerContainer() throws -> URL { + let appGroup = try getAppGroupName() + guard let groupFileManagerContainer = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: appGroup) + else { + throw ShareViewControllerError.unknownGroup(name: appGroup) + } + return groupFileManagerContainer + } override func viewDidLoad() { super.viewDidLoad() @@ -27,12 +75,6 @@ class ShareViewController: SLComposeServiceViewController { } else { print("Error: \(NO_INFO_PLIST_INDENTIFIER_ERROR)") } - - if let hostAppUrlScheme = Bundle.main.object(forInfoDictionaryKey: HOST_URL_SCHEME_INFO_PLIST_KEY) as? String { - self.hostAppUrlScheme = hostAppUrlScheme - } else { - print("Error: \(NO_INFO_PLIST_URL_SCHEME_ERROR)") - } } override func isContentValid() -> Bool { @@ -57,73 +99,56 @@ class ShareViewController: SLComposeServiceViewController { func handlePost(_ items: [NSExtensionItem], extraData: [String:Any]? = nil) { DispatchQueue.global().async { - guard let hostAppId = self.hostAppId else { - self.exit(withError: NO_INFO_PLIST_INDENTIFIER_ERROR) - return - } - guard let userDefaults = UserDefaults(suiteName: "group.\(hostAppId)") else { - self.exit(withError: NO_APP_GROUP_ERROR) - return - } - - if let data = extraData { - self.storeExtraData(data) - } else { - self.removeExtraData() - } - - let semaphore = DispatchSemaphore(value: 0) - var results: [Any] = [] - - for item in items { - guard let attachments = item.attachments else { - self.cancelRequest() - return + do { + let userDefaults = try self.getUserDefaultsInstance() + + if let data = extraData { + try self.storeExtraData(data) + } else { + try self.removeExtraData() } - - for provider in attachments { - if provider.isText { - self.storeText(withProvider: provider, semaphore) - } else if provider.isURL { - self.storeUrl(withProvider: provider, semaphore) - } else { - self.storeFile(withProvider: provider, semaphore) + + let semaphore = DispatchSemaphore(value: 0) + + for item in items { + guard let attachments = item.attachments else { + self.cancelRequest() + return } - - semaphore.wait() + + for provider in attachments { + if provider.isText { + self.storeText(withProvider: provider, semaphore) + } else if provider.isURL { + self.storeUrl(withProvider: provider, semaphore) + } else { + self.storeFile(withProvider: provider, semaphore) + } + + semaphore.wait() + } + } - } - userDefaults.set(self.sharedItems, - forKey: USER_DEFAULTS_KEY) - userDefaults.synchronize() + userDefaults.set(self.sharedItems, forKey: USER_DEFAULTS_KEY) + userDefaults.synchronize() - self.openHostApp() + try self.openHostApp() + } + catch { + self.extensionContext!.cancelRequest(withError: error) + } } } - func storeExtraData(_ data: [String:Any]) { - guard let hostAppId = self.hostAppId else { - print("Error: \(NO_INFO_PLIST_INDENTIFIER_ERROR)") - return - } - guard let userDefaults = UserDefaults(suiteName: "group.\(hostAppId)") else { - print("Error: \(NO_APP_GROUP_ERROR)") - return - } + func storeExtraData(_ data: [String:Any]) throws { + let userDefaults = try self.getUserDefaultsInstance() userDefaults.set(data, forKey: USER_DEFAULTS_EXTRA_DATA_KEY) userDefaults.synchronize() } - func removeExtraData() { - guard let hostAppId = self.hostAppId else { - print("Error: \(NO_INFO_PLIST_INDENTIFIER_ERROR)") - return - } - guard let userDefaults = UserDefaults(suiteName: "group.\(hostAppId)") else { - print("Error: \(NO_APP_GROUP_ERROR)") - return - } + func removeExtraData() throws { + let userDefaults = try self.getUserDefaultsInstance() userDefaults.removeObject(forKey: USER_DEFAULTS_EXTRA_DATA_KEY) userDefaults.synchronize() } @@ -162,53 +187,36 @@ class ShareViewController: SLComposeServiceViewController { func storeFile(withProvider provider: NSItemProvider, _ semaphore: DispatchSemaphore) { provider.loadItem(forTypeIdentifier: kUTTypeData as String, options: nil) { (data, error) in - guard (error == nil) else { - self.exit(withError: error.debugDescription) - return - } - guard let url = data as? URL else { - self.exit(withError: COULD_NOT_FIND_IMG_ERROR) - return - } - guard let hostAppId = self.hostAppId else { - self.exit(withError: NO_INFO_PLIST_INDENTIFIER_ERROR) - return - } - guard let groupFileManagerContainer = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.\(hostAppId)") - else { - self.exit(withError: NO_APP_GROUP_ERROR) - return + do { + guard (error == nil) else { + throw ShareViewControllerError.fileAccessError(debugDescription: error.debugDescription) + } + guard let url = data as? URL else { + throw ShareViewControllerError.fileAccessError(debugDescription: COULD_NOT_FIND_IMG_ERROR) + } + + let groupFileManagerContainer = try self.getGroupFileManagerContainer() + let mimeType = url.extractMimeType() + let fileExtension = url.pathExtension + let fileName = UUID().uuidString + let filePath = groupFileManagerContainer + .appendingPathComponent("\(fileName).\(fileExtension)") + + try self.moveFileToDisk(from: url, to: filePath) + self.sharedItems.append([DATA_KEY: filePath.absoluteString, MIME_TYPE_KEY: mimeType]) + semaphore.signal() } - - let mimeType = url.extractMimeType() - let fileExtension = url.pathExtension - let fileName = UUID().uuidString - let filePath = groupFileManagerContainer - .appendingPathComponent("\(fileName).\(fileExtension)") - - guard self.moveFileToDisk(from: url, to: filePath) else { - self.exit(withError: COULD_NOT_SAVE_FILE_ERROR) - return + catch { + self.extensionContext!.cancelRequest(withError: error) } - - self.sharedItems.append([DATA_KEY: filePath.absoluteString, MIME_TYPE_KEY: mimeType]) - semaphore.signal() } } - func moveFileToDisk(from srcUrl: URL, to destUrl: URL) -> Bool { - do { - if FileManager.default.fileExists(atPath: destUrl.path) { - try FileManager.default.removeItem(at: destUrl) - } - try FileManager.default.copyItem(at: srcUrl, to: destUrl) - } catch (let error) { - print("Could not save file from \(srcUrl) to \(destUrl): \(error)") - return false + func moveFileToDisk(from srcUrl: URL, to destUrl: URL) throws { + if FileManager.default.fileExists(atPath: destUrl.path) { + try FileManager.default.removeItem(at: destUrl) } - - return true + try FileManager.default.copyItem(at: srcUrl, to: destUrl) } func exit(withError error: String) { @@ -216,13 +224,17 @@ class ShareViewController: SLComposeServiceViewController { cancelRequest() } - internal func openHostApp() { - guard let urlScheme = self.hostAppUrlScheme else { - exit(withError: NO_INFO_PLIST_URL_SCHEME_ERROR) - return + internal func getHostAppUrl() throws -> URL? { + let urlScheme = try self.getUrlScheme() + if !urlScheme.hasSuffix("://") { + return URL(string: urlScheme + "://") } - let url = URL(string: urlScheme) + return URL(string: urlScheme) + } + + internal func openHostApp() throws { + let url = try getHostAppUrl() let selectorOpenURL = sel_registerName("openURL:") var responder: UIResponder? = self From 676da895b14fdb4245419b4011617cffb79edb0f Mon Sep 17 00:00:00 2001 From: Chris Jansen Date: Fri, 29 Dec 2023 16:33:26 +0100 Subject: [PATCH 2/4] Remove unused constants --- ios/Constants.swift | 5 ----- ios/ShareViewController.swift | 11 ----------- 2 files changed, 16 deletions(-) diff --git a/ios/Constants.swift b/ios/Constants.swift index fd1d2b6d..e1c50d17 100644 --- a/ios/Constants.swift +++ b/ios/Constants.swift @@ -12,15 +12,10 @@ public let DISMISS_SHARE_EXTENSION_WITH_ERROR_CODE = 1 public let NO_URL_TYPES_ERROR_MESSAGE = "You have not defined CFBundleURLTypes in your Info.plist" public let NO_URL_SCHEMES_ERROR_MESSAGE = "You have not defined CFBundleURLSchemes in your Info.plist" -public let NO_SCHEME_ERROR_MESSAGE = "You have not defined a scheme under CFBundleURLSchemes in your Info.plist" public let NO_APP_GROUP_ERROR = "Failed to get App Group User Defaults. Did you set up an App Group on your App and Share Extension?" -public let NO_INFO_PLIST_INDENTIFIER_ERROR = "You haven't defined \(HOST_APP_IDENTIFIER_INFO_PLIST_KEY) in your Share Extension's Info.plist" -public let NO_INFO_PLIST_URL_SCHEME_ERROR = "You haven't defined \(HOST_URL_SCHEME_INFO_PLIST_KEY) in your Share Extension's Info.plist" public let COULD_NOT_FIND_STRING_ERROR = "Couldn't find string" public let COULD_NOT_FIND_URL_ERROR = "Couldn't find url" public let COULD_NOT_FIND_IMG_ERROR = "Couldn't find image" -public let COULD_NOT_PARSE_IMG_ERROR = "Couldn't parse image" -public let COULD_NOT_SAVE_FILE_ERROR = "Couldn't save file on disk" public let NO_EXTENSION_CONTEXT_ERROR = "No extension context attached" public let NO_DELEGATE_ERROR = "No ReactShareViewDelegate attached" public let COULD_NOT_FIND_ITEMS_ERROR = "Couldn't find items attached to this share" diff --git a/ios/ShareViewController.swift b/ios/ShareViewController.swift index 577c5059..40ce4429 100644 --- a/ios/ShareViewController.swift +++ b/ios/ShareViewController.swift @@ -21,7 +21,6 @@ enum ShareViewControllerError: Error { } class ShareViewController: SLComposeServiceViewController { - var hostAppId: String? var sharedItems: [[String: String]] = [] internal func getStringFromPlist(key: String) throws -> String { @@ -67,16 +66,6 @@ class ShareViewController: SLComposeServiceViewController { return groupFileManagerContainer } - override func viewDidLoad() { - super.viewDidLoad() - - if let hostAppId = Bundle.main.object(forInfoDictionaryKey: HOST_APP_IDENTIFIER_INFO_PLIST_KEY) as? String { - self.hostAppId = hostAppId - } else { - print("Error: \(NO_INFO_PLIST_INDENTIFIER_ERROR)") - } - } - override func isContentValid() -> Bool { // Do validation of contentText and/or NSExtensionContext attachments here return true From 7b5495434238cd056d72709245a3c62db07a5928 Mon Sep 17 00:00:00 2001 From: Chris Jansen Date: Fri, 29 Dec 2023 16:43:23 +0100 Subject: [PATCH 3/4] Update IOS_INSTRUCTIONS.md with the new group property --- IOS_INSTRUCTIONS.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/IOS_INSTRUCTIONS.md b/IOS_INSTRUCTIONS.md index f3eb2fc2..092f864e 100644 --- a/IOS_INSTRUCTIONS.md +++ b/IOS_INSTRUCTIONS.md @@ -89,6 +89,8 @@ Repeat this process for the Share Extension target, with the exact same group na Add the following to your app's `Info.plist` (if you already had other URL Schemes, make sure the one you're adding now is the FIRST one): ```OpenStep Property List +AppGroup +THE_APP_GROUP_CREATED_ABOVE CFBundleURLTypes @@ -106,6 +108,11 @@ Add the following to your app's `Info.plist` (if you already had other URL Schem Add the following to your Share Extension's `Info.plist`: ```OpenStep Property List +AppGroup +THE_APP_GROUP_CREATED_ABOVE + + + NSExtension NSExtensionAttributes