Skip to content
colis

@colis/react

Pickup inside your own app.

The delivery page is one way to hand work over. If your clients already log into a portal of yours, the hooks put the same sending and pickup there: a file input, a code field that repairs typos, a download. The AWS SDK stays on your server.

point it at the transfer routes
import { ColisProvider } from '@colis/react'

export default function Providers({ children }) {
  return <ColisProvider baseUrl="/api/transfers">{children}</ColisProvider>
}

Not on npm yet: the packages are built from the repository until they are published.

01Sending

A file input, a code.

sendFile() takes a File straight off an input, keeping its name and type. Failures land in error rather than rejecting, because an event handler should not need a try/catch.

import { useSendTransfer } from '@colis/react'

function SendDeliverable() {
  const { sendFile, transfer, isPending, error } = useSendTransfer()

  return (
    <>
      <input
        type="file"
        disabled={isPending}
        onChange={(event) => event.target.files?.[0] && sendFile(event.target.files[0])}
      />
      {transfer && <p>Send your client this code: {transfer.code}</p>}
      {error && <p>{error.message}</p>}
    </>
  )
}

The hook posts to the transfer routes on your server, which hold the bucket credentials. The browser never sees a key, and your authorize function decides who may send.

transfer carries the code, the size and the expiry. Show the code grouped in fours; the receiving side accepts it with or without the spaces.

The library

02Pickup

Look it up, show it, then download.

load() fetches what a code holds without moving the bytes, so the client sees a filename and a size before anything is downloaded. loadBytes() brings the file across.

import { useReceiveTransfer, useSyncCodeInput } from '@colis/react'

function ClientPickup() {
  const input = useSyncCodeInput()
  const { load, loadBytes, transfer, notFound, isPending } = useReceiveTransfer()

  async function download() {
    const bytes = await loadBytes(input.code!)
    if (bytes) saveToDisk(new Blob([bytes]), transfer?.filename ?? 'file') // your helper
  }

  return (
    <>
      <input {...input.inputProps} placeholder="K7QP 2M4X" />
      <button onClick={() => load(input.code!)} disabled={!input.isComplete || isPending}>
        Find my delivery
      </button>
      {notFound && <p>Unknown or expired code.</p>}
      {transfer?.kind === 'file' && (
        <button onClick={download}>
          Download {transfer.filename} · {transfer.size} bytes
        </button>
      )}
    </>
  )
}

The receipt and the two answers are the delivery page’s own routes, not the hooks’: post to /:code/opened and /:code/verdict yourself, or send your clients to the page.

03The code input

What the client typed stays untouched.

useSyncCodeInput does the repair in the browser, before any request. Rewriting the field under the cursor is the one thing that makes these inputs miserable, so it never does.

What codes.normalize() looks up

Complete. Separators dropped, case folded, and O, I and L read as 0, 1 and 1, because Crockford base32 has no O, I or L to confuse them with.

Alphabet 0123456789ABCDEFGHJKMNPQRSTVWXYZ · 8 chars · 40 bits

value is verbatim. code is the canonical form to submit, null while what is typed cannot be one. isComplete is the moment to enable the button.

inputProps carries the keyboard and autofill hints a one-time code wants: autoComplete="one-time-code", capitals, no autocorrect, and a numeric keyboard when the alphabet is digits.

Pass the same shape your server configured, { length: 4, alphabet }, and both halves follow.

04Guarantees

A client hammering a button gets one answer.

Every call aborts the one before it, a late reply from a superseded call is dropped rather than published, and nothing is written after unmount.

Client hooks, App Router ready

Every export is a client hook and the build carries 'use client', so it drops straight into the Next.js App Router. React 18 or later.

Tokens and custom clients

Pass headers to the provider for a token, or client to bring your own, which is also how you drive it in tests with no network at all.

Status you can render

status is idle, pending, success or error, and notFound covers both an unknown and an expired code, the way the protocol does.

useSendTransfer()
send, sendFile, transfer, status, isPending, error, reset
useReceiveTransfer()
load, loadBytes, burn, transfer, data, notFound, status, isPending, error, reset
useSyncCodeInput()
value, setValue, code, isComplete, error, reset, inputProps
useTransferClient()
the underlying client, for anything the hooks do not cover

Send it. They sign off. You know.

Your storage, a delivery page under your name, a webhook at every step. Nothing hosted by someone else.