expo-document-picker 11.2.2 → 11.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,172 @@
1
+ import ExpoModulesCore
2
+ import UIKit
3
+ import MobileCoreServices
4
+
5
+ struct PickingContext {
6
+ let promise: Promise
7
+ let options: DocumentPickerOptions
8
+ let delegate: DocumentPickingDelegate
9
+ }
10
+
11
+ public class DocumentPickerModule: Module, PickingResultHandler {
12
+ private var pickingContext: PickingContext?
13
+
14
+ public func definition() -> ModuleDefinition {
15
+ Name("ExpoDocumentPicker")
16
+
17
+ AsyncFunction("getDocumentAsync") { (options: DocumentPickerOptions, promise: Promise) in
18
+ if pickingContext != nil {
19
+ throw PickingInProgressException()
20
+ }
21
+
22
+ guard let currentVc = appContext?.utilities?.currentViewController() else {
23
+ throw MissingViewControllerException()
24
+ }
25
+
26
+ let documentPickerVC = createDocumentPicker(with: options)
27
+ let pickerDelegate = DocumentPickingDelegate(resultHandler: self)
28
+
29
+ pickingContext = PickingContext(promise: promise, options: options, delegate: pickerDelegate)
30
+
31
+ documentPickerVC.delegate = pickerDelegate
32
+ documentPickerVC.presentationController?.delegate = pickerDelegate
33
+ documentPickerVC.allowsMultipleSelection = options.multiple
34
+
35
+ if UIDevice.current.userInterfaceIdiom == .pad {
36
+ let viewFrame = currentVc.view.frame
37
+ documentPickerVC.popoverPresentationController?.sourceRect = CGRect(
38
+ x: viewFrame.midX,
39
+ y: viewFrame.maxY,
40
+ width: 0,
41
+ height: 0
42
+ )
43
+ documentPickerVC.popoverPresentationController?.sourceView = currentVc.view
44
+ documentPickerVC.modalPresentationStyle = .pageSheet
45
+ }
46
+ currentVc.present(documentPickerVC, animated: true)
47
+ }.runOnQueue(.main)
48
+ }
49
+
50
+ func didPickDocumentsAt(urls: [URL]) {
51
+ guard let options = self.pickingContext?.options,
52
+ let promise = self.pickingContext?.promise else {
53
+ log.error("Picking context has been lost.")
54
+ return
55
+ }
56
+ pickingContext = nil
57
+
58
+ do {
59
+ if options.multiple {
60
+ let assets = try urls.map {
61
+ try readDocumentDetails(
62
+ documentUrl: $0,
63
+ copy: options.copyToCacheDirectory
64
+ )
65
+ }
66
+ promise.resolve(DocumentPickerResponse(assets: assets))
67
+ } else {
68
+ let asset = try readDocumentDetails(
69
+ documentUrl: urls[0],
70
+ copy: options.copyToCacheDirectory
71
+ )
72
+ promise.resolve(DocumentPickerResponse(assets: [asset]))
73
+ }
74
+ } catch {
75
+ promise.reject(error)
76
+ }
77
+ }
78
+
79
+ func didCancelPicking() {
80
+ guard let context = pickingContext else {
81
+ log.error("Picking context lost")
82
+ return
83
+ }
84
+
85
+ pickingContext = nil
86
+ context.promise.resolve(DocumentPickerResponse(canceled: true))
87
+ }
88
+
89
+ private func getFileSize(path: URL) -> Int? {
90
+ guard let resource = try? path.resourceValues(forKeys: [.fileSizeKey, .isDirectoryKey]) else {
91
+ return 0
92
+ }
93
+
94
+ if let isDirectory = resource.isDirectory {
95
+ if !isDirectory {
96
+ return resource.fileSize
97
+ }
98
+ }
99
+
100
+ guard let contents = try? FileManager.default.contentsOfDirectory(atPath: path.absoluteString) else {
101
+ return 0
102
+ }
103
+
104
+ let folderSize = contents.reduce(0) { currentSize, file in
105
+ let fileSize = getFileSize(path: path.appendingPathComponent(file)) ?? 0
106
+ return currentSize + fileSize
107
+ }
108
+
109
+ return folderSize
110
+ }
111
+
112
+ private func readDocumentDetails(documentUrl: URL, copy: Bool) throws -> DocumentInfo {
113
+ let pathExtension = documentUrl.pathExtension
114
+ var newUrl = documentUrl
115
+
116
+ guard let fileSystem = self.appContext?.fileSystem else {
117
+ throw Exceptions.FileSystemModuleNotFound()
118
+ }
119
+
120
+ guard let fileSize = try? getFileSize(path: documentUrl) else {
121
+ throw InvalidFileException()
122
+ }
123
+
124
+ if copy {
125
+ let directory = fileSystem.cachesDirectory.appending("DocumentPicker")
126
+ let path = fileSystem.generatePath(inDirectory: directory, withExtension: pathExtension)
127
+ newUrl = URL(fileURLWithPath: path)
128
+ try FileManager.default.copyItem(at: documentUrl, to: newUrl)
129
+ }
130
+
131
+ let mimeType = self.getMimeType(from: pathExtension)
132
+
133
+ return DocumentInfo(
134
+ uri: newUrl.absoluteString,
135
+ name: documentUrl.lastPathComponent,
136
+ size: fileSize,
137
+ mimeType: mimeType
138
+ )
139
+ }
140
+
141
+ private func getMimeType(from pathExtension: String) -> String? {
142
+ if #available(iOS 14, *) {
143
+ return UTType(filenameExtension: pathExtension)?.preferredMIMEType
144
+ } else {
145
+ if let uti = UTTypeCreatePreferredIdentifierForTag(
146
+ kUTTagClassFilenameExtension,
147
+ pathExtension as NSString, nil
148
+ )?.takeRetainedValue() {
149
+ if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() {
150
+ return mimetype as String
151
+ }
152
+ }
153
+ return nil
154
+ }
155
+ }
156
+
157
+ private func createDocumentPicker(with options: DocumentPickerOptions) -> UIDocumentPickerViewController {
158
+ if #available(iOS 14.0, *) {
159
+ let utTypes = options.type.compactMap { $0.toUTType() }
160
+ return UIDocumentPickerViewController(
161
+ forOpeningContentTypes: utTypes,
162
+ asCopy: true
163
+ )
164
+ } else {
165
+ let utiTypes = options.type.map { $0.toUTI() }
166
+ return UIDocumentPickerViewController(
167
+ documentTypes: utiTypes,
168
+ in: .import
169
+ )
170
+ }
171
+ }
172
+ }
@@ -0,0 +1,68 @@
1
+ import ExpoModulesCore
2
+ import MobileCoreServices
3
+
4
+ struct DocumentPickerOptions: Record {
5
+ @Field
6
+ var copyToCacheDirectory: Bool
7
+
8
+ @Field
9
+ var type: [MimeType]
10
+
11
+ @Field
12
+ var multiple: Bool
13
+ }
14
+
15
+ internal enum MimeType: String, Enumerable {
16
+ case item = "*/*"
17
+ case image = "image/*"
18
+ case video = "video/*"
19
+ case audio = "audio/*"
20
+ case text = "text/*"
21
+
22
+ @available(iOS 14.0, *)
23
+ func toUTType() -> UTType? {
24
+ switch self {
25
+ case .item:
26
+ return UTType.item
27
+ case .image:
28
+ return UTType.image
29
+ case .video:
30
+ return UTType.video
31
+ case .audio:
32
+ return UTType.audio
33
+ case .text:
34
+ return UTType.text
35
+ default:
36
+ return UTType(mimeType: self.rawValue)
37
+ }
38
+ }
39
+
40
+ func toUTI() -> String {
41
+ var uti: CFString
42
+
43
+ switch self {
44
+ case .item:
45
+ uti = kUTTypeItem
46
+ case .image:
47
+ uti = kUTTypeImage
48
+ case .video:
49
+ uti = kUTTypeVideo
50
+ case .audio:
51
+ uti = kUTTypeAudio
52
+ case .text:
53
+ uti = kUTTypeText
54
+ default:
55
+ if let ref = UTTypeCreatePreferredIdentifierForTag(
56
+ kUTTagClassMIMEType,
57
+ self.rawValue as CFString,
58
+ nil
59
+ )?.takeRetainedValue() {
60
+ uti = ref
61
+ } else {
62
+ uti = kUTTypeItem
63
+ }
64
+ }
65
+
66
+ return uti as String
67
+ }
68
+ }
@@ -0,0 +1,13 @@
1
+ import ExpoModulesCore
2
+
3
+ internal struct DocumentPickerResponse: Record {
4
+ @Field var assets: [DocumentInfo]? = nil
5
+ @Field var canceled: Bool = false
6
+ }
7
+
8
+ internal struct DocumentInfo: Record {
9
+ @Field var uri: String = ""
10
+ @Field var name: String? = nil
11
+ @Field var size: Int = 0
12
+ @Field var mimeType: String? = nil
13
+ }
@@ -0,0 +1,26 @@
1
+ import UIKit
2
+
3
+ protocol PickingResultHandler {
4
+ func didPickDocumentsAt(urls: [URL])
5
+ func didCancelPicking()
6
+ }
7
+
8
+ internal class DocumentPickingDelegate: NSObject, UIDocumentPickerDelegate, UIAdaptivePresentationControllerDelegate {
9
+ private let resultHandler: PickingResultHandler
10
+
11
+ init(resultHandler: PickingResultHandler) {
12
+ self.resultHandler = resultHandler
13
+ }
14
+
15
+ func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
16
+ self.resultHandler.didPickDocumentsAt(urls: urls)
17
+ }
18
+
19
+ func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
20
+ self.resultHandler.didCancelPicking()
21
+ }
22
+
23
+ func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
24
+ self.resultHandler.didCancelPicking()
25
+ }
26
+ }
@@ -0,0 +1,19 @@
1
+ import ExpoModulesCore
2
+
3
+ internal class InvalidFileException: Exception {
4
+ override var reason: String {
5
+ "Unable to get file size"
6
+ }
7
+ }
8
+
9
+ internal class PickingInProgressException: Exception {
10
+ override var reason: String {
11
+ "Different document picking in progress. Await other document picking first"
12
+ }
13
+ }
14
+
15
+ internal class MissingViewControllerException: Exception {
16
+ override var reason: String {
17
+ "Could not find the current ViewController"
18
+ }
19
+ }
@@ -3,7 +3,7 @@ require 'json'
3
3
  package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
4
 
5
5
  Pod::Spec.new do |s|
6
- s.name = 'EXDocumentPicker'
6
+ s.name = 'ExpoDocumentPicker'
7
7
  s.version = package['version']
8
8
  s.summary = package['description']
9
9
  s.description = package['description']
@@ -11,15 +11,22 @@ Pod::Spec.new do |s|
11
11
  s.author = package['author']
12
12
  s.homepage = package['homepage']
13
13
  s.platform = :ios, '13.0'
14
+ s.swift_version = '5.4'
14
15
  s.source = { git: 'https://github.com/expo/expo.git' }
15
16
  s.static_framework = true
16
17
 
17
18
  s.dependency 'ExpoModulesCore'
19
+
20
+ # Swift/Objective-C compatibility
21
+ s.pod_target_xcconfig = {
22
+ 'DEFINES_MODULE' => 'YES',
23
+ 'SWIFT_COMPILATION_MODE' => 'wholemodule'
24
+ }
18
25
 
19
26
  if !$ExpoUseSources&.include?(package['name']) && ENV['EXPO_USE_SOURCE'].to_i == 0 && File.exist?("#{s.name}.xcframework") && Gem::Version.new(Pod::VERSION) >= Gem::Version.new('1.10.0')
20
- s.source_files = "#{s.name}/**/*.h"
27
+ s.source_files = "**/*.h"
21
28
  s.vendored_frameworks = "#{s.name}.xcframework"
22
29
  else
23
- s.source_files = "#{s.name}/**/*.{h,m}"
30
+ s.source_files = "**/*.{h,m,swift}"
24
31
  end
25
32
  end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-document-picker",
3
- "version": "11.2.2",
3
+ "version": "11.4.0",
4
4
  "description": "Provides access to the system's UI for selecting documents from the available providers on the user's device.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -40,5 +40,5 @@
40
40
  "peerDependencies": {
41
41
  "expo": "*"
42
42
  },
43
- "gitHead": "449f9edf2282fa984b6f694ce63867dffc65b580"
43
+ "gitHead": "4ba50c428c8369bb6b3a51a860d4898ad4ccbe78"
44
44
  }
@@ -1,6 +1,6 @@
1
1
  import { Platform } from 'expo-modules-core';
2
2
 
3
- import { DocumentPickerOptions, DocumentResult } from './types';
3
+ import { DocumentPickerOptions, DocumentPickerResult } from './types';
4
4
 
5
5
  export default {
6
6
  get name(): string {
@@ -10,10 +10,10 @@ export default {
10
10
  async getDocumentAsync({
11
11
  type = '*/*',
12
12
  multiple = false,
13
- }: DocumentPickerOptions): Promise<DocumentResult> {
13
+ }: DocumentPickerOptions): Promise<DocumentPickerResult> {
14
14
  // SSR guard
15
15
  if (!Platform.isDOMAvailable) {
16
- return { type: 'cancel' };
16
+ return { canceled: true, assets: null };
17
17
  }
18
18
 
19
19
  const input = document.createElement('input');
@@ -39,9 +39,11 @@ export default {
39
39
  reader.onload = ({ target }) => {
40
40
  const uri = (target as any).result;
41
41
  resolve({
42
+ canceled: false,
42
43
  type: 'success',
43
44
  uri,
44
45
  mimeType,
46
+ assets: [],
45
47
  name: targetFile.name,
46
48
  file: targetFile,
47
49
  lastModified: targetFile.lastModified,
@@ -52,7 +54,7 @@ export default {
52
54
  // Read in the image file as a binary string.
53
55
  reader.readAsDataURL(targetFile);
54
56
  } else {
55
- resolve({ type: 'cancel' });
57
+ resolve({ canceled: true, assets: null });
56
58
  }
57
59
 
58
60
  document.body.removeChild(input);
package/src/index.ts CHANGED
@@ -1,6 +1,39 @@
1
1
  import ExpoDocumentPicker from './ExpoDocumentPicker';
2
- import { DocumentPickerOptions, DocumentResult } from './types';
3
- export { DocumentPickerOptions, DocumentResult };
2
+ import { DocumentPickerOptions, DocumentPickerResult } from './types';
3
+
4
+ const DEPRECATED_RESULT_KEYS = [
5
+ 'name',
6
+ 'size',
7
+ 'uri',
8
+ 'mimeType',
9
+ 'lastModified',
10
+ 'file',
11
+ 'output',
12
+ ];
13
+
14
+ function mergeDeprecatedResult(result: DocumentPickerResult): DocumentPickerResult {
15
+ const firstAsset = result.assets?.[0];
16
+ const deprecatedResult = {
17
+ ...result,
18
+ get type() {
19
+ console.warn(
20
+ 'Key "type" in the document picker result is deprecated and will be removed in SDK 49, use "canceled" instead'
21
+ );
22
+ return this.canceled ? 'cancel' : 'success';
23
+ },
24
+ };
25
+ for (const key of DEPRECATED_RESULT_KEYS) {
26
+ Object.defineProperty(deprecatedResult, key, {
27
+ get() {
28
+ console.warn(
29
+ `Key "${key}" in the document picker result is deprecated and will be removed in SDK 49, you can access selected assets through the "assets" array instead`
30
+ );
31
+ return firstAsset?.[key];
32
+ },
33
+ });
34
+ }
35
+ return deprecatedResult;
36
+ }
4
37
 
5
38
  // @needsAudit
6
39
  /**
@@ -18,9 +51,16 @@ export async function getDocumentAsync({
18
51
  type = '*/*',
19
52
  copyToCacheDirectory = true,
20
53
  multiple = false,
21
- }: DocumentPickerOptions = {}): Promise<DocumentResult> {
54
+ }: DocumentPickerOptions = {}): Promise<DocumentPickerResult> {
22
55
  if (typeof type === 'string') {
23
56
  type = [type] as string[];
24
57
  }
25
- return await ExpoDocumentPicker.getDocumentAsync({ type, copyToCacheDirectory, multiple });
58
+ const result = await ExpoDocumentPicker.getDocumentAsync({
59
+ type,
60
+ copyToCacheDirectory,
61
+ multiple,
62
+ });
63
+ return mergeDeprecatedResult(result);
26
64
  }
65
+
66
+ export * from './types';
package/src/types.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  export type DocumentPickerOptions = {
3
3
  /**
4
4
  * The [MIME type(s)](https://en.wikipedia.org/wiki/Media_type) of the documents that are available
5
- * to be picked. Is also supports wildcards like `'image/*'` to choose any image. To allow any type
5
+ * to be picked. It also supports wildcards like `'image/*'` to choose any image. To allow any type
6
6
  * of document you can use `'&ast;/*'`.
7
7
  * @default '&ast;/*'
8
8
  */
@@ -18,56 +18,100 @@ export type DocumentPickerOptions = {
18
18
  /**
19
19
  * Allows multiple files to be selected from the system UI.
20
20
  * @default false
21
- * @platform web
21
+ *
22
22
  */
23
23
  multiple?: boolean;
24
24
  };
25
25
 
26
+ export type DocumentPickerAsset = {
27
+ /**
28
+ * Document original name.
29
+ */
30
+ name: string;
31
+ /**
32
+ * Document size in bytes.
33
+ */
34
+ size?: number;
35
+ /**
36
+ * An URI to the local document file.
37
+ */
38
+ uri: string;
39
+ /**
40
+ * Document MIME type.
41
+ */
42
+ mimeType?: string;
43
+ /**
44
+ * Timestamp of last document modification.
45
+ */
46
+ lastModified?: number;
47
+ /**
48
+ * `File` object for the parity with web File API.
49
+ * @platform web
50
+ */
51
+ file?: File;
52
+ /**
53
+ * `FileList` object for the parity with web File API.
54
+ * @platform web
55
+ */
56
+ output?: FileList | null;
57
+ };
58
+
26
59
  // @needsAudit @docsMissing
60
+ export type DocumentPickerResult = {
61
+ /**
62
+ * Boolean flag which shows if request was canceled. If asset data have been returned this should
63
+ * always be `false`.
64
+ */
65
+ canceled: boolean;
66
+ type?: string;
67
+ /**
68
+ * Document original name.
69
+ */
70
+ name?: string;
71
+ /**
72
+ * Document size in bytes.
73
+ */
74
+ size?: number;
75
+ /**
76
+ * An array of picked assets or `null` when the request was canceled.
77
+ */
78
+ assets: DocumentPickerAsset[] | null;
79
+ /**
80
+ * An URI to the local document file.
81
+ */
82
+ uri?: string;
83
+ /**
84
+ * Document MIME type.
85
+ */
86
+ mimeType?: string;
87
+ /**
88
+ * Timestamp of last document modification.
89
+ */
90
+ lastModified?: number;
91
+ /**
92
+ * `File` object for the parity with web File API.
93
+ * @platform web
94
+ */
95
+ file?: File;
96
+ /**
97
+ * `FileList` object for the parity with web File API.
98
+ * @platform web
99
+ */
100
+ output?: FileList | null;
101
+ } & (DocumentPickerSuccessResult | DocumentPickerCanceledResult);
102
+
27
103
  /**
28
- * First object represents the result when the document pick has been cancelled.
29
- * The second one represents the successful document pick result.
104
+ * @hidden
30
105
  */
31
- export type DocumentResult =
32
- | {
33
- /**
34
- * Field indicating that the document pick has been cancelled.
35
- */
36
- type: 'cancel';
37
- }
38
- | {
39
- /**
40
- * Field indicating that the document pick has been successful.
41
- */
42
- type: 'success';
43
- /**
44
- * Document original name.
45
- */
46
- name: string;
47
- /**
48
- * Document size in bytes.
49
- */
50
- size?: number;
51
- /**
52
- * An URI to the local document file.
53
- */
54
- uri: string;
55
- /**
56
- * Document MIME type.
57
- */
58
- mimeType?: string;
59
- /**
60
- * Timestamp of last document modification.
61
- */
62
- lastModified?: number;
63
- /**
64
- * `File` object for the parity with web File API.
65
- * @platform web
66
- */
67
- file?: File;
68
- /**
69
- * `FileList` object for the parity with web File API.
70
- * @platform web
71
- */
72
- output?: FileList | null;
73
- };
106
+ export type DocumentPickerSuccessResult = {
107
+ canceled: false;
108
+ assets: DocumentPickerAsset[];
109
+ };
110
+
111
+ /**
112
+ * @hidden
113
+ */
114
+ export type DocumentPickerCanceledResult = {
115
+ canceled: true;
116
+ assets: null;
117
+ };
@@ -1,7 +0,0 @@
1
- // Copyright © 2018 650 Industries. All rights reserved.
2
-
3
- #import <ExpoModulesCore/EXExportedModule.h>
4
- #import <ExpoModulesCore/EXModuleRegistryConsumer.h>
5
-
6
- @interface EXDocumentPickerModule : EXExportedModule <EXModuleRegistryConsumer>
7
- @end