Skip to content

Commit d664505

Browse files
committed
feat(expo): add pickFiles wrapper over expo-document-picker
1 parent 9ddc528 commit d664505

5 files changed

Lines changed: 107 additions & 0 deletions

File tree

packages/expo/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"expo": ">=52.0.0",
6262
"expo-constants": ">=16.0.0",
6363
"expo-device": ">=6.0.0",
64+
"expo-document-picker": "*",
6465
"react": ">=18.3.0",
6566
"react-native": ">=0.74.0",
6667
"react-native-gesture-handler": ">=2.16.0",
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { describe, expect, mock, test } from "bun:test"
2+
3+
mock.module("expo-document-picker", () => ({
4+
getDocumentAsync: async () => ({ canceled: true }),
5+
}))
6+
7+
describe("pickFiles", () => {
8+
test("returns empty array when canceled", async () => {
9+
const { pickFiles } = await import("./file-picker")
10+
const out = await pickFiles({ multiple: true })
11+
expect(out).toEqual([])
12+
})
13+
})
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { describe, expect, mock, test } from "bun:test"
2+
3+
mock.module("expo-document-picker", () => ({
4+
getDocumentAsync: async () => ({
5+
canceled: false,
6+
assets: [
7+
{ uri: "file:///tmp/a.png", name: "a.png", mimeType: "image/png", size: 100 },
8+
{ uri: "file:///tmp/b.pdf", name: "b.pdf", mimeType: "application/pdf", size: 200 },
9+
],
10+
}),
11+
}))
12+
13+
describe("pickFiles", () => {
14+
test("returns Attachment[] from a successful pick", async () => {
15+
const { pickFiles } = await import("./file-picker")
16+
const out = await pickFiles({ multiple: true })
17+
expect(out).toHaveLength(2)
18+
expect(out[0]?.filename).toBe("a.png")
19+
expect(out[0]?.mime).toBe("image/png")
20+
expect(out[0]?.size).toBe(100)
21+
expect(out[0]?.isImage).toBe(true)
22+
})
23+
})
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type { Attachment } from "@reprojs/sdk-utils"
2+
import type { DocumentPickerResult } from "expo-document-picker"
3+
4+
type GetDocumentAsync = (opts?: {
5+
multiple?: boolean
6+
copyToCacheDirectory?: boolean
7+
}) => Promise<DocumentPickerResult>
8+
9+
/**
10+
* Wraps expo-document-picker.getDocumentAsync. Returns an empty array on
11+
* cancel or when the picker module is unavailable. Each asset is converted
12+
* to an Attachment — the blob field is a placeholder Blob; the intake-client
13+
* uses the previewUrl (file:// uri) at submit time so we don't read every
14+
* file into memory the moment it's picked.
15+
*/
16+
export async function pickFiles({ multiple = true }: { multiple?: boolean } = {}): Promise<
17+
Attachment[]
18+
> {
19+
let getDocumentAsync: GetDocumentAsync | undefined
20+
try {
21+
const mod = await import("expo-document-picker")
22+
getDocumentAsync = mod.getDocumentAsync
23+
} catch {
24+
return []
25+
}
26+
if (!getDocumentAsync) return []
27+
28+
const result = await getDocumentAsync({
29+
multiple,
30+
copyToCacheDirectory: true,
31+
})
32+
33+
if (result.canceled || !result.assets) return []
34+
35+
return result.assets.map((asset, i) => {
36+
const mime = asset.mimeType ?? "application/octet-stream"
37+
return {
38+
id: `picker-${Date.now()}-${i}`,
39+
blob: new Blob([], { type: mime }), // Placeholder — intake-client uses uri at submit time.
40+
filename: asset.name,
41+
mime,
42+
size: asset.size ?? 0,
43+
isImage: mime.startsWith("image/"),
44+
previewUrl: asset.uri,
45+
} satisfies Attachment
46+
})
47+
}

packages/expo/src/vendor.d.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Ambient stub for optional peer dependencies that are not installed in the
3+
* workspace but must be importable at runtime in the host app.
4+
*/
5+
6+
declare module "expo-document-picker" {
7+
export interface DocumentPickerAsset {
8+
uri: string
9+
name: string
10+
mimeType?: string
11+
size?: number
12+
}
13+
14+
export type DocumentPickerResult =
15+
| { canceled: true; assets?: never }
16+
| { canceled: false; assets: DocumentPickerAsset[] }
17+
18+
export function getDocumentAsync(options?: {
19+
multiple?: boolean
20+
copyToCacheDirectory?: boolean
21+
type?: string | string[]
22+
}): Promise<DocumentPickerResult>
23+
}

0 commit comments

Comments
 (0)