PHPackages                             sghimire/mobile-file-access - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. sghimire/mobile-file-access

ActiveNativephp-plugin

sghimire/mobile-file-access
===========================

Generic native file save/open (Storage Access Framework / UIDocumentPicker) for NativePHP Mobile — pick any file into your app or save arbitrary bytes to a user-chosen location, with no storage permissions required.

1.0.0(today)01↑2900%MITPHPPHP ^8.2

Since Jul 27Pushed todayCompare

[ Source](https://github.com/SandipGhimire/NativePHP-MobileFileAccess)[ Packagist](https://packagist.org/packages/sghimire/mobile-file-access)[ RSS](/packages/sghimire-mobile-file-access/feed)WikiDiscussions master Synced today

READMEChangelogDependencies (3)Versions (2)Used By (0)

Mobile File Access
==================

[](#mobile-file-access)

Generic native "save file" / "open file" for [NativePHP Mobile](https://nativephp.com) apps, powered by the Storage Access Framework on Android and `UIDocumentPickerViewController` on iOS.

Not tied to any particular file type or app — use it to import/export a backup, save a generated PDF, let a user attach an arbitrary document, or read any file the user picks from their device or cloud storage (Files app / Google Drive / Dropbox, wherever the system document picker can reach).

Features
--------

[](#features)

- Save arbitrary bytes to a location the user chooses, with a suggested file name.
- Read any file the user picks, back as base64 — including from cloud-backed providers exposed through the system Files app.
- **No storage permissions required on either platform.** The document picker grants per-file access on the user's behalf; this plugin never requests `WRITE_EXTERNAL_STORAGE`/`READ_EXTERNAL_STORAGE` (Android) and declares no `Info.plist` usage description (iOS).
- Fluent, chainable builders in both PHP and JavaScript.
- `FileSaved` / `SaveCancelled` / `FilePicked` / `PickCancelled` Laravel events, or bind straight to Livewire with `#[OnNative]`.
- Works from PHP (Blade/Livewire) and from JavaScript (Vue, React, Inertia, or plain JS).

Requirements
------------

[](#requirements)

- PHP ^8.2
- [`nativephp/mobile`](https://nativephp.com) ^3.0
- iOS 14+ / Android API 23+
- `livewire/livewire` — only needed if you use the `#[OnNative]` attribute

Installation
------------

[](#installation)

```
composer require sghimire/mobile-file-access
```

Laravel's package auto-discovery registers `FileAccessServiceProvider` for you. Then register the plugin with NativePHP:

```
php artisan native:plugin:register
```

This wires up the plugin's `nativephp.json` manifest (bridge functions — no permissions to merge, there aren't any) into your native build. Rebuild/reinstall the native shell afterwards (`php artisan native:install` or `native:run`) so the bridge functions are picked up.

How It Works (Under the Hood)
-----------------------------

[](#how-it-works-under-the-hood)

Same two-phase pattern as every async call in this plugin family — a synchronous "start" acknowledgement, then the real result delivered later through two parallel channels.

1. **Request out.** `FileAccess::save(...)->save()` / `FileAccess::pick()->pick()` (PHP) and `FileAccess.save(...)` / `FileAccess.pick()` (JS) both reach the same bridge — JS via `fetch('/_native/api/call', { method: 'FileAccess.Save' | 'FileAccess.Pick', params })`, PHP via `nativephp_call(...)`. The bridge router opens the native picker: `ActivityResultContracts.CreateDocument`/`OpenDocument` on Android, `UIDocumentPickerViewController` on iOS.
2. **Immediate ack.** The call returns right away confirming the picker is open — not that the user has chosen anything yet.
3. **Result comes back later**, once, per call. On Android, the native side calls `NativeActionCoordinator.dispatchEvent(...)`; on iOS, `LaravelBridge.shared.send?(...)`. Both inject a script that fires a `native-event` `CustomEvent` on `document` (what the JS `On()`/`Off()` helpers listen for) *and* make a call back into Laravel that instantiates and dispatches the real event class (what `Event::listen()` / `#[OnNative]` pick up). The user backing out of the picker delivers `SaveCancelled`/`PickCancelled` the same way.

Because results are asynchronous and delivered to PHP and JS independently, always drive your UI from the events — never from the return value of `save()`/`pick()`.

---

PHP Usage
---------

[](#php-usage)

### The `FileAccess` facade

[](#the-fileaccess-facade)

```
use Sandip\FileAccess\Native\Facades\FileAccess;

// Save some bytes
FileAccess::save('report.pdf', $pdfBytes)
    ->mimeType('application/pdf')  // hint only — the file name's extension is what matters
    ->id('export-report')          // correlate this save with its events
    ->save();

// Open any file
FileAccess::pick()
    ->mimeTypes(['application/pdf'])  // restrict the picker, or omit for any file (default ['*/*'])
    ->id('import-report')
    ->pick();
```

`save()`/`pick()` on the builder return `bool` — `true` once the request reached the native bridge, `false` if it couldn't be started (e.g. running outside the native shell, or the builder was already started once).

If you never call `->save()`/`->pick()` explicitly, it fires automatically when the builder object is destructed — so `FileAccess::save('report.pdf', $bytes);` alone is enough to trigger it. Calling it yourself is recommended so you can check the return value.

#### `PendingFileSave` methods

[](#pendingfilesave-methods)

MethodDescription`mimeType(string $mimeType)`MIME type hint. Defaults to `application/octet-stream`.`id(string $id)`Custom correlation ID, delivered back on the event.`getId()`Read the current correlation ID, or `null`.`save()`Send the save request to the native bridge. Returns `bool`.#### `PendingFilePick` methods

[](#pendingfilepick-methods)

MethodDescription`mimeTypes(array $mimeTypes)`Restrict the picker to one or more MIME types. Throws `InvalidArgumentException` if empty. Defaults to `['*/*']`.`id(string $id)`Custom correlation ID, delivered back on the event.`getId()`Read the current correlation ID, or `null`.`pick()`Send the pick request to the native bridge. Returns `bool`.### Listening for results

[](#listening-for-results)

```
use Sandip\FileAccess\Native\Events\FileAccess\FileSaved;
use Sandip\FileAccess\Native\Events\FileAccess\SaveCancelled;
use Sandip\FileAccess\Native\Events\FileAccess\FilePicked;
use Sandip\FileAccess\Native\Events\FileAccess\PickCancelled;
use Illuminate\Support\Facades\Event;

Event::listen(function (FileSaved $event) {
    $event->fileName; // string — the name the file was saved as
    $event->size;     // int — bytes written
    $event->id;       // ?string
});

Event::listen(function (SaveCancelled $event) {
    $event->reason; // ?string — "user_cancelled", "write_failed", ...
    $event->id;     // ?string
});

Event::listen(function (FilePicked $event) {
    $event->fileName;      // string
    $event->mimeType;      // string
    $event->size;          // int — bytes
    $event->contentBase64; // string — base64-encoded file contents
    $event->id;            // ?string
});

Event::listen(function (PickCancelled $event) {
    $event->reason; // ?string — "user_cancelled", "read_failed", ...
    $event->id;     // ?string
});
```

### Livewire: `#[OnNative]`

[](#livewire-onnative)

```
use Livewire\Component;
use Sandip\FileAccess\Native\Attributes\OnNative;
use Sandip\FileAccess\Native\Events\FileAccess\FilePicked;
use Sandip\FileAccess\Native\Facades\FileAccess;

class DocumentImporter extends Component
{
    public function startImport(): void
    {
        FileAccess::pick()->id('import')->mimeTypes(['application/pdf'])->pick();
    }

    #[OnNative(FilePicked::class)]
    public function onFilePicked(string $fileName, string $mimeType, int $size, string $contentBase64, ?string $id): void
    {
        file_put_contents(storage_path("app/imports/{$fileName}"), base64_decode($contentBase64));
    }
}
```

---

JavaScript Usage
----------------

[](#javascript-usage)

### Importing

[](#importing)

This package doesn't publish a `#nativephp` import alias (that's reserved for NativePHP's first-party plugins). Import the file directly — either from the vendor path, or copy it into your own `resources/js/` and import it from there:

```
import { FileAccess, On, Off, Events } from '../../vendor/sghimire/mobile-file-access/resources/js/file-access.js';
```

Full TypeScript types are included in `file-access.d.ts` alongside it.

### Saving a file

[](#saving-a-file)

`FileAccess.save(fileName, content)` returns a thenable builder — `await` it directly, or chain builder methods first. `content` accepts a string (assumed already base64), a `Uint8Array`, or an `ArrayBuffer`.

```
import { FileAccess } from '../../vendor/sghimire/mobile-file-access/resources/js/file-access.js';

await FileAccess.save('backup.json', JSON.stringify(data));

// or fully configured, with binary content
await FileAccess.save('report.pdf', pdfBytes)
  .mimeType('application/pdf')
  .id('export-report');
```

### Picking a file

[](#picking-a-file)

```
import { FileAccess } from '../../vendor/sghimire/mobile-file-access/resources/js/file-access.js';

await FileAccess.pick();

// or restrict to specific types
await FileAccess.pick().mimeTypes(['application/pdf']).id('import-report');
```

### Listening for events

[](#listening-for-events)

```
import { FileAccess, On, Off, Events, fromBase64 } from '../../vendor/sghimire/mobile-file-access/resources/js/file-access.js';

function handleSaved(payload) {
  // payload: { fileName: string, size: number, id: string | null }
}

function handleSaveCancelled(payload) {
  // payload: { reason: string | null, message?: string, id: string | null }
}

function handlePicked(payload) {
  // payload: { fileName: string, mimeType: string, size: number, contentBase64: string, id: string | null }
  const bytes = fromBase64(payload.contentBase64);
}

function handlePickCancelled(payload) {
  // payload: { reason: string | null, message?: string, id: string | null }
}

On(Events.FileAccess.FileSaved, handleSaved);
On(Events.FileAccess.SaveCancelled, handleSaveCancelled);
On(Events.FileAccess.FilePicked, handlePicked);
On(Events.FileAccess.PickCancelled, handlePickCancelled);

// later, e.g. on component unmount
Off(Events.FileAccess.FileSaved, handleSaved);
Off(Events.FileAccess.FilePicked, handlePicked);
```

### Vue 3 example: encrypted backup export/import

[](#vue-3-example-encrypted-backup-exportimport)

```

import { ref, onMounted, onUnmounted } from 'vue';
import {
  FileAccess,
  On,
  Off,
  Events,
  fromBase64,
} from '../../vendor/sghimire/mobile-file-access/resources/js/file-access.js';

const status = ref('');

function handleSaved(payload) {
  status.value = `Saved ${payload.fileName} (${payload.size} bytes)`;
}

function handlePicked(payload) {
  const bytes = fromBase64(payload.contentBase64);
  status.value = `Picked ${payload.fileName}: ${bytes.length} bytes`;
}

onMounted(() => {
  On(Events.FileAccess.FileSaved, handleSaved);
  On(Events.FileAccess.FilePicked, handlePicked);
});

onUnmounted(() => {
  Off(Events.FileAccess.FileSaved, handleSaved);
  Off(Events.FileAccess.FilePicked, handlePicked);
});

async function exportBackup(bytes) {
  await FileAccess.save('backup.bin', bytes).id('backup-export');
}

async function importBackup() {
  await FileAccess.pick().id('backup-import');
}

  Import backup
  {{ status }}

```

### JS API reference

[](#js-api-reference)

ExportSignatureDescription`FileAccess.save(fileName, content)``(string, string | Uint8Array | ArrayBuffer) => PendingFileSave`Start building a save request.`.mimeType(mimeType)``(string) => this`MIME type hint. Defaults to `application/octet-stream`.`.id(id)``(string) => this`Custom correlation ID.`.getId()``() => string | null`Read the current correlation ID.`FileAccess.pick()``() => PendingFilePick`Start building a pick request.`.mimeTypes(types)``(string[]) => this`Restrict the picker to given MIME types. Throws if empty.`On(event, callback)``(string, (payload, eventName) => void) => void`Subscribe to a native event.`Off(event, callback)``(string, (payload, eventName) => void) => void`Unsubscribe.`toBase64(bytes)``(string | Uint8Array | ArrayBuffer) => string`Encode bytes to base64 (chunked, safe for large files).`fromBase64(base64)``(string) => Uint8Array`Decode a base64 string back to bytes.`Events.FileAccess.FileSaved``string`Event name constant.`Events.FileAccess.SaveCancelled``string`Event name constant.`Events.FileAccess.FilePicked``string`Event name constant.`Events.FileAccess.PickCancelled``string`Event name constant.`await`-ing (or `.then`-ing) a `PendingFileSave`/`PendingFilePick` sends the request to the native bridge exactly once — awaiting it twice is a no-op the second time.

---

Events reference
----------------

[](#events-reference)

### `FileSaved`

[](#filesaved)

PropertyTypeDescription`fileName``string`The name the file was saved as.`size``int` / `number`Bytes written.`id``?string` / `string | null`The correlation ID from `.id()`, if any.### `SaveCancelled`

[](#savecancelled)

PropertyTypeDescription`reason``?string` / `string | null``"user_cancelled"`, `"write_failed"`, or `"no_root_view_controller"` (iOS).`message``?string`Present alongside `write_failed`.`id``?string` / `string | null`The correlation ID from `.id()`, if any.### `FilePicked`

[](#filepicked)

PropertyTypeDescription`fileName``string`The picked file's display name.`mimeType``string`Best-effort MIME type from the content provider.`size``int` / `number`Bytes read.`contentBase64``string`The file's contents, base64-encoded.`id``?string` / `string | null`The correlation ID from `.id()`, if any.### `PickCancelled`

[](#pickcancelled)

PropertyTypeDescription`reason``?string` / `string | null``"user_cancelled"`, `"read_failed"`, or `"no_root_view_controller"` (iOS).`message``?string`Present alongside `read_failed`.`id``?string` / `string | null`The correlation ID from `.id()`, if any.- PHP classes: `Sandip\FileAccess\Native\Events\FileAccess\{FileSaved,SaveCancelled,FilePicked,PickCancelled}`
- JS event name constants: `Events.FileAccess.{FileSaved,SaveCancelled,FilePicked,PickCancelled}`

Why no permissions?
-------------------

[](#why-no-permissions)

Both platforms delegate file access entirely to a system-owned picker:

- **Android** uses `Intent.ACTION_CREATE_DOCUMENT` and `Intent.ACTION_OPEN_DOCUMENT` (the Storage Access Framework) via `ActivityResultContracts.CreateDocument`/`OpenDocument`. The system grants your app a scoped, per-URI permission for exactly the file the user chose — no `WRITE_EXTERNAL_STORAGE`/`READ_EXTERNAL_STORAGE` manifest entry is requested or needed, on any supported API level.
- **iOS** uses `UIDocumentPickerViewController`, which similarly hands back a security-scoped URL for just the chosen file/destination. No `Info.plist` usage-description key applies here (those are only for Camera/Photo Library/Microphone-style device resources).

If you need something this plugin doesn't cover — e.g. picking straight from the photo library — pair it with a plugin built for that (like `sghimire/mobile-scanner` for camera/QR, or NativePHP's own media picker), which will declare whatever permission that flow actually needs.

Platform notes
--------------

[](#platform-notes)

AndroidiOSMin OS versionAPI 2314.0PermissionnonenoneNative implementation`resources/android/FileAccessFunctions.kt` (Storage Access Framework via `ActivityResultContracts`)`resources/ios/FileAccessFunctions.swift` (`UIDocumentPickerViewController`)Both are configured automatically by `nativephp.json` — you don't need to edit native project files by hand.

Testing
-------

[](#testing)

```
composer install
composer test
```

Outside of a compiled native shell, `FileAccess::save(...)->save()` and `FileAccess::pick()->pick()` return `false` (there's no bridge to call) — this is expected and is exactly what the test suite asserts.

License
-------

[](#license)

MIT

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance100

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity45

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/1db5353ad841234381ab200ae49ad44daab8050979e84a49a0a2ae2db0d953b9?d=identicon)[SandipGhimire](/maintainers/SandipGhimire)

---

Top Contributors

[![SandipGhimire](https://avatars.githubusercontent.com/u/200928662?v=4)](https://github.com/SandipGhimire "SandipGhimire (1 commits)")

---

Tags

laravelfilesmobilenativephpfile accessstorage-access-frameworkdocument-picker

###  Code Quality

TestsPest

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/sghimire-mobile-file-access/health.svg)

```
[![Health](https://phpackages.com/badges/sghimire-mobile-file-access/health.svg)](https://phpackages.com/packages/sghimire-mobile-file-access)
```

###  Alternatives

[nativephp/mobile-starter

The skeleton application for NativePHP for Mobile.

202.5k](/packages/nativephp-mobile-starter)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
