Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve app group configuration #298

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion IOS_INSTRUCTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<key>AppGroup</key>
<string>THE_APP_GROUP_CREATED_ABOVE</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
Expand All @@ -106,11 +108,16 @@ 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
<key>AppGroup</key>
<string>THE_APP_GROUP_CREATED_ABOVE</string>
<!-- DEPRECATED: HostAppBundleIdentifier is only used for backwards compatibility -->
<!-- This property only exists because the plugin previously assumed that your group name would match your bundle name.
<!-- To migrate rename 'HostAppBundleIdentifier' to 'AppGroup' and prefix it's value with 'group.'
<key>HostAppBundleIdentifier</key>
<string>YOUR_APP_TARGET_BUNDLE_ID</string>
<key>HostAppURLScheme</key>
<string>YOUR_APP_URL_SCHEME_DEFINED_ABOVE</string>
<!-- This url scheme CONTAINS :// at the end - E.G. "mycustomscheme://"-->
<!-- This value can contain a scheme with or without :// at the end. e.g.: "myscheme" or "myscheme://" both work fine -->
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
Expand Down
6 changes: 1 addition & 5 deletions ios/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -32,6 +27,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"
Expand Down
28 changes: 20 additions & 8 deletions ios/Modules/ShareMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
233 changes: 117 additions & 116 deletions ios/ShareViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +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]] = []

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)")
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)
}

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)")

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 isContentValid() -> Bool {
// Do validation of contentText and/or NSExtensionContext attachments here
return true
Expand All @@ -57,73 +88,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()
}
Expand Down Expand Up @@ -162,67 +176,54 @@ 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) {
print("Error: \(error)")
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

Expand Down