Upload widget

The upload widget sends files from the browser straight to CalmLens, shows each file's progress and moderation decision, and gives your app the asset ID. Point it at an upload preset and a token endpoint, then send the asset ID to your server with the record it belongs to. Widgets covers installing it, styling and translating.

Add the upload widget

The widget uploads through an upload preset, so files go straight from the browser to CalmLens and your API key stays on your server. Create a preset that requires a token and shows the decision to the uploader (see Upload presets), then add an endpoint that signs a token for each file:

const { token, expiresAt } = await client.uploadTokens.sign({
  uploadPresetId: "preset_4Lk8Qw2Xm9Rt5Vb3Nc7P",
  externalId: "post-42",
  expiresInSeconds: 600,
});

Point tokenUrl at that endpoint. The widget sends it a POST with the file's name, type and size as JSON and expects { token } back. To fetch tokens another way, pass token, a function that receives the file and returns a token.

import { type CalmLensUpload, CalmLensUploader } from "@calmlens/sdk/react";

export function PhotoUploader({ onUploaded }: { onUploaded: (upload: CalmLensUpload) => void }) {
  return (
    <CalmLensUploader
      uploadPresetId="preset_4Lk8Qw2Xm9Rt5Vb3Nc7P"
      tokenUrl="/api/upload-token"
      accept="image/*"
      maxFileSize={10 * 1024 * 1024}
      onUploaded={onUploaded}
    />
  );
}

For each file, the widget:

  • Refuses files that don't match accept or are larger than maxFileSize before sending anything. Your preset's own file types and size limit still apply.
  • Shows the bytes sent, then checks the upload until it has a decision, for up to 15 seconds by default (decisionTimeoutMs). An upload still pending after that is left for your server's webhook to settle.
  • Shows the decision: allowed, limited, age-restricted, blocked or purged. Errors such as 413 upload-preset/too-large get their own message, and a failed upload can be retried.
  • Calls onUploaded with { assetId, status, file } unless the file was blocked or purged. Vue emits uploaded, and the custom element dispatches a calmlens-uploaded event with the same detail.

Send the asset ID to your server with the record it belongs to, and publish from the status your server reads from CalmLens, as described in Save the asset ID with the post.

multiple accepts several files, maxFiles caps how many (10 by default) and concurrency sets how many upload at once (3 by default). capture opens the camera on phones. Without multiple, the widget shows the picked file in place of the drop zone, with a button to choose another.

React Native

@calmlens/sdk/react-native has the same uploader for React Native and Expo apps, with a pick button in place of the drop zone. Pass pick, a function that opens your app's picker and resolves with the picked files, or none when the person cancels. A File from expo-file-system uploads as it is, and so does anything with a name, type, size, uri and arrayBuffer(). With expo-image-picker:

import { type CalmLensUpload, CalmLensUploader, type PickFiles } from "@calmlens/sdk/react-native";
import { File } from "expo-file-system";
import * as ImagePicker from "expo-image-picker";

const pickPhotos: PickFiles = async ({ multiple }) => {
  const result = await ImagePicker.launchImageLibraryAsync({
    mediaTypes: ["images"],
    allowsMultipleSelection: multiple,
  });
  return result.canceled ? [] : result.assets.map((asset) => new File(asset.uri));
};

export function PhotoUploader({
  onUploaded,
}: {
  onUploaded: (upload: CalmLensUpload) => void;
}) {
  return (
    <CalmLensUploader
      uploadPresetId="preset_4Lk8Qw2Xm9Rt5Vb3Nc7P"
      tokenUrl="https://your-app.example/api/upload-token"
      accept="image/*"
      pick={pickPhotos}
      onUploaded={onUploaded}
    />
  );
}

A preview: files stay in your browser. Include “sensitive” or “blocked” in a file's name to see those decisions.

The other options work as on the web. To send your app's own sign-in header with each token request, pass token, a function that fetches a token, instead of tokenUrl. Each picked photo's preview comes from its uri, and its bytes are read when its upload starts. The pick buttons have the testID calmlens-choose for end-to-end tests. For your own screens, useCalmLensUpload runs the same upload queue; the Calmsnap example is built on it.

Let anyone upload

For a preset open to anyone with its ID, leave out tokenUrl. The preset's daily limit, file types and allowed sites are then what protect your usage; see Upload without a server.

import { type CalmLensUpload, CalmLensUploader } from "@calmlens/sdk/react";

export function PublicPhotoUploader({
  onUploaded,
}: {
  onUploaded: (upload: CalmLensUpload) => void;
}) {
  return (
    <CalmLensUploader
      uploadPresetId="preset_4Lk8Qw2Xm9Rt5Vb3Nc7P"
      accept="image/*"
      maxFileSize={10 * 1024 * 1024}
      onUploaded={onUploaded}
    />
  );
}

Upload when a form is submitted

To upload with the rest of a form, set autoUpload: false (the no-auto-upload attribute on <calmlens-uploader>). Picked files then wait in the selected stage until you call start() on the upload controller. Create the controller yourself with useCalmLensUpload (React, React Native and Vue) or createUploadController from your library's import, and pass it to the widget as controller (a property on the custom element). The widget then shows that controller's files and takes the upload options from it, so your form can read the picked files, call start() when it is submitted and clear() to reset. Uploads keep running if the widget unmounts, for example when a form moves to its next step.

Upload through your own API

When files must pass through your server, for example to attach them to a signed-in user's records, pass an upload function instead of an upload preset. It receives the file with an abort signal and an onProgress callback, and returns the asset ID and, if your API has one, the moderation status. createProgressFetch(onProgress) returns a fetch that reports the bytes sent. Add checkStatus so the widget can check a pending upload; without it, the widget shows the upload as pending. Upload with your server builds both endpoints.

import {
  type CalmLensUpload,
  CalmLensUploader,
  type CustomUploadContext,
  createProgressFetch,
} from "@calmlens/sdk/react";

// Your server stores the file with CalmLensServerClient and answers with the asset.
async function uploadPhoto(file: File, { signal, onProgress }: CustomUploadContext) {
  const send = createProgressFetch(onProgress);
  const response = await send("/api/photos", {
    method: "POST",
    headers: { "Content-Type": file.type, "X-File-Name": encodeURIComponent(file.name) },
    body: file,
    signal,
  });
  if (!response.ok) throw new Error(`Upload failed (${response.status})`);
  const { assetId, status } = await response.json();
  return { assetId, status };
}

async function checkPhoto(assetId: string, { signal }: { signal: AbortSignal }) {
  const response = await fetch(`/api/photos/${assetId}`, { signal });
  return (await response.json()).status;
}

export function OwnApiUploader({ onUploaded }: { onUploaded: (upload: CalmLensUpload) => void }) {
  return (
    <CalmLensUploader
      upload={uploadPhoto}
      checkStatus={checkPhoto}
      accept="image/*"
      onUploaded={onUploaded}
    />
  );
}

Throw an error to show the file as failed. A CalmLensError shows the message for its code, so your API's error codes can have their own messages.

Signing in

Signing out

Cookies on CalmLens

We use essential cookies for sign-in and security. With your permission, functional cookies remember your preferences and Google Analytics measures website use and signups. Optional ad measurement cookies link Google ad clicks to signups. Personalised advertising stays off. Manage cookies