diff --git a/Makefile b/Makefile index 1d3489d3a..8f85ed563 100644 --- a/Makefile +++ b/Makefile @@ -67,7 +67,9 @@ post-install: @# Must remove the .babelrc for 0.42.0 to work correctly @# Need to copy custom ImagePickerModule.java that implements correct permission checks for android @rm node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/ImagePickerModule.java - @cp ./ImagePickerModule.java node_modules/react-native-image-picker/android/src/main/java/com/imagepicker + @cp ./native_modules/ImagePickerModule.java node_modules/react-native-image-picker/android/src/main/java/com/imagepicker + @# Need to copy custom RNDocumentPicker.m that implements direct access to the document picker in iOS + @cp ./native_modules/RNDocumentPicker.m node_modules/react-native-document-picker/ios/RNDocumentPicker/RNDocumentPicker.m @rm -f node_modules/intl/.babelrc @# Hack to get react-intl and its dependencies to work with react-native @# Based off of https://github.com/este/este/blob/master/gulp/native-fix.js diff --git a/android/app/build.gradle b/android/app/build.gradle index 3b307e42d..ae67eb3d7 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -168,6 +168,7 @@ android { } dependencies { + compile project(':react-native-document-picker') compile project(':react-native-doc-viewer') compile project(':react-native-video') compile fileTree(dir: "libs", include: ["*.jar"]) diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java index 91877eb26..c85123980 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java @@ -7,6 +7,7 @@ import android.content.Context; import android.os.Bundle; import com.facebook.react.ReactApplication; +import com.reactnativedocumentpicker.ReactNativeDocumentPicker; import com.reactlibrary.RNReactNativeDocViewerPackage; import com.brentvatne.react.ReactVideoPackage; import com.horcrux.svg.SvgPackage; @@ -72,6 +73,7 @@ public class MainApplication extends NavigationApplication implements INotificat new ReactNativeYouTube(), new ReactVideoPackage(), new RNReactNativeDocViewerPackage(), + new ReactNativeDocumentPicker(), new SharePackage() ); } diff --git a/android/app/src/main/java/com/mattermost/share/RealPathUtil.java b/android/app/src/main/java/com/mattermost/share/RealPathUtil.java index adf714a07..69478cb0b 100644 --- a/android/app/src/main/java/com/mattermost/share/RealPathUtil.java +++ b/android/app/src/main/java/com/mattermost/share/RealPathUtil.java @@ -64,7 +64,9 @@ public class RealPathUtil { return getDataColumn(context, contentUri, selection, selectionArgs); } - } else if ("content".equalsIgnoreCase(uri.getScheme())) { + } + + if ("content".equalsIgnoreCase(uri.getScheme())) { // MediaStore (and general) if (isGooglePhotosUri(uri)) { diff --git a/android/app/src/main/java/com/mattermost/share/ShareModule.java b/android/app/src/main/java/com/mattermost/share/ShareModule.java index 6500b660a..63ebe8b8d 100644 --- a/android/app/src/main/java/com/mattermost/share/ShareModule.java +++ b/android/app/src/main/java/com/mattermost/share/ShareModule.java @@ -89,6 +89,23 @@ public class ShareModule extends ReactContextBaseJavaModule { promise.resolve(processIntent()); } + @ReactMethod + public void getFilePath(String filePath, Promise promise) { + Activity currentActivity = getCurrentActivity(); + WritableMap map = Arguments.createMap(); + + if (currentActivity != null) { + Uri uri = Uri.parse(filePath); + String path = RealPathUtil.getRealPathFromURI(currentActivity, uri); + if (path != null) { + String text = "file://" + path; + map.putString("filePath", text); + } + } + + promise.resolve(map); + } + public WritableArray processIntent() { WritableMap map = Arguments.createMap(); WritableArray items = Arguments.createArray(); diff --git a/android/settings.gradle b/android/settings.gradle index 310331065..830921b9f 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,4 +1,6 @@ rootProject.name = 'Mattermost' +include ':react-native-document-picker' +project(':react-native-document-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-document-picker/android') include ':react-native-doc-viewer' project(':react-native-doc-viewer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-doc-viewer/android') include ':react-native-video' diff --git a/app/components/attachment_button.js b/app/components/attachment_button.js index 7e6fe9bfd..189f0c612 100644 --- a/app/components/attachment_button.js +++ b/app/components/attachment_button.js @@ -3,17 +3,21 @@ import PropTypes from 'prop-types'; import {intlShape} from 'react-intl'; import { Alert, + NativeModules, Platform, StyleSheet, TouchableOpacity, } from 'react-native'; import Icon from 'react-native-vector-icons/Ionicons'; +import {DocumentPicker} from 'react-native-document-picker'; import ImagePicker from 'react-native-image-picker'; import Permissions from 'react-native-permissions'; import {PermissionTypes} from 'app/constants'; import {changeOpacity} from 'app/utils/theme'; +const ShareExtension = NativeModules.MattermostShare; + export default class AttachmentButton extends PureComponent { static propTypes = { blurTextBox: PropTypes.func.isRequired, @@ -141,6 +145,32 @@ export default class AttachmentButton extends PureComponent { }); }; + attachFileFromFiles = async () => { + const hasPermission = await this.hasStoragePermission(); + + if (hasPermission) { + DocumentPicker.show({ + filetype: [Platform.OS === 'ios' ? 'public.item' : '*/*'], + }, async (error, res) => { + if (error) { + return; + } + + if (Platform.OS === 'android') { + // For android we need to retrieve the realPath in case the file being imported is from the cloud + const newUri = await ShareExtension.getFilePath(res.uri); + if (newUri.filePath) { + res.uri = newUri.filePath; + } else { + return; + } + } + + this.uploadFiles([res]); + }); + } + }; + hasPhotoPermission = async () => { if (Platform.OS === 'ios') { const {formatMessage} = this.context.intl; @@ -194,6 +224,59 @@ export default class AttachmentButton extends PureComponent { return true; }; + hasStoragePermission = async () => { + if (Platform.OS === 'android') { + const {formatMessage} = this.context.intl; + let permissionRequest; + const hasPermissionToStorage = await Permissions.check('storage'); + + switch (hasPermissionToStorage) { + case PermissionTypes.UNDETERMINED: + permissionRequest = await Permissions.request('storage'); + if (permissionRequest !== PermissionTypes.AUTHORIZED) { + return false; + } + break; + case PermissionTypes.DENIED: { + const canOpenSettings = await Permissions.canOpenSettings(); + let grantOption = null; + if (canOpenSettings) { + grantOption = { + text: formatMessage({ + id: 'mobile.android.permission_denied_retry', + defaultMessage: 'Set permission', + }), + onPress: () => Permissions.openSettings(), + }; + } + + Alert.alert( + formatMessage({ + id: 'mobile.android.storage_permission_denied_title', + defaultMessage: 'File Storage access is required', + }), + formatMessage({ + id: 'mobile.android.storage_permission_denied_description', + defaultMessage: 'To upload images from your Android device, please change your permission settings.', + }), + [ + grantOption, + { + text: formatMessage({ + id: 'mobile.android.permission_denied_dismiss', + defaultMessage: 'Dismiss', + }), + }, + ] + ); + return false; + } + } + } + + return true; + }; + uploadFiles = (images) => { this.props.uploadFiles(images); }; @@ -251,6 +334,15 @@ export default class AttachmentButton extends PureComponent { }); } + options.items.push({ + action: () => this.handleFileAttachmentOption(this.attachFileFromFiles), + text: { + id: 'mobile.file_upload.browse', + defaultMessage: 'Browse Files', + }, + icon: 'file', + }); + this.props.navigator.showModal({ screen: 'OptionsModal', title: '', diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 0a1eb3607..7c03b5dd3 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -2035,6 +2035,8 @@ "mobile.android.permission_denied_retry": "Set permission", "mobile.android.photos_permission_denied_description": "To upload images from your library, please change your permission settings.", "mobile.android.photos_permission_denied_title": "Photo library access is required", + "mobile.android.storage_permission_denied_title": "File Storage access is required", + "mobile.android.storage_permission_denied_description": "To upload images from your Android device, please change your permission settings.", "mobile.android.videos_permission_denied_description": "To upload videos from your library, please change your permission settings.", "mobile.android.videos_permission_denied_title": "Video library access is required", "mobile.announcement_banner.dismiss": "Dismiss", @@ -2135,6 +2137,7 @@ "mobile.failed_network_action.description": "There seems to be a problem with your internet connection. Make sure you have an active connection and try again.", "mobile.failed_network_action.retry": "Try Again", "mobile.failed_network_action.title": "No internet connection", + "mobile.file_upload.browse": "Browse Files", "mobile.file_upload.camera": "Take Photo or Video", "mobile.file_upload.library": "Photo Library", "mobile.file_upload.max_warning": "Uploads limited to 5 files maximum.", diff --git a/fastlane/Fastfile b/fastlane/Fastfile index fb3387545..d412befc4 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -94,6 +94,13 @@ platform :ios do ) end + unless ENV['IOS_ICLOUD_CONTAINER'].nil? || ENV['IOS_ICLOUD_CONTAINER'].empty? || ENV['IOS_ICLOUD_CONTAINER'] == 'iCloud.com.mattermost.rnbeta' + update_icloud_container_identifiers( + entitlements_file: './ios/Mattermost/Mattermost.entitlements', + icloud_container_identifiers: [ENV['IOS_ICLOUD_CONTAINER']] + ) + end + if ENV['IOS_APP_NAME'] != 'Mattermost Beta' update_info_plist( xcodeproj: './ios/Mattermost.xcodeproj', diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index af76ca003..cb9fcd0d2 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -65,6 +65,7 @@ 7F50C96B203C6E80007CA374 /* SessionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F50C969203C6E80007CA374 /* SessionManager.m */; }; 7F642DF02093533300F3165E /* libRNDeviceInfo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F642DED2093530B00F3165E /* libRNDeviceInfo.a */; }; 7F642DF1209353A400F3165E /* libRNDeviceInfo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F642DED2093530B00F3165E /* libRNDeviceInfo.a */; }; + 7F5CA9A0208FE3B9004F91CE /* libRNDocumentPicker.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F5CA991208FE38F004F91CE /* libRNDocumentPicker.a */; }; 7F6877B31E7836070094B63F /* libToolTipMenu.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F6877B01E7835E50094B63F /* libToolTipMenu.a */; }; 7F6C47A51FE87E8C00F5A912 /* PerformRequests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F6C47A41FE87E8C00F5A912 /* PerformRequests.m */; }; 7F7D7F98201645E100D31155 /* libReactNativePermissions.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F7D7F87201645D300D31155 /* libReactNativePermissions.a */; }; @@ -422,6 +423,13 @@ remoteGlobalIDString = 3DBE0D0D1F3B181C0099AA32; remoteInfo = "fishhook-tvOS"; }; + 7F5CA990208FE38F004F91CE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7F5CA956208FE38F004F91CE /* RNDocumentPicker.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = E01DD9DB1D2311A600C39062; + remoteInfo = RNDocumentPicker; + }; 7F63D2801E6C957C001FAE12 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 7F63D27B1E6C957C001FAE12 /* RCTPushNotification.xcodeproj */; @@ -697,6 +705,7 @@ 7F43D6051F6BF9EB001FC614 /* libPods-Mattermost.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPods-Mattermost.a"; path = "../../../../../../../Library/Developer/Xcode/DerivedData/Mattermost-czlinsdviifujheezzjvmisotjrm/Build/Products/Debug-iphonesimulator/libPods-Mattermost.a"; sourceTree = ""; }; 7F50C968203C6E80007CA374 /* SessionManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionManager.h; sourceTree = ""; }; 7F50C969203C6E80007CA374 /* SessionManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SessionManager.m; sourceTree = ""; }; + 7F5CA956208FE38F004F91CE /* RNDocumentPicker.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNDocumentPicker.xcodeproj; path = "../node_modules/react-native-document-picker/ios/RNDocumentPicker.xcodeproj"; sourceTree = ""; }; 7F63D27B1E6C957C001FAE12 /* RCTPushNotification.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTPushNotification.xcodeproj; path = "../node_modules/react-native/Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj"; sourceTree = ""; }; 7F63D2C21E6DD98A001FAE12 /* Mattermost.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Mattermost.entitlements; path = Mattermost/Mattermost.entitlements; sourceTree = ""; }; 7F642DE72093530B00F3165E /* RNDeviceInfo.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNDeviceInfo.xcodeproj; path = "../node_modules/react-native-device-info/ios/RNDeviceInfo.xcodeproj"; sourceTree = ""; }; @@ -802,6 +811,7 @@ 7F43D5DD1F6BF95F001FC614 /* libRNCookieManagerIOS.a in Frameworks */, 7F43D6401F6BFA82001FC614 /* libRCTPushNotification.a in Frameworks */, 7FDB92B11F706F58006CDFD1 /* libRNImagePicker.a in Frameworks */, + 7F5CA9A0208FE3B9004F91CE /* libRNDocumentPicker.a in Frameworks */, 7F43D5DE1F6BF96A001FC614 /* libRCTYouTube.a in Frameworks */, 7F43D6061F6BF9EB001FC614 /* libPods-Mattermost.a in Frameworks */, A9B746D2CFA9CEBFB8AE2B5B /* libPods-Mattermost.a in Frameworks */, @@ -1131,6 +1141,14 @@ name = Products; sourceTree = ""; }; + 7F5CA957208FE38F004F91CE /* Products */ = { + isa = PBXGroup; + children = ( + 7F5CA991208FE38F004F91CE /* libRNDocumentPicker.a */, + ); + name = Products; + sourceTree = ""; + }; 7F63D27C1E6C957C001FAE12 /* Products */ = { isa = PBXGroup; children = ( @@ -1299,6 +1317,7 @@ 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( + 7F5CA956208FE38F004F91CE /* RNDocumentPicker.xcodeproj */, 7F642DE72093530B00F3165E /* RNDeviceInfo.xcodeproj */, 7FD8DE972029ECDE001AAC5E /* RNTableView.xcodeproj */, 7F7D7F52201645D300D31155 /* ReactNativePermissions.xcodeproj */, @@ -1470,6 +1489,9 @@ com.apple.Push = { enabled = 1; }; + com.apple.iCloud = { + enabled = 1; + }; }; }; 7FFE32991FD9CB640038C7A0 = { @@ -1597,6 +1619,10 @@ ProductGroup = 7F642DE82093530B00F3165E /* Products */; ProjectRef = 7F642DE72093530B00F3165E /* RNDeviceInfo.xcodeproj */; }, + { + ProductGroup = 7F5CA957208FE38F004F91CE /* Products */; + ProjectRef = 7F5CA956208FE38F004F91CE /* RNDocumentPicker.xcodeproj */; + }, { ProductGroup = 3752181C1F4B9E320035444B /* Products */; ProjectRef = 546CB0A5BB8F453890CBA559 /* RNFetchBlob.xcodeproj */; @@ -1957,6 +1983,13 @@ remoteRef = 7F4C1F851FBE572B0029D1DF /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 7F5CA991208FE38F004F91CE /* libRNDocumentPicker.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRNDocumentPicker.a; + remoteRef = 7F5CA990208FE38F004F91CE /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 7F63D2811E6C957C001FAE12 /* libRCTPushNotification.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; @@ -2491,6 +2524,7 @@ "$(SRCROOT)/../node_modules/react-native-video/ios", "$(SRCROOT)/../node_modules/react-native-doc-viewer/ios/**", "$(SRCROOT)/../node_modules/react-native-safe-area/ios/RNSafeArea", + "$(SRCROOT)/../node_modules/react-native-document-picker/**", "$(SRCROOT)/../node_modules/react-native-device-info/ios/**", ); INFOPLIST_FILE = Mattermost/Info.plist; @@ -2541,6 +2575,7 @@ "$(SRCROOT)/../node_modules/react-native-video/ios", "$(SRCROOT)/../node_modules/react-native-doc-viewer/ios/**", "$(SRCROOT)/../node_modules/react-native-safe-area/ios/RNSafeArea", + "$(SRCROOT)/../node_modules/react-native-document-picker/**", "$(SRCROOT)/../node_modules/react-native-device-info/ios/**", ); INFOPLIST_FILE = Mattermost/Info.plist; diff --git a/ios/Mattermost/Mattermost.entitlements b/ios/Mattermost/Mattermost.entitlements index 4c980b9c5..c72a97053 100644 --- a/ios/Mattermost/Mattermost.entitlements +++ b/ios/Mattermost/Mattermost.entitlements @@ -4,6 +4,20 @@ aps-environment development + com.apple.developer.icloud-container-identifiers + + iCloud.$(CFBundleIdentifier) + + com.apple.developer.icloud-services + + CloudDocuments + + com.apple.developer.ubiquity-container-identifiers + + iCloud.$(CFBundleIdentifier) + + com.apple.developer.ubiquity-kvstore-identifier + $(TeamIdentifierPrefix)$(CFBundleIdentifier) com.apple.security.application-groups group.com.mattermost.rnbeta diff --git a/ImagePickerModule.java b/native_modules/ImagePickerModule.java similarity index 100% rename from ImagePickerModule.java rename to native_modules/ImagePickerModule.java diff --git a/native_modules/RNDocumentPicker.m b/native_modules/RNDocumentPicker.m new file mode 100644 index 000000000..639066d2d --- /dev/null +++ b/native_modules/RNDocumentPicker.m @@ -0,0 +1,100 @@ +#import "RNDocumentPicker.h" + +#if __has_include() +#import +#import +#else // back compatibility for RN version < 0.40 +#import "RCTConvert.h" +#import "RCTBridge.h" +#endif + +#define IDIOM UI_USER_INTERFACE_IDIOM() +#define IPAD UIUserInterfaceIdiomPad + +@interface RNDocumentPicker () +@end + + +@implementation RNDocumentPicker { + NSMutableArray *composeViews; + NSMutableArray *composeCallbacks; +} + +@synthesize bridge = _bridge; + +- (instancetype)init +{ + if ((self = [super init])) { + composeCallbacks = [[NSMutableArray alloc] init]; + composeViews = [[NSMutableArray alloc] init]; + } + return self; +} + +- (dispatch_queue_t)methodQueue +{ + return dispatch_get_main_queue(); +} + +RCT_EXPORT_MODULE() + +RCT_EXPORT_METHOD(show:(NSDictionary *)options + callback:(RCTResponseSenderBlock)callback) { + + NSArray *allowedUTIs = [RCTConvert NSArray:options[@"filetype"]]; + UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:(NSArray *)allowedUTIs inMode:UIDocumentPickerModeImport]; + + [composeCallbacks addObject:callback]; + + + documentPicker.delegate = self; + documentPicker.modalPresentationStyle = UIModalPresentationFormSheet; + + UIViewController *rootViewController = [[[[UIApplication sharedApplication]delegate] window] rootViewController]; + while (rootViewController.modalViewController) { + rootViewController = rootViewController.modalViewController; + } + + if ( IDIOM == IPAD ) { + NSNumber *top = [RCTConvert NSNumber:options[@"top"]]; + NSNumber *left = [RCTConvert NSNumber:options[@"left"]]; + [documentPicker.popoverPresentationController setSourceRect: CGRectMake([left floatValue], [top floatValue], 0, 0)]; + [documentPicker.popoverPresentationController setSourceView: rootViewController.view]; + } + + [rootViewController presentViewController:documentPicker animated:YES completion:nil]; +} + + +- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url { + if (controller.documentPickerMode == UIDocumentPickerModeImport) { + RCTResponseSenderBlock callback = [composeCallbacks lastObject]; + [composeCallbacks removeLastObject]; + + [url startAccessingSecurityScopedResource]; + + NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] init]; + __block NSError *error; + + [coordinator coordinateReadingItemAtURL:url options:NSFileCoordinatorReadingResolvesSymbolicLink error:&error byAccessor:^(NSURL *newURL) { + NSMutableDictionary* result = [NSMutableDictionary dictionary]; + + [result setValue:newURL.absoluteString forKey:@"uri"]; + [result setValue:[newURL lastPathComponent] forKey:@"fileName"]; + + NSError *attributesError = nil; + NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:newURL.path error:&attributesError]; + if(!attributesError) { + [result setValue:[fileAttributes objectForKey:NSFileSize] forKey:@"fileSize"]; + } else { + NSLog(@"%@", attributesError); + } + + callback(@[[NSNull null], result]); + }]; + + [url stopAccessingSecurityScopedResource]; + } +} + +@end diff --git a/package-lock.json b/package-lock.json index 7186448b7..521f23849 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14426,6 +14426,11 @@ "resolved": "https://registry.npmjs.org/react-native-doc-viewer/-/react-native-doc-viewer-2.7.8.tgz", "integrity": "sha1-Q9BlMo+xt+yQ0yHlvSNHEegHyPY=" }, + "react-native-document-picker": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/react-native-document-picker/-/react-native-document-picker-2.1.0.tgz", + "integrity": "sha512-BFCBXwz8xuLvHLVFVeQM+RhaY8yZ38PEWt9WSbq5VIoZ/VssP6uu51XxOfdwaMALOrAHIojK0SiYnd155upZAg==" + }, "react-native-drawer": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/react-native-drawer/-/react-native-drawer-2.5.0.tgz", diff --git a/package.json b/package.json index 9c9539e0b..3eed9e9a3 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "react-native-cookies": "3.2.0", "react-native-device-info": "0.21.5", "react-native-doc-viewer": "2.7.8", + "react-native-document-picker": "2.1.0", "react-native-drawer": "2.5.0", "react-native-exception-handler": "2.7.5", "react-native-fast-image": "4.0.8",