diff --git a/ios/ReactNativeShareExtension.m b/ios/ReactNativeShareExtension.m index 10703791..65e81aff 100644 --- a/ios/ReactNativeShareExtension.m +++ b/ios/ReactNativeShareExtension.m @@ -1,9 +1,6 @@ #import "ReactNativeShareExtension.h" #import "React/RCTRootView.h" -#define ITEM_IDENTIFIER @"public.url" -#define IMAGE_IDENTIFIER @"public.image" - NSExtensionContext* extensionContext; @implementation ReactNativeShareExtension { @@ -34,74 +31,236 @@ - (void)viewDidLoad { } -RCT_EXPORT_METHOD(close) { - [extensionContext completeRequestReturningItems:nil - completionHandler:nil]; +RCT_EXPORT_METHOD(close:(NSString *)appGroupId) { + [self cleanUpTempFiles:appGroupId]; + [extensionContext completeRequestReturningItems:nil + completionHandler:nil]; } - - RCT_REMAP_METHOD(data, - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) + appGroupId: (NSString *)appGroupId + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { - [self extractDataFromContext: extensionContext withCallback:^(NSURL* url,NSString* contentType ,NSException* err) { - NSDictionary *inventory = @{ - @"type": contentType, - @"value": [url absoluteString] - }; - - resolve(inventory); - }]; + [self extractDataFromContext: extensionContext withAppGroup:appGroupId andCallback:^(NSArray* items ,NSError* err) { + if (items == nil) { + resolve(nil); + return; + } + resolve(items[0]); + }]; } -- (void)extractDataFromContext:(NSExtensionContext *)context withCallback:(void(^)(NSURL *url, NSString* contentType ,NSException *exception))callback { - @try { - NSExtensionItem *item = [context.inputItems firstObject]; - NSArray *attachments = item.attachments; - __block NSItemProvider *urlProvider = nil; - __block NSItemProvider *imageProvider = nil; - [attachments enumerateObjectsUsingBlock:^(NSItemProvider *provider, NSUInteger idx, BOOL *stop) { - if([provider hasItemConformingToTypeIdentifier:ITEM_IDENTIFIER]) { - urlProvider = provider; - *stop = YES; - }else if ([provider hasItemConformingToTypeIdentifier:IMAGE_IDENTIFIER]){ - imageProvider = provider; - *stop = YES; - - } - }]; - - if(urlProvider) { - [urlProvider loadItemForTypeIdentifier:ITEM_IDENTIFIER options:nil completionHandler:^(id item, NSError *error) { - NSURL *url = (NSURL *)item; - - if(callback) { - callback(url,@"text/plain" ,nil); - } - }]; - }else if (imageProvider){ - [imageProvider loadItemForTypeIdentifier:IMAGE_IDENTIFIER options:nil completionHandler:^(id item, NSError *error) { - NSURL *url = (NSURL *)item; - - if(callback) { - callback(url,[[[url absoluteString] pathExtension] lowercaseString] ,nil); - } - }]; - - - } - else { - if(callback) { - callback(nil, nil,[NSException exceptionWithName:@"Error" reason:@"couldn't find provider" userInfo:nil]); - } - } - } - @catch (NSException *exception) { - if(callback) { - callback(nil,nil ,exception); - } - } +RCT_REMAP_METHOD(dataMulti, + appGroupId: (NSString *)appGroupId + resolverMulti:(RCTPromiseResolveBlock)resolve + rejecterMulti:(RCTPromiseRejectBlock)reject) +{ + [self extractDataFromContext: extensionContext withAppGroup: appGroupId andCallback:^(NSArray* items ,NSError* err) { + if (err) { + reject(@"dataMulti", @"Failed to extract attachment content", err); + return; + } + resolve(items); + }]; +} + +typedef void (^ProviderCallback)(NSString *content, NSString *contentType, BOOL owner, NSError *err); + +- (void)extractDataFromContext:(NSExtensionContext *)context withAppGroup:(NSString *) appGroupId andCallback:(void(^)(NSArray *items ,NSError *err))callback { + @try { + NSExtensionItem *item = [context.inputItems firstObject]; + NSArray *attachments = item.attachments; + NSMutableArray *items = [[NSMutableArray alloc] init]; + + __block int attachmentIdx = 0; + __block ProviderCallback providerCb = nil; + __block __weak ProviderCallback weakProviderCb = nil; + providerCb = ^ void (NSString *content, NSString *contentType, BOOL owner, NSError *err) { + if (err) { + callback(nil, err); + return; + } + + if (content != nil) { + [items addObject:@{ + @"type": contentType, + @"value": content, + @"owner": [NSNumber numberWithBool:owner], + }]; + } + + ++attachmentIdx; + if (attachmentIdx == [attachments count]) { + callback(items, nil); + } else { + [self extractDataFromProvider:attachments[attachmentIdx] withAppGroup:appGroupId andCallback: weakProviderCb]; + } + }; + weakProviderCb = providerCb; + [self extractDataFromProvider:attachments[0] withAppGroup:appGroupId andCallback: providerCb]; + } + @catch (NSException *exc) { + NSError *error = [NSError errorWithDomain:@"fiftythree.paste" code:1 userInfo:@{ + @"reason": [exc description] + }]; + callback(nil, error); + } +} + +- (void)extractDataFromProvider:(NSItemProvider *)provider withAppGroup:(NSString *) appGroupId andCallback:(void(^)(NSString* content, NSString* contentType, BOOL owner, NSError *err))callback { + + if([provider hasItemConformingToTypeIdentifier:@"public.image"]) { + [provider loadItemForTypeIdentifier:@"public.image" options:nil completionHandler:^(id item, NSError *error) { + if (error) { + callback(nil, nil, NO, error); + return; + } + + @try { + if ([item isKindOfClass: NSURL.class]) { + NSURL *url = (NSURL *)item; + return callback([url absoluteString], @"public.image", NO, nil); + } else if ([item isKindOfClass: UIImage.class]) { + UIImage *image = (UIImage *)item; + NSString *fileName = [NSString stringWithFormat:@"%@.jpg", [[NSUUID UUID] UUIDString]]; + NSURL *tempContainerURL = [ReactNativeShareExtension tempContainerURL:appGroupId]; + if (tempContainerURL == nil){ + return callback(nil, nil, NO, nil); + } + + NSURL *tempFileURL = [tempContainerURL URLByAppendingPathComponent: fileName]; + BOOL created = [UIImageJPEGRepresentation(image, 0.95) writeToFile:[tempFileURL path] atomically:YES]; + if (created) { + return callback([tempFileURL absoluteString], @"public.image", YES, nil); + } else { + return callback(nil, nil, NO, nil); + } + } else if ([item isKindOfClass: NSData.class]) { + NSString *fileName = [NSString stringWithFormat:@"%@.jpg", [[NSUUID UUID] UUIDString]]; + NSData *data = (NSData *)item; + UIImage *image = [UIImage imageWithData:data]; + NSURL *tempContainerURL = [ReactNativeShareExtension tempContainerURL:appGroupId]; + if (tempContainerURL == nil){ + return callback(nil, nil, NO, nil); + } + NSURL *tempFileURL = [tempContainerURL URLByAppendingPathComponent: fileName]; + BOOL created = [UIImageJPEGRepresentation(image, 0.95) writeToFile:[tempFileURL path] atomically:YES]; + if (created) { + return callback([tempFileURL absoluteString], @"public.image", YES, nil); + } else { + return callback(nil, nil, NO, nil); + } + } else { + // Do nothing, some type we don't support. + return callback(nil, nil, NO, nil); + } + } + @catch(NSException *exc) { + NSError *error = [NSError errorWithDomain:@"fiftythree.paste" code:2 userInfo:@{ + @"reason": [exc description] + }]; + callback(nil, nil, NO, error); + } + }]; + return; + } + + if([provider hasItemConformingToTypeIdentifier:@"public.file-url"]) { + [provider loadItemForTypeIdentifier:@"public.file-url" options:nil completionHandler:^(id item, NSError *error) { + if (error) { + callback(nil, nil, NO, error); + return; + } + + if ([item isKindOfClass:NSURL.class]) { + return callback([(NSURL *)item absoluteString], @"public.file-url", NO, nil); + } else if ([item isKindOfClass:NSString.class]) { + return callback((NSString *)item, @"public.file-url", NO, nil); + } + callback(nil, nil, NO, nil); + }]; + return; + } + + if([provider hasItemConformingToTypeIdentifier:@"public.url"]) { + [provider loadItemForTypeIdentifier:@"public.url" options:nil completionHandler:^(id item, NSError *error) { + if (error) { + callback(nil, nil, NO, error); + return; + } + + if ([item isKindOfClass:NSURL.class]) { + return callback([(NSURL *)item absoluteString], @"public.url", NO, nil); + } else if ([item isKindOfClass:NSString.class]) { + return callback((NSString *)item, @"public.url", NO, nil); + } + }]; + return; + } + + if([provider hasItemConformingToTypeIdentifier:@"public.plain-text"]) { + [provider loadItemForTypeIdentifier:@"public.plain-text" options:nil completionHandler:^(id item, NSError *error) { + if (error) { + callback(nil, nil, NO, error); + return; + } + + if ([item isKindOfClass:NSString.class]) { + return callback((NSString *)item, @"public.plain-text", NO, nil); + } else if ([item isKindOfClass:NSAttributedString.class]) { + NSAttributedString *str = (NSAttributedString *)item; + return callback([str string], @"public.plain-text", NO, nil); + } else if ([item isKindOfClass:NSData.class]) { + NSString *str = [[NSString alloc] initWithData:(NSData *)item encoding:NSUTF8StringEncoding]; + if (str) { + return callback(str, @"public.plain-text", NO, nil); + } else { + return callback(nil, nil, NO, nil); + } + } else { + return callback(nil, nil, NO, nil); + } + }]; + return; + } + + callback(nil, nil, NO, nil); +} + ++ (NSURL*) tempContainerURL: (NSString*)appGroupId { + NSFileManager *manager = [NSFileManager defaultManager]; + NSURL *containerURL = [manager containerURLForSecurityApplicationGroupIdentifier: appGroupId]; + NSURL *tempDirectoryURL = [containerURL URLByAppendingPathComponent:@"shareTempItems"]; + if (![manager fileExistsAtPath:[tempDirectoryURL path]]) { + NSError *err; + [manager createDirectoryAtURL:tempDirectoryURL withIntermediateDirectories:YES attributes:nil error:&err]; + if (err) { + return nil; + } + } + + return tempDirectoryURL; +} + +- (void) cleanUpTempFiles:(NSString *)appGroupId { + NSURL *tmpDirectoryURL = [ReactNativeShareExtension tempContainerURL:appGroupId]; + if (tmpDirectoryURL == nil) { + return; + } + + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSError *error; + NSArray *tmpFiles = [fileManager contentsOfDirectoryAtPath:[tmpDirectoryURL path] error:&error]; + if (error) { + return; + } + + for (NSString *file in tmpFiles) + { + error = nil; + [fileManager removeItemAtPath:[[tmpDirectoryURL URLByAppendingPathComponent:file] path] error:&error]; + } } @end diff --git a/lib/index.js b/lib/index.js index aa17dce1..05a71965 100644 --- a/lib/index.js +++ b/lib/index.js @@ -4,6 +4,7 @@ import { NativeModules } from 'react-native' // const { type, value } = await NativeModules.ShareExtension.data() // NativeModules.ShareExtension.close() export default { - data: () => NativeModules.ReactNativeShareExtension.data(), - close: () => NativeModules.ReactNativeShareExtension.close() + data: (appGroupId) => NativeModules.ReactNativeShareExtension.data(appGroupId), + dataMulti: (appGroupId) => NativeModules.ReactNativeShareExtension.dataMulti(appGroupId), + close: (appGroupId) => NativeModules.ReactNativeShareExtension.close(appGroupId) } diff --git a/package.json b/package.json index 6e7c17af..2d81e456 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,92 @@ { - "name": "react-native-share-extension", - "version": "1.1.1", - "description": "share extension using react-native for both ios and android", - "main": "lib/index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "_args": [ + [ + { + "raw": "react-native-share-extension", + "scope": null, + "escapedName": "react-native-share-extension", + "name": "react-native-share-extension", + "rawSpec": "", + "spec": "latest", + "type": "tag" + }, + "/Users/mark/code/PresentMobile" + ] + ], + "_from": "react-native-share-extension@latest", + "_id": "react-native-share-extension@1.1.1", + "_inCache": true, + "_installable": true, + "_location": "/react-native-share-extension", + "_nodeVersion": "6.9.1", + "_npmOperationalInternal": { + "host": "packages-12-west.internal.npmjs.com", + "tmp": "tmp/react-native-share-extension-1.1.1.tgz_1483985855792_0.05685301055200398" }, - "repository": { - "type": "git", - "url": "git@github.com:alinz/react-native-share-extension.git" + "_npmUser": { + "name": "alinz", + "email": "a.najafizadeh@gmail.com" }, - "keywords": [ - "react-component", - "react-native", - "share-extension" + "_npmVersion": "3.10.8", + "_phantomChildren": {}, + "_requested": { + "raw": "react-native-share-extension", + "scope": null, + "escapedName": "react-native-share-extension", + "name": "react-native-share-extension", + "rawSpec": "", + "spec": "latest", + "type": "tag" + }, + "_requiredBy": [ + "#USER", + "/" ], + "_resolved": "https://registry.npmjs.org/react-native-share-extension/-/react-native-share-extension-1.1.1.tgz", + "_shasum": "da2c4758c8a800c2241e797deb90298b5388053c", + "_shrinkwrap": null, + "_spec": "react-native-share-extension", + "_where": "/Users/mark/code/PresentMobile", "author": { "name": "Ali Najafizadeh", "email": "a.najafizadeh@gmail.com", "url": "http://github.com/alinz" }, - "license": "MIT", "bugs": { "url": "https://github.com/alinz/react-native-share-extension" }, - "homepage": "https://github.com/alinz/react-native-share-extension" + "dependencies": {}, + "description": "share extension using react-native for both ios and android", + "devDependencies": {}, + "directories": {}, + "dist": { + "shasum": "da2c4758c8a800c2241e797deb90298b5388053c", + "tarball": "https://registry.npmjs.org/react-native-share-extension/-/react-native-share-extension-1.1.1.tgz" + }, + "gitHead": "66d3a720cda1a032aaea2d4609a7f427c252a2b2", + "homepage": "https://github.com/alinz/react-native-share-extension", + "keywords": [ + "react-component", + "react-native", + "share-extension" + ], + "license": "MIT", + "main": "lib/index.js", + "maintainers": [ + { + "name": "alinz", + "email": "a.najafizadeh@gmail.com" + } + ], + "name": "react-native-share-extension", + "optionalDependencies": {}, + "readme": "ERROR: No README data found!", + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/alinz/react-native-share-extension.git" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "version": "1.1.1" }