File Storage
Docs menu

Guides

Mobile widgets

Embed a full file manager or a UI part (picker, upload, media) in iOS and Android apps. Previews on the public widgets page are free; production mounts need Pro+ (same plan as white label).

Overview

Mobile apps host File Storage inside a WebView. You can either load our hosted shell (cdn.filestorage.io/widget.html) with query parameters, or load the CDN JS/CSS yourself and call FileStorageSDK.mountWidget(). Storage credentials never go in the app binary - they stay on your proxy or Managed backend, same as the web.

Widget modes

widgetUITypical use
fullComplete file manager (sidebar + browse)In-app files screen
pickerBrowse + select (read-oriented)Attach / insert a file
uploadUpload-focused chromeImport from camera roll / documents
mediaPhotos-first media libraryGallery / brand assets

Hosted widget URL

Fastest path for native apps - no local HTML bundle required:

widget.html query string
https://cdn.filestorage.io/widget.html\
  ?apiKey=fs_live_…\
  &widget=picker\
  &proxyBaseUrl=https://api.example.com/fm\
  &publicObjectBaseUrl=https://cdn.example.com

Use httpBaseUrl instead of proxyBaseUrl when you implement the HTTP contract. Register your app’s WebView origin / website in the dashboard so license checks succeed.

mountWidget() (JavaScript)

Custom WebView page
<!-- In a WebView HTML shell you control, or any mobile webview page -->
<link rel="stylesheet" href="https://cdn.filestorage.io/file-storage.css" />
<div id="fm" style="height:100dvh"></div>
<script src="https://cdn.filestorage.io/file-storage.min.js"></script>
<script>
  FileStorageSDK.mountWidget({
    el: '#fm',
    apiKey: 'fs_live_…',
    widget: 'picker', // 'full' | 'picker' | 'upload' | 'media'
    s3: {
      proxyBaseUrl: 'https://api.example.com/fm',
      publicObjectBaseUrl: 'https://cdn.example.com',
      headers: () => ({ Authorization: 'Bearer ' + sessionToken }),
    },
  })

  window.addEventListener('filestorage:pick', (e) => {
    // Bridge to native if needed
    console.log(e.detail)
  })
</script>

mountWidget is equivalent to mount({ …, widget }) with embedded chrome that fills the host - ideal for phone viewports. Same store options as web: s3, http, or store.

Native hosts

iOS (WKWebView)

FileStorageWidgetController.swift
import WebKit

final class FileStorageWidgetController: UIViewController, WKScriptMessageHandler {
  private var webView: WKWebView!

  override func viewDidLoad() {
    super.viewDidLoad()
    let config = WKWebViewConfiguration()
    config.userContentController.add(self, name: "filestorage")
    webView = WKWebView(frame: view.bounds, configuration: config)
    webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    view.addSubview(webView)

    var comps = URLComponents(string: "https://cdn.filestorage.io/widget.html")!
    comps.queryItems = [
      URLQueryItem(name: "apiKey", value: "fs_live_…"),
      URLQueryItem(name: "widget", value: "picker"),
      URLQueryItem(name: "proxyBaseUrl", value: "https://api.example.com/fm"),
      URLQueryItem(name: "publicObjectBaseUrl", value: "https://cdn.example.com"),
    ]
    webView.load(URLRequest(url: comps.url!))
  }

  func userContentController(_ userContentController: WKUserContentController,
                             didReceive message: WKScriptMessage) {
    guard message.name == "filestorage",
          let body = message.body as? String,
          let data = body.data(using: .utf8),
          let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
          let file = json["file"] as? [String: Any] else { return }
    // file["path"], file["name"], file["url"], file["mimeType"]
    print(file)
  }
}

Android (WebView)

FileStorageWidgetFragment.kt
class FileStorageWidgetFragment : Fragment() {
  override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
  ): View {
    val web = WebView(requireContext())
    web.settings.javaScriptEnabled = true
    web.settings.domStorageEnabled = true
    web.addJavascriptInterface(object {
      @JavascriptInterface
      fun postMessage(payload: String) {
        // Parse JSON: { type: "filestorage:pick", file: { path, name, url, mimeType } }
      }
    }, "FileStorageAndroid")

    val url = Uri.parse("https://cdn.filestorage.io/widget.html").buildUpon()
      .appendQueryParameter("apiKey", "fs_live_…")
      .appendQueryParameter("widget", "full")
      .appendQueryParameter("proxyBaseUrl", "https://api.example.com/fm")
      .appendQueryParameter("publicObjectBaseUrl", "https://cdn.example.com")
      .build()
    web.loadUrl(url.toString())
    return web
  }
}

React Native

FileStorageWidget.tsx
import { useRef } from 'react'
import { WebView } from 'react-native-webview'

const WIDGET =
  'https://cdn.filestorage.io/widget.html' +
  '?apiKey=fs_live_…' +
  '&widget=media' +
  '&proxyBaseUrl=https://api.example.com/fm' +
  '&publicObjectBaseUrl=https://cdn.example.com'

export function FileStorageWidget() {
  const ref = useRef(null)
  return (
    <WebView
      ref={ref}
      source={{ uri: WIDGET }}
      onMessage={(e) => {
        const msg = JSON.parse(e.nativeEvent.data)
        if (msg.type === 'filestorage:pick') {
          // msg.file.path / name / url / mimeType
        }
      }}
      javaScriptEnabled
      domStorageEnabled
      style={{ flex: 1 }}
    />
  )
}

Flutter

file_storage_widget.dart
import 'dart:convert';
import 'package:webview_flutter/webview_flutter.dart';

final uri = Uri.https('cdn.filestorage.io', '/widget.html', {
  'apiKey': 'fs_live_…',
  'widget': 'upload',
  'proxyBaseUrl': 'https://api.example.com/fm',
  'publicObjectBaseUrl': 'https://cdn.example.com',
});

final controller = WebViewController()
  ..setJavaScriptMode(JavaScriptMode.unrestricted)
  ..addJavaScriptChannel(
    'FileStorageAndroid', // also receives ReactNative-style posts via bridge in widget.html
    onMessageReceived: (JavaScriptMessage message) {
      final data = jsonDecode(message.message) as Map<String, dynamic>;
      if (data['type'] == 'filestorage:pick') {
        // data['file']
      }
    },
  )
  ..loadRequest(uri);

Pick events

When the user selects a file (picker flows / double-click insert), the SDK dispatches filestorage:pick on window. The hosted widget.html forwards that payload to:

  • webkit.messageHandlers.filestorage (iOS)
  • FileStorageAndroid.postMessage (Android / Flutter channel name)
  • ReactNativeWebView.postMessage (React Native)
Message shape
{
  "type": "filestorage:pick",
  "file": {
    "path": "/campaigns/hero.jpg",
    "name": "hero.jpg",
    "url": "https://cdn.example.com/campaigns/hero.jpg",
    "mimeType": "image/jpeg"
  }
}

License & pricing

  • Public gallery at /widgets - free to view.
  • Production mountWidget / hosted shell - Pro+ API key (from $29/mo). Free keys unlock the web file manager only.
  • White label (no “Powered by” badge) is included on the same Pro+ plans.
  • Same website registration rules as the web embed.
  • Do not ship long-lived cloud secrets inside the mobile binary.

Troubleshooting

SymptomFix
Blank WebView / license gatePro+ apiKey; register the WebView host under Dashboard → Websites. Free keys return MOBILE_WIDGET_LOCKED.
CORS / network errorsAllow the WebView origin on your S3 proxy or HTTP API.
No pick callbacksUse widget=picker (or handle pick in your shell); confirm the message handler name matches.
Uploads failProxy must accept multipart; pass auth via headers on the store config in a custom HTML shell.